2 * Copyright 2000-2011 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package com.intellij.openapi.vfs;
18 import com.intellij.openapi.diagnostic.Logger;
19 import com.intellij.openapi.fileTypes.FileType;
20 import com.intellij.openapi.fileTypes.FileTypeRegistry;
21 import com.intellij.openapi.util.*;
22 import com.intellij.openapi.util.text.StringUtil;
23 import com.intellij.openapi.vfs.encoding.EncodingRegistry;
24 import org.jetbrains.annotations.NonNls;
25 import org.jetbrains.annotations.NotNull;
26 import org.jetbrains.annotations.Nullable;
28 import java.io.IOException;
29 import java.io.InputStream;
30 import java.io.OutputStream;
31 import java.nio.charset.Charset;
34 * Represents a file in <code>{@link VirtualFileSystem}</code>. A particular file is represented by the same
35 * <code>VirtualFile</code> instance for the entire lifetime of the IntelliJ IDEA process, unless the file
36 * is deleted, in which case {@link #isValid()} for the instance will return <code>false</code>.
38 * If an in-memory implementation of VirtualFile is required, {@link com.intellij.testFramework.LightVirtualFile}
39 * (Extended API) can be used.
41 * Please see <a href="http://confluence.jetbrains.net/display/IDEADEV/IntelliJ+IDEA+Virtual+File+System">IntelliJ IDEA Virtual File System</a>
42 * for high-level overview.
44 * @see VirtualFileSystem
45 * @see VirtualFileManager
47 public abstract class VirtualFile extends UserDataHolderBase implements ModificationTracker {
48 private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vfs.VirtualFile");
49 public static final Key<Object> REQUESTOR_MARKER = Key.create("REQUESTOR_MARKER");
50 private static final Key<byte[]> BOM_KEY = Key.create("BOM");
51 private static final Key<Charset> CHARSET_KEY = Key.create("CHARSET");
52 public static final VirtualFile[] EMPTY_ARRAY = new VirtualFile[0];
54 protected VirtualFile() {
58 * Gets the name of this file.
64 public abstract String getName();
67 * Gets the {@link VirtualFileSystem} this file belongs to.
69 * @return the {@link VirtualFileSystem}
72 public abstract VirtualFileSystem getFileSystem();
75 * Gets the path of this file. Path is a string which uniquely identifies file within given
76 * <code>{@link VirtualFileSystem}</code>. Format of the path depends on the concrete file system.
77 * For <code>{@link com.intellij.openapi.vfs.LocalFileSystem}</code> it is an absolute file path with file separator characters
78 * (File.separatorChar) replaced to the forward slash ('/').
82 public abstract String getPath();
85 * Gets the URL of this file. The URL is a string which uniquely identifies file in all file systems.
86 * It has the following format: <code><protocol>://<path></code>.
88 * File can be found by its URL using {@link VirtualFileManager#findFileByUrl} method.
90 * @return the URL consisting of protocol and path
91 * @see VirtualFileManager#findFileByUrl
92 * @see VirtualFile#getPath
93 * @see VirtualFileSystem#getProtocol
96 public String getUrl() {
97 return VirtualFileManager.constructUrl(getFileSystem().getProtocol(), getPath());
101 * Fetches "presentable URL" of this file. "Presentable URL" is a string to be used for displaying this
104 * @return the presentable URL.
105 * @see VirtualFileSystem#extractPresentableUrl
107 public final String getPresentableUrl() {
108 return getFileSystem().extractPresentableUrl(getPath());
112 * Used as a property name in the {@link VirtualFilePropertyEvent} fired when the name of a
113 * {@link VirtualFile} changes.
115 * @see VirtualFileListener#propertyChanged
116 * @see VirtualFilePropertyEvent#getPropertyName
118 @NonNls public static final String PROP_NAME = "name";
121 * Used as a property name in the {@link VirtualFilePropertyEvent} fired when the encoding of a
122 * {@link VirtualFile} changes.
124 * @see VirtualFileListener#propertyChanged
125 * @see VirtualFilePropertyEvent#getPropertyName
127 @NonNls public static final String PROP_ENCODING = "encoding";
130 * Used as a property name in the {@link VirtualFilePropertyEvent} fired when the write permission of a
131 * {@link VirtualFile} changes.
133 * @see VirtualFileListener#propertyChanged
134 * @see VirtualFilePropertyEvent#getPropertyName
136 @NonNls public static final String PROP_WRITABLE = "writable";
139 * Gets the extension of this file. If file name contains '.' extension is the substring from the last '.'
140 * to the end of the name, otherwise extension is null.
142 * @return the extension or null if file name doesn't contain '.'
146 public String getExtension() {
147 String name = getName();
148 int index = name.lastIndexOf('.');
149 if (index < 0) return null;
150 return name.substring(index + 1);
154 * Gets the file name without the extension. If file name contains '.' the substring till the last '.' is returned.
155 * Otherwise the same value as <code>{@link #getName}</code> method returns is returned.
157 * @return the name without extension
158 * if there is no '.' in it
162 public String getNameWithoutExtension() {
163 String name = getName();
164 int index = name.lastIndexOf('.');
165 if (index < 0) return name;
166 return name.substring(0, index);
171 * Renames this file to the <code>newName</code>.<p>
172 * This method should be only called within write-action.
173 * See {@link com.intellij.openapi.application.Application#runWriteAction(Runnable)}.
175 * @param requestor any object to control who called this method. Note that
176 * it is considered to be an external change if <code>requestor</code> is <code>null</code>.
177 * See {@link VirtualFileEvent#getRequestor}
178 * @param newName the new file name
179 * @throws IOException if file failed to be renamed
181 public void rename(Object requestor, @NotNull @NonNls String newName) throws IOException {
182 if (getName().equals(newName)) return;
183 if (!isValidName(newName)) {
184 throw new IOException(VfsBundle.message("file.invalid.name.error", newName));
187 getFileSystem().renameFile(requestor, this, newName);
191 * Checks whether this file has write permission. Note that this value may be cached and may differ from
192 * the write permission of the physical file.
194 * @return <code>true</code> if this file is writable, <code>false</code> otherwise
196 public abstract boolean isWritable();
199 * Checks whether this file is a directory.
201 * @return <code>true</code> if this file is a directory, <code>false</code> otherwise
203 public abstract boolean isDirectory();
206 * Checks whether this file is a symbolic link.
209 * @return <code>true</code> if this file is a symbolic link, <code>false</code> otherwise
211 public boolean isSymLink() {
216 public String resolveSymLink() {
221 * Checks whether this file is a special (e.g. FIFO or device) file.
224 * @return <code>true</code> if the file exists and is a special one, <code>false</code> otherwise
226 public boolean isSpecialFile() {
231 * Attempts to resolve a symbolic link represented by this file and returns link target.
234 * @return <code>this</code> if the file isn't a symbolic link;
235 * instance of <code>VirtualFile</code> if the link was successfully resolved;
236 * <code>null</code> otherwise
239 public VirtualFile getRealFile() {
244 * Checks whether this <code>VirtualFile</code> is valid. File can be invalidated either by deleting it or one of its
245 * parents with {@link #delete} method or by an external change.
246 * If file is not valid only {@link #equals}, {@link #hashCode} and methods from
247 * {@link UserDataHolder} can be called for it. Using any other methods for an invalid {@link VirtualFile} instance
248 * produce unpredictable results.
250 * @return <code>true</code> if this is a valid file, <code>false</code> otherwise
252 public abstract boolean isValid();
255 * Gets the parent <code>VirtualFile</code>.
257 * @return the parent file or <code>null</code> if this file is a root directory
259 public abstract VirtualFile getParent();
262 * Gets the child files.
264 * @return array of the child files or <code>null</code> if this file is not a directory
266 public abstract VirtualFile[] getChildren();
269 * Finds child of this file with the given name.
271 * @param name the file name to search by
272 * @return the file if found any, <code>null</code> otherwise
275 public VirtualFile findChild(@NotNull @NonNls String name) {
276 VirtualFile[] children = getChildren();
277 if (children == null) return null;
278 for (VirtualFile child : children) {
279 if (child.nameEquals(name)) {
287 public VirtualFile findOrCreateChildData(Object requestor, @NotNull @NonNls String name) throws IOException {
288 final VirtualFile child = findChild(name);
289 if (child != null) return child;
290 return createChildData(requestor, name);
294 * @return the {@link FileType} of this file.
295 * When IDEA has no idea what the file type is (i.e. file type is not registered via {@link FileTypeRegistry}),
296 * it returns {@link com.intellij.openapi.fileTypes.FileTypes#UNKNOWN}
299 public FileType getFileType() {
300 return FileTypeRegistry.getInstance().getFileTypeByFile(this);
304 * Finds file by path relative to this file.
306 * @param relPath the relative path to search by
307 * @return the file if found any, <code>null</code> otherwise
310 public VirtualFile findFileByRelativePath(@NotNull @NonNls String relPath) {
311 if (relPath.length() == 0) return this;
312 relPath = StringUtil.trimStart(relPath, "/");
314 int index = relPath.indexOf('/');
315 if (index < 0) index = relPath.length();
316 String name = relPath.substring(0, index);
319 if (name.equals(".")) {
322 else if (name.equals("..")) {
326 child = findChild(name);
329 if (child == null) return null;
331 if (index < relPath.length()) {
332 return child.findFileByRelativePath(relPath.substring(index + 1));
340 * Creates a subdirectory in this directory. This method should be only called within write-action.
341 * See {@link com.intellij.openapi.application.Application#runWriteAction}.
343 * @param requestor any object to control who called this method. Note that
344 * it is considered to be an external change if <code>requestor</code> is <code>null</code>.
345 * See {@link VirtualFileEvent#getRequestor}
346 * @param name directory name
347 * @return <code>VirtualFile</code> representing the created directory
348 * @throws java.io.IOException if directory failed to be created
350 public VirtualFile createChildDirectory(Object requestor, @NonNls String name) throws IOException {
351 if (!isDirectory()) {
352 throw new IOException(VfsBundle.message("directory.create.wrong.parent.error"));
356 throw new IOException(VfsBundle.message("invalid.directory.create.files"));
359 if (!isValidName(name)) {
360 throw new IOException(VfsBundle.message("directory.invalid.name.error", name));
363 if (findChild(name) != null) {
364 throw new IOException(VfsBundle.message("file.create.already.exists.error", getUrl(), name));
367 return getFileSystem().createChildDirectory(requestor, this, name);
371 * Creates a new file in this directory. This method should be only called within write-action.
372 * See {@link com.intellij.openapi.application.Application#runWriteAction}.
374 * @param requestor any object to control who called this method. Note that
375 * it is considered to be an external change if <code>requestor</code> is <code>null</code>.
376 * See {@link VirtualFileEvent#getRequestor}
377 * @return <code>VirtualFile</code> representing the created file
378 * @throws IOException if file failed to be created
380 public VirtualFile createChildData(Object requestor, @NotNull @NonNls String name) throws IOException {
381 if (!isDirectory()) {
382 throw new IOException(VfsBundle.message("file.create.wrong.parent.error"));
386 throw new IOException(VfsBundle.message("invalid.directory.create.files"));
389 if (!isValidName(name)) {
390 throw new IOException(VfsBundle.message("file.invalid.name.error", name));
393 if (findChild(name) != null) {
394 throw new IOException(VfsBundle.message("file.create.already.exists.error", getUrl(), name));
397 return getFileSystem().createChildFile(requestor, this, name);
401 * Deletes this file. This method should be only called within write-action.
402 * See {@link com.intellij.openapi.application.Application#runWriteAction}.
404 * @param requestor any object to control who called this method. Note that
405 * it is considered to be an external change if <code>requestor</code> is <code>null</code>.
406 * See {@link VirtualFileEvent#getRequestor}
407 * @throws IOException if file failed to be deleted
409 public void delete(Object requestor) throws IOException {
410 LOG.assertTrue(isValid(), "Deleting invalid file");
411 getFileSystem().deleteFile(requestor, this);
415 * Moves this file to another directory. This method should be only called within write-action.
416 * See {@link com.intellij.openapi.application.Application#runWriteAction}.
418 * @param requestor any object to control who called this method. Note that
419 * it is considered to be an external change if <code>requestor</code> is <code>null</code>.
420 * See {@link VirtualFileEvent#getRequestor}
421 * @param newParent the directory to move this file to
422 * @throws IOException if file failed to be moved
424 public void move(final Object requestor, @NotNull final VirtualFile newParent) throws IOException {
425 if (getFileSystem() != newParent.getFileSystem()) {
426 throw new IOException(VfsBundle.message("file.move.error", newParent.getPresentableUrl()));
429 EncodingRegistry.doActionAndRestoreEncoding(this, new ThrowableComputable<VirtualFile, IOException>() {
431 public VirtualFile compute() throws IOException {
432 getFileSystem().moveFile(requestor, VirtualFile.this, newParent);
433 return VirtualFile.this;
438 public VirtualFile copy(final Object requestor, @NotNull final VirtualFile newParent, @NotNull final String copyName) throws IOException {
439 if (getFileSystem() != newParent.getFileSystem()) {
440 throw new IOException(VfsBundle.message("file.copy.error", newParent.getPresentableUrl()));
443 if (!newParent.isDirectory()) {
444 throw new IOException(VfsBundle.message("file.copy.target.must.be.directory"));
447 return EncodingRegistry.doActionAndRestoreEncoding(this, new ThrowableComputable<VirtualFile, IOException>() {
449 public VirtualFile compute() throws IOException {
450 return getFileSystem().copyFile(requestor, VirtualFile.this, newParent, copyName);
456 public final void setBinaryContent(byte[] content) throws IOException {
457 setBinaryContent(content, -1, -1);
461 * @return Retrieve the charset file has been loaded with (if loaded) and would be saved with (if would).
463 public Charset getCharset() {
464 Charset charset = getUserData(CHARSET_KEY);
465 if (charset == null) {
466 charset = EncodingRegistry.getInstance().getDefaultCharset();
472 public void setCharset(final Charset charset) {
473 final Charset old = getUserData(CHARSET_KEY);
474 putUserData(CHARSET_KEY, charset);
475 if (Comparing.equal(charset, old)) return;
476 byte[] bom = charset == null ? null : CharsetToolkit.getBom(charset);
477 byte[] existingBOM = getBOM();
478 if (bom == null && charset != null && CharsetToolkit.canHaveBom(charset, existingBOM)) {
483 if (old != null) { //do not send on detect
484 VirtualFileManager.getInstance().notifyPropertyChanged(this, PROP_ENCODING, old, charset);
488 public boolean isCharsetSet() {
489 return getUserData(CHARSET_KEY) != null;
492 public void setBinaryContent(final byte[] content, long newModificationStamp, long newTimeStamp) throws IOException {
493 setBinaryContent(content, newModificationStamp, newTimeStamp, this);
495 public void setBinaryContent(final byte[] content, long newModificationStamp, long newTimeStamp, Object requestor) throws IOException {
496 OutputStream outputStream = null;
498 outputStream = getOutputStream(requestor, newModificationStamp, newTimeStamp);
499 outputStream.write(content);
500 outputStream.flush();
503 if (outputStream != null) outputStream.close();
508 * Creates the <code>OutputStream</code> for this file.
509 * 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.
511 * @param requestor any object to control who called this method. Note that
512 * it is considered to be an external change if <code>requestor</code> is <code>null</code>.
513 * See {@link VirtualFileEvent#getRequestor}
514 * @return <code>OutputStream</code>
515 * @throws IOException if an I/O error occurs
517 public final OutputStream getOutputStream(Object requestor) throws IOException {
518 return getOutputStream(requestor, -1, -1);
522 * Gets the <code>OutputStream</code> for this file and sets modification stamp and time stamp to the specified values
523 * after closing the stream.<p>
525 * Normally you should not use this method.
527 * 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.
529 * @param requestor any object to control who called this method. Note that
530 * it is considered to be an external change if <code>requestor</code> is <code>null</code>.
531 * See {@link VirtualFileEvent#getRequestor}
532 * @param newModificationStamp new modification stamp or -1 if no special value should be set
533 * @param newTimeStamp new time stamp or -1 if no special value should be set
534 * @return <code>OutputStream</code>
535 * @throws IOException if an I/O error occurs
536 * @see #getModificationStamp()
539 public abstract OutputStream getOutputStream(Object requestor, long newModificationStamp, long newTimeStamp) throws IOException;
542 * Returns file content as an array of bytes.
543 * Has the same effect as contentsToByteArray(true).
545 * @return file content
546 * @throws IOException if an I/O error occurs
547 * @see #contentsToByteArray(boolean)
548 * @see #getInputStream()
551 public abstract byte[] contentsToByteArray() throws IOException;
554 * Returns file content as an array of bytes.
556 * @param cacheContent set true to
557 * @return file content
558 * @throws IOException if an I/O error occurs
559 * @see #contentsToByteArray()
562 public byte[] contentsToByteArray(boolean cacheContent) throws IOException {
563 return contentsToByteArray();
568 * Gets modification stamp value. Modification stamp is a value changed by any modification
569 * of the content of the file. Note that it is not related to the file modification time.
571 * @return modification stamp
572 * @see #getTimeStamp()
574 public long getModificationStamp() {
575 throw new UnsupportedOperationException();
579 * Gets the timestamp for this file. Note that this value may be cached and may differ from
580 * the timestamp of the physical file.
583 * @see java.io.File#lastModified
585 public abstract long getTimeStamp();
588 * File length in bytes.
590 * @return the length of this file.
592 public abstract long getLength();
595 * Refreshes the cached file information from the physical file system. If this file is not a directory
596 * the timestamp value is refreshed and <code>contentsChanged</code> event is fired if it is changed.<p>
597 * If this file is a directory the set of its children is refreshed. If recursive value is <code>true</code> all
598 * children are refreshed recursively.
600 * This method should be only called within write-action.
601 * See {@link com.intellij.openapi.application.Application#runWriteAction}.
603 * @param asynchronous if <code>true</code> then the operation will be performed in a separate thread,
604 * otherwise will be performed immediately
605 * @param recursive whether to refresh all the files in this directory recursively
607 public void refresh(boolean asynchronous, boolean recursive) {
608 refresh(asynchronous, recursive, null);
612 * The same as {@link #refresh(boolean, boolean)} but also runs <code>postRunnable</code>
613 * after the operation is completed.
615 public abstract void refresh(boolean asynchronous, boolean recursive, @Nullable Runnable postRunnable);
617 public String getPresentableName() {
622 public long getModificationCount() {
623 return isValid() ? getTimeStamp() : -1;
628 * @return whether file name equals to this name
629 * result depends on the filesystem specifics
631 protected boolean nameEquals(@NotNull @NonNls String name) {
632 return getName().equals(name);
636 * Gets the <code>InputStream</code> for this file.
637 * 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.
639 * @return <code>InputStream</code>
640 * @throws IOException if an I/O error occurs
641 * @see #contentsToByteArray
643 public abstract InputStream getInputStream() throws IOException;
646 public byte[] getBOM() {
647 return getUserData(BOM_KEY);
650 public void setBOM(@Nullable byte[] BOM) {
651 putUserData(BOM_KEY, BOM);
655 public String toString() {
656 return "VirtualFile: " + getPresentableUrl();
659 public boolean exists() {
663 public boolean isInLocalFileSystem() {
667 public static boolean isValidName(@NotNull String name) {
668 return name.indexOf('\\') < 0 && name.indexOf('/') < 0;