blob: 6e135018d35626b0b07ae7c725f6654f4c77169c [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.newvfs.persistent;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ex.ApplicationEx;
import com.intellij.openapi.components.ApplicationComponent;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.FileTypeRegistry;
import com.intellij.openapi.fileTypes.FileTypes;
import com.intellij.openapi.util.LowMemoryWatcher;
import com.intellij.openapi.util.ShutDownTracker;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.*;
import com.intellij.openapi.vfs.*;
import com.intellij.openapi.vfs.ex.temp.TempFileSystem;
import com.intellij.openapi.vfs.newvfs.*;
import com.intellij.openapi.vfs.newvfs.events.*;
import com.intellij.openapi.vfs.newvfs.impl.*;
import com.intellij.util.*;
import com.intellij.util.containers.ConcurrentIntObjectMap;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.EmptyIntHashSet;
import com.intellij.util.containers.StripedLockIntObjectConcurrentHashMap;
import com.intellij.util.io.ReplicatorInputStream;
import com.intellij.util.io.URLUtil;
import com.intellij.util.messages.MessageBus;
import gnu.trove.*;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import java.io.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @author max
*/
public class PersistentFSImpl extends PersistentFS implements ApplicationComponent {
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vfs.newvfs.persistent.PersistentFS");
private final MessageBus myEventBus;
private final ReadWriteLock myRootsLock = new ReentrantReadWriteLock();
private final Map<String, VirtualFileSystemEntry> myRoots = ContainerUtil.newTroveMap(FileUtil.PATH_HASHING_STRATEGY);
private final TIntObjectHashMap<VirtualFileSystemEntry> myRootsById = new TIntObjectHashMap<VirtualFileSystemEntry>();
private final ConcurrentIntObjectMap<VirtualFileSystemEntry> myIdToDirCache = new StripedLockIntObjectConcurrentHashMap<VirtualFileSystemEntry>();
private final Object myInputLock = new Object();
private final AtomicBoolean myShutDown = new AtomicBoolean(false);
@SuppressWarnings("FieldCanBeLocal")
private final LowMemoryWatcher myWatcher = LowMemoryWatcher.register(new Runnable() {
@Override
public void run() {
clearIdCache();
}
});
public PersistentFSImpl(@NotNull MessageBus bus) {
myEventBus = bus;
ShutDownTracker.getInstance().registerShutdownTask(new Runnable() {
@Override
public void run() {
performShutdown();
}
});
}
@Override
public void initComponent() {
FSRecords.connect();
}
@Override
public void disposeComponent() {
performShutdown();
}
private void performShutdown() {
if (myShutDown.compareAndSet(false, true)) {
LOG.info("VFS dispose started");
FSRecords.dispose();
LOG.info("VFS dispose completed");
}
}
@Override
@NonNls
@NotNull
public String getComponentName() {
return "app.component.PersistentFS";
}
@Override
public boolean areChildrenLoaded(@NotNull final VirtualFile dir) {
return areChildrenLoaded(getFileId(dir));
}
@Override
public long getCreationTimestamp() {
return FSRecords.getCreationTimestamp();
}
@NotNull
private static NewVirtualFileSystem getDelegate(@NotNull VirtualFile file) {
return (NewVirtualFileSystem)file.getFileSystem();
}
@Override
public boolean wereChildrenAccessed(@NotNull final VirtualFile dir) {
return FSRecords.wereChildrenAccessed(getFileId(dir));
}
@Override
@NotNull
public String[] list(@NotNull final VirtualFile file) {
int id = getFileId(file);
FSRecords.NameId[] nameIds = FSRecords.listAll(id);
if (!areChildrenLoaded(id)) {
nameIds = persistAllChildren(file, id, nameIds);
}
return ContainerUtil.map2Array(nameIds, String.class, new Function<FSRecords.NameId, String>() {
@Override
public String fun(FSRecords.NameId id) {
return id.name.toString();
}
});
}
@Override
@NotNull
public String[] listPersisted(@NotNull VirtualFile parent) {
return listPersisted(FSRecords.list(getFileId(parent)));
}
@NotNull
private static String[] listPersisted(@NotNull int[] childrenIds) {
String[] names = ArrayUtil.newStringArray(childrenIds.length);
for (int i = 0; i < childrenIds.length; i++) {
names[i] = FSRecords.getName(childrenIds[i]);
}
return names;
}
@NotNull
private static FSRecords.NameId[] persistAllChildren(@NotNull final VirtualFile file, final int id, @NotNull FSRecords.NameId[] current) {
final NewVirtualFileSystem fs = replaceWithNativeFS(getDelegate(file));
String[] delegateNames = VfsUtil.filterNames(fs.list(file));
if (delegateNames.length == 0 && current.length > 0) {
return current;
}
Set<String> toAdd = ContainerUtil.newHashSet(delegateNames);
for (FSRecords.NameId nameId : current) {
toAdd.remove(nameId.name.toString());
}
final TIntArrayList childrenIds = new TIntArrayList(current.length + toAdd.size());
final List<FSRecords.NameId> nameIds = ContainerUtil.newArrayListWithCapacity(current.length + toAdd.size());
for (FSRecords.NameId nameId : current) {
childrenIds.add(nameId.id);
nameIds.add(nameId);
}
for (String newName : toAdd) {
FakeVirtualFile child = new FakeVirtualFile(file, newName);
FileAttributes attributes = fs.getAttributes(child);
if (attributes != null) {
int childId = createAndFillRecord(fs, child, id, attributes);
childrenIds.add(childId);
nameIds.add(new FSRecords.NameId(childId, FileNameCache.storeName(newName), newName));
}
}
FSRecords.updateList(id, childrenIds.toNativeArray());
setChildrenCached(id);
return nameIds.toArray(new FSRecords.NameId[nameIds.size()]);
}
public static void setChildrenCached(int id) {
int flags = FSRecords.getFlags(id);
FSRecords.setFlags(id, flags | CHILDREN_CACHED_FLAG, true);
}
@Override
@NotNull
public FSRecords.NameId[] listAll(@NotNull VirtualFile parent) {
final int parentId = getFileId(parent);
FSRecords.NameId[] nameIds = FSRecords.listAll(parentId);
if (!areChildrenLoaded(parentId)) {
return persistAllChildren(parent, parentId, nameIds);
}
return nameIds;
}
private static boolean areChildrenLoaded(final int parentId) {
return (FSRecords.getFlags(parentId) & CHILDREN_CACHED_FLAG) != 0;
}
@Override
@Nullable
public DataInputStream readAttribute(@NotNull final VirtualFile file, @NotNull final FileAttribute att) {
return FSRecords.readAttributeWithLock(getFileId(file), att);
}
@Override
@NotNull
public DataOutputStream writeAttribute(@NotNull final VirtualFile file, @NotNull final FileAttribute att) {
return FSRecords.writeAttribute(getFileId(file), att);
}
@Nullable
private static DataInputStream readContent(@NotNull VirtualFile file) {
return FSRecords.readContent(getFileId(file));
}
@Nullable
private static DataInputStream readContentById(int contentId) {
return FSRecords.readContentById(contentId);
}
@NotNull
private static DataOutputStream writeContent(@NotNull VirtualFile file, boolean readOnly) {
return FSRecords.writeContent(getFileId(file), readOnly);
}
private static void writeContent(@NotNull VirtualFile file, ByteSequence content, boolean readOnly) throws IOException {
FSRecords.writeContent(getFileId(file), content, readOnly);
}
@Override
public int storeUnlinkedContent(@NotNull byte[] bytes) {
return FSRecords.storeUnlinkedContent(bytes);
}
@Override
public int getModificationCount(@NotNull final VirtualFile file) {
return FSRecords.getModCount(getFileId(file));
}
@Override
public int getCheapFileSystemModificationCount() {
return FSRecords.getLocalModCount();
}
@Override
public int getFilesystemModificationCount() {
return FSRecords.getModCount();
}
private static boolean writeAttributesToRecord(final int id,
final int parentId,
@NotNull VirtualFile file,
@NotNull NewVirtualFileSystem fs,
@NotNull FileAttributes attributes) {
String name = file.getName();
if (!name.isEmpty()) {
if (namesEqual(fs, name, FSRecords.getName(id))) return false; // TODO: Handle root attributes change.
}
else {
if (areChildrenLoaded(id)) return false; // TODO: hack
}
FSRecords.writeAttributesToRecord(id, parentId, attributes, name);
return true;
}
@Override
public int getFileAttributes(int id) {
assert id > 0;
//noinspection MagicConstant
return FSRecords.getFlags(id);
}
@Override
public boolean isDirectory(@NotNull final VirtualFile file) {
return isDirectory(getFileAttributes(getFileId(file)));
}
private static int getParent(final int id) {
assert id > 0;
return FSRecords.getParent(id);
}
private static boolean namesEqual(@NotNull VirtualFileSystem fs, @NotNull String n1, String n2) {
return fs.isCaseSensitive() ? n1.equals(n2) : n1.equalsIgnoreCase(n2);
}
@Override
public boolean exists(@NotNull final VirtualFile fileOrDirectory) {
return ((VirtualFileWithId)fileOrDirectory).getId() > 0;
}
@Override
public long getTimeStamp(@NotNull final VirtualFile file) {
return FSRecords.getTimestamp(getFileId(file));
}
@Override
public void setTimeStamp(@NotNull final VirtualFile file, final long modStamp) throws IOException {
final int id = getFileId(file);
FSRecords.setTimestamp(id, modStamp);
getDelegate(file).setTimeStamp(file, modStamp);
}
private static int getFileId(@NotNull VirtualFile file) {
final int id = ((VirtualFileWithId)file).getId();
if (id <= 0) {
throw new InvalidVirtualFileAccessException(file);
}
return id;
}
@Override
public boolean isSymLink(@NotNull VirtualFile file) {
return isSymLink(getFileAttributes(getFileId(file)));
}
@Override
public String resolveSymLink(@NotNull VirtualFile file) {
throw new UnsupportedOperationException();
}
@Override
public boolean isSpecialFile(@NotNull VirtualFile file) {
return isSpecialFile(getFileAttributes(getFileId(file)));
}
@Override
public boolean isWritable(@NotNull VirtualFile file) {
return (getFileAttributes(getFileId(file)) & IS_READ_ONLY) == 0;
}
@Override
public boolean isHidden(@NotNull VirtualFile file) {
return (getFileAttributes(getFileId(file)) & IS_HIDDEN) != 0;
}
@Override
public void setWritable(@NotNull final VirtualFile file, final boolean writableFlag) throws IOException {
getDelegate(file).setWritable(file, writableFlag);
boolean oldWritable = isWritable(file);
if (oldWritable != writableFlag) {
processEvent(new VFilePropertyChangeEvent(this, file, VirtualFile.PROP_WRITABLE, oldWritable, writableFlag, false));
}
}
@Override
public int getId(@NotNull VirtualFile parent, @NotNull String childName, @NotNull NewVirtualFileSystem fs) {
int parentId = getFileId(parent);
int[] children = FSRecords.list(parentId);
if (children.length > 0) {
// fast path, check that some child has same nameId as given name, this avoid O(N) on retrieving names for processing non-cached children
int nameId = FSRecords.getNameId(childName);
for (final int childId : children) {
if (nameId == FSRecords.getNameId(childId)) {
return childId;
}
}
// for case sensitive system the above check is exhaustive in consistent state of vfs
}
for (final int childId : children) {
if (namesEqual(fs, childName, FSRecords.getName(childId))) return childId;
}
final VirtualFile fake = new FakeVirtualFile(parent, childName);
final FileAttributes attributes = fs.getAttributes(fake);
if (attributes != null) {
final int child = createAndFillRecord(fs, fake, parentId, attributes);
FSRecords.updateList(parentId, ArrayUtil.append(children, child));
return child;
}
return 0;
}
@Override
public long getLength(@NotNull final VirtualFile file) {
long len;
if (mustReloadContent(file)) {
len = reloadLengthFromDelegate(file, getDelegate(file));
}
else {
final int id = getFileId(file);
len = FSRecords.getLength(id);
}
return len;
}
@NotNull
@Override
public VirtualFile copyFile(Object requestor, @NotNull VirtualFile file, @NotNull VirtualFile parent, @NotNull String name) throws IOException {
getDelegate(file).copyFile(requestor, file, parent, name);
processEvent(new VFileCopyEvent(requestor, file, parent, name));
final VirtualFile child = parent.findChild(name);
if (child == null) {
throw new IOException("Cannot create child");
}
return child;
}
@NotNull
@Override
public VirtualFile createChildDirectory(Object requestor, @NotNull VirtualFile parent, @NotNull String dir) throws IOException {
getDelegate(parent).createChildDirectory(requestor, parent, dir);
processEvent(new VFileCreateEvent(requestor, parent, dir, true, false));
final VirtualFile child = parent.findChild(dir);
if (child == null) {
throw new IOException("Cannot create child directory '" + dir + "' at " + parent.getPath());
}
return child;
}
@NotNull
@Override
public VirtualFile createChildFile(Object requestor, @NotNull VirtualFile parent, @NotNull String file) throws IOException {
getDelegate(parent).createChildFile(requestor, parent, file);
processEvent(new VFileCreateEvent(requestor, parent, file, false, false));
final VirtualFile child = parent.findChild(file);
if (child == null) {
throw new IOException("Cannot create child file '" + file + "' at " + parent.getPath());
}
return child;
}
@Override
public void deleteFile(final Object requestor, @NotNull final VirtualFile file) throws IOException {
final NewVirtualFileSystem delegate = getDelegate(file);
delegate.deleteFile(requestor, file);
if (!delegate.exists(file)) {
processEvent(new VFileDeleteEvent(requestor, file, false));
}
}
@Override
public void renameFile(final Object requestor, @NotNull VirtualFile file, @NotNull String newName) throws IOException {
getDelegate(file).renameFile(requestor, file, newName);
String oldName = file.getName();
if (!newName.equals(oldName)) {
processEvent(new VFilePropertyChangeEvent(requestor, file, VirtualFile.PROP_NAME, oldName, newName, false));
}
}
@Override
@NotNull
public byte[] contentsToByteArray(@NotNull final VirtualFile file) throws IOException {
return contentsToByteArray(file, true);
}
@Override
@NotNull
public byte[] contentsToByteArray(@NotNull final VirtualFile file, boolean cacheContent) throws IOException {
InputStream contentStream = null;
boolean reloadFromDelegate;
boolean outdated;
int fileId;
synchronized (myInputLock) {
fileId = getFileId(file);
outdated = checkFlag(fileId, MUST_RELOAD_CONTENT) || FSRecords.getLength(fileId) == -1L;
reloadFromDelegate = outdated || (contentStream = readContent(file)) == null;
}
if (reloadFromDelegate) {
final NewVirtualFileSystem delegate = getDelegate(file);
final byte[] content;
if (outdated) {
// in this case, file can have out-of-date length. so, update it first (it's needed for correct contentsToByteArray() work)
// see IDEA-90813 for possible bugs
FSRecords.setLength(fileId, delegate.getLength(file));
content = delegate.contentsToByteArray(file);
}
else {
// a bit of optimization
content = delegate.contentsToByteArray(file);
FSRecords.setLength(fileId, content.length);
}
ApplicationEx application = (ApplicationEx)ApplicationManager.getApplication();
// we should cache every local files content
// because the local history feature is currently depends on this cache,
// perforce offline mode as well
if ((!delegate.isReadOnly() ||
// do not cache archive content unless asked
cacheContent && !application.isInternal() && !application.isUnitTestMode()) &&
content.length <= PersistentFSConstants.FILE_LENGTH_TO_CACHE_THRESHOLD) {
synchronized (myInputLock) {
writeContent(file, new ByteSequence(content), delegate.isReadOnly());
setFlag(file, MUST_RELOAD_CONTENT, false);
}
}
return content;
}
else {
try {
final int length = (int)file.getLength();
assert length >= 0 : file;
return FileUtil.loadBytes(contentStream, length);
}
catch (IOException e) {
throw FSRecords.handleError(e);
}
}
}
@Override
@NotNull
public byte[] contentsToByteArray(int contentId) throws IOException {
final DataInputStream stream = readContentById(contentId);
assert stream != null : contentId;
return FileUtil.loadBytes(stream);
}
@Override
@NotNull
public InputStream getInputStream(@NotNull final VirtualFile file) throws IOException {
synchronized (myInputLock) {
InputStream contentStream;
if (mustReloadContent(file) || (contentStream = readContent(file)) == null) {
NewVirtualFileSystem delegate = getDelegate(file);
long len = reloadLengthFromDelegate(file, delegate);
InputStream nativeStream = delegate.getInputStream(file);
if (len > PersistentFSConstants.FILE_LENGTH_TO_CACHE_THRESHOLD) return nativeStream;
return createReplicator(file, nativeStream, len, delegate.isReadOnly());
}
else {
return contentStream;
}
}
}
private static long reloadLengthFromDelegate(@NotNull VirtualFile file, @NotNull NewVirtualFileSystem delegate) {
final long len = delegate.getLength(file);
FSRecords.setLength(getFileId(file), len);
return len;
}
private InputStream createReplicator(@NotNull final VirtualFile file,
final InputStream nativeStream,
final long fileLength,
final boolean readOnly) throws IOException {
if (nativeStream instanceof BufferExposingByteArrayInputStream) {
// optimization
BufferExposingByteArrayInputStream byteStream = (BufferExposingByteArrayInputStream )nativeStream;
byte[] bytes = byteStream.getInternalBuffer();
storeContentToStorage(fileLength, file, readOnly, bytes, bytes.length);
return nativeStream;
}
@SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
final BufferExposingByteArrayOutputStream cache = new BufferExposingByteArrayOutputStream((int)fileLength);
return new ReplicatorInputStream(nativeStream, cache) {
@Override
public void close() throws IOException {
super.close();
storeContentToStorage(fileLength, file, readOnly, cache.getInternalBuffer(), cache.size());
}
};
}
private void storeContentToStorage(long fileLength,
@NotNull VirtualFile file,
boolean readOnly, @NotNull byte[] bytes, int bytesLength)
throws IOException {
synchronized (myInputLock) {
if (bytesLength == fileLength) {
writeContent(file, new ByteSequence(bytes, 0, bytesLength), readOnly);
setFlag(file, MUST_RELOAD_CONTENT, false);
}
else {
setFlag(file, MUST_RELOAD_CONTENT, true);
}
}
}
private static boolean mustReloadContent(@NotNull VirtualFile file) {
int fileId = getFileId(file);
return checkFlag(fileId, MUST_RELOAD_CONTENT) || FSRecords.getLength(fileId) == -1L;
}
@Override
@NotNull
public OutputStream getOutputStream(@NotNull final VirtualFile file,
final Object requestor,
final long modStamp,
final long timeStamp) throws IOException {
return new ByteArrayOutputStream() {
private boolean closed; // protection against user calling .close() twice
@Override
public void close() throws IOException {
if (closed) return;
super.close();
VFileContentChangeEvent event = new VFileContentChangeEvent(requestor, file, file.getModificationStamp(), modStamp, false);
List<VFileContentChangeEvent> events = Collections.singletonList(event);
BulkFileListener publisher = myEventBus.syncPublisher(VirtualFileManager.VFS_CHANGES);
publisher.before(events);
NewVirtualFileSystem delegate = getDelegate(file);
OutputStream ioFileStream = delegate.getOutputStream(file, requestor, modStamp, timeStamp);
// FSRecords.ContentOutputStream already buffered, no need to wrap in BufferedStream
OutputStream persistenceStream = writeContent(file, delegate.isReadOnly());
try {
persistenceStream.write(buf, 0, count);
}
finally {
try {
ioFileStream.write(buf, 0, count);
}
finally {
closed = true;
persistenceStream.close();
ioFileStream.close();
executeTouch(file, false, event.getModificationStamp());
publisher.after(events);
}
}
}
};
}
@Override
public int acquireContent(@NotNull VirtualFile file) {
return FSRecords.acquireFileContent(getFileId(file));
}
@Override
public void releaseContent(int contentId) {
FSRecords.releaseContent(contentId);
}
@Override
public int getCurrentContentId(@NotNull VirtualFile file) {
return FSRecords.getContentId(getFileId(file));
}
@Override
public void moveFile(final Object requestor, @NotNull final VirtualFile file, @NotNull final VirtualFile newParent) throws IOException {
getDelegate(file).moveFile(requestor, file, newParent);
processEvent(new VFileMoveEvent(requestor, file, newParent));
}
private void processEvent(@NotNull VFileEvent event) {
processEvents(Collections.singletonList(event));
}
private static class EventWrapper {
private final VFileDeleteEvent event;
private final int id;
private EventWrapper(final VFileDeleteEvent event, final int id) {
this.event = event;
this.id = id;
}
}
@NotNull private static final Comparator<EventWrapper> DEPTH_COMPARATOR = new Comparator<EventWrapper>() {
@Override
public int compare(@NotNull final EventWrapper o1, @NotNull final EventWrapper o2) {
return o1.event.getFileDepth() - o2.event.getFileDepth();
}
};
@NotNull
private static List<VFileEvent> validateEvents(@NotNull List<VFileEvent> events) {
final List<EventWrapper> deletionEvents = ContainerUtil.newArrayList();
for (int i = 0, size = events.size(); i < size; i++) {
final VFileEvent event = events.get(i);
if (event instanceof VFileDeleteEvent && event.isValid()) {
deletionEvents.add(new EventWrapper((VFileDeleteEvent)event, i));
}
}
final TIntHashSet invalidIDs;
if (deletionEvents.isEmpty()) {
invalidIDs = EmptyIntHashSet.INSTANCE;
}
else {
ContainerUtil.quickSort(deletionEvents, DEPTH_COMPARATOR);
invalidIDs = new TIntHashSet(deletionEvents.size());
final Set<VirtualFile> dirsToBeDeleted = new THashSet<VirtualFile>(deletionEvents.size());
nextEvent:
for (EventWrapper wrapper : deletionEvents) {
final VirtualFile candidate = wrapper.event.getFile();
VirtualFile parent = candidate;
while (parent != null) {
if (dirsToBeDeleted.contains(parent)) {
invalidIDs.add(wrapper.id);
continue nextEvent;
}
parent = parent.getParent();
}
if (candidate.isDirectory()) {
dirsToBeDeleted.add(candidate);
}
}
}
final List<VFileEvent> filtered = new ArrayList<VFileEvent>(events.size() - invalidIDs.size());
for (int i = 0, size = events.size(); i < size; i++) {
final VFileEvent event = events.get(i);
if (event.isValid() && !(event instanceof VFileDeleteEvent && invalidIDs.contains(i))) {
filtered.add(event);
}
}
return filtered;
}
@Override
public void processEvents(@NotNull List<VFileEvent> events) {
ApplicationManager.getApplication().assertWriteAccessAllowed();
List<VFileEvent> validated = validateEvents(events);
BulkFileListener publisher = myEventBus.syncPublisher(VirtualFileManager.VFS_CHANGES);
publisher.before(validated);
THashMap<VirtualFile, List<VFileEvent>> parentToChildrenEventsChanges = null;
for (VFileEvent event : validated) {
VirtualFile changedParent = null;
if (event instanceof VFileCreateEvent) {
changedParent = ((VFileCreateEvent)event).getParent();
((VFileCreateEvent)event).resetCache();
}
else if (event instanceof VFileDeleteEvent) {
changedParent = ((VFileDeleteEvent)event).getFile().getParent();
}
if (changedParent != null) {
if (parentToChildrenEventsChanges == null) parentToChildrenEventsChanges = new THashMap<VirtualFile, List<VFileEvent>>();
List<VFileEvent> parentChildrenChanges = parentToChildrenEventsChanges.get(changedParent);
if (parentChildrenChanges == null) {
parentToChildrenEventsChanges.put(changedParent, parentChildrenChanges = new SmartList<VFileEvent>());
}
parentChildrenChanges.add(event);
}
else {
applyEvent(event);
}
}
if (parentToChildrenEventsChanges != null) {
parentToChildrenEventsChanges.forEachEntry(new TObjectObjectProcedure<VirtualFile, List<VFileEvent>>() {
@Override
public boolean execute(VirtualFile parent, List<VFileEvent> childrenEvents) {
applyChildrenChangeEvents(parent, childrenEvents);
return true;
}
});
parentToChildrenEventsChanges.clear();
}
publisher.after(validated);
}
private void applyChildrenChangeEvents(VirtualFile parent, List<VFileEvent> events) {
final NewVirtualFileSystem delegate = getDelegate(parent);
TIntArrayList childrenIdsUpdated = new TIntArrayList();
List<VirtualFile> childrenToBeUpdated = new SmartList<VirtualFile>();
final int parentId = getFileId(parent);
assert parentId != 0;
TIntHashSet parentChildrenIds = new TIntHashSet(FSRecords.list(parentId));
boolean hasRemovedChildren = false;
for (VFileEvent event : events) {
if (event instanceof VFileCreateEvent) {
String name = ((VFileCreateEvent)event).getChildName();
final VirtualFile fake = new FakeVirtualFile(parent, name);
final FileAttributes attributes = delegate.getAttributes(fake);
if (attributes != null) {
final int childId = createAndFillRecord(delegate, fake, parentId, attributes);
assert parent instanceof VirtualDirectoryImpl : parent;
final VirtualDirectoryImpl dir = (VirtualDirectoryImpl)parent;
VirtualFileSystemEntry child = dir.createChild(name, childId, dir.getFileSystem());
childrenToBeUpdated.add(child);
childrenIdsUpdated.add(childId);
parentChildrenIds.add(childId);
}
}
else if (event instanceof VFileDeleteEvent) {
VirtualFile file = ((VFileDeleteEvent)event).getFile();
if (!file.exists()) {
LOG.error("Deleting a file, which does not exist: " + file.getPath());
continue;
}
hasRemovedChildren = true;
int id = getFileId(file);
childrenToBeUpdated.add(file);
childrenIdsUpdated.add(-id);
parentChildrenIds.remove(id);
}
}
FSRecords.updateList(parentId, parentChildrenIds.toArray());
if (hasRemovedChildren) clearIdCache();
VirtualDirectoryImpl parentImpl = (VirtualDirectoryImpl)parent;
for (int i = 0, len = childrenIdsUpdated.size(); i < len; ++i) {
final int childId = childrenIdsUpdated.get(i);
final VirtualFile childFile = childrenToBeUpdated.get(i);
if (childId > 0) {
parentImpl.addChild((VirtualFileSystemEntry)childFile);
}
else {
FSRecords.deleteRecordRecursively(-childId);
parentImpl.removeChild(childFile);
invalidateSubtree(childFile);
}
}
}
@Override
@Nullable
public VirtualFileSystemEntry findRoot(@NotNull String basePath, @NotNull NewVirtualFileSystem fs) {
if (basePath.isEmpty()) {
LOG.error("Invalid root, fs=" + fs);
return null;
}
String rootUrl = normalizeRootUrl(basePath, fs);
myRootsLock.readLock().lock();
try {
VirtualFileSystemEntry root = myRoots.get(rootUrl);
if (root != null) return root;
}
finally {
myRootsLock.readLock().unlock();
}
final VirtualFileSystemEntry newRoot;
int rootId = FSRecords.findRootRecord(rootUrl);
VfsData.Segment segment = VfsData.getSegment(rootId, true);
VfsData.DirectoryData directoryData = new VfsData.DirectoryData();
if (fs instanceof JarFileSystem) {
String parentPath = basePath.substring(0, basePath.indexOf(JarFileSystem.JAR_SEPARATOR));
VirtualFile parentFile = LocalFileSystem.getInstance().findFileByPath(parentPath);
if (parentFile == null) return null;
FileType type = FileTypeRegistry.getInstance().getFileTypeByFileName(parentFile.getName());
if (type != FileTypes.ARCHIVE) return null;
newRoot = new JarRoot(fs, rootId, segment, directoryData, parentFile);
}
else {
newRoot = new FsRoot(fs, rootId, segment, directoryData, basePath);
}
FileAttributes attributes = fs.getAttributes(new StubVirtualFile() {
@NotNull
@Override
public String getPath() {
return newRoot.getPath();
}
@Nullable
@Override
public VirtualFile getParent() {
return null;
}
});
if (attributes == null || !attributes.isDirectory()) {
return null;
}
boolean mark = false;
myRootsLock.writeLock().lock();
try {
VirtualFileSystemEntry root = myRoots.get(rootUrl);
if (root != null) return root;
VfsData.initFile(rootId, segment, -1, directoryData);
mark = writeAttributesToRecord(rootId, 0, newRoot, fs, attributes);
myRoots.put(rootUrl, newRoot);
myRootsById.put(rootId, newRoot);
}
finally {
myRootsLock.writeLock().unlock();
}
if (!mark && attributes.lastModified != FSRecords.getTimestamp(rootId)) {
newRoot.markDirtyRecursively();
}
LOG.assertTrue(rootId == newRoot.getId(), "root=" + newRoot + " expected=" + rootId + " actual=" + newRoot.getId());
return newRoot;
}
@NotNull
private static String normalizeRootUrl(@NotNull String basePath, @NotNull NewVirtualFileSystem fs) {
// need to protect against relative path of the form "/x/../y"
return UriUtil.trimTrailingSlashes(
fs.getProtocol() + URLUtil.SCHEME_SEPARATOR + VfsImplUtil.normalize(fs, FileUtil.toCanonicalPath(basePath)));
}
@Override
public void clearIdCache() {
myIdToDirCache.clear();
}
private static final int DEPTH_LIMIT = 75;
@Override
@Nullable
public NewVirtualFile findFileById(final int id) {
return findFileById(id, false, null, 0);
}
@Override
public NewVirtualFile findFileByIdIfCached(final int id) {
return findFileById(id, true, null, 0);
}
@Nullable
private VirtualFileSystemEntry findFileById(int id, boolean cachedOnly, TIntArrayList visited, int mask) {
VirtualFileSystemEntry cached = myIdToDirCache.get(id);
if (cached != null) return cached;
if (visited != null && (visited.size() >= DEPTH_LIMIT || (mask & id) == id && visited.contains(id))) {
@NonNls String sb = "Dead loop detected in persistent FS (id=" + id + " cached-only=" + cachedOnly + "):";
for (int i = 0; i < visited.size(); i++) {
int _id = visited.get(i);
sb += "\n " + _id + " '" + getName(_id) + "' " +
String.format("%02x", getFileAttributes(_id)) + ' ' + myIdToDirCache.containsKey(_id);
}
LOG.error(sb);
return null;
}
int parentId = getParent(id);
if (parentId >= id) {
if (visited == null) visited = new TIntArrayList(DEPTH_LIMIT);
}
if (visited != null) visited.add(id);
VirtualFileSystemEntry result;
if (parentId == 0) {
myRootsLock.readLock().lock();
try {
result = myRootsById.get(id);
}
finally {
myRootsLock.readLock().unlock();
}
}
else {
VirtualFileSystemEntry parentFile = findFileById(parentId, cachedOnly, visited, mask | id);
if (parentFile instanceof VirtualDirectoryImpl) {
result = ((VirtualDirectoryImpl)parentFile).findChildById(id, cachedOnly);
}
else {
result = null;
}
}
if (result != null && result.isDirectory()) {
VirtualFileSystemEntry old = myIdToDirCache.put(id, result);
if (old != null) result = old;
}
return result;
}
@Override
@NotNull
public VirtualFile[] getRoots() {
myRootsLock.readLock().lock();
try {
Collection<VirtualFileSystemEntry> roots = myRoots.values();
return VfsUtilCore.toVirtualFileArray(roots);
}
finally {
myRootsLock.readLock().unlock();
}
}
@Override
@NotNull
public VirtualFile[] getRoots(@NotNull final NewVirtualFileSystem fs) {
final List<VirtualFile> roots = new ArrayList<VirtualFile>();
myRootsLock.readLock().lock();
try {
for (NewVirtualFile root : myRoots.values()) {
if (root.getFileSystem() == fs) {
roots.add(root);
}
}
}
finally {
myRootsLock.readLock().unlock();
}
return VfsUtilCore.toVirtualFileArray(roots);
}
@Override
@NotNull
public VirtualFile[] getLocalRoots() {
List<VirtualFile> roots = ContainerUtil.newSmartList();
myRootsLock.readLock().lock();
try {
for (NewVirtualFile root : myRoots.values()) {
if (root.isInLocalFileSystem() && !(root.getFileSystem() instanceof TempFileSystem)) {
roots.add(root);
}
}
}
finally {
myRootsLock.readLock().unlock();
}
return VfsUtilCore.toVirtualFileArray(roots);
}
private VirtualFileSystemEntry applyEvent(@NotNull VFileEvent event) {
try {
if (event instanceof VFileCreateEvent) {
final VFileCreateEvent createEvent = (VFileCreateEvent)event;
return executeCreateChild(createEvent.getParent(), createEvent.getChildName());
}
else if (event instanceof VFileDeleteEvent) {
final VFileDeleteEvent deleteEvent = (VFileDeleteEvent)event;
executeDelete(deleteEvent.getFile());
}
else if (event instanceof VFileContentChangeEvent) {
final VFileContentChangeEvent contentUpdateEvent = (VFileContentChangeEvent)event;
executeTouch(contentUpdateEvent.getFile(), contentUpdateEvent.isFromRefresh(), contentUpdateEvent.getModificationStamp());
}
else if (event instanceof VFileCopyEvent) {
final VFileCopyEvent copyEvent = (VFileCopyEvent)event;
return executeCreateChild(copyEvent.getNewParent(), copyEvent.getNewChildName());
}
else if (event instanceof VFileMoveEvent) {
final VFileMoveEvent moveEvent = (VFileMoveEvent)event;
executeMove(moveEvent.getFile(), moveEvent.getNewParent());
}
else if (event instanceof VFilePropertyChangeEvent) {
final VFilePropertyChangeEvent propertyChangeEvent = (VFilePropertyChangeEvent)event;
if (VirtualFile.PROP_NAME.equals(propertyChangeEvent.getPropertyName())) {
executeRename(propertyChangeEvent.getFile(), (String)propertyChangeEvent.getNewValue());
}
else if (VirtualFile.PROP_WRITABLE.equals(propertyChangeEvent.getPropertyName())) {
executeSetWritable(propertyChangeEvent.getFile(), ((Boolean)propertyChangeEvent.getNewValue()).booleanValue());
}
else if (VirtualFile.PROP_HIDDEN.equals(propertyChangeEvent.getPropertyName())) {
executeSetHidden(propertyChangeEvent.getFile(), ((Boolean)propertyChangeEvent.getNewValue()).booleanValue());
}
else if (VirtualFile.PROP_SYMLINK_TARGET.equals(propertyChangeEvent.getPropertyName())) {
executeSetTarget(propertyChangeEvent.getFile(), (String)propertyChangeEvent.getNewValue());
}
}
}
catch (Exception e) {
// Exception applying single event should not prevent other events from applying.
LOG.error(e);
}
return null;
}
@NotNull
@NonNls
public String toString() {
return "PersistentFS";
}
private static VirtualFileSystemEntry executeCreateChild(@NotNull VirtualFile parent, @NotNull String name) {
final NewVirtualFileSystem delegate = getDelegate(parent);
final VirtualFile fake = new FakeVirtualFile(parent, name);
final FileAttributes attributes = delegate.getAttributes(fake);
if (attributes != null) {
final int parentId = getFileId(parent);
final int childId = createAndFillRecord(delegate, fake, parentId, attributes);
appendIdToParentList(parentId, childId);
assert parent instanceof VirtualDirectoryImpl : parent;
final VirtualDirectoryImpl dir = (VirtualDirectoryImpl)parent;
VirtualFileSystemEntry child = dir.createChild(name, childId, dir.getFileSystem());
dir.addChild(child);
return child;
}
return null;
}
private static int createAndFillRecord(@NotNull NewVirtualFileSystem delegateSystem,
@NotNull VirtualFile delegateFile,
int parentId,
@NotNull FileAttributes attributes) {
final int childId = FSRecords.createRecord();
writeAttributesToRecord(childId, parentId, delegateFile, delegateSystem, attributes);
return childId;
}
private static void appendIdToParentList(final int parentId, final int childId) {
int[] childrenList = FSRecords.list(parentId);
childrenList = ArrayUtil.append(childrenList, childId);
FSRecords.updateList(parentId, childrenList);
}
private void executeDelete(@NotNull VirtualFile file) {
if (!file.exists()) {
LOG.error("Deleting a file, which does not exist: " + file.getPath());
return;
}
clearIdCache();
int id = getFileId(file);
final VirtualFile parent = file.getParent();
final int parentId = parent == null ? 0 : getFileId(parent);
if (parentId == 0) {
String rootUrl = normalizeRootUrl(file.getPath(), (NewVirtualFileSystem)file.getFileSystem());
myRootsLock.writeLock().lock();
try {
myRoots.remove(rootUrl);
myRootsById.remove(id);
FSRecords.deleteRootRecord(id);
}
finally {
myRootsLock.writeLock().unlock();
}
}
else {
removeIdFromParentList(parentId, id, parent, file);
VirtualDirectoryImpl directory = (VirtualDirectoryImpl)file.getParent();
assert directory != null : file;
directory.removeChild(file);
}
FSRecords.deleteRecordRecursively(id);
invalidateSubtree(file);
}
private static void invalidateSubtree(@NotNull VirtualFile file) {
final VirtualFileSystemEntry impl = (VirtualFileSystemEntry)file;
impl.invalidate();
for (VirtualFile child : impl.getCachedChildren()) {
invalidateSubtree(child);
}
}
private static void removeIdFromParentList(final int parentId, final int id, @NotNull VirtualFile parent, VirtualFile file) {
int[] childList = FSRecords.list(parentId);
int index = ArrayUtil.indexOf(childList, id);
if (index == -1) {
throw new RuntimeException("Cannot find child (" + id + ")" + file
+ "\n\tin (" + parentId + ")" + parent
+ "\n\tactual children:" + Arrays.toString(childList));
}
childList = ArrayUtil.remove(childList, index);
FSRecords.updateList(parentId, childList);
}
private static void executeRename(@NotNull VirtualFile file, @NotNull final String newName) {
final int id = getFileId(file);
FSRecords.setName(id, newName);
((VirtualFileSystemEntry)file).setNewName(newName);
}
private static void executeSetWritable(@NotNull VirtualFile file, boolean writableFlag) {
setFlag(file, IS_READ_ONLY, !writableFlag);
((VirtualFileSystemEntry)file).updateProperty(VirtualFile.PROP_WRITABLE, writableFlag);
}
private static void executeSetHidden(@NotNull VirtualFile file, boolean hiddenFlag) {
setFlag(file, IS_HIDDEN, hiddenFlag);
((VirtualFileSystemEntry)file).updateProperty(VirtualFile.PROP_HIDDEN, hiddenFlag);
}
private static void executeSetTarget(@NotNull VirtualFile file, String target) {
((VirtualFileSystemEntry)file).setLinkTarget(target);
}
private static void setFlag(@NotNull VirtualFile file, int mask, boolean value) {
setFlag(getFileId(file), mask, value);
}
private static void setFlag(final int id, final int mask, final boolean value) {
int oldFlags = FSRecords.getFlags(id);
int flags = value ? oldFlags | mask : oldFlags & ~mask;
if (oldFlags != flags) {
FSRecords.setFlags(id, flags, true);
}
}
private static boolean checkFlag(int fileId, int mask) {
return (FSRecords.getFlags(fileId) & mask) != 0;
}
private static void executeTouch(@NotNull VirtualFile file, boolean reloadContentFromDelegate, long newModificationStamp) {
if (reloadContentFromDelegate) {
setFlag(file, MUST_RELOAD_CONTENT, true);
}
final NewVirtualFileSystem delegate = getDelegate(file);
final FileAttributes attributes = delegate.getAttributes(file);
FSRecords.setLength(getFileId(file), attributes != null ? attributes.length : DEFAULT_LENGTH);
FSRecords.setTimestamp(getFileId(file), attributes != null ? attributes.lastModified : DEFAULT_TIMESTAMP);
((VirtualFileSystemEntry)file).setModificationStamp(newModificationStamp);
}
private void executeMove(@NotNull VirtualFile file, @NotNull VirtualFile newParent) {
clearIdCache();
final int fileId = getFileId(file);
final int newParentId = getFileId(newParent);
final int oldParentId = getFileId(file.getParent());
removeIdFromParentList(oldParentId, fileId, file.getParent(), file);
FSRecords.setParent(fileId, newParentId);
appendIdToParentList(newParentId, fileId);
((VirtualFileSystemEntry)file).setParent(newParent);
}
@Override
public String getName(int id) {
assert id > 0;
return FSRecords.getName(id);
}
@TestOnly
public void cleanPersistedContents() {
final int[] roots = FSRecords.listRoots();
for (int root : roots) {
cleanPersistedContentsRecursively(root);
}
}
@TestOnly
private void cleanPersistedContentsRecursively(int id) {
if (isDirectory(getFileAttributes(id))) {
for (int child : FSRecords.list(id)) {
cleanPersistedContentsRecursively(child);
}
}
else {
setFlag(id, MUST_RELOAD_CONTENT, true);
}
}
private abstract static class AbstractRoot extends VirtualDirectoryImpl {
public AbstractRoot(int id, VfsData.Segment segment, VfsData.DirectoryData data, NewVirtualFileSystem fs) {
super(id, segment, data, null, fs);
}
@NotNull
@Override
public abstract CharSequence getNameSequence();
@Override
protected abstract char[] appendPathOnFileSystem(int accumulatedPathLength, int[] positionRef);
@Override
public void setNewName(@NotNull String newName) {
throw new IncorrectOperationException();
}
@Override
public final void setParent(@NotNull VirtualFile newParent) {
throw new IncorrectOperationException();
}
}
private static class JarRoot extends AbstractRoot {
private final VirtualFile myParentLocalFile;
private final String myParentPath;
private JarRoot(@NotNull NewVirtualFileSystem fs, int id, VfsData.Segment segment, VfsData.DirectoryData data, VirtualFile parentLocalFile) {
super(id, segment, data, fs);
myParentLocalFile = parentLocalFile;
myParentPath = myParentLocalFile.getPath();
}
@NotNull
@Override
public CharSequence getNameSequence() {
return myParentLocalFile.getName();
}
@Override
protected char[] appendPathOnFileSystem(int accumulatedPathLength, int[] positionRef) {
char[] chars = new char[myParentPath.length() + JarFileSystem.JAR_SEPARATOR.length() + accumulatedPathLength];
positionRef[0] = copyString(chars, positionRef[0], myParentPath);
positionRef[0] = copyString(chars, positionRef[0], JarFileSystem.JAR_SEPARATOR);
return chars;
}
}
private static class FsRoot extends AbstractRoot {
private final String myName;
private FsRoot(@NotNull NewVirtualFileSystem fs, int id, VfsData.Segment segment, VfsData.DirectoryData data, @NotNull String basePath) {
super(id, segment, data, fs);
myName = FileUtil.toSystemIndependentName(basePath);
}
@NotNull
@Override
public CharSequence getNameSequence() {
return myName;
}
@Override
protected char[] appendPathOnFileSystem(int pathLength, int[] position) {
String name = getName();
int nameLength = name.length();
int rootPathLength = pathLength + nameLength;
// otherwise we called this as a part of longer file path calculation and slash will be added anyway
boolean appendSlash = SystemInfo.isWindows && nameLength == 2 && name.charAt(1) == ':' && pathLength == 0;
if (appendSlash) ++rootPathLength;
char[] chars = new char[rootPathLength];
position[0] = copyString(chars, position[0], name);
if (appendSlash) {
chars[position[0]++] = '/';
}
return chars;
}
}
}