/* * 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; } }, 0); 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; } } }