blob: 8c36b3129fbbc168e041a9bb72947389609e9b34 [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.openapi.vfs.impl;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.ApplicationComponent;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.*;
import com.intellij.openapi.vfs.ex.temp.TempFileSystem;
import com.intellij.openapi.vfs.newvfs.BulkFileListener;
import com.intellij.openapi.vfs.newvfs.events.*;
import com.intellij.openapi.vfs.pointers.VirtualFilePointer;
import com.intellij.openapi.vfs.pointers.VirtualFilePointerContainer;
import com.intellij.openapi.vfs.pointers.VirtualFilePointerListener;
import com.intellij.openapi.vfs.pointers.VirtualFilePointerManager;
import com.intellij.util.ConcurrencyUtil;
import com.intellij.util.Function;
import com.intellij.util.containers.ConcurrentHashMap;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.io.URLUtil;
import com.intellij.util.messages.MessageBus;
import gnu.trove.THashMap;
import gnu.trove.TObjectIntHashMap;
import gnu.trove.TObjectIntProcedure;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import java.util.*;
import java.util.concurrent.ConcurrentMap;
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()]);
}
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 = new ConcurrentHashMap<Disposable, DelegatingDisposable>(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();
}
}