Merge branch 'Jalview-JS/develop' into merge_js_develop
[jalview.git] / src / jalview / util / FileUtils.java
1 package jalview.util;
2
3 import java.io.File;
4 import java.io.IOException;
5 import java.nio.file.DirectoryStream;
6 import java.nio.file.FileSystems;
7 import java.nio.file.Files;
8 import java.nio.file.Path;
9 import java.nio.file.PathMatcher;
10 import java.nio.file.Paths;
11 import java.util.ArrayList;
12 import java.util.Arrays;
13 import java.util.List;
14
15 /**
16  * Miscellaneous file-related functions
17  */
18 public final class FileUtils
19 {
20
21   /**
22    * Answers the executable file for the given command, or null if not found or
23    * not executable. The path to the executable is the command name prefixed by
24    * the given folder path, optionally with .exe appended.
25    * 
26    * @param cmd
27    *          command short name, for example hmmbuild
28    * @param binaryPath
29    *          parent folder for the executable
30    * @return
31    */
32   public static File getExecutable(String cmd, String binaryPath)
33   {
34     File file = new File(binaryPath, cmd);
35     if (!file.canExecute())
36     {
37       file = new File(binaryPath, cmd + ".exe");
38       {
39         if (!file.canExecute())
40         {
41           file = null;
42         }
43       }
44     }
45     return file;
46   }
47
48   /**
49    * Answers the path to the folder containing the given executable file, by
50    * searching the PATH environment variable. Answers null if no such executable
51    * can be found.
52    * 
53    * @param cmd
54    * @return
55    */
56   public static String getPathTo(String cmd)
57   {
58     String paths = System.getenv("PATH");
59     // backslash is to escape regular expression argument
60     for (String path : paths.split("\\" + File.pathSeparator))
61     {
62       if (getExecutable(cmd, path) != null)
63       {
64         return path;
65       }
66     }
67     return null;
68   }
69
70   /**
71    * A convenience method to create a temporary file that is deleted on exit of
72    * the JVM
73    * 
74    * @param prefix
75    * @param suffix
76    * @return
77    * @throws IOException
78    */
79   public static File createTempFile(String prefix, String suffix)
80           throws IOException
81   {
82     File f = File.createTempFile(prefix, suffix);
83     f.deleteOnExit();
84     return f;
85   }
86
87   /**
88    * Answers a (possibly empty) list of file paths found by searching below
89    * <code>from</code> that match the supplied regular expression
90    * <code>pattern</code>. Results may include <code>from</code> itself if it
91    * matches. If an exception occurs it is written to syserr and any results up to
92    * that point are returned. Note that the regular expression match applies to
93    * the whole path of any matched file.
94    * <p>
95    * WARNING: because the whole directory tree below <code>from</code> is
96    * searched, this method may be slow if used for a high level directory, or may
97    * exit prematurely if security or other exceptions occur.
98    * 
99    * <pre>
100    * Example: 
101    *   findMatchingPaths(Paths.get("C:/Program Files"), ".*&#47chimera.exe$")
102    * </pre>
103    * 
104    * @param from
105    * @param pattern
106    * 
107    * @return
108    * @see https://stackoverflow.com/questions/794381/how-to-find-files-that-match-a-wildcard-string-in-java/31685610#comment62441832_31685610
109    */
110   public static List<String> findMatchingPaths(Path from, String pattern)
111   {
112     List<String> matches = new ArrayList<>();
113     PathMatcher pathMatcher = FileSystems.getDefault()
114             .getPathMatcher("regex:" + pattern);
115     try
116     {
117       Files.walk(from).filter(pathMatcher::matches)
118               .forEach(m -> matches.add(m.toString()));
119     } catch (IOException e)
120     {
121       System.err.println(
122               "Error searching for " + pattern + " : " + e.toString());
123     }
124
125     return matches;
126   }
127
128   /**
129    * Answers a (possibly empty) list of paths to files below the given root path,
130    * that match the given pattern. The pattern should be a '/' delimited set of
131    * glob patterns, each of which is used to match child file names (not full
132    * paths). Note that 'directory spanning' glob patterns (**) are <em>not</em>
133    * supported by this method.
134    * <p>
135    * For example
136    * 
137    * <pre>
138    *   findMatches("C:\\", "Program Files*&#47Chimera*&#47bin/{chimera,chimera.exe}"
139    * </pre>
140    * 
141    * would match "C:\Program Files\Chimera 1.11\bin\chimera.exe" and "C:\Program
142    * Files (x86)\Chimera 1.10.1\bin\chimera"
143    * 
144    * @param root
145    * @param pattern
146    * @return
147    * @see https://docs.oracle.com/javase/tutorial/essential/io/fileOps.html#glob
148    */
149   public static List<String> findMatches(String root, String pattern)
150   {
151     List<String> results = new ArrayList<>();
152     try
153     {
154       Path from = Paths.get(root);
155       findMatches(results, from, Arrays.asList(pattern.split("/")));
156     } catch (Throwable e)
157     {
158       // Paths.get can throw IllegalArgumentException
159       System.err.println(String.format("Error searching %s for %s: %s",
160               root, pattern, e.toString()));
161     }
162
163     return results;
164   }
165
166   /**
167    * A helper method that performs recursive search of file patterns and adds any
168    * 'leaf node' matches to the results list
169    * 
170    * @param results
171    * @param from
172    * @param patterns
173    */
174   protected static void findMatches(List<String> results, Path from,
175           List<String> patterns)
176   {
177     if (patterns.isEmpty())
178     {
179       /*
180        * reached end of recursion with all components matched
181        */
182       results.add(from.toString());
183       return;
184     }
185
186     String pattern = patterns.get(0);
187     try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(from,
188             pattern))
189     {
190       dirStream.forEach(p -> {
191
192         /*
193          * matched a next level file - search below it
194          * (ignore non-directory non-leaf matches)
195          */
196         List<String> subList = patterns.subList(1, patterns.size());
197         if (subList.isEmpty() || p.toFile().isDirectory())
198         {
199           findMatches(results, p, subList);
200         }
201       });
202     } catch (IOException e)
203     {
204       System.err.println(String.format("Error searching %s: %s", pattern,
205               e.toString()));
206     }
207   }
208 }