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 found == null ? new LightFilePointer(url) : new LightFilePointer(found);
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, parentDisposable, listener);
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
212 VirtualFilePointerImpl pointer = getOrCreate(listener, path, Pair.create(file, url));
213 DelegatingDisposable.registerDisposable(parentDisposable, pointer);
217 private final Map<String, IdentityVirtualFilePointer> myUrlToIdentity = new THashMap<>();
219 private IdentityVirtualFilePointer getOrCreateIdentity(@NotNull String url,
220 @Nullable VirtualFile found,
221 @NotNull Disposable parentDisposable,
222 VirtualFilePointerListener listener) {
223 IdentityVirtualFilePointer pointer = myUrlToIdentity.get(url);
224 if (pointer == null) {
225 pointer = new IdentityVirtualFilePointer(found, url,listener){
227 public void dispose() {
228 synchronized (VirtualFilePointerManagerImpl.this) {
230 myUrlToIdentity.remove(url);
234 myUrlToIdentity.put(url, pointer);
236 DelegatingDisposable.registerDisposable(parentDisposable, pointer);
238 pointer.incrementUsageCount(1);
243 private static String cleanupPath(@NotNull String path, boolean isJar) {
244 path = FileUtil.normalize(path);
245 path = trimTrailingSeparators(path, isJar);
249 private static String trimTrailingSeparators(@NotNull String path, boolean isJar) {
250 while (StringUtil.endsWithChar(path, '/') && !(isJar && path.endsWith(JarFileSystem.JAR_SEPARATOR))) {
251 path = StringUtil.trimEnd(path, "/");
257 private VirtualFilePointerImpl getOrCreate(@Nullable VirtualFilePointerListener listener,
258 @NotNull String path,
259 @NotNull Pair<VirtualFile, String> fileAndUrl) {
260 FilePointerPartNode root = myPointers.get(listener);
261 FilePointerPartNode node;
263 root = new FilePointerPartNode(path, null, fileAndUrl);
264 root.pointersUnder++;
265 myPointers.put(listener, root);
269 node = root.findPointerOrCreate(path, 0, fileAndUrl, 1);
272 VirtualFilePointerImpl pointer = node.getAnyPointer();
273 if (pointer == null) {
274 pointer = new VirtualFilePointerImpl(listener);
275 node.associate(pointer, fileAndUrl);
277 pointer.incrementUsageCount(1);
279 root.checkConsistency();
285 public synchronized VirtualFilePointer duplicate(@NotNull VirtualFilePointer pointer,
286 @NotNull Disposable parent,
287 @Nullable VirtualFilePointerListener listener) {
288 VirtualFile file = pointer.getFile();
289 return file == null ? create(pointer.getUrl(), parent, listener) : create(file, parent, listener);
292 private synchronized void assertAllPointersDisposed() {
293 for (Map.Entry<VirtualFilePointerListener, FilePointerPartNode> entry : myPointers.entrySet()) {
294 FilePointerPartNode root = entry.getValue();
295 List<FilePointerPartNode> left = new ArrayList<>();
296 root.addPointersUnder(null, false, "", left);
297 List<VirtualFilePointerImpl> pointers = new ArrayList<>();
298 for (FilePointerPartNode node : left) {
299 node.addAllPointersTo(pointers);
301 if (!pointers.isEmpty()) {
302 VirtualFilePointerImpl p = pointers.get(0);
304 p.throwDisposalError("Not disposed pointer: "+p);
307 for (VirtualFilePointerImpl pointer : pointers) {
314 synchronized (myContainers) {
315 if (!myContainers.isEmpty()) {
316 VirtualFilePointerContainerImpl container = myContainers.iterator().next();
317 container.throwDisposalError("Not disposed container");
322 private final Set<VirtualFilePointerImpl> myStoredPointers = ContainerUtil.newIdentityTroveSet();
325 public void storePointers() {
326 myStoredPointers.clear();
327 addAllPointersTo(myStoredPointers);
331 public void assertPointersAreDisposed() {
332 List<VirtualFilePointerImpl> pointers = new ArrayList<>();
333 addAllPointersTo(pointers);
335 for (VirtualFilePointerImpl pointer : pointers) {
336 if (!myStoredPointers.contains(pointer)) {
337 pointer.throwDisposalError("Virtual pointer hasn't been disposed: "+pointer);
342 myStoredPointers.clear();
347 private void addAllPointersTo(@NotNull Collection<VirtualFilePointerImpl> pointers) {
348 List<FilePointerPartNode> out = new ArrayList<>();
349 for (FilePointerPartNode root : myPointers.values()) {
350 root.addPointersUnder(null, false, "", out);
352 for (FilePointerPartNode node : out) {
353 node.addAllPointersTo(pointers);
358 public void dispose() {
363 public VirtualFilePointerContainer createContainer(@NotNull Disposable parent) {
364 return createContainer(parent, null);
369 public synchronized VirtualFilePointerContainer createContainer(@NotNull Disposable parent, @Nullable VirtualFilePointerListener listener) {
370 return registerContainer(parent, new VirtualFilePointerContainerImpl(this, parent, listener));
374 private VirtualFilePointerContainer registerContainer(@NotNull Disposable parent, @NotNull final VirtualFilePointerContainerImpl virtualFilePointerContainer) {
375 synchronized (myContainers) {
376 myContainers.add(virtualFilePointerContainer);
378 Disposer.register(parent, new Disposable() {
380 public void dispose() {
381 Disposer.dispose(virtualFilePointerContainer);
383 synchronized (myContainers) {
384 removed = myContainers.remove(virtualFilePointerContainer);
386 if (!ApplicationManager.getApplication().isUnitTestMode()) {
394 public String toString() {
395 return "Disposing container " + virtualFilePointerContainer;
398 return virtualFilePointerContainer;
401 private List<EventDescriptor> myEvents = Collections.emptyList();
402 private List<FilePointerPartNode> myNodesToUpdateUrl = Collections.emptyList();
403 private List<FilePointerPartNode> myNodesToFire = Collections.emptyList();
406 public void before(@NotNull final List<? extends VFileEvent> events) {
407 List<FilePointerPartNode> toFireEvents = new ArrayList<>();
408 List<FilePointerPartNode> toUpdateUrl = new ArrayList<>();
409 VirtualFilePointer[] toFirePointers;
411 synchronized (this) {
412 incModificationCount();
413 for (VFileEvent event : events) {
414 if (event instanceof VFileDeleteEvent) {
415 final VFileDeleteEvent deleteEvent = (VFileDeleteEvent)event;
416 addPointersUnder(deleteEvent.getFile(), false, "", toFireEvents);
419 else if (event instanceof VFileCreateEvent) {
420 final VFileCreateEvent createEvent = (VFileCreateEvent)event;
421 addPointersUnder(createEvent.getParent(), true, createEvent.getChildName(), toFireEvents);
423 else if (event instanceof VFileCopyEvent) {
424 final VFileCopyEvent copyEvent = (VFileCopyEvent)event;
425 addPointersUnder(copyEvent.getNewParent(), true, copyEvent.getFile().getName(), toFireEvents);
427 else if (event instanceof VFileMoveEvent) {
428 final VFileMoveEvent moveEvent = (VFileMoveEvent)event;
429 VirtualFile eventFile = moveEvent.getFile();
430 addPointersUnder(moveEvent.getNewParent(), true, eventFile.getName(), toFireEvents);
432 List<FilePointerPartNode> nodes = new ArrayList<>();
433 addPointersUnder(eventFile, false, "", nodes);
434 for (FilePointerPartNode node : nodes) {
435 VirtualFilePointerImpl pointer = node.getAnyPointer();
436 VirtualFile file = pointer == null ? null : pointer.getFile();
438 toUpdateUrl.add(node);
442 else if (event instanceof VFilePropertyChangeEvent) {
443 final VFilePropertyChangeEvent change = (VFilePropertyChangeEvent)event;
444 if (VirtualFile.PROP_NAME.equals(change.getPropertyName())
445 && !Comparing.equal(change.getOldValue(), change.getNewValue())) {
446 VirtualFile eventFile = change.getFile();
447 VirtualFile parent = eventFile.getParent(); // e.g. for LightVirtualFiles
448 addPointersUnder(parent, true, change.getNewValue().toString(), toFireEvents);
450 List<FilePointerPartNode> nodes = new ArrayList<>();
451 addPointersUnder(eventFile, false, "", nodes);
452 for (FilePointerPartNode node : nodes) {
453 VirtualFilePointerImpl pointer = node.getAnyPointer();
454 VirtualFile file = pointer == null ? null : pointer.getFile();
456 toUpdateUrl.add(node);
463 myEvents = new ArrayList<>();
464 toFirePointers = toPointers(toFireEvents);
465 for (final VirtualFilePointerListener listener : myPointers.keySet()) {
466 if (listener == null) continue;
467 List<VirtualFilePointer> filtered = ContainerUtil.filter(toFirePointers,
468 pointer -> ((VirtualFilePointerImpl)pointer).getListener() == listener);
469 if (!filtered.isEmpty()) {
470 EventDescriptor event = new EventDescriptor(listener, filtered.toArray(new VirtualFilePointer[filtered.size()]));
476 for (EventDescriptor descriptor : myEvents) {
477 descriptor.fireBefore();
480 if (!toFireEvents.isEmpty()) {
481 myBus.syncPublisher(VirtualFilePointerListener.TOPIC).beforeValidityChanged(toFirePointers);
484 myNodesToFire = toFireEvents;
485 myNodesToUpdateUrl = toUpdateUrl;
490 void assertConsistency() {
491 for (FilePointerPartNode root : myPointers.values()) {
492 root.checkConsistency();
497 public void after(@NotNull final List<? extends VFileEvent> events) {
498 incModificationCount();
500 for (FilePointerPartNode node : myNodesToUpdateUrl) {
501 synchronized (this) {
502 String urlBefore = node.myFileAndUrl.second;
503 Pair<VirtualFile,String> after = node.update();
504 String urlAfter = after.second;
505 if (URL_COMPARATOR.compare(urlBefore, urlAfter) != 0 || !urlAfter.endsWith(node.part)) {
506 List<VirtualFilePointerImpl> myPointers = new SmartList<>();
507 node.addAllPointersTo(myPointers);
509 // url has changed, reinsert
510 int useCount = node.useCount;
511 FilePointerPartNode root = node.remove();
512 FilePointerPartNode newNode = root.findPointerOrCreate(VfsUtilCore.urlToPath(urlAfter), 0, after, myPointers.size());
513 VirtualFilePointer existingPointer = newNode.getAnyPointer();
514 if (existingPointer != null) {
515 // can happen when e.g. file renamed to the existing file
516 // merge two pointers
517 for (FilePointerPartNode n = newNode; n != null; n = n.parent) {
518 n.pointersUnder += myPointers.size();
521 newNode.addAllPointersTo(myPointers);
522 VirtualFilePointerImpl[] newMyPointers = myPointers.toArray(new VirtualFilePointerImpl[myPointers.size()]);
523 newNode.associate(newMyPointers, after);
524 newNode.incrementUsageCount(useCount);
529 VirtualFilePointer[] pointersToFireArray = toPointers(myNodesToFire);
530 for (VirtualFilePointer pointer : pointersToFireArray) {
531 ((VirtualFilePointerImpl)pointer).myNode.update();
534 for (EventDescriptor event : myEvents) {
538 if (pointersToFireArray.length != 0) {
539 myBus.syncPublisher(VirtualFilePointerListener.TOPIC).validityChanged(pointersToFireArray);
542 myNodesToUpdateUrl = Collections.emptyList();
543 myEvents = Collections.emptyList();
544 myNodesToFire = Collections.emptyList();
548 void removeNode(@NotNull FilePointerPartNode node, VirtualFilePointerListener listener) {
549 FilePointerPartNode root = node.remove();
550 boolean rootNodeEmpty = root.children.length == 0 ;
552 myPointers.remove(listener);
558 public long getModificationCount() {
559 // depend on PersistentFS.getInstance().getStructureModificationCount() because com.intellij.openapi.vfs.impl.FilePointerPartNode.update is
560 // depend on its own modcount because we need to change both before and after VFS changes
561 return super.getModificationCount() + PersistentFS.getInstance().getStructureModificationCount();
564 private static class DelegatingDisposable implements Disposable {
565 private static final ConcurrentMap<Disposable, DelegatingDisposable> ourInstances = ContainerUtil.newConcurrentMap(ContainerUtil.<Disposable>identityStrategy());
566 private final TObjectIntHashMap<VirtualFilePointerImpl> myCounts = new TObjectIntHashMap<>(); // guarded by this
567 private final Disposable myParent;
569 private DelegatingDisposable(@NotNull Disposable parent) {
573 private static void registerDisposable(@NotNull Disposable parentDisposable, @NotNull VirtualFilePointerImpl pointer) {
574 DelegatingDisposable result = ourInstances.get(parentDisposable);
575 if (result == null) {
576 DelegatingDisposable newDisposable = new DelegatingDisposable(parentDisposable);
577 result = ConcurrencyUtil.cacheOrGet(ourInstances, parentDisposable, newDisposable);
578 if (result == newDisposable) {
579 Disposer.register(parentDisposable, result);
583 synchronized (result) {
584 result.myCounts.put(pointer, result.myCounts.get(pointer) + 1);
589 public void dispose() {
590 ourInstances.remove(myParent);
591 synchronized (this) {
592 myCounts.forEachEntry((pointer, disposeCount) -> {
593 int after = pointer.incrementUsageCount(-disposeCount + 1);
594 LOG.assertTrue(after > 0, after);
603 int numberOfPointers() {
605 for (FilePointerPartNode root : myPointers.values()) {
606 number = root.numberOfPointersUnder();
612 int numberOfListeners() {
613 return myPointers.keySet().size();
617 int numberOfCachedUrlToIdentity() {
618 return myUrlToIdentity.size();