/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2008 jOpenDocument, by ILM Informatique. All rights reserved. * * The contents of this file are subject to the terms of the GNU * General Public License Version 3 only ("GPL"). * You may not use this file except in compliance with the License. * You can obtain a copy of the License at http://www.gnu.org/licenses/gpl-3.0.html * See the License for the specific language governing permissions and limitations under the License. * * When distributing the software, include this License Header Notice in each file. * */ package org.jopendocument.util; import org.jopendocument.util.StringUtils.Escaper; import org.jopendocument.util.cc.ExnTransformer; import org.jopendocument.util.cc.IClosure; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.RandomAccessFile; import java.io.Reader; import java.net.URL; import java.nio.channels.FileChannel; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; public final class FileUtils { private FileUtils() { // all static } /** * All the files (see {@link File#isFile()}) contained in the passed dir. * * @param dir the root directory to search. * @return a List of String. */ public static List<String> listR(File dir) { return listR_rec(dir, "."); } private static List<String> listR_rec(File dir, String prefix) { if (!dir.isDirectory()) return null; final List<String> res = new ArrayList<String>(); final File[] children = dir.listFiles(); for (int i = 0; i < children.length; i++) { final String newPrefix = prefix + "/" + children[i].getName(); if (children[i].isFile()) { // MAYBE add a way to restrict added files res.add(newPrefix); } else if (children[i].isDirectory()) { res.addAll(listR_rec(children[i], newPrefix)); } } return res; } public static void walk(File dir, IClosure<File> c) { walk(dir, c, RecursionType.BREADTH_FIRST); } public static void walk(File dir, IClosure<File> c, RecursionType type) { if (type == RecursionType.BREADTH_FIRST) c.executeChecked(dir); if (dir.isDirectory()) { for (final File child : dir.listFiles()) { walk(child, c, type); } } if (type == RecursionType.DEPTH_FIRST) c.executeChecked(dir); } public static final List<File> list(File root, final int depth) { return list(root, depth, null); } /** * Finds all files at the specified depth below <code>root</code>. * * @param root the base directory * @param depth the depth of the returned files. * @param ff a filter, can be <code>null</code>. * @return a list of files <code>depth</code> levels beneath <code>root</code>. */ public static final List<File> list(File root, final int depth, final FileFilter ff) { if (!root.exists()) return Collections.<File> emptyList(); if (depth == 0) { return ff.accept(root) ? Collections.singletonList(root) : Collections.<File> emptyList(); } else if (depth == 1) { final File[] listFiles = root.listFiles(ff); if (listFiles == null) throw new IllegalStateException("cannot list " + root); return Arrays.asList(listFiles); } else { final File[] childDirs = root.listFiles(DIR_FILTER); if (childDirs == null) throw new IllegalStateException("cannot list " + root); final List<File> res = new ArrayList<File>(); for (final File child : childDirs) { res.addAll(list(child, depth - 1, ff)); } return res; } } /** * Returns the relative path from one file to another in the same filesystem tree. * * @param fromDir the starting directory, eg /a/b/. * @param to the file to get to, eg /a/x/y.txt. * @return the relative path, eg "../x/y.txt". * @throws IOException if an error occurs while canonicalizing the files. * @throws IllegalArgumentException if fromDir is not directory, or the files have no common * ancestors (for example on Windows on 2 different letters). */ public static final String relative(File fromDir, File to) throws IOException { if (!fromDir.isDirectory()) throw new IllegalArgumentException(fromDir + " is not a directory"); final File fromF = fromDir.getCanonicalFile(); final File toF = to.getCanonicalFile(); final List<File> toPath = getAncestors(toF); final List<File> fromPath = getAncestors(fromF); if (!toPath.get(0).equals(fromPath.get(0))) throw new IllegalArgumentException("'" + fromF + "' and '" + toF + "' have no common ancestor"); int commonIndex = Math.min(toPath.size(), fromPath.size()) - 1; boolean found = false; while (commonIndex >= 0 && !found) { found = fromPath.get(commonIndex).equals(toPath.get(commonIndex)); if (!found) commonIndex--; } // on remonte jusqu'Ă  l'ancĂȘtre commun final List<String> complete = new ArrayList<String>(Collections.nCopies(fromPath.size() - 1 - commonIndex, "..")); if (complete.isEmpty()) complete.add("."); // puis on descend vers 'to' for (File f : toPath.subList(commonIndex + 1, toPath.size())) { complete.add(f.getName()); } return CollectionUtils.join(complete, File.separator); } // return each ancestor of f (including itself) // eg [/, /folder, /folder/dir] for /folder/dir public final static List<File> getAncestors(File f) { final List<File> path = new ArrayList<File>(); File currentF = f; while (currentF != null) { path.add(0, currentF); currentF = currentF.getParentFile(); } return path; } public final static File addSuffix(File f, String suffix) { return new File(f.getParentFile(), f.getName() + suffix); } // ** shell /** * Behave like the 'mv' unix utility, ie handle cross filesystems mv and <code>dest</code> being * a directory. * * @param f the source file. * @param dest the destination file or directory. * @return the error or <code>null</code> if there was none. */ public static String mv(File f, File dest) { final File canonF; File canonDest; try { canonF = f.getCanonicalFile(); canonDest = dest.getCanonicalFile(); } catch (IOException e) { return ExceptionUtils.getStackTrace(e); } if (canonF.equals(canonDest)) // nothing to do return null; if (canonDest.isDirectory()) canonDest = new File(canonDest, canonF.getName()); final File destF; if (canonDest.exists()) return canonDest + " exists"; else if (!canonDest.getParentFile().exists()) return "parent of " + canonDest + " does not exist"; else destF = canonDest; if (!canonF.renameTo(destF)) { try { copyDirectory(canonF, destF); if (destF.exists()) rmR(canonF); } catch (IOException e) { return ExceptionUtils.getStackTrace(e); } } return null; } public static void copyFile(File in, File out) throws IOException { final FileChannel sourceChannel = new FileInputStream(in).getChannel(); final FileChannel destinationChannel = new FileOutputStream(out).getChannel(); sourceChannel.transferTo(0, sourceChannel.size(), destinationChannel); sourceChannel.close(); destinationChannel.close(); } public static void copyDirectory(File in, File out) throws IOException { copyDirectory(in, out, Collections.<String> emptySet()); } public static final Set<String> VersionControl = CollectionUtils.createSet(".svn", "CVS"); public static void copyDirectory(File in, File out, final Set<String> toIgnore) throws IOException { if (toIgnore.contains(in.getName())) return; if (in.isDirectory()) { if (!out.exists()) { out.mkdir(); } String[] children = in.list(); for (int i = 0; i < children.length; i++) { copyDirectory(new File(in, children[i]), new File(out, children[i]), toIgnore); } } else { if (!in.getName().equals("Thumbs.db")) { copyFile(in, out); } } } /** * Delete recursively the passed directory. If a deletion fails, the method stops attempting to * delete and returns false. * * @param dir the dir to be deleted. * @return <code>true</code> if all deletions were successful. */ public static boolean rmR(File dir) { if (dir.isDirectory()) { File[] children = dir.listFiles(); for (int i = 0; i < children.length; i++) { boolean success = rmR(children[i]); if (!success) { return false; } } } // The directory is now empty so delete it return dir.delete(); } public static final File mkdir_p(File dir) throws IOException { if (!dir.exists()) { if (!dir.mkdirs()) { throw new IOException("cannot create directory " + dir); } } return dir; } // **io /** * Read a file line by line with the default encoding and returns the concatenation of these. * * @param f the file to read. * @return the content of f. * @throws IOException if a pb occur while reading. */ public static final String read(File f) throws IOException { return read(f, null); } /** * Read a file line by line and returns the concatenation of these. * * @param f the file to read. * @param charset the encoding of <code>f</code>, <code>null</code> means default encoding. * @return the content of f. * @throws IOException if a pb occur while reading. */ public static final String read(File f, String charset) throws IOException { return read(new FileInputStream(f), charset); } public static final String read(InputStream ins, String charset) throws IOException { final Reader reader; if (charset == null) reader = new InputStreamReader(ins); else reader = new InputStreamReader(ins, charset); return read(reader); } public static final String read(final Reader reader) throws IOException { final BufferedReader in = new BufferedReader(reader); String line; String res = ""; while ((line = in.readLine()) != null) { res += line + "\n"; } in.close(); return res; } /** * Read the whole content of a file. * * @param f the file to read. * @return its content. * @throws IOException if a pb occur while reading. * @throws IllegalArgumentException if f is longer than <code>Integer.MAX_VALUE</code>. */ public static final byte[] readBytes(File f) throws IOException { // no need for a Buffer since we read everything at once final InputStream in = new FileInputStream(f); if (f.length() > Integer.MAX_VALUE) throw new IllegalArgumentException("file longer than Integer.MAX_VALUE" + f.length()); final byte[] res = new byte[(int) f.length()]; in.read(res); in.close(); return res; } public static void write(String s, File f) throws IOException { write(s, f, false); } public static void write(String s, File f, boolean append) throws IOException { final BufferedWriter w = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f, append))); try { w.write(s); } finally { w.close(); } } /** * Execute the passed transformer with the lock on the passed file. * * @param <T> return type. * @param f the file to lock. * @param transf what to do on the file. * @return what <code>transf</code> returns. * @throws Exception if an error occurs. */ public static final <T> T doWithLock(final File f, ExnTransformer<RandomAccessFile, T, ?> transf) throws Exception { // don't use FileOutputStream : it truncates the file on creation RandomAccessFile out = null; try { mkdir_p(f.getParentFile()); // we need write to obtain lock out = new RandomAccessFile(f, "rw"); out.getChannel().lock(); final T res = transf.transformChecked(out); // this also release the lock out.close(); out = null; return res; } catch (final Exception e) { // if anything happens, try to close // don't use finally{close()} otherwise if it raise an exception // the original error is discarded Exception toThrow = e; if (out != null) try { out.close(); } catch (final IOException e2) { // too bad, just add the error toThrow = ExceptionUtils.createExn(IOException.class, "couldn't close: " + e2.getMessage(), e); } throw toThrow; } } private static final Map<URL, File> files = new HashMap<URL, File>(); private static final File getShortCutFile() throws IOException { return getFile(FileUtils.class.getResource("shortcut.vbs")); } // windows cannot execute a string, it demands a file public static final File getFile(final URL url) throws IOException { final File shortcutFile; final File currentFile = files.get(url); if (currentFile == null || !currentFile.exists()) { shortcutFile = File.createTempFile("windowsIsLame", ".vbs"); shortcutFile.deleteOnExit(); files.put(url, shortcutFile); final InputStream stream = url.openStream(); final FileOutputStream out = new FileOutputStream(shortcutFile); try { StreamUtils.copy(stream, out); } finally { out.close(); stream.close(); } } else shortcutFile = currentFile; return shortcutFile; } /** * Create a symbolic link from <code>link</code> to <code>target</code>. * * @param target the target of the link, eg ".". * @param link the file to create or replace, eg "l". * @return the link if the creation was successfull, <code>null</code> otherwise, eg "l.LNK". * @throws IOException if an error occurs. */ public static final File ln(final File target, final File link) throws IOException { final String os = System.getProperty("os.name"); final Process ps; final File res; if (os.startsWith("Windows")) { // using the .vbs since it doesn't depends on cygwin // and cygwin's ln is weird : // 1. needs CYGWIN=winsymlinks to create a shortcut, but even then "ln -f" doesn't work // since it tries to delete l instead of l.LNK // 2. it sets the system flag so "dir" doesn't show the shortcut (unless you add /AS) // 3. the shortcut is recognized as a symlink thanks to a special attribute that can get // lost (e.g. copying in eclipse) ps = Runtime.getRuntime().exec(new String[] { "cscript", getShortCutFile().getAbsolutePath(), link.getAbsolutePath(), target.getCanonicalPath() }); res = new File(link.getParentFile(), link.getName() + ".LNK"); } else { final String rel = FileUtils.relative(link.getAbsoluteFile().getParentFile(), target); // add -f to replace existing links // add -n so that ln -sf aDir anExistantLinkToIt succeed final String[] cmdarray = { "ln", "-sfn", rel, link.getAbsolutePath() }; ps = Runtime.getRuntime().exec(cmdarray); res = link; } try { final int exitValue = ps.waitFor(); if (exitValue == 0) return res; else throw new IOException("Abnormal exit value: " + exitValue); } catch (InterruptedException e) { throw ExceptionUtils.createExn(IOException.class, "interrupted", e); } } /** * Resolve a symbolic link or a windows shortcut. * * @param link the shortcut, e.g. shortcut.lnk. * @return the target of <code>link</code>, <code>null</code> if not found, e.g. target.txt. * @throws IOException if an error occurs. */ public static final File readlink(final File link) throws IOException { final String os = System.getProperty("os.name"); final Process ps; if (os.startsWith("Windows")) { ps = Runtime.getRuntime().exec(new String[] { "cscript", "//NoLogo", getShortCutFile().getAbsolutePath(), link.getAbsolutePath() }); } else { // add -f to canonicalize ps = Runtime.getRuntime().exec(new String[] { "readlink", "-f", link.getAbsolutePath() }); } try { final BufferedReader reader = new BufferedReader(new InputStreamReader(ps.getInputStream())); final String res = reader.readLine(); reader.close(); if (ps.waitFor() != 0 || res == null || res.length() == 0) return null; else return new File(res); } catch (InterruptedException e) { throw ExceptionUtils.createExn(IOException.class, "interrupted", e); } } /** * Tries to open the passed file as if it were graphically opened by the current user (respect * user's "open with"). If a native way to open the file can't be found, tries the passed list * of executables. * * @param f the file to open. * @param executables a list of executables to try, e.g. ["ooffice", "soffice"]. * @throws IOException if the file can't be opened. */ public static final void open(File f, String[] executables) throws IOException { try { openNative(f); } catch (IOException exn) { for (int i = 0; i < executables.length; i++) { final String executable = executables[i]; try { Runtime.getRuntime().exec(new String[] { executable, f.getCanonicalPath() }); return; } catch (IOException e) { // try the next one } } throw ExceptionUtils.createExn(IOException.class, "unable to open " + f + " with: " + Arrays.asList(executables), exn); } } /** * Open the passed file as if it were graphically opened by the current user (user's "open * with"). * * @param f the file to open. * @throws IOException if f couldn't be opened. */ private static final void openNative(File f) throws IOException { final String os = System.getProperty("os.name"); final String[] cmdarray; if (os.startsWith("Windows")) { cmdarray = new String[] { "cmd", "/c", "start", "\"\"", f.getCanonicalPath() }; } else if (os.startsWith("Mac OS")) { cmdarray = new String[] { "open", f.getCanonicalPath() }; } else if (os.startsWith("Linux")) { cmdarray = new String[] { "xdg-open", f.getCanonicalPath() }; } else { throw new IOException("unknown way to open " + f); } try { // can wait since the command return as soon as the native application is launched // (i.e. this won't wait 30s for OpenOffice) final int res = Runtime.getRuntime().exec(cmdarray).waitFor(); if (res != 0) throw new IOException("error (" + res + ") executing " + Arrays.asList(cmdarray)); } catch (InterruptedException e) { throw ExceptionUtils.createExn(IOException.class, "interrupted waiting for " + Arrays.asList(cmdarray), e); } } static final boolean gnomeRunning() { try { return Runtime.getRuntime().exec(new String[] { "pgrep", "-u", System.getProperty("user.name"), "nautilus" }).waitFor() == 0; } catch (Exception e) { return false; } } private static final Map<String, String> ext2mime; static { ext2mime = new HashMap<String, String>(); ext2mime.put(".xml", "text/xml"); ext2mime.put(".jpg", "image/jpeg"); ext2mime.put(".png", "image/png"); ext2mime.put(".tiff", "image/tiff"); } /** * Try to guess the media type of the passed file name (see <a * href="http://www.iana.org/assignments/media-types">iana</a>). * * @param fname a file name. * @return its mime type. */ public static final String findMimeType(String fname) { for (final Map.Entry<String, String> e : ext2mime.entrySet()) { if (fname.toLowerCase().endsWith(e.getKey())) return e.getValue(); } return null; } /** * Chars not valid in filenames. */ public static final Collection<Character> INVALID_CHARS; /** * An escaper suitable for producing valid filenames. */ public static final Escaper FILENAME_ESCAPER = new StringUtils.Escaper('\'', 'Q'); static { // from windows explorer FILENAME_ESCAPER.add('"', 'D').add(':', 'C').add('/', 'S').add('\\', 'A'); FILENAME_ESCAPER.add('<', 'L').add('>', 'G').add('*', 'R').add('|', 'P').add('?', 'M'); INVALID_CHARS = FILENAME_ESCAPER.getEscapedChars(); } public static final FileFilter DIR_FILTER = new FileFilter() { @Override public boolean accept(File f) { return f.isDirectory(); } }; public static final FileFilter REGULAR_FILE_FILTER = new FileFilter() { @Override public boolean accept(File f) { return f.isFile(); } }; /** * Return a filter that select regular files ending in <code>ext</code>. * * @param ext the end of the name, eg ".xml". * @return the corresponding filter. */ public static final FileFilter createEndFileFilter(final String ext) { return new FileFilter() { @Override public boolean accept(File f) { return f.isFile() && f.getName().endsWith(ext); } }; } }