@Override
  public void after(@NotNull final List<? extends VFileEvent> events) {
    incModificationCount();

    for (FilePointerPartNode node : myPointersToUpdateUrl) {
      synchronized (this) {
        VirtualFilePointerImpl pointer = node.leaf;
        String urlBefore = pointer.getUrlNoUpdate();
        Pair<VirtualFile, String> after = node.update();
        String urlAfter = after.second;
        if (URL_COMPARATOR.compare(urlBefore, urlAfter) != 0) {
          // url has changed, reinsert
          FilePointerPartNode root = myPointers.get(pointer.getListener());
          int useCount = node.useCount;
          node.remove();
          FilePointerPartNode newNode =
              root.findPointerOrCreate(VfsUtilCore.urlToPath(urlAfter), 0, after);
          VirtualFilePointerImpl existingPointer = newNode.leaf;
          if (existingPointer != null) {
            // can happen when e.g. file renamed to the existing file
            // merge two pointers
            pointer.myNode = newNode;
          } else {
            newNode.associate(pointer, after);
          }
          newNode.incrementUsageCount(useCount);
        }
      }
    }

    VirtualFilePointer[] pointersToFireArray = toPointers(myPointersToFire);
    for (VirtualFilePointer pointer : pointersToFireArray) {
      ((VirtualFilePointerImpl) pointer).myNode.update();
    }

    for (EventDescriptor event : myEvents) {
      event.fireAfter();
    }

    if (pointersToFireArray.length != 0) {
      myBus.syncPublisher(VirtualFilePointerListener.TOPIC).validityChanged(pointersToFireArray);
    }

    myPointersToUpdateUrl = Collections.emptyList();
    myEvents = Collections.emptyList();
    myPointersToFire = Collections.emptyList();
    for (FilePointerPartNode root : myPointers.values()) {
      root.checkConsistency();
    }
  }
Ejemplo n.º 2
0
 @RequiredWriteAction
 private void processEvent(@NotNull VFileEvent event) {
   processEvents(Collections.singletonList(event));
 }
