use new FileUtilRt.createFilterByExtension method
[idea/community.git] / plugins / ui-designer / jps-plugin / src / org / jetbrains / jps / uiDesigner / compiler / FormsBindingManager.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.openapi.util.Key;
19 import com.intellij.openapi.util.SystemInfo;
20 import com.intellij.openapi.util.io.FileUtil;
21 import com.intellij.openapi.util.io.FileUtilRt;
22 import com.intellij.openapi.util.text.StringUtil;
23 import gnu.trove.THashMap;
24 import gnu.trove.THashSet;
25 import org.jetbrains.annotations.NotNull;
26 import org.jetbrains.annotations.Nullable;
27 import org.jetbrains.jps.ModuleChunk;
28 import org.jetbrains.jps.builders.DirtyFilesHolder;
29 import org.jetbrains.jps.builders.FileProcessor;
30 import org.jetbrains.jps.builders.java.JavaBuilderUtil;
31 import org.jetbrains.jps.builders.java.JavaSourceRootDescriptor;
32 import org.jetbrains.jps.incremental.*;
33 import org.jetbrains.jps.incremental.fs.CompilationRound;
34 import org.jetbrains.jps.incremental.java.CopyResourcesUtil;
35 import org.jetbrains.jps.incremental.java.FormsParsing;
36 import org.jetbrains.jps.incremental.storage.OneToManyPathsMapping;
37 import org.jetbrains.jps.model.JpsProject;
38 import org.jetbrains.jps.model.java.JpsJavaExtensionService;
39 import org.jetbrains.jps.model.java.compiler.JpsCompilerExcludes;
40 import org.jetbrains.jps.model.java.compiler.JpsJavaCompilerConfiguration;
41 import org.jetbrains.jps.uiDesigner.model.JpsUiDesignerConfiguration;
42 import org.jetbrains.jps.uiDesigner.model.JpsUiDesignerExtensionService;
43
44 import java.io.File;
45 import java.io.IOException;
46 import java.util.*;
47
48 /**
49  * @author Eugene Zhuravlev
50  *         Date: 11/20/12
51  */
52 public class FormsBindingManager extends FormsBuilder {
53   private static final String JAVA_EXTENSION = ".java";
54   private static final Key<Boolean> FORCE_FORMS_REBUILD_FLAG = Key.create("_forms_rebuild_flag_");
55   private static final Key<Boolean> FORMS_REBUILD_FORCED = Key.create("_forms_rebuild_forced_flag_");
56   public FormsBindingManager() {
57     super(BuilderCategory.SOURCE_PROCESSOR, "form-bindings");
58   }
59
60   @Override
61   public void buildStarted(CompileContext context) {
62     FORCE_FORMS_REBUILD_FLAG.set(context, getMarkerFile(context).exists());
63   }
64
65   @Override
66   public void chunkBuildFinished(CompileContext context, ModuleChunk chunk) {
67     FORMS_REBUILD_FORCED.set(context, null); // clear the flag on per-chunk basis
68     super.chunkBuildFinished(context, chunk);
69   }
70
71   @Override
72   public void buildFinished(CompileContext context) {
73     final boolean previousValue = FORCE_FORMS_REBUILD_FLAG.get(context, Boolean.FALSE);
74     final JpsUiDesignerConfiguration config = JpsUiDesignerExtensionService.getInstance().getUiDesignerConfiguration(context.getProjectDescriptor().getProject());
75     final boolean currentRebuildValue = config != null && !config.isInstrumentClasses();
76     if (previousValue != currentRebuildValue) {
77       final File marker = getMarkerFile(context);
78       if (currentRebuildValue) {
79         FileUtil.createIfDoesntExist(marker);
80       }
81       else {
82         FileUtil.delete(marker);
83       }
84     }
85   }
86
87   @NotNull
88   private static File getMarkerFile(CompileContext context) {
89     return new File(context.getProjectDescriptor().dataManager.getDataPaths().getDataStorageRoot(), "forms_rebuild_required");
90   }
91
92   @Override
93   public List<String> getCompilableFileExtensions() {
94     return Collections.singletonList(FORM_EXTENSION);
95   }
96
97   @Override
98   public ExitCode build(CompileContext context, ModuleChunk chunk, DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder, OutputConsumer outputConsumer) throws ProjectBuildException, IOException {
99     ExitCode exitCode = ExitCode.NOTHING_DONE;
100     final JpsProject project = context.getProjectDescriptor().getProject();
101     final JpsUiDesignerConfiguration config = JpsUiDesignerExtensionService.getInstance().getOrCreateUiDesignerConfiguration(project);
102
103     if (!config.isInstrumentClasses() && !config.isCopyFormsRuntimeToOutput()) {
104       return exitCode;
105     }
106
107     final Map<File, ModuleBuildTarget> filesToCompile = new THashMap<File, ModuleBuildTarget>(FileUtil.FILE_HASHING_STRATEGY);
108     final Map<File, ModuleBuildTarget> formsToCompile = new THashMap<File, ModuleBuildTarget>(FileUtil.FILE_HASHING_STRATEGY);
109     final Map<File, Collection<File>> srcToForms = new THashMap<File, Collection<File>>(FileUtil.FILE_HASHING_STRATEGY);
110
111     if (!JavaBuilderUtil.isForcedRecompilationAllJavaModules(context) && config.isInstrumentClasses() && FORCE_FORMS_REBUILD_FLAG.get(context, Boolean.FALSE)) {
112       // force compilation of all forms, but only once per chunk
113       if (!FORMS_REBUILD_FORCED.get(context, Boolean.FALSE)) {
114         FORMS_REBUILD_FORCED.set(context, Boolean.TRUE);
115         FSOperations.markDirty(context, CompilationRound.CURRENT, chunk, FORM_SOURCES_FILTER);
116       }
117     }
118
119     dirtyFilesHolder.processDirtyFiles(new FileProcessor<JavaSourceRootDescriptor, ModuleBuildTarget>() {
120       public boolean apply(ModuleBuildTarget target, File file, JavaSourceRootDescriptor descriptor) throws IOException {
121         if (JAVA_SOURCES_FILTER.accept(file)) {
122           filesToCompile.put(file, target);
123         }
124         else if (FORM_SOURCES_FILTER.accept(file)) {
125           formsToCompile.put(file, target);
126         }
127         return true;
128       }
129     });
130
131     if (config.isInstrumentClasses()) {
132       final JpsJavaCompilerConfiguration configuration = JpsJavaExtensionService.getInstance().getOrCreateCompilerConfiguration(project);
133       final JpsCompilerExcludes excludes = configuration.getCompilerExcludes();
134
135       // force compilation of bound source file if the form is dirty
136       for (final Map.Entry<File, ModuleBuildTarget> entry : formsToCompile.entrySet()) {
137         final File form = entry.getKey();
138         final ModuleBuildTarget target = entry.getValue();
139         final Collection<File> sources = findBoundSourceCandidates(context, target, form);
140         for (File boundSource : sources) {
141           if (!excludes.isExcluded(boundSource)) {
142             addBinding(boundSource, form, srcToForms);
143             FSOperations.markDirty(context, CompilationRound.CURRENT, boundSource);
144             filesToCompile.put(boundSource, target);
145             exitCode = ExitCode.OK;
146           }
147         }
148       }
149
150       // form should be considered dirty if the class it is bound to is dirty
151       final OneToManyPathsMapping sourceToFormMap = context.getProjectDescriptor().dataManager.getSourceToFormMap();
152       for (Map.Entry<File, ModuleBuildTarget> entry : filesToCompile.entrySet()) {
153         final File srcFile = entry.getKey();
154         final ModuleBuildTarget target = entry.getValue();
155         final Collection<String> boundForms = sourceToFormMap.getState(srcFile.getPath());
156         if (boundForms != null) {
157           for (String formPath : boundForms) {
158             final File formFile = new File(formPath);
159             if (!excludes.isExcluded(formFile) && formFile.exists()) {
160               addBinding(srcFile, formFile, srcToForms);
161               FSOperations.markDirty(context, CompilationRound.CURRENT, formFile);
162               formsToCompile.put(formFile, target);
163               exitCode = ExitCode.OK;
164             }
165           }
166         }
167       }
168     }
169
170     FORMS_TO_COMPILE.set(context, srcToForms.isEmpty()? null : srcToForms);
171
172     if (config.isCopyFormsRuntimeToOutput() && containsValidForm(formsToCompile.keySet())) {
173       for (ModuleBuildTarget target : chunk.getTargets()) {
174         if (!target.isTests()) {
175           final File outputDir = target.getOutputDir();
176           if (outputDir != null) {
177             final String outputRoot = FileUtil.toSystemIndependentName(outputDir.getPath());
178             final List<File> generatedFiles = CopyResourcesUtil.copyFormsRuntime(outputRoot, false);
179             if (!generatedFiles.isEmpty()) {
180               exitCode = ExitCode.OK;
181               // now inform others about files just copied
182               for (File file : generatedFiles) {
183                 outputConsumer.registerOutputFile(target, file, Collections.<String>emptyList());
184               }
185             }
186           }
187         }
188       }
189     }
190
191     return exitCode;
192   }
193
194   private static boolean containsValidForm(Set<File> files) {
195     for (File file : files) {
196       try {
197         if (FormsParsing.readBoundClassName(file) != null) {
198           return true;
199         }
200       }
201       catch (IOException ignore) {
202       }
203     }
204     return false;
205   }
206
207   @NotNull
208   private static Collection<File> findBoundSourceCandidates(CompileContext context, final ModuleBuildTarget target, File form) throws IOException {
209     final List<JavaSourceRootDescriptor> targetRoots = context.getProjectDescriptor().getBuildRootIndex().getTargetRoots(target, context);
210     if (targetRoots.isEmpty()) {
211       return Collections.emptyList();
212     }
213     final String className = FormsParsing.readBoundClassName(form);
214     if (className == null) {
215       return Collections.emptyList();
216     }
217     for (JavaSourceRootDescriptor rd : targetRoots) {
218       final File boundSource = findSourceForClass(rd, className);
219       if (boundSource != null) {
220         return Collections.singleton(boundSource);
221       }
222     }
223
224     final Set<File> candidates = new THashSet<File>(FileUtil.FILE_HASHING_STRATEGY);
225     for (JavaSourceRootDescriptor rd : targetRoots) {
226       candidates.addAll(findPossibleSourcesForClass(rd, className));
227     }
228     return candidates;
229   }
230
231   @Nullable
232   private static File findSourceForClass(JavaSourceRootDescriptor rd, final @Nullable String boundClassName) throws IOException {
233     if (boundClassName == null) {
234       return null;
235     }
236     String relPath = suggestRelativePath(rd, boundClassName);
237     while (true) {
238       final File candidate = new File(rd.getRootFile(), relPath);
239       if (candidate.exists()) {
240         return candidate.isFile() ? candidate : null;
241       }
242       final int index = relPath.lastIndexOf('/');
243       if (index <= 0) {
244         return null;
245       }
246       relPath = relPath.substring(0, index) + JAVA_EXTENSION;
247     }
248   }
249
250   @NotNull
251   private static Collection<File> findPossibleSourcesForClass(JavaSourceRootDescriptor rd, final @Nullable String boundClassName) throws IOException {
252     if (boundClassName == null) {
253       return Collections.emptyList();
254     }
255     String relPath = suggestRelativePath(rd, boundClassName);
256     final File containingDirectory = new File(rd.getRootFile(), relPath).getParentFile();
257     if (containingDirectory == null) {
258       return Collections.emptyList();
259     }
260     final File[] files = containingDirectory.listFiles(FileUtilRt.createFilterByExtension("java"));
261     if (files == null || files.length == 0) {
262       return Collections.emptyList();
263     }
264     return Arrays.asList(files); 
265   }
266
267   @NotNull
268   private static String suggestRelativePath(@NotNull JavaSourceRootDescriptor rd, @NotNull String className) {
269     String clsName = className;
270     String prefix = rd.getPackagePrefix();
271     if (!StringUtil.isEmpty(prefix)) {
272       if (!StringUtil.endsWith(prefix, ".")) {
273         prefix += ".";
274       }
275       if (SystemInfo.isFileSystemCaseSensitive? StringUtil.startsWith(clsName, prefix) : StringUtil.startsWithIgnoreCase(clsName, prefix)) {
276         clsName = clsName.substring(prefix.length());
277       }
278     }
279
280     return clsName.replace('.', '/') + JAVA_EXTENSION;
281   }
282 }