87f784a97498f26186419fb51360ecf427b10ac4
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / vfs / impl / VirtualFilePointerManagerImpl.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.openapi.vfs.impl;
17
18 import com.intellij.openapi.Disposable;
19 import com.intellij.openapi.application.ApplicationManager;
20 import com.intellij.openapi.components.ApplicationComponent;
21 import com.intellij.openapi.diagnostic.Logger;
22 import com.intellij.openapi.util.*;
23 import com.intellij.openapi.util.io.FileUtil;
24 import com.intellij.openapi.util.text.StringUtil;
25 import com.intellij.openapi.vfs.*;
26 import com.intellij.openapi.vfs.ex.temp.TempFileSystem;
27 import com.intellij.openapi.vfs.newvfs.BulkFileListener;
28 import com.intellij.openapi.vfs.newvfs.events.*;
29 import com.intellij.openapi.vfs.newvfs.persistent.PersistentFS;
30 import com.intellij.openapi.vfs.pointers.VirtualFilePointer;
31 import com.intellij.openapi.vfs.pointers.VirtualFilePointerContainer;
32 import com.intellij.openapi.vfs.pointers.VirtualFilePointerListener;
33 import com.intellij.openapi.vfs.pointers.VirtualFilePointerManager;
34 import com.intellij.util.ConcurrencyUtil;
35 import com.intellij.util.SmartList;
36 import com.intellij.util.containers.ContainerUtil;
37 import com.intellij.util.io.URLUtil;
38 import com.intellij.util.messages.MessageBus;
39 import gnu.trove.THashMap;
40 import gnu.trove.TObjectIntHashMap;
41 import org.jetbrains.annotations.NonNls;
42 import org.jetbrains.annotations.NotNull;
43 import org.jetbrains.annotations.Nullable;
44 import org.jetbrains.annotations.TestOnly;
45
46 import java.util.*;
47 import java.util.concurrent.ConcurrentMap;
48
49 public class VirtualFilePointerManagerImpl extends VirtualFilePointerManager implements ApplicationComponent, ModificationTracker, BulkFileListener {
50   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vfs.impl.VirtualFilePointerManagerImpl");
51   private final TempFileSystem TEMP_FILE_SYSTEM;
52   private final LocalFileSystem LOCAL_FILE_SYSTEM;
53   private final JarFileSystem JAR_FILE_SYSTEM;
54   // guarded by this
55   private final Map<VirtualFilePointerListener, FilePointerPartNode> myPointers = new LinkedHashMap<>();
56
57   // compare by identity because VirtualFilePointerContainer has too smart equals
58   // guarded by myContainers
59   private final Set<VirtualFilePointerContainerImpl> myContainers = ContainerUtil.newIdentityTroveSet();
60   @NotNull private final VirtualFileManager myVirtualFileManager;
61   @NotNull private final MessageBus myBus;
62   private static final Comparator<String> URL_COMPARATOR = SystemInfo.isFileSystemCaseSensitive ? String::compareTo : String::compareToIgnoreCase;
63
64   VirtualFilePointerManagerImpl(@NotNull VirtualFileManager virtualFileManager,
65                                 @NotNull MessageBus bus,
66                                 @NotNull TempFileSystem tempFileSystem,
67                                 @NotNull LocalFileSystem localFileSystem,
68                                 @NotNull JarFileSystem jarFileSystem) {
69     myVirtualFileManager = virtualFileManager;
70     myBus = bus;
71     bus.connect().subscribe(VirtualFileManager.VFS_CHANGES, this);
72     TEMP_FILE_SYSTEM = tempFileSystem;
73     LOCAL_FILE_SYSTEM = localFileSystem;
74     JAR_FILE_SYSTEM = jarFileSystem;
75   }
76
77   @Override
78   public void initComponent() {
79   }
80
81   @Override
82   public void disposeComponent() {
83     assertAllPointersDisposed();
84   }
85
86   @NotNull
87   @Override
88   public String getComponentName() {
89     return "VirtualFilePointerManager";
90   }
91
92   private static class EventDescriptor {
93     @NotNull private final VirtualFilePointerListener myListener;
94     @NotNull private final VirtualFilePointer[] myPointers;
95
96     private EventDescriptor(@NotNull VirtualFilePointerListener listener, @NotNull VirtualFilePointer[] pointers) {
97       myListener = listener;
98       myPointers = pointers;
99     }
100
101     private void fireBefore() {
102       if (myPointers.length != 0) {
103         myListener.beforeValidityChanged(myPointers);
104       }
105     }
106
107     private void fireAfter() {
108       if (myPointers.length != 0) {
109         myListener.validityChanged(myPointers);
110       }
111     }
112   }
113
114   @NotNull
115   private static VirtualFilePointer[] toPointers(@NotNull List<FilePointerPartNode> nodes) {
116     if (nodes.isEmpty()) return VirtualFilePointer.EMPTY_ARRAY;
117     List<VirtualFilePointer> list = new ArrayList<>(nodes.size());
118     for (FilePointerPartNode node : nodes) {
119       node.addAllPointersTo(list);
120     }
121     return list.toArray(new VirtualFilePointer[list.size()]);
122   }
123
124   @TestOnly
125   VirtualFilePointer[] getPointersUnder(VirtualFile parent, String childName) {
126     List<FilePointerPartNode> nodes = new ArrayList<>();
127     addPointersUnder(parent, true, childName, nodes);
128     return toPointers(nodes);
129   }
130
131   private void addPointersUnder(VirtualFile parent,
132                                 boolean separator,
133                                 @NotNull CharSequence childName,
134                                 @NotNull List<FilePointerPartNode> out) {
135     for (FilePointerPartNode root : myPointers.values()) {
136       root.addPointersUnder(parent, separator, childName, out);
137     }
138   }
139
140   @Override
141   @NotNull
142   public synchronized VirtualFilePointer create(@NotNull String url, @NotNull Disposable parent, @Nullable VirtualFilePointerListener listener) {
143     return create(null, url, parent, listener);
144   }
145
146   @Override
147   @NotNull
148   public synchronized VirtualFilePointer create(@NotNull VirtualFile file, @NotNull Disposable parent, @Nullable VirtualFilePointerListener listener) {
149     return create(file, null, parent, listener);
150   }
151
152   @NotNull
153   private VirtualFilePointer create(@Nullable("null means the pointer will be created from the (not null) url") VirtualFile file,
154                                     @Nullable("null means url has to be computed from the (not-null) file path") String url,
155                                     @NotNull Disposable parentDisposable,
156                                     @Nullable VirtualFilePointerListener listener) {
157     VirtualFileSystem fileSystem;
158     String protocol;
159     String path;
160     if (file == null) {
161       int protocolEnd = url.indexOf(URLUtil.SCHEME_SEPARATOR);
162       if (protocolEnd == -1) {
163         protocol = null;
164         fileSystem = null;
165         path = url;
166       }
167       else {
168         protocol = url.substring(0, protocolEnd);
169         fileSystem = myVirtualFileManager.getFileSystem(protocol);
170         path = url.substring(protocolEnd + URLUtil.SCHEME_SEPARATOR.length());
171       }
172     }
173     else {
174       fileSystem = file.getFileSystem();
175       protocol = fileSystem.getProtocol();
176       path = file.getPath();
177       url = VirtualFileManager.constructUrl(protocol, path);
178     }
179
180     if (fileSystem == TEMP_FILE_SYSTEM) {
181       // for tests, recreate always
182       VirtualFile found = file == null ? VirtualFileManager.getInstance().findFileByUrl(url) : file;
183       return new IdentityVirtualFilePointer(found, url);
184     }
185
186     boolean isJar = fileSystem == JAR_FILE_SYSTEM;
187     if (fileSystem != LOCAL_FILE_SYSTEM && !isJar) {
188       // we are unable to track alien file systems for now
189       VirtualFile found = fileSystem == null ? null : file != null ? file : VirtualFileManager.getInstance().findFileByUrl(url);
190       // if file is null, this pointer will never be alive
191       return getOrCreateIdentity(url, found);
192     }
193
194     if (file == null) {
195       String cleanPath = cleanupPath(path, isJar);
196       // if newly created path is the same as substringed from url one then the url did not change, we can reuse it
197       //noinspection StringEquality
198       if (cleanPath != path) {
199         url = VirtualFileManager.constructUrl(protocol, cleanPath);
200         path = cleanPath;
201       }
202       if (url.contains("..")) {
203         // the url of the form "/x/../y" should resolve to "/y" (or something else in the case of symlinks)
204         file = VirtualFileManager.getInstance().findFileByUrl(url);
205         if (file != null) {
206           url = file.getUrl();
207           path = file.getPath();
208         }
209       }
210     }
211     // else url has come from VirtualFile.getPath() and is good enough
212
213     VirtualFilePointerImpl pointer = getOrCreate(parentDisposable, listener, path, Pair.create(file, url));
214     DelegatingDisposable.registerDisposable(parentDisposable, pointer);
215     return pointer;
216   }
217
218   private final Map<String, IdentityVirtualFilePointer> myUrlToIdentity = new THashMap<>();
219   @NotNull
220   private IdentityVirtualFilePointer getOrCreateIdentity(@NotNull String url, @Nullable VirtualFile found) {
221     return myUrlToIdentity.computeIfAbsent(url, __ -> new IdentityVirtualFilePointer(found, url));
222   }
223
224   @NotNull
225   private static String cleanupPath(@NotNull String path, boolean isJar) {
226     path = FileUtil.normalize(path);
227     path = trimTrailingSeparators(path, isJar);
228     return path;
229   }
230
231   private static String trimTrailingSeparators(@NotNull String path, boolean isJar) {
232     while (StringUtil.endsWithChar(path, '/') && !(isJar && path.endsWith(JarFileSystem.JAR_SEPARATOR))) {
233       path = StringUtil.trimEnd(path, "/");
234     }
235     return path;
236   }
237
238   @NotNull
239   private VirtualFilePointerImpl getOrCreate(@NotNull Disposable parentDisposable,
240                                              @Nullable VirtualFilePointerListener listener,
241                                              @NotNull String path,
242                                              @NotNull Pair<VirtualFile, String> fileAndUrl) {
243     FilePointerPartNode root = myPointers.get(listener);
244     FilePointerPartNode node;
245     if (root == null) {
246       root = new FilePointerPartNode(path, null, fileAndUrl);
247       root.pointersUnder++;
248       myPointers.put(listener, root);
249       node = root;
250     }
251     else {
252       node = root.findPointerOrCreate(path, 0, fileAndUrl, 1);
253     }
254
255     VirtualFilePointerImpl pointer = node.getAnyPointer();
256     if (pointer == null) {
257       pointer = new VirtualFilePointerImpl(listener, parentDisposable, fileAndUrl);
258       node.associate(pointer, fileAndUrl);
259     }
260     pointer.myNode.incrementUsageCount(1);
261
262     root.checkConsistency();
263     return pointer;
264   }
265
266   @Override
267   @NotNull
268   public synchronized VirtualFilePointer duplicate(@NotNull VirtualFilePointer pointer,
269                                                    @NotNull Disposable parent,
270                                                    @Nullable VirtualFilePointerListener listener) {
271     VirtualFile file = pointer.getFile();
272     return file == null ? create(pointer.getUrl(), parent, listener) : create(file, parent, listener);
273   }
274
275   private synchronized void assertAllPointersDisposed() {
276     for (Map.Entry<VirtualFilePointerListener, FilePointerPartNode> entry : myPointers.entrySet()) {
277       FilePointerPartNode root = entry.getValue();
278       List<FilePointerPartNode> left = new ArrayList<>();
279       root.addPointersUnder(null, false, "", left);
280       List<VirtualFilePointerImpl> pointers = new ArrayList<>();
281       for (FilePointerPartNode node : left) {
282         node.addAllPointersTo(pointers);
283       }
284       if (!pointers.isEmpty()) {
285         VirtualFilePointerImpl p = pointers.get(0);
286         try {
287           p.throwDisposalError("Not disposed pointer: "+p);
288         }
289         finally {
290           for (VirtualFilePointerImpl pointer : pointers) {
291             pointer.dispose();
292           }
293         }
294       }
295     }
296
297     synchronized (myContainers) {
298       if (!myContainers.isEmpty()) {
299         VirtualFilePointerContainerImpl container = myContainers.iterator().next();
300         container.throwDisposalError("Not disposed container");
301       }
302     }
303   }
304
305   private final Set<VirtualFilePointerImpl> myStoredPointers = ContainerUtil.newIdentityTroveSet();
306
307   @TestOnly
308   public void storePointers() {
309     myStoredPointers.clear();
310     addAllPointersTo(myStoredPointers);
311   }
312
313   @TestOnly
314   public void assertPointersAreDisposed() {
315     List<VirtualFilePointerImpl> pointers = new ArrayList<>();
316     addAllPointersTo(pointers);
317     try {
318       for (VirtualFilePointerImpl pointer : pointers) {
319         if (!myStoredPointers.contains(pointer)) {
320           pointer.throwDisposalError("Virtual pointer hasn't been disposed: "+pointer);
321         }
322       }
323     }
324     finally {
325       myStoredPointers.clear();
326     }
327   }
328
329   @TestOnly
330   private void addAllPointersTo(@NotNull Collection<VirtualFilePointerImpl> pointers) {
331     List<FilePointerPartNode> out = new ArrayList<>();
332     for (FilePointerPartNode root : myPointers.values()) {
333       root.addPointersUnder(null, false, "", out);
334     }
335     for (FilePointerPartNode node : out) {
336       node.addAllPointersTo(pointers);
337     }
338   }
339
340   @Override
341   public void dispose() {
342   }
343
344   @Override
345   @NotNull
346   public VirtualFilePointerContainer createContainer(@NotNull Disposable parent) {
347     return createContainer(parent, null);
348   }
349
350   @Override
351   @NotNull
352   public synchronized VirtualFilePointerContainer createContainer(@NotNull Disposable parent, @Nullable VirtualFilePointerListener listener) {
353     return registerContainer(parent, new VirtualFilePointerContainerImpl(this, parent, listener));
354   }
355
356   @NotNull
357   private VirtualFilePointerContainer registerContainer(@NotNull Disposable parent, @NotNull final VirtualFilePointerContainerImpl virtualFilePointerContainer) {
358     synchronized (myContainers) {
359       myContainers.add(virtualFilePointerContainer);
360     }
361     Disposer.register(parent, new Disposable() {
362       @Override
363       public void dispose() {
364         Disposer.dispose(virtualFilePointerContainer);
365         boolean removed;
366         synchronized (myContainers) {
367           removed = myContainers.remove(virtualFilePointerContainer);
368         }
369         if (!ApplicationManager.getApplication().isUnitTestMode()) {
370           assert removed;
371         }
372       }
373
374       @Override
375       @NonNls
376       @NotNull
377       public String toString() {
378         return "Disposing container " + virtualFilePointerContainer;
379       }
380     });
381     return virtualFilePointerContainer;
382   }
383
384   private List<EventDescriptor> myEvents = Collections.emptyList();
385   private List<FilePointerPartNode> myNodesToUpdateUrl = Collections.emptyList();
386   private List<FilePointerPartNode> myNodesToFire = Collections.emptyList();
387
388   @Override
389   public void before(@NotNull final List<? extends VFileEvent> events) {
390     List<FilePointerPartNode> toFireEvents = new ArrayList<>();
391     List<FilePointerPartNode> toUpdateUrl = new ArrayList<>();
392     VirtualFilePointer[] toFirePointers;
393
394     synchronized (this) {
395       incModificationCount();
396       for (VFileEvent event : events) {
397         if (event instanceof VFileDeleteEvent) {
398           final VFileDeleteEvent deleteEvent = (VFileDeleteEvent)event;
399           addPointersUnder(deleteEvent.getFile(), false, "", toFireEvents);
400
401         }
402         else if (event instanceof VFileCreateEvent) {
403           final VFileCreateEvent createEvent = (VFileCreateEvent)event;
404           addPointersUnder(createEvent.getParent(), true, createEvent.getChildName(), toFireEvents);
405         }
406         else if (event instanceof VFileCopyEvent) {
407           final VFileCopyEvent copyEvent = (VFileCopyEvent)event;
408           addPointersUnder(copyEvent.getNewParent(), true, copyEvent.getFile().getName(), toFireEvents);
409         }
410         else if (event instanceof VFileMoveEvent) {
411           final VFileMoveEvent moveEvent = (VFileMoveEvent)event;
412           VirtualFile eventFile = moveEvent.getFile();
413           addPointersUnder(moveEvent.getNewParent(), true, eventFile.getName(), toFireEvents);
414
415           List<FilePointerPartNode> nodes = new ArrayList<>();
416           addPointersUnder(eventFile, false, "", nodes);
417           for (FilePointerPartNode node : nodes) {
418             VirtualFilePointerImpl pointer = node.getAnyPointer();
419             VirtualFile file = pointer == null ? null : pointer.getFile();
420             if (file != null) {
421               toUpdateUrl.add(node);
422             }
423           }
424         }
425         else if (event instanceof VFilePropertyChangeEvent) {
426           final VFilePropertyChangeEvent change = (VFilePropertyChangeEvent)event;
427           if (VirtualFile.PROP_NAME.equals(change.getPropertyName())
428               && !Comparing.equal(change.getOldValue(), change.getNewValue())) {
429             VirtualFile eventFile = change.getFile();
430             VirtualFile parent = eventFile.getParent(); // e.g. for LightVirtualFiles
431             addPointersUnder(parent, true, change.getNewValue().toString(), toFireEvents);
432
433             List<FilePointerPartNode> nodes = new ArrayList<>();
434             addPointersUnder(eventFile, false, "", nodes);
435             for (FilePointerPartNode node : nodes) {
436               VirtualFilePointerImpl pointer = node.getAnyPointer();
437               VirtualFile file = pointer == null ? null : pointer.getFile();
438               if (file != null) {
439                 toUpdateUrl.add(node);
440               }
441             }
442           }
443         }
444       }
445
446       myEvents = new ArrayList<>();
447       toFirePointers = toPointers(toFireEvents);
448       for (final VirtualFilePointerListener listener : myPointers.keySet()) {
449         if (listener == null) continue;
450         List<VirtualFilePointer> filtered = ContainerUtil.filter(toFirePointers,
451                                                                  pointer -> ((VirtualFilePointerImpl)pointer).getListener() == listener);
452         if (!filtered.isEmpty()) {
453           EventDescriptor event = new EventDescriptor(listener, filtered.toArray(new VirtualFilePointer[filtered.size()]));
454           myEvents.add(event);
455         }
456       }
457     }
458
459     for (EventDescriptor descriptor : myEvents) {
460       descriptor.fireBefore();
461     }
462
463     if (!toFireEvents.isEmpty()) {
464       myBus.syncPublisher(VirtualFilePointerListener.TOPIC).beforeValidityChanged(toFirePointers);
465     }
466
467     myNodesToFire = toFireEvents;
468     myNodesToUpdateUrl = toUpdateUrl;
469
470     assertConsistency();
471   }
472
473   void assertConsistency() {
474     for (FilePointerPartNode root : myPointers.values()) {
475       root.checkConsistency();
476     }
477   }
478
479   @Override
480   public void after(@NotNull final List<? extends VFileEvent> events) {
481     incModificationCount();
482
483     for (FilePointerPartNode node : myNodesToUpdateUrl) {
484       synchronized (this) {
485         String urlBefore = node.myFileAndUrl.second;
486         Pair<VirtualFile,String> after = node.update();
487         String urlAfter = after.second;
488         if (URL_COMPARATOR.compare(urlBefore, urlAfter) != 0 || !urlAfter.endsWith(node.part)) {
489           List<VirtualFilePointerImpl> myPointers = new SmartList<>();
490           node.addAllPointersTo(myPointers);
491
492           // url has changed, reinsert
493           int useCount = node.useCount;
494           FilePointerPartNode root = node.remove();
495           FilePointerPartNode newNode = root.findPointerOrCreate(VfsUtilCore.urlToPath(urlAfter), 0, after, myPointers.size());
496           VirtualFilePointer existingPointer = newNode.getAnyPointer();
497           if (existingPointer != null) {
498             // can happen when e.g. file renamed to the existing file
499             // merge two pointers
500             for (FilePointerPartNode n = newNode; n != null; n = n.parent) {
501               n.pointersUnder += myPointers.size();
502             }
503           }
504           newNode.addAllPointersTo(myPointers);
505           VirtualFilePointerImpl[] newMyPointers = myPointers.toArray(new VirtualFilePointerImpl[myPointers.size()]);
506           newNode.associate(newMyPointers, after);
507           newNode.incrementUsageCount(useCount);
508         }
509       }
510     }
511
512     VirtualFilePointer[] pointersToFireArray = toPointers(myNodesToFire);
513     for (VirtualFilePointer pointer : pointersToFireArray) {
514       ((VirtualFilePointerImpl)pointer).myNode.update();
515     }
516
517     for (EventDescriptor event : myEvents) {
518       event.fireAfter();
519     }
520
521     if (pointersToFireArray.length != 0) {
522       myBus.syncPublisher(VirtualFilePointerListener.TOPIC).validityChanged(pointersToFireArray);
523     }
524
525     myNodesToUpdateUrl = Collections.emptyList();
526     myEvents = Collections.emptyList();
527     myNodesToFire = Collections.emptyList();
528     assertConsistency();
529   }
530
531   void removeNode(@NotNull FilePointerPartNode node, VirtualFilePointerListener listener) {
532     FilePointerPartNode root = node.remove();
533     boolean rootNodeEmpty = root.children.length == 0 ;
534     if (rootNodeEmpty) {
535       myPointers.remove(listener);
536     }
537     assertConsistency();
538   }
539
540   @Override
541   public long getModificationCount() {
542     // depend on PersistentFS.getInstance().getStructureModificationCount() because com.intellij.openapi.vfs.impl.FilePointerPartNode.update is
543     // depend on its own modcount because we need to change both before and after VFS changes
544     return super.getModificationCount() + PersistentFS.getInstance().getStructureModificationCount();
545   }
546
547   private static class DelegatingDisposable implements Disposable {
548     private static final ConcurrentMap<Disposable, DelegatingDisposable> ourInstances = ContainerUtil.newConcurrentMap(ContainerUtil.<Disposable>identityStrategy());
549     private final TObjectIntHashMap<VirtualFilePointerImpl> myCounts = new TObjectIntHashMap<>();
550     private final Disposable myParent;
551
552     private DelegatingDisposable(@NotNull Disposable parent) {
553       myParent = parent;
554     }
555
556     private static void registerDisposable(@NotNull Disposable parentDisposable, @NotNull VirtualFilePointerImpl pointer) {
557       DelegatingDisposable result = ourInstances.get(parentDisposable);
558       if (result == null) {
559         DelegatingDisposable newDisposable = new DelegatingDisposable(parentDisposable);
560         result = ConcurrencyUtil.cacheOrGet(ourInstances, parentDisposable, newDisposable);
561         if (result == newDisposable) {
562           Disposer.register(parentDisposable, result);
563         }
564       }
565
566       synchronized (result) {
567         result.myCounts.put(pointer, result.myCounts.get(pointer) + 1);
568       }
569     }
570
571     @Override
572     public void dispose() {
573       ourInstances.remove(myParent);
574       synchronized (this) {
575         myCounts.forEachEntry((pointer, disposeCount) -> {
576           int after = pointer.myNode.incrementUsageCount(-disposeCount + 1);
577           LOG.assertTrue(after > 0, after);
578           pointer.dispose();
579           return true;
580         });
581       }
582     }
583   }
584
585   @TestOnly
586   int numberOfPointers() {
587     int number = 0;
588     for (FilePointerPartNode root : myPointers.values()) {
589       number = root.numberOfPointersUnder();
590     }
591     return number;
592   }
593   @TestOnly
594   int numberOfListeners() {
595     return myPointers.keySet().size();
596   }
597 }