PathUtils.java

/*
 * SmartSprites Project
 *
 * Copyright (C) 2007-2009, Stanisław Osiński.
 * 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 SmartSprites Project nor the names of its contributors
 *   may  be used  to endorse  or  promote  products derived   from  this  software
 *   without specific prior written permission.
 *
 * - We kindly request that you include in the end-user documentation provided with
 *   the redistribution and/or in the software itself an acknowledgement equivalent
 *   to  the  following: "This product includes software developed by the SmartSprites
 *   Project."
 *
 * 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.
 */
package org.carrot2.util;

import com.google.common.base.Strings;

import java.io.File;
import java.util.StringTokenizer;

/**
 * This class defines utilities methods helping to determine path-related information such as relative paths.
 * <p>
 * The original code comes from <b>org.codehaus.plexus.util.PathTool</b>.
 *
 * @author Ibrahim Chaehoi
 * @author <a href="mailto:pete-apache-dev@kazmier.com">Pete Kazmier</a>
 * @author <a href="mailto:vmassol@apache.org">Vincent Massol</a>
 * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
 */
public class PathUtils {

    /**
     * Instantiates a new path utils.
     */
    private PathUtils() {
        // Prevent Instantiation
    }

    /**
     * This method can calculate the relative path between two pathes on a file system.
     *
     * <pre>
     * PathUtils.getRelativeFilePath( null, null )                                   = ""
     * PathUtils.getRelativeFilePath( null, "/usr/local/java/bin" )                  = ""
     * PathUtils.getRelativeFilePath( "/usr/local", null )                           = ""
     * PathUtils.getRelativeFilePath( "/usr/local", "/usr/local/java/bin" )          = "java/bin"
     * PathUtils.getRelativeFilePath( "/usr/local", "/usr/local/java/bin/" )         = "java/bin"
     * PathUtils.getRelativeFilePath( "/usr/local/java/bin", "/usr/local/" )         = "../.."
     * PathUtils.getRelativeFilePath( "/usr/local/", "/usr/local/java/bin/java.sh" ) = "java/bin/java.sh"
     * PathUtils.getRelativeFilePath( "/usr/local/java/bin/java.sh", "/usr/local/" ) = "../../.."
     * PathUtils.getRelativeFilePath( "/usr/local/", "/bin" )                        = "../../bin"
     * PathUtils.getRelativeFilePath( "/bin", "/usr/local/" )                        = "../usr/local"
     * </pre>
     *
     * Note: On Windows based system, the <code>/</code> character should be replaced by <code>\</code> character.
     *
     * @param oldPath
     *            the old path
     * @param newPath
     *            the new path
     *
     * @return a relative file path from <code>oldPath</code>.
     */
    public static final String getRelativeFilePath(final String oldPath, final String newPath) {
        if (Strings.isNullOrEmpty(oldPath) || Strings.isNullOrEmpty(newPath)) {
            return "";
        }

        // normalise the path delimiters
        String fromPath = new File(oldPath).getPath();
        String toPath = new File(newPath).getPath();

        // strip any leading slashes if its a windows path
        if (toPath.matches("^\\[a-zA-Z]:")) {
            toPath = toPath.substring(1);
        }
        if (fromPath.matches("^\\[a-zA-Z]:")) {
            fromPath = fromPath.substring(1);
        }

        // lowercase windows drive letters.
        if (fromPath.startsWith(":", 1)) {
            fromPath = Character.toLowerCase(fromPath.charAt(0)) + fromPath.substring(1);
        }
        if (toPath.startsWith(":", 1)) {
            toPath = Character.toLowerCase(toPath.charAt(0)) + toPath.substring(1);
        }

        // check for the presence of windows drives. No relative way of
        // traversing from one to the other.
        if (toPath.startsWith(":", 1) && fromPath.startsWith(":", 1)
                && !toPath.substring(0, 1).equals(fromPath.substring(0, 1))) {
            // they both have drive path element but they dont match, no relative path
            return null;
        }

        if ((toPath.startsWith(":", 1) && !fromPath.startsWith(":", 1))
                || (!toPath.startsWith(":", 1) && fromPath.startsWith(":", 1))) {
            // one has a drive path element and the other doesn't, no relative path.
            return null;
        }

        String resultPath = buildRelativePath(toPath, fromPath, File.separatorChar);

        if (newPath.endsWith(File.separator) && !resultPath.endsWith(File.separator)) {
            return resultPath + File.separator;
        }

        return resultPath;
    }

    // ----------------------------------------------------------------------
    // Private methods
    // ----------------------------------------------------------------------

    /**
     * Builds the relative path.
     *
     * @param toPath
     *            the to path
     * @param fromPath
     *            the from path
     * @param separatorChar
     *            the separator char
     *
     * @return the string
     */
    private static String buildRelativePath(String toPath, String fromPath, final char separatorChar) {
        // use tokenizer to traverse paths and for lazy checking
        StringTokenizer toTokenizer = new StringTokenizer(toPath, String.valueOf(separatorChar));
        StringTokenizer fromTokenizer = new StringTokenizer(fromPath, String.valueOf(separatorChar));

        int count = 0;

        // walk along the to path looking for divergence from the from path
        while (toTokenizer.hasMoreTokens() && fromTokenizer.hasMoreTokens()) {
            if (separatorChar == '\\') {
                if (!fromTokenizer.nextToken().equalsIgnoreCase(toTokenizer.nextToken())) {
                    break;
                }
            } else if (!fromTokenizer.nextToken().equals(toTokenizer.nextToken())) {
                break;
            }

            count++;
        }

        // Reinitialize the tokenizers to count positions to retrieve the
        // gobbled token

        toTokenizer = new StringTokenizer(toPath, String.valueOf(separatorChar));
        fromTokenizer = new StringTokenizer(fromPath, String.valueOf(separatorChar));

        while (count-- > 0) {
            fromTokenizer.nextToken();
            toTokenizer.nextToken();
        }

        StringBuilder relativePath = new StringBuilder();

        // add back refs for the rest of from location.
        while (fromTokenizer.hasMoreTokens()) {
            fromTokenizer.nextToken();

            relativePath.append("..");

            if (fromTokenizer.hasMoreTokens()) {
                relativePath.append(separatorChar);
            }
        }

        if (relativePath.length() != 0 && toTokenizer.hasMoreTokens()) {
            relativePath.append(separatorChar);
        }

        // add fwd fills for whatevers left of newPath.
        while (toTokenizer.hasMoreTokens()) {
            relativePath.append(toTokenizer.nextToken());

            if (toTokenizer.hasMoreTokens()) {
                relativePath.append(separatorChar);
            }
        }
        return relativePath.toString();
    }
}