1ff4b0b1f5a3cbbc9f47b0e608602c4de968b0f3
[idea/community.git] / java / idea-ui / src / com / intellij / openapi / roots / ui / configuration / ModuleEditor.java
1 // Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.openapi.roots.ui.configuration;
3
4 import com.intellij.facet.impl.ProjectFacetsConfigurator;
5 import com.intellij.openapi.Disposable;
6 import com.intellij.openapi.actionSystem.DataProvider;
7 import com.intellij.openapi.actionSystem.LangDataKeys;
8 import com.intellij.openapi.diagnostic.Logger;
9 import com.intellij.openapi.extensions.ExtensionPointName;
10 import com.intellij.openapi.module.Module;
11 import com.intellij.openapi.module.ModuleConfigurationEditor;
12 import com.intellij.openapi.module.impl.ModuleConfigurationStateImpl;
13 import com.intellij.openapi.options.Configurable;
14 import com.intellij.openapi.options.ConfigurationException;
15 import com.intellij.openapi.options.ModuleConfigurableEP;
16 import com.intellij.openapi.project.Project;
17 import com.intellij.openapi.roots.*;
18 import com.intellij.openapi.roots.impl.libraries.LibraryEx;
19 import com.intellij.openapi.roots.libraries.Library;
20 import com.intellij.openapi.roots.libraries.LibraryTable;
21 import com.intellij.ui.navigation.History;
22 import com.intellij.ui.navigation.Place;
23 import com.intellij.util.EventDispatcher;
24 import com.intellij.util.containers.ContainerUtil;
25 import gnu.trove.THashSet;
26 import org.jetbrains.annotations.NonNls;
27 import org.jetbrains.annotations.NotNull;
28 import org.jetbrains.annotations.Nullable;
29
30 import javax.swing.*;
31 import java.awt.*;
32 import java.lang.reflect.InvocationHandler;
33 import java.lang.reflect.InvocationTargetException;
34 import java.lang.reflect.Method;
35 import java.lang.reflect.Proxy;
36 import java.util.List;
37 import java.util.*;
38
39 /**
40  * @author Eugene Zhuravlev
41  */
42 public abstract class ModuleEditor implements Place.Navigator, Disposable {
43   private static final Logger LOG = Logger.getInstance(ModuleEditor.class);
44   private static final ExtensionPointName<ModuleConfigurableEP> MODULE_CONFIGURABLES = ExtensionPointName.create("com.intellij.moduleConfigurable");
45   public static final String SELECTED_EDITOR_NAME = "selectedEditor";
46
47   private final Project myProject;
48   private JPanel myGenericSettingsPanel;
49   private ModificationOfImportedModelWarningComponent myModificationOfImportedModelWarningComponent;
50   private ModifiableRootModel myModifiableRootModel; // important: in order to correctly update OrderEntries UI use corresponding proxy for the model
51
52   private final ModulesProvider myModulesProvider;
53   private String myName;
54   private final Module myModule;
55
56   protected final List<ModuleConfigurationEditor> myEditors = new ArrayList<>();
57   private ModifiableRootModel myModifiableRootModelProxy;
58
59   private final EventDispatcher<ChangeListener> myEventDispatcher = EventDispatcher.create(ChangeListener.class);
60   @NonNls private static final String METHOD_COMMIT = "commit";
61   private boolean myEditorsInitialized;
62
63   protected History myHistory;
64
65   public ModuleEditor(Project project, ModulesProvider modulesProvider,
66                       @NotNull Module module) {
67     myProject = project;
68     myModulesProvider = modulesProvider;
69     myModule = module;
70     myName = module.getName();
71   }
72
73   public void init(History history) {
74     myHistory = history;
75
76     for (ModuleConfigurationEditor each : myEditors) {
77       if (each instanceof ModuleElementsEditor) {
78         ((ModuleElementsEditor)each).setHistory(myHistory);
79       }
80     }
81
82     restoreSelectedEditor();
83   }
84
85   public abstract ProjectFacetsConfigurator getFacetsConfigurator();
86
87   protected abstract JComponent createCenterPanel();
88
89   @Nullable
90   public abstract ModuleConfigurationEditor getSelectedEditor();
91
92   public abstract void selectEditor(String displayName);
93
94   protected abstract void restoreSelectedEditor();
95
96   @Nullable
97   public abstract ModuleConfigurationEditor getEditor(@NotNull String displayName);
98
99   protected abstract void disposeCenterPanel();
100
101   public interface ChangeListener extends EventListener {
102     void moduleStateChanged(ModifiableRootModel moduleRootModel);
103   }
104
105   public void addChangeListener(ChangeListener listener) {
106     myEventDispatcher.addListener(listener);
107   }
108
109   public void removeChangeListener(ChangeListener listener) {
110     myEventDispatcher.removeListener(listener);
111   }
112
113   @Nullable
114   public Module getModule() {
115     final Module[] all = myModulesProvider.getModules();
116     for (Module each : all) {
117       if (each == myModule) return myModule;
118     }
119
120     return myModulesProvider.getModule(myName);
121   }
122
123   public ModifiableRootModel getModifiableRootModel() {
124     if (myModifiableRootModel == null) {
125       final Module module = getModule();
126       if (module != null) {
127         myModifiableRootModel = ModuleRootManagerEx.getInstanceEx(module).getModifiableModelForMultiCommit(new UIRootConfigurationAccessor(myProject));
128       }
129     }
130     return myModifiableRootModel;
131   }
132
133   public OrderEntry @NotNull [] getOrderEntries() {
134     if (myModifiableRootModel == null) { // do not clone all model if not necessary
135       return ModuleRootManager.getInstance(getModule()).getOrderEntries();
136     }
137     return myModifiableRootModel.getOrderEntries();
138   }
139
140   public ModifiableRootModel getModifiableRootModelProxy() {
141     if (myModifiableRootModelProxy == null) {
142       final ModifiableRootModel rootModel = getModifiableRootModel();
143       if (rootModel != null) {
144         myModifiableRootModelProxy = (ModifiableRootModel)Proxy.newProxyInstance(
145           getClass().getClassLoader(), new Class[]{ModifiableRootModel.class}, new ModifiableRootModelInvocationHandler(rootModel)
146         );
147       }
148     }
149     return myModifiableRootModelProxy;
150   }
151
152   public ModuleRootModel getRootModel() {
153     if (myModifiableRootModel != null) {
154       return getModifiableRootModelProxy();
155     }
156     return ModuleRootManager.getInstance(myModule);
157   }
158
159   public boolean isModified() {
160     for (ModuleConfigurationEditor moduleElementsEditor : myEditors) {
161       if (moduleElementsEditor.isModified()) {
162         return true;
163       }
164     }
165     return false;
166   }
167
168   private void createEditors(@Nullable Module module) {
169     if (module == null) return;
170
171     ModuleConfigurationState state = createModuleConfigurationState();
172     for (ModuleConfigurationEditorProvider provider : collectProviders(module)) {
173       ModuleConfigurationEditor[] editors = provider.createEditors(state);
174       if (editors.length > 0 && provider instanceof ModuleConfigurationEditorProviderEx &&
175           ((ModuleConfigurationEditorProviderEx)provider).isCompleteEditorSet()) {
176         myEditors.clear();
177         ContainerUtil.addAll(myEditors, editors);
178         break;
179       }
180       else {
181         ContainerUtil.addAll(myEditors, editors);
182       }
183     }
184
185     for (Configurable moduleConfigurable : module.getComponentInstancesOfType(Configurable.class)) {
186       reportDeprecatedModuleEditor(moduleConfigurable.getClass());
187       myEditors.add(new ModuleConfigurableWrapper(moduleConfigurable));
188     }
189     for(ModuleConfigurableEP extension : MODULE_CONFIGURABLES.getExtensionList(module)) {
190       if (extension.canCreateConfigurable()) {
191         Configurable configurable = extension.createConfigurable();
192         if (configurable != null) {
193           reportDeprecatedModuleEditor(configurable.getClass());
194           myEditors.add(new ModuleConfigurableWrapper(configurable));
195         }
196       }
197     }
198     for (ModuleConfigurationEditor editor : myEditors) {
199       if (editor instanceof ModuleElementsEditor) {
200         ((ModuleElementsEditor)editor).addListener(this::updateImportedModelWarning);
201       }
202     }
203   }
204
205   private static final Set<Class<?>> ourReportedDeprecatedClasses = new HashSet<>();
206   private static void reportDeprecatedModuleEditor(@NotNull Class<?> aClass) {
207     if (ourReportedDeprecatedClasses.add(aClass)) {
208       LOG.warn(aClass.getName() + " uses deprecated way to register itself as a module editor. " + ModuleConfigurationEditorProvider.class.getName() + " extension point should be used instead");
209     }
210   }
211
212   private static ModuleConfigurationEditorProvider @NotNull [] collectProviders(@NotNull Module module) {
213     List<ModuleConfigurationEditorProvider> result = new ArrayList<>(module.getComponentInstancesOfType(ModuleConfigurationEditorProvider.class));
214     for (ModuleConfigurationEditorProvider component : result) {
215       reportDeprecatedModuleEditor(component.getClass());
216     }
217     ContainerUtil.addAll(result, ModuleConfigurationEditorProvider.EP_NAME.getExtensions(module));
218     return result.toArray(new ModuleConfigurationEditorProvider[0]);
219   }
220
221   @NotNull
222   public ModuleConfigurationState createModuleConfigurationState() {
223     return new ModuleConfigurationStateImpl(myProject, myModulesProvider) {
224       @Override
225       public ModifiableRootModel getRootModel() {
226         return getModifiableRootModelProxy();
227       }
228
229       @Override
230       public FacetsProvider getFacetsProvider() {
231         return getFacetsConfigurator();
232       }
233     };
234   }
235
236   @NotNull
237   private JPanel createPanel() {
238     getModifiableRootModel(); //initialize model if needed
239     getModifiableRootModelProxy();
240
241     myGenericSettingsPanel = new ModuleEditorPanel();
242
243     createEditors(getModule());
244
245     final JComponent component = createCenterPanel();
246     myGenericSettingsPanel.add(component, BorderLayout.CENTER);
247     myModificationOfImportedModelWarningComponent = new ModificationOfImportedModelWarningComponent();
248     myGenericSettingsPanel.add(myModificationOfImportedModelWarningComponent.getLabel(), BorderLayout.SOUTH);
249     updateImportedModelWarning();
250     myEditorsInitialized = true;
251     return myGenericSettingsPanel;
252   }
253
254   @NotNull
255   public JPanel getPanel() {
256     if (myGenericSettingsPanel == null) {
257       myGenericSettingsPanel = createPanel();
258     }
259
260     return myGenericSettingsPanel;
261   }
262
263   public void moduleCountChanged() {
264     updateOrderEntriesInEditors(false);
265   }
266
267   private void updateOrderEntriesInEditors(boolean forceInitEditors) {
268     if (getModule() != null) { //module with attached module libraries was deleted
269       if (myEditorsInitialized || forceInitEditors) {
270         getPanel();  //init editor if needed
271         for (final ModuleConfigurationEditor myEditor : myEditors) {
272           myEditor.moduleStateChanged();
273         }
274         updateImportedModelWarning();
275       }
276       myEventDispatcher.getMulticaster().moduleStateChanged(getModifiableRootModelProxy());
277     }
278   }
279
280   private void updateImportedModelWarning() {
281     if (!myEditorsInitialized) return;
282
283     ProjectModelExternalSource externalSource = ModuleRootManager.getInstance(myModule).getExternalSource();
284     if (externalSource != null && isModified()) {
285       myModificationOfImportedModelWarningComponent.showWarning("Module '" + myModule.getName() + "'", externalSource);
286     }
287     else {
288       myModificationOfImportedModelWarningComponent.hideWarning();
289     }
290   }
291
292   public void updateCompilerOutputPathChanged(String baseUrl, String moduleName){
293     if (myGenericSettingsPanel == null) return; //wasn't initialized yet
294     for (final ModuleConfigurationEditor myEditor : myEditors) {
295       if (myEditor instanceof ModuleElementsEditor) {
296         ((ModuleElementsEditor)myEditor).moduleCompileOutputChanged(baseUrl, moduleName);
297       }
298     }
299   }
300
301   @Override
302   public void dispose() {
303     try {
304       for (final ModuleConfigurationEditor myEditor : myEditors) {
305         myEditor.disposeUIResources();
306       }
307
308       myEditors.clear();
309
310       disposeCenterPanel();
311
312       if (myModifiableRootModel != null) {
313         myModifiableRootModel.dispose();
314       }
315
316       myGenericSettingsPanel = null;
317     }
318     finally {
319       resetModifiableModel();
320     }
321   }
322
323   public ModifiableRootModel apply() throws ConfigurationException {
324     for (ModuleConfigurationEditor editor : myEditors) {
325       editor.saveData();
326       editor.apply();
327     }
328     return myModifiableRootModel;
329   }
330
331   void resetModifiableModel() {
332     myModifiableRootModel = null;
333     myModifiableRootModelProxy = null;
334   }
335
336   public void canApply() throws ConfigurationException {
337     for (ModuleConfigurationEditor editor : myEditors) {
338       if (editor instanceof ModuleElementsEditor) {
339         ((ModuleElementsEditor)editor).canApply();
340       }
341     }
342   }
343
344   @NotNull
345   public String getName() {
346     return myName;
347   }
348
349   private class ModifiableRootModelInvocationHandler implements InvocationHandler, ProxyDelegateAccessor {
350     private final ModifiableRootModel myDelegateModel;
351     @NonNls private final Set<String> myCheckedNames = ContainerUtil
352       .set("addOrderEntry", "addLibraryEntry", "addInvalidLibrary", "addModuleOrderEntry", "addInvalidModuleEntry", "removeOrderEntry",
353            "setSdk", "inheritSdk", "inheritCompilerOutputPath", "setExcludeOutput", "replaceEntryOfType", "rearrangeOrderEntries");
354
355     ModifiableRootModelInvocationHandler(@NotNull ModifiableRootModel model) {
356       myDelegateModel = model;
357     }
358
359     @Override
360     public Object invoke(Object object, Method method, Object[] params) throws Throwable {
361       final boolean needUpdate = myCheckedNames.contains(method.getName());
362       try {
363         final Object result = method.invoke(myDelegateModel, unwrapParams(params));
364         if (result instanceof LibraryTable) {
365           return Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{LibraryTable.class},
366                                         new LibraryTableInvocationHandler((LibraryTable)result));
367         }
368         return result;
369       }
370       catch (InvocationTargetException e) {
371         throw e.getCause();
372       }
373       finally {
374         if (needUpdate) {
375           updateOrderEntriesInEditors(true);
376         }
377       }
378     }
379
380     @Override
381     public Object getDelegate() {
382       return myDelegateModel;
383     }
384   }
385
386   private class LibraryTableInvocationHandler implements InvocationHandler, ProxyDelegateAccessor {
387     private final LibraryTable myDelegateTable;
388     @NonNls private final Set<String> myCheckedNames = new THashSet<>(Collections.singletonList("removeLibrary" /*,"createLibrary"*/));
389
390     LibraryTableInvocationHandler(@NotNull LibraryTable table) {
391       myDelegateTable = table;
392     }
393
394     @Override
395     public Object invoke(Object object, Method method, Object[] params) throws Throwable {
396       final boolean needUpdate = myCheckedNames.contains(method.getName());
397       try {
398         final Object result = method.invoke(myDelegateTable, unwrapParams(params));
399         if (result instanceof Library) {
400           return Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{result instanceof LibraryEx ? LibraryEx.class : Library.class},
401                                         new LibraryInvocationHandler((Library)result));
402         }
403         if (result instanceof LibraryTable.ModifiableModel) {
404           return Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{LibraryTable.ModifiableModel.class},
405                                         new LibraryTableModelInvocationHandler((LibraryTable.ModifiableModel)result));
406         }
407         if (result instanceof Library[]) {
408           Library[] libraries = (Library[])result;
409           for (int idx = 0; idx < libraries.length; idx++) {
410             Library library = libraries[idx];
411             libraries[idx] =
412             (Library)Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{library instanceof LibraryEx ? LibraryEx.class : Library.class},
413                                             new LibraryInvocationHandler(library));
414           }
415         }
416         return result;
417       }
418       catch (InvocationTargetException e) {
419         throw e.getCause();
420       }
421       finally {
422         if (needUpdate) {
423           updateOrderEntriesInEditors(true);
424         }
425       }
426     }
427
428     @Override
429     public Object getDelegate() {
430       return myDelegateTable;
431     }
432   }
433
434   private class LibraryInvocationHandler implements InvocationHandler, ProxyDelegateAccessor {
435     private final Library myDelegateLibrary;
436
437     LibraryInvocationHandler(@NotNull Library delegateLibrary) {
438       myDelegateLibrary = delegateLibrary;
439     }
440
441     @Override
442     public Object invoke(Object object, Method method, Object[] params) throws Throwable {
443       try {
444         final Object result = method.invoke(myDelegateLibrary, unwrapParams(params));
445         if (result instanceof LibraryEx.ModifiableModelEx) {
446           return Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{LibraryEx.ModifiableModelEx.class},
447                                         new LibraryModifiableModelInvocationHandler((LibraryEx.ModifiableModelEx)result));
448         }
449         return result;
450       }
451       catch (InvocationTargetException e) {
452         throw e.getCause();
453       }
454     }
455
456     @Override
457     public Object getDelegate() {
458       return myDelegateLibrary;
459     }
460   }
461
462   private class LibraryModifiableModelInvocationHandler implements InvocationHandler, ProxyDelegateAccessor {
463     private final Library.ModifiableModel myDelegateModel;
464
465     LibraryModifiableModelInvocationHandler(@NotNull Library.ModifiableModel delegateModel) {
466       myDelegateModel = delegateModel;
467     }
468
469     @Override
470     public Object invoke(Object object, Method method, Object[] params) throws Throwable {
471       final boolean needUpdate = METHOD_COMMIT.equals(method.getName());
472       try {
473         return method.invoke(myDelegateModel, unwrapParams(params));
474       }
475       catch (InvocationTargetException e) {
476         throw e.getCause();
477       }
478       finally {
479         if (needUpdate) {
480           updateOrderEntriesInEditors(true);
481         }
482       }
483     }
484
485     @Override
486     public Object getDelegate() {
487       return myDelegateModel;
488     }
489   }
490
491   private class LibraryTableModelInvocationHandler implements InvocationHandler, ProxyDelegateAccessor {
492     private final LibraryTable.ModifiableModel myDelegateModel;
493
494     LibraryTableModelInvocationHandler(@NotNull LibraryTable.ModifiableModel delegateModel) {
495       myDelegateModel = delegateModel;
496     }
497
498     @Override
499     public Object invoke(Object object, Method method, Object[] params) throws Throwable {
500       final boolean needUpdate = METHOD_COMMIT.equals(method.getName());
501       try {
502         Object result = method.invoke(myDelegateModel, unwrapParams(params));
503         if (result instanceof Library[]) {
504           Library[] libraries = (Library[])result;
505           for (int idx = 0; idx < libraries.length; idx++) {
506             Library library = libraries[idx];
507             libraries[idx] =
508             (Library)Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{LibraryEx.class},
509                                             new LibraryInvocationHandler(library));
510           }
511         }
512         if (result instanceof Library) {
513           result =
514           Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{LibraryEx.class},
515                                  new LibraryInvocationHandler((Library)result));
516         }
517         return result;
518       }
519       catch (InvocationTargetException e) {
520         throw e.getCause();
521       }
522       finally {
523         if (needUpdate) {
524           updateOrderEntriesInEditors(true);
525         }
526       }
527     }
528
529     @Override
530     public Object getDelegate() {
531       return myDelegateModel;
532     }
533   }
534
535   public interface ProxyDelegateAccessor {
536     Object getDelegate();
537   }
538
539   private static Object[] unwrapParams(Object[] params) {
540     if (params == null || params.length == 0) {
541       return params;
542     }
543     final Object[] unwrappedParams = new Object[params.length];
544     for (int idx = 0; idx < params.length; idx++) {
545       Object param = params[idx];
546       if (param != null && Proxy.isProxyClass(param.getClass())) {
547         final InvocationHandler invocationHandler = Proxy.getInvocationHandler(param);
548         if (invocationHandler instanceof ProxyDelegateAccessor) {
549           param = ((ProxyDelegateAccessor)invocationHandler).getDelegate();
550         }
551       }
552       unwrappedParams[idx] = param;
553     }
554     return unwrappedParams;
555   }
556
557   @Nullable
558   public String getHelpTopic() {
559     if (myEditors.isEmpty()) {
560       return null;
561     }
562     final ModuleConfigurationEditor selectedEditor = getSelectedEditor();
563     return selectedEditor != null ? selectedEditor.getHelpTopic() : null;
564   }
565
566   public void setModuleName(@NotNull String name) {
567     myName = name;
568   }
569
570   private class ModuleEditorPanel extends JPanel implements DataProvider{
571     ModuleEditorPanel() {
572       super(new BorderLayout());
573     }
574
575     @Override
576     public Object getData(@NotNull String dataId) {
577       if (LangDataKeys.MODULE_CONTEXT.is(dataId)) {
578         return getModule();
579       }
580       return null;
581     }
582   }
583 }