blob: b3fc7550adfad0a29750fa44ebd979c3fa24dd61 [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;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.roots.ContentIterator;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.SystemInfoRt;
import com.intellij.openapi.util.io.BufferExposingByteArrayInputStream;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.Function;
import com.intellij.util.PathUtil;
import com.intellij.util.Processor;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.Convertor;
import com.intellij.util.containers.DistinctRootsCollection;
import com.intellij.util.io.URLUtil;
import com.intellij.util.text.StringFactory;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import static com.intellij.openapi.vfs.VirtualFileVisitor.VisitorException;
public class VfsUtilCore {
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vfs.VfsUtilCore");
public static final String LOCALHOST_URI_PATH_PREFIX = "localhost/";
public static final char VFS_SEPARATOR_CHAR = '/';
private static final String PROTOCOL_DELIMITER = ":";
/**
* Checks whether the <code>ancestor {@link com.intellij.openapi.vfs.VirtualFile}</code> is parent of <code>file
* {@link com.intellij.openapi.vfs.VirtualFile}</code>.
*
* @param ancestor the file
* @param file the file
* @param strict if <code>false</code> then this method returns <code>true</code> if <code>ancestor</code>
* and <code>file</code> are equal
* @return <code>true</code> if <code>ancestor</code> is parent of <code>file</code>; <code>false</code> otherwise
*/
public static boolean isAncestor(@NotNull VirtualFile ancestor, @NotNull VirtualFile file, boolean strict) {
if (!file.getFileSystem().equals(ancestor.getFileSystem())) return false;
VirtualFile parent = strict ? file.getParent() : file;
while (true) {
if (parent == null) return false;
if (parent.equals(ancestor)) return true;
parent = parent.getParent();
}
}
/**
* @return {@code true} if {@code file} is located under one of {@code roots} or equal to one of them
*/
public static boolean isUnder(@NotNull VirtualFile file, @Nullable Set<VirtualFile> roots) {
if (roots == null || roots.isEmpty()) return false;
VirtualFile parent = file;
while (parent != null) {
if (roots.contains(parent)) {
return true;
}
parent = parent.getParent();
}
return false;
}
/**
* @return {@code true} if {@code url} is located under one of {@code rootUrls} or equal to one of them
*/
public static boolean isUnder(@NotNull String url, @Nullable Collection<String> rootUrls) {
if (rootUrls == null || rootUrls.isEmpty()) return false;
for (String excludesUrl : rootUrls) {
if (isEqualOrAncestor(excludesUrl, url)) {
return true;
}
}
return false;
}
public static boolean isEqualOrAncestor(@NotNull String ancestorUrl, @NotNull String fileUrl) {
if (ancestorUrl.equals(fileUrl)) return true;
if (StringUtil.endsWithChar(ancestorUrl, '/')) {
return fileUrl.startsWith(ancestorUrl);
}
else {
return StringUtil.startsWithConcatenation(fileUrl, ancestorUrl, "/");
}
}
public static boolean isAncestor(@NotNull File ancestor, @NotNull File file, boolean strict) {
File parent = strict ? file.getParentFile() : file;
while (parent != null) {
if (parent.equals(ancestor)) return true;
parent = parent.getParentFile();
}
return false;
}
@Nullable
public static String getRelativePath(@NotNull VirtualFile file, @NotNull VirtualFile ancestor) {
return getRelativePath(file, ancestor, VFS_SEPARATOR_CHAR);
}
/**
* Gets the relative path of <code>file</code> to its <code>ancestor</code>. Uses <code>separator</code> for
* separating files.
*
* @param file the file
* @param ancestor parent file
* @param separator character to use as files separator
* @return the relative path or {@code null} if {@code ancestor} is not ancestor for {@code file}
*/
@Nullable
public static String getRelativePath(@NotNull VirtualFile file, @NotNull VirtualFile ancestor, char separator) {
if (!file.getFileSystem().equals(ancestor.getFileSystem())) {
return null;
}
int length = 0;
VirtualFile parent = file;
while (true) {
if (parent == null) return null;
if (parent.equals(ancestor)) break;
if (length > 0) {
length++;
}
length += parent.getNameSequence().length();
parent = parent.getParent();
}
char[] chars = new char[length];
int index = chars.length;
parent = file;
while (true) {
if (parent.equals(ancestor)) break;
if (index < length) {
chars[--index] = separator;
}
CharSequence name = parent.getNameSequence();
for (int i = name.length() - 1; i >= 0; i--) {
chars[--index] = name.charAt(i);
}
parent = parent.getParent();
}
return StringFactory.createShared(chars);
}
@Nullable
public static VirtualFile getVirtualFileForJar(@Nullable VirtualFile entryVFile) {
if (entryVFile == null) return null;
final String path = entryVFile.getPath();
final int separatorIndex = path.indexOf("!/");
if (separatorIndex < 0) return null;
String localPath = path.substring(0, separatorIndex);
return VirtualFileManager.getInstance().findFileByUrl("file://" + localPath);
}
/**
* Makes a copy of the <code>file</code> in the <code>toDir</code> folder and returns it.
*
* @param requestor any object to control who called this method. Note that
* it is considered to be an external change if <code>requestor</code> is <code>null</code>.
* See {@link com.intellij.openapi.vfs.VirtualFileEvent#getRequestor}
* @param file file to make a copy of
* @param toDir directory to make a copy in
* @return a copy of the file
* @throws java.io.IOException if file failed to be copied
*/
@NotNull
public static VirtualFile copyFile(Object requestor, @NotNull VirtualFile file, @NotNull VirtualFile toDir) throws IOException {
return copyFile(requestor, file, toDir, file.getName());
}
/**
* Makes a copy of the <code>file</code> in the <code>toDir</code> folder with the <code>newName</code> and returns it.
*
* @param requestor any object to control who called this method. Note that
* it is considered to be an external change if <code>requestor</code> is <code>null</code>.
* See {@link com.intellij.openapi.vfs.VirtualFileEvent#getRequestor}
* @param file file to make a copy of
* @param toDir directory to make a copy in
* @param newName new name of the file
* @return a copy of the file
* @throws java.io.IOException if file failed to be copied
*/
@NotNull
public static VirtualFile copyFile(Object requestor, @NotNull VirtualFile file, @NotNull VirtualFile toDir, @NotNull @NonNls String newName)
throws IOException {
final VirtualFile newChild = toDir.createChildData(requestor, newName);
// [jeka] TODO: to be discussed if the copy should have the same timestamp as the original
//OutputStream out = newChild.getOutputStream(requestor, -1, file.getActualTimeStamp());
newChild.setBinaryContent(file.contentsToByteArray());
return newChild;
}
@NotNull
public static InputStream byteStreamSkippingBOM(@NotNull byte[] buf, @NotNull VirtualFile file) throws IOException {
@SuppressWarnings("IOResourceOpenedButNotSafelyClosed") BufferExposingByteArrayInputStream stream = new BufferExposingByteArrayInputStream(buf);
return inputStreamSkippingBOM(stream, file);
}
@NotNull
public static InputStream inputStreamSkippingBOM(@NotNull InputStream stream, @NotNull VirtualFile file) throws IOException {
return CharsetToolkit.inputStreamSkippingBOM(stream);
}
@NotNull
public static OutputStream outputStreamAddingBOM(@NotNull OutputStream stream, @NotNull VirtualFile file) throws IOException {
byte[] bom = file.getBOM();
if (bom != null) {
stream.write(bom);
}
return stream;
}
public static boolean iterateChildrenRecursively(@NotNull final VirtualFile root,
@Nullable final VirtualFileFilter filter,
@NotNull final ContentIterator iterator) {
final VirtualFileVisitor.Result result = visitChildrenRecursively(root, new VirtualFileVisitor() {
@NotNull
@Override
public Result visitFileEx(@NotNull VirtualFile file) {
if (filter != null && !filter.accept(file)) return SKIP_CHILDREN;
if (!iterator.processFile(file)) return skipTo(root);
return CONTINUE;
}
});
return !Comparing.equal(result.skipToParent, root);
}
@SuppressWarnings("UnsafeVfsRecursion")
@NotNull
public static VirtualFileVisitor.Result visitChildrenRecursively(@NotNull VirtualFile file,
@NotNull VirtualFileVisitor<?> visitor) throws VisitorException {
boolean pushed = false;
try {
final boolean visited = visitor.allowVisitFile(file);
if (visited) {
VirtualFileVisitor.Result result = visitor.visitFileEx(file);
if (result.skipChildren) return result;
}
Iterable<VirtualFile> childrenIterable = null;
VirtualFile[] children = null;
try {
if (file.isValid() && visitor.allowVisitChildren(file) && !visitor.depthLimitReached()) {
childrenIterable = visitor.getChildrenIterable(file);
if (childrenIterable == null) {
children = file.getChildren();
}
}
}
catch (InvalidVirtualFileAccessException e) {
LOG.info("Ignoring: " + e.getMessage());
return VirtualFileVisitor.CONTINUE;
}
if (childrenIterable != null) {
visitor.saveValue();
pushed = true;
for (VirtualFile child : childrenIterable) {
VirtualFileVisitor.Result result = visitChildrenRecursively(child, visitor);
if (result.skipToParent != null && !Comparing.equal(result.skipToParent, child)) return result;
}
}
else if (children != null && children.length != 0) {
visitor.saveValue();
pushed = true;
for (VirtualFile child : children) {
VirtualFileVisitor.Result result = visitChildrenRecursively(child, visitor);
if (result.skipToParent != null && !Comparing.equal(result.skipToParent, child)) return result;
}
}
if (visited) {
visitor.afterChildrenVisited(file);
}
return VirtualFileVisitor.CONTINUE;
}
finally {
visitor.restoreValue(pushed);
}
}
public static <E extends Exception> VirtualFileVisitor.Result visitChildrenRecursively(@NotNull VirtualFile file,
@NotNull VirtualFileVisitor visitor,
@NotNull Class<E> eClass) throws E {
try {
return visitChildrenRecursively(file, visitor);
}
catch (VisitorException e) {
final Throwable cause = e.getCause();
if (eClass.isInstance(cause)) {
throw eClass.cast(cause);
}
throw e;
}
}
/**
* Returns {@code true} if given virtual file represents broken symbolic link (which points to non-existent file).
*/
public static boolean isBrokenLink(@NotNull VirtualFile file) {
return file.is(VFileProperty.SYMLINK) && file.getCanonicalPath() == null;
}
/**
* Returns {@code true} if given virtual file represents broken or recursive symbolic link.
*/
public static boolean isInvalidLink(@NotNull VirtualFile link) {
final VirtualFile target = link.getCanonicalFile();
return target == null || target.equals(link) || isAncestor(target, link, true);
}
@NotNull
public static String loadText(@NotNull VirtualFile file) throws IOException {
return loadText(file, (int)file.getLength());
}
@NotNull
public static String loadText(@NotNull VirtualFile file, int length) throws IOException {
InputStreamReader reader = new InputStreamReader(file.getInputStream(), file.getCharset());
try {
return StringFactory.createShared(FileUtil.loadText(reader, length));
}
finally {
reader.close();
}
}
@NotNull
public static VirtualFile[] toVirtualFileArray(@NotNull Collection<? extends VirtualFile> files) {
int size = files.size();
if (size == 0) return VirtualFile.EMPTY_ARRAY;
//noinspection SSBasedInspection
return files.toArray(new VirtualFile[size]);
}
@NotNull
public static String urlToPath(@NonNls @Nullable String url) {
if (url == null) return "";
return VirtualFileManager.extractPath(url);
}
@NotNull
public static File virtualToIoFile(@NotNull VirtualFile file) {
return new File(PathUtil.toPresentableUrl(file.getUrl()));
}
@NotNull
public static String pathToUrl(@NonNls @NotNull String path) {
return VirtualFileManager.constructUrl(URLUtil.FILE_PROTOCOL, path);
}
public static List<File> virtualToIoFiles(@NotNull Collection<VirtualFile> scope) {
return ContainerUtil.map2List(scope, new Function<VirtualFile, File>() {
@Override
public File fun(VirtualFile file) {
return virtualToIoFile(file);
}
});
}
@NotNull
public static String toIdeaUrl(@NotNull String url) {
return toIdeaUrl(url, true);
}
@NotNull
public static String toIdeaUrl(@NotNull String url, boolean removeLocalhostPrefix) {
int index = url.indexOf(":/");
if (index < 0 || (index + 2) >= url.length()) {
return url;
}
if (url.charAt(index + 2) != '/') {
String prefix = url.substring(0, index);
String suffix = url.substring(index + 2);
if (SystemInfoRt.isWindows) {
return prefix + URLUtil.SCHEME_SEPARATOR + suffix;
}
else if (removeLocalhostPrefix && prefix.equals(URLUtil.FILE_PROTOCOL) && suffix.startsWith(LOCALHOST_URI_PATH_PREFIX)) {
// sometimes (e.g. in Google Chrome for Mac) local file url is prefixed with 'localhost' so we need to remove it
return prefix + ":///" + suffix.substring(LOCALHOST_URI_PATH_PREFIX.length());
}
else {
return prefix + ":///" + suffix;
}
}
else if (url.charAt(index + 3) == '/' && SystemInfoRt.isWindows && url.regionMatches(0, StandardFileSystems.FILE_PROTOCOL_PREFIX, 0, StandardFileSystems.FILE_PROTOCOL_PREFIX.length())) {
// file:///C:/test/file.js -> file://C:/test/file.js
for (int i = index + 4; i < url.length(); i++) {
char c = url.charAt(i);
if (c == '/') {
break;
}
else if (c == ':') {
return StandardFileSystems.FILE_PROTOCOL_PREFIX + url.substring(index + 4);
}
}
return url;
}
return url;
}
@NotNull
public static String fixURLforIDEA(@NotNull String url) {
// removeLocalhostPrefix - false due to backward compatibility reasons
return toIdeaUrl(url, false);
}
@NotNull
public static String convertFromUrl(@NotNull URL url) {
String protocol = url.getProtocol();
String path = url.getPath();
if (protocol.equals(URLUtil.JAR_PROTOCOL)) {
if (StringUtil.startsWithConcatenation(path, URLUtil.FILE_PROTOCOL, PROTOCOL_DELIMITER)) {
try {
URL subURL = new URL(path);
path = subURL.getPath();
}
catch (MalformedURLException e) {
throw new RuntimeException(VfsBundle.message("url.parse.unhandled.exception"), e);
}
}
else {
throw new RuntimeException(new IOException(VfsBundle.message("url.parse.error", url.toExternalForm())));
}
}
if (SystemInfo.isWindows || SystemInfo.isOS2) {
while (!path.isEmpty() && path.charAt(0) == '/') {
path = path.substring(1, path.length());
}
}
path = URLUtil.unescapePercentSequences(path);
return protocol + "://" + path;
}
@NotNull
public static String fixIDEAUrl(@NotNull String ideaUrl ) {
int idx = ideaUrl.indexOf("://");
if( idx >= 0 ) {
String s = ideaUrl.substring(0, idx);
if (s.equals(StandardFileSystems.JAR_PROTOCOL)) {
//noinspection HardCodedStringLiteral
s = "jar:file";
}
ideaUrl = s+":/"+ideaUrl.substring(idx+3);
}
return ideaUrl;
}
@SuppressWarnings({"HardCodedStringLiteral"})
@Nullable
public static VirtualFile findRelativeFile(@NotNull String uri, @Nullable VirtualFile base) {
if (base != null) {
if (!base.isValid()){
LOG.error("Invalid file name: " + base.getName() + ", url: " + uri);
}
}
uri = uri.replace('\\', '/');
if (uri.startsWith("file:///")) {
uri = uri.substring("file:///".length());
if (!SystemInfo.isWindows) uri = "/" + uri;
}
else if (uri.startsWith("file:/")) {
uri = uri.substring("file:/".length());
if (!SystemInfo.isWindows) uri = "/" + uri;
}
else if (uri.startsWith("file:")) {
uri = uri.substring("file:".length());
}
VirtualFile file = null;
if (uri.startsWith("jar:file:/")) {
uri = uri.substring("jar:file:/".length());
if (!SystemInfo.isWindows) uri = "/" + uri;
file = VirtualFileManager.getInstance().findFileByUrl(StandardFileSystems.JAR_PROTOCOL_PREFIX + uri);
}
else {
if (!SystemInfo.isWindows && StringUtil.startsWithChar(uri, '/')) {
file = StandardFileSystems.local().findFileByPath(uri);
}
else if (SystemInfo.isWindows && uri.length() >= 2 && Character.isLetter(uri.charAt(0)) && uri.charAt(1) == ':') {
file = StandardFileSystems.local().findFileByPath(uri);
}
}
if (file == null && uri.contains(URLUtil.JAR_SEPARATOR)) {
file = StandardFileSystems.jar().findFileByPath(uri);
if (file == null && base == null) {
file = VirtualFileManager.getInstance().findFileByUrl(uri);
}
}
if (file == null) {
if (base == null) return StandardFileSystems.local().findFileByPath(uri);
if (!base.isDirectory()) base = base.getParent();
if (base == null) return StandardFileSystems.local().findFileByPath(uri);
file = VirtualFileManager.getInstance().findFileByUrl(base.getUrl() + "/" + uri);
if (file == null) return null;
}
return file;
}
public static boolean processFilesRecursively(@NotNull VirtualFile root, @NotNull Processor<VirtualFile> processor) {
if (!processor.process(root)) return false;
if (root.isDirectory()) {
final LinkedList<VirtualFile[]> queue = new LinkedList<VirtualFile[]>();
queue.add(root.getChildren());
do {
final VirtualFile[] files = queue.removeFirst();
for (VirtualFile file : files) {
if (!processor.process(file)) return false;
if (file.isDirectory()) {
queue.add(file.getChildren());
}
}
} while (!queue.isEmpty());
}
return true;
}
/**
* Gets the common ancestor for passed files, or null if the files do not have common ancestors.
*
* @param file1 fist file
* @param file2 second file
* @return common ancestor for the passed files. Returns <code>null</code> if
* the files do not have common ancestor
*/
@Nullable
public static VirtualFile getCommonAncestor(@NotNull VirtualFile file1, @NotNull VirtualFile file2) {
if (!file1.getFileSystem().equals(file2.getFileSystem())) {
return null;
}
VirtualFile[] path1 = getPathComponents(file1);
VirtualFile[] path2 = getPathComponents(file2);
int lastEqualIdx = -1;
for (int i = 0; i < path1.length && i < path2.length; i++) {
if (path1[i].equals(path2[i])) {
lastEqualIdx = i;
}
else {
break;
}
}
return lastEqualIdx == -1 ? null : path1[lastEqualIdx];
}
/**
* Gets an array of files representing paths from root to the passed file.
*
* @param file the file
* @return virtual files which represents paths from root to the passed file
*/
@NotNull
static VirtualFile[] getPathComponents(@NotNull VirtualFile file) {
ArrayList<VirtualFile> componentsList = new ArrayList<VirtualFile>();
while (file != null) {
componentsList.add(file);
file = file.getParent();
}
int size = componentsList.size();
VirtualFile[] components = new VirtualFile[size];
for (int i = 0; i < size; i++) {
components[i] = componentsList.get(size - i - 1);
}
return components;
}
/**
* this collection will keep only distinct files/folders, e.g. C:\foo\bar will be removed when C:\foo is added
*/
public static class DistinctVFilesRootsCollection extends DistinctRootsCollection<VirtualFile> {
public DistinctVFilesRootsCollection() {
}
public DistinctVFilesRootsCollection(Collection<VirtualFile> virtualFiles) {
super(virtualFiles);
}
public DistinctVFilesRootsCollection(VirtualFile[] collection) {
super(collection);
}
@Override
protected boolean isAncestor(@NotNull VirtualFile ancestor, @NotNull VirtualFile virtualFile) {
return VfsUtilCore.isAncestor(ancestor, virtualFile, false);
}
}
public static void processFilesRecursively(@NotNull VirtualFile root,
@NotNull Processor<VirtualFile> processor,
@NotNull Convertor<VirtualFile, Boolean> directoryFilter) {
if (!processor.process(root)) return;
if (root.isDirectory() && directoryFilter.convert(root)) {
final LinkedList<VirtualFile[]> queue = new LinkedList<VirtualFile[]>();
queue.add(root.getChildren());
do {
final VirtualFile[] files = queue.removeFirst();
for (VirtualFile file : files) {
if (!processor.process(file)) return;
if (file.isDirectory() && directoryFilter.convert(file)) {
queue.add(file.getChildren());
}
}
} while (!queue.isEmpty());
}
}
}