optimisation: way to get file boolean attributes in batch
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / vfs / impl / local / LocalFileSystemBase.java
1 /*
2  * Copyright 2000-2012 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.impl.local;
17
18 import com.intellij.ide.GeneralSettings;
19 import com.intellij.openapi.application.Application;
20 import com.intellij.openapi.application.ApplicationManager;
21 import com.intellij.openapi.diagnostic.Logger;
22 import com.intellij.openapi.util.SystemInfo;
23 import com.intellij.openapi.util.io.FileSystemUtil;
24 import com.intellij.openapi.util.io.FileUtil;
25 import com.intellij.openapi.util.text.StringUtil;
26 import com.intellij.openapi.vfs.*;
27 import com.intellij.openapi.vfs.ex.VirtualFileManagerEx;
28 import com.intellij.openapi.vfs.newvfs.*;
29 import com.intellij.openapi.vfs.newvfs.impl.FakeVirtualFile;
30 import com.intellij.util.ArrayUtil;
31 import com.intellij.util.Processor;
32 import com.intellij.util.ThrowableConsumer;
33 import com.intellij.util.io.SafeFileOutputStream;
34 import com.intellij.util.io.fs.IFile;
35 import org.intellij.lang.annotations.MagicConstant;
36 import org.jetbrains.annotations.NotNull;
37 import org.jetbrains.annotations.Nullable;
38
39 import java.io.*;
40 import java.lang.reflect.Method;
41 import java.util.ArrayList;
42 import java.util.List;
43 import java.util.Locale;
44
45 /**
46  * @author Dmitry Avdeev
47  */
48 public abstract class LocalFileSystemBase extends LocalFileSystem {
49   protected static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vfs.impl.local.LocalFileSystemImpl");
50
51   private final List<LocalFileOperationsHandler> myHandlers = new ArrayList<LocalFileOperationsHandler>();
52
53   @Override
54   @Nullable
55   public VirtualFile findFileByPath(@NotNull String path) {
56     String canonicalPath = getVfsCanonicalPath(path);
57     if (canonicalPath == null) return null;
58     return VfsImplUtil.findFileByPath(this, path);
59   }
60
61   @Override
62   public VirtualFile findFileByPathIfCached(@NotNull String path) {
63     String canonicalPath = getVfsCanonicalPath(path);
64     if (canonicalPath == null) return null;
65     return VfsImplUtil.findFileByPathIfCached(this, canonicalPath);
66   }
67
68   @Override
69   @Nullable
70   public VirtualFile refreshAndFindFileByPath(@NotNull String path) {
71     String canonicalPath = getVfsCanonicalPath(path);
72     if (canonicalPath == null) return null;
73     return VfsImplUtil.refreshAndFindFileByPath(this, canonicalPath);
74   }
75
76   @Override
77   public VirtualFile findFileByIoFile(@NotNull File file) {
78     String path = file.getAbsolutePath();
79     if (path == null) return null;
80     return findFileByPath(path.replace(File.separatorChar, '/'));
81   }
82
83   @Override
84   @Nullable
85   public VirtualFile findFileByIoFile(@NotNull final IFile file) {
86     String path = file.getPath();
87     if (path == null) return null;
88     return findFileByPath(path.replace(File.separatorChar, '/'));
89   }
90
91   @Nullable
92   protected static String getVfsCanonicalPath(@NotNull String path) {
93     if (path.isEmpty()) {
94       try {
95         return new File("").getCanonicalPath();
96       }
97       catch (IOException e) {
98         return path;
99       }
100     }
101
102     if (SystemInfo.isWindows) {
103       if (path.startsWith("//") || path.startsWith("\\\\")) {
104         return path;
105       }
106
107       if (path.charAt(0) == '/') path = path.substring(1); //hack over new File(path).toUrl().getFile()
108       if (path.contains("~")) {
109         try {
110           return new File(path.replace('/', File.separatorChar)).getCanonicalPath().replace(File.separatorChar, '/');
111         }
112         catch (IOException e) {
113           return null;
114         }
115       }
116     }
117     else {
118       if (!StringUtil.startsWithChar(path, '/')) {
119         path = new File(path).getAbsolutePath();
120       }
121     }
122
123
124     return path.replace(File.separatorChar, '/');
125   }
126
127   @NotNull
128   protected static File convertToIOFile(@NotNull final VirtualFile file) {
129     String path = file.getPath();
130     if (StringUtil.endsWithChar(path, ':') && path.length() == 2 && (SystemInfo.isWindows || SystemInfo.isOS2)) {
131       path += "/"; // Make 'c:' resolve to a root directory for drive c:, not the current directory on that drive
132     }
133
134     return new File(path);
135   }
136
137   private static final Method JAVA_IO_FILESYSTEM_GET_BOOLEAN_ATTRIBUTES_METHOD;
138   private static final Object/* java.io.FileSystem */ JAVA_IO_FILESYSTEM;
139   // copied from FileSystem
140
141   static {
142     Object fs;
143     Method getBooleanAttributes;
144     try {
145       Class<?> fsClass = Class.forName("java.io.FileSystem");
146       Method getFileSystem = fsClass.getMethod("getFileSystem");
147       getFileSystem.setAccessible(true);
148       fs = getFileSystem.invoke(null);
149       getBooleanAttributes = fsClass.getDeclaredMethod("getBooleanAttributes", File.class);
150       if (fs == null || getBooleanAttributes == null) {
151         fs = null;
152         getBooleanAttributes = null;
153       }
154       else {
155         getBooleanAttributes.setAccessible(true);
156       }
157     }
158     catch (Exception e) {
159       fs = null;
160       getBooleanAttributes = null;
161     }
162     JAVA_IO_FILESYSTEM = fs;
163     JAVA_IO_FILESYSTEM_GET_BOOLEAN_ATTRIBUTES_METHOD = getBooleanAttributes;
164   }
165
166
167
168   @NotNull
169   private static File convertToIOFileAndCheck(@NotNull final VirtualFile file) throws FileNotFoundException {
170     final File ioFile = convertToIOFile(file);
171
172     int intFlags = getBooleanAttributes(ioFile);
173     if (intFlags != -1) {
174       if ((intFlags & BA_EXISTS) != 0 && (intFlags & BA_REGULAR) == 0) {
175         throw new FileNotFoundException("Not a file: " + ioFile);
176       }
177       return ioFile;
178     }
179     if (ioFile.exists() && !ioFile.isFile()) {
180       throw new FileNotFoundException("Not a file: " + ioFile);
181     }
182
183     return ioFile;
184   }
185
186   // todo[r.sh] use NIO2 API after migration to JDK 7
187   // returns -1 if could not get attributes
188   @MagicConstant(flags = {BA_EXISTS, BA_REGULAR, BA_DIRECTORY, BA_HIDDEN})
189   private static int getBooleanAttributes(@NotNull File f) {
190     if (JAVA_IO_FILESYSTEM_GET_BOOLEAN_ATTRIBUTES_METHOD != null) {
191       try {
192         Object flags = JAVA_IO_FILESYSTEM_GET_BOOLEAN_ATTRIBUTES_METHOD.invoke(JAVA_IO_FILESYSTEM, f);
193         return ((Integer)flags).intValue();
194       }
195       catch (Exception ignored) { }
196     }
197     return -1;
198   }
199
200   @Override
201   public boolean exists(@NotNull final VirtualFile fileOrDirectory) {
202     String path = fileOrDirectory.getPath();
203     if (fileOrDirectory.getParent() == null && path.startsWith("//")) return true; // Windows UNC root like //unit-333
204     if (StringUtil.isEmpty(path)) return true; // fake top dir for Windows
205     return convertToIOFile(fileOrDirectory).exists();
206   }
207
208   @Override
209   public long getLength(@NotNull final VirtualFile file) {
210     return convertToIOFile(file).length();
211   }
212
213   @Override
214   public long getTimeStamp(@NotNull final VirtualFile file) {
215     return convertToIOFile(file).lastModified();
216   }
217
218   @Override
219   public boolean isDirectory(@NotNull final VirtualFile file) {
220     return convertToIOFile(file).isDirectory();
221   }
222
223   @Override
224   public boolean isWritable(@NotNull final VirtualFile file) {
225     return convertToIOFile(file).canWrite();
226   }
227
228   @Override
229   public boolean isSymLink(@NotNull final VirtualFile file) {
230     return FileSystemUtil.isSymLink(file.getPath());
231   }
232
233   @Override
234   public String resolveSymLink(@NotNull VirtualFile file) {
235     return FileSystemUtil.resolveSymLink(file.getPath());
236   }
237
238   @Override
239   public boolean isSpecialFile(@NotNull final VirtualFile file) {
240     if (!SystemInfo.isUnix) return false;
241     final File ioFile = convertToIOFile(file);
242     return isSpecialFile(ioFile);
243   }
244
245   private static boolean isSpecialFile(@NotNull File ioFile) {
246     int flags = getBooleanAttributes(ioFile);
247     if (flags != -1) {
248       return (flags & (BA_REGULAR | BA_DIRECTORY | BA_EXISTS)) == (~BA_REGULAR & ~BA_DIRECTORY & BA_EXISTS);
249     }
250     return !ioFile.isFile() && !ioFile.isDirectory() && ioFile.exists();
251   }
252
253   @Override
254   @NotNull
255   public String[] list(@NotNull final VirtualFile file) {
256     if (file.getParent() == null) {
257       final File[] roots = File.listRoots();
258       if (roots.length == 1 && roots[0].getName().isEmpty()) {
259         return roots[0].list();
260       }
261       if (file.getName().isEmpty()) {
262         // return drive letter names for the 'fake' root on windows
263         final String[] names = new String[roots.length];
264         for (int i = 0; i < names.length; i++) {
265           String name = roots[i].getPath();
266           if (name.endsWith(File.separator)) {
267             name = name.substring(0, name.length() - File.separator.length());
268           }
269           names[i] = name;
270         }
271         return names;
272       }
273     }
274
275     if (isInvalidSymLink(file)) {
276       return ArrayUtil.EMPTY_STRING_ARRAY;
277     }
278
279     final String[] names = convertToIOFile(file).list();
280     return names != null ? names : ArrayUtil.EMPTY_STRING_ARRAY;
281   }
282
283   protected static boolean isInvalidSymLink(@NotNull final VirtualFile file) {
284     if (!file.isSymLink()) return false;
285     final VirtualFile realFile = file.getRealFile();
286     return realFile == null ||
287            realFile == file ||
288            FileUtil.isAncestor(convertToIOFile(realFile), convertToIOFile(file), true);
289   }
290
291   @Override
292   @NotNull
293   public String getProtocol() {
294     return PROTOCOL;
295   }
296
297   @Override
298   @Nullable
299   public String normalize(@NotNull final String path) {
300     return getVfsCanonicalPath(path);
301   }
302
303   @Override
304   public VirtualFile refreshAndFindFileByIoFile(@NotNull File file) {
305     String path = file.getAbsolutePath();
306     if (path == null) return null;
307     return refreshAndFindFileByPath(path.replace(File.separatorChar, '/'));
308   }
309
310   @Override
311   @Nullable
312   public VirtualFile refreshAndFindFileByIoFile(@NotNull final IFile ioFile) {
313     String path = ioFile.getPath();
314     if (path == null) return null;
315     return refreshAndFindFileByPath(path.replace(File.separatorChar, '/'));
316   }
317
318   @Override
319   public void refreshIoFiles(@NotNull Iterable<File> files) {
320     refreshIoFiles(files, false, false, null);
321   }
322
323   @Override
324   public void refreshIoFiles(@NotNull Iterable<File> files, boolean async, boolean recursive, @Nullable Runnable onFinish) {
325     final VirtualFileManagerEx manager = (VirtualFileManagerEx)VirtualFileManager.getInstance();
326
327     Application app = ApplicationManager.getApplication();
328     boolean fireCommonRefreshSession = app.isDispatchThread() || app.isWriteAccessAllowed();
329     if (fireCommonRefreshSession) manager.fireBeforeRefreshStart(false);
330
331     try {
332       List<VirtualFile> filesToRefresh = new ArrayList<VirtualFile>();
333
334       for (File file : files) {
335         final VirtualFile virtualFile = refreshAndFindFileByIoFile(file);
336         if (virtualFile != null) {
337           filesToRefresh.add(virtualFile);
338         }
339       }
340
341       RefreshQueue.getInstance().refresh(async, recursive, onFinish, VfsUtil.toVirtualFileArray(filesToRefresh));
342     }
343     finally {
344       if (fireCommonRefreshSession) manager.fireAfterRefreshFinish(false);
345     }
346   }
347
348   @Override
349   public void refreshFiles(@NotNull Iterable<VirtualFile> files) {
350     refreshFiles(files, false, false, null);
351   }
352
353   @Override
354   public void refreshFiles(@NotNull Iterable<VirtualFile> files, boolean async, boolean recursive, @Nullable Runnable onFinish) {
355     List<VirtualFile> list = new ArrayList<VirtualFile>();
356     for (VirtualFile file : files) {
357       list.add(file);
358     }
359
360     RefreshQueue.getInstance().refresh(async, recursive, onFinish, VfsUtil.toVirtualFileArray(list));
361   }
362
363   @Override
364   public byte[] physicalContentsToByteArray(@NotNull final VirtualFile virtualFile) throws IOException {
365     return virtualFile.contentsToByteArray();
366   }
367
368   @Override
369   public long physicalLength(@NotNull final VirtualFile virtualFile) {
370     return virtualFile.getLength();
371   }
372
373   @Override
374   public void registerAuxiliaryFileOperationsHandler(@NotNull LocalFileOperationsHandler handler) {
375     if (myHandlers.contains(handler)) {
376       LOG.error("Handler " + handler + " already registered.");
377     }
378     myHandlers.add(handler);
379   }
380
381   @Override
382   public void unregisterAuxiliaryFileOperationsHandler(@NotNull LocalFileOperationsHandler handler) {
383     if (!myHandlers.remove(handler)) {
384       LOG.error("Handler" + handler + " haven't been registered or already unregistered.");
385     }
386   }
387
388   @Override
389   public boolean processCachedFilesInSubtree(@NotNull final VirtualFile file, @NotNull Processor<VirtualFile> processor) {
390     return file.getFileSystem() != this
391            || processFile((NewVirtualFile)file, processor);
392   }
393
394   private static boolean processFile(@NotNull NewVirtualFile file, @NotNull Processor<VirtualFile> processor) {
395     if (!processor.process(file)) return false;
396     if (file.isDirectory()) {
397       for (final VirtualFile child : file.getCachedChildren()) {
398         if (!processFile((NewVirtualFile)child, processor)) return false;
399       }
400     }
401     return true;
402   }
403
404   private boolean auxDelete(@NotNull VirtualFile file) throws IOException {
405     for (LocalFileOperationsHandler handler : myHandlers) {
406       if (handler.delete(file)) return true;
407     }
408
409     return false;
410   }
411
412   private boolean auxMove(@NotNull VirtualFile file, @NotNull VirtualFile toDir) throws IOException {
413     for (LocalFileOperationsHandler handler : myHandlers) {
414       if (handler.move(file, toDir)) return true;
415     }
416     return false;
417   }
418
419   private void auxNotifyCompleted(@NotNull ThrowableConsumer<LocalFileOperationsHandler, IOException> consumer) {
420     for (LocalFileOperationsHandler handler : myHandlers) {
421       handler.afterDone(consumer);
422     }
423   }
424
425   @Nullable
426   private File auxCopy(@NotNull VirtualFile file, @NotNull VirtualFile toDir, @NotNull String copyName) throws IOException {
427     for (LocalFileOperationsHandler handler : myHandlers) {
428       final File copy = handler.copy(file, toDir, copyName);
429       if (copy != null) return copy;
430     }
431     return null;
432   }
433
434   private boolean auxRename(@NotNull VirtualFile file, @NotNull String newName) throws IOException {
435     for (LocalFileOperationsHandler handler : myHandlers) {
436       if (handler.rename(file, newName)) return true;
437     }
438     return false;
439   }
440
441   private boolean auxCreateFile(@NotNull VirtualFile dir, @NotNull String name) throws IOException {
442     for (LocalFileOperationsHandler handler : myHandlers) {
443       if (handler.createFile(dir, name)) return true;
444     }
445     return false;
446   }
447
448   private boolean auxCreateDirectory(@NotNull VirtualFile dir, @NotNull String name) throws IOException {
449     for (LocalFileOperationsHandler handler : myHandlers) {
450       if (handler.createDirectory(dir, name)) return true;
451     }
452     return false;
453   }
454
455   private static void delete(@NotNull File physicalFile) throws IOException {
456     if (!FileSystemUtil.isSymLink(physicalFile)) {
457       File[] list = physicalFile.listFiles();
458       if (list != null) {
459         for (File aList : list) {
460           delete(aList);
461         }
462       }
463     }
464     if (!physicalFile.delete()) {
465       throw new IOException(VfsBundle.message("file.delete.error", physicalFile.getPath()));
466     }
467   }
468
469   @Override
470   @NotNull
471   public VirtualFile createChildDirectory(final Object requestor, @NotNull final VirtualFile parent, @NotNull final String dir) throws IOException {
472     final File ioDir = new File(convertToIOFile(parent), dir);
473     final boolean succeed = auxCreateDirectory(parent, dir) || ioDir.mkdirs();
474     auxNotifyCompleted(new ThrowableConsumer<LocalFileOperationsHandler, IOException>() {
475       @Override
476       public void consume(LocalFileOperationsHandler handler) throws IOException {
477         handler.createDirectory(parent, dir);
478       }
479     });
480     if (!succeed) {
481       throw new IOException("Failed to create directory: " + ioDir.getPath());
482     }
483
484     return new FakeVirtualFile(parent, dir);
485   }
486
487   @Override
488   public VirtualFile createChildFile(final Object requestor, @NotNull final VirtualFile parent, @NotNull final String file) throws IOException {
489     final File ioFile = new File(convertToIOFile(parent), file);
490     final boolean succeed = auxCreateFile(parent, file) || FileUtil.createIfDoesntExist(ioFile);
491     auxNotifyCompleted(new ThrowableConsumer<LocalFileOperationsHandler, IOException>() {
492       @Override
493       public void consume(LocalFileOperationsHandler handler) throws IOException {
494         handler.createFile(parent, file);
495       }
496     });
497     if (!succeed) {
498       throw new IOException("Failed to create child file at " + ioFile.getPath());
499     }
500
501     return new FakeVirtualFile(parent, file);
502   }
503
504   @Override
505   public void deleteFile(final Object requestor, @NotNull final VirtualFile file) throws IOException {
506     if (!auxDelete(file)) {
507       delete(convertToIOFile(file));
508     }
509     auxNotifyCompleted(new ThrowableConsumer<LocalFileOperationsHandler, IOException>() {
510       @Override
511       public void consume(LocalFileOperationsHandler handler) throws IOException {
512         handler.delete(file);
513       }
514     });
515   }
516
517   @Override
518   public boolean isCaseSensitive() {
519     return SystemInfo.isFileSystemCaseSensitive;
520   }
521
522   @Override
523   @NotNull
524   public InputStream getInputStream(@NotNull final VirtualFile file) throws IOException {
525     return new BufferedInputStream(new FileInputStream(convertToIOFileAndCheck(file)));
526   }
527
528   @Override
529   @NotNull
530   public byte[] contentsToByteArray(@NotNull final VirtualFile file) throws IOException {
531     final FileInputStream stream = new FileInputStream(convertToIOFileAndCheck(file));
532     try {
533       final int length = (int)file.getLength();
534       assert length >= 0 : file;
535       return FileUtil.loadBytes(stream, length);
536     }
537     finally {
538       stream.close();
539     }
540   }
541
542   @Override
543   @NotNull
544   public OutputStream getOutputStream(@NotNull final VirtualFile file, final Object requestor, final long modStamp, final long timeStamp) throws IOException {
545     final File ioFile = convertToIOFileAndCheck(file);
546     @SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
547     final OutputStream stream = shallUseSafeStream(requestor, file) ? new SafeFileOutputStream(ioFile, SystemInfo.isUnix) : new FileOutputStream(ioFile);
548     return new BufferedOutputStream(stream) {
549       @Override
550       public void close() throws IOException {
551         super.close();
552         if (timeStamp > 0 && ioFile.exists()) {
553           if (!ioFile.setLastModified(timeStamp)) {
554             LOG.warn("Failed: " + file.getPath() + ", new:" + timeStamp + ", old:" + ioFile.lastModified());
555           }
556         }
557       }
558     };
559   }
560
561   private static boolean shallUseSafeStream(final Object requestor, @NotNull VirtualFile file) {
562     return requestor instanceof SafeWriteRequestor && GeneralSettings.getInstance().isUseSafeWrite() && !file.isSymLink();
563   }
564
565   @Override
566   public void moveFile(final Object requestor, @NotNull final VirtualFile file, @NotNull final VirtualFile newParent) throws IOException {
567     if (!auxMove(file, newParent)) {
568       final File ioFrom = convertToIOFile(file);
569       final File ioParent = convertToIOFile(newParent);
570       if (!ioParent.isDirectory()) {
571         throw new IOException("Target '" + ioParent + "' is not a directory");
572       }
573       if (!ioFrom.renameTo(new File(ioParent, file.getName()))) {
574         throw new IOException("Move failed: '" + file.getPath() + "' to '" + newParent.getPath() +"'");
575       }
576     }
577     auxNotifyCompleted(new ThrowableConsumer<LocalFileOperationsHandler, IOException>() {
578       @Override
579       public void consume(LocalFileOperationsHandler handler) throws IOException {
580         handler.move(file, newParent);
581       }
582     });
583   }
584
585   @Override
586   public void renameFile(final Object requestor, @NotNull final VirtualFile file, @NotNull final String newName) throws IOException {
587     if (!file.exists()) {
588       throw new IOException("File to move does not exist: " + file.getPath());
589     }
590
591     final VirtualFile parent = file.getParent();
592     assert parent != null;
593
594     if (!auxRename(file, newName)) {
595       if (!convertToIOFile(file).renameTo(new File(convertToIOFile(parent), newName))) {
596         throw new IOException("Destination already exists: " + parent.getPath() + "/" + newName);
597       }
598     }
599     auxNotifyCompleted(new ThrowableConsumer<LocalFileOperationsHandler, IOException>() {
600       @Override
601       public void consume(LocalFileOperationsHandler handler) throws IOException {
602         handler.rename(file, newName);
603       }
604     });
605   }
606
607   @Override
608   public VirtualFile copyFile(final Object requestor,
609                               @NotNull final VirtualFile vFile,
610                               @NotNull final VirtualFile newParent,
611                               @NotNull final String copyName) throws IOException {
612     File physicalFile = convertToIOFile(vFile);
613     if (isSpecialFile(physicalFile)) {
614       throw new FileNotFoundException("Not a file: " + physicalFile);
615     }
616
617     File physicalCopy = auxCopy(vFile, newParent, copyName);
618
619     try {
620       if (physicalCopy == null) {
621         File newPhysicalParent = convertToIOFile(newParent);
622         physicalCopy = new File(newPhysicalParent, copyName);
623
624         try {
625           if (physicalFile.isDirectory()) {
626             FileUtil.copyDir(physicalFile, physicalCopy);
627           }
628           else {
629             FileUtil.copy(physicalFile, physicalCopy);
630           }
631         }
632         catch (IOException e) {
633           FileUtil.delete(physicalCopy);
634           throw e;
635         }
636       }
637     }
638     finally {
639       auxNotifyCompleted(new ThrowableConsumer<LocalFileOperationsHandler, IOException>() {
640         @Override
641         public void consume(LocalFileOperationsHandler handler) throws IOException {
642           handler.copy(vFile, newParent, copyName);
643         }
644       });
645     }
646     return new FakeVirtualFile(newParent, copyName);
647   }
648
649   @Override
650   public void setTimeStamp(@NotNull final VirtualFile file, final long timeStamp) {
651     final File ioFile = convertToIOFile(file);
652     if (ioFile.exists() && !ioFile.setLastModified(timeStamp)) {
653       LOG.warn("Failed: " + file.getPath() + ", new:" + timeStamp + ", old:" + ioFile.lastModified());
654     }
655   }
656
657   @Override
658   public void setWritable(@NotNull final VirtualFile file, final boolean writableFlag) throws IOException {
659     FileUtil.setReadOnlyAttribute(file.getPath(), !writableFlag);
660     final File ioFile = convertToIOFile(file);
661     if (ioFile.canWrite() != writableFlag) {
662       throw new IOException("Failed to change read-only flag for " + ioFile.getPath());
663     }
664   }
665
666   @Override
667   protected String extractRootPath(@NotNull final String path) {
668     if (path.isEmpty()) {
669       try {
670         return extractRootPath(new File("").getCanonicalPath());
671       }
672       catch (IOException e) {
673         throw new RuntimeException(e);
674       }
675     }
676
677     if (SystemInfo.isWindows) {
678       if (path.length() >= 2 && path.charAt(1) == ':') {
679         // Drive letter
680         return path.substring(0, 2).toUpperCase(Locale.US);
681       }
682
683       if (path.startsWith("//") || path.startsWith("\\\\")) {
684         // UNC. Must skip exactly two path elements like [\\ServerName\ShareName]\pathOnShare\file.txt
685         // Root path is in square brackets here.
686
687         int slashCount = 0;
688         int idx;
689         for (idx = 2; idx < path.length() && slashCount < 2; idx++) {
690           final char c = path.charAt(idx);
691           if (c == '\\' || c == '/') {
692             slashCount++;
693             idx--;
694           }
695         }
696
697         return path.substring(0, idx);
698       }
699
700       return "";
701     }
702
703     return StringUtil.startsWithChar(path, '/') ? "/" : "";
704   }
705
706   @Override
707   public int getRank() {
708     return 1;
709   }
710
711   @Override
712   public boolean markNewFilesAsDirty() {
713     return true;
714   }
715
716   @Override
717   public String getCanonicallyCasedName(@NotNull final VirtualFile file) {
718     if (isCaseSensitive()) {
719       return super.getCanonicallyCasedName(file);
720     }
721
722     final String originalFileName = file.getName();
723     try {
724       final File ioFile = convertToIOFile(file);
725       final File ioCanonicalFile = ioFile.getCanonicalFile();
726       String canonicalFileName = ioCanonicalFile.getName();
727       if (!SystemInfo.isUnix) {
728         return canonicalFileName;
729       }
730       // linux & mac support symbolic links
731       // unfortunately canonical file resolves sym links
732       // so its name may differ from name of origin file
733       //
734       // Here FS is case sensitive, so let's check that original and
735       // canonical file names are equal if we ignore name case
736       if (canonicalFileName.compareToIgnoreCase(originalFileName) == 0) {
737         // p.s. this should cover most cases related to not symbolic links
738         return canonicalFileName;
739       }
740
741       // Ok, names are not equal. Let's try to find corresponding file name
742       // among original file parent directory
743       final File parentFile = ioFile.getParentFile();
744       if (parentFile != null) {
745         // I hope ls works fast on Unix
746         final String[] canonicalFileNames = parentFile.list();
747         if (canonicalFileNames != null) {
748           for (String name : canonicalFileNames) {
749             // if names are equals
750             if (name.compareToIgnoreCase(originalFileName) == 0) {
751               return name;
752             }
753           }
754         }
755       }
756       // No luck. So ein mist!
757       // Ok, garbage in, garbage out. We may return original or canonical name
758       // no difference. Let's return canonical name just to preserve previous
759       // behaviour of this code.
760       return canonicalFileName;
761     }
762     catch (IOException e) {
763       return originalFileName;
764     }
765   }
766
767   @Override
768   public int getBooleanAttributes(@NotNull VirtualFile file, int flags) {
769     int attributes = getBooleanAttributes(convertToIOFile(file));
770     if (attributes != -1) return attributes & flags;
771     return ((flags & BA_EXISTS) != 0 && exists(file) ? BA_EXISTS : 0) |
772            ((flags & BA_DIRECTORY) != 0 && isDirectory(file) ? BA_DIRECTORY : 0) |
773            ((flags & BA_REGULAR) != 0 && !isSpecialFile(file) ? BA_REGULAR : 0) |
774            ((flags & BA_HIDDEN) != 0 && convertToIOFile(file).isHidden() ? BA_HIDDEN : 0)
775       ;
776   }
777
778   @Override
779   public void refresh(final boolean asynchronous) {
780     RefreshQueue.getInstance().refresh(asynchronous, true, null, ManagingFS.getInstance().getRoots(this));
781   }
782 }