jps: CompiledClass accepts multiple source files
[idea/community.git] / plugins / ui-designer / jps-plugin / src / org / jetbrains / jps / uiDesigner / compiler / FormsInstrumenter.java
1 /*
2  * Copyright 2000-2012 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package org.jetbrains.jps.uiDesigner.compiler;
17
18 import com.intellij.compiler.instrumentation.FailSafeClassReader;
19 import com.intellij.compiler.instrumentation.InstrumentationClassFinder;
20 import com.intellij.compiler.instrumentation.InstrumenterClassWriter;
21 import com.intellij.openapi.application.PathManager;
22 import com.intellij.openapi.util.io.FileUtil;
23 import com.intellij.uiDesigner.compiler.*;
24 import com.intellij.uiDesigner.compiler.Utils;
25 import com.intellij.uiDesigner.core.GridConstraints;
26 import com.intellij.uiDesigner.lw.CompiledClassPropertiesProvider;
27 import com.intellij.uiDesigner.lw.LwRootContainer;
28 import gnu.trove.THashMap;
29 import gnu.trove.THashSet;
30 import org.jetbrains.annotations.Nullable;
31 import org.jetbrains.jps.ModuleChunk;
32 import org.jetbrains.jps.ProjectPaths;
33 import org.jetbrains.jps.builders.DirtyFilesHolder;
34 import org.jetbrains.jps.builders.java.JavaBuilderUtil;
35 import org.jetbrains.jps.builders.java.JavaSourceRootDescriptor;
36 import org.jetbrains.jps.builders.logging.ProjectBuilderLogger;
37 import org.jetbrains.jps.incremental.*;
38 import org.jetbrains.jps.incremental.instrumentation.ClassProcessingBuilder;
39 import org.jetbrains.jps.incremental.messages.BuildMessage;
40 import org.jetbrains.jps.incremental.messages.CompilerMessage;
41 import org.jetbrains.jps.incremental.messages.ProgressMessage;
42 import org.jetbrains.jps.incremental.storage.OneToManyPathsMapping;
43 import org.jetbrains.jps.model.JpsDummyElement;
44 import org.jetbrains.jps.model.JpsProject;
45 import org.jetbrains.jps.model.java.JpsJavaSdkType;
46 import org.jetbrains.jps.model.library.sdk.JpsSdk;
47 import org.jetbrains.jps.uiDesigner.model.JpsUiDesignerConfiguration;
48 import org.jetbrains.jps.uiDesigner.model.JpsUiDesignerExtensionService;
49 import org.jetbrains.org.objectweb.asm.ClassReader;
50
51 import java.io.*;
52 import java.util.*;
53
54 /**
55  * @author Eugene Zhuravlev
56  *         Date: 11/20/12
57  */
58 public class FormsInstrumenter extends FormsBuilder {
59   public static final String BUILDER_NAME = "forms";
60
61   public FormsInstrumenter() {
62     super(BuilderCategory.CLASS_INSTRUMENTER, BUILDER_NAME);
63   }
64
65   @Override
66   public ExitCode build(CompileContext context, ModuleChunk chunk, DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder, OutputConsumer outputConsumer) throws ProjectBuildException, IOException {
67     final JpsProject project = context.getProjectDescriptor().getProject();
68     final JpsUiDesignerConfiguration config = JpsUiDesignerExtensionService.getInstance().getOrCreateUiDesignerConfiguration(project);
69     if (!config.isInstrumentClasses()) {
70       return ExitCode.NOTHING_DONE;
71     }
72
73     final Map<File, Collection<File>> srcToForms = FORMS_TO_COMPILE.get(context);
74     FORMS_TO_COMPILE.set(context, null);
75
76     if (srcToForms == null || srcToForms.isEmpty()) {
77       return ExitCode.NOTHING_DONE;
78     }
79
80     final Set<File> formsToCompile = new THashSet<File>(FileUtil.FILE_HASHING_STRATEGY);
81     for (Collection<File> files : srcToForms.values()) {
82       formsToCompile.addAll(files);
83     }
84
85     if (JavaBuilderUtil.isCompileJavaIncrementally(context)) {
86       final ProjectBuilderLogger logger = context.getLoggingManager().getProjectBuilderLogger();
87       if (logger.isEnabled()) {
88         logger.logCompiledFiles(formsToCompile, getPresentableName(), "Compiling forms:");
89       }
90     }
91
92     try {
93       final Collection<File> platformCp = ProjectPaths.getPlatformCompilationClasspath(chunk, false);
94
95       final List<File> classpath = new ArrayList<File>();
96       classpath.addAll(ProjectPaths.getCompilationClasspath(chunk, false));
97       classpath.add(getResourcePath(GridConstraints.class)); // forms_rt.jar
98       final Map<File, String> chunkSourcePath = ProjectPaths.getSourceRootsWithDependents(chunk);
99       classpath.addAll(chunkSourcePath.keySet()); // sourcepath for loading forms resources
100       final JpsSdk<JpsDummyElement> sdk = chunk.representativeTarget().getModule().getSdk(JpsJavaSdkType.INSTANCE);
101       final InstrumentationClassFinder finder = ClassProcessingBuilder.createInstrumentationClassFinder(sdk, platformCp, classpath, outputConsumer);
102
103       try {
104         final Map<File, Collection<File>> processed = instrumentForms(context, chunk, chunkSourcePath, finder, formsToCompile, outputConsumer);
105
106         final OneToManyPathsMapping sourceToFormMap = context.getProjectDescriptor().dataManager.getSourceToFormMap();
107
108         for (Map.Entry<File, Collection<File>> entry : processed.entrySet()) {
109           final File src = entry.getKey();
110           final Collection<File> forms = entry.getValue();
111
112           final Collection<String> formPaths = new ArrayList<String>(forms.size());
113           for (File form : forms) {
114             formPaths.add(form.getPath());
115           }
116           sourceToFormMap.update(src.getPath(), formPaths);
117           srcToForms.remove(src);
118         }
119         // clean mapping
120         for (File srcFile : srcToForms.keySet()) {
121           sourceToFormMap.remove(srcFile.getPath());
122         }
123       }
124       finally {
125         finder.releaseResources();
126       }
127     }
128     finally {
129       context.processMessage(new ProgressMessage("Finished instrumenting forms [" + chunk.getPresentableShortName() + "]"));
130     }
131
132     return ExitCode.OK;
133   }
134
135   @Override
136   public List<String> getCompilableFileExtensions() {
137     return Collections.emptyList();
138   }
139
140   private Map<File, Collection<File>> instrumentForms(
141     CompileContext context, ModuleChunk chunk, final Map<File, String> chunkSourcePath, final InstrumentationClassFinder finder, Collection<File> forms, OutputConsumer outConsumer
142   ) throws ProjectBuildException {
143
144     final Map<File, Collection<File>> instrumented = new THashMap<File, Collection<File>>(FileUtil.FILE_HASHING_STRATEGY);
145     final Map<String, File> class2form = new HashMap<String, File>();
146
147     final MyNestedFormLoader nestedFormsLoader =
148       new MyNestedFormLoader(chunkSourcePath, ProjectPaths.getOutputPathsWithDependents(chunk));
149
150     for (File formFile : forms) {
151       final LwRootContainer rootContainer;
152       try {
153         rootContainer = Utils.getRootContainer(
154           formFile.toURI().toURL(), new CompiledClassPropertiesProvider( finder.getLoader())
155         );
156       }
157       catch (AlienFormFileException e) {
158         // ignore non-IDEA forms
159         continue;
160       }
161       catch (UnexpectedFormElementException e) {
162         context.processMessage(new CompilerMessage(getPresentableName(), BuildMessage.Kind.ERROR, e.getMessage(), formFile.getPath()));
163         LOG.info(e);
164         continue;
165       }
166       catch (UIDesignerException e) {
167         context.processMessage(new CompilerMessage(getPresentableName(), BuildMessage.Kind.ERROR, e.getMessage(), formFile.getPath()));
168         LOG.info(e);
169         continue;
170       }
171       catch (Exception e) {
172         throw new ProjectBuildException("Cannot process form file " + formFile.getAbsolutePath(), e);
173       }
174
175       final String classToBind = rootContainer.getClassToBind();
176       if (classToBind == null) {
177         continue;
178       }
179
180       final CompiledClass compiled = findClassFile(outConsumer, classToBind);
181       if (compiled == null) {
182         context.processMessage(new CompilerMessage(
183           getPresentableName(), BuildMessage.Kind.WARNING, "Class to bind does not exist: " + classToBind, formFile.getAbsolutePath())
184         );
185         continue;
186       }
187
188       final File alreadyProcessedForm = class2form.get(classToBind);
189       if (alreadyProcessedForm != null) {
190         context.processMessage(
191           new CompilerMessage(
192             getPresentableName(), BuildMessage.Kind.WARNING,
193             formFile.getAbsolutePath() + ": The form is bound to the class " + classToBind + ".\nAnother form " + alreadyProcessedForm.getAbsolutePath() + " is also bound to this class",
194             formFile.getAbsolutePath())
195         );
196         continue;
197       }
198
199       class2form.put(classToBind, formFile);
200       for (File file : compiled.getSourceFiles()) {
201         addBinding(file, formFile, instrumented);
202       }
203
204
205       try {
206         context.processMessage(new ProgressMessage("Instrumenting forms... [" + chunk.getPresentableShortName() + "]"));
207
208         final BinaryContent originalContent = compiled.getContent();
209         final ClassReader classReader =
210           new FailSafeClassReader(originalContent.getBuffer(), originalContent.getOffset(), originalContent.getLength());
211
212         final int version = ClassProcessingBuilder.getClassFileVersion(classReader);
213         final InstrumenterClassWriter classWriter = new InstrumenterClassWriter(classReader, ClassProcessingBuilder.getAsmClassWriterFlags(version), finder);
214         final AsmCodeGenerator codeGenerator = new AsmCodeGenerator(rootContainer, finder, nestedFormsLoader, false, classWriter);
215         final byte[] patchedBytes = codeGenerator.patchClass(classReader);
216         if (patchedBytes != null) {
217           compiled.setContent(new BinaryContent(patchedBytes));
218         }
219
220         final FormErrorInfo[] warnings = codeGenerator.getWarnings();
221         for (final FormErrorInfo warning : warnings) {
222           context.processMessage(
223             new CompilerMessage(getPresentableName(), BuildMessage.Kind.WARNING, warning.getErrorMessage(), formFile.getAbsolutePath())
224           );
225         }
226
227         final FormErrorInfo[] errors = codeGenerator.getErrors();
228         if (errors.length > 0) {
229           StringBuilder message = new StringBuilder();
230           for (final FormErrorInfo error : errors) {
231             if (message.length() > 0) {
232               message.append("\n");
233             }
234             message.append(formFile.getAbsolutePath()).append(": ").append(error.getErrorMessage());
235           }
236           context.processMessage(new CompilerMessage(getPresentableName(), BuildMessage.Kind.ERROR, message.toString()));
237         }
238       }
239       catch (Exception e) {
240         context.processMessage(new CompilerMessage(getPresentableName(), BuildMessage.Kind.ERROR, "Forms instrumentation failed" + e.getMessage(), formFile.getAbsolutePath()));
241       }
242     }
243     return instrumented;
244   }
245
246
247   private static CompiledClass findClassFile(OutputConsumer outputConsumer, String classToBind) {
248     final Map<String, CompiledClass> compiled = outputConsumer.getCompiledClasses();
249     while (true) {
250       final CompiledClass fo = compiled.get(classToBind);
251       if (fo != null) {
252         return fo;
253       }
254       final int dotIndex = classToBind.lastIndexOf('.');
255       if (dotIndex <= 0) {
256         return null;
257       }
258       classToBind = classToBind.substring(0, dotIndex) + "$" + classToBind.substring(dotIndex + 1);
259     }
260   }
261
262   private static File getResourcePath(Class aClass) {
263     return new File(PathManager.getResourceRoot(aClass, "/" + aClass.getName().replace('.', '/') + ".class"));
264   }
265
266   private static class MyNestedFormLoader implements NestedFormLoader {
267     private final Map<File, String> mySourceRoots;
268     private final Collection<File> myOutputRoots;
269     private final HashMap<String, LwRootContainer> myCache = new HashMap<String, LwRootContainer>();
270
271     /**
272      * @param sourceRoots all source roots for current module chunk and all dependent recursively
273      * @param outputRoots output roots for this module chunk and all dependent recursively
274      */
275     public MyNestedFormLoader(Map<File, String> sourceRoots, Collection<File> outputRoots) {
276       mySourceRoots = sourceRoots;
277       myOutputRoots = outputRoots;
278     }
279
280     public LwRootContainer loadForm(String formFileName) throws Exception {
281       if (myCache.containsKey(formFileName)) {
282         return myCache.get(formFileName);
283       }
284
285       final String relPath = FileUtil.toSystemIndependentName(formFileName);
286
287       for (Map.Entry<File, String> entry : mySourceRoots.entrySet()) {
288         final File sourceRoot = entry.getKey();
289         final String prefix = entry.getValue();
290         String path = relPath;
291         if (prefix != null && FileUtil.startsWith(path, prefix)) {
292           path = path.substring(prefix.length());
293         }
294         final File formFile = new File(sourceRoot, path);
295         if (formFile.exists()) {
296           final BufferedInputStream stream = new BufferedInputStream(new FileInputStream(formFile));
297           try {
298             return loadForm(formFileName, stream);
299           }
300           finally {
301             stream.close();
302           }
303         }
304       }
305
306       throw new Exception("Cannot find nested form file " + formFileName);
307     }
308
309     private LwRootContainer loadForm(String formFileName, InputStream resourceStream) throws Exception {
310       final LwRootContainer container = Utils.getRootContainer(resourceStream, null);
311       myCache.put(formFileName, container);
312       return container;
313     }
314
315     public String getClassToBindName(LwRootContainer container) {
316       final String className = container.getClassToBind();
317       for (File outputRoot : myOutputRoots) {
318         final String result = getJVMClassName(outputRoot, className.replace('.', '/'));
319         if (result != null) {
320           return result.replace('/', '.');
321         }
322       }
323       return className;
324     }
325   }
326
327   @Nullable
328   private static String getJVMClassName(File outputRoot, String className) {
329     while (true) {
330       final File candidateClass = new File(outputRoot, className + ".class");
331       if (candidateClass.exists()) {
332         return className;
333       }
334       final int position = className.lastIndexOf('/');
335       if (position < 0) {
336         return null;
337       }
338       className = className.substring(0, position) + '$' + className.substring(position + 1);
339     }
340   }
341
342 }