optimize removing invalid files
[idea/community.git] / platform / projectModel-api / src / com / intellij / lang / PerFileMappingsBase.java
1 /*
2  * Copyright 2000-2015 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.lang;
17
18 import com.intellij.injected.editor.VirtualFileWindow;
19 import com.intellij.openapi.components.PersistentStateComponent;
20 import com.intellij.openapi.project.Project;
21 import com.intellij.openapi.project.ProjectManager;
22 import com.intellij.openapi.roots.impl.FilePropertyPusher;
23 import com.intellij.openapi.roots.impl.PushedFilePropertiesUpdater;
24 import com.intellij.openapi.util.Comparing;
25 import com.intellij.openapi.util.Key;
26 import com.intellij.openapi.vfs.VirtualFile;
27 import com.intellij.openapi.vfs.VirtualFileManager;
28 import com.intellij.psi.PsiDocumentManager;
29 import com.intellij.testFramework.LightVirtualFile;
30 import com.intellij.util.containers.ContainerUtil;
31 import gnu.trove.THashMap;
32 import org.jdom.Element;
33 import org.jetbrains.annotations.NotNull;
34 import org.jetbrains.annotations.Nullable;
35 import org.jetbrains.annotations.TestOnly;
36
37 import java.util.*;
38
39 /**
40  * @author gregsh
41  */
42 public abstract class PerFileMappingsBase<T> implements PersistentStateComponent<Element>, PerFileMappings<T> {
43   private final Map<VirtualFile, T> myMappings = ContainerUtil.newHashMap();
44
45   @Nullable
46   protected FilePropertyPusher<T> getFilePropertyPusher() {
47     return null;
48   }
49
50   @Nullable
51   protected Project getProject() { return null; }
52
53   @NotNull
54   @Override
55   public Map<VirtualFile, T> getMappings() {
56     synchronized (myMappings) {
57       cleanup();
58       return Collections.unmodifiableMap(myMappings);
59     }
60   }
61
62   private void cleanup() {
63     for (Iterator<VirtualFile> i = myMappings.keySet().iterator(); i.hasNext();) {
64       VirtualFile file = i.next();
65       if (file != null /* PROJECT, top-level */ && !file.isValid()) {
66         i.remove();
67       }
68     }
69   }
70
71   @Override
72   @Nullable
73   public T getMapping(@Nullable VirtualFile file) {
74     FilePropertyPusher<T> pusher = getFilePropertyPusher();
75     T t = getMappingInner(file, myMappings, pusher == null? null : pusher.getFileDataKey());
76     return t == null? getDefaultMapping(file) : t;
77   }
78
79   @Nullable
80   protected static <T> T getMappingInner(@Nullable VirtualFile file, @Nullable Map<VirtualFile, T> mappings, @Nullable Key<T> pusherKey) {
81     if (file instanceof VirtualFileWindow) {
82       final VirtualFileWindow window = (VirtualFileWindow)file;
83       file = window.getDelegate();
84     }
85     VirtualFile originalFile = file instanceof LightVirtualFile ? ((LightVirtualFile)file).getOriginalFile() : null;
86     if (Comparing.equal(originalFile, file)) originalFile = null;
87
88     if (file != null) {
89       final T pushedValue = pusherKey == null? null : file.getUserData(pusherKey);
90       if (pushedValue != null) return pushedValue;
91     }
92     if (originalFile != null) {
93       final T pushedValue = pusherKey == null? null : originalFile.getUserData(pusherKey);
94       if (pushedValue != null) return pushedValue;
95     }
96     if (mappings == null) return null;
97     synchronized (mappings) {
98       T t = getMappingForHierarchy(file, mappings);
99       if (t != null) return t;
100       t = getMappingForHierarchy(originalFile, mappings);
101       if (t != null) return t;
102       return mappings.get(null);
103     }
104   }
105
106   private static <T> T getMappingForHierarchy(@Nullable VirtualFile file, @NotNull Map<VirtualFile, T> mappings) {
107     for (VirtualFile cur = file; cur != null; cur = cur.getParent()) {
108       T t = mappings.get(cur);
109       if (t != null) return t;
110     }
111     return null;
112   }
113
114   @Override
115   public T chosenToStored(VirtualFile file, T value) {
116     return value;
117   }
118
119   @Override
120   public boolean isSelectable(T value) {
121     return true;
122   }
123
124   @Override
125   @Nullable
126   public T getDefaultMapping(@Nullable VirtualFile file) {
127     return null;
128   }
129
130   @Nullable
131   public T getImmediateMapping(@Nullable VirtualFile file) {
132     synchronized (myMappings) {
133       return myMappings.get(file);
134     }
135   }
136
137   @Override
138   public void setMappings(@NotNull final Map<VirtualFile, T> mappings) {
139     Collection<VirtualFile> oldFiles;
140     synchronized (myMappings) {
141       oldFiles = ContainerUtil.newArrayList(myMappings.keySet());
142       myMappings.clear();
143       myMappings.putAll(mappings);
144       cleanup();
145     }
146     Project project = getProject();
147     handleMappingChange(mappings.keySet(), oldFiles, project != null && !project.isDefault());
148   }
149
150   public void setMapping(@Nullable final VirtualFile file, @Nullable T dialect) {
151     synchronized (myMappings) {
152       if (dialect == null) {
153         myMappings.remove(file);
154       }
155       else {
156         myMappings.put(file, dialect);
157       }
158     }
159     List<VirtualFile> files = ContainerUtil.createMaybeSingletonList(file);
160     handleMappingChange(files, files, false);
161   }
162
163   private void handleMappingChange(Collection<VirtualFile> files, Collection<VirtualFile> oldFiles, boolean includeOpenFiles) {
164     Project project = getProject();
165     FilePropertyPusher<T> pusher = getFilePropertyPusher();
166     if (project != null && pusher != null) {
167       for (VirtualFile oldFile : oldFiles) {
168         if (oldFile == null) continue; // project
169         oldFile.putUserData(pusher.getFileDataKey(), null);
170       }
171       if (!project.isDefault()) {
172         PushedFilePropertiesUpdater.getInstance(project).pushAll(pusher);
173       }
174     }
175     if (shouldReparseFiles()) {
176       Project[] projects = project == null ? ProjectManager.getInstance().getOpenProjects() : new Project[] { project };
177       for (Project p : projects) {
178         PsiDocumentManager.getInstance(p).reparseFiles(files, includeOpenFiles);
179       }
180     }
181   }
182
183   @Override
184   public Collection<T> getAvailableValues(VirtualFile file) {
185     return getAvailableValues();
186   }
187
188   protected abstract List<T> getAvailableValues();
189
190   @Nullable
191   protected abstract String serialize(T t);
192
193   @Override
194   public Element getState() {
195     synchronized (myMappings) {
196       cleanup();
197       final Element element = new Element("x");
198       final List<VirtualFile> files = new ArrayList<VirtualFile>(myMappings.keySet());
199       Collections.sort(files, (o1, o2) -> {
200         if (o1 == null || o2 == null) return o1 == null ? o2 == null ? 0 : 1 : -1;
201         return o1.getPath().compareTo(o2.getPath());
202       });
203       for (VirtualFile file : files) {
204         final T dialect = myMappings.get(file);
205         String value = serialize(dialect);
206         if (value != null) {
207           final Element child = new Element("file");
208           element.addContent(child);
209           child.setAttribute("url", file == null ? "PROJECT" : file.getUrl());
210           child.setAttribute(getValueAttribute(), value);
211         }
212       }
213       return element;
214     }
215   }
216
217   @Nullable
218   protected T handleUnknownMapping(VirtualFile file, String value) {
219     return null;
220   }
221
222   @NotNull
223   protected String getValueAttribute() {
224     return "value";
225   }
226
227   @Override
228   public void loadState(final Element state) {
229     synchronized (myMappings) {
230       final THashMap<String, T> dialectMap = new THashMap<String, T>();
231       for (T dialect : getAvailableValues()) {
232         String key = serialize(dialect);
233         if (key != null) {
234           dialectMap.put(key, dialect);
235         }
236       }
237       myMappings.clear();
238       final List<Element> files = state.getChildren("file");
239       for (Element fileElement : files) {
240         final String url = fileElement.getAttributeValue("url");
241         final String dialectID = fileElement.getAttributeValue(getValueAttribute());
242         final VirtualFile file = url.equals("PROJECT") ? null : VirtualFileManager.getInstance().findFileByUrl(url);
243         T dialect = dialectMap.get(dialectID);
244         if (dialect == null) {
245           dialect = handleUnknownMapping(file, dialectID);
246           if (dialect == null) continue;
247         }
248         if (file != null || url.equals("PROJECT")) {
249           myMappings.put(file, dialect);
250         }
251       }
252     }
253   }
254
255   @TestOnly
256   public void cleanupForNextTest() {
257     synchronized (myMappings) {
258       myMappings.clear();
259     }
260   }
261
262   protected boolean shouldReparseFiles() {
263     return true;
264   }
265
266   public boolean hasMappings() {
267     synchronized (myMappings) {
268       return !myMappings.isEmpty();
269     }
270   }
271 }