public class VirtualFilePointerManagerImpl extends VirtualFilePointerManager
    implements ApplicationComponent, ModificationTracker, BulkFileListener {
  private static final Logger LOG =
      Logger.getInstance("#com.intellij.openapi.vfs.impl.VirtualFilePointerManagerImpl");
  private final TempFileSystem TEMP_FILE_SYSTEM;
  private final LocalFileSystem LOCAL_FILE_SYSTEM;
  private final JarFileSystem JAR_FILE_SYSTEM;
  // guarded by this
  private final Map<VirtualFilePointerListener, FilePointerPartNode> myPointers =
      new LinkedHashMap<VirtualFilePointerListener, FilePointerPartNode>();

  // compare by identity because VirtualFilePointerContainer has too smart equals
  // guarded by myContainers
  private final Set<VirtualFilePointerContainerImpl> myContainers =
      ContainerUtil.newIdentityTroveSet();
  @NotNull private final VirtualFileManager myVirtualFileManager;
  @NotNull private final MessageBus myBus;
  private static final Comparator<String> URL_COMPARATOR =
      SystemInfo.isFileSystemCaseSensitive
          ? new Comparator<String>() {
            @Override
            public int compare(@NotNull String url1, @NotNull String url2) {
              return url1.compareTo(url2);
            }
          }
          : new Comparator<String>() {
            @Override
            public int compare(@NotNull String url1, @NotNull String url2) {
              return url1.compareToIgnoreCase(url2);
            }
          };

  VirtualFilePointerManagerImpl(
      @NotNull VirtualFileManager virtualFileManager,
      @NotNull MessageBus bus,
      @NotNull TempFileSystem tempFileSystem,
      @NotNull LocalFileSystem localFileSystem,
      @NotNull JarFileSystem jarFileSystem) {
    myVirtualFileManager = virtualFileManager;
    myBus = bus;
    bus.connect().subscribe(VirtualFileManager.VFS_CHANGES, this);
    TEMP_FILE_SYSTEM = tempFileSystem;
    LOCAL_FILE_SYSTEM = localFileSystem;
    JAR_FILE_SYSTEM = jarFileSystem;
  }

  @Override
  public void initComponent() {}

  @Override
  public void disposeComponent() {
    assertAllPointersDisposed();
  }

  @NotNull
  @Override
  public String getComponentName() {
    return "VirtualFilePointerManager";
  }

  private static class EventDescriptor {
    @NotNull private final VirtualFilePointerListener myListener;
    @NotNull private final VirtualFilePointer[] myPointers;

    private EventDescriptor(
        @NotNull VirtualFilePointerListener listener, @NotNull VirtualFilePointer[] pointers) {
      myListener = listener;
      myPointers = pointers;
    }

    private void fireBefore() {
      if (myPointers.length != 0) {
        myListener.beforeValidityChanged(myPointers);
      }
    }

    private void fireAfter() {
      if (myPointers.length != 0) {
        myListener.validityChanged(myPointers);
      }
    }
  }

  @NotNull
  private static VirtualFilePointer[] toPointers(@NotNull List<FilePointerPartNode> pointers) {
    if (pointers.isEmpty()) return VirtualFilePointer.EMPTY_ARRAY;
    List<VirtualFilePointer> list =
        ContainerUtil.mapNotNull(
            pointers,
            new Function<FilePointerPartNode, VirtualFilePointer>() {
              @Override
              public VirtualFilePointer fun(FilePointerPartNode pair) {
                return pair.leaf;
              }
            });

    return list.toArray(new VirtualFilePointer[list.size()]);
  }

  @TestOnly
  public VirtualFilePointer[] getPointersUnder(VirtualFile parent, String childName) {
    List<FilePointerPartNode> nodes = new ArrayList<FilePointerPartNode>();
    addPointersUnder(parent, true, childName, nodes);
    return toPointers(nodes);
  }

  private void addPointersUnder(
      VirtualFile parent,
      boolean separator,
      @NotNull CharSequence childName,
      @NotNull List<FilePointerPartNode> out) {
    for (FilePointerPartNode root : myPointers.values()) {
      root.getPointersUnder(parent, separator, childName, out);
    }
  }

  @Override
  @NotNull
  public synchronized VirtualFilePointer create(
      @NotNull String url,
      @NotNull Disposable parent,
      @Nullable VirtualFilePointerListener listener) {
    return create(null, url, parent, listener);
  }

  @Override
  @NotNull
  public synchronized VirtualFilePointer create(
      @NotNull VirtualFile file,
      @NotNull Disposable parent,
      @Nullable VirtualFilePointerListener listener) {
    return create(file, null, parent, listener);
  }

  @NotNull
  private VirtualFilePointer create(
      @Nullable("null means the pointer will be created from the (not null) url") VirtualFile file,
      @Nullable("null means url has to be computed from the (not-null) file path") String url,
      @NotNull Disposable parentDisposable,
      @Nullable VirtualFilePointerListener listener) {
    VirtualFileSystem fileSystem;
    String protocol;
    String path;
    if (file == null) {
      int protocolEnd = url.indexOf(URLUtil.SCHEME_SEPARATOR);
      if (protocolEnd == -1) {
        protocol = null;
        fileSystem = null;
      } else {
        protocol = url.substring(0, protocolEnd);
        fileSystem = myVirtualFileManager.getFileSystem(protocol);
      }
      path = url.substring(protocolEnd + URLUtil.SCHEME_SEPARATOR.length());
    } else {
      fileSystem = file.getFileSystem();
      protocol = fileSystem.getProtocol();
      path = file.getPath();
      url = VirtualFileManager.constructUrl(protocol, path);
    }

    if (fileSystem == TEMP_FILE_SYSTEM) {
      // for tests, recreate always
      VirtualFile found = file == null ? VirtualFileManager.getInstance().findFileByUrl(url) : file;
      return new IdentityVirtualFilePointer(found, url);
    }

    boolean isJar = fileSystem == JAR_FILE_SYSTEM;
    if (fileSystem != LOCAL_FILE_SYSTEM && !isJar) {
      // we are unable to track alien file systems for now
      VirtualFile found =
          fileSystem == null
              ? null
              : file != null ? file : VirtualFileManager.getInstance().findFileByUrl(url);
      // if file is null, this pointer will never be alive
      return getOrCreateIdentity(url, found);
    }

    if (file == null) {
      String cleanPath = cleanupPath(path, isJar);
      // if newly created path is the same as substringed from url one then the url did not change,
      // we can reuse it
      //noinspection StringEquality
      if (cleanPath != path) {
        url = VirtualFileManager.constructUrl(protocol, cleanPath);
        path = cleanPath;
      }
    }
    // else url has come from VirtualFile.getPath() and is good enough

    VirtualFilePointerImpl pointer =
        getOrCreate(parentDisposable, listener, path, Pair.create(file, url));
    DelegatingDisposable.registerDisposable(parentDisposable, pointer);
    return pointer;
  }

  private final Map<String, IdentityVirtualFilePointer> myUrlToIdentity =
      new THashMap<String, IdentityVirtualFilePointer>();

  @NotNull
  private IdentityVirtualFilePointer getOrCreateIdentity(@NotNull String url, VirtualFile found) {
    IdentityVirtualFilePointer pointer = myUrlToIdentity.get(url);
    if (pointer == null) {
      pointer = new IdentityVirtualFilePointer(found, url);
      myUrlToIdentity.put(url, pointer);
    }
    return pointer;
  }

  @NotNull
  private static String cleanupPath(@NotNull String path, boolean isJar) {
    path = FileUtil.normalize(path);
    path = trimTrailingSeparators(path, isJar);
    return path;
  }

  private static String trimTrailingSeparators(@NotNull String path, boolean isJar) {
    while (StringUtil.endsWithChar(path, '/')
        && !(isJar && path.endsWith(JarFileSystem.JAR_SEPARATOR))) {
      path = StringUtil.trimEnd(path, "/");
    }
    return path;
  }

  @NotNull
  private VirtualFilePointerImpl getOrCreate(
      @NotNull Disposable parentDisposable,
      @Nullable VirtualFilePointerListener listener,
      @NotNull String path,
      @NotNull Pair<VirtualFile, String> fileAndUrl) {
    FilePointerPartNode root = myPointers.get(listener);
    FilePointerPartNode node;
    if (root == null) {
      root = new FilePointerPartNode(path, null, fileAndUrl);
      myPointers.put(listener, root);
      node = root;
    } else {
      node = root.findPointerOrCreate(path, 0, fileAndUrl);
    }

    VirtualFilePointerImpl pointer;
    if (node.leaf == null) {
      pointer = new VirtualFilePointerImpl(listener, parentDisposable, fileAndUrl);
      node.associate(pointer, fileAndUrl);
    } else {
      pointer = node.leaf;
    }
    pointer.myNode.incrementUsageCount(1);

    root.checkConsistency();
    return pointer;
  }

  @Override
  @NotNull
  public synchronized VirtualFilePointer duplicate(
      @NotNull VirtualFilePointer pointer,
      @NotNull Disposable parent,
      @Nullable VirtualFilePointerListener listener) {
    VirtualFile file = pointer.getFile();
    return file == null
        ? create(pointer.getUrl(), parent, listener)
        : create(file, parent, listener);
  }

  private synchronized void assertAllPointersDisposed() {
    for (Map.Entry<VirtualFilePointerListener, FilePointerPartNode> entry : myPointers.entrySet()) {
      FilePointerPartNode root = entry.getValue();
      ArrayList<FilePointerPartNode> left = new ArrayList<FilePointerPartNode>();
      root.getPointersUnder(null, false, "", left);
      if (!left.isEmpty()) {
        VirtualFilePointerImpl p = left.get(0).leaf;
        try {
          p.throwDisposalError("Not disposed pointer: " + p);
        } finally {
          for (FilePointerPartNode pair : left) {
            VirtualFilePointerImpl pointer = pair.leaf;
            pointer.dispose();
          }
        }
      }
    }

    synchronized (myContainers) {
      if (!myContainers.isEmpty()) {
        VirtualFilePointerContainerImpl container = myContainers.iterator().next();
        container.throwDisposalError("Not disposed container");
      }
    }
  }

  private final Set<VirtualFilePointerImpl> myStoredPointers = ContainerUtil.newIdentityTroveSet();

  @TestOnly
  public void storePointers() {
    myStoredPointers.clear();
    addAllPointers(myStoredPointers);
  }

  @TestOnly
  public void assertPointersAreDisposed() {
    List<VirtualFilePointerImpl> pointers = new ArrayList<VirtualFilePointerImpl>();
    addAllPointers(pointers);
    try {
      for (VirtualFilePointerImpl pointer : pointers) {
        if (!myStoredPointers.contains(pointer)) {
          pointer.throwDisposalError("Virtual pointer hasn't been disposed: " + pointer);
        }
      }
    } finally {
      myStoredPointers.clear();
    }
  }

  private void addAllPointers(@NotNull Collection<VirtualFilePointerImpl> pointers) {
    List<FilePointerPartNode> out = new ArrayList<FilePointerPartNode>();
    for (FilePointerPartNode root : myPointers.values()) {
      root.getPointersUnder(null, false, "", out);
    }
    for (FilePointerPartNode node : out) {
      pointers.add(node.leaf);
    }
  }

  @Override
  public void dispose() {}

  @Override
  @NotNull
  public VirtualFilePointerContainer createContainer(@NotNull Disposable parent) {
    return createContainer(parent, null);
  }

  @Override
  @NotNull
  public synchronized VirtualFilePointerContainer createContainer(
      @NotNull Disposable parent, @Nullable VirtualFilePointerListener listener) {
    return registerContainer(parent, new VirtualFilePointerContainerImpl(this, parent, listener));
  }

  @NotNull
  private VirtualFilePointerContainer registerContainer(
      @NotNull Disposable parent,
      @NotNull final VirtualFilePointerContainerImpl virtualFilePointerContainer) {
    synchronized (myContainers) {
      myContainers.add(virtualFilePointerContainer);
    }
    Disposer.register(
        parent,
        new Disposable() {
          @Override
          public void dispose() {
            Disposer.dispose(virtualFilePointerContainer);
            boolean removed;
            synchronized (myContainers) {
              removed = myContainers.remove(virtualFilePointerContainer);
            }
            if (!ApplicationManager.getApplication().isUnitTestMode()) {
              assert removed;
            }
          }

          @Override
          @NonNls
          @NotNull
          public String toString() {
            return "Disposing container " + virtualFilePointerContainer;
          }
        });
    return virtualFilePointerContainer;
  }

  private List<EventDescriptor> myEvents = Collections.emptyList();
  private List<FilePointerPartNode> myPointersToUpdateUrl = Collections.emptyList();
  private List<FilePointerPartNode> myPointersToFire = Collections.emptyList();

  @Override
  public void before(@NotNull final List<? extends VFileEvent> events) {
    List<FilePointerPartNode> toFireEvents = new ArrayList<FilePointerPartNode>();
    List<FilePointerPartNode> toUpdateUrl = new ArrayList<FilePointerPartNode>();
    VirtualFilePointer[] toFirePointers;

    synchronized (this) {
      incModificationCount();
      for (VFileEvent event : events) {
        if (event instanceof VFileDeleteEvent) {
          final VFileDeleteEvent deleteEvent = (VFileDeleteEvent) event;
          addPointersUnder(deleteEvent.getFile(), false, "", toFireEvents);

        } else if (event instanceof VFileCreateEvent) {
          final VFileCreateEvent createEvent = (VFileCreateEvent) event;
          addPointersUnder(createEvent.getParent(), true, createEvent.getChildName(), toFireEvents);
        } else if (event instanceof VFileCopyEvent) {
          final VFileCopyEvent copyEvent = (VFileCopyEvent) event;
          addPointersUnder(
              copyEvent.getNewParent(), true, copyEvent.getFile().getName(), toFireEvents);
        } else if (event instanceof VFileMoveEvent) {
          final VFileMoveEvent moveEvent = (VFileMoveEvent) event;
          VirtualFile eventFile = moveEvent.getFile();
          addPointersUnder(moveEvent.getNewParent(), true, eventFile.getName(), toFireEvents);

          List<FilePointerPartNode> nodes = new ArrayList<FilePointerPartNode>();
          addPointersUnder(eventFile, false, "", nodes);
          for (FilePointerPartNode pair : nodes) {
            VirtualFile file = pair.leaf.getFile();
            if (file != null) {
              toUpdateUrl.add(pair);
            }
          }
        } else if (event instanceof VFilePropertyChangeEvent) {
          final VFilePropertyChangeEvent change = (VFilePropertyChangeEvent) event;
          if (VirtualFile.PROP_NAME.equals(change.getPropertyName())) {
            VirtualFile eventFile = change.getFile();
            VirtualFile parent = eventFile.getParent(); // e.g. for LightVirtualFiles
            addPointersUnder(parent, true, change.getNewValue().toString(), toFireEvents);

            List<FilePointerPartNode> nodes = new ArrayList<FilePointerPartNode>();
            addPointersUnder(eventFile, false, "", nodes);
            for (FilePointerPartNode pair : nodes) {
              VirtualFile file = pair.leaf.getFile();
              if (file != null) {
                toUpdateUrl.add(pair);
              }
            }
          }
        }
      }

      myEvents = new ArrayList<EventDescriptor>();
      toFirePointers = toPointers(toFireEvents);
      for (final VirtualFilePointerListener listener : myPointers.keySet()) {
        if (listener == null) continue;
        List<VirtualFilePointer> filtered =
            ContainerUtil.filter(
                toFirePointers,
                new Condition<VirtualFilePointer>() {
                  @Override
                  public boolean value(VirtualFilePointer pointer) {
                    return ((VirtualFilePointerImpl) pointer).getListener() == listener;
                  }
                });
        if (!filtered.isEmpty()) {
          EventDescriptor event =
              new EventDescriptor(
                  listener, filtered.toArray(new VirtualFilePointer[filtered.size()]));
          myEvents.add(event);
        }
      }
    }

    for (EventDescriptor descriptor : myEvents) {
      descriptor.fireBefore();
    }

    if (!toFireEvents.isEmpty()) {
      myBus.syncPublisher(VirtualFilePointerListener.TOPIC).beforeValidityChanged(toFirePointers);
    }

    myPointersToFire = toFireEvents;
    myPointersToUpdateUrl = toUpdateUrl;
  }

  @Override
  public void after(@NotNull final List<? extends VFileEvent> events) {
    incModificationCount();

    for (FilePointerPartNode node : myPointersToUpdateUrl) {
      synchronized (this) {
        VirtualFilePointerImpl pointer = node.leaf;
        String urlBefore = pointer.getUrlNoUpdate();
        Pair<VirtualFile, String> after = node.update();
        String urlAfter = after.second;
        if (URL_COMPARATOR.compare(urlBefore, urlAfter) != 0) {
          // url has changed, reinsert
          FilePointerPartNode root = myPointers.get(pointer.getListener());
          int useCount = node.useCount;
          node.remove();
          FilePointerPartNode newNode =
              root.findPointerOrCreate(VfsUtilCore.urlToPath(urlAfter), 0, after);
          VirtualFilePointerImpl existingPointer = newNode.leaf;
          if (existingPointer != null) {
            // can happen when e.g. file renamed to the existing file
            // merge two pointers
            pointer.myNode = newNode;
          } else {
            newNode.associate(pointer, after);
          }
          newNode.incrementUsageCount(useCount);
        }
      }
    }

    VirtualFilePointer[] pointersToFireArray = toPointers(myPointersToFire);
    for (VirtualFilePointer pointer : pointersToFireArray) {
      ((VirtualFilePointerImpl) pointer).myNode.update();
    }

    for (EventDescriptor event : myEvents) {
      event.fireAfter();
    }

    if (pointersToFireArray.length != 0) {
      myBus.syncPublisher(VirtualFilePointerListener.TOPIC).validityChanged(pointersToFireArray);
    }

    myPointersToUpdateUrl = Collections.emptyList();
    myEvents = Collections.emptyList();
    myPointersToFire = Collections.emptyList();
    for (FilePointerPartNode root : myPointers.values()) {
      root.checkConsistency();
    }
  }

  void removeNode(@NotNull FilePointerPartNode node, VirtualFilePointerListener listener) {
    boolean rootNodeEmpty = node.remove();
    if (rootNodeEmpty) {
      myPointers.remove(listener);
    } else {
      myPointers.get(listener).checkConsistency();
    }
  }

  private static class DelegatingDisposable implements Disposable {
    private static final ConcurrentMap<Disposable, DelegatingDisposable> ourInstances =
        ContainerUtil.newConcurrentMap(ContainerUtil.<Disposable>identityStrategy());
    private final TObjectIntHashMap<VirtualFilePointerImpl> myCounts =
        new TObjectIntHashMap<VirtualFilePointerImpl>();
    private final Disposable myParent;

    private DelegatingDisposable(@NotNull Disposable parent) {
      myParent = parent;
    }

    static void registerDisposable(
        @NotNull Disposable parentDisposable, @NotNull VirtualFilePointerImpl pointer) {
      DelegatingDisposable result = ourInstances.get(parentDisposable);
      if (result == null) {
        DelegatingDisposable newDisposable = new DelegatingDisposable(parentDisposable);
        result = ConcurrencyUtil.cacheOrGet(ourInstances, parentDisposable, newDisposable);
        if (result == newDisposable) {
          Disposer.register(parentDisposable, result);
        }
      }

      synchronized (result) {
        result.myCounts.put(pointer, result.myCounts.get(pointer) + 1);
      }
    }

    @Override
    public void dispose() {
      ourInstances.remove(myParent);
      synchronized (this) {
        myCounts.forEachEntry(
            new TObjectIntProcedure<VirtualFilePointerImpl>() {
              @Override
              public boolean execute(VirtualFilePointerImpl pointer, int disposeCount) {
                int after = pointer.myNode.incrementUsageCount(-disposeCount + 1);
                LOG.assertTrue(after > 0, after);
                pointer.dispose();
                return true;
              }
            });
      }
    }
  }

  @TestOnly
  int numberOfPointers() {
    int number = 0;
    for (FilePointerPartNode root : myPointers.values()) {
      number = root.getPointersUnder();
    }
    return number;
  }

  @TestOnly
  int numberOfListeners() {
    return myPointers.keySet().size();
  }
}
public class VirtualFilePointerManagerImpl extends VirtualFilePointerManager
    implements ApplicationComponent, ModificationTracker, BulkFileListener {
  private static final Logger LOG =
      Logger.getInstance("#com.intellij.openapi.vfs.impl.VirtualFilePointerManagerImpl");
  private final TempFileSystem myTempFileSystem;
  private final LocalFileSystem myLocalFileSystem;
  private volatile long myVfsModificationCounter;
  // guarded by this
  private final Map<VirtualFilePointerListener, FilePointerPartNode> myPointers =
      new LinkedHashMap<VirtualFilePointerListener, FilePointerPartNode>();

  // compare by identity because VirtualFilePointerContainer has too smart equals
  // guarded by myContainers
  private final Set<VirtualFilePointerContainerImpl> myContainers =
      ContainerUtil.<VirtualFilePointerContainerImpl>newIdentityTroveSet();
  @NotNull private final VirtualFileManager myVirtualFileManager;
  @NotNull private final MessageBus myBus;
  private static final Comparator<String> URL_COMPARATOR =
      SystemInfo.isFileSystemCaseSensitive
          ? new Comparator<String>() {
            @Override
            public int compare(@NotNull String url1, @NotNull String url2) {
              return url1.compareTo(url2);
            }
          }
          : new Comparator<String>() {
            @Override
            public int compare(@NotNull String url1, @NotNull String url2) {
              return url1.compareToIgnoreCase(url2);
            }
          };

  VirtualFilePointerManagerImpl(
      @NotNull VirtualFileManager virtualFileManager,
      @NotNull MessageBus bus,
      @NotNull TempFileSystem tempFileSystem,
      @NotNull LocalFileSystem localFileSystem) {
    myVirtualFileManager = virtualFileManager;
    myBus = bus;
    bus.connect().subscribe(VirtualFileManager.VFS_CHANGES, this);
    myTempFileSystem = tempFileSystem;
    myLocalFileSystem = localFileSystem;
  }

  @Override
  public long getModificationCount() {
    return myVfsModificationCounter;
  }

  @Override
  public void initComponent() {}

  @Override
  public void disposeComponent() {
    assertAllPointersDisposed();
  }

  @NotNull
  @Override
  public String getComponentName() {
    return "VirtualFilePointerManager";
  }

  private static class EventDescriptor {
    @NotNull private final VirtualFilePointerListener myListener;
    @NotNull private final VirtualFilePointer[] myPointers;

    private EventDescriptor(
        @NotNull VirtualFilePointerListener listener, @NotNull VirtualFilePointer[] pointers) {
      myListener = listener;
      myPointers = pointers;
    }

    private void fireBefore() {
      if (myPointers.length != 0) {
        myListener.beforeValidityChanged(myPointers);
      }
    }

    private void fireAfter() {
      if (myPointers.length != 0) {
        myListener.validityChanged(myPointers);
      }
    }
  }

  @NotNull
  private static VirtualFilePointer[] toPointers(@NotNull List<FilePointerPartNode> pointers) {
    if (pointers.isEmpty()) return VirtualFilePointer.EMPTY_ARRAY;
    List<VirtualFilePointer> list =
        ContainerUtil.mapNotNull(
            pointers,
            new Function<FilePointerPartNode, VirtualFilePointer>() {
              @Override
              public VirtualFilePointer fun(FilePointerPartNode pair) {
                return pair.leaf;
              }
            });

    return list.toArray(new VirtualFilePointer[list.size()]);
  }

  private void addPointersUnder(@NotNull String path, @NotNull List<FilePointerPartNode> out) {
    for (FilePointerPartNode root : myPointers.values()) {
      root.getPointersUnder(path, 0, out);
    }
  }

  @Override
  @NotNull
  public synchronized VirtualFilePointer create(
      @NotNull String url,
      @NotNull Disposable parent,
      @Nullable VirtualFilePointerListener listener) {
    return create(null, url, parent, listener);
  }

  @Override
  @NotNull
  public synchronized VirtualFilePointer create(
      @NotNull VirtualFile file,
      @NotNull Disposable parent,
      @Nullable VirtualFilePointerListener listener) {
    return create(file, file.getUrl(), parent, listener);
  }

  @NotNull
  private VirtualFilePointer create(
      @Nullable VirtualFile file,
      @NotNull String url,
      @NotNull final Disposable parentDisposable,
      @Nullable VirtualFilePointerListener listener) {
    String protocol;
    IVirtualFileSystem fileSystem;
    if (file == null) {
      protocol = VirtualFileManager.extractProtocol(url);
      fileSystem = myVirtualFileManager.getFileSystem(protocol);
    } else {
      protocol = null;
      fileSystem = file.getFileSystem();
    }
    if (fileSystem == myTempFileSystem) {
      // for tests, recreate always
      VirtualFile found = file == null ? VirtualFileManager.getInstance().findFileByUrl(url) : file;
      return new IdentityVirtualFilePointer(found, url);
    }
    if (fileSystem != myLocalFileSystem && !(fileSystem instanceof ArchiveFileSystem)) {
      // we are unable to track alien file systems for now
      VirtualFile found =
          fileSystem == null
              ? null
              : file != null ? file : VirtualFileManager.getInstance().findFileByUrl(url);
      // if file is null, this pointer will never be alive
      return getOrCreateIdentity(url, found);
    }

    String path;
    if (file == null) {
      path = VirtualFileManager.extractPath(url);
      path = cleanupPath(path, protocol, fileSystem);
      url = VirtualFileManager.constructUrl(protocol, path);
    } else {
      path = file.getPath();
      // url has come from VirtualFile.getUrl() and is good enough
    }

    VirtualFilePointerImpl pointer =
        getOrCreate(parentDisposable, listener, path, Pair.create(file, url));

    DelegatingDisposable.registerDisposable(parentDisposable, pointer);

    return pointer;
  }

  private final Map<String, IdentityVirtualFilePointer> myUrlToIdentity =
      new THashMap<String, IdentityVirtualFilePointer>();

  @NotNull
  private IdentityVirtualFilePointer getOrCreateIdentity(@NotNull String url, VirtualFile found) {
    IdentityVirtualFilePointer pointer = myUrlToIdentity.get(url);
    if (pointer == null) {
      pointer = new IdentityVirtualFilePointer(found, url);
      myUrlToIdentity.put(url, pointer);
    }
    return pointer;
  }

  private static String cleanupPath(
      String path, @NotNull String protocol, IVirtualFileSystem fileSystem) {
    path = FileUtil.toSystemIndependentName(path);

    path = stripTrailingPathSeparator(path, protocol, fileSystem);
    path = removeDoubleSlashes(path);
    return path;
  }

  @NotNull
  private static String removeDoubleSlashes(@NotNull String path) {
    while (true) {
      int i = path.lastIndexOf("//");
      if (i != -1) {
        path = path.substring(0, i) + path.substring(i + 1);
      } else {
        break;
      }
    }
    return path;
  }

  @NotNull
  private VirtualFilePointerImpl getOrCreate(
      @NotNull Disposable parentDisposable,
      @Nullable VirtualFilePointerListener listener,
      @NotNull String path,
      @NotNull Pair<VirtualFile, String> fileAndUrl) {
    FilePointerPartNode root = myPointers.get(listener);
    FilePointerPartNode node;
    if (root == null) {
      root = new FilePointerPartNode(path, null, fileAndUrl);
      myPointers.put(listener, root);
      node = root;
    } else {
      node = root.findPointerOrCreate(path, 0, fileAndUrl);
    }

    VirtualFilePointerImpl pointer;
    if (node.leaf == null) {
      pointer = new VirtualFilePointerImpl(listener, parentDisposable, fileAndUrl);
      node.associate(pointer, fileAndUrl);
    } else {
      pointer = node.leaf;
    }
    pointer.myNode.incrementUsageCount(1);

    root.checkStructure();
    return pointer;
  }

  @NotNull
  private static String stripTrailingPathSeparator(
      @NotNull String path, @NotNull String protocol, IVirtualFileSystem fileSystem) {
    while (!path.isEmpty()
        && path.charAt(path.length() - 1) == '/'
        && !(fileSystem instanceof ArchiveFileSystem)
        && path.endsWith(ArchiveFileSystem.ARCHIVE_SEPARATOR)) {
      path = StringUtil.trimEnd(path, "/");
    }
    return path;
  }

  @Override
  @NotNull
  public synchronized VirtualFilePointer duplicate(
      @NotNull VirtualFilePointer pointer,
      @NotNull Disposable parent,
      @Nullable VirtualFilePointerListener listener) {
    VirtualFile file = pointer.getFile();
    return file == null
        ? create(pointer.getUrl(), parent, listener)
        : create(file, parent, listener);
  }

  private synchronized void assertAllPointersDisposed() {
    for (Map.Entry<VirtualFilePointerListener, FilePointerPartNode> entry : myPointers.entrySet()) {
      FilePointerPartNode root = entry.getValue();
      ArrayList<FilePointerPartNode> left = new ArrayList<FilePointerPartNode>();
      root.getPointersUnder("", 0, left);
      if (!left.isEmpty()) {
        VirtualFilePointerImpl p = left.get(0).leaf;
        try {
          p.throwDisposalError("Not disposed pointer: " + p);
        } finally {
          for (FilePointerPartNode pair : left) {
            VirtualFilePointerImpl pointer = pair.leaf;
            pointer.dispose();
          }
        }
      }
    }

    synchronized (myContainers) {
      if (!myContainers.isEmpty()) {
        VirtualFilePointerContainerImpl container = myContainers.iterator().next();
        container.throwDisposalError("Not disposed container");
      }
    }
  }

  private final Set<VirtualFilePointerImpl> myStoredPointers =
      ContainerUtil.<VirtualFilePointerImpl>newIdentityTroveSet();

  @TestOnly
  public void storePointers() {
    // assert myStoredPointers.isEmpty() : myStoredPointers;
    myStoredPointers.clear();
    addAllPointers(myStoredPointers);
  }

  @TestOnly
  public void assertPointersAreDisposed() {
    List<VirtualFilePointerImpl> pointers = new ArrayList<VirtualFilePointerImpl>();
    addAllPointers(pointers);
    try {
      for (VirtualFilePointerImpl pointer : pointers) {
        if (!myStoredPointers.contains(pointer)) {
          pointer.throwDisposalError("Virtual pointer hasn't been disposed: " + pointer);
        }
      }
    } finally {
      myStoredPointers.clear();
    }
  }

  private void addAllPointers(Collection<VirtualFilePointerImpl> pointers) {
    List<FilePointerPartNode> out = new ArrayList<FilePointerPartNode>();
    for (FilePointerPartNode root : myPointers.values()) {
      root.getPointersUnder("", 0, out);
    }
    for (FilePointerPartNode node : out) {
      pointers.add(node.leaf);
    }
  }

  @Override
  public void dispose() {}

  private void incModificationCounter() {
    myVfsModificationCounter++;
  }

  @Override
  @NotNull
  public VirtualFilePointerContainer createContainer(@NotNull Disposable parent) {
    return createContainer(parent, null);
  }

  @Override
  @NotNull
  public synchronized VirtualFilePointerContainer createContainer(
      @NotNull Disposable parent, @Nullable VirtualFilePointerListener listener) {
    return registerContainer(parent, new VirtualFilePointerContainerImpl(this, parent, listener));
  }

  @NotNull
  private VirtualFilePointerContainer registerContainer(
      @NotNull Disposable parent,
      @NotNull final VirtualFilePointerContainerImpl virtualFilePointerContainer) {
    synchronized (myContainers) {
      myContainers.add(virtualFilePointerContainer);
    }
    Disposer.register(
        parent,
        new Disposable() {
          @Override
          public void dispose() {
            Disposer.dispose(virtualFilePointerContainer);
            boolean removed;
            synchronized (myContainers) {
              removed = myContainers.remove(virtualFilePointerContainer);
            }
            if (!ApplicationManager.getApplication().isUnitTestMode()) {
              assert removed;
            }
          }

          @NonNls
          @NotNull
          public String toString() {
            return "Disposing container " + virtualFilePointerContainer;
          }
        });
    return virtualFilePointerContainer;
  }

  @Nullable private List<EventDescriptor> myEvents = Collections.emptyList();
  @Nullable private List<FilePointerPartNode> myPointersToUpdateUrl = Collections.emptyList();
  @Nullable private List<FilePointerPartNode> myPointersToFire = Collections.emptyList();

  @Override
  public void before(@NotNull final List<? extends VFileEvent> events) {
    List<FilePointerPartNode> toFireEvents = new ArrayList<FilePointerPartNode>();
    List<FilePointerPartNode> toUpdateUrl = new ArrayList<FilePointerPartNode>();
    VirtualFilePointer[] toFirePointers;

    synchronized (this) {
      incModificationCounter();
      for (VFileEvent event : events) {
        if (event instanceof VFileDeleteEvent) {
          final VFileDeleteEvent deleteEvent = (VFileDeleteEvent) event;
          String path = deleteEvent.getFile().getPath();
          addPointersUnder(path, toFireEvents);
        } else if (event instanceof VFileCreateEvent) {
          final VFileCreateEvent createEvent = (VFileCreateEvent) event;
          String url = createEvent.getPath();
          addPointersUnder(url, toFireEvents);
        } else if (event instanceof VFileCopyEvent) {
          final VFileCopyEvent copyEvent = (VFileCopyEvent) event;
          String url = copyEvent.getNewParent().getPath() + "/" + copyEvent.getFile().getName();
          addPointersUnder(url, toFireEvents);
        } else if (event instanceof VFileMoveEvent) {
          final VFileMoveEvent moveEvent = (VFileMoveEvent) event;
          List<FilePointerPartNode> nodes = new ArrayList<FilePointerPartNode>();
          addPointersUnder(moveEvent.getFile().getPath(), nodes);
          for (FilePointerPartNode pair : nodes) {
            VirtualFile file = pair.leaf.getFile();
            if (file != null) {
              toUpdateUrl.add(pair);
            }
          }
        } else if (event instanceof VFilePropertyChangeEvent) {
          final VFilePropertyChangeEvent change = (VFilePropertyChangeEvent) event;
          if (VirtualFile.PROP_NAME.equals(change.getPropertyName())) {
            List<FilePointerPartNode> nodes = new ArrayList<FilePointerPartNode>();
            addPointersUnder(change.getFile().getPath(), nodes);
            for (FilePointerPartNode pair : nodes) {
              VirtualFile file = pair.leaf.getFile();
              if (file != null) {
                toUpdateUrl.add(pair);
              }
            }
          }
        }
      }

      myEvents = new ArrayList<EventDescriptor>();
      toFirePointers = toPointers(toFireEvents);
      for (final VirtualFilePointerListener listener : myPointers.keySet()) {
        if (listener == null) continue;
        List<VirtualFilePointer> filtered =
            ContainerUtil.filter(
                toFirePointers,
                new Condition<VirtualFilePointer>() {
                  @Override
                  public boolean value(VirtualFilePointer pointer) {
                    return ((VirtualFilePointerImpl) pointer).getListener() == listener;
                  }
                });
        if (!filtered.isEmpty()) {
          EventDescriptor event =
              new EventDescriptor(
                  listener, filtered.toArray(new VirtualFilePointer[filtered.size()]));
          myEvents.add(event);
        }
      }
    }

    for (EventDescriptor descriptor : myEvents) {
      descriptor.fireBefore();
    }

    if (!toFireEvents.isEmpty()) {
      myBus.syncPublisher(VirtualFilePointerListener.TOPIC).beforeValidityChanged(toFirePointers);
    }

    myPointersToFire = toFireEvents;
    myPointersToUpdateUrl = toUpdateUrl;
  }

  @Override
  public void after(@NotNull final List<? extends VFileEvent> events) {
    incModificationCounter();

    for (FilePointerPartNode node : myPointersToUpdateUrl) {
      synchronized (this) {
        VirtualFilePointerImpl pointer = node.leaf;
        String urlBefore = pointer.getUrlNoUpdate();
        Pair<VirtualFile, String> after = node.update();
        String urlAfter = after.second;
        if (URL_COMPARATOR.compare(urlBefore, urlAfter) != 0) {
          // url has changed, reinsert
          FilePointerPartNode root = myPointers.get(pointer.getListener());
          int useCount = node.useCount;
          node.remove();
          FilePointerPartNode newNode =
              root.findPointerOrCreate(VfsUtilCore.urlToPath(urlAfter), 0, after);
          VirtualFilePointerImpl existingPointer = newNode.leaf;
          if (existingPointer != null) {
            // can happen when e.g. file renamed to the existing file
            // merge two pointers
            pointer.myNode = newNode;
          } else {
            newNode.associate(pointer, after);
          }
          newNode.incrementUsageCount(useCount);
        }
      }
    }

    VirtualFilePointer[] pointersToFireArray = toPointers(myPointersToFire);
    for (VirtualFilePointer pointer : pointersToFireArray) {
      ((VirtualFilePointerImpl) pointer).myNode.update();
    }

    for (EventDescriptor event : myEvents) {
      event.fireAfter();
    }

    if (pointersToFireArray.length != 0) {
      myBus.syncPublisher(VirtualFilePointerListener.TOPIC).validityChanged(pointersToFireArray);
    }

    myPointersToUpdateUrl = Collections.emptyList();
    myEvents = Collections.emptyList();
    myPointersToFire = Collections.emptyList();
    for (FilePointerPartNode root : myPointers.values()) {
      root.checkStructure();
    }
  }

  void removeNode(@NotNull FilePointerPartNode node, VirtualFilePointerListener listener) {
    boolean rootNodeEmpty = node.remove();
    if (rootNodeEmpty) {
      myPointers.remove(listener);
    } else {
      myPointers.get(listener).checkStructure();
    }
  }

  private static class DelegatingDisposable implements Disposable {
    private static final Map<Disposable, DelegatingDisposable> ourInstances =
        new IdentityHashMap<Disposable, DelegatingDisposable>();
    private final TObjectIntHashMap<VirtualFilePointerImpl> myCounts =
        new TObjectIntHashMap<VirtualFilePointerImpl>();
    private final Disposable myParent;

    private DelegatingDisposable(Disposable parent) {
      myParent = parent;
    }

    static void registerDisposable(Disposable parentDisposable, VirtualFilePointerImpl pointer) {
      synchronized (ourInstances) {
        DelegatingDisposable result = ourInstances.get(parentDisposable);
        if (result == null) {
          ourInstances.put(parentDisposable, result = new DelegatingDisposable(parentDisposable));
          Disposer.register(parentDisposable, result);
        }

        result.myCounts.put(pointer, result.myCounts.get(pointer) + 1);
      }
    }

    @Override
    public void dispose() {
      synchronized (ourInstances) {
        ourInstances.remove(myParent);

        for (Object o : myCounts.keys()) {
          VirtualFilePointerImpl pointer = (VirtualFilePointerImpl) o;
          int disposeCount = myCounts.get(pointer);
          int after = pointer.myNode.incrementUsageCount(-disposeCount + 1);
          LOG.assertTrue(after > 0, after);
          pointer.dispose();
        }
      }
    }
  }

  @TestOnly
  int numberOfPointers() {
    int number = 0;
    for (FilePointerPartNode root : myPointers.values()) {
      number = root.getPointersUnder();
    }
    return number;
  }

  @TestOnly
  int numberOfListeners() {
    return myPointers.keySet().size();
  }
}