257587f549e0116711803669dd0d342dd4da5a70
[idea/community.git] / platform / core-api / src / com / intellij / openapi / vfs / VirtualFile.java
1 /*
2  * Copyright 2000-2011 JetBrains s.r.o.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 package com.intellij.openapi.vfs;
17
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;
27
28 import java.io.IOException;
29 import java.io.InputStream;
30 import java.io.OutputStream;
31 import java.nio.charset.Charset;
32
33 /**
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>.
37  * <p/>
38  * If an in-memory implementation of VirtualFile is required, {@link com.intellij.testFramework.LightVirtualFile}
39  * (Extended API) can be used.
40  * <p/>
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.
43  *
44  * @see VirtualFileSystem
45  * @see VirtualFileManager
46  */
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];
53
54   protected VirtualFile() {
55   }
56
57   /**
58    * Gets the name of this file.
59    *
60    * @return file name
61    */
62   @NotNull
63   @NonNls
64   public abstract String getName();
65
66   /**
67    * Gets the {@link VirtualFileSystem} this file belongs to.
68    *
69    * @return the {@link VirtualFileSystem}
70    */
71   @NotNull
72   public abstract VirtualFileSystem getFileSystem();
73
74   /**
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 ('/').
79    *
80    * @return the path
81    */
82   public abstract String getPath();
83
84   /**
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>&lt;protocol&gt;://&lt;path&gt;</code>.
87    * <p/>
88    * File can be found by its URL using {@link VirtualFileManager#findFileByUrl} method.
89    *
90    * @return the URL consisting of protocol and path
91    * @see VirtualFileManager#findFileByUrl
92    * @see VirtualFile#getPath
93    * @see VirtualFileSystem#getProtocol
94    */
95   @NotNull
96   public String getUrl() {
97     return VirtualFileManager.constructUrl(getFileSystem().getProtocol(), getPath());
98   }
99
100   /**
101    * Fetches "presentable URL" of this file. "Presentable URL" is a string to be used for displaying this
102    * file in the UI.
103    *
104    * @return the presentable URL.
105    * @see VirtualFileSystem#extractPresentableUrl
106    */
107   public final String getPresentableUrl() {
108     return getFileSystem().extractPresentableUrl(getPath());
109   }
110
111   /**
112    * Used as a property name in the {@link VirtualFilePropertyEvent} fired when the name of a
113    * {@link VirtualFile} changes.
114    *
115    * @see VirtualFileListener#propertyChanged
116    * @see VirtualFilePropertyEvent#getPropertyName
117    */
118   @NonNls public static final String PROP_NAME = "name";
119
120   /**
121    * Used as a property name in the {@link VirtualFilePropertyEvent} fired when the encoding of a
122    * {@link VirtualFile} changes.
123    *
124    * @see VirtualFileListener#propertyChanged
125    * @see VirtualFilePropertyEvent#getPropertyName
126    */
127   @NonNls public static final String PROP_ENCODING = "encoding";
128
129   /**
130    * Used as a property name in the {@link VirtualFilePropertyEvent} fired when the write permission of a
131    * {@link VirtualFile} changes.
132    *
133    * @see VirtualFileListener#propertyChanged
134    * @see VirtualFilePropertyEvent#getPropertyName
135    */
136   @NonNls public static final String PROP_WRITABLE = "writable";
137
138   /**
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.
141    *
142    * @return the extension or null if file name doesn't contain '.'
143    */
144   @Nullable
145   @NonNls
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);
151   }
152
153   /**
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.
156    *
157    * @return the name without extension
158    *         if there is no '.' in it
159    */
160   @NonNls
161   @NotNull
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);
167   }
168
169
170   /**
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)}.
174    *
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
180    */
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));
185     }
186
187     getFileSystem().renameFile(requestor, this, newName);
188   }
189
190   /**
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.
193    *
194    * @return <code>true</code> if this file is writable, <code>false</code> otherwise
195    */
196   public abstract boolean isWritable();
197
198   /**
199    * Checks whether this file is a directory.
200    *
201    * @return <code>true</code> if this file is a directory, <code>false</code> otherwise
202    */
203   public abstract boolean isDirectory();
204
205   /**
206    * Checks whether this file is a symbolic link.
207    *
208    * @since 11.0
209    * @return <code>true</code> if this file is a symbolic link, <code>false</code> otherwise
210    */
211   public boolean isSymLink() {
212     return false;
213   }
214
215   @Nullable
216   public String resolveSymLink() {
217     return null;
218   }
219
220   /**
221    * Checks whether this file is a special (e.g. FIFO or device) file.
222    *
223    * @since 11.0
224    * @return <code>true</code> if the file exists and is a special one, <code>false</code> otherwise
225    */
226   public boolean isSpecialFile() {
227     return false;
228   }
229
230   /**
231    * Attempts to resolve a symbolic link represented by this file and returns link target.
232    *
233    * @since 11.0
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
237    */
238   @Nullable
239   public VirtualFile getRealFile() {
240     return this;
241   }
242
243   /**
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.
249    *
250    * @return <code>true</code> if this is a valid file, <code>false</code> otherwise
251    */
252   public abstract boolean isValid();
253
254   /**
255    * Gets the parent <code>VirtualFile</code>.
256    *
257    * @return the parent file or <code>null</code> if this file is a root directory
258    */
259   public abstract VirtualFile getParent();
260
261   /**
262    * Gets the child files.
263    *
264    * @return array of the child files or <code>null</code> if this file is not a directory
265    */
266   public abstract VirtualFile[] getChildren();
267
268   /**
269    * Finds child of this file with the given name.
270    *
271    * @param name the file name to search by
272    * @return the file if found any, <code>null</code> otherwise
273    */
274   @Nullable
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)) {
280         return child;
281       }
282     }
283     return null;
284   }
285
286   @Nullable
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);
291   }
292
293   /**
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}
297    */
298   @NotNull
299   public FileType getFileType() {
300     return FileTypeRegistry.getInstance().getFileTypeByFile(this);
301   }
302
303   /**
304    * Finds file by path relative to this file.
305    *
306    * @param relPath the relative path to search by
307    * @return the file if found any, <code>null</code> otherwise
308    */
309   @Nullable
310   public VirtualFile findFileByRelativePath(@NotNull @NonNls String relPath) {
311     if (relPath.length() == 0) return this;
312     relPath = StringUtil.trimStart(relPath, "/");
313
314     int index = relPath.indexOf('/');
315     if (index < 0) index = relPath.length();
316     String name = relPath.substring(0, index);
317
318     VirtualFile child;
319     if (name.equals(".")) {
320       child = this;
321     }
322     else if (name.equals("..")) {
323       child = getParent();
324     }
325     else {
326       child = findChild(name);
327     }
328
329     if (child == null) return null;
330
331     if (index < relPath.length()) {
332       return child.findFileByRelativePath(relPath.substring(index + 1));
333     }
334     else {
335       return child;
336     }
337   }
338
339   /**
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}.
342    *
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
349    */
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"));
353     }
354
355     if (!isValid()) {
356       throw new IOException(VfsBundle.message("invalid.directory.create.files"));
357     }
358
359     if (!isValidName(name)) {
360       throw new IOException(VfsBundle.message("directory.invalid.name.error", name));
361     }
362
363     if (findChild(name) != null) {
364       throw new IOException(VfsBundle.message("file.create.already.exists.error", getUrl(), name));
365     }
366
367     return getFileSystem().createChildDirectory(requestor, this, name);
368   }
369
370   /**
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}.
373    *
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
379    */
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"));
383     }
384
385     if (!isValid()) {
386       throw new IOException(VfsBundle.message("invalid.directory.create.files"));
387     }
388
389     if (!isValidName(name)) {
390       throw new IOException(VfsBundle.message("file.invalid.name.error", name));
391     }
392
393     if (findChild(name) != null) {
394       throw new IOException(VfsBundle.message("file.create.already.exists.error", getUrl(), name));
395     }
396
397     return getFileSystem().createChildFile(requestor, this, name);
398   }
399
400   /**
401    * Deletes this file. This method should be only called within write-action.
402    * See {@link com.intellij.openapi.application.Application#runWriteAction}.
403    *
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
408    */
409   public void delete(Object requestor) throws IOException {
410     LOG.assertTrue(isValid(), "Deleting invalid file");
411     getFileSystem().deleteFile(requestor, this);
412   }
413
414   /**
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}.
417    *
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
423    */
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()));
427     }
428
429     EncodingRegistry.doActionAndRestoreEncoding(this, new ThrowableComputable<VirtualFile, IOException>() {
430       @Override
431       public VirtualFile compute() throws IOException {
432         getFileSystem().moveFile(requestor, VirtualFile.this, newParent);
433         return VirtualFile.this;
434       }
435     });
436   }
437
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()));
441     }
442
443     if (!newParent.isDirectory()) {
444       throw new IOException(VfsBundle.message("file.copy.target.must.be.directory"));
445     }
446
447     return EncodingRegistry.doActionAndRestoreEncoding(this, new ThrowableComputable<VirtualFile, IOException>() {
448       @Override
449       public VirtualFile compute() throws IOException {
450         return getFileSystem().copyFile(requestor, VirtualFile.this, newParent, copyName);
451       }
452     });
453   }
454
455
456   public final void setBinaryContent(byte[] content) throws IOException {
457     setBinaryContent(content, -1, -1);
458   }
459
460   /**
461    * @return Retrieve the charset file has been loaded with (if loaded) and would be saved with (if would).
462    */
463   public Charset getCharset() {
464     Charset charset = getUserData(CHARSET_KEY);
465     if (charset == null) {
466       charset = EncodingRegistry.getInstance().getDefaultCharset();
467       setCharset(charset);
468     }
469     return charset;
470   }
471
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)) {
479       bom = existingBOM;
480     }
481     setBOM(bom);
482
483     if (old != null) { //do not send on detect
484       VirtualFileManager.getInstance().notifyPropertyChanged(this, PROP_ENCODING, old, charset);
485     }
486   }
487
488   public boolean isCharsetSet() {
489     return getUserData(CHARSET_KEY) != null;
490   }
491
492   public void setBinaryContent(final byte[] content, long newModificationStamp, long newTimeStamp) throws IOException {
493     setBinaryContent(content, newModificationStamp, newTimeStamp, this);
494   }
495   public void setBinaryContent(final byte[] content, long newModificationStamp, long newTimeStamp, Object requestor) throws IOException {
496     OutputStream outputStream = null;
497     try {
498       outputStream = getOutputStream(requestor, newModificationStamp, newTimeStamp);
499       outputStream.write(content);
500       outputStream.flush();
501     }
502     finally {
503       if (outputStream != null) outputStream.close();
504     }
505   }
506
507   /**
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.
510    *
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
516    */
517   public final OutputStream getOutputStream(Object requestor) throws IOException {
518     return getOutputStream(requestor, -1, -1);
519   }
520
521   /**
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>
524    * <p/>
525    * Normally you should not use this method.
526    *
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.
528    *
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()
537    */
538   @NotNull
539   public abstract OutputStream getOutputStream(Object requestor, long newModificationStamp, long newTimeStamp) throws IOException;
540
541   /**
542    * Returns file content as an array of bytes.
543    * Has the same effect as contentsToByteArray(true).
544    *
545    * @return file content
546    * @throws IOException if an I/O error occurs
547    * @see #contentsToByteArray(boolean)
548    * @see #getInputStream()
549    */
550   @NotNull
551   public abstract byte[] contentsToByteArray() throws IOException;
552
553   /**
554    * Returns file content as an array of bytes.
555    *
556    * @param cacheContent set true to
557    * @return file content
558    * @throws IOException if an I/O error occurs
559    * @see #contentsToByteArray()
560    */
561   @NotNull
562   public byte[] contentsToByteArray(boolean cacheContent) throws IOException {
563     return contentsToByteArray();
564   }
565
566
567   /**
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.
570    *
571    * @return modification stamp
572    * @see #getTimeStamp()
573    */
574   public long getModificationStamp() {
575     throw new UnsupportedOperationException();
576   }
577
578   /**
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.
581    *
582    * @return timestamp
583    * @see java.io.File#lastModified
584    */
585   public abstract long getTimeStamp();
586
587   /**
588    * File length in bytes.
589    *
590    * @return the length of this file.
591    */
592   public abstract long getLength();
593
594   /**
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.
599    * <p/>
600    * This method should be only called within write-action.
601    * See {@link com.intellij.openapi.application.Application#runWriteAction}.
602    *
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
606    */
607   public void refresh(boolean asynchronous, boolean recursive) {
608     refresh(asynchronous, recursive, null);
609   }
610
611   /**
612    * The same as {@link #refresh(boolean, boolean)} but also runs <code>postRunnable</code>
613    * after the operation is completed.
614    */
615   public abstract void refresh(boolean asynchronous, boolean recursive, @Nullable Runnable postRunnable);
616
617   public String getPresentableName() {
618     return getName();
619   }
620
621   @Override
622   public long getModificationCount() {
623     return isValid() ? getTimeStamp() : -1;
624   }
625
626   /**
627    * @param name
628    * @return whether file name equals to this name
629    *         result depends on the filesystem specifics
630    */
631   protected boolean nameEquals(@NotNull @NonNls String name) {
632     return getName().equals(name);
633   }
634
635   /**
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.
638    *
639    * @return <code>InputStream</code>
640    * @throws IOException if an I/O error occurs
641    * @see #contentsToByteArray
642    */
643   public abstract InputStream getInputStream() throws IOException;
644
645   @Nullable
646   public byte[] getBOM() {
647     return getUserData(BOM_KEY);
648   }
649
650   public void setBOM(@Nullable byte[] BOM) {
651     putUserData(BOM_KEY, BOM);
652   }
653
654   @NonNls
655   public String toString() {
656     return "VirtualFile: " + getPresentableUrl();
657   }
658
659   public boolean exists() {
660     return isValid();
661   }
662
663   public boolean isInLocalFileSystem() {
664     return false;
665   }
666
667   public static boolean isValidName(@NotNull String name) {
668     return name.indexOf('\\') < 0 && name.indexOf('/') < 0;
669   }
670 }