| /* |
| * Copyright 2000-2012 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 org.jetbrains.jps.uiDesigner.compiler; |
| |
| import com.intellij.compiler.instrumentation.InstrumentationClassFinder; |
| import com.intellij.compiler.instrumentation.InstrumenterClassWriter; |
| import com.intellij.openapi.application.PathManager; |
| import com.intellij.openapi.util.io.FileUtil; |
| import com.intellij.uiDesigner.compiler.*; |
| import com.intellij.uiDesigner.compiler.Utils; |
| import com.intellij.uiDesigner.core.GridConstraints; |
| import com.intellij.uiDesigner.lw.CompiledClassPropertiesProvider; |
| import com.intellij.uiDesigner.lw.LwRootContainer; |
| import gnu.trove.THashMap; |
| import gnu.trove.THashSet; |
| import org.jetbrains.annotations.Nullable; |
| import org.jetbrains.jps.ModuleChunk; |
| import org.jetbrains.jps.ProjectPaths; |
| import org.jetbrains.jps.builders.DirtyFilesHolder; |
| import org.jetbrains.jps.builders.java.JavaBuilderUtil; |
| import org.jetbrains.jps.builders.java.JavaSourceRootDescriptor; |
| import org.jetbrains.jps.builders.logging.ProjectBuilderLogger; |
| import org.jetbrains.jps.incremental.*; |
| import org.jetbrains.jps.incremental.instrumentation.ClassProcessingBuilder; |
| import org.jetbrains.jps.incremental.messages.BuildMessage; |
| import org.jetbrains.jps.incremental.messages.CompilerMessage; |
| import org.jetbrains.jps.incremental.messages.ProgressMessage; |
| import org.jetbrains.jps.incremental.storage.OneToManyPathsMapping; |
| import org.jetbrains.jps.model.JpsProject; |
| import org.jetbrains.jps.uiDesigner.model.JpsUiDesignerConfiguration; |
| import org.jetbrains.jps.uiDesigner.model.JpsUiDesignerExtensionService; |
| import org.jetbrains.org.objectweb.asm.ClassReader; |
| |
| import java.io.*; |
| import java.util.*; |
| |
| /** |
| * @author Eugene Zhuravlev |
| * Date: 11/20/12 |
| */ |
| public class FormsInstrumenter extends FormsBuilder { |
| public static final String BUILDER_NAME = "forms"; |
| |
| public FormsInstrumenter() { |
| super(BuilderCategory.CLASS_INSTRUMENTER, BUILDER_NAME); |
| } |
| |
| @Override |
| public ExitCode build(CompileContext context, ModuleChunk chunk, DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder, OutputConsumer outputConsumer) throws ProjectBuildException, IOException { |
| final JpsProject project = context.getProjectDescriptor().getProject(); |
| final JpsUiDesignerConfiguration config = JpsUiDesignerExtensionService.getInstance().getOrCreateUiDesignerConfiguration(project); |
| if (!config.isInstrumentClasses()) { |
| return ExitCode.NOTHING_DONE; |
| } |
| |
| final Map<File, Collection<File>> srcToForms = FORMS_TO_COMPILE.get(context); |
| FORMS_TO_COMPILE.set(context, null); |
| |
| if (srcToForms == null || srcToForms.isEmpty()) { |
| return ExitCode.NOTHING_DONE; |
| } |
| |
| final Set<File> formsToCompile = new THashSet<File>(FileUtil.FILE_HASHING_STRATEGY); |
| for (Collection<File> files : srcToForms.values()) { |
| formsToCompile.addAll(files); |
| } |
| |
| if (JavaBuilderUtil.isCompileJavaIncrementally(context)) { |
| final ProjectBuilderLogger logger = context.getLoggingManager().getProjectBuilderLogger(); |
| if (logger.isEnabled()) { |
| logger.logCompiledFiles(formsToCompile, getPresentableName(), "Compiling forms:"); |
| } |
| } |
| |
| try { |
| final Collection<File> platformCp = ProjectPaths.getPlatformCompilationClasspath(chunk, false); |
| |
| final List<File> classpath = new ArrayList<File>(); |
| classpath.addAll(ProjectPaths.getCompilationClasspath(chunk, false)); |
| classpath.add(getResourcePath(GridConstraints.class)); // forms_rt.jar |
| final Map<File, String> chunkSourcePath = ProjectPaths.getSourceRootsWithDependents(chunk); |
| classpath.addAll(chunkSourcePath.keySet()); // sourcepath for loading forms resources |
| |
| final InstrumentationClassFinder finder = ClassProcessingBuilder.createInstrumentationClassFinder(platformCp, classpath, outputConsumer); |
| |
| try { |
| final Map<File, Collection<File>> processed = instrumentForms(context, chunk, chunkSourcePath, finder, formsToCompile, outputConsumer); |
| |
| final OneToManyPathsMapping sourceToFormMap = context.getProjectDescriptor().dataManager.getSourceToFormMap(); |
| |
| for (Map.Entry<File, Collection<File>> entry : processed.entrySet()) { |
| final File src = entry.getKey(); |
| final Collection<File> forms = entry.getValue(); |
| |
| final Collection<String> formPaths = new ArrayList<String>(forms.size()); |
| for (File form : forms) { |
| formPaths.add(form.getPath()); |
| } |
| sourceToFormMap.update(src.getPath(), formPaths); |
| srcToForms.remove(src); |
| } |
| // clean mapping |
| for (File srcFile : srcToForms.keySet()) { |
| sourceToFormMap.remove(srcFile.getPath()); |
| } |
| } |
| finally { |
| finder.releaseResources(); |
| } |
| } |
| finally { |
| context.processMessage(new ProgressMessage("Finished instrumenting forms [" + chunk.getPresentableShortName() + "]")); |
| } |
| |
| return ExitCode.OK; |
| } |
| |
| @Override |
| public List<String> getCompilableFileExtensions() { |
| return Collections.emptyList(); |
| } |
| |
| private Map<File, Collection<File>> instrumentForms( |
| CompileContext context, ModuleChunk chunk, final Map<File, String> chunkSourcePath, final InstrumentationClassFinder finder, Collection<File> forms, OutputConsumer outConsumer |
| ) throws ProjectBuildException { |
| |
| final Map<File, Collection<File>> instrumented = new THashMap<File, Collection<File>>(FileUtil.FILE_HASHING_STRATEGY); |
| final Map<String, File> class2form = new HashMap<String, File>(); |
| |
| final MyNestedFormLoader nestedFormsLoader = |
| new MyNestedFormLoader(chunkSourcePath, ProjectPaths.getOutputPathsWithDependents(chunk)); |
| |
| for (File formFile : forms) { |
| final LwRootContainer rootContainer; |
| try { |
| rootContainer = Utils.getRootContainer( |
| formFile.toURI().toURL(), new CompiledClassPropertiesProvider( finder.getLoader()) |
| ); |
| } |
| catch (AlienFormFileException e) { |
| // ignore non-IDEA forms |
| continue; |
| } |
| catch (UnexpectedFormElementException e) { |
| context.processMessage(new CompilerMessage(getPresentableName(), BuildMessage.Kind.ERROR, e.getMessage(), formFile.getPath())); |
| LOG.info(e); |
| continue; |
| } |
| catch (UIDesignerException e) { |
| context.processMessage(new CompilerMessage(getPresentableName(), BuildMessage.Kind.ERROR, e.getMessage(), formFile.getPath())); |
| LOG.info(e); |
| continue; |
| } |
| catch (Exception e) { |
| throw new ProjectBuildException("Cannot process form file " + formFile.getAbsolutePath(), e); |
| } |
| |
| final String classToBind = rootContainer.getClassToBind(); |
| if (classToBind == null) { |
| continue; |
| } |
| |
| final CompiledClass compiled = findClassFile(outConsumer, classToBind); |
| if (compiled == null) { |
| context.processMessage(new CompilerMessage( |
| getPresentableName(), BuildMessage.Kind.WARNING, "Class to bind does not exist: " + classToBind, formFile.getAbsolutePath()) |
| ); |
| continue; |
| } |
| |
| final File alreadyProcessedForm = class2form.get(classToBind); |
| if (alreadyProcessedForm != null) { |
| context.processMessage( |
| new CompilerMessage( |
| getPresentableName(), BuildMessage.Kind.WARNING, |
| formFile.getAbsolutePath() + ": The form is bound to the class " + classToBind + ".\nAnother form " + alreadyProcessedForm.getAbsolutePath() + " is also bound to this class", |
| formFile.getAbsolutePath()) |
| ); |
| continue; |
| } |
| |
| class2form.put(classToBind, formFile); |
| addBinding(compiled.getSourceFile(), formFile, instrumented); |
| |
| try { |
| context.processMessage(new ProgressMessage("Instrumenting forms... [" + chunk.getPresentableShortName() + "]")); |
| |
| final BinaryContent originalContent = compiled.getContent(); |
| final ClassReader classReader = |
| new ClassReader(originalContent.getBuffer(), originalContent.getOffset(), originalContent.getLength()); |
| |
| final int version = ClassProcessingBuilder.getClassFileVersion(classReader); |
| final InstrumenterClassWriter classWriter = new InstrumenterClassWriter(ClassProcessingBuilder.getAsmClassWriterFlags(version), finder); |
| final AsmCodeGenerator codeGenerator = new AsmCodeGenerator(rootContainer, finder, nestedFormsLoader, false, classWriter); |
| final byte[] patchedBytes = codeGenerator.patchClass(classReader); |
| if (patchedBytes != null) { |
| compiled.setContent(new BinaryContent(patchedBytes)); |
| } |
| |
| final FormErrorInfo[] warnings = codeGenerator.getWarnings(); |
| for (final FormErrorInfo warning : warnings) { |
| context.processMessage( |
| new CompilerMessage(getPresentableName(), BuildMessage.Kind.WARNING, warning.getErrorMessage(), formFile.getAbsolutePath()) |
| ); |
| } |
| |
| final FormErrorInfo[] errors = codeGenerator.getErrors(); |
| if (errors.length > 0) { |
| StringBuilder message = new StringBuilder(); |
| for (final FormErrorInfo error : errors) { |
| if (message.length() > 0) { |
| message.append("\n"); |
| } |
| message.append(formFile.getAbsolutePath()).append(": ").append(error.getErrorMessage()); |
| } |
| context.processMessage(new CompilerMessage(getPresentableName(), BuildMessage.Kind.ERROR, message.toString())); |
| } |
| } |
| catch (Exception e) { |
| context.processMessage(new CompilerMessage(getPresentableName(), BuildMessage.Kind.ERROR, "Forms instrumentation failed" + e.getMessage(), formFile.getAbsolutePath())); |
| } |
| } |
| return instrumented; |
| } |
| |
| |
| private static CompiledClass findClassFile(OutputConsumer outputConsumer, String classToBind) { |
| final Map<String, CompiledClass> compiled = outputConsumer.getCompiledClasses(); |
| while (true) { |
| final CompiledClass fo = compiled.get(classToBind); |
| if (fo != null) { |
| return fo; |
| } |
| final int dotIndex = classToBind.lastIndexOf('.'); |
| if (dotIndex <= 0) { |
| return null; |
| } |
| classToBind = classToBind.substring(0, dotIndex) + "$" + classToBind.substring(dotIndex + 1); |
| } |
| } |
| |
| private static File getResourcePath(Class aClass) { |
| return new File(PathManager.getResourceRoot(aClass, "/" + aClass.getName().replace('.', '/') + ".class")); |
| } |
| |
| private static class MyNestedFormLoader implements NestedFormLoader { |
| private final Map<File, String> mySourceRoots; |
| private final Collection<File> myOutputRoots; |
| private final HashMap<String, LwRootContainer> myCache = new HashMap<String, LwRootContainer>(); |
| |
| /** |
| * @param sourceRoots all source roots for current module chunk and all dependent recursively |
| * @param outputRoots output roots for this module chunk and all dependent recursively |
| */ |
| public MyNestedFormLoader(Map<File, String> sourceRoots, Collection<File> outputRoots) { |
| mySourceRoots = sourceRoots; |
| myOutputRoots = outputRoots; |
| } |
| |
| public LwRootContainer loadForm(String formFileName) throws Exception { |
| if (myCache.containsKey(formFileName)) { |
| return myCache.get(formFileName); |
| } |
| |
| final String relPath = FileUtil.toSystemIndependentName(formFileName); |
| |
| for (Map.Entry<File, String> entry : mySourceRoots.entrySet()) { |
| final File sourceRoot = entry.getKey(); |
| final String prefix = entry.getValue(); |
| String path = relPath; |
| if (prefix != null && FileUtil.startsWith(path, prefix)) { |
| path = path.substring(prefix.length()); |
| } |
| final File formFile = new File(sourceRoot, path); |
| if (formFile.exists()) { |
| final BufferedInputStream stream = new BufferedInputStream(new FileInputStream(formFile)); |
| try { |
| return loadForm(formFileName, stream); |
| } |
| finally { |
| stream.close(); |
| } |
| } |
| } |
| |
| throw new Exception("Cannot find nested form file " + formFileName); |
| } |
| |
| private LwRootContainer loadForm(String formFileName, InputStream resourceStream) throws Exception { |
| final LwRootContainer container = Utils.getRootContainer(resourceStream, null); |
| myCache.put(formFileName, container); |
| return container; |
| } |
| |
| public String getClassToBindName(LwRootContainer container) { |
| final String className = container.getClassToBind(); |
| for (File outputRoot : myOutputRoots) { |
| final String result = getJVMClassName(outputRoot, className.replace('.', '/')); |
| if (result != null) { |
| return result.replace('/', '.'); |
| } |
| } |
| return className; |
| } |
| } |
| |
| @Nullable |
| private static String getJVMClassName(File outputRoot, String className) { |
| while (true) { |
| final File candidateClass = new File(outputRoot, className + ".class"); |
| if (candidateClass.exists()) { |
| return className; |
| } |
| final int position = className.lastIndexOf('/'); |
| if (position < 0) { |
| return null; |
| } |
| className = className.substring(0, position) + '$' + className.substring(position + 1); |
| } |
| } |
| |
| } |