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