| /* |
| * Copyright 2000-2009 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.intellij.ant; |
| |
| import com.intellij.compiler.instrumentation.InstrumentationClassFinder; |
| import com.intellij.compiler.instrumentation.InstrumenterClassWriter; |
| import com.intellij.compiler.notNullVerification.NotNullVerifyingInstrumenter; |
| import com.intellij.uiDesigner.compiler.*; |
| import com.intellij.uiDesigner.lw.CompiledClassPropertiesProvider; |
| import com.intellij.uiDesigner.lw.LwRootContainer; |
| import org.apache.tools.ant.BuildException; |
| import org.apache.tools.ant.Project; |
| import org.apache.tools.ant.taskdefs.Javac; |
| import org.apache.tools.ant.types.Path; |
| import org.jetbrains.org.objectweb.asm.ClassReader; |
| import org.jetbrains.org.objectweb.asm.ClassVisitor; |
| import org.jetbrains.org.objectweb.asm.ClassWriter; |
| import org.jetbrains.org.objectweb.asm.Opcodes; |
| |
| import java.io.*; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.util.*; |
| |
| public class Javac2 extends Javac { |
| public static final String PROPERTY_INSTRUMENTATION_INCLUDE_JAVA_RUNTIME = "javac2.instrumentation.includeJavaRuntime"; |
| private ArrayList myFormFiles; |
| private List myNestedFormPathList; |
| private boolean instrumentNotNull = true; |
| |
| public Javac2() { |
| } |
| |
| /** |
| * Check if Java classes should be actually compiled by the task. This method is overridden by |
| * {@link com.intellij.ant.InstrumentIdeaExtensions} task in order to suppress actual compilation |
| * of the java sources. |
| * |
| * @return true if the java classes are compiled, false if just instrumentation is performed. |
| */ |
| protected boolean areJavaClassesCompiled() { |
| return true; |
| } |
| |
| /** |
| * This method is called when option that supported only for the case when java sources are compiled |
| * and it is not supported for the case when only instrumentation is performed. |
| * |
| * @param optionName the option name to warn about. |
| */ |
| private void unsupportedOptionMessage(final String optionName) { |
| if (!areJavaClassesCompiled()) { |
| log("The option " + optionName + " is not supported by InstrumentIdeaExtensions task", Project.MSG_ERR); |
| } |
| } |
| |
| public boolean getInstrumentNotNull() { |
| return instrumentNotNull; |
| } |
| |
| public void setInstrumentNotNull(boolean instrumentNotNull) { |
| this.instrumentNotNull = instrumentNotNull; |
| } |
| |
| /** |
| * The overridden setter method that warns about unsupported option. |
| * |
| * @param v the option value |
| */ |
| public void setDebugLevel(String v) { |
| unsupportedOptionMessage("debugLevel"); |
| super.setDebugLevel(v); |
| } |
| |
| /** |
| * The overridden setter method that warns about unsupported option. |
| * |
| * @param list the option value |
| */ |
| public void setListfiles(boolean list) { |
| unsupportedOptionMessage("listFiles"); |
| super.setListfiles(list); |
| } |
| |
| /** |
| * The overridden setter method that warns about unsupported option. |
| * |
| * @param memoryInitialSize the option value |
| */ |
| public void setMemoryInitialSize(String memoryInitialSize) { |
| unsupportedOptionMessage("memoryInitialSize"); |
| super.setMemoryInitialSize(memoryInitialSize); |
| } |
| |
| /** |
| * The overridden setter method that warns about unsupported option. |
| * |
| * @param memoryMaximumSize the option value |
| */ |
| public void setMemoryMaximumSize(String memoryMaximumSize) { |
| unsupportedOptionMessage("memoryMaximumSize"); |
| super.setMemoryMaximumSize(memoryMaximumSize); |
| } |
| |
| /** |
| * The overridden setter method that warns about unsupported option. |
| * |
| * @param encoding the option value |
| */ |
| public void setEncoding(String encoding) { |
| unsupportedOptionMessage("encoding"); |
| super.setEncoding(encoding); |
| } |
| |
| /** |
| * The overridden setter method that warns about unsupported option. |
| * |
| * @param optimize the option value |
| */ |
| public void setOptimize(boolean optimize) { |
| unsupportedOptionMessage("optimize"); |
| super.setOptimize(optimize); |
| } |
| |
| /** |
| * The overridden setter method that warns about unsupported option. |
| * |
| * @param depend the option value |
| */ |
| public void setDepend(boolean depend) { |
| unsupportedOptionMessage("depend"); |
| super.setDepend(depend); |
| } |
| |
| /** |
| * The overridden setter method that warns about unsupported option. |
| * |
| * @param f the option value |
| */ |
| public void setFork(boolean f) { |
| unsupportedOptionMessage("fork"); |
| super.setFork(f); |
| } |
| |
| /** |
| * The overridden setter method that warns about unsupported option. |
| * |
| * @param forkExec the option value |
| */ |
| public void setExecutable(String forkExec) { |
| unsupportedOptionMessage("executable"); |
| super.setExecutable(forkExec); |
| } |
| |
| /** |
| * The overridden setter method that warns about unsupported option. |
| * |
| * @param compiler the option value |
| */ |
| public void setCompiler(String compiler) { |
| unsupportedOptionMessage("compiler"); |
| super.setCompiler(compiler); |
| } |
| |
| /** |
| * Sets the nested form directories that will be used during the |
| * compilation. |
| * @param nestedformdirs a list of {@link PrefixedPath} |
| */ |
| public void setNestedformdirs(List nestedformdirs) { |
| myNestedFormPathList = nestedformdirs; |
| } |
| |
| /** |
| * Gets the nested form directories that will be used during the |
| * compilation. |
| * @return the extension directories as a list of {@link PrefixedPath} |
| */ |
| public List getNestedformdirs() { |
| return myNestedFormPathList; |
| } |
| |
| /** |
| * Adds a path to nested form directories. |
| * @return a path to be configured |
| */ |
| public PrefixedPath createNestedformdirs() { |
| PrefixedPath p = new PrefixedPath(getProject()); |
| if (myNestedFormPathList == null) { |
| myNestedFormPathList = new ArrayList(); |
| } |
| myNestedFormPathList.add(p); |
| return p; |
| } |
| |
| |
| |
| /** |
| * The overridden compile method that does not actually compiles java sources but only instruments |
| * class files. |
| */ |
| protected void compile() { |
| // compile java |
| if (areJavaClassesCompiled()) { |
| super.compile(); |
| } |
| |
| InstrumentationClassFinder finder = buildClasspathClassLoader(); |
| if (finder == null) { |
| return; |
| } |
| try { |
| instrumentForms(finder); |
| |
| if (getInstrumentNotNull()) { |
| //NotNull instrumentation |
| final int instrumented = instrumentNotNull(getDestdir(), finder); |
| log("Added @NotNull assertions to " + instrumented + " files", Project.MSG_INFO); |
| } |
| |
| } |
| finally { |
| finder.releaseResources(); |
| } |
| } |
| |
| /** |
| * Instrument forms |
| * |
| * @param finder a classloader to use |
| */ |
| private void instrumentForms(final InstrumentationClassFinder finder) { |
| // we instrument every file, because we cannot find which files should not be instrumented without dependency storage |
| final ArrayList formsToInstrument = myFormFiles; |
| |
| if (formsToInstrument.size() == 0) { |
| log("No forms to instrument found", Project.MSG_VERBOSE); |
| return; |
| } |
| |
| final HashMap class2form = new HashMap(); |
| |
| for (int i = 0; i < formsToInstrument.size(); i++) { |
| final File formFile = (File)formsToInstrument.get(i); |
| |
| log("compiling form " + formFile.getAbsolutePath(), Project.MSG_VERBOSE); |
| final LwRootContainer rootContainer; |
| try { |
| rootContainer = Utils.getRootContainer(formFile.toURI().toURL(), new CompiledClassPropertiesProvider(finder.getLoader())); |
| } |
| catch (AlienFormFileException e) { |
| // ignore non-IDEA forms |
| continue; |
| } |
| catch (Exception e) { |
| fireError("Cannot process form file " + formFile.getAbsolutePath() + ". Reason: " + e); |
| continue; |
| } |
| |
| final String classToBind = rootContainer.getClassToBind(); |
| if (classToBind == null) { |
| continue; |
| } |
| |
| String name = classToBind.replace('.', '/'); |
| File classFile = getClassFile(name); |
| if (classFile == null) { |
| log(formFile.getAbsolutePath() + ": Class to bind does not exist: " + classToBind, Project.MSG_WARN); |
| continue; |
| } |
| |
| final File alreadyProcessedForm = (File)class2form.get(classToBind); |
| if (alreadyProcessedForm != null) { |
| fireError(formFile.getAbsolutePath() + |
| ": " + |
| "The form is bound to the class " + |
| classToBind + |
| ".\n" + |
| "Another form " + |
| alreadyProcessedForm.getAbsolutePath() + |
| " is also bound to this class."); |
| continue; |
| } |
| class2form.put(classToBind, formFile); |
| |
| try { |
| int version; |
| InputStream stream = new FileInputStream(classFile); |
| try { |
| version = getClassFileVersion(new ClassReader(stream)); |
| } |
| finally { |
| stream.close(); |
| } |
| AntNestedFormLoader formLoader = new AntNestedFormLoader(finder.getLoader(), myNestedFormPathList); |
| InstrumenterClassWriter classWriter = new InstrumenterClassWriter(getAsmClassWriterFlags(version), finder); |
| final AsmCodeGenerator codeGenerator = new AsmCodeGenerator(rootContainer, finder, formLoader, false, classWriter); |
| codeGenerator.patchFile(classFile); |
| final FormErrorInfo[] warnings = codeGenerator.getWarnings(); |
| |
| for (int j = 0; j < warnings.length; j++) { |
| log(formFile.getAbsolutePath() + ": " + warnings[j].getErrorMessage(), Project.MSG_WARN); |
| } |
| final FormErrorInfo[] errors = codeGenerator.getErrors(); |
| if (errors.length > 0) { |
| StringBuffer message = new StringBuffer(); |
| for (int j = 0; j < errors.length; j++) { |
| if (message.length() > 0) { |
| message.append("\n"); |
| } |
| message.append(formFile.getAbsolutePath()).append(": ").append(errors[j].getErrorMessage()); |
| } |
| fireError(message.toString()); |
| } |
| } |
| catch (Exception e) { |
| fireError("Forms instrumentation failed for " + formFile.getAbsolutePath() + ": " + e.toString()); |
| } |
| } |
| } |
| |
| /** |
| * @return the flags for class writer |
| */ |
| private static int getAsmClassWriterFlags(int version) { |
| return version >= Opcodes.V1_6 && version != Opcodes.V1_1 ? ClassWriter.COMPUTE_FRAMES : ClassWriter.COMPUTE_MAXS; |
| } |
| |
| /** |
| * Create class loader based on classpath, bootclasspath, and sourcepath. |
| * |
| * @return a URL classloader |
| */ |
| private InstrumentationClassFinder buildClasspathClassLoader() { |
| final StringBuffer classPathBuffer = new StringBuffer(); |
| final Project project = getProject(); |
| final Path cp = new Path(project); |
| appendPath(cp, getBootclasspath()); |
| cp.setLocation(getDestdir().getAbsoluteFile()); |
| appendPath(cp, getClasspath()); |
| appendPath(cp, getSourcepath()); |
| appendPath(cp, getSrcdir()); |
| if (getIncludeantruntime()) { |
| cp.addExisting(cp.concatSystemClasspath("last")); |
| } |
| boolean shouldInclude = getIncludejavaruntime(); |
| if (!shouldInclude) { |
| if (project != null) { |
| final String propValue = project.getProperty(PROPERTY_INSTRUMENTATION_INCLUDE_JAVA_RUNTIME); |
| shouldInclude = !("false".equalsIgnoreCase(propValue) || "no".equalsIgnoreCase(propValue)); |
| } |
| else { |
| shouldInclude = true; |
| } |
| } |
| if (shouldInclude) { |
| cp.addJavaRuntime(); |
| } |
| |
| cp.addExtdirs(getExtdirs()); |
| |
| final String[] pathElements = cp.list(); |
| for (int i = 0; i < pathElements.length; i++) { |
| final String pathElement = pathElements[i]; |
| classPathBuffer.append(File.pathSeparator); |
| classPathBuffer.append(pathElement); |
| } |
| |
| final String classPath = classPathBuffer.toString(); |
| log("classpath=" + classPath, Project.MSG_VERBOSE); |
| |
| try { |
| return createInstrumentationClassFinder(classPath); |
| } |
| catch (MalformedURLException e) { |
| fireError(e.getMessage()); |
| return null; |
| } |
| } |
| |
| /** |
| * Append path to class path if the appened path is not empty and is not null |
| * |
| * @param cp the path to modify |
| * @param p the path to append |
| */ |
| private void appendPath(Path cp, final Path p) { |
| if (p != null && p.size() > 0) { |
| cp.append(p); |
| } |
| } |
| |
| /** |
| * Instrument classes with NotNull annotations |
| * |
| * @param dir the directory with classes to instrument (the directory is processed recursively) |
| * @param finder the classloader to use |
| * @return the amount of classes actually affected by instrumentation |
| */ |
| private int instrumentNotNull(File dir, final InstrumentationClassFinder finder) { |
| int instrumented = 0; |
| final File[] files = dir.listFiles(); |
| for (int i = 0; i < files.length; i++) { |
| File file = files[i]; |
| final String name = file.getName(); |
| if (name.endsWith(".class")) { |
| final String path = file.getPath(); |
| log("Adding @NotNull assertions to " + path, Project.MSG_VERBOSE); |
| try { |
| final FileInputStream inputStream = new FileInputStream(file); |
| try { |
| ClassReader reader = new ClassReader(inputStream); |
| |
| int version = getClassFileVersion(reader); |
| |
| if (version >= Opcodes.V1_5) { |
| ClassWriter writer = new InstrumenterClassWriter(getAsmClassWriterFlags(version), finder); |
| |
| if (NotNullVerifyingInstrumenter.processClassFile(reader, writer)) { |
| final FileOutputStream fileOutputStream = new FileOutputStream(path); |
| try { |
| fileOutputStream.write(writer.toByteArray()); |
| instrumented++; |
| } |
| finally { |
| fileOutputStream.close(); |
| } |
| } |
| } |
| } |
| finally { |
| inputStream.close(); |
| } |
| } |
| catch (IOException e) { |
| log("Failed to instrument @NotNull assertion for " + path + ": " + e.getMessage(), Project.MSG_WARN); |
| } |
| catch (Exception e) { |
| fireError("@NotNull instrumentation failed for " + path + ": " + e.toString()); |
| } |
| } |
| else if (file.isDirectory()) { |
| instrumented += instrumentNotNull(file, finder); |
| } |
| } |
| |
| return instrumented; |
| } |
| |
| private static int getClassFileVersion(ClassReader reader) { |
| final int[] classfileVersion = new int[1]; |
| reader.accept(new ClassVisitor(Opcodes.ASM5) { |
| public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { |
| classfileVersion[0] = version; |
| } |
| }, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); |
| |
| return classfileVersion[0]; |
| } |
| |
| private void fireError(final String message) { |
| if (failOnError) { |
| throw new BuildException(message, getLocation()); |
| } |
| else { |
| log(message, Project.MSG_ERR); |
| } |
| } |
| |
| private File getClassFile(String className) { |
| final String classOrInnerName = getClassOrInnerName(className); |
| if (classOrInnerName == null) return null; |
| return new File(getDestdir().getAbsolutePath(), classOrInnerName + ".class"); |
| } |
| |
| private String getClassOrInnerName(String className) { |
| File classFile = new File(getDestdir().getAbsolutePath(), className + ".class"); |
| if (classFile.exists()) return className; |
| int position = className.lastIndexOf('/'); |
| if (position == -1) return null; |
| return getClassOrInnerName(className.substring(0, position) + '$' + className.substring(position + 1)); |
| } |
| |
| protected void resetFileLists() { |
| super.resetFileLists(); |
| myFormFiles = new ArrayList(); |
| } |
| |
| protected void scanDir(final File srcDir, final File destDir, final String[] files) { |
| super.scanDir(srcDir, destDir, files); |
| for (int i = 0; i < files.length; i++) { |
| final String file = files[i]; |
| if (file.endsWith(".form")) { |
| log("Found form file " + file, Project.MSG_VERBOSE); |
| myFormFiles.add(new File(srcDir, file)); |
| } |
| } |
| } |
| |
| private static InstrumentationClassFinder createInstrumentationClassFinder(final String classPath) throws MalformedURLException { |
| final ArrayList urls = new ArrayList(); |
| for (StringTokenizer tokenizer = new StringTokenizer(classPath, File.pathSeparator); tokenizer.hasMoreTokens();) { |
| final String s = tokenizer.nextToken(); |
| urls.add(new File(s).toURI().toURL()); |
| } |
| final URL[] urlsArr = (URL[])urls.toArray(new URL[urls.size()]); |
| return new InstrumentationClassFinder(urlsArr); |
| } |
| |
| private class AntNestedFormLoader implements NestedFormLoader { |
| private final ClassLoader myLoader; |
| private final List myNestedFormPathList; |
| private final HashMap myFormCache = new HashMap(); |
| |
| public AntNestedFormLoader(final ClassLoader loader, List nestedFormPathList) { |
| myLoader = loader; |
| myNestedFormPathList = nestedFormPathList; |
| } |
| |
| public LwRootContainer loadForm(String formFilePath) throws Exception { |
| if (myFormCache.containsKey(formFilePath)) { |
| return (LwRootContainer)myFormCache.get(formFilePath); |
| } |
| |
| String lowerFormFilePath = formFilePath.toLowerCase(); |
| log("Searching for form " + lowerFormFilePath, Project.MSG_VERBOSE); |
| for (Iterator iterator = myFormFiles.iterator(); iterator.hasNext();) { |
| File file = (File)iterator.next(); |
| String name = file.getAbsolutePath().replace(File.separatorChar, '/').toLowerCase(); |
| log("Comparing with " + name, Project.MSG_VERBOSE); |
| if (name.endsWith(lowerFormFilePath)) { |
| return loadForm(formFilePath, new FileInputStream(file)); |
| } |
| } |
| |
| if (myNestedFormPathList != null) { |
| for (int i = 0; i < myNestedFormPathList.size(); i++) { |
| PrefixedPath path = (PrefixedPath)myNestedFormPathList.get(i); |
| File formFile = path.findFile(formFilePath); |
| if (formFile != null) { |
| return loadForm(formFilePath, new FileInputStream(formFile)); |
| } |
| } |
| } |
| InputStream resourceStream = myLoader.getResourceAsStream(formFilePath); |
| if (resourceStream != null) { |
| return loadForm(formFilePath, resourceStream); |
| } |
| throw new Exception("Cannot find nested form file " + formFilePath); |
| } |
| |
| private LwRootContainer loadForm(String formFileName, InputStream resourceStream) throws Exception { |
| final LwRootContainer container = Utils.getRootContainer(resourceStream, null); |
| myFormCache.put(formFileName, container); |
| return container; |
| } |
| |
| public String getClassToBindName(LwRootContainer container) { |
| final String className = container.getClassToBind(); |
| String result = getClassOrInnerName(className.replace('.', '/')); |
| if (result != null) return result.replace('/', '.'); |
| return className; |
| } |
| } |
| } |