IDEA-120097 Gradle sourceCompatibility is ignored in multi-module builds
[idea/community.git] / platform / external-system-impl / src / com / intellij / openapi / externalSystem / service / project / manage / AbstractModuleDataService.java
1 /*
2  * Copyright 2000-2015 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 com.intellij.openapi.externalSystem.service.project.manage;
17
18 import com.intellij.compiler.CompilerConfiguration;
19 import com.intellij.ide.util.projectWizard.ModuleBuilder;
20 import com.intellij.openapi.application.ApplicationManager;
21 import com.intellij.openapi.diagnostic.Logger;
22 import com.intellij.openapi.externalSystem.model.DataNode;
23 import com.intellij.openapi.externalSystem.model.ProjectKeys;
24 import com.intellij.openapi.externalSystem.model.ProjectSystemId;
25 import com.intellij.openapi.externalSystem.model.project.ExternalSystemSourceType;
26 import com.intellij.openapi.externalSystem.model.project.ModuleData;
27 import com.intellij.openapi.externalSystem.model.project.OrderAware;
28 import com.intellij.openapi.externalSystem.model.project.ProjectData;
29 import com.intellij.openapi.externalSystem.service.project.IdeModifiableModelsProvider;
30 import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil;
31 import com.intellij.openapi.externalSystem.util.ExternalSystemBundle;
32 import com.intellij.openapi.externalSystem.util.ExternalSystemConstants;
33 import com.intellij.openapi.externalSystem.util.ExternalSystemUiUtil;
34 import com.intellij.openapi.module.ModifiableModuleModel;
35 import com.intellij.openapi.module.Module;
36 import com.intellij.openapi.project.Project;
37 import com.intellij.openapi.roots.*;
38 import com.intellij.openapi.ui.DialogWrapper;
39 import com.intellij.openapi.util.Computable;
40 import com.intellij.openapi.util.Key;
41 import com.intellij.openapi.util.Pair;
42 import com.intellij.openapi.util.io.FileUtil;
43 import com.intellij.openapi.vfs.VfsUtilCore;
44 import com.intellij.pom.java.LanguageLevel;
45 import com.intellij.ui.CheckBoxList;
46 import com.intellij.ui.IdeBorderFactory;
47 import com.intellij.ui.components.JBScrollPane;
48 import com.intellij.util.*;
49 import com.intellij.util.containers.ContainerUtil;
50 import com.intellij.util.containers.ContainerUtilRt;
51 import org.jetbrains.annotations.NotNull;
52 import org.jetbrains.annotations.Nullable;
53
54 import javax.swing.*;
55 import java.awt.*;
56 import java.util.*;
57 import java.util.List;
58
59 /**
60  * @author Vladislav.Soroka
61  * @since 8/5/2015
62  */
63 public abstract class AbstractModuleDataService<E extends ModuleData> extends AbstractProjectDataService<E, Module> {
64
65   public static final Key<ModuleData> MODULE_DATA_KEY = Key.create("MODULE_DATA_KEY");
66   public static final Key<Module> MODULE_KEY = Key.create("LINKED_MODULE");
67   public static final Key<Map<OrderEntry, OrderAware>> ORDERED_DATA_MAP_KEY = Key.create("ORDER_ENTRY_DATA_MAP");
68   public static final Key<Set<String>> ORPHAN_MODULE_FILES = Key.create("ORPHAN_FILES");
69
70   private static final Logger LOG = Logger.getInstance(AbstractModuleDataService.class);
71
72   @Override
73   public void importData(@NotNull final Collection<DataNode<E>> toImport,
74                          @Nullable ProjectData projectData,
75                          @NotNull final Project project,
76                          @NotNull IdeModifiableModelsProvider modelsProvider) {
77     if (toImport.isEmpty()) {
78       return;
79     }
80
81     final Collection<DataNode<E>> toCreate = filterExistingModules(toImport, modelsProvider, project);
82     if (!toCreate.isEmpty()) {
83       createModules(toCreate, modelsProvider, project);
84     }
85     for (DataNode<E> node : toImport) {
86       Module module = node.getUserData(MODULE_KEY);
87       if (module != null) {
88         setModuleOptions(module, node);
89
90         final ModifiableModuleModel modifiableModel = modelsProvider.getModifiableModuleModel();
91         modifiableModel.setModuleGroupPath(module, node.getData().getIdeModuleGroup());
92         ModifiableRootModel modifiableRootModel = modelsProvider.getModifiableRootModel(module);
93         syncPaths(module, modifiableRootModel, node.getData());
94         setLanguageLevel(modifiableRootModel, node.getData());
95       }
96     }
97   }
98
99   private void createModules(@NotNull Collection<DataNode<E>> toCreate,
100                              @NotNull IdeModifiableModelsProvider modelsProvider,
101                              @NotNull Project project) {
102     for (final DataNode<E> module : toCreate) {
103       ModuleData data = module.getData();
104       final Module created = modelsProvider.newModule(data.getModuleFilePath(), data.getModuleTypeId());
105       module.putUserData(MODULE_KEY, created);
106       Set<String> orphanFiles = project.getUserData(ORPHAN_MODULE_FILES);
107       if(orphanFiles != null) {
108         orphanFiles.remove(created.getModuleFilePath());
109       }
110
111       // Ensure that the dependencies are clear (used to be not clear when manually removing the module and importing it via gradle)
112       final ModifiableRootModel modifiableRootModel = modelsProvider.getModifiableRootModel(created);
113       modifiableRootModel.inheritSdk();
114
115       RootPolicy<Object> visitor = new RootPolicy<Object>() {
116         @Override
117         public Object visitLibraryOrderEntry(LibraryOrderEntry libraryOrderEntry, Object value) {
118           modifiableRootModel.removeOrderEntry(libraryOrderEntry);
119           return value;
120         }
121
122         @Override
123         public Object visitModuleOrderEntry(ModuleOrderEntry moduleOrderEntry, Object value) {
124           modifiableRootModel.removeOrderEntry(moduleOrderEntry);
125           return value;
126         }
127       };
128
129       for (OrderEntry orderEntry : modifiableRootModel.getOrderEntries()) {
130         orderEntry.accept(visitor, null);
131       }
132     }
133   }
134
135   @NotNull
136   private Collection<DataNode<E>> filterExistingModules(@NotNull Collection<DataNode<E>> modules,
137                                                         @NotNull IdeModifiableModelsProvider modelsProvider,
138                                                         @NotNull Project project) {
139     Collection<DataNode<E>> result = ContainerUtilRt.newArrayList();
140     for (DataNode<E> node : modules) {
141       ModuleData moduleData = node.getData();
142       Module module = modelsProvider.findIdeModule(moduleData.getInternalName());
143       if (module == null) {
144         result.add(node);
145       }
146       else {
147         if (!FileUtil.pathsEqual(module.getModuleFilePath(), moduleData.getModuleFilePath())) {
148           modelsProvider.getModifiableModuleModel().disposeModule(module);
149           result.add(node);
150           Set<String> orphanFiles = project.getUserData(ORPHAN_MODULE_FILES);
151           if (orphanFiles == null) {
152             project.putUserData(ORPHAN_MODULE_FILES, orphanFiles = ContainerUtil.newHashSet());
153           }
154           orphanFiles.add(module.getModuleFilePath());
155         }
156         else {
157           node.putUserData(MODULE_KEY, module);
158         }
159       }
160     }
161     return result;
162   }
163
164   private static void syncPaths(@NotNull Module module, @NotNull ModifiableRootModel modifiableModel, @NotNull ModuleData data) {
165     CompilerModuleExtension extension = modifiableModel.getModuleExtension(CompilerModuleExtension.class);
166     if (extension == null) {
167       //modifiableModel.dispose();
168       LOG.warn(String.format("Can't sync paths for module '%s'. Reason: no compiler extension is found for it", module.getName()));
169       return;
170     }
171     String compileOutputPath = data.getCompileOutputPath(ExternalSystemSourceType.SOURCE);
172     extension.setCompilerOutputPath(compileOutputPath != null ? VfsUtilCore.pathToUrl(compileOutputPath) : null);
173
174     String testCompileOutputPath = data.getCompileOutputPath(ExternalSystemSourceType.TEST);
175     extension.setCompilerOutputPathForTests(testCompileOutputPath != null ? VfsUtilCore.pathToUrl(testCompileOutputPath) : null);
176
177     extension.inheritCompilerOutputPath(data.isInheritProjectCompileOutputPath());
178   }
179
180   @Override
181   public void removeData(@NotNull final Computable<Collection<Module>> toRemoveComputable,
182                          @NotNull final Collection<DataNode<E>> toIgnore,
183                          @NotNull final ProjectData projectData,
184                          @NotNull final Project project,
185                          @NotNull final IdeModifiableModelsProvider modelsProvider) {
186     final Collection<Module> toRemove = toRemoveComputable.compute();
187     final List<Module> modules = new SmartList<Module>(toRemove);
188     for (DataNode<E> moduleDataNode : toIgnore) {
189       final Module module = modelsProvider.findIdeModule(moduleDataNode.getData());
190       ContainerUtil.addIfNotNull(modules, module);
191     }
192
193     if (modules.isEmpty()) {
194       return;
195     }
196
197     ContainerUtil.removeDuplicates(modules);
198
199     for (Module module : modules) {
200       if (module.isDisposed()) continue;
201       unlinkModuleFromExternalSystem(module);
202     }
203
204     ruleOrphanModules(modules, project, projectData.getOwner(), new Consumer<List<Module>>() {
205       @Override
206       public void consume(final List<Module> modules) {
207         for (Module module : modules) {
208           if (module.isDisposed()) continue;
209           String path = module.getModuleFilePath();
210           final ModifiableModuleModel moduleModel = modelsProvider.getModifiableModuleModel();
211           moduleModel.disposeModule(module);
212           ModuleBuilder.deleteModuleFile(path);
213         }
214       }
215     });
216   }
217
218   /**
219    * There is a possible case that an external module has been un-linked from ide project. There are two ways to process
220    * ide modules which correspond to that external project:
221    * <pre>
222    * <ol>
223    *   <li>Remove them from ide project as well;</li>
224    *   <li>Keep them at ide project as well;</li>
225    * </ol>
226    * </pre>
227    * This method handles that situation, i.e. it asks a user what should be done and acts accordingly.
228    *
229    * @param orphanModules    modules which correspond to the un-linked external project
230    * @param project          current ide project
231    * @param externalSystemId id of the external system which project has been un-linked from ide project
232    */
233   private static void ruleOrphanModules(@NotNull final List<Module> orphanModules,
234                                         @NotNull final Project project,
235                                         @NotNull final ProjectSystemId externalSystemId,
236                                         @NotNull final Consumer<List<Module>> result) {
237     ExternalSystemApiUtil.executeOnEdt(true, new Runnable() {
238       @Override
239       public void run() {
240         List<Module> toRemove = ContainerUtil.newSmartList();
241         if (ApplicationManager.getApplication().isHeadlessEnvironment()) {
242           toRemove.addAll(orphanModules);
243         }
244         else {
245           final JPanel content = new JPanel(new GridBagLayout());
246           content.add(new JLabel(ExternalSystemBundle.message("orphan.modules.text", externalSystemId.getReadableName())),
247                       ExternalSystemUiUtil.getFillLineConstraints(0));
248
249           final CheckBoxList<Module> orphanModulesList = new CheckBoxList<Module>();
250           orphanModulesList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
251           orphanModulesList.setItems(orphanModules, new Function<Module, String>() {
252             @Override
253             public String fun(Module module) {
254               return module.getName();
255             }
256           });
257           for (Module module : orphanModules) {
258             orphanModulesList.setItemSelected(module, true);
259           }
260           orphanModulesList.setBorder(IdeBorderFactory.createEmptyBorder(8));
261           content.add(orphanModulesList, ExternalSystemUiUtil.getFillLineConstraints(0));
262           content.setBorder(IdeBorderFactory.createEmptyBorder(0, 0, 8, 0));
263
264           DialogWrapper dialog = new DialogWrapper(project) {
265             {
266               setTitle(ExternalSystemBundle.message("import.title", externalSystemId.getReadableName()));
267               init();
268             }
269
270             @Nullable
271             @Override
272             protected JComponent createCenterPanel() {
273               return new JBScrollPane(content);
274             }
275
276             @NotNull
277             protected Action[] createActions() {
278               return new Action[]{getOKAction()};
279             }
280           };
281
282           dialog.showAndGet();
283
284           for (int i = 0; i < orphanModules.size(); i++) {
285             Module module = orphanModules.get(i);
286             if (orphanModulesList.isItemSelected(i)) {
287               toRemove.add(module);
288             }
289           }
290         }
291         result.consume(toRemove);
292       }
293     });
294   }
295
296   public static void unlinkModuleFromExternalSystem(@NotNull Module module) {
297     module.clearOption(ExternalSystemConstants.EXTERNAL_SYSTEM_ID_KEY);
298     module.clearOption(ExternalSystemConstants.LINKED_PROJECT_ID_KEY);
299     module.clearOption(ExternalSystemConstants.LINKED_PROJECT_PATH_KEY);
300     module.clearOption(ExternalSystemConstants.ROOT_PROJECT_PATH_KEY);
301     module.clearOption(ExternalSystemConstants.EXTERNAL_SYSTEM_MODULE_GROUP_KEY);
302     module.clearOption(ExternalSystemConstants.EXTERNAL_SYSTEM_MODULE_VERSION_KEY);
303   }
304
305   protected void setModuleOptions(Module module, DataNode<E> moduleDataNode) {
306     ModuleData moduleData = moduleDataNode.getData();
307     module.putUserData(MODULE_DATA_KEY, moduleData);
308
309     module.setOption(ExternalSystemConstants.EXTERNAL_SYSTEM_ID_KEY, moduleData.getOwner().toString());
310     module.setOption(ExternalSystemConstants.LINKED_PROJECT_ID_KEY, moduleData.getId());
311     module.setOption(ExternalSystemConstants.LINKED_PROJECT_PATH_KEY, moduleData.getLinkedExternalProjectPath());
312     final ProjectData projectData = moduleDataNode.getData(ProjectKeys.PROJECT);
313     module.setOption(ExternalSystemConstants.ROOT_PROJECT_PATH_KEY, projectData != null ? projectData.getLinkedExternalProjectPath() : "");
314
315     if (moduleData.getGroup() != null) {
316       module.setOption(ExternalSystemConstants.EXTERNAL_SYSTEM_MODULE_GROUP_KEY, moduleData.getGroup());
317     }
318     if (moduleData.getVersion() != null) {
319       module.setOption(ExternalSystemConstants.EXTERNAL_SYSTEM_MODULE_VERSION_KEY, moduleData.getVersion());
320     }
321
322     // clear maven option
323     module.clearOption("org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule");
324   }
325
326   @Override
327   public void postProcess(@NotNull Collection<DataNode<E>> toImport,
328                           @Nullable ProjectData projectData,
329                           @NotNull Project project,
330                           @NotNull IdeModifiableModelsProvider modelsProvider) {
331     for (DataNode<E> moduleDataNode : toImport) {
332       final Module module = moduleDataNode.getUserData(MODULE_KEY);
333       if (module == null) continue;
334       final Map<OrderEntry, OrderAware> orderAwareMap = moduleDataNode.getUserData(ORDERED_DATA_MAP_KEY);
335       if (orderAwareMap != null) {
336         rearrangeOrderEntries(orderAwareMap, modelsProvider.getModifiableRootModel(module));
337       }
338       setBytecodeTargetLevel(project, module, moduleDataNode.getData());
339     }
340   }
341
342   @Override
343   public void onSuccessImport(@NotNull Project project) {
344     final Set<String> orphanFiles = project.getUserData(ORPHAN_MODULE_FILES);
345     if(orphanFiles != null && !orphanFiles.isEmpty()) {
346       ExternalSystemApiUtil.executeOnEdt(false, new Runnable() {
347         @Override
348         public void run() {
349           for (String orphanFile : orphanFiles) {
350             ModuleBuilder.deleteModuleFile(orphanFile);
351           }
352         }
353       });
354       project.putUserData(ORPHAN_MODULE_FILES, null);
355     }
356   }
357
358   protected void rearrangeOrderEntries(@NotNull Map<OrderEntry, OrderAware> orderEntryDataMap,
359                                        @NotNull ModifiableRootModel modifiableRootModel) {
360     final OrderEntry[] orderEntries = modifiableRootModel.getOrderEntries();
361     final int length = orderEntries.length;
362     final OrderEntry[] newOrder = new OrderEntry[length];
363     final PriorityQueue<Pair<OrderEntry, OrderAware>> priorityQueue = new PriorityQueue<Pair<OrderEntry, OrderAware>>(
364       11, new Comparator<Pair<OrderEntry, OrderAware>>() {
365       @Override
366       public int compare(Pair<OrderEntry, OrderAware> o1, Pair<OrderEntry, OrderAware> o2) {
367         int order1 = o1.second.getOrder();
368         int order2 = o2.second.getOrder();
369         return order1 != order2 ? order1 < order2 ? -1 : 1 : 0;
370       }
371     });
372
373     int shift = 0;
374     for (int i = 0; i < length; i++) {
375       OrderEntry orderEntry = orderEntries[i];
376       final OrderAware orderAware = orderEntryDataMap.get(orderEntry);
377       if (orderAware == null) {
378         newOrder[i] = orderEntry;
379         shift++;
380       }
381       else {
382         priorityQueue.add(Pair.create(orderEntry, orderAware));
383       }
384     }
385
386     Pair<OrderEntry, OrderAware> pair;
387     while ((pair = priorityQueue.poll()) != null) {
388       final OrderEntry orderEntry = pair.first;
389       final OrderAware orderAware = pair.second;
390       final int order = orderAware.getOrder() != -1 ? orderAware.getOrder() : length - 1;
391       final int newPlace = findNewPlace(newOrder, order - shift);
392       assert newPlace != -1;
393       newOrder[newPlace] = orderEntry;
394     }
395
396     if (LOG.isDebugEnabled()) {
397       final boolean changed = !ArrayUtil.equals(orderEntries, newOrder, new Comparator<OrderEntry>() {
398         @Override
399         public int compare(OrderEntry o1, OrderEntry o2) {
400           return o1.compareTo(o2);
401         }
402       });
403       LOG.debug(String.format("rearrange status (%s): %s", modifiableRootModel.getModule(), changed ? "modified" : "not modified"));
404     }
405     modifiableRootModel.rearrangeOrderEntries(newOrder);
406   }
407
408   private static int findNewPlace(OrderEntry[] newOrder, int newIndex) {
409     int idx = newIndex;
410     while (idx < 0 || (idx < newOrder.length && newOrder[idx] != null)) {
411       idx++;
412     }
413     if (idx >= newOrder.length) {
414       idx = newIndex - 1;
415       while (idx >= 0 && (idx >= newOrder.length || newOrder[idx] != null)) {
416         idx--;
417       }
418     }
419     return idx == -1 ? -1 : idx;
420   }
421
422   private void setLanguageLevel(@NotNull ModifiableRootModel modifiableRootModel, E data) {
423     LanguageLevel level = LanguageLevel.parse(data.getSourceCompatibility());
424     if (level != null) {
425       try {
426         modifiableRootModel.getModuleExtension(LanguageLevelModuleExtension.class).setLanguageLevel(level);
427       }
428       catch (IllegalArgumentException e) {
429         LOG.debug(e);
430       }
431     }
432   }
433
434   private void setBytecodeTargetLevel(@NotNull Project project, @NotNull Module module, @NotNull E data) {
435     String targetLevel = data.getTargetCompatibility();
436     if (targetLevel != null) {
437       CompilerConfiguration configuration = CompilerConfiguration.getInstance(project);
438       configuration.setBytecodeTargetLevel(module, targetLevel);
439     }
440   }
441 }