JAL-4420 Allow preprocessing of arguments if they are all simply filenames (or URLs...
[jalview.git] / src / jalview / util / FileUtils.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.util;
22
23 import java.io.File;
24 import java.io.IOException;
25 import java.net.MalformedURLException;
26 import java.net.URL;
27 import java.nio.file.FileSystems;
28 import java.nio.file.FileVisitOption;
29 import java.nio.file.FileVisitResult;
30 import java.nio.file.Files;
31 import java.nio.file.Path;
32 import java.nio.file.PathMatcher;
33 import java.nio.file.Paths;
34 import java.nio.file.SimpleFileVisitor;
35 import java.nio.file.attribute.BasicFileAttributes;
36 import java.util.ArrayList;
37 import java.util.Collections;
38 import java.util.EnumSet;
39 import java.util.List;
40 import java.util.stream.Collectors;
41
42 import jalview.bin.Console;
43
44 public class FileUtils
45 {
46   /*
47    * Given string glob pattern (see
48    * https://docs.oracle.com/javase/7/docs/api/java/nio/file/FileSystem.html#getPathMatcher(java.lang.String)
49    * ) return a List of Files that match the pattern.
50    * Note this is a java style glob, not necessarily a bash-style glob, though there are sufficient similarities. 
51    */
52   public static List<File> getFilesFromGlob(String pattern)
53   {
54     return getFilesFromGlob(pattern, true);
55   }
56
57   public static List<File> getFilesFromGlob(String pattern,
58           boolean allowSingleFilenameThatDoesNotExist)
59   {
60     pattern = substituteHomeDir(pattern);
61     String relativePattern = pattern.startsWith(File.separator) ? null
62             : pattern;
63     List<File> files = new ArrayList<>();
64     /*
65      * For efficiency of the Files.walkFileTree(), let's find the longest path that doesn't need globbing.
66      * We look for the first glob character * { ? and then look for the last File.separator before that.
67      * Then we can reset the path to look at and shorten the globbing pattern.
68      * Relative paths can be used in pattern, which work from the pwd (though these are converted into
69      * full paths in the match). 
70      */
71     int firstGlobChar = -1;
72     boolean foundGlobChar = false;
73     for (char c : new char[] { '*', '{', '?' })
74     {
75       if (pattern.indexOf(c) > -1
76               && (pattern.indexOf(c) < firstGlobChar || !foundGlobChar))
77       {
78         firstGlobChar = pattern.indexOf(c);
79         foundGlobChar = true;
80       }
81     }
82     int lastFS = pattern.lastIndexOf(File.separatorChar, firstGlobChar);
83     if (foundGlobChar)
84     {
85       String pS = pattern.substring(0, lastFS + 1);
86       String rest = pattern.substring(lastFS + 1);
87       if ("".equals(pS))
88       {
89         pS = ".";
90       }
91       Path parentDir = Paths.get(pS);
92       if (parentDir.toFile().exists())
93       {
94         try
95         {
96           String glob = "glob:" + parentDir.toString() + File.separator
97                   + rest;
98           PathMatcher pm = FileSystems.getDefault().getPathMatcher(glob);
99           int maxDepth = rest.contains("**") ? 1028
100                   : (int) (rest.chars()
101                           .filter(ch -> ch == File.separatorChar).count())
102                           + 1;
103
104           Files.walkFileTree(parentDir,
105                   EnumSet.of(FileVisitOption.FOLLOW_LINKS), maxDepth,
106                   new SimpleFileVisitor<Path>()
107                   {
108                     @Override
109                     public FileVisitResult visitFile(Path path,
110                             BasicFileAttributes attrs) throws IOException
111                     {
112                       if (pm.matches(path))
113                       {
114                         files.add(path.toFile());
115                       }
116                       return FileVisitResult.CONTINUE;
117                     }
118
119                     @Override
120                     public FileVisitResult visitFileFailed(Path file,
121                             IOException exc) throws IOException
122                     {
123                       return FileVisitResult.CONTINUE;
124                     }
125                   });
126         } catch (IOException e)
127         {
128           e.printStackTrace();
129         }
130       }
131     }
132     else
133     {
134       // no wildcards
135       File f = new File(pattern);
136       if (allowSingleFilenameThatDoesNotExist || f.exists())
137       {
138         files.add(f);
139       }
140     }
141     Collections.sort(files);
142
143     return files;
144   }
145
146   public static List<String> getFilenamesFromGlob(String pattern)
147   {
148     // convert list of Files to list of File.getPath() Strings
149     return getFilesFromGlob(pattern).stream().map(f -> f.getPath())
150             .collect(Collectors.toList());
151   }
152
153   public static String substituteHomeDir(String path)
154   {
155     return path.startsWith("~" + File.separator)
156             ? System.getProperty("user.home") + path.substring(1)
157             : path;
158   }
159
160   /*
161    * This method returns the basename of File file
162    */
163   public static String getBasename(File file)
164   {
165     return getBasenameOrExtension(file, false);
166   }
167
168   /*
169    * This method returns the extension of File file.
170    */
171   public static String getExtension(File file)
172   {
173     return getBasenameOrExtension(file, true);
174   }
175
176   public static String getBasenameOrExtension(File file, boolean extension)
177   {
178     if (file == null)
179       return null;
180
181     String value = null;
182     String filename = file.getName();
183     int lastDot = filename.lastIndexOf('.');
184     if (lastDot > 0) // don't truncate if starts with '.'
185     {
186       value = extension ? filename.substring(lastDot + 1)
187               : filename.substring(0, lastDot);
188     }
189     else
190     {
191       value = extension ? "" : filename;
192     }
193     return value;
194   }
195
196   /*
197    * This method returns the dirname of the first --append or --open value. 
198    * Used primarily for substitutions in output filenames.
199    */
200   public static String getDirname(File file)
201   {
202     if (file == null)
203       return null;
204
205     String dirname = null;
206     File p = file.getParentFile();
207     if (p == null)
208     {
209       p = new File(".");
210     }
211     File d = new File(substituteHomeDir(p.getPath()));
212     dirname = d.getPath();
213     return dirname;
214   }
215
216   public static String convertWildcardsToPath(String value, String wildcard,
217           String dirname, String basename)
218   {
219     if (value == null)
220     {
221       return null;
222     }
223     StringBuilder path = new StringBuilder();
224     int lastFileSeparatorIndex = value.lastIndexOf(File.separatorChar);
225     int wildcardBeforeIndex = value.indexOf(wildcard);
226     if (lastFileSeparatorIndex > wildcard.length() - 1
227             && wildcardBeforeIndex < lastFileSeparatorIndex)
228     {
229       path.append(value.substring(0, wildcardBeforeIndex));
230       path.append(dirname);
231       path.append(value.substring(wildcardBeforeIndex + wildcard.length(),
232               lastFileSeparatorIndex + 1));
233     }
234     else
235     {
236       path.append(value.substring(0, lastFileSeparatorIndex + 1));
237     }
238     int wildcardAfterIndex = value.indexOf(wildcard,
239             lastFileSeparatorIndex);
240     if (wildcardAfterIndex > lastFileSeparatorIndex)
241     {
242       path.append(value.substring(lastFileSeparatorIndex + 1,
243               wildcardAfterIndex));
244       path.append(basename);
245       path.append(value.substring(wildcardAfterIndex + wildcard.length()));
246     }
247     else
248     {
249       path.append(value.substring(lastFileSeparatorIndex + 1));
250     }
251     return path.toString();
252   }
253
254   public static File getParentDir(File file)
255   {
256     if (file == null)
257     {
258       return null;
259     }
260     File parentDir = file.getAbsoluteFile().getParentFile();
261     return parentDir;
262   }
263
264   public static boolean checkParentDir(File file, boolean mkdirs)
265   {
266     if (file == null)
267     {
268       return false;
269     }
270     File parentDir = getParentDir(file);
271     if (parentDir.exists())
272     {
273       // already exists, nothing to do so nothing to worry about!
274       return true;
275     }
276
277     if (!mkdirs)
278     {
279       return false;
280     }
281
282     Path path = file.toPath();
283     for (int i = 0; i < path.getNameCount(); i++)
284     {
285       Path p = path.getName(i);
286       if ("..".equals(p.toString()))
287       {
288         Console.warn("Cautiously not running mkdirs on " + file.toString()
289                 + " because the path to be made contains '..'");
290         return false;
291       }
292     }
293
294     return parentDir.mkdirs();
295   }
296
297   /**
298    * get a guessed file extension from a String only
299    * 
300    * @param String
301    *          filename
302    * @return String extension
303    */
304   public static String getExtension(String filename)
305   {
306     return getBaseOrExtension(filename, true);
307   }
308
309   /**
310    * getBase returns everything in a path/URI up to (and including) an extension
311    * dot. Note this is not the same as getBasename() since getBasename() only
312    * gives the filename base, not the path too. If no extension dot is found
313    * (i.e. a dot in character position 2 or more of the filename (after the last
314    * slash) then the whole path is considered the base.
315    * 
316    * @param filename
317    * @return String base
318    */
319   public static String getBase(String filename)
320   {
321     return getBaseOrExtension(filename, false);
322   }
323
324   public static String getBaseOrExtension(String filename0,
325           boolean extension)
326   {
327     if (filename0 == null)
328     {
329       return null;
330     }
331     String filename = filename0;
332     boolean isUrl = false;
333     if (HttpUtils.startsWithHttpOrHttps(filename))
334     {
335       try
336       {
337         URL url = new URL(filename);
338         filename = url.getPath();
339         isUrl = true;
340       } catch (MalformedURLException e)
341       {
342         // continue to treat as a filename
343       }
344     }
345     int dot = filename.lastIndexOf('.');
346     int slash = filename.lastIndexOf('/');
347     if (!File.separator.equals("/") && !isUrl)
348     {
349       slash = filename.lastIndexOf(File.separator);
350     }
351     // only the dot of the filename (not dots in path) and not if it's a .hidden
352     // file
353     boolean hasExtension = dot > slash + 1;
354     if (extension)
355     {
356       return hasExtension ? filename.substring(dot + 1) : null;
357     }
358     else
359     {
360       dot = filename0.lastIndexOf('.');
361       return hasExtension ? filename0.substring(0, dot + 1) : filename0;
362     }
363   }
364 }