2 * Copyright 2000-2015 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package com.intellij.openapi.vfs.impl;
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;
47 import java.util.concurrent.ConcurrentMap;
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;
55 private final Map<VirtualFilePointerListener, FilePointerPartNode> myPointers = new LinkedHashMap<>();
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;
64 VirtualFilePointerManagerImpl(@NotNull VirtualFileManager virtualFileManager,
65 @NotNull MessageBus bus,
66 @NotNull TempFileSystem tempFileSystem,
67 @NotNull LocalFileSystem localFileSystem,
68 @NotNull JarFileSystem jarFileSystem) {
69 myVirtualFileManager = virtualFileManager;
71 bus.connect().subscribe(VirtualFileManager.VFS_CHANGES, this);
72 TEMP_FILE_SYSTEM = tempFileSystem;
73 LOCAL_FILE_SYSTEM = localFileSystem;
74 JAR_FILE_SYSTEM = jarFileSystem;
78 public void initComponent() {
82 public void disposeComponent() {
83 assertAllPointersDisposed();
88 public String getComponentName() {
89 return "VirtualFilePointerManager";
92 private static class EventDescriptor {
93 @NotNull private final VirtualFilePointerListener myListener;
94 @NotNull private final VirtualFilePointer[] myPointers;
96 private EventDescriptor(@NotNull VirtualFilePointerListener listener, @NotNull VirtualFilePointer[] pointers) {
97 myListener = listener;
98 myPointers = pointers;
101 private void fireBefore() {
102 if (myPointers.length != 0) {
103 myListener.beforeValidityChanged(myPointers);
107 private void fireAfter() {
108 if (myPointers.length != 0) {
109 myListener.validityChanged(myPointers);
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);
121 return list.toArray(new VirtualFilePointer[list.size()]);
125 VirtualFilePointer[] getPointersUnder(VirtualFile parent, String childName) {
126 List<FilePointerPartNode> nodes = new ArrayList<>();
127 addPointersUnder(parent, true, childName, nodes);
128 return toPointers(nodes);
131 private void addPointersUnder(VirtualFile parent,
133 @NotNull CharSequence childName,
134 @NotNull List<FilePointerPartNode> out) {
135 for (FilePointerPartNode root : myPointers.values()) {
136 root.addPointersUnder(parent, separator, childName, out);
142 public synchronized VirtualFilePointer create(@NotNull String url, @NotNull Disposable parent, @Nullable VirtualFilePointerListener listener) {
143 return create(null, url, parent, listener);
148 public synchronized VirtualFilePointer create(@NotNull VirtualFile file, @NotNull Disposable parent, @Nullable VirtualFilePointerListener listener) {
149 return create(file, null, parent, listener);
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;
161 int protocolEnd = url.indexOf(URLUtil.SCHEME_SEPARATOR);
162 if (protocolEnd == -1) {
168 protocol = url.substring(0, protocolEnd);
169 fileSystem = myVirtualFileManager.getFileSystem(protocol);
170 path = url.substring(protocolEnd + URLUtil.SCHEME_SEPARATOR.length());
174 fileSystem = file.getFileSystem();
175 protocol = fileSystem.getProtocol();
176 path = file.getPath();
177 url = VirtualFileManager.constructUrl(protocol, path);
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);
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);
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);
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);
207 path = file.getPath();
211 // else url has come from VirtualFile.getPath() and is good enough
213 VirtualFilePointerImpl pointer = getOrCreate(parentDisposable, listener, path, Pair.create(file, url));
214 DelegatingDisposable.registerDisposable(parentDisposable, pointer);
218 private final Map<String, IdentityVirtualFilePointer> myUrlToIdentity = new THashMap<>();
220 private IdentityVirtualFilePointer getOrCreateIdentity(@NotNull String url, @Nullable VirtualFile found) {
221 return myUrlToIdentity.computeIfAbsent(url, __ -> new IdentityVirtualFilePointer(found, url));
225 private static String cleanupPath(@NotNull String path, boolean isJar) {
226 path = FileUtil.normalize(path);
227 path = trimTrailingSeparators(path, isJar);
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, "/");
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;
246 root = new FilePointerPartNode(path, null, fileAndUrl);
247 root.pointersUnder++;
248 myPointers.put(listener, root);
252 node = root.findPointerOrCreate(path, 0, fileAndUrl, 1);
255 VirtualFilePointerImpl pointer = node.getAnyPointer();
256 if (pointer == null) {
257 pointer = new VirtualFilePointerImpl(listener, parentDisposable, fileAndUrl);
258 node.associate(pointer, fileAndUrl);
260 pointer.myNode.incrementUsageCount(1);
262 root.checkConsistency();
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);
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);
284 if (!pointers.isEmpty()) {
285 VirtualFilePointerImpl p = pointers.get(0);
287 p.throwDisposalError("Not disposed pointer: "+p);
290 for (VirtualFilePointerImpl pointer : pointers) {
297 synchronized (myContainers) {
298 if (!myContainers.isEmpty()) {
299 VirtualFilePointerContainerImpl container = myContainers.iterator().next();
300 container.throwDisposalError("Not disposed container");
305 private final Set<VirtualFilePointerImpl> myStoredPointers = ContainerUtil.newIdentityTroveSet();
308 public void storePointers() {
309 myStoredPointers.clear();
310 addAllPointersTo(myStoredPointers);
314 public void assertPointersAreDisposed() {
315 List<VirtualFilePointerImpl> pointers = new ArrayList<>();
316 addAllPointersTo(pointers);
318 for (VirtualFilePointerImpl pointer : pointers) {
319 if (!myStoredPointers.contains(pointer)) {
320 pointer.throwDisposalError("Virtual pointer hasn't been disposed: "+pointer);
325 myStoredPointers.clear();
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);
335 for (FilePointerPartNode node : out) {
336 node.addAllPointersTo(pointers);
341 public void dispose() {
346 public VirtualFilePointerContainer createContainer(@NotNull Disposable parent) {
347 return createContainer(parent, null);
352 public synchronized VirtualFilePointerContainer createContainer(@NotNull Disposable parent, @Nullable VirtualFilePointerListener listener) {
353 return registerContainer(parent, new VirtualFilePointerContainerImpl(this, parent, listener));
357 private VirtualFilePointerContainer registerContainer(@NotNull Disposable parent, @NotNull final VirtualFilePointerContainerImpl virtualFilePointerContainer) {
358 synchronized (myContainers) {
359 myContainers.add(virtualFilePointerContainer);
361 Disposer.register(parent, new Disposable() {
363 public void dispose() {
364 Disposer.dispose(virtualFilePointerContainer);
366 synchronized (myContainers) {
367 removed = myContainers.remove(virtualFilePointerContainer);
369 if (!ApplicationManager.getApplication().isUnitTestMode()) {
377 public String toString() {
378 return "Disposing container " + virtualFilePointerContainer;
381 return virtualFilePointerContainer;
384 private List<EventDescriptor> myEvents = Collections.emptyList();
385 private List<FilePointerPartNode> myNodesToUpdateUrl = Collections.emptyList();
386 private List<FilePointerPartNode> myNodesToFire = Collections.emptyList();
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;
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);
402 else if (event instanceof VFileCreateEvent) {
403 final VFileCreateEvent createEvent = (VFileCreateEvent)event;
404 addPointersUnder(createEvent.getParent(), true, createEvent.getChildName(), toFireEvents);
406 else if (event instanceof VFileCopyEvent) {
407 final VFileCopyEvent copyEvent = (VFileCopyEvent)event;
408 addPointersUnder(copyEvent.getNewParent(), true, copyEvent.getFile().getName(), toFireEvents);
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);
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();
421 toUpdateUrl.add(node);
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);
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();
439 toUpdateUrl.add(node);
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()]));
459 for (EventDescriptor descriptor : myEvents) {
460 descriptor.fireBefore();
463 if (!toFireEvents.isEmpty()) {
464 myBus.syncPublisher(VirtualFilePointerListener.TOPIC).beforeValidityChanged(toFirePointers);
467 myNodesToFire = toFireEvents;
468 myNodesToUpdateUrl = toUpdateUrl;
473 void assertConsistency() {
474 for (FilePointerPartNode root : myPointers.values()) {
475 root.checkConsistency();
480 public void after(@NotNull final List<? extends VFileEvent> events) {
481 incModificationCount();
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);
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();
504 newNode.addAllPointersTo(myPointers);
505 VirtualFilePointerImpl[] newMyPointers = myPointers.toArray(new VirtualFilePointerImpl[myPointers.size()]);
506 newNode.associate(newMyPointers, after);
507 newNode.incrementUsageCount(useCount);
512 VirtualFilePointer[] pointersToFireArray = toPointers(myNodesToFire);
513 for (VirtualFilePointer pointer : pointersToFireArray) {
514 ((VirtualFilePointerImpl)pointer).myNode.update();
517 for (EventDescriptor event : myEvents) {
521 if (pointersToFireArray.length != 0) {
522 myBus.syncPublisher(VirtualFilePointerListener.TOPIC).validityChanged(pointersToFireArray);
525 myNodesToUpdateUrl = Collections.emptyList();
526 myEvents = Collections.emptyList();
527 myNodesToFire = Collections.emptyList();
531 void removeNode(@NotNull FilePointerPartNode node, VirtualFilePointerListener listener) {
532 FilePointerPartNode root = node.remove();
533 boolean rootNodeEmpty = root.children.length == 0 ;
535 myPointers.remove(listener);
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();
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;
552 private DelegatingDisposable(@NotNull Disposable parent) {
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);
566 synchronized (result) {
567 result.myCounts.put(pointer, result.myCounts.get(pointer) + 1);
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);
586 int numberOfPointers() {
588 for (FilePointerPartNode root : myPointers.values()) {
589 number = root.numberOfPointersUnder();
594 int numberOfListeners() {
595 return myPointers.keySet().size();