Merge remote-tracking branch 'origin/master' into amakeev/cmake-iml
[idea/community.git] / platform / lang-impl / src / com / intellij / openapi / roots / impl / storage / ClasspathStorage.java
1 /*
2  * Copyright 2000-2016 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.roots.impl.storage;
17
18 import com.intellij.application.options.PathMacrosCollector;
19 import com.intellij.configurationStore.StateStorageBase;
20 import com.intellij.ide.highlighter.ModuleFileType;
21 import com.intellij.notification.Notification;
22 import com.intellij.notification.NotificationType;
23 import com.intellij.openapi.application.AccessToken;
24 import com.intellij.openapi.application.ReadAction;
25 import com.intellij.openapi.components.ComponentManager;
26 import com.intellij.openapi.components.StateStorage;
27 import com.intellij.openapi.components.TrackingPathMacroSubstitutor;
28 import com.intellij.openapi.components.impl.stores.StateStorageManager;
29 import com.intellij.openapi.components.impl.stores.StorageManagerListener;
30 import com.intellij.openapi.components.impl.stores.StorageUtil;
31 import com.intellij.openapi.diagnostic.Logger;
32 import com.intellij.openapi.module.Module;
33 import com.intellij.openapi.module.ModuleUtilCore;
34 import com.intellij.openapi.roots.ModifiableRootModel;
35 import com.intellij.openapi.roots.ModuleRootModel;
36 import com.intellij.openapi.roots.impl.ModuleRootManagerImpl;
37 import com.intellij.openapi.roots.impl.ModuleRootManagerImpl.ModuleRootManagerState;
38 import com.intellij.openapi.roots.impl.RootModelImpl;
39 import com.intellij.openapi.util.Key;
40 import com.intellij.openapi.util.SystemInfo;
41 import com.intellij.openapi.util.WriteExternalException;
42 import com.intellij.openapi.util.io.FileUtil;
43 import com.intellij.openapi.vfs.VirtualFile;
44 import com.intellij.openapi.vfs.VirtualFileManager;
45 import com.intellij.openapi.vfs.newvfs.BulkFileListener;
46 import com.intellij.openapi.vfs.newvfs.events.VFileContentChangeEvent;
47 import com.intellij.openapi.vfs.newvfs.events.VFileEvent;
48 import com.intellij.openapi.vfs.newvfs.events.VFilePropertyChangeEvent;
49 import com.intellij.util.messages.MessageBusConnection;
50 import org.jdom.Element;
51 import org.jetbrains.annotations.NotNull;
52 import org.jetbrains.annotations.Nullable;
53 import org.jetbrains.jps.model.serialization.JpsProjectLoader;
54
55 import java.io.IOException;
56 import java.util.Collections;
57 import java.util.List;
58 import java.util.Set;
59
60 // Boolean - false as not loaded, true as loaded
61 public final class ClasspathStorage extends StateStorageBase<Boolean> {
62   private static final Key<Boolean> ERROR_NOTIFIED_KEY =Key.create("ClasspathStorage.ERROR_NOTIFIED_KEY"); 
63   private static final Logger LOG = Logger.getInstance(ClasspathStorage.class);
64
65   private final ClasspathStorageProvider.ClasspathConverter myConverter;
66
67   private final TrackingPathMacroSubstitutor myPathMacroSubstitutor;
68
69   public ClasspathStorage(@NotNull final Module module, @NotNull StateStorageManager storageManager) {
70     String storageType = module.getOptionValue(JpsProjectLoader.CLASSPATH_ATTRIBUTE);
71     if (storageType == null) {
72       throw new IllegalStateException("Classpath storage requires non-default storage type");
73     }
74
75     ClasspathStorageProvider provider = getProvider(storageType);
76     if (provider == null) {
77       if (module.getUserData(ERROR_NOTIFIED_KEY) == null) {
78         Notification n = new Notification(StorageUtil.NOTIFICATION_GROUP_ID, "Cannot load module '" + module.getName() + "'",
79                                           "Support for " + storageType + " format is not installed.", NotificationType.ERROR);
80         n.notify(module.getProject());
81         module.putUserData(ERROR_NOTIFIED_KEY, Boolean.TRUE);
82         LOG.info("Classpath storage provider " + storageType + " not found");
83       }
84       
85       myConverter = new MissingClasspathConverter();
86     } else {
87       myConverter = provider.createConverter(module);
88     }
89
90     myPathMacroSubstitutor = storageManager.getMacroSubstitutor();
91
92     final List<String> paths = myConverter.getFilePaths();
93     MessageBusConnection busConnection = module.getMessageBus().connect();
94     busConnection.subscribe(VirtualFileManager.VFS_CHANGES, new BulkFileListener.Adapter() {
95       @Override
96       public void after(@NotNull List<? extends VFileEvent> events) {
97         for (VFileEvent event : events) {
98           if (!event.isFromRefresh() || !(event instanceof VFileContentChangeEvent)) {
99             continue;
100           }
101
102           for (String path : paths) {
103             if (path.equals(event.getPath())) {
104               module.getMessageBus().syncPublisher(StateStorageManager.STORAGE_TOPIC).storageFileChanged(event, ClasspathStorage.this, module);
105               return;
106             }
107           }
108         }
109       }
110     });
111
112     busConnection.subscribe(StateStorageManager.STORAGE_TOPIC, new StorageManagerListener() {
113       private String fileNameToModuleName(@NotNull String fileName) {
114         return fileName.substring(0, fileName.length() - ModuleFileType.DOT_DEFAULT_EXTENSION.length());
115       }
116
117       @Override
118       public void storageFileChanged(@NotNull VFileEvent event, @NotNull StateStorage storage, @NotNull ComponentManager componentManager) {
119         assert componentManager == module;
120         if (!(event instanceof VFilePropertyChangeEvent)) {
121           return;
122         }
123
124         VFilePropertyChangeEvent propertyEvent = (VFilePropertyChangeEvent)event;
125         if (propertyEvent.getPropertyName().equals(VirtualFile.PROP_NAME)) {
126           String oldFileName = (String)propertyEvent.getOldValue();
127           if (oldFileName.endsWith(ModuleFileType.DOT_DEFAULT_EXTENSION)) {
128             ClasspathStorageProvider provider = getProvider(ClassPathStorageUtil.getStorageType(module));
129             if (provider != null) {
130               provider.moduleRenamed(module, fileNameToModuleName(oldFileName), fileNameToModuleName((String)propertyEvent.getNewValue()));
131             }
132           }
133         }
134       }
135     });
136   }
137
138   @Nullable
139   @Override
140   public <S> S deserializeState(@Nullable Element serializedState, @NotNull Class<S> stateClass, @Nullable S mergeInto) {
141     if (serializedState == null) {
142       return null;
143     }
144
145     ModuleRootManagerState state = new ModuleRootManagerState();
146     state.readExternal(serializedState);
147     //noinspection unchecked
148     return (S)state;
149   }
150
151   @Override
152   protected boolean hasState(@NotNull Boolean storageData, @NotNull String componentName) {
153     return !storageData;
154   }
155
156   @Nullable
157   @Override
158   public Element getSerializedState(@NotNull Boolean storageData, Object component, @NotNull String componentName, boolean archive) {
159     if (storageData) {
160       return null;
161     }
162
163     Element element = new Element("component");
164     ModifiableRootModel model = null;
165     AccessToken token = ReadAction.start();
166     try {
167       model = ((ModuleRootManagerImpl)component).getModifiableModel();
168       // IDEA-137969 Eclipse integration: external remove of classpathentry is not synchronized
169       model.clear();
170       try {
171         myConverter.readClasspath(model);
172       }
173       catch (IOException e) {
174         throw new RuntimeException(e);
175       }
176       ((RootModelImpl)model).writeExternal(element);
177     }
178     catch (WriteExternalException e) {
179       LOG.error(e);
180     }
181     finally {
182       try {
183         token.finish();
184       }
185       finally {
186         if (model != null) {
187           model.dispose();
188         }
189       }
190     }
191
192     if (myPathMacroSubstitutor != null) {
193       myPathMacroSubstitutor.expandPaths(element);
194       myPathMacroSubstitutor.addUnknownMacros("NewModuleRootManager", PathMacrosCollector.getMacroNames(element));
195     }
196
197     getStorageDataRef().set(true);
198     return element;
199   }
200
201   @NotNull
202   @Override
203   protected Boolean loadData() {
204     return false;
205   }
206
207   @Override
208   @NotNull
209   public ExternalizationSession startExternalization() {
210     return myConverter.startExternalization();
211   }
212
213   @Override
214   public void analyzeExternalChangesAndUpdateIfNeed(@NotNull Set<String> componentNames) {
215     // if some file changed, so, changed
216     componentNames.add("NewModuleRootManager");
217     getStorageDataRef().set(false);
218   }
219
220   @Nullable
221   public static ClasspathStorageProvider getProvider(@NotNull String type) {
222     if (type.equals(ClassPathStorageUtil.DEFAULT_STORAGE)) {
223       return null;
224     }
225
226     for (ClasspathStorageProvider provider : ClasspathStorageProvider.EXTENSION_POINT_NAME.getExtensions()) {
227       if (type.equals(provider.getID())) {
228         return provider;
229       }
230     }
231     return null;
232   }
233
234   @NotNull
235   public static String getStorageRootFromOptions(@NotNull Module module) {
236     String moduleRoot = ModuleUtilCore.getModuleDirPath(module);
237     String storageRef = module.getOptionValue(JpsProjectLoader.CLASSPATH_DIR_ATTRIBUTE);
238     if (storageRef == null) {
239       return moduleRoot;
240     }
241
242     storageRef = FileUtil.toSystemIndependentName(storageRef);
243     if (SystemInfo.isWindows ? FileUtil.isAbsolutePlatformIndependent(storageRef) : FileUtil.isUnixAbsolutePath(storageRef)) {
244       return storageRef;
245     }
246     else {
247       return moduleRoot + '/' + storageRef;
248     }
249   }
250
251   public static void setStorageType(@NotNull ModuleRootModel model, @NotNull String storageId) {
252     Module module = model.getModule();
253     String oldStorageType = ClassPathStorageUtil.getStorageType(module);
254     if (oldStorageType.equals(storageId)) {
255       return;
256     }
257
258     ClasspathStorageProvider provider = getProvider(oldStorageType);
259     if (provider != null) {
260       provider.detach(module);
261     }
262
263     provider = getProvider(storageId);
264     if (provider == null) {
265       module.clearOption(JpsProjectLoader.CLASSPATH_ATTRIBUTE);
266       module.clearOption(JpsProjectLoader.CLASSPATH_DIR_ATTRIBUTE);
267     }
268     else {
269       module.setOption(JpsProjectLoader.CLASSPATH_ATTRIBUTE, storageId);
270       String root = provider.getContentRoot(model);
271       if (root == null) {
272         module.clearOption(JpsProjectLoader.CLASSPATH_DIR_ATTRIBUTE);
273       }
274       else {
275         module.setOption(JpsProjectLoader.CLASSPATH_DIR_ATTRIBUTE, root);
276       }
277     }
278   }
279
280   public static void modulePathChanged(Module module, String newPath) {
281     ClasspathStorageProvider provider = getProvider(ClassPathStorageUtil.getStorageType(module));
282     if (provider != null) {
283       provider.modulePathChanged(module, newPath);
284     }
285   }
286
287   private static class MissingClasspathConverter implements ClasspathStorageProvider.ClasspathConverter {
288     @NotNull
289     @Override
290     public List<String> getFilePaths() {
291       return Collections.emptyList();
292     }
293
294     @NotNull
295     @Override
296     public ExternalizationSession startExternalization() {
297       return new ExternalizationSession() {
298         @Override
299         public void setState(@Nullable Object component, @NotNull String componentName, @NotNull Object state) {
300         }
301
302         @Nullable
303         @Override
304         public SaveSession createSaveSession() {
305           return null;
306         }
307       };
308     }
309
310     @Override
311     public void readClasspath(@NotNull ModifiableRootModel model) throws IOException {
312     }
313   }
314 }