vfs: support uri with custom protocol, like temp://, in VfsUtilCore#findRelativeFile...
[idea/community.git] / platform / core-api / src / com / intellij / openapi / vfs / VfsUtilCore.java
1 /*
2  * Copyright 2000-2015 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.roots.ContentIterator;
20 import com.intellij.openapi.util.Comparing;
21 import com.intellij.openapi.util.SystemInfo;
22 import com.intellij.openapi.util.SystemInfoRt;
23 import com.intellij.openapi.util.io.BufferExposingByteArrayInputStream;
24 import com.intellij.openapi.util.io.FileUtil;
25 import com.intellij.openapi.util.text.StringUtil;
26 import com.intellij.util.Function;
27 import com.intellij.util.PathUtil;
28 import com.intellij.util.Processor;
29 import com.intellij.util.containers.ContainerUtil;
30 import com.intellij.util.containers.Convertor;
31 import com.intellij.util.containers.DistinctRootsCollection;
32 import com.intellij.util.io.URLUtil;
33 import com.intellij.util.lang.UrlClassLoader;
34 import com.intellij.util.text.StringFactory;
35 import org.jetbrains.annotations.NonNls;
36 import org.jetbrains.annotations.NotNull;
37 import org.jetbrains.annotations.Nullable;
38
39 import java.io.*;
40 import java.net.MalformedURLException;
41 import java.net.URL;
42 import java.util.*;
43
44 import static com.intellij.openapi.vfs.VirtualFileVisitor.VisitorException;
45
46 public class VfsUtilCore {
47   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vfs.VfsUtilCore");
48
49   @NonNls private static final String MAILTO = "mailto";
50
51   public static final String LOCALHOST_URI_PATH_PREFIX = "localhost/";
52   public static final char VFS_SEPARATOR_CHAR = '/';
53
54   private static final String PROTOCOL_DELIMITER = ":";
55
56   /**
57    * Checks whether the <code>ancestor {@link VirtualFile}</code> is parent of <code>file
58    * {@link VirtualFile}</code>.
59    *
60    * @param ancestor the file
61    * @param file     the file
62    * @param strict   if <code>false</code> then this method returns <code>true</code> if <code>ancestor</code>
63    *                 and <code>file</code> are equal
64    * @return <code>true</code> if <code>ancestor</code> is parent of <code>file</code>; <code>false</code> otherwise
65    */
66   public static boolean isAncestor(@NotNull VirtualFile ancestor, @NotNull VirtualFile file, boolean strict) {
67     if (!file.getFileSystem().equals(ancestor.getFileSystem())) return false;
68     VirtualFile parent = strict ? file.getParent() : file;
69     while (true) {
70       if (parent == null) return false;
71       if (parent.equals(ancestor)) return true;
72       parent = parent.getParent();
73     }
74   }
75
76   /**
77    * @return {@code true} if {@code file} is located under one of {@code roots} or equal to one of them
78    */
79   public static boolean isUnder(@NotNull VirtualFile file, @Nullable Set<VirtualFile> roots) {
80     if (roots == null || roots.isEmpty()) return false;
81
82     VirtualFile parent = file;
83     while (parent != null) {
84       if (roots.contains(parent)) {
85         return true;
86       }
87       parent = parent.getParent();
88     }
89     return false;
90   }
91
92   /**
93    * @return {@code true} if {@code url} is located under one of {@code rootUrls} or equal to one of them
94    */
95   public static boolean isUnder(@NotNull String url, @Nullable Collection<String> rootUrls) {
96     if (rootUrls == null || rootUrls.isEmpty()) return false;
97
98     for (String excludesUrl : rootUrls) {
99       if (isEqualOrAncestor(excludesUrl, url)) {
100         return true;
101       }
102     }
103     return false;
104   }
105
106   public static boolean isEqualOrAncestor(@NotNull String ancestorUrl, @NotNull String fileUrl) {
107     if (ancestorUrl.equals(fileUrl)) return true;
108     if (StringUtil.endsWithChar(ancestorUrl, '/')) {
109       return fileUrl.startsWith(ancestorUrl);
110     }
111     else {
112       return StringUtil.startsWithConcatenation(fileUrl, ancestorUrl, "/");
113     }
114   }
115
116   public static boolean isAncestor(@NotNull File ancestor, @NotNull File file, boolean strict) {
117     File parent = strict ? file.getParentFile() : file;
118     while (parent != null) {
119       if (parent.equals(ancestor)) return true;
120       parent = parent.getParentFile();
121     }
122
123     return false;
124   }
125
126   @Nullable
127   public static String getRelativePath(@NotNull VirtualFile file, @NotNull VirtualFile ancestor) {
128     return getRelativePath(file, ancestor, VFS_SEPARATOR_CHAR);
129   }
130
131   /**
132    * Gets the relative path of <code>file</code> to its <code>ancestor</code>. Uses <code>separator</code> for
133    * separating files.
134    *
135    * @param file      the file
136    * @param ancestor  parent file
137    * @param separator character to use as files separator
138    * @return the relative path or {@code null} if {@code ancestor} is not ancestor for {@code file}
139    */
140   @Nullable
141   public static String getRelativePath(@NotNull VirtualFile file, @NotNull VirtualFile ancestor, char separator) {
142     if (!file.getFileSystem().equals(ancestor.getFileSystem())) {
143       return null;
144     }
145
146     int length = 0;
147     VirtualFile parent = file;
148     while (true) {
149       if (parent == null) return null;
150       if (parent.equals(ancestor)) break;
151       if (length > 0) {
152         length++;
153       }
154       length += parent.getNameSequence().length();
155       parent = parent.getParent();
156     }
157
158     char[] chars = new char[length];
159     int index = chars.length;
160     parent = file;
161     while (true) {
162       if (parent.equals(ancestor)) break;
163       if (index < length) {
164         chars[--index] = separator;
165       }
166       CharSequence name = parent.getNameSequence();
167       for (int i = name.length() - 1; i >= 0; i--) {
168         chars[--index] = name.charAt(i);
169       }
170       parent = parent.getParent();
171     }
172     return StringFactory.createShared(chars);
173   }
174
175   @Nullable
176   public static VirtualFile getVirtualFileForJar(@Nullable VirtualFile entryVFile) {
177     if (entryVFile == null) return null;
178     final String path = entryVFile.getPath();
179     final int separatorIndex = path.indexOf("!/");
180     if (separatorIndex < 0) return null;
181
182     String localPath = path.substring(0, separatorIndex);
183     return VirtualFileManager.getInstance().findFileByUrl("file://" + localPath);
184   }
185
186   /**
187    * Makes a copy of the <code>file</code> in the <code>toDir</code> folder and returns it.
188    *
189    * @param requestor any object to control who called this method. Note that
190    *                  it is considered to be an external change if <code>requestor</code> is <code>null</code>.
191    *                  See {@link VirtualFileEvent#getRequestor}
192    * @param file      file to make a copy of
193    * @param toDir     directory to make a copy in
194    * @return a copy of the file
195    * @throws IOException if file failed to be copied
196    */
197   @NotNull
198   public static VirtualFile copyFile(Object requestor, @NotNull VirtualFile file, @NotNull VirtualFile toDir) throws IOException {
199     return copyFile(requestor, file, toDir, file.getName());
200   }
201
202   /**
203    * Makes a copy of the <code>file</code> in the <code>toDir</code> folder with the <code>newName</code> and returns it.
204    *
205    * @param requestor any object to control who called this method. Note that
206    *                  it is considered to be an external change if <code>requestor</code> is <code>null</code>.
207    *                  See {@link VirtualFileEvent#getRequestor}
208    * @param file      file to make a copy of
209    * @param toDir     directory to make a copy in
210    * @param newName   new name of the file
211    * @return a copy of the file
212    * @throws IOException if file failed to be copied
213    */
214   @NotNull
215   public static VirtualFile copyFile(Object requestor, @NotNull VirtualFile file, @NotNull VirtualFile toDir, @NotNull @NonNls String newName)
216     throws IOException {
217     final VirtualFile newChild = toDir.createChildData(requestor, newName);
218     // [jeka] TODO: to be discussed if the copy should have the same timestamp as the original
219     //OutputStream out = newChild.getOutputStream(requestor, -1, file.getActualTimeStamp());
220     newChild.setBinaryContent(file.contentsToByteArray());
221     return newChild;
222   }
223
224   @NotNull
225   public static InputStream byteStreamSkippingBOM(@NotNull byte[] buf, @NotNull VirtualFile file) throws IOException {
226     @SuppressWarnings("IOResourceOpenedButNotSafelyClosed") BufferExposingByteArrayInputStream stream = new BufferExposingByteArrayInputStream(buf);
227     return inputStreamSkippingBOM(stream, file);
228   }
229
230   @NotNull
231   public static InputStream inputStreamSkippingBOM(@NotNull InputStream stream, @NotNull VirtualFile file) throws IOException {
232     return CharsetToolkit.inputStreamSkippingBOM(stream);
233   }
234
235   @NotNull
236   public static OutputStream outputStreamAddingBOM(@NotNull OutputStream stream, @NotNull VirtualFile file) throws IOException {
237     byte[] bom = file.getBOM();
238     if (bom != null) {
239       stream.write(bom);
240     }
241     return stream;
242   }
243
244   public static boolean iterateChildrenRecursively(@NotNull final VirtualFile root,
245                                                    @Nullable final VirtualFileFilter filter,
246                                                    @NotNull final ContentIterator iterator) {
247     final VirtualFileVisitor.Result result = visitChildrenRecursively(root, new VirtualFileVisitor() {
248       @NotNull
249       @Override
250       public Result visitFileEx(@NotNull VirtualFile file) {
251         if (filter != null && !filter.accept(file)) return SKIP_CHILDREN;
252         if (!iterator.processFile(file)) return skipTo(root);
253         return CONTINUE;
254       }
255     });
256     return !Comparing.equal(result.skipToParent, root);
257   }
258
259   @SuppressWarnings({"UnsafeVfsRecursion", "Duplicates"})
260   @NotNull
261   public static VirtualFileVisitor.Result visitChildrenRecursively(@NotNull VirtualFile file,
262                                                                    @NotNull VirtualFileVisitor<?> visitor) throws VisitorException {
263     boolean pushed = false;
264     try {
265       final boolean visited = visitor.allowVisitFile(file);
266       if (visited) {
267         VirtualFileVisitor.Result result = visitor.visitFileEx(file);
268         if (result.skipChildren) return result;
269       }
270
271       Iterable<VirtualFile> childrenIterable = null;
272       VirtualFile[] children = null;
273
274       try {
275         if (file.isValid() && visitor.allowVisitChildren(file) && !visitor.depthLimitReached()) {
276           childrenIterable = visitor.getChildrenIterable(file);
277           if (childrenIterable == null) {
278             children = file.getChildren();
279           }
280         }
281       }
282       catch (InvalidVirtualFileAccessException e) {
283         LOG.info("Ignoring: " + e.getMessage());
284         return VirtualFileVisitor.CONTINUE;
285       }
286
287       if (childrenIterable != null) {
288         visitor.saveValue();
289         pushed = true;
290         for (VirtualFile child : childrenIterable) {
291           VirtualFileVisitor.Result result = visitChildrenRecursively(child, visitor);
292           if (result.skipToParent != null && !Comparing.equal(result.skipToParent, child)) return result;
293         }
294       }
295       else if (children != null && children.length != 0) {
296         visitor.saveValue();
297         pushed = true;
298         for (VirtualFile child : children) {
299           VirtualFileVisitor.Result result = visitChildrenRecursively(child, visitor);
300           if (result.skipToParent != null && !Comparing.equal(result.skipToParent, child)) return result;
301         }
302       }
303
304       if (visited) {
305         visitor.afterChildrenVisited(file);
306       }
307
308       return VirtualFileVisitor.CONTINUE;
309     }
310     finally {
311       visitor.restoreValue(pushed);
312     }
313   }
314
315   public static <E extends Exception> VirtualFileVisitor.Result visitChildrenRecursively(@NotNull VirtualFile file,
316                                                                                          @NotNull VirtualFileVisitor visitor,
317                                                                                          @NotNull Class<E> eClass) throws E {
318     try {
319       return visitChildrenRecursively(file, visitor);
320     }
321     catch (VisitorException e) {
322       final Throwable cause = e.getCause();
323       if (eClass.isInstance(cause)) {
324         throw eClass.cast(cause);
325       }
326       throw e;
327     }
328   }
329
330   /**
331    * Returns {@code true} if given virtual file represents broken symbolic link (which points to non-existent file).
332    */
333   public static boolean isBrokenLink(@NotNull VirtualFile file) {
334     return file.is(VFileProperty.SYMLINK) && file.getCanonicalPath() == null;
335   }
336
337   /**
338    * Returns {@code true} if given virtual file represents broken or recursive symbolic link.
339    */
340   public static boolean isInvalidLink(@NotNull VirtualFile link) {
341     final VirtualFile target = link.getCanonicalFile();
342     return target == null || target.equals(link) || isAncestor(target, link, true);
343   }
344
345   @NotNull
346   public static String loadText(@NotNull VirtualFile file) throws IOException {
347     return loadText(file, (int)file.getLength());
348   }
349
350   @NotNull
351   public static String loadText(@NotNull VirtualFile file, int length) throws IOException {
352     InputStreamReader reader = new InputStreamReader(file.getInputStream(), file.getCharset());
353     try {
354       return StringFactory.createShared(FileUtil.loadText(reader, length));
355     }
356     finally {
357       reader.close();
358     }
359   }
360
361   @NotNull
362   public static VirtualFile[] toVirtualFileArray(@NotNull Collection<? extends VirtualFile> files) {
363     int size = files.size();
364     if (size == 0) return VirtualFile.EMPTY_ARRAY;
365     //noinspection SSBasedInspection
366     return files.toArray(new VirtualFile[size]);
367   }
368
369   @NotNull
370   public static String urlToPath(@NonNls @Nullable String url) {
371     if (url == null) return "";
372     return VirtualFileManager.extractPath(url);
373   }
374
375   @NotNull
376   public static File virtualToIoFile(@NotNull VirtualFile file) {
377     return new File(PathUtil.toPresentableUrl(file.getUrl()));
378   }
379
380   @NotNull
381   public static String pathToUrl(@NonNls @NotNull String path) {
382     return VirtualFileManager.constructUrl(URLUtil.FILE_PROTOCOL, path);
383   }
384
385   public static List<File> virtualToIoFiles(@NotNull Collection<VirtualFile> scope) {
386     return ContainerUtil.map2List(scope, new Function<VirtualFile, File>() {
387       @Override
388       public File fun(VirtualFile file) {
389         return virtualToIoFile(file);
390       }
391     });
392   }
393
394   @NotNull
395   public static String toIdeaUrl(@NotNull String url) {
396     return toIdeaUrl(url, true);
397   }
398
399   @NotNull
400   public static String toIdeaUrl(@NotNull String url, boolean removeLocalhostPrefix) {
401     int index = url.indexOf(":/");
402     if (index < 0 || (index + 2) >= url.length()) {
403       return url;
404     }
405
406     if (url.charAt(index + 2) != '/') {
407       String prefix = url.substring(0, index);
408       String suffix = url.substring(index + 2);
409
410       if (SystemInfoRt.isWindows) {
411         return prefix + URLUtil.SCHEME_SEPARATOR + suffix;
412       }
413       else if (removeLocalhostPrefix && prefix.equals(URLUtil.FILE_PROTOCOL) && suffix.startsWith(LOCALHOST_URI_PATH_PREFIX)) {
414         // sometimes (e.g. in Google Chrome for Mac) local file url is prefixed with 'localhost' so we need to remove it
415         return prefix + ":///" + suffix.substring(LOCALHOST_URI_PATH_PREFIX.length());
416       }
417       else {
418         return prefix + ":///" + suffix;
419       }
420     }
421     else if (SystemInfoRt.isWindows && (index + 3) < url.length() && url.charAt(index + 3) == '/' && url.regionMatches(0, StandardFileSystems.FILE_PROTOCOL_PREFIX, 0, StandardFileSystems.FILE_PROTOCOL_PREFIX.length())) {
422       // file:///C:/test/file.js -> file://C:/test/file.js
423       for (int i = index + 4; i < url.length(); i++) {
424         char c = url.charAt(i);
425         if (c == '/') {
426           break;
427         }
428         else if (c == ':') {
429           return StandardFileSystems.FILE_PROTOCOL_PREFIX + url.substring(index + 4);
430         }
431       }
432       return url;
433     }
434     return url;
435   }
436
437   @NotNull
438   public static String fixURLforIDEA(@NotNull String url) {
439     // removeLocalhostPrefix - false due to backward compatibility reasons
440     return toIdeaUrl(url, false);
441   }
442
443   @NotNull
444   public static String convertFromUrl(@NotNull URL url) {
445     String protocol = url.getProtocol();
446     String path = url.getPath();
447     if (protocol.equals(URLUtil.JAR_PROTOCOL)) {
448       if (StringUtil.startsWithConcatenation(path, URLUtil.FILE_PROTOCOL, PROTOCOL_DELIMITER)) {
449         try {
450           URL subURL = new URL(path);
451           path = subURL.getPath();
452         }
453         catch (MalformedURLException e) {
454           throw new RuntimeException(VfsBundle.message("url.parse.unhandled.exception"), e);
455         }
456       }
457       else {
458         throw new RuntimeException(new IOException(VfsBundle.message("url.parse.error", url.toExternalForm())));
459       }
460     }
461     if (SystemInfo.isWindows || SystemInfo.isOS2) {
462       while (!path.isEmpty() && path.charAt(0) == '/') {
463         path = path.substring(1, path.length());
464       }
465     }
466
467     path = URLUtil.unescapePercentSequences(path);
468     return protocol + "://" + path;
469   }
470
471   /**
472    * Converts VsfUrl info {@link URL}.
473    *
474    * @param vfsUrl VFS url (as constructed by {@link VirtualFile#getUrl()}
475    * @return converted URL or null if error has occurred.
476    */
477   @Nullable
478   public static URL convertToURL(@NotNull String vfsUrl) {
479     if (vfsUrl.startsWith(StandardFileSystems.JAR_PROTOCOL_PREFIX)) {
480       try {
481         return new URL("jar:file:///" + vfsUrl.substring(StandardFileSystems.JAR_PROTOCOL_PREFIX.length()));
482       }
483       catch (MalformedURLException e) {
484         return null;
485       }
486     }
487
488     // [stathik] for supporting mail URLs in Plugin Manager
489     if (vfsUrl.startsWith(MAILTO)) {
490       try {
491         return new URL (vfsUrl);
492       }
493       catch (MalformedURLException e) {
494         return null;
495       }
496     }
497
498     String[] split = vfsUrl.split("://");
499
500     if (split.length != 2) {
501       LOG.debug("Malformed VFS URL: " + vfsUrl);
502       return null;
503     }
504
505     String protocol = split[0];
506     String path = split[1];
507
508     try {
509       if (protocol.equals(StandardFileSystems.FILE_PROTOCOL)) {
510         return new URL(StandardFileSystems.FILE_PROTOCOL, "", path);
511       }
512       else {
513         return UrlClassLoader.internProtocol(new URL(vfsUrl));
514       }
515     }
516     catch (MalformedURLException e) {
517       LOG.debug("MalformedURLException occurred:" + e.getMessage());
518       return null;
519     }
520   }
521
522   @NotNull
523   public static String fixIDEAUrl(@NotNull String ideaUrl ) {
524     final String ideaProtocolMarker = "://";
525     int idx = ideaUrl.indexOf(ideaProtocolMarker);
526     if( idx >= 0 ) {
527       String s = ideaUrl.substring(0, idx);
528
529       if (s.equals(StandardFileSystems.JAR_PROTOCOL)) {
530         //noinspection HardCodedStringLiteral
531         s = "jar:file";
532       }
533       final String urlWithoutProtocol = ideaUrl.substring(idx + ideaProtocolMarker.length());
534       ideaUrl = s + ":" + (urlWithoutProtocol.startsWith("/") ? "" : "/") + urlWithoutProtocol;
535     }
536
537     return ideaUrl;
538   }
539
540   @SuppressWarnings({"HardCodedStringLiteral"})
541   @Nullable
542   public static VirtualFile findRelativeFile(@NotNull String uri, @Nullable VirtualFile base) {
543     if (base != null) {
544       if (!base.isValid()){
545         LOG.error("Invalid file name: " + base.getName() + ", url: " + uri);
546       }
547     }
548
549     uri = uri.replace('\\', '/');
550
551     if (uri.startsWith("file:///")) {
552       uri = uri.substring("file:///".length());
553       if (!SystemInfo.isWindows) uri = "/" + uri;
554     }
555     else if (uri.startsWith("file:/")) {
556       uri = uri.substring("file:/".length());
557       if (!SystemInfo.isWindows) uri = "/" + uri;
558     }
559     else if (uri.startsWith("file:")) {
560       uri = uri.substring("file:".length());
561     }
562
563     VirtualFile file = null;
564
565     if (uri.startsWith("jar:file:/")) {
566       uri = uri.substring("jar:file:/".length());
567       if (!SystemInfo.isWindows) uri = "/" + uri;
568       file = VirtualFileManager.getInstance().findFileByUrl(StandardFileSystems.JAR_PROTOCOL_PREFIX + uri);
569     }
570     else {
571       if (!SystemInfo.isWindows && StringUtil.startsWithChar(uri, '/')) {
572         file = StandardFileSystems.local().findFileByPath(uri);
573       }
574       else if (SystemInfo.isWindows && uri.length() >= 2 && Character.isLetter(uri.charAt(0)) && uri.charAt(1) == ':') {
575         file = StandardFileSystems.local().findFileByPath(uri);
576       }
577     }
578
579     if (file == null && uri.contains(URLUtil.JAR_SEPARATOR)) {
580       file = StandardFileSystems.jar().findFileByPath(uri);
581       if (file == null && base == null) {
582         file = VirtualFileManager.getInstance().findFileByUrl(uri);
583       }
584     }
585
586     if (file == null) {
587       if (base != null && !base.isDirectory()) {
588         base = base.getParent();
589       }
590       if (base == null) {
591         file = StandardFileSystems.local().findFileByPath(uri);
592         if (file == null) {
593           file = VirtualFileManager.getInstance().findFileByUrl(uri);
594         }
595       }
596       else {
597         file = VirtualFileManager.getInstance().findFileByUrl(base.getUrl() + "/" + uri);
598       }
599     }
600
601     return file;
602   }
603
604   public static boolean processFilesRecursively(@NotNull VirtualFile root, @NotNull Processor<VirtualFile> processor) {
605     if (!processor.process(root)) return false;
606
607     if (root.isDirectory()) {
608       final LinkedList<VirtualFile[]> queue = new LinkedList<VirtualFile[]>();
609
610       queue.add(root.getChildren());
611
612       do {
613         final VirtualFile[] files = queue.removeFirst();
614
615         for (VirtualFile file : files) {
616           if (!processor.process(file)) return false;
617           if (file.isDirectory()) {
618             queue.add(file.getChildren());
619           }
620         }
621       } while (!queue.isEmpty());
622     }
623
624     return true;
625   }
626
627   /**
628    * Gets the common ancestor for passed files, or null if the files do not have common ancestors.
629    *
630    * @param file1 fist file
631    * @param file2 second file
632    * @return common ancestor for the passed files. Returns <code>null</code> if
633    *         the files do not have common ancestor
634    */
635   @Nullable
636   public static VirtualFile getCommonAncestor(@NotNull VirtualFile file1, @NotNull VirtualFile file2) {
637     if (!file1.getFileSystem().equals(file2.getFileSystem())) {
638       return null;
639     }
640
641     VirtualFile[] path1 = getPathComponents(file1);
642     VirtualFile[] path2 = getPathComponents(file2);
643
644     int lastEqualIdx = -1;
645     for (int i = 0; i < path1.length && i < path2.length; i++) {
646       if (path1[i].equals(path2[i])) {
647         lastEqualIdx = i;
648       }
649       else {
650         break;
651       }
652     }
653     return lastEqualIdx == -1 ? null : path1[lastEqualIdx];
654   }
655
656   /**
657    * Gets an array of files representing paths from root to the passed file.
658    *
659    * @param file the file
660    * @return virtual files which represents paths from root to the passed file
661    */
662   @NotNull
663   static VirtualFile[] getPathComponents(@NotNull VirtualFile file) {
664     ArrayList<VirtualFile> componentsList = new ArrayList<VirtualFile>();
665     while (file != null) {
666       componentsList.add(file);
667       file = file.getParent();
668     }
669     int size = componentsList.size();
670     VirtualFile[] components = new VirtualFile[size];
671     for (int i = 0; i < size; i++) {
672       components[i] = componentsList.get(size - i - 1);
673     }
674     return components;
675   }
676
677   /**
678    * this collection will keep only distinct files/folders, e.g. C:\foo\bar will be removed when C:\foo is added
679    */
680   public static class DistinctVFilesRootsCollection extends DistinctRootsCollection<VirtualFile> {
681     public DistinctVFilesRootsCollection() {
682     }
683
684     public DistinctVFilesRootsCollection(Collection<VirtualFile> virtualFiles) {
685       super(virtualFiles);
686     }
687
688     public DistinctVFilesRootsCollection(VirtualFile[] collection) {
689       super(collection);
690     }
691
692     @Override
693     protected boolean isAncestor(@NotNull VirtualFile ancestor, @NotNull VirtualFile virtualFile) {
694       return VfsUtilCore.isAncestor(ancestor, virtualFile, false);
695     }
696   }
697
698   public static void processFilesRecursively(@NotNull VirtualFile root,
699                                              @NotNull Processor<VirtualFile> processor,
700                                              @NotNull Convertor<VirtualFile, Boolean> directoryFilter) {
701     if (!processor.process(root)) return;
702
703     if (root.isDirectory() && directoryFilter.convert(root)) {
704       final LinkedList<VirtualFile[]> queue = new LinkedList<VirtualFile[]>();
705
706       queue.add(root.getChildren());
707
708       do {
709         final VirtualFile[] files = queue.removeFirst();
710
711         for (VirtualFile file : files) {
712           if (!processor.process(file)) return;
713           if (file.isDirectory() && directoryFilter.convert(file)) {
714             queue.add(file.getChildren());
715           }
716         }
717       } while (!queue.isEmpty());
718     }
719   }
720 }