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