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