package org.apache.maven.shared.utils; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import java.io.File; import java.util.StringTokenizer; import javax.annotation.Nonnull; import javax.annotation.Nullable; /** * Path tool contains static methods to assist in determining path-related * information such as relative paths. * <p/> * This class originally got developed at Apache Anakia and later maintained * in maven-utils of Apache Maven-1. * Some external fixes by Apache Committers have been applied later. */ public class PathTool { /** * Determines the relative path of a filename from a base directory. * This method is useful in building relative links within pages of * a web site. It provides similar functionality to Anakia's * <code>$relativePath</code> context variable. The arguments to * this method may contain either forward or backward slashes as * file separators. The relative path returned is formed using * forward slashes as it is expected this path is to be used as a * link in a web page (again mimicking Anakia's behavior). * <p/> * This method is thread-safe. * <br/> * <pre> * PathTool.getRelativePath( null, null ) = "" * PathTool.getRelativePath( null, "/usr/local/java/bin" ) = "" * PathTool.getRelativePath( "/usr/local/", null ) = "" * PathTool.getRelativePath( "/usr/local/", "/usr/local/java/bin" ) = ".." * PathTool.getRelativePath( "/usr/local/", "/usr/local/java/bin/java.sh" ) = "../.." * PathTool.getRelativePath( "/usr/local/java/bin/java.sh", "/usr/local/" ) = "" * </pre> * * @param basedir The base directory. * @param filename The filename that is relative to the base * directory. * @return The relative path of the filename from the base * directory. This value is not terminated with a forward slash. * A zero-length string is returned if: the filename is not relative to * the base directory, <code>basedir</code> is null or zero-length, * or <code>filename</code> is null or zero-length. */ public static String getRelativePath( @Nullable String basedir, @Nullable String filename ) { basedir = uppercaseDrive( basedir ); filename = uppercaseDrive( filename ); /* * Verify the arguments and make sure the filename is relative * to the base directory. */ if ( basedir == null || basedir.length() == 0 || filename == null || filename.length() == 0 || !filename.startsWith( basedir ) ) { return ""; } /* * Normalize the arguments. First, determine the file separator * that is being used, then strip that off the end of both the * base directory and filename. */ String separator = determineSeparator( filename ); basedir = StringUtils.chompLast( basedir, separator ); filename = StringUtils.chompLast( filename, separator ); /* * Remove the base directory from the filename to end up with a * relative filename (relative to the base directory). This * filename is then used to determine the relative path. */ String relativeFilename = filename.substring( basedir.length() ); return determineRelativePath( relativeFilename, separator ); } /** * This method can calculate the relative path between two pathes on a file system. * <br/> * <pre> * PathTool.getRelativeFilePath( null, null ) = "" * PathTool.getRelativeFilePath( null, "/usr/local/java/bin" ) = "" * PathTool.getRelativeFilePath( "/usr/local", null ) = "" * PathTool.getRelativeFilePath( "/usr/local", "/usr/local/java/bin" ) = "java/bin" * PathTool.getRelativeFilePath( "/usr/local", "/usr/local/java/bin/" ) = "java/bin" * PathTool.getRelativeFilePath( "/usr/local/java/bin", "/usr/local/" ) = "../.." * PathTool.getRelativeFilePath( "/usr/local/", "/usr/local/java/bin/java.sh" ) = "java/bin/java.sh" * PathTool.getRelativeFilePath( "/usr/local/java/bin/java.sh", "/usr/local/" ) = "../../.." * PathTool.getRelativeFilePath( "/usr/local/", "/bin" ) = "../../bin" * PathTool.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 old path * @param newPath new path * @return a relative file path from <code>oldPath</code>. */ public static String getRelativeFilePath( final String oldPath, final String newPath ) { if ( StringUtils.isEmpty( oldPath ) || StringUtils.isEmpty( 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 doesnt, 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 // ---------------------------------------------------------------------- /** * Determines the relative path of a filename. For each separator * within the filename (except the leading if present), append the * "../" string to the return value. * * @param filename The filename to parse. * @param separator The separator used within the filename. * @return The relative path of the filename. This value is not * terminated with a forward slash. A zero-length string is * returned if: the filename is zero-length. */ @Nonnull private static String determineRelativePath( @Nonnull String filename, @Nonnull String separator ) { if ( filename.length() == 0 ) { return ""; } /* * Count the slashes in the relative filename, but exclude the * leading slash. If the path has no slashes, then the filename * is relative to the current directory. */ int slashCount = StringUtils.countMatches( filename, separator ) - 1; if ( slashCount <= 0 ) { return "."; } /* * The relative filename contains one or more slashes indicating * that the file is within one or more directories. Thus, each * slash represents a "../" in the relative path. */ StringBuilder sb = new StringBuilder(); for ( int i = 0; i < slashCount; i++ ) { sb.append( "../" ); } /* * Finally, return the relative path but strip the trailing * slash to mimic Anakia's behavior. */ return StringUtils.chop( sb.toString() ); } /** * Helper method to determine the file separator (forward or * backward slash) used in a filename. The slash that occurs more * often is returned as the separator. * * @param filename The filename parsed to determine the file * separator. * @return The file separator used within <code>filename</code>. * This value is either a forward or backward slash. */ private static String determineSeparator( String filename ) { int forwardCount = StringUtils.countMatches( filename, "/" ); int backwardCount = StringUtils.countMatches( filename, "\\" ); return forwardCount >= backwardCount ? "/" : "\\"; } /** * Cygwin prefers lowercase drive letters, but other parts of maven use uppercase * * @param path old path * @return String */ static String uppercaseDrive( @Nullable String path ) { if ( path == null ) { return null; } if ( path.length() >= 2 && path.charAt( 1 ) == ':' ) { path = Character.toUpperCase( path.charAt( 0 ) ) + path.substring( 1 ); } return path; } @Nonnull private static String buildRelativePath( @Nonnull String toPath, @Nonnull String fromPath, final char separatorChar ) { // use tokeniser to traverse paths and for lazy checking StringTokenizer toTokeniser = new StringTokenizer( toPath, String.valueOf( separatorChar ) ); StringTokenizer fromTokeniser = new StringTokenizer( fromPath, String.valueOf( separatorChar ) ); int count = 0; // walk along the to path looking for divergence from the from path while ( toTokeniser.hasMoreTokens() && fromTokeniser.hasMoreTokens() ) { if ( separatorChar == '\\' ) { if ( !fromTokeniser.nextToken().equalsIgnoreCase( toTokeniser.nextToken() ) ) { break; } } else { if ( !fromTokeniser.nextToken().equals( toTokeniser.nextToken() ) ) { break; } } count++; } // reinitialise the tokenisers to count positions to retrieve the // gobbled token toTokeniser = new StringTokenizer( toPath, String.valueOf( separatorChar ) ); fromTokeniser = new StringTokenizer( fromPath, String.valueOf( separatorChar ) ); while ( count-- > 0 ) { fromTokeniser.nextToken(); toTokeniser.nextToken(); } StringBuilder relativePath = new StringBuilder(); // add back refs for the rest of from location. while ( fromTokeniser.hasMoreTokens() ) { fromTokeniser.nextToken(); relativePath.append( ".." ); if ( fromTokeniser.hasMoreTokens() ) { relativePath.append( separatorChar ); } } if ( relativePath.length() != 0 && toTokeniser.hasMoreTokens() ) { relativePath.append( separatorChar ); } // add fwd fills for whatevers left of newPath. while ( toTokeniser.hasMoreTokens() ) { relativePath.append( toTokeniser.nextToken() ); if ( toTokeniser.hasMoreTokens() ) { relativePath.append( separatorChar ); } } return relativePath.toString(); } }