package org.cdlib.xtf.util; /** * Copyright (c) 2004, Regents of the University of California * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * - Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * - Neither the name of the University of California nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ import java.io.*; //////////////////////////////////////////////////////////////////////////////// /** The <code>Path</code> class provides a number of utilities that makes * working with file system paths easier. This class is effectively a * "package" in that all its members are static, and do not rely on * instance variables. <br><br> */ public class Path { ////////////////////////////////////////////////////////////////////////////// /** Normalize the specified file system path. <br><br> * * This function performs a number of "cleanup" operations to create * a standardized (or normalized) path. These operations include: <br><br> * * - Stripping any leading or trailing spaces from the passed in path. * <br><br> * * - Converts DOS/Windows paths (with backslash characters) into UNIX * standard format (with forward slash characters.) <br><br> * * - Removes any double slash characters that may have been created * when two partial path strings were concatenated. <br><br> * * - Converts drive letters to uppercase (in Windows) * * - Removes any occurrences of "./" * * - Removes any occurrences of "xxx/../" * * - Finally, if the resulting path is not an empty string, this function * guarantees that the path ends in a slash character, in anticipation * of a filename being tacked on to the end. <br><br> * * @param path The path to normalize. <br><br> * * @return * A normalized version of the original path string passed. */ public final static String normalizePath(String path) { // Create a buffer in which we can normalize the Path. StringBuffer trimPath = new StringBuffer(); // Remove any leading or trailing whitespace from the Path. trimPath.append(path.trim()); // Determine the length of the Path. int len = trimPath.length(); int lastSlash = -1; int lastLastSlash = -1; for (int i = 0; i < len; i++) { assert len == trimPath.length(); // Normalize Path to use forward instead of back slashes. if (trimPath.charAt(i) == '\\') trimPath.setCharAt(i, '/'); if (i < len - 1 && trimPath.charAt(i + 1) == '\\') trimPath.setCharAt(i + 1, '/'); // Remove any double slashes created by concatenating partial // normalized paths together. // if (i < len - 1 && trimPath.charAt(i) == '/' && trimPath.charAt(i + 1) == '/') { trimPath.deleteCharAt(i); len--; i--; } // Remove ./ if (i < len - 1 && i == lastSlash + 1 && trimPath.charAt(i) == '.' && trimPath.charAt(i + 1) == '/') { trimPath.delete(i, i + 2); len -= 2; } // Remove xxx/../ if (i < len - 2 && i == lastSlash + 1 && lastLastSlash >= 0 && trimPath.charAt(lastLastSlash+1) != '.' && trimPath.charAt(i) == '.' && trimPath.charAt(i + 1) == '.' && trimPath.charAt(i + 2) == '/') { trimPath.delete(lastLastSlash, i + 2); len -= (i + 2 - lastLastSlash); i = lastLastSlash; lastSlash = trimPath.lastIndexOf("/", lastLastSlash - 1); } if (trimPath.charAt(i) == '/') { lastLastSlash = lastSlash; lastSlash = i; } } assert len == trimPath.length(); // If the normalized Path is empty, return an empty string. if (len == 0) return ""; // Change drive letters to upper-case. if (len >= 3 && trimPath.charAt(1) == ':' && trimPath.charAt(2) == '/') trimPath.setCharAt(0, Character.toUpperCase(trimPath.charAt(0))); // If the Path did not end in a forward slash, add one. if (trimPath.charAt(len - 1) != '/') trimPath.append("/"); // Return the resulting normalized Path. return trimPath.toString(); } // private normalizePath() ////////////////////////////////////////////////////////////////////////////// /** Normalize a file name. <br><br> * * This function performs a number of "cleanup" operations to create * a standardized (or normalized) file name. <br><br> * * @param path The file name (optionally preceeded by a path) * to normalize. <br><br> * * @return * A normalized version of the original file name string passed. * * @.notes * This function does its work by calling the * {@link Path#normalizePath(String) normalizePath() } * function to normalize the filename and path (if any), and then * simply removes the trailing slash. */ public final static String normalizeFileName(String path) { // Let the Path normalization code do the hard work. String filename = normalizePath(path); // Now if the resulting normalized Path ends in a slash, remove it. if (filename.length() > 0 && filename.charAt(filename.length() - 1) == '/') filename = filename.substring(0, filename.length() - 1); // Return the result to the caller. return filename; } // private normalizeFileName() ////////////////////////////////////////////////////////////////////////////// /** Normalize a path or file name. <br><br> * * This function performs a number of "cleanup" operations to create * a standardized (or normalized) file name. <br><br> * * @param pathOrFileName The path or file name (optionally preceeded by * a path) to normalize. <br><br> * * @return * A normalized version of the original file name string passed. * * @.notes * This function does its work by calling the * {@link Path#normalizePath(String) normalizePath() } * function to normalize the filename and path (if any). If the original * path ended with a slash, the new one will also. If not, the new one * will not. */ public final static String normalize(String pathOrFileName) { if (pathOrFileName.endsWith("/") || pathOrFileName.endsWith("\\")) return normalizePath(pathOrFileName); else return normalizeFileName(pathOrFileName); } // public normalize() ////////////////////////////////////////////////////////////////////////////// /** Create the specified file system path. <br><br> * * This function creates the specified file system path if it does not * already exist. * * @param path The file system path to create. <br><br> * * @return * <code>true</code> - The file system path was successfully created. <br> * <code>false</code> - An file system path was <b>not</b> created * due to errors. <br><br> * * @.notes * This method calls the function * {@link Path#normalizePath(String) normalizePath()} to help ensure the * successful creation of the specified path. <br><br> * * Any directories specified in the path that do not already exist are * created. Thus, this function can create paths where some or none of * the parent directories exist. <br><br> */ public final static boolean createPath(String path) { boolean ret = false; // First normalize the path name. File thePath = new File(normalizePath(path)); // Then, if the specified path exists, return early. if (thePath.exists()) return true; // If the path did not exist, make it now. ret = thePath.mkdirs(); // Tell the caller how we did making the new path. return ret; } // public createPath() ////////////////////////////////////////////////////////////////////////////// /** Remove the specified path from the file system. <br><br> * * This function removes directories from the specified path, starting with * the lowest directory and moving up until either the entire path has been * removed or a non-empty directory is encountered. <br><br> * * @param path The file system path to remove. <br><br> * * @return * <code>true</code> - Part or all of the specified path was removed. <br> * <code>false</code> - None of the specified path could be removed * (either because none of the directories in the * path were empty, or because of other errors.) * <br><br> */ public final static boolean deletePath(String path) { File f = new File(path); // Try to delete the file, then its parent directory, and so on. Eventually // File.delete() will return false when we get to a non-empty directory. // int nDeleted = 0; for (; f.delete(); f = f.getParentFile()) nDeleted++; // Let the caller know whether we managed to delete anything. return nDeleted > 0; } // public deletePath() ////////////////////////////////////////////////////////////////////////////// /** * Find the part of the long path that, when all symbolic links are fully * resolved, maps to the short path (when the short path also is fully * resolved.) */ public final static String calcPrefix(String longPath, String shortPath) throws IOException { // Find the part of the long path that, when all symlinks are fully // resolved, maps to the short path when it's fully resolved. // String normShort = normalizePath(new File(shortPath).getCanonicalPath()); while (true) { longPath = normalizePath(longPath); String normLong = normalizePath(new File(longPath).getCanonicalPath()); if (normLong.equals(normShort)) return longPath; // Strip one directory from the end of the long path, and try again. int slashPos = longPath.lastIndexOf('/', longPath.length() - 2); if (slashPos < 0) return null; longPath = longPath.substring(0, slashPos); } // while( true ) } // public calcPrefix() /** * Utility function to delete a specified directory and all its files and * subdirectories. <br><br> * * @throws IOException if we fail to delete the entire directory and all * sub-files and subdirectories. */ public static void deleteDir(File dir) throws IOException { // If the specified directory exists... if (dir.isDirectory()) { // Get it's contents. String[] children = dir.list(); // Delete the contents of the directory. for (int i = 0; i < children.length; i++) deleteDir(new File(dir, children[i])); } // if( dir.isDirectory() ) // At this point we either have a file or an empty directory, so // delete it directly. // if (dir.canRead() && !dir.delete()) { throw new IOException("Unable to delete '" + dir.toString() + "'"); } } // deleteDir() /** * Utility function to resolve a child path against a parent path. Unlike * the File(File,String) constructor, this first checks if the child * path is absolute. If it is, the parent file is completely ignored. * * @param parentDir - Directory against which to resolve the child, * <b>if</b> the child is a relative path. * @param childPath - An absolute path, or else a relative path to * resolve against <code>parentFile</code>. * @return - The resulting fully resolved path. */ public static String resolveRelOrAbs(File parentDir, String childPath) { if (parentDir == null) return normalize(childPath); return normalize(resolveRelOrAbs(parentDir.toString(), childPath)); } // resolveRelOrAbs() /** * Utility function to resolve a child path against a parent path. Unlike * the File(File,String) constructor, this first checks if the child * path is absolute. If it is, the parent file is completely ignored. * * @param parentDir - Directory against which to resolve the child, * <b>if</b> the child is a relative path. * @param childPath - An absolute path, or else a relative path to * resolve against <code>parentFile</code>. * @return - The resulting fully resolved path. */ public static String resolveRelOrAbs(String parentDir, String childPath) { childPath = normalize(childPath); if (parentDir == null) return childPath; parentDir = normalizePath(parentDir); // If the child path is absolute, just return it. if (childPath.startsWith("/")) return childPath; if (childPath.length() > 1 && childPath.charAt(1) == ':') return childPath; // Otherwise, resolve against the parent. return parentDir + childPath; } // resolveRelOrAbs() /** Copies a source file to the specified destination. Creates the * destination file if it doesn't exist; overwrites it otherwise. */ public static void copyFile(File src, File dst) throws IOException { InputStream in = new FileInputStream(src); OutputStream out = new FileOutputStream(dst); // Transfer bytes from in to out byte[] buf = new byte[(int)Math.min(src.length(), 1024 * 256)]; int len; while ((len = in.read(buf)) > 0) out.write(buf, 0, len); in.close(); out.close(); } // copyFile() // Perform a basic regression test on the Path routines. public static final Tester tester = new Tester("Path") { protected void testImpl() throws Exception { String x; x = Path.normalizePath("c:\\tmp\\foo"); assert x.equals("C:/tmp/foo/"); x = Path.normalizeFileName("xyz/./foo.txt/"); assert x.equals("xyz/foo.txt"); x = Path.normalizeFileName("./foo/bar.txt"); assert x.equals("foo/bar.txt"); x = Path.normalize("/usr/tmp/../foo/bar.txt"); assert x.equals("/usr/foo/bar.txt"); x = Path.normalize("/usr/local/tmp/../../foo/bar/"); assert x.equals("/usr/foo/bar/"); } // testImpl() }; } // class Path