cleanup
[idea/community.git] / platform / lang-impl / src / com / intellij / facet / FacetManagerImpl.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
3 package com.intellij.facet;
4
5 import com.intellij.facet.impl.FacetLoadingErrorDescription;
6 import com.intellij.facet.impl.FacetModelBase;
7 import com.intellij.facet.impl.FacetModelImpl;
8 import com.intellij.facet.impl.FacetUtil;
9 import com.intellij.facet.impl.invalid.InvalidFacet;
10 import com.intellij.facet.impl.invalid.InvalidFacetConfiguration;
11 import com.intellij.facet.impl.invalid.InvalidFacetManager;
12 import com.intellij.facet.impl.invalid.InvalidFacetType;
13 import com.intellij.openapi.application.ApplicationManager;
14 import com.intellij.openapi.components.PersistentStateComponent;
15 import com.intellij.openapi.components.State;
16 import com.intellij.openapi.diagnostic.Logger;
17 import com.intellij.openapi.module.Module;
18 import com.intellij.openapi.module.ModuleComponent;
19 import com.intellij.openapi.module.ModuleType;
20 import com.intellij.openapi.module.ProjectLoadingErrorsNotifier;
21 import com.intellij.openapi.project.Project;
22 import com.intellij.openapi.project.ProjectBundle;
23 import com.intellij.openapi.project.ProjectUtilCore;
24 import com.intellij.openapi.roots.ExternalProjectSystemRegistry;
25 import com.intellij.openapi.roots.ProjectModelExternalSource;
26 import com.intellij.openapi.updateSettings.impl.pluginsAdvertisement.UnknownFeaturesCollector;
27 import com.intellij.openapi.util.*;
28 import com.intellij.openapi.util.text.StringUtil;
29 import com.intellij.util.containers.ContainerUtil;
30 import com.intellij.util.messages.MessageBus;
31 import org.jdom.Element;
32 import org.jetbrains.annotations.NonNls;
33 import org.jetbrains.annotations.NotNull;
34 import org.jetbrains.annotations.Nullable;
35 import org.jetbrains.jps.model.serialization.facet.FacetManagerState;
36 import org.jetbrains.jps.model.serialization.facet.FacetState;
37
38 import java.util.*;
39 import java.util.function.Predicate;
40
41 /**
42  * @author nik
43  */
44 @State(name = FacetManagerImpl.COMPONENT_NAME, useLoadedStateAsExisting = false)
45 public final class FacetManagerImpl extends FacetManager implements ModuleComponent, PersistentStateComponent<FacetManagerState> {
46   private static final Logger LOG = Logger.getInstance("#com.intellij.facet.FacetManagerImpl");
47   @NonNls public static final String COMPONENT_NAME = "FacetManager";
48
49   private final Module myModule;
50   private final FacetManagerModel myModel = new FacetManagerModel();
51   private boolean myInsideCommit;
52   private final MessageBus myMessageBus;
53   private boolean myModuleAdded;
54   private final FacetFromExternalSourcesStorage myExternalSourcesStorage;
55
56   public FacetManagerImpl(final Module module, MessageBus messageBus, FacetFromExternalSourcesStorage externalSourcesStorage) {
57     myModule = module;
58     myMessageBus = messageBus;
59     //explicit dependency on FacetFromExternalSourcesStorage is required to ensure that it'll initialized and its settings will be stored on save
60     myExternalSourcesStorage = externalSourcesStorage;
61   }
62
63   @Override
64   @NotNull
65   public ModifiableFacetModel createModifiableModel() {
66     FacetModelImpl model = new FacetModelImpl(this);
67     model.addFacetsFromManager();
68     return model;
69   }
70
71   @Override
72   @NotNull
73   public Facet<?>[] getAllFacets() {
74     return myModel.getAllFacets();
75   }
76
77   @Override
78   @Nullable
79   public <F extends Facet<?>> F getFacetByType(FacetTypeId<F> typeId) {
80     return myModel.getFacetByType(typeId);
81   }
82
83   @Override
84   @Nullable
85   public <F extends Facet<?>> F findFacet(final FacetTypeId<F> type, final String name) {
86     return myModel.findFacet(type, name);
87   }
88
89   @Override
90   @Nullable
91   public <F extends Facet<?>> F getFacetByType(@NotNull final Facet<?> underlyingFacet, final FacetTypeId<F> typeId) {
92     return myModel.getFacetByType(underlyingFacet, typeId);
93   }
94
95   @Override
96   @NotNull
97   public <F extends Facet<?>> Collection<F> getFacetsByType(@NotNull final Facet<?> underlyingFacet, final FacetTypeId<F> typeId) {
98     return myModel.getFacetsByType(underlyingFacet, typeId);
99   }
100
101
102   @Override
103   @NotNull
104   public <F extends Facet<?>> Collection<F> getFacetsByType(FacetTypeId<F> typeId) {
105     return myModel.getFacetsByType(typeId);
106   }
107
108
109   @Override
110   @NotNull
111   public Facet<?>[] getSortedFacets() {
112     return myModel.getSortedFacets();
113   }
114
115   @Override
116   @NotNull
117   public String getFacetName(@NotNull Facet<?> facet) {
118     return myModel.getFacetName(facet);
119   }
120
121   @Override
122   @NotNull
123   public <F extends Facet<?>, C extends FacetConfiguration> F createFacet(@NotNull final FacetType<F, C> type, @NotNull final String name, @NotNull final C configuration,
124                                                                           @Nullable final Facet<?> underlying) {
125     final F facet = type.createFacet(myModule, name, configuration, underlying);
126     assertTrue(facet.getModule() == myModule, facet, "module");
127     assertTrue(facet.getConfiguration() == configuration, facet, "configuration");
128     assertTrue(Comparing.equal(facet.getName(), name), facet, "name");
129     assertTrue(facet.getUnderlyingFacet() == underlying, facet, "underlyingFacet");
130     return facet;
131   }
132
133   @Override
134   @NotNull
135   public <F extends Facet<?>, C extends FacetConfiguration> F createFacet(@NotNull final FacetType<F, C> type, @NotNull final String name, @Nullable final Facet<?> underlying) {
136     C configuration = ProjectFacetManager.getInstance(myModule.getProject()).createDefaultConfiguration(type);
137     return createFacet(type, name, configuration, underlying);
138   }
139
140   @Override
141   @NotNull
142   public <F extends Facet<?>, C extends FacetConfiguration> F addFacet(@NotNull final FacetType<F, C> type, @NotNull final String name, @Nullable final Facet<?> underlying) {
143     final ModifiableFacetModel model = createModifiableModel();
144     final F facet = createFacet(type, name, underlying);
145     model.addFacet(facet);
146     model.commit();
147     return facet;
148   }
149
150   private static void assertTrue(final boolean value, final Facet<?> facet, final String parameter) {
151     if (!value) {
152       LOG.error("Facet type " + facet.getType().getClass().getName() + " violates the contract of FacetType.createFacet method about '" +
153                 parameter + "' parameter");
154     }
155   }
156
157   private void addFacets(final List<? extends FacetState> facetStates, final Facet<?> underlyingFacet, ModifiableFacetModel model) {
158
159     FacetTypeRegistry registry = FacetTypeRegistry.getInstance();
160     for (FacetState child : facetStates) {
161       final String typeId = child.getFacetType();
162       if (typeId == null) {
163         addInvalidFacet(child, model, underlyingFacet, ProjectBundle.message("error.message.facet.type.isn.t.specified"));
164         continue;
165       }
166
167       final FacetType<?,?> type = registry.findFacetType(typeId);
168       if (type == null) {
169         addInvalidFacet(child, model, underlyingFacet, ProjectBundle.message("error.message.unknown.facet.type.0", typeId), typeId);
170         continue;
171       }
172
173       ModuleType<?> moduleType = ModuleType.get(myModule);
174       if (!type.isSuitableModuleType(moduleType)) {
175         addInvalidFacet(child, model, underlyingFacet, ProjectBundle.message("error.message.0.facets.are.not.allowed.in.1",
176                                                                       type.getPresentableName(), moduleType.getName()));
177         continue;
178       }
179
180       FacetType<?,?> expectedUnderlyingType = null;
181       FacetTypeId<?> underlyingTypeId = type.getUnderlyingFacetType();
182       if (underlyingTypeId != null) {
183         expectedUnderlyingType = registry.findFacetType(underlyingTypeId);
184       }
185       FacetType<?, ?> actualUnderlyingType = underlyingFacet != null ? underlyingFacet.getType() : null;
186       if (expectedUnderlyingType != null) {
187         if (!expectedUnderlyingType.equals(actualUnderlyingType)) {
188           addInvalidFacet(child, model, underlyingFacet, ProjectBundle.message("error.message.0.facet.must.be.placed.under.1.facet",
189                                                                         type.getPresentableName(),
190                                                                         expectedUnderlyingType.getPresentableName()));
191           continue;
192         }
193       }
194       else if (actualUnderlyingType != null) {
195         addInvalidFacet(child, model, underlyingFacet, ProjectBundle.message("error.message.0.cannot.be.placed.under.1",
196                                                                       type.getPresentableName(), actualUnderlyingType.getPresentableName()));
197         continue;
198       }
199
200       try {
201         addFacet(type, child, underlyingFacet, model);
202       }
203       catch (InvalidDataException e) {
204         LOG.info(e);
205         addInvalidFacet(child, model, underlyingFacet, ProjectBundle.message("error.message.cannot.load.facet.configuration.0", e.getMessage()));
206       }
207     }
208   }
209
210   private void addInvalidFacet(final FacetState state,
211                                ModifiableFacetModel model,
212                                final Facet<?> underlyingFacet,
213                                final String errorMessage) {
214     addInvalidFacet(state, model, underlyingFacet, errorMessage, null);
215   }
216
217   private void addInvalidFacet(final FacetState state,
218                                ModifiableFacetModel model,
219                                final Facet<?> underlyingFacet,
220                                final String errorMessage,
221                                final String typeId) {
222     final InvalidFacetManager invalidFacetManager = InvalidFacetManager.getInstance(myModule.getProject());
223     final InvalidFacetType type = InvalidFacetType.getInstance();
224     final InvalidFacetConfiguration configuration = new InvalidFacetConfiguration(state, errorMessage);
225     final InvalidFacet facet = createFacet(type, StringUtil.notNullize(state.getName()), configuration, underlyingFacet);
226     model.addFacet(facet);
227     if (!invalidFacetManager.isIgnored(facet)) {
228       FacetLoadingErrorDescription description = new FacetLoadingErrorDescription(facet);
229       ProjectLoadingErrorsNotifier.getInstance(myModule.getProject()).registerError(description);
230       if (typeId != null) {
231         UnknownFeaturesCollector.getInstance(myModule.getProject()).registerUnknownFeature("com.intellij.facetType", typeId, "Facet");
232       }
233     }
234   }
235
236   private <F extends Facet<C>, C extends FacetConfiguration> void addFacet(final FacetType<F, C> type, final FacetState state, final Facet<?> underlyingFacet,
237                                                                            final ModifiableFacetModel model) throws InvalidDataException {
238     Collection<F> facetsOfThisType = underlyingFacet == null ? model.getFacetsByType(type.getId())
239                                                              : model.getFacetsByType(underlyingFacet, type.getId());
240     if (type.isOnlyOneFacetAllowed() && !facetsOfThisType.isEmpty() && facetsOfThisType.stream().anyMatch(f -> !f.getName().equals(state.getName()))) {
241       LOG.info("'" + state.getName() + "' facet removed from module " + myModule.getName() + ", because only one "
242                + type.getPresentableName() + " facet allowed");
243       return;
244     }
245
246     F facet = null;
247     if (!facetsOfThisType.isEmpty() && ProjectUtilCore.isExternalStorageEnabled(myModule.getProject())) {
248       facet = facetsOfThisType.stream().filter(f -> f.getName().equals(state.getName())).findFirst().orElse(null);
249       if (facet != null) {
250         Element newConfiguration = state.getConfiguration();
251         //There may be two states of the same facet if configuration is stored in one file but configuration of its sub-facet is stored in another.
252         //In that case only one of the states will have the real configuration and we'll merge them here.
253         if (newConfiguration != null) {
254           FacetUtil.loadFacetConfiguration(facet.getConfiguration(), newConfiguration);
255         }
256       }
257     }
258
259     if (facet == null) {
260       final C configuration = type.createDefaultConfiguration();
261       final Element config = state.getConfiguration();
262       FacetUtil.loadFacetConfiguration(configuration, config);
263       String name = state.getName();
264       facet = createFacet(type, name, configuration, underlyingFacet);
265       if (facet instanceof JDOMExternalizable) {
266         //todo[nik] remove
267         ((JDOMExternalizable)facet).readExternal(config);
268       }
269       String externalSystemId = state.getExternalSystemId();
270       if (externalSystemId != null) {
271         facet.setExternalSource(ExternalProjectSystemRegistry.getInstance().getSourceById(externalSystemId));
272       }
273       model.addFacet(facet);
274     }
275     addFacets(state.getSubFacets(), facet, model);
276   }
277
278   @Override
279   public void noStateLoaded() {
280     doLoadState(null);
281   }
282
283   @Override
284   public void loadState(@NotNull FacetManagerState state) {
285     doLoadState(state);
286   }
287
288   protected void doLoadState(@Nullable FacetManagerState state) {
289     ModifiableFacetModel model = new FacetModelImpl(this);
290     FacetManagerState importedFacetsState = myExternalSourcesStorage.getLoadedState();
291     addFacets(ContainerUtil.concat(state == null ? Collections.emptyList() : state.getFacets(), importedFacetsState.getFacets()), null, model);
292     commit(model, false);
293   }
294
295   @Override
296   @NotNull
297   public FacetManagerState getState() {
298     return saveState(getImportedFacetPredicate(myModule.getProject()).negate());
299   }
300
301   @NotNull
302   static Predicate<Facet<?>> getImportedFacetPredicate(@NotNull Project project) {
303     if (ProjectUtilCore.isExternalStorageEnabled(project)) {
304       //we can store imported facets in a separate component only if that component will be stored separately, otherwise we will get modified *.iml files
305       return facet -> facet.getExternalSource() != null;
306     }
307     return facet -> false;
308   }
309
310   @NotNull
311   FacetManagerState saveState(Predicate<? super Facet<?>> filter) {
312     FacetManagerState managerState = new FacetManagerState();
313
314     final Facet<?>[] facets = getSortedFacets();
315
316     Map<Facet<?>, List<FacetState>> states = new HashMap<>();
317     states.put(null, managerState.getFacets());
318
319     for (Facet<?> facet : facets) {
320       if (!filter.test(facet)) continue;
321       final Facet<?> underlyingFacet = facet.getUnderlyingFacet();
322
323       FacetState facetState = createFacetState(facet, myModule.getProject());
324       if (!(facet instanceof InvalidFacet)) {
325         final Element config;
326         try {
327           FacetConfiguration configuration = facet.getConfiguration();
328           config = FacetUtil.saveFacetConfiguration(configuration);
329           if (facet instanceof JDOMExternalizable) {
330             //todo[nik] remove
331             ((JDOMExternalizable)facet).writeExternal(config);
332           }
333         }
334         catch (WriteExternalException e) {
335           continue;
336         }
337         facetState.setConfiguration(config);
338       }
339
340       getOrCreateTargetFacetList(underlyingFacet, states, myModule.getProject()).add(facetState);
341       states.put(facet, facetState.getSubFacets());
342     }
343     return managerState;
344   }
345
346   /**
347    * Configuration of some facet may be stored in one file, but configuration of its underlying facet may be stored in another file. For such
348    * sub-facets we create parent elements which don't store configuration but only name and type.
349    */
350   private static List<FacetState> getOrCreateTargetFacetList(Facet<?> underlyingFacet, Map<Facet<?>, List<FacetState>> states, @NotNull Project project) {
351     List<FacetState> facetStateList = states.get(underlyingFacet);
352     if (facetStateList == null) {
353       FacetState state = createFacetState(underlyingFacet, project);
354       getOrCreateTargetFacetList(underlyingFacet.getUnderlyingFacet(), states, project).add(state);
355       facetStateList = state.getSubFacets();
356       states.put(underlyingFacet, facetStateList);
357     }
358     return facetStateList;
359   }
360
361   private static FacetState createFacetState(@NotNull Facet<?> facet, @NotNull Project project) {
362     if (facet instanceof InvalidFacet) {
363       return ((InvalidFacet)facet).getConfiguration().getFacetState();
364     }
365     else {
366       FacetState facetState = new FacetState();
367       ProjectModelExternalSource externalSource = facet.getExternalSource();
368       if (externalSource != null && ProjectUtilCore.isExternalStorageEnabled(project)) {
369         //set this attribute only if such facets will be stored separately, otherwise we will get modified *.iml files
370         facetState.setExternalSystemId(externalSource.getId());
371       }
372       facetState.setFacetType(facet.getType().getStringId());
373       facetState.setName(facet.getName());
374       return facetState;
375     }
376   }
377
378
379   public void commit(final ModifiableFacetModel model) {
380     ApplicationManager.getApplication().assertWriteAccessAllowed();
381     commit(model, true);
382   }
383
384   private void commit(final ModifiableFacetModel model, final boolean fireEvents) {
385     LOG.assertTrue(!myInsideCommit, "Recursive commit");
386
387     Set<Facet<?>> toRemove = new HashSet<>(Arrays.asList(getAllFacets()));
388     List<Facet<?>> toAdd = new ArrayList<>();
389     List<FacetRenameInfo> toRename = new ArrayList<>();
390
391     final FacetManagerListener publisher = myMessageBus.syncPublisher(FACETS_TOPIC);
392
393     try {
394       myInsideCommit = true;
395
396       for (Facet<?> facet : model.getAllFacets()) {
397         boolean isNew = !toRemove.remove(facet);
398         if (isNew) {
399           toAdd.add(facet);
400         }
401       }
402
403       List<Facet<?>> newFacets = new ArrayList<>();
404       for (Facet<?> facet : getAllFacets()) {
405         if (!toRemove.contains(facet)) {
406           newFacets.add(facet);
407         }
408       }
409       newFacets.addAll(toAdd);
410
411       for (Facet<?> facet : newFacets) {
412         final String newName = model.getNewName(facet);
413         if (newName != null && !newName.equals(facet.getName())) {
414           toRename.add(new FacetRenameInfo(facet, facet.getName(), newName));
415         }
416       }
417
418       if (fireEvents) {
419         for (Facet<?> facet : toAdd) {
420           publisher.beforeFacetAdded(facet);
421         }
422         for (Facet<?> facet : toRemove) {
423           publisher.beforeFacetRemoved(facet);
424         }
425         for (FacetRenameInfo info : toRename) {
426           publisher.beforeFacetRenamed(info.myFacet);
427         }
428       }
429
430       for (FacetRenameInfo info : toRename) {
431         info.myFacet.setName(info.myNewName);
432       }
433       myModel.setAllFacets(newFacets.toArray(Facet.EMPTY_ARRAY));
434     }
435     finally {
436       myInsideCommit = false;
437     }
438
439     if (myModuleAdded) {
440       for (Facet<?> facet : toAdd) {
441         facet.initFacet();
442       }
443     }
444     for (Facet<?> facet : toRemove) {
445       Disposer.dispose(facet);
446     }
447
448     if (fireEvents) {
449       for (Facet<?> facet : toAdd) {
450         publisher.facetAdded(facet);
451       }
452       for (Facet<?> facet : toRemove) {
453         publisher.facetRemoved(facet);
454       }
455       for (FacetRenameInfo info : toRename) {
456         publisher.facetRenamed(info.myFacet, info.myOldName);
457       }
458     }
459     for (Facet<?> facet : toAdd) {
460       final Module module = facet.getModule();
461       if (!module.equals(myModule)) {
462         LOG.error(facet + " is created for module " + module + " but added to module " + myModule);
463       }
464       final FacetType<?,?> type = facet.getType();
465       if (type.isOnlyOneFacetAllowed()) {
466         if (type.getUnderlyingFacetType() == null) {
467           final Collection<?> facets = getFacetsByType(type.getId());
468           if (facets.size() > 1) {
469             LOG.error("Only one '" + type.getPresentableName() + "' facet per module allowed, but " + facets.size() + " facets found in module '" +
470                       myModule.getName() + "'");
471           }
472         }
473         else {
474           final Facet<?> underlyingFacet = facet.getUnderlyingFacet();
475           LOG.assertTrue(underlyingFacet != null, "Underlying facet is not specified for '" + facet.getName() + "'");
476           final Collection<?> facets = getFacetsByType(underlyingFacet, type.getId());
477           if (facets.size() > 1) {
478             LOG.error("Only one '" + type.getPresentableName() + "' facet per parent facet allowed, but " + facets.size() + " sub-facets found in facet " + underlyingFacet.getName());
479           }
480         }
481       }
482     }
483   }
484
485   public void setExternalSource(Facet<?> facet, ProjectModelExternalSource externalSource) {
486     facet.setExternalSource(externalSource);
487   }
488
489   Set<ProjectModelExternalSource> getExternalSources() {
490     return myModel.myExternalSources;
491   }
492
493   @Override
494   public void moduleAdded() {
495     if (myModuleAdded) return;
496
497     for (Facet<?> facet : getAllFacets()) {
498       facet.initFacet();
499     }
500     myModuleAdded = true;
501   }
502
503   private static class FacetManagerModel extends FacetModelBase {
504     private Facet<?>[] myAllFacets = Facet.EMPTY_ARRAY;
505     private final Set<ProjectModelExternalSource> myExternalSources = new LinkedHashSet<>();
506
507     @Override
508     @NotNull
509     public Facet<?>[] getAllFacets() {
510       return myAllFacets;
511     }
512
513     void setAllFacets(final Facet<?>[] allFacets) {
514       myExternalSources.clear();
515       for (Facet<?> facet : allFacets) {
516         ContainerUtil.addIfNotNull(myExternalSources, facet.getExternalSource());
517       }
518       myAllFacets = allFacets;
519       facetsChanged();
520     }
521   }
522
523   private static class FacetRenameInfo {
524     private final Facet<?> myFacet;
525     private final String myOldName;
526     private final String myNewName;
527
528     FacetRenameInfo(final Facet<?> facet, final String oldName, final String newName) {
529       myFacet = facet;
530       myOldName = oldName;
531       myNewName = newName;
532     }
533   }
534 }