1377d19c8bfee31ab69dfb32d32a681dde9119d8
[idea/community.git] / platform / platform-api / src / com / intellij / openapi / vfs / VfsUtil.java
1 /*
2  * Copyright 2000-2017 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.application.Result;
19 import com.intellij.openapi.application.WriteAction;
20 import com.intellij.openapi.diagnostic.Logger;
21 import com.intellij.openapi.fileTypes.FileTypeManager;
22 import com.intellij.openapi.fileTypes.FileTypes;
23 import com.intellij.openapi.util.Comparing;
24 import com.intellij.openapi.util.Condition;
25 import com.intellij.openapi.util.SystemInfo;
26 import com.intellij.openapi.util.io.FileUtil;
27 import com.intellij.openapi.util.text.StringUtil;
28 import com.intellij.openapi.vfs.newvfs.NewVirtualFile;
29 import com.intellij.util.*;
30 import com.intellij.util.containers.ContainerUtil;
31 import com.intellij.util.io.URLUtil;
32 import gnu.trove.THashSet;
33 import org.jetbrains.annotations.NonNls;
34 import org.jetbrains.annotations.NotNull;
35 import org.jetbrains.annotations.Nullable;
36
37 import java.io.File;
38 import java.io.FileNotFoundException;
39 import java.io.IOException;
40 import java.io.InputStream;
41 import java.net.URI;
42 import java.net.URISyntaxException;
43 import java.net.URL;
44 import java.nio.charset.Charset;
45 import java.util.*;
46
47 public class VfsUtil extends VfsUtilCore {
48   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vfs.VfsUtil");
49
50   public static void saveText(@NotNull VirtualFile file, @NotNull String text) throws IOException {
51     Charset charset = file.getCharset();
52     file.setBinaryContent(text.getBytes(charset.name()));
53   }
54
55   /**
56    * Copies all files matching the <code>filter</code> from <code>fromDir</code> to <code>toDir</code>.
57    * Symlinks end special files are ignored.
58    *
59    * @param requestor any object to control who called this method. Note that
60    *                  it is considered to be an external change if <code>requestor</code> is <code>null</code>.
61    *                  See {@link VirtualFileEvent#getRequestor}
62    * @param fromDir   the directory to copy from
63    * @param toDir     the directory to copy to
64    * @param filter    {@link VirtualFileFilter}
65    * @throws IOException if files failed to be copied
66    */
67   public static void copyDirectory(Object requestor,
68                                    @NotNull VirtualFile fromDir,
69                                    @NotNull VirtualFile toDir,
70                                    @Nullable VirtualFileFilter filter) throws IOException {
71     @SuppressWarnings("UnsafeVfsRecursion") VirtualFile[] children = fromDir.getChildren();
72     for (VirtualFile child : children) {
73       if (!child.is(VFileProperty.SYMLINK) && !child.is(VFileProperty.SPECIAL) && (filter == null || filter.accept(child))) {
74         if (!child.isDirectory()) {
75           copyFile(requestor, child, toDir);
76         }
77         else {
78           VirtualFile newChild = toDir.findChild(child.getName());
79           if (newChild == null) {
80             newChild = toDir.createChildDirectory(requestor, child.getName());
81           }
82           copyDirectory(requestor, child, newChild, filter);
83         }
84       }
85     }
86   }
87
88   /**
89    * Makes a copy of the <code>file</code> in the <code>toDir</code> folder and returns it.
90    * Handles both files and directories.
91    *
92    * @param requestor any object to control who called this method. Note that
93    *                  it is considered to be an external change if <code>requestor</code> is <code>null</code>.
94    *                  See {@link VirtualFileEvent#getRequestor}
95    * @param file      file or directory to make a copy of
96    * @param toDir     directory to make a copy in
97    * @return a copy of the file
98    * @throws IOException if file failed to be copied
99    */
100   public static VirtualFile copy(Object requestor, @NotNull VirtualFile file, @NotNull VirtualFile toDir) throws IOException {
101     if (file.isDirectory()) {
102       VirtualFile newDir = toDir.createChildDirectory(requestor, file.getName());
103       copyDirectory(requestor, file, newDir, null);
104       return newDir;
105     }
106     else {
107       return copyFile(requestor, file, toDir);
108     }
109   }
110
111   /**
112    * Gets the array of common ancestors for passed files.
113    *
114    * @param files array of files
115    * @return array of common ancestors for passed files
116    */
117   @NotNull
118   public static VirtualFile[] getCommonAncestors(@NotNull VirtualFile[] files) {
119     // Separate files by first component in the path.
120     HashMap<VirtualFile,Set<VirtualFile>> map = new HashMap<>();
121     for (VirtualFile aFile : files) {
122       VirtualFile directory = aFile.isDirectory() ? aFile : aFile.getParent();
123       if (directory == null) return VirtualFile.EMPTY_ARRAY;
124       VirtualFile[] path = getPathComponents(directory);
125       Set<VirtualFile> filesSet;
126       final VirtualFile firstPart = path[0];
127       if (map.containsKey(firstPart)) {
128         filesSet = map.get(firstPart);
129       }
130       else {
131         filesSet = new THashSet<>();
132         map.put(firstPart, filesSet);
133       }
134       filesSet.add(directory);
135     }
136     // Find common ancestor for each set of files.
137     ArrayList<VirtualFile> ancestorsList = new ArrayList<>();
138     for (Set<VirtualFile> filesSet : map.values()) {
139       VirtualFile ancestor = null;
140       for (VirtualFile file : filesSet) {
141         if (ancestor == null) {
142           ancestor = file;
143           continue;
144         }
145         ancestor = getCommonAncestor(ancestor, file);
146         //assertTrue(ancestor != null);
147       }
148       ancestorsList.add(ancestor);
149       filesSet.clear();
150     }
151     return toVirtualFileArray(ancestorsList);
152   }
153
154   /**
155    * Gets the common ancestor for passed files, or {@code null} if the files do not have common ancestors.
156    */
157   @Nullable
158   public static VirtualFile getCommonAncestor(@NotNull Collection<? extends VirtualFile> files) {
159     VirtualFile ancestor = null;
160     for (VirtualFile file : files) {
161       if (ancestor == null) {
162         ancestor = file;
163       }
164       else {
165         ancestor = getCommonAncestor(ancestor, file);
166         if (ancestor == null) return null;
167       }
168     }
169     return ancestor;
170   }
171
172   @Nullable
173   public static VirtualFile findRelativeFile(@Nullable VirtualFile base, String ... path) {
174     VirtualFile file = base;
175
176     for (String pathElement : path) {
177       if (file == null) return null;
178       if ("..".equals(pathElement)) {
179         file = file.getParent();
180       }
181       else {
182         file = file.findChild(pathElement);
183       }
184     }
185
186     return file;
187   }
188
189   /**
190    * Searches for the file specified by given java,net.URL.
191    * Note that this method currently tested only for "file" and "jar" protocols under Unix and Windows
192    *
193    * @param url the URL to find file by
194    * @return <code>{@link VirtualFile}</code> if the file was found, <code>null</code> otherwise
195    */
196   @Nullable
197   public static VirtualFile findFileByURL(@NotNull URL url) {
198     VirtualFileManager virtualFileManager = VirtualFileManager.getInstance();
199     return findFileByURL(url, virtualFileManager);
200   }
201
202   @Nullable
203   public static VirtualFile findFileByURL(@NotNull URL url, @NotNull VirtualFileManager virtualFileManager) {
204     String vfUrl = convertFromUrl(url);
205     return virtualFileManager.findFileByUrl(vfUrl);
206   }
207
208   @Nullable
209   public static VirtualFile findFileByIoFile(@NotNull File file, boolean refreshIfNeeded) {
210     LocalFileSystem fileSystem = LocalFileSystem.getInstance();
211     VirtualFile virtualFile = fileSystem.findFileByIoFile(file);
212     if (refreshIfNeeded && (virtualFile == null || !virtualFile.isValid())) {
213       virtualFile = fileSystem.refreshAndFindFileByIoFile(file);
214     }
215     return virtualFile;
216   }
217
218   public static VirtualFile copyFileRelative(Object requestor, @NotNull VirtualFile file, @NotNull VirtualFile toDir, @NotNull String relativePath) throws IOException {
219     StringTokenizer tokenizer = new StringTokenizer(relativePath,"/");
220     VirtualFile curDir = toDir;
221
222     while (true) {
223       String token = tokenizer.nextToken();
224       if (tokenizer.hasMoreTokens()) {
225         VirtualFile childDir = curDir.findChild(token);
226         if (childDir == null) {
227           childDir = curDir.createChildDirectory(requestor, token);
228         }
229         curDir = childDir;
230       }
231       else {
232         return copyFile(requestor, file, curDir, token);
233       }
234     }
235   }
236
237   /**
238    * @return correct URL, must be used only for external communication
239    */
240   @NotNull
241   public static URI toUri(@NotNull VirtualFile file) {
242     String path = file.getPath();
243     try {
244       String protocol = file.getFileSystem().getProtocol();
245       if (file.isInLocalFileSystem()) {
246         if (SystemInfo.isWindows && path.charAt(0) != '/') {
247           path = '/' + path;
248         }
249         return new URI(protocol, "", path, null, null);
250       }
251       if (URLUtil.HTTP_PROTOCOL.equals(protocol)) {
252         return new URI(URLUtil.HTTP_PROTOCOL + URLUtil.SCHEME_SEPARATOR + path);
253       }
254       return new URI(protocol, path, null);
255     }
256     catch (URISyntaxException e) {
257       throw new IllegalArgumentException(e);
258     }
259   }
260
261   /**
262    * @return correct URL, must be used only for external communication
263    */
264   @NotNull
265   public static URI toUri(@NotNull File file) {
266     String path = file.toURI().getPath();
267     try {
268       if (SystemInfo.isWindows && path.charAt(0) != '/') {
269         path = '/' + path;
270       }
271       return new URI(StandardFileSystems.FILE_PROTOCOL, "", path, null, null);
272     }
273     catch (URISyntaxException e) {
274       throw new IllegalArgumentException(e);
275     }
276   }
277
278   /**
279    * uri - may be incorrect (escaping or missed "/" before disk name under windows), may be not fully encoded,
280    * may contains query and fragment
281    * @return correct URI, must be used only for external communication
282    */
283   @Nullable
284   public static URI toUri(@NonNls @NotNull String uri) {
285     int index = uri.indexOf("://");
286     if (index < 0) {
287       // true URI, like mailto:
288       try {
289         return new URI(uri);
290       }
291       catch (URISyntaxException e) {
292         LOG.debug(e);
293         return null;
294       }
295     }
296
297     if (SystemInfo.isWindows && uri.startsWith(LocalFileSystem.PROTOCOL_PREFIX)) {
298       int firstSlashIndex = index + "://".length();
299       if (uri.charAt(firstSlashIndex) != '/') {
300         uri = LocalFileSystem.PROTOCOL_PREFIX + '/' + uri.substring(firstSlashIndex);
301       }
302     }
303
304     try {
305       return new URI(uri);
306     }
307     catch (URISyntaxException e) {
308       LOG.debug("uri is not fully encoded", e);
309       // so, uri is not fully encoded (space)
310       try {
311         int fragmentIndex = uri.lastIndexOf('#');
312         String path = uri.substring(index + 1, fragmentIndex > 0 ? fragmentIndex : uri.length());
313         String fragment = fragmentIndex > 0 ? uri.substring(fragmentIndex + 1) : null;
314         return new URI(uri.substring(0, index), path, fragment);
315       }
316       catch (URISyntaxException e1) {
317         LOG.debug(e1);
318         return null;
319       }
320     }
321   }
322
323   /**
324    * Returns the relative path from one virtual file to another.
325    *
326    * @param src           the file from which the relative path is built.
327    * @param dst           the file to which the path is built.
328    * @param separatorChar the separator for the path components.
329    * @return the relative path, or null if the files have no common ancestor.
330    * @since 5.0.2
331    */
332   @Nullable
333   public static String getPath(@NotNull VirtualFile src, @NotNull VirtualFile dst, char separatorChar) {
334     final VirtualFile commonAncestor = getCommonAncestor(src, dst);
335     if (commonAncestor != null) {
336       StringBuilder buffer = new StringBuilder();
337       if (!Comparing.equal(src, commonAncestor)) {
338         while (!Comparing.equal(src.getParent(), commonAncestor)) {
339           buffer.append("..").append(separatorChar);
340           src = src.getParent();
341         }
342       }
343       buffer.append(getRelativePath(dst, commonAncestor, separatorChar));
344       return buffer.toString();
345     }
346
347     return null;
348   }
349
350   public static String getUrlForLibraryRoot(@NotNull File libraryRoot) {
351     String path = FileUtil.toSystemIndependentName(libraryRoot.getAbsolutePath());
352     if (FileTypeManager.getInstance().getFileTypeByFileName(libraryRoot.getName()) == FileTypes.ARCHIVE) {
353       return VirtualFileManager.constructUrl(JarFileSystem.getInstance().getProtocol(), path + JarFileSystem.JAR_SEPARATOR);
354     }
355     else {
356       return VirtualFileManager.constructUrl(LocalFileSystem.getInstance().getProtocol(), path);
357     }
358   }
359
360   public static VirtualFile createChildSequent(Object requestor, @NotNull VirtualFile dir, @NotNull String prefix, @NotNull String extension) throws IOException {
361     String dotExt = PathUtil.makeFileName("", extension);
362     String fileName = prefix + dotExt;
363     int i = 1;
364     while (dir.findChild(fileName) != null) {
365       fileName = prefix + "_" + i + dotExt;
366       i++;
367     }
368     return dir.createChildData(requestor, fileName);
369   }
370
371   @NotNull
372   public static String[] filterNames(@NotNull String[] names) {
373     int filteredCount = 0;
374     for (String string : names) {
375       if (isBadName(string)) filteredCount++;
376     }
377     if (filteredCount == 0) return names;
378
379     String[] result = ArrayUtil.newStringArray(names.length - filteredCount);
380     int count = 0;
381     for (String string : names) {
382       if (isBadName(string)) continue;
383       result[count++] = string;
384     }
385
386     return result;
387   }
388
389   public static boolean isBadName(String name) {
390     return name == null || name.isEmpty() || "/".equals(name) || "\\".equals(name);
391   }
392
393   public static VirtualFile createDirectories(@NotNull final String directoryPath) throws IOException {
394     return new WriteAction<VirtualFile>() {
395       @Override
396       protected void run(@NotNull Result<VirtualFile> result) throws Throwable {
397         VirtualFile res = createDirectoryIfMissing(directoryPath);
398         result.setResult(res);
399       }
400     }.execute().throwException().getResultObject();
401   }
402
403   public static VirtualFile createDirectoryIfMissing(@Nullable VirtualFile parent, String relativePath) throws IOException {
404     if (parent == null) {
405       return createDirectoryIfMissing(relativePath);
406     }
407
408     for (String each : StringUtil.split(relativePath, "/")) {
409       VirtualFile child = parent.findChild(each);
410       if (child == null) {
411         child = parent.createChildDirectory(LocalFileSystem.getInstance(), each);
412       }
413       parent = child;
414     }
415     return parent;
416   }
417
418   @Nullable
419   public static VirtualFile createDirectoryIfMissing(@NotNull String directoryPath) throws IOException {
420     String path = FileUtil.toSystemIndependentName(directoryPath);
421     final VirtualFile file = LocalFileSystem.getInstance().refreshAndFindFileByPath(path);
422     if (file == null) {
423       int pos = path.lastIndexOf('/');
424       if (pos < 0) return null;
425       VirtualFile parent = createDirectoryIfMissing(path.substring(0, pos));
426       if (parent == null) return null;
427       final String dirName = path.substring(pos + 1);
428       VirtualFile child = parent.findChild(dirName);
429       if (child != null && child.isDirectory()) return child;
430       return parent.createChildDirectory(LocalFileSystem.getInstance(), dirName);
431     }
432     return file;
433   }
434
435   /**
436    * Returns all files in some virtual files recursively
437    * @param root virtual file to get descendants
438    * @return descendants
439    */
440   @NotNull
441   public static List<VirtualFile> collectChildrenRecursively(@NotNull VirtualFile root) {
442     List<VirtualFile> result = new ArrayList<>();
443     visitChildrenRecursively(root, new VirtualFileVisitor(VirtualFileVisitor.NO_FOLLOW_SYMLINKS) {
444       @Override
445       public boolean visitFile(@NotNull VirtualFile file) {
446         result.add(file);
447         return true;
448       }
449     });
450     return result;
451   }
452
453   public static void processFileRecursivelyWithoutIgnored(@NotNull VirtualFile root, @NotNull Processor<VirtualFile> processor) {
454     FileTypeManager ftm = FileTypeManager.getInstance();
455     visitChildrenRecursively(root, new VirtualFileVisitor() {
456       @NotNull
457       @Override
458       public Result visitFileEx(@NotNull VirtualFile file) {
459         if (!processor.process(file)) return skipTo(root);
460         return file.isDirectory() && ftm.isFileIgnored(file) ? SKIP_CHILDREN : CONTINUE;
461       }
462     });
463   }
464
465   @NotNull
466   public static String getReadableUrl(@NotNull final VirtualFile file) {
467     String url = null;
468     if (file.isInLocalFileSystem()) {
469       url = file.getPresentableUrl();
470     }
471     if (url == null) {
472       url = file.getUrl();
473     }
474     return url;
475   }
476
477   @Nullable
478   public static VirtualFile getUserHomeDir() {
479     final String path = SystemProperties.getUserHome();
480     return LocalFileSystem.getInstance().findFileByPath(FileUtil.toSystemIndependentName(path));
481   }
482
483   @NotNull
484   public static VirtualFile[] getChildren(@NotNull VirtualFile dir) {
485     VirtualFile[] children = dir.getChildren();
486     return children == null ? VirtualFile.EMPTY_ARRAY : children;
487   }
488
489   @NotNull
490   public static List<VirtualFile> getChildren(@NotNull VirtualFile dir, @NotNull VirtualFileFilter filter) {
491     List<VirtualFile> result = null;
492     for (VirtualFile child : dir.getChildren()) {
493       if (filter.accept(child)) {
494         if (result == null) result = ContainerUtil.newSmartList();
495         result.add(child);
496       }
497     }
498     return result != null ? result : ContainerUtil.emptyList();
499   }
500
501   /**
502    * @param url Url for virtual file
503    * @return url for parent directory of virtual file
504    */
505   @Nullable
506   public static String getParentDir(@Nullable final String url) {
507     if (url == null) {
508       return null;
509     }
510     final int index = url.lastIndexOf(VfsUtilCore.VFS_SEPARATOR_CHAR);
511     return index < 0 ? null : url.substring(0, index);
512   }
513
514   /**
515    * @param urlOrPath Url for virtual file
516    * @return file name
517    */
518   @Nullable
519   public static String extractFileName(@Nullable final String urlOrPath) {
520     if (urlOrPath == null) {
521       return null;
522     }
523     final int index = urlOrPath.lastIndexOf(VfsUtilCore.VFS_SEPARATOR_CHAR);
524     return index < 0 ? null : urlOrPath.substring(index+1);
525   }
526
527   @NotNull
528   public static List<VirtualFile> markDirty(boolean recursive, boolean reloadChildren, @NotNull VirtualFile... files) {
529     List<VirtualFile> list = ContainerUtil.filter(files, Condition.NOT_NULL);
530     if (list.isEmpty()) {
531       return Collections.emptyList();
532     }
533
534     for (VirtualFile file : list) {
535       if (reloadChildren && file.isValid()) {
536         file.getChildren();
537       }
538
539       if (file instanceof NewVirtualFile) {
540         if (recursive) {
541           ((NewVirtualFile)file).markDirtyRecursively();
542         }
543         else {
544           ((NewVirtualFile)file).markDirty();
545         }
546       }
547     }
548     return list;
549   }
550
551   /**
552    * Refreshes the VFS information of the given files from the local file system.
553    * <p/>
554    * This refresh is performed without help of the FileWatcher,
555    * which means that all given files will be refreshed even if the FileWatcher didn't report any changes in them.
556    * This method is slower, but more reliable, and should be preferred
557    * when it is essential to make sure all the given VirtualFiles are actually refreshed from disk.
558    * <p/>
559    * NB: when invoking synchronous refresh from a thread other than the event dispatch thread, the current thread must
560    * NOT be in a read action.
561    *
562    * @see VirtualFile#refresh(boolean, boolean)
563    */
564   public static void markDirtyAndRefresh(boolean async, boolean recursive, boolean reloadChildren, @NotNull VirtualFile... files) {
565     List<VirtualFile> list = markDirty(recursive, reloadChildren, files);
566     if (list.isEmpty()) return;
567     LocalFileSystem.getInstance().refreshFiles(list, async, recursive, null);
568   }
569
570   @NotNull
571   public static VirtualFile getRootFile(@NotNull VirtualFile file) {
572     while (true) {
573       VirtualFile parent = file.getParent();
574       if (parent == null) break;
575       file = parent;
576     }
577     return file;
578   }
579
580   //<editor-fold desc="Deprecated stuff.">
581   /** @deprecated to be removed in IDEA 2018 */
582   public static void copyFromResource(@NotNull VirtualFile file, @NonNls @NotNull String resourceUrl) throws IOException {
583     InputStream out = VfsUtil.class.getResourceAsStream(resourceUrl);
584     if (out == null) {
585       throw new FileNotFoundException(resourceUrl);
586     }
587     try {
588       byte[] bytes = FileUtil.adaptiveLoadBytes(out);
589       file.setBinaryContent(bytes);
590     } finally {
591       out.close();
592     }
593   }
594
595   /** @deprecated use {@link VfsUtilCore#toIdeaUrl(String)} to be removed in IDEA 2019 */
596   @SuppressWarnings("MethodOverridesStaticMethodOfSuperclass")
597   public static String toIdeaUrl(@NotNull String url) {
598     return toIdeaUrl(url, true);
599   }
600
601   /** @deprecated to be removed in IDEA 2018 */
602   public static <T> T processInputStream(@NotNull final VirtualFile file, @NotNull Function<InputStream, T> function) {
603     InputStream stream = null;
604     try {
605       stream = file.getInputStream();
606       return function.fun(stream);
607     }
608     catch (IOException e) {
609       LOG.error(e);
610     }
611     finally {
612       try {
613         if (stream != null) {
614           stream.close();
615         }
616       }
617       catch (IOException e) {
618         LOG.error(e);
619       }
620     }
621     return null;
622   }
623   //</editor-fold>
624 }