001package org.unix4j.util;
002
003import java.io.File;
004import java.io.FilenameFilter;
005import java.net.URL;
006import java.util.ArrayList;
007import java.util.Arrays;
008import java.util.LinkedList;
009import java.util.List;
010import java.util.regex.Pattern;
011
012import org.unix4j.variable.Arg;
013
014/**
015 * Utility class with static methods involving files.
016 */
017public class FileUtil {
018
019        public static final String ROOT_UNIX = "/";
020
021        // absolute prefix also: \\\\ i.e. actually \\ for network drive
022        public static final String ROOT_WINDOWS = "C:\\";
023        public static final String ROOT_WINDOWS_NETWORK = "\\\\";
024
025        public static final String ROOT = isWindows() ? ROOT_WINDOWS : ROOT_UNIX;
026
027        private static boolean isWindows() {
028                return OS.Windows.equals(OS.current());
029        }
030
031        /**
032         * Returns the user's current working directory taken from the system
033         * property "user.dir".
034         * 
035         * @return the user's current working directory
036         * @see System#getProperties()
037         */
038        public static File getUserDir() {
039                return new File(System.getProperty("user.dir"));
040        }
041
042        /**
043         * Returns the specified files in a new mutable array list. This is similar
044         * to {@link Arrays#asList(Object...)} but the returned list is expandable.
045         * 
046         * @param files
047         *            the files to add to a new list
048         * @return a new array list containing the specified files
049         */
050        public static List<File> toList(File... files) {
051                final List<File> list = new ArrayList<File>(files.length + 2);
052                for (int i = 0; i < files.length; i++) {
053                        list.add(files[i]);
054                }
055                return list;
056        }
057
058        /**
059         * Returns the path of the given {@code file} relative to the given
060         * {@code root}.
061         * <p>
062         * The relative path is evaluated as follows:
063         * <ol>
064         * <li>If {@code root} is the same as {@code file}, "." is returned</li>
065         * <li>If {@code root} is the direct parent directory of {@code file}, the
066         * simple file name is returned</li>
067         * <li>If {@code root} is a non-direct parent directory of {@code file}, the
068         * relative path from {@code root} to {@code file} is returned</li>
069         * <li>If {@code root} is not parent directory of {@code file}, the relative
070         * path is the path from {@code root} to the common ancestor and then to
071         * {@code file}</li>
072         * <li>If {@code root} is not parent directory of {@code file} and the only
073         * common ancestor of the two is the root directory, the absolute path of
074         * {@code file} is returned</li>
075         * </ol>
076         * Examples (same order as above):
077         * <ol>
078         * <li>("/home/john", "/home/john") &rarr; "."</li>
079         * <li>("/home/john", "/home/john/notes.txt") &rarr; "notes.txt"</li>
080         * <li>("/home/john", "/home/john/documents/important") &rarr;
081         * "./documents/important"</li>
082         * <li>("/home/john", "/home/smith/public/holidays.pdf") &rarr;
083         * "../smith/public/holidays.pdf"</li>
084         * <li>("/home/john", "/var/tmp/test.out") &rarr; "/var/tmp/test.out"</li>
085         * </ol>
086         * 
087         * @param root
088         *            the root directory for the relative path
089         * @param file
090         *            the file whose path should be returned
091         * @return the path of {@code file} relative to {@code root}
092         */
093        public static String getRelativePath(File root, File file) {
094                return new RelativePathBase(root).getRelativePathFor(file);
095        }
096
097        /**
098         * Returns an absolute file for the given input file, resolving relative
099         * paths on the basis of the given {@code currentDirectory}. If the given
100         * {@code file} represents an absolute path or a variable, it is returned
101         * unchanged. If {@code currentDirectory==null},
102         * {@link File#getAbsoluteFile()} is returned.
103         * 
104         * @param currentDirectory
105         *            the current directory
106         * @param file
107         *            the file to be returned as absolute file
108         * @return the absolute path version of the given file with
109         *         {@code currentDirectory} as basis for relative paths
110         * @see File#isAbsolute()
111         * @see Arg#isVariable(String)
112         * @see File#getAbsoluteFile()
113         */
114        public static File toAbsoluteFile(File currentDirectory, File file) {
115                if (file.isAbsolute() || Arg.isVariable(file.getPath())) {
116                        return file;
117                }
118                if (currentDirectory == null) {
119                        return file.getAbsoluteFile();
120                }
121                return new File(currentDirectory, file.getPath()).getAbsoluteFile();
122        }
123
124        /**
125         * Returns all path elements of the given {@code file}. The absolute path of
126         * the file is used to evaluate the path elements.
127         * <p>
128         * For instance, a list with the 3 elements "var", "tmp", "out.txt" is
129         * returned for an input file "/var/tmp/out.txt".
130         * 
131         * @param file
132         *            the file whose path elements should be returned
133         * @return the path elements of {@code file}
134         */
135        public static List<String> getPathElements(File file) {
136                file = file.getAbsoluteFile();
137                final List<String> elements = new LinkedList<String>();
138                do {
139                        elements.add(0, file.getName());
140                        file = file.getParentFile();
141                } while (file != null);
142                elements.remove(0);
143                return elements;
144        }
145
146        /**
147         * Expands files if necessary, meaning that input files with wildcards are
148         * expanded. If the specified {@code files} list contains no wildcard, the
149         * files are simply returned; all wildcard files are expanded.
150         * 
151         * @param paths
152         *            the file paths, possibly containing wildcard parts
153         * @return the expanded files resolving wildcards
154         */
155        public static List<File> expandFiles(String... paths) {
156                return expandFiles(FileUtil.getUserDir(), paths);
157        }
158
159        /**
160         * Expands files if necessary, meaning that input files with wildcards are
161         * expanded. If the specified {@code files} list contains no wildcard, the
162         * files are simply returned; all wildcard files are expanded. The given
163         * current directory serves as basis for all relative paths.
164         * 
165         * @param currentDirectory
166         *            the basis for all relative paths
167         * @param paths
168         *            the file paths, possibly containing wildcard parts
169         * @return the expanded files resolving wildcards
170         */
171        public static List<File> expandFiles(File currentDirectory, String... paths) {
172                final List<File> expanded = new ArrayList<File>(paths.length);
173                for (final String path : paths) {
174                        addFileExpanded(currentDirectory, new File(path), expanded);
175                }
176                return expanded;
177        }
178
179        private static void addFileExpanded(File currentDirectory, File file,
180                        List<File> expandedFiles) {
181                if (!file.isAbsolute()) {
182                        file = new File(currentDirectory, file.getPath());
183                }
184                if (isWildcardFileName(file.getPath())) {
185                        final List<String> parts = new LinkedList<String>();
186                        File f = file;
187                        File p;
188                        do {
189                                parts.add(0, f.getName());
190                                p = f;
191                                f = f.getParentFile();
192                        } while (f != null);
193                        if (p.isDirectory()) {
194                                // we pass p (the first directory) as starting directory
195                                parts.remove(0);
196                        } else if (p.getPath().endsWith("\\")) {
197                                // must be a drive, such as \\mydrive\bla with parts {"",
198                                // "mydrive", "bla"}
199                                parts.remove(0);
200                        }
201
202                        // descend again until first wildcard part is found
203                        while (!parts.isEmpty() && !isWildcardFileName(parts.get(0))) {
204                                p = new File(p, parts.remove(0));
205                        }
206                        if (!p.isDirectory()) {
207                                // what now? throw exception? trace error?
208                                throw new IllegalArgumentException("file not found: " + file
209                                                + " [root=" + p + ", currentDirectory="
210                                                + currentDirectory + "]");
211                        }
212                        listFiles(p, parts, expandedFiles);
213                } else {
214                        if (file.exists()) {
215                                expandedFiles.add(file);
216                        } else {
217                                // try file as relative path
218                                final File relFile = new File(currentDirectory, file.getPath());
219                                if (relFile.exists()) {
220                                        expandedFiles.add(relFile);
221                                } else {
222                                        // what now? throw exception? trace error?
223                                        throw new IllegalArgumentException("file not found: "
224                                                        + file + " [currentDirectory=" + currentDirectory
225                                                        + "]");
226                                }
227                        }
228                }
229        }
230
231        private static void listFiles(File file, List<String> parts, List<File> dest) {
232                final String part = parts.remove(0);
233                final FilenameFilter filter = getFileNameFilter(part);
234                for (final File f : file.listFiles(filter)) {
235                        if (parts.isEmpty()) {
236                                dest.add(f);
237                        } else {
238                                if (f.isDirectory()) {
239                                        listFiles(f, parts, dest);
240                                }
241                        }
242                }
243                parts.add(0, part);
244        }
245
246        /**
247         * Returns a file name filter for the specified name. The name should be
248         * either a simple file name (not a path) or a wildcard expression such as
249         * "*" or "*.java".
250         * <p>
251         * The wildcards "*" and "?" are supported. "*" stands for any character
252         * repeated 0 to many times, "?" for exactly one arbitrary character. Both
253         * characters can be escaped with a preceding backslash character \ (Unix
254         * and MAC) or % character (Windows).
255         * 
256         * @param name
257         *            the name or pattern without path
258         * @return a file name filter matching either the name or the pattern
259         */
260        public static FilenameFilter getFileNameFilter(String name) {
261                if (isWildcardFileName(name)) {
262                        if (isWindows()) {
263                                // escape char is %
264                                name = name.replace("%%", "%_");
265                                name = name.replace("%.", "%/").replace(".", "\\.")
266                                                .replace("%/", "\\.");
267                                name = name.replace("%*", "%/").replace("*", ".*")
268                                                .replace("%/", "\\*");
269                                name = name.replace("%?", "%/").replace("?", ".")
270                                                .replace("%/", "\\?");
271                                name = name.replace("%_", "%");
272                        } else {
273                                // escape char is \
274                                name = name.replace("\\\\", "\\_");
275                                name = name.replace("\\.", "\\/").replace(".", "\\.")
276                                                .replace("\\/", "\\.");
277                                name = name.replace("\\*", "\\/").replace("*", ".*")
278                                                .replace("\\/", "\\*");
279                                name = name.replace("\\?", "\\/").replace("?", ".")
280                                                .replace("\\/", "\\?");
281                                name = name.replace("\\_", "\\\\");
282                        }
283                        final Pattern pattern = Pattern.compile(name);
284                        return new FilenameFilter() {
285                                @Override
286                                public boolean accept(File dir, String name) {
287                                        return pattern.matcher(name).matches();
288                                }
289                        };
290                } else {
291                        final String fileName = name;
292                        return new FilenameFilter() {
293                                @Override
294                                public boolean accept(File dir, String name) {
295                                        return fileName.equals(name);
296                                }
297                        };
298                }
299        }
300
301        /**
302         * Returns true if the given name or path contains unescaped wildcard
303         * characters. The characters "*" and "?" are considered wildcard chars if
304         * they are not escaped with a preceding backslash character \ (Unix and
305         * MAC) or % character (Windows).
306         * 
307         * @param name
308         *            the name or path
309         * @return true if the name contains unescaped wildcard characters
310         */
311        public static boolean isWildcardFileName(String name) {
312                if (name.contains("*") || name.contains("?")) {
313                        final String unescaped;
314                        if (isWindows()) {
315                                unescaped = name.replace("%%", "%_").replace("%*", "%_")
316                                                .replace("%?", "%_");
317                        } else {
318                                unescaped = name.replace("\\\\", "\\_").replace("\\*", "\\_")
319                                                .replace("\\?", "\\_");
320                        }
321                        return unescaped.contains("*") || unescaped.contains("?");
322                }
323                return false;
324        }
325
326        /**
327         * This method returns the output directory of a given class.
328         * 
329         * Example: Given a class: com.abc.def.MyClass Which is outputted to a
330         * directory: /home/ben/myproject/target/com/abc/dev/MyClass.class This
331         * method will return: /home/ben/myproject/target
332         * 
333         * @param clazz
334         * @return A file representing the output directory containing the class and
335         *         parent packages
336         */
337
338        public static File getOutputDirectoryGivenClass(final Class<?> clazz) {
339                File parentDir = getDirectoryOfClassFile(clazz);
340                final int packageDepth = clazz.getName().split("\\.").length;
341                for (int i = 0; i < (packageDepth - 1); i++) {
342                        parentDir = parentDir.getParentFile();
343                }
344                return parentDir;
345        }
346
347        /**
348         * This method returns the parent directory of a given class.
349         * 
350         * Example: Given a class: com.abc.def.MyClass Which is outputted to a
351         * directory: /home/ben/myproject/target/com/abc/def/MyClass.class This
352         * method will return: /home/ben/myproject/target/com/abc/def
353         * 
354         * @param clazz
355         * @return A file representing the output directory containing the class and
356         *         parent packages
357         */
358        public static File getDirectoryOfClassFile(Class<?> clazz) {
359                final String resource = "/" + clazz.getName().replace(".", "/")
360                                + ".class";
361                final URL classFileURL = clazz.getResource(resource);
362                final File classFile = new File(classFileURL.getFile());
363                return classFile.getParentFile();
364        }
365
366        /**
367         * This method returns the parent directory of a given class.
368         * 
369         * Example: Given a class: com.abc.def.MyClass Which is outputted to a
370         * directory: /home/ben/myproject/target/com/abc/def/MyClass.class This
371         * method will return: /home/ben/myproject/target/com/abc/def
372         * 
373         * @param className
374         *            fully qualified class name
375         * @return A file representing the output directory containing the class and
376         *         parent packages
377         */
378        public static File getDirectoryOfClassFile(final String className) {
379                final String resource = "/" + className.replace(".", "/") + ".class";
380                final URL classFileURL = FileUtil.class.getResource(resource);
381                final File classFile = new File(classFileURL.getFile());
382                return classFile.getParentFile();
383        }
384
385        // no instances
386        private FileUtil() {
387                super();
388        }
389}