blob: bc898a49987b33bda30e943887b67e799ca70bca [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.impl;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.impl.ApplicationInfoImpl;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.FileAttributes;
import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.impl.win32.Win32LocalFileSystem;
import com.intellij.openapi.vfs.newvfs.NewVirtualFile;
import com.intellij.openapi.vfs.newvfs.NewVirtualFileSystem;
import com.intellij.openapi.vfs.newvfs.RefreshQueue;
import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent;
import com.intellij.openapi.vfs.newvfs.persistent.FSRecords;
import com.intellij.openapi.vfs.newvfs.persistent.PersistentFS;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Function;
import com.intellij.util.UriUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.keyFMap.KeyFMap;
import gnu.trove.TIntHashSet;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* @author max
*/
public class VirtualDirectoryImpl extends VirtualFileSystemEntry {
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vfs.newvfs.impl.VirtualDirectoryImpl");
public static boolean CHECK = ApplicationManager.getApplication().isUnitTestMode();
static final VirtualDirectoryImpl NULL_VIRTUAL_FILE =
new VirtualDirectoryImpl(-42, null, null, null, LocalFileSystem.getInstance()) {
@Override
public String toString() {
return "NULL";
}
};
private final VfsData.DirectoryData myData;
private final NewVirtualFileSystem myFs;
public VirtualDirectoryImpl(int id, VfsData.Segment segment, VfsData.DirectoryData data, VirtualDirectoryImpl parent, NewVirtualFileSystem fs) {
super(id, segment, parent);
myData = data;
myFs = fs;
}
@Override
@NotNull
public NewVirtualFileSystem getFileSystem() {
return myFs;
}
@Nullable
private VirtualFileSystemEntry findChild(@NotNull String name,
boolean doRefresh,
boolean ensureCanonicalName,
@NotNull NewVirtualFileSystem delegate) {
boolean ignoreCase = !delegate.isCaseSensitive();
VirtualFileSystemEntry result = doFindChild(name, ensureCanonicalName, delegate, ignoreCase);
//noinspection UseVirtualFileEquals
if (result == NULL_VIRTUAL_FILE) {
result = doRefresh ? createAndFindChildWithEventFire(name, delegate) : null;
}
else if (result != null && doRefresh && delegate.isDirectory(result) != result.isDirectory()) {
RefreshQueue.getInstance().refresh(false, false, null, result);
result = findChild(name, false, ensureCanonicalName, delegate);
}
if (result == null) {
synchronized (myData) {
addToAdoptedChildren(ignoreCase, name);
}
}
return result;
}
private void addToAdoptedChildren(final boolean ignoreCase, @NotNull final String name) {
if (myData.isAdoptedName(name)) return; //already added
if (!allChildrenLoaded()) {
myData.addAdoptedName(name, getFileSystem().isCaseSensitive());
}
int indexInReal = findIndex(myData.myChildrenIds, name, ignoreCase);
if (indexInReal >= 0) {
// there suddenly can be that we ask to add name to adopted whereas it already contains in the real part
// in this case we should remove it from there
removeFromArray(indexInReal);
}
assertConsistency(ignoreCase, name);
}
@Nullable // null if there can't be a child with this name, NULL_VIRTUAL_FILE
private VirtualFileSystemEntry doFindChildInArray(@NotNull String name, boolean ignoreCase) {
synchronized (myData) {
if (myData.isAdoptedName(name)) return NULL_VIRTUAL_FILE;
int[] array = myData.myChildrenIds;
int indexInReal = findIndex(array, name, ignoreCase);
if (indexInReal >= 0) {
return VfsData.getFileById(array[indexInReal], this);
}
return null;
}
}
@Nullable // null if there can't be a child with this name, NULL_VIRTUAL_FILE if cached as absent, the file if found
private VirtualFileSystemEntry doFindChild(@NotNull String name,
boolean ensureCanonicalName,
@NotNull NewVirtualFileSystem delegate,
boolean ignoreCase) {
if (name.isEmpty()) {
return null;
}
VirtualFileSystemEntry found = doFindChildInArray(name, ignoreCase);
if (found != null) return found;
if (allChildrenLoaded()) {
return NULL_VIRTUAL_FILE;
}
if (ensureCanonicalName) {
name = UriUtil.trimTrailingSlashes(UriUtil.trimLeadingSlashes(FileUtilRt.toSystemIndependentName(name)));
if (name.indexOf('/') != -1) return null; // name must not contain slashes in the middle
VirtualFile fake = new FakeVirtualFile(this, name);
name = delegate.getCanonicallyCasedName(fake);
if (name.isEmpty()) return null;
}
VirtualFileSystemEntry child;
synchronized (myData) {
// maybe another doFindChild() sneaked in the middle
if (myData.isAdoptedName(name)) return NULL_VIRTUAL_FILE;
int[] array = myData.myChildrenIds;
int indexInReal = findIndex(array, name, ignoreCase);
// double check
if (indexInReal >= 0) {
return VfsData.getFileById(array[indexInReal], this);
}
// do not extract getId outside the synchronized block since it will cause a concurrency problem.
int id = ourPersistence.getId(this, name, delegate);
if (id <= 0) {
return null;
}
child = createChild(FileNameCache.storeName(name), id, delegate);
int[] after = myData.myChildrenIds;
if (after != array) {
// in tests when we call assertAccessInTests it can load a huge number of files which lead to children modification
// so fall back to slow path
addChild(child);
}
else {
insertChildAt(child, indexInReal);
assertConsistency(!delegate.isCaseSensitive(), name);
}
}
if (!child.isDirectory()) {
// access check should only be called when child is actually added to the parent, otherwise it may break VirtualFilePointers validity
//noinspection TestOnlyProblems
VfsRootAccess.assertAccessInTests(child, getFileSystem());
}
return child;
}
private VirtualFileSystemEntry[] getArraySafely() {
synchronized (myData) {
return myData.getFileChildren(Math.abs(getId()), this);
}
}
@NotNull
public VirtualFileSystemEntry createChild(String name, int id, @NotNull NewVirtualFileSystem delegate) {
return createChild(FileNameCache.storeName(name), id, delegate);
}
@NotNull
private VirtualFileSystemEntry createChild(int nameId, int id, @NotNull NewVirtualFileSystem delegate) {
final int attributes = ourPersistence.getFileAttributes(id);
VfsData.Segment segment = VfsData.getSegment(id, true);
VfsData.initFile(id, segment, nameId,
PersistentFS.isDirectory(attributes) ? new VfsData.DirectoryData() : KeyFMap.EMPTY_MAP);
LOG.assertTrue(!(getFileSystem() instanceof Win32LocalFileSystem));
VirtualFileSystemEntry child = VfsData.getFileById(id, this);
assert child != null;
segment.setFlag(id, IS_SYMLINK_FLAG, PersistentFS.isSymLink(attributes));
segment.setFlag(id, IS_SPECIAL_FLAG, PersistentFS.isSpecialFile(attributes));
segment.setFlag(id, IS_WRITABLE_FLAG, PersistentFS.isWritable(attributes));
segment.setFlag(id, IS_HIDDEN_FLAG, PersistentFS.isHidden(attributes));
child.updateLinkStatus();
if (delegate.markNewFilesAsDirty()) {
child.markDirty();
}
return child;
}
@Nullable
private VirtualFileSystemEntry createAndFindChildWithEventFire(@NotNull String name, @NotNull NewVirtualFileSystem delegate) {
final VirtualFile fake = new FakeVirtualFile(this, name);
final FileAttributes attributes = delegate.getAttributes(fake);
if (attributes == null) return null;
final String realName = delegate.getCanonicallyCasedName(fake);
final VFileCreateEvent event = new VFileCreateEvent(null, this, realName, attributes.isDirectory(), true);
RefreshQueue.getInstance().processSingleEvent(event);
return findChild(realName);
}
@Override
@Nullable
public NewVirtualFile refreshAndFindChild(@NotNull String name) {
return findChild(name, true, true, getFileSystem());
}
@Override
@Nullable
public NewVirtualFile findChildIfCached(@NotNull String name) {
final boolean ignoreCase = !getFileSystem().isCaseSensitive();
VirtualFileSystemEntry found = doFindChildInArray(name, ignoreCase);
//noinspection UseVirtualFileEquals
return found == NULL_VIRTUAL_FILE ? null : found;
}
@Override
@NotNull
public Iterable<VirtualFile> iterInDbChildren() {
if (!ourPersistence.wereChildrenAccessed(this)) {
return Collections.emptyList();
}
if (!ourPersistence.areChildrenLoaded(this)) {
final String[] names = ourPersistence.listPersisted(this);
final NewVirtualFileSystem delegate = PersistentFS.replaceWithNativeFS(getFileSystem());
for (String name : names) {
findChild(name, false, false, delegate);
}
}
return getCachedChildren();
}
@Override
@NotNull
public VirtualFile[] getChildren() {
NewVirtualFileSystem delegate = getFileSystem();
final boolean ignoreCase = !delegate.isCaseSensitive();
synchronized (myData) {
if (allChildrenLoaded()) {
assertConsistency(ignoreCase);
return getArraySafely();
}
final boolean wasChildrenLoaded = ourPersistence.areChildrenLoaded(this);
final FSRecords.NameId[] childrenIds = ourPersistence.listAll(this);
int[] result;
if (childrenIds.length == 0) {
result = ArrayUtil.EMPTY_INT_ARRAY;
}
else {
Arrays.sort(childrenIds, new Comparator<FSRecords.NameId>() {
@Override
public int compare(FSRecords.NameId o1, FSRecords.NameId o2) {
CharSequence name1 = o1.name;
CharSequence name2 = o2.name;
int cmp = compareNames(name1, name2, ignoreCase);
if (cmp == 0 && name1 != name2) {
LOG.error(ourPersistence + " returned duplicate file names("+name1+","+name2+")" +
" ignoreCase: "+ignoreCase+
" SystemInfo.isFileSystemCaseSensitive: "+ SystemInfo.isFileSystemCaseSensitive+
" SystemInfo.OS: "+ SystemInfo.OS_NAME+" "+SystemInfo.OS_VERSION+
" wasChildrenLoaded: "+wasChildrenLoaded+
" in the dir: "+VirtualDirectoryImpl.this+";" +
" children: "+Arrays.toString(childrenIds));
}
return cmp;
}
});
TIntHashSet prevChildren = new TIntHashSet(myData.myChildrenIds);
result = new int[childrenIds.length];
for (int i = 0; i < childrenIds.length; i++) {
FSRecords.NameId child = childrenIds[i];
result[i] = child.id;
prevChildren.remove(child.id);
if (VfsData.getFileById(child.id, this) == null) {
createChild(child.nameId, child.id, delegate);
}
}
if (!prevChildren.isEmpty()) {
LOG.error("Loaded child disappeared: " +
"parent=" + verboseToString.fun(this) +
"; child=" + verboseToString.fun(VfsData.getFileById(prevChildren.toArray()[0], this)));
}
}
if (getId() > 0) {
myData.myChildrenIds = result;
assertConsistency(ignoreCase, childrenIds);
setChildrenLoaded();
}
return getArraySafely();
}
}
private void assertConsistency(boolean ignoreCase, @NotNull Object... details) {
if (!CHECK || ApplicationInfoImpl.isInPerformanceTest()) return;
int[] childrenIds = myData.myChildrenIds;
for (int i = 1; i < childrenIds.length; i++) {
int id = childrenIds[i];
int prev = childrenIds[i - 1];
CharSequence name = VfsData.getNameByFileId(id);
CharSequence prevName = VfsData.getNameByFileId(prev);
int cmp = compareNames(name, prevName, ignoreCase);
if (cmp <= 0) {
error(verboseToString.fun(VfsData.getFileById(prev, this)) + " is wrongly placed before " + verboseToString.fun(VfsData.getFileById(id, this)), getArraySafely(), details);
}
}
}
private static final Function<VirtualFileSystemEntry, String> verboseToString = new Function<VirtualFileSystemEntry, String>() {
@Override
public String fun(VirtualFileSystemEntry file) {
if (file == null) return "null";
//noinspection HardCodedStringLiteral
return file + " (name: '" + file.getName()
+ "', " + file.getClass()
+ ", parent: "+file.getParent()
+ "; id: "+file.getId()
+ "; FS: " +file.getFileSystem()
+ "; delegate.attrs: " +file.getFileSystem().getAttributes(file)
+ "; caseSensitive: " +file.getFileSystem().isCaseSensitive()
+ "; canonical: " +file.getFileSystem().getCanonicallyCasedName(file)
+ ") ";
}
};
private static void error(@NonNls String message, VirtualFileSystemEntry[] array, Object... details) {
String children = StringUtil.join(array, verboseToString, ",");
throw new AssertionError(
message + "; children: " + children + "\nDetails: " + ContainerUtil.map(
details, new Function<Object, Object>() {
@Override
public Object fun(Object o) {
return o instanceof Object[] ? Arrays.toString((Object[])o) : o;
}
}));
}
@Override
@Nullable
public VirtualFileSystemEntry findChild(@NotNull final String name) {
return findChild(name, false, true, getFileSystem());
}
public VirtualFileSystemEntry findChildById(int id, boolean cachedOnly) {
synchronized (myData) {
if (ArrayUtil.indexOf(myData.myChildrenIds, id) >= 0) {
return VfsData.getFileById(id, this);
}
}
if (cachedOnly) return null;
String name = ourPersistence.getName(id);
return findChild(name, false, false, getFileSystem());
}
@NotNull
@Override
public byte[] contentsToByteArray() throws IOException {
throw new IOException("Cannot get content of directory: " + this);
}
public void addChild(@NotNull VirtualFileSystemEntry child) {
final String childName = child.getName();
final boolean ignoreCase = !getFileSystem().isCaseSensitive();
synchronized (myData) {
int indexInReal = findIndex(myData.myChildrenIds, childName, ignoreCase);
myData.removeAdoptedName(childName);
if (indexInReal < 0) {
insertChildAt(child, indexInReal);
}
// else already stored
assertConsistency(ignoreCase, child);
}
}
private void insertChildAt(@NotNull VirtualFileSystemEntry file, int negativeIndex) {
@NotNull int[] array = myData.myChildrenIds;
int[] appended = new int[array.length + 1];
int i = -negativeIndex -1;
System.arraycopy(array, 0, appended, 0, i);
appended[i] = file.getId();
System.arraycopy(array, i, appended, i + 1, array.length - i);
myData.myChildrenIds = appended;
}
public void removeChild(@NotNull VirtualFile file) {
boolean ignoreCase = !getFileSystem().isCaseSensitive();
String name = file.getName();
synchronized (myData) {
addToAdoptedChildren(ignoreCase, name);
assertConsistency(ignoreCase, file);
}
}
private void removeFromArray(int index) {
myData.myChildrenIds = ArrayUtil.remove(myData.myChildrenIds, index);
}
public boolean allChildrenLoaded() {
return getFlagInt(CHILDREN_CACHED);
}
private void setChildrenLoaded() {
setFlagInt(CHILDREN_CACHED, true);
}
@NotNull
public List<String> getSuspiciousNames() {
synchronized (myData) {
return myData.getAdoptedNames();
}
}
private static int findIndex(final int[] array, @NotNull CharSequence name, boolean ignoreCase) {
int low = 0;
int high = array.length - 1;
while (low <= high) {
int mid = low + high >>> 1;
int cmp = -compareNames(VfsData.getNameByFileId(array[mid]), name, ignoreCase);
if (cmp > 0) {
low = mid + 1;
}
else if (cmp < 0) {
high = mid - 1;
}
else {
return mid; // key found
}
}
return -(low + 1); // key not found.
}
private static int compareNames(@NotNull CharSequence name1, @NotNull CharSequence name2, boolean ignoreCase) {
int d = name1.length() - name2.length();
if (d != 0) return d;
for (int i = 0; i < name1.length(); i++) {
// com.intellij.openapi.util.text.StringUtil.compare(String,String,boolean) inconsistent
d = StringUtil.compare(name1.charAt(i), name2.charAt(i), ignoreCase);
if (d != 0) return d;
}
return 0;
}
@Override
public boolean isDirectory() {
return true;
}
@Override
@NotNull
public List<VirtualFile> getCachedChildren() {
return Arrays.<VirtualFile>asList(getArraySafely());
}
@Override
public InputStream getInputStream() throws IOException {
throw new IOException("getInputStream() must not be called against a directory: " + getUrl());
}
@Override
@NotNull
public OutputStream getOutputStream(final Object requestor, final long newModificationStamp, final long newTimeStamp) throws IOException {
throw new IOException("getOutputStream() must not be called against a directory: " + getUrl());
}
@Override
public void markDirtyRecursively() {
markDirty();
markDirtyRecursivelyInternal();
}
// optimisation: do not travel up unnecessary
private void markDirtyRecursivelyInternal() {
for (VirtualFileSystemEntry child : getArraySafely()) {
child.markDirtyInternal();
if (child instanceof VirtualDirectoryImpl) {
((VirtualDirectoryImpl)child).markDirtyRecursivelyInternal();
}
}
}
@Override
protected void setUserMap(KeyFMap map) {
myData.myUserMap = map;
}
@NotNull
@Override
protected KeyFMap getUserMap() {
return myData.myUserMap;
}
@Override
protected boolean changeUserMap(KeyFMap oldMap, KeyFMap newMap) {
return myData.changeUserMap(oldMap, UserDataInterner.internUserData(newMap));
}
}