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