blob: e1deb9b87bdf106ce0a3924e4ccd5ca93eb637d4 [file] [log] [blame]
/*
* Copyright 2000-2013 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.fileTypes.FileType;
import com.intellij.openapi.fileTypes.FileTypeRegistry;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.encoding.EncodingRegistry;
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.nio.charset.Charset;
/**
* Represents a file in <code>{@link VirtualFileSystem}</code>. A particular file is represented by the same
* <code>VirtualFile</code> instance for the entire lifetime of the IntelliJ IDEA process, unless the file
* is deleted, in which case {@link #isValid()} for the instance will return <code>false</code>.
* <p/>
* If an in-memory implementation of VirtualFile is required, {@link com.intellij.testFramework.LightVirtualFile}
* can be used.
* <p/>
* Please see <a href="http://confluence.jetbrains.net/display/IDEADEV/IntelliJ+IDEA+Virtual+File+System">IntelliJ IDEA Virtual File System</a>
* for high-level overview.
*
* @see VirtualFileSystem
* @see VirtualFileManager
*/
public abstract class VirtualFile extends UserDataHolderBase implements ModificationTracker {
public static final Key<Object> REQUESTOR_MARKER = Key.create("REQUESTOR_MARKER");
public static final VirtualFile[] EMPTY_ARRAY = new VirtualFile[0];
/**
* Used as a property name in the {@link VirtualFilePropertyEvent} fired when the name of a
* {@link VirtualFile} changes.
*
* @see VirtualFileListener#propertyChanged
* @see VirtualFilePropertyEvent#getPropertyName
*/
@NonNls public static final String PROP_NAME = "name";
/**
* Used as a property name in the {@link VirtualFilePropertyEvent} fired when the encoding of a
* {@link VirtualFile} changes.
*
* @see VirtualFileListener#propertyChanged
* @see VirtualFilePropertyEvent#getPropertyName
*/
@NonNls public static final String PROP_ENCODING = "encoding";
/**
* Used as a property name in the {@link VirtualFilePropertyEvent} fired when the write permission of a
* {@link VirtualFile} changes.
*
* @see VirtualFileListener#propertyChanged
* @see VirtualFilePropertyEvent#getPropertyName
*/
@NonNls public static final String PROP_WRITABLE = "writable";
/**
* Used as a property name in the {@link VirtualFilePropertyEvent} fired when a visibility of a
* {@link VirtualFile} changes.
*
* @see VirtualFileListener#propertyChanged
* @see VirtualFilePropertyEvent#getPropertyName
*/
@NonNls public static final String PROP_HIDDEN = VFileProperty.HIDDEN.getName();
/**
* Used as a property name in the {@link VirtualFilePropertyEvent} fired when a symlink target of a
* {@link VirtualFile} changes.
*
* @see VirtualFileListener#propertyChanged
* @see VirtualFilePropertyEvent#getPropertyName
*/
@NonNls public static final String PROP_SYMLINK_TARGET = "symlink";
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vfs.VirtualFile");
private static final Key<byte[]> BOM_KEY = Key.create("BOM");
private static final Key<Charset> CHARSET_KEY = Key.create("CHARSET");
protected VirtualFile() { }
/**
* Gets the name of this file.
*
* @return file name
*/
@NotNull
@NonNls
public abstract String getName();
/**
* Gets the {@link VirtualFileSystem} this file belongs to.
*
* @return the {@link VirtualFileSystem}
*/
@NotNull
public abstract VirtualFileSystem getFileSystem();
/**
* Gets the path of this file. Path is a string which uniquely identifies file within given
* <code>{@link VirtualFileSystem}</code>. Format of the path depends on the concrete file system.
* For <code>{@link com.intellij.openapi.vfs.LocalFileSystem}</code> it is an absolute file path with file separator characters
* (File.separatorChar) replaced to the forward slash ('/').
*
* @return the path
*/
@SuppressWarnings("JavadocReference")
public abstract String getPath();
/**
* Gets the URL of this file. The URL is a string which uniquely identifies file in all file systems.
* It has the following format: <code>&lt;protocol&gt;://&lt;path&gt;</code>.
* <p/>
* File can be found by its URL using {@link VirtualFileManager#findFileByUrl} method.
*
* @return the URL consisting of protocol and path
* @see VirtualFileManager#findFileByUrl
* @see VirtualFile#getPath
* @see VirtualFileSystem#getProtocol
*/
@NotNull
public String getUrl() {
return VirtualFileManager.constructUrl(getFileSystem().getProtocol(), getPath());
}
/**
* Fetches "presentable URL" of this file. "Presentable URL" is a string to be used for displaying this
* file in the UI.
*
* @return the presentable URL.
* @see VirtualFileSystem#extractPresentableUrl
*/
public final String getPresentableUrl() {
if (!isValid()) return null;
return getFileSystem().extractPresentableUrl(getPath());
}
/**
* Gets the extension of this file. If file name contains '.' extension is the substring from the last '.'
* to the end of the name, otherwise extension is null.
*
* @return the extension or null if file name doesn't contain '.'
*/
@Nullable
@NonNls
public String getExtension() {
String name = getName();
int index = name.lastIndexOf('.');
if (index < 0) return null;
return name.substring(index + 1);
}
/**
* Gets the file name without the extension. If file name contains '.' the substring till the last '.' is returned.
* Otherwise the same value as <code>{@link #getName}</code> method returns is returned.
*
* @return the name without extension
* if there is no '.' in it
*/
@NonNls
@NotNull
public String getNameWithoutExtension() {
String name = getName();
int index = name.lastIndexOf('.');
if (index < 0) return name;
return name.substring(0, index);
}
/**
* Renames this file to the <code>newName</code>.<p>
* This method should be only called within write-action.
* See {@link com.intellij.openapi.application.Application#runWriteAction(Runnable)}.
*
* @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 VirtualFileEvent#getRequestor}
* @param newName the new file name
* @throws IOException if file failed to be renamed
*/
public void rename(Object requestor, @NotNull @NonNls String newName) throws IOException {
if (getName().equals(newName)) return;
if (!isValidName(newName)) {
throw new IOException(VfsBundle.message("file.invalid.name.error", newName));
}
getFileSystem().renameFile(requestor, this, newName);
}
/**
* Checks whether this file has write permission. Note that this value may be cached and may differ from
* the write permission of the physical file.
*
* @return <code>true</code> if this file is writable, <code>false</code> otherwise
*/
public abstract boolean isWritable();
public void setWritable(boolean writable) throws IOException {
throw new IOException("Not supported");
}
/**
* Checks whether this file is a directory.
*
* @return <code>true</code> if this file is a directory, <code>false</code> otherwise
*/
public abstract boolean isDirectory();
/** @deprecated use {@link #is(VFileProperty)} (to remove in IDEA 14) */
@SuppressWarnings("UnusedDeclaration")
public boolean isSymLink() {
return is(VFileProperty.SYMLINK);
}
/** @deprecated use {@link #is(VFileProperty)} (to remove in IDEA 14) */
@SuppressWarnings("UnusedDeclaration")
public boolean isSpecialFile() {
return is(VFileProperty.SPECIAL);
}
/**
* Checks whether this file has a specific property.
*
* @return <code>true</code> if the file has a specific property, <code>false</code> otherwise
* @since 13.0
*/
public boolean is(@NotNull VFileProperty property) {
return false;
}
/**
* Resolves all symbolic links containing in a path to this file and returns a path to a link target (in platform-independent format).
* <p/>
* <b>Note</b>: please use this method judiciously. In most cases VFS clients don't need to resolve links in paths and should
* work with those provided by a user.
*
* @return <code>getPath()</code> if there are no symbolic links in a file's path;
* <code>getCanonicalFile().getPath()</code> if the link was successfully resolved;
* <code>null</code> otherwise
* @since 11.1
*/
@Nullable
public String getCanonicalPath() {
return getPath();
}
/**
* Resolves all symbolic links containing in a path to this file and returns a link target.
* <p/>
* <b>Note</b>: please use this method judiciously. In most cases VFS clients don't need to resolve links in paths and should
* work with those provided by a user.
*
* @return <code>this</code> if there are no symbolic links in a file's path;
* instance of <code>VirtualFile</code> if the link was successfully resolved;
* <code>null</code> otherwise
* @since 11.1
*/
@Nullable
public VirtualFile getCanonicalFile() {
return this;
}
/**
* Checks whether this <code>VirtualFile</code> is valid. File can be invalidated either by deleting it or one of its
* parents with {@link #delete} method or by an external change.
* If file is not valid only {@link #equals}, {@link #hashCode} and methods from
* {@link UserDataHolder} can be called for it. Using any other methods for an invalid {@link VirtualFile} instance
* produce unpredictable results.
*
* @return <code>true</code> if this is a valid file, <code>false</code> otherwise
*/
public abstract boolean isValid();
/**
* Gets the parent <code>VirtualFile</code>.
*
* @return the parent file or <code>null</code> if this file is a root directory
*/
public abstract VirtualFile getParent();
/**
* Gets the child files.
*
* @return array of the child files or <code>null</code> if this file is not a directory
*/
public abstract VirtualFile[] getChildren();
/**
* Finds child of this file with the given name.
*
* @param name the file name to search by
* @return the file if found any, <code>null</code> otherwise
*/
@Nullable
public VirtualFile findChild(@NotNull @NonNls String name) {
VirtualFile[] children = getChildren();
if (children == null) return null;
for (VirtualFile child : children) {
if (child.nameEquals(name)) {
return child;
}
}
return null;
}
@Nullable
public VirtualFile findOrCreateChildData(Object requestor, @NotNull @NonNls String name) throws IOException {
final VirtualFile child = findChild(name);
if (child != null) return child;
return createChildData(requestor, name);
}
/**
* @return the {@link FileType} of this file.
* When IDEA has no idea what the file type is (i.e. file type is not registered via {@link FileTypeRegistry}),
* it returns {@link com.intellij.openapi.fileTypes.FileTypes#UNKNOWN}
*/
@SuppressWarnings("JavadocReference")
@NotNull
public FileType getFileType() {
return FileTypeRegistry.getInstance().getFileTypeByFile(this);
}
/**
* Finds file by path relative to this file.
*
* @param relPath the relative path with / used as separators
* @return the file if found any, <code>null</code> otherwise
*/
@Nullable
public VirtualFile findFileByRelativePath(@NotNull @NonNls String relPath) {
if (relPath.isEmpty()) return this;
relPath = StringUtil.trimStart(relPath, "/");
int index = relPath.indexOf('/');
if (index < 0) index = relPath.length();
String name = relPath.substring(0, index);
VirtualFile child;
if (name.equals(".")) {
child = this;
}
else if (name.equals("..")) {
if (is(VFileProperty.SYMLINK)) {
final VirtualFile canonicalFile = getCanonicalFile();
child = canonicalFile != null ? canonicalFile.getParent() : null;
}
else {
child = getParent();
}
}
else {
child = findChild(name);
}
if (child == null) return null;
if (index < relPath.length()) {
return child.findFileByRelativePath(relPath.substring(index + 1));
}
return child;
}
/**
* Creates a subdirectory in this directory. This method should be only called within write-action.
* See {@link com.intellij.openapi.application.Application#runWriteAction}.
*
* @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 VirtualFileEvent#getRequestor}
* @param name directory name
* @return <code>VirtualFile</code> representing the created directory
* @throws java.io.IOException if directory failed to be created
*/
public VirtualFile createChildDirectory(Object requestor, @NonNls String name) throws IOException {
if (!isDirectory()) {
throw new IOException(VfsBundle.message("directory.create.wrong.parent.error"));
}
if (!isValid()) {
throw new IOException(VfsBundle.message("invalid.directory.create.files"));
}
if (!isValidName(name)) {
throw new IOException(VfsBundle.message("directory.invalid.name.error", name));
}
if (findChild(name) != null) {
throw new IOException(VfsBundle.message("file.create.already.exists.error", getUrl(), name));
}
return getFileSystem().createChildDirectory(requestor, this, name);
}
/**
* Creates a new file in this directory. This method should be only called within write-action.
* See {@link com.intellij.openapi.application.Application#runWriteAction}.
*
* @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 VirtualFileEvent#getRequestor}
* @return <code>VirtualFile</code> representing the created file
* @throws IOException if file failed to be created
*/
public VirtualFile createChildData(Object requestor, @NotNull @NonNls String name) throws IOException {
if (!isDirectory()) {
throw new IOException(VfsBundle.message("file.create.wrong.parent.error"));
}
if (!isValid()) {
throw new IOException(VfsBundle.message("invalid.directory.create.files"));
}
if (!isValidName(name)) {
throw new IOException(VfsBundle.message("file.invalid.name.error", name));
}
if (findChild(name) != null) {
throw new IOException(VfsBundle.message("file.create.already.exists.error", getUrl(), name));
}
return getFileSystem().createChildFile(requestor, this, name);
}
/**
* Deletes this file. This method should be only called within write-action.
* See {@link com.intellij.openapi.application.Application#runWriteAction}.
*
* @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 VirtualFileEvent#getRequestor}
* @throws IOException if file failed to be deleted
*/
public void delete(Object requestor) throws IOException {
LOG.assertTrue(isValid(), "Deleting invalid file");
getFileSystem().deleteFile(requestor, this);
}
/**
* Moves this file to another directory. This method should be only called within write-action.
* See {@link com.intellij.openapi.application.Application#runWriteAction}.
*
* @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 VirtualFileEvent#getRequestor}
* @param newParent the directory to move this file to
* @throws IOException if file failed to be moved
*/
public void move(final Object requestor, @NotNull final VirtualFile newParent) throws IOException {
if (getFileSystem() != newParent.getFileSystem()) {
throw new IOException(VfsBundle.message("file.move.error", newParent.getPresentableUrl()));
}
EncodingRegistry.doActionAndRestoreEncoding(this, new ThrowableComputable<VirtualFile, IOException>() {
@Override
public VirtualFile compute() throws IOException {
getFileSystem().moveFile(requestor, VirtualFile.this, newParent);
return VirtualFile.this;
}
});
}
public VirtualFile copy(final Object requestor, @NotNull final VirtualFile newParent, @NotNull final String copyName) throws IOException {
if (getFileSystem() != newParent.getFileSystem()) {
throw new IOException(VfsBundle.message("file.copy.error", newParent.getPresentableUrl()));
}
if (!newParent.isDirectory()) {
throw new IOException(VfsBundle.message("file.copy.target.must.be.directory"));
}
return EncodingRegistry.doActionAndRestoreEncoding(this, new ThrowableComputable<VirtualFile, IOException>() {
@Override
public VirtualFile compute() throws IOException {
return getFileSystem().copyFile(requestor, VirtualFile.this, newParent, copyName);
}
});
}
/**
* @return Retrieve the charset file has been loaded with (if loaded) and would be saved with (if would).
*/
public Charset getCharset() {
Charset charset = getStoredCharset();
if (charset == null) {
charset = EncodingRegistry.getInstance().getDefaultCharset();
setCharset(charset);
}
return charset;
}
@Nullable
protected Charset getStoredCharset() {
return getUserData(CHARSET_KEY);
}
protected void storeCharset(Charset charset) {
putUserData(CHARSET_KEY, charset);
}
public void setCharset(final Charset charset) {
setCharset(charset, null);
}
public void setCharset(final Charset charset, @Nullable Runnable whenChanged) {
final Charset old = getStoredCharset();
storeCharset(charset);
if (Comparing.equal(charset, old)) return;
byte[] bom = charset == null ? null : CharsetToolkit.getMandatoryBom(charset);
byte[] existingBOM = getBOM();
if (bom == null && charset != null && existingBOM != null) {
bom = CharsetToolkit.canHaveBom(charset, existingBOM) ? existingBOM : null;
}
setBOM(bom);
if (old != null) { //do not send on detect
if (whenChanged != null) whenChanged.run();
VirtualFileManager.getInstance().notifyPropertyChanged(this, PROP_ENCODING, old, charset);
}
}
public boolean isCharsetSet() {
return getStoredCharset() != null;
}
public final void setBinaryContent(@NotNull byte[] content) throws IOException {
setBinaryContent(content, -1, -1);
}
public void setBinaryContent(@NotNull byte[] content, long newModificationStamp, long newTimeStamp) throws IOException {
setBinaryContent(content, newModificationStamp, newTimeStamp, this);
}
public void setBinaryContent(@NotNull byte[] content, long newModificationStamp, long newTimeStamp, Object requestor) throws IOException {
final OutputStream outputStream = getOutputStream(requestor, newModificationStamp, newTimeStamp);
try {
outputStream.write(content);
outputStream.flush();
}
finally {
outputStream.close();
}
}
/**
* Creates the <code>OutputStream</code> for this file.
* Writes BOM first, if there is any. See <a href=http://unicode.org/faq/utf_bom.html>Unicode Byte Order Mark FAQ</a> for an explanation.
*
* @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 VirtualFileEvent#getRequestor}
* @return <code>OutputStream</code>
* @throws IOException if an I/O error occurs
*/
public final OutputStream getOutputStream(Object requestor) throws IOException {
return getOutputStream(requestor, -1, -1);
}
/**
* Gets the <code>OutputStream</code> for this file and sets modification stamp and time stamp to the specified values
* after closing the stream.<p>
* <p/>
* Normally you should not use this method.
*
* Writes BOM first, if there is any. See <a href=http://unicode.org/faq/utf_bom.html>Unicode Byte Order Mark FAQ</a> for an explanation.
*
* @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 VirtualFileEvent#getRequestor}
* @param newModificationStamp new modification stamp or -1 if no special value should be set
* @param newTimeStamp new time stamp or -1 if no special value should be set
* @return <code>OutputStream</code>
* @throws IOException if an I/O error occurs
* @see #getModificationStamp()
*/
@NotNull
public abstract OutputStream getOutputStream(Object requestor, long newModificationStamp, long newTimeStamp) throws IOException;
/**
* Returns file content as an array of bytes.
* Has the same effect as contentsToByteArray(true).
*
* @return file content
* @throws IOException if an I/O error occurs
* @see #contentsToByteArray(boolean)
* @see #getInputStream()
*/
@NotNull
public abstract byte[] contentsToByteArray() throws IOException;
/**
* Returns file content as an array of bytes.
*
* @param cacheContent set true to
* @return file content
* @throws IOException if an I/O error occurs
* @see #contentsToByteArray()
*/
@NotNull
public byte[] contentsToByteArray(boolean cacheContent) throws IOException {
return contentsToByteArray();
}
/**
* Gets modification stamp value. Modification stamp is a value changed by any modification
* of the content of the file. Note that it is not related to the file modification time.
*
* @return modification stamp
* @see #getTimeStamp()
*/
public long getModificationStamp() {
throw new UnsupportedOperationException();
}
/**
* Gets the timestamp for this file. Note that this value may be cached and may differ from
* the timestamp of the physical file.
*
* @return timestamp
* @see java.io.File#lastModified
*/
public abstract long getTimeStamp();
/**
* File length in bytes.
*
* @return the length of this file.
*/
public abstract long getLength();
/**
* Refreshes the cached file information from the physical file system. If this file is not a directory
* the timestamp value is refreshed and <code>contentsChanged</code> event is fired if it is changed.<p>
* If this file is a directory the set of its children is refreshed. If recursive value is <code>true</code> all
* children are refreshed recursively.
* <p/>
* When invoking synchronous refresh from a thread other than the event dispatch thread, the current thread must
* NOT be in a read action, otherwise a deadlock may occur.
*
* @param asynchronous if <code>true</code>, the method will return immediately and the refresh will be processed
* in the background. If <code>false</code>, the method will return only after the refresh
* is done and the VFS change events caused by the refresh have been fired and processed
* in the event dispatch thread. Instead of synchronous refreshes, it's recommended to use
* asynchronous refreshes with a <code>postRunnable</code> whenever possible.
* @param recursive whether to refresh all the files in this directory recursively
*/
public void refresh(boolean asynchronous, boolean recursive) {
refresh(asynchronous, recursive, null);
}
/**
* The same as {@link #refresh(boolean, boolean)} but also runs <code>postRunnable</code>
* after the operation is completed.
*/
public abstract void refresh(boolean asynchronous, boolean recursive, @Nullable Runnable postRunnable);
public String getPresentableName() {
return getName();
}
@Override
public long getModificationCount() {
return isValid() ? getTimeStamp() : -1;
}
/**
* @return whether file name equals to this name
* result depends on the filesystem specifics
*/
protected boolean nameEquals(@NotNull @NonNls String name) {
return getName().equals(name);
}
/**
* Gets the <code>InputStream</code> for this file.
* Skips BOM if there is any. See <a href=http://unicode.org/faq/utf_bom.html>Unicode Byte Order Mark FAQ</a> for an explanation.
*
* @return <code>InputStream</code>
* @throws IOException if an I/O error occurs
* @see #contentsToByteArray
*/
public abstract InputStream getInputStream() throws IOException;
@Nullable
public byte[] getBOM() {
return getUserData(BOM_KEY);
}
public void setBOM(@Nullable byte[] BOM) {
putUserData(BOM_KEY, BOM);
}
@NonNls
public String toString() {
return "VirtualFile: " + getPresentableUrl();
}
public boolean exists() {
return isValid();
}
public boolean isInLocalFileSystem() {
return false;
}
public static boolean isValidName(@NotNull String name) {
return name.indexOf('\\') < 0 && name.indexOf('/') < 0;
}
}