Coverage Report - org.apache.maven.shared.utils.PathTool
 
Classes in this File Line Coverage Branch Coverage Complexity
PathTool
85%
64/75
69%
54/78
0
 
 1  
 package org.apache.maven.shared.utils;
 2  
 
 3  
 /*
 4  
  * Licensed to the Apache Software Foundation (ASF) under one
 5  
  * or more contributor license agreements.  See the NOTICE file
 6  
  * distributed with this work for additional information
 7  
  * regarding copyright ownership.  The ASF licenses this file
 8  
  * to you under the Apache License, Version 2.0 (the
 9  
  * "License"); you may not use this file except in compliance
 10  
  * with the License.  You may obtain a copy of the License at
 11  
  *
 12  
  *   http://www.apache.org/licenses/LICENSE-2.0
 13  
  *
 14  
  * Unless required by applicable law or agreed to in writing,
 15  
  * software distributed under the License is distributed on an
 16  
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 17  
  * KIND, either express or implied.  See the License for the
 18  
  * specific language governing permissions and limitations
 19  
  * under the License.
 20  
  */
 21  
 
 22  
 import java.io.File;
 23  
 import java.util.StringTokenizer;
 24  
 
 25  
 import javax.annotation.Nonnull;
 26  
 import javax.annotation.Nullable;
 27  
 
 28  
 /**
 29  
  * Path tool contains static methods to assist in determining path-related
 30  
  * information such as relative paths.
 31  
  * <p/>
 32  
  * This class originally got developed at Apache Anakia and later maintained
 33  
  * in maven-utils of Apache Maven-1.
 34  
  * Some external fixes by Apache Committers have been applied later.
 35  
  */
 36  0
 public class PathTool
 37  
 {
 38  
     /**
 39  
      * Determines the relative path of a filename from a base directory.
 40  
      * This method is useful in building relative links within pages of
 41  
      * a web site.  It provides similar functionality to Anakia's
 42  
      * <code>$relativePath</code> context variable.  The arguments to
 43  
      * this method may contain either forward or backward slashes as
 44  
      * file separators.  The relative path returned is formed using
 45  
      * forward slashes as it is expected this path is to be used as a
 46  
      * link in a web page (again mimicking Anakia's behavior).
 47  
      * <p/>
 48  
      * This method is thread-safe.
 49  
      * <br/>
 50  
      * <pre>
 51  
      * PathTool.getRelativePath( null, null )                                   = ""
 52  
      * PathTool.getRelativePath( null, "/usr/local/java/bin" )                  = ""
 53  
      * PathTool.getRelativePath( "/usr/local/", null )                          = ""
 54  
      * PathTool.getRelativePath( "/usr/local/", "/usr/local/java/bin" )         = ".."
 55  
      * PathTool.getRelativePath( "/usr/local/", "/usr/local/java/bin/java.sh" ) = "../.."
 56  
      * PathTool.getRelativePath( "/usr/local/java/bin/java.sh", "/usr/local/" ) = ""
 57  
      * </pre>
 58  
      *
 59  
      * @param basedir  The base directory.
 60  
      * @param filename The filename that is relative to the base
 61  
      *                 directory.
 62  
      * @return The relative path of the filename from the base
 63  
      *         directory.  This value is not terminated with a forward slash.
 64  
      *         A zero-length string is returned if: the filename is not relative to
 65  
      *         the base directory, <code>basedir</code> is null or zero-length,
 66  
      *         or <code>filename</code> is null or zero-length.
 67  
      */
 68  
     public static String getRelativePath( @Nullable String basedir, @Nullable String filename )
 69  
     {
 70  6
         basedir = uppercaseDrive( basedir );
 71  6
         filename = uppercaseDrive( filename );
 72  
 
 73  
         /*
 74  
          * Verify the arguments and make sure the filename is relative
 75  
          * to the base directory.
 76  
          */
 77  6
         if ( basedir == null || basedir.length() == 0 || filename == null || filename.length() == 0
 78  
             || !filename.startsWith( basedir ) )
 79  
         {
 80  4
             return "";
 81  
         }
 82  
 
 83  
         /*
 84  
          * Normalize the arguments.  First, determine the file separator
 85  
          * that is being used, then strip that off the end of both the
 86  
          * base directory and filename.
 87  
          */
 88  2
         String separator = determineSeparator( filename );
 89  2
         basedir = StringUtils.chompLast( basedir, separator );
 90  2
         filename = StringUtils.chompLast( filename, separator );
 91  
 
 92  
         /*
 93  
          * Remove the base directory from the filename to end up with a
 94  
          * relative filename (relative to the base directory).  This
 95  
          * filename is then used to determine the relative path.
 96  
          */
 97  2
         String relativeFilename = filename.substring( basedir.length() );
 98  
 
 99  2
         return determineRelativePath( relativeFilename, separator );
 100  
     }
 101  
 
 102  
     /**
 103  
      * This method can calculate the relative path between two pathes on a file system.
 104  
      * <br/>
 105  
      * <pre>
 106  
      * PathTool.getRelativeFilePath( null, null )                                   = ""
 107  
      * PathTool.getRelativeFilePath( null, "/usr/local/java/bin" )                  = ""
 108  
      * PathTool.getRelativeFilePath( "/usr/local", null )                           = ""
 109  
      * PathTool.getRelativeFilePath( "/usr/local", "/usr/local/java/bin" )          = "java/bin"
 110  
      * PathTool.getRelativeFilePath( "/usr/local", "/usr/local/java/bin/" )         = "java/bin"
 111  
      * PathTool.getRelativeFilePath( "/usr/local/java/bin", "/usr/local/" )         = "../.."
 112  
      * PathTool.getRelativeFilePath( "/usr/local/", "/usr/local/java/bin/java.sh" ) = "java/bin/java.sh"
 113  
      * PathTool.getRelativeFilePath( "/usr/local/java/bin/java.sh", "/usr/local/" ) = "../../.."
 114  
      * PathTool.getRelativeFilePath( "/usr/local/", "/bin" )                        = "../../bin"
 115  
      * PathTool.getRelativeFilePath( "/bin", "/usr/local/" )                        = "../usr/local"
 116  
      * </pre>
 117  
      * Note: On Windows based system, the <code>/</code> character should be replaced by <code>\</code> character.
 118  
      *
 119  
      * @param oldPath old path
 120  
      * @param newPath new path
 121  
      * @return a relative file path from <code>oldPath</code>.
 122  
      */
 123  
     public static String getRelativeFilePath( final String oldPath, final String newPath )
 124  
     {
 125  10
         if ( StringUtils.isEmpty( oldPath ) || StringUtils.isEmpty( newPath ) )
 126  
         {
 127  3
             return "";
 128  
         }
 129  
 
 130  
         // normalise the path delimiters
 131  7
         String fromPath = new File( oldPath ).getPath();
 132  7
         String toPath = new File( newPath ).getPath();
 133  
 
 134  
         // strip any leading slashes if its a windows path
 135  7
         if ( toPath.matches( "^\\[a-zA-Z]:" ) )
 136  
         {
 137  0
             toPath = toPath.substring( 1 );
 138  
         }
 139  7
         if ( fromPath.matches( "^\\[a-zA-Z]:" ) )
 140  
         {
 141  0
             fromPath = fromPath.substring( 1 );
 142  
         }
 143  
 
 144  
         // lowercase windows drive letters.
 145  7
         if ( fromPath.startsWith( ":", 1 ) )
 146  
         {
 147  0
             fromPath = Character.toLowerCase( fromPath.charAt( 0 ) ) + fromPath.substring( 1 );
 148  
         }
 149  7
         if ( toPath.startsWith( ":", 1 ) )
 150  
         {
 151  0
             toPath = Character.toLowerCase( toPath.charAt( 0 ) ) + toPath.substring( 1 );
 152  
         }
 153  
 
 154  
         // check for the presence of windows drives. No relative way of
 155  
         // traversing from one to the other.
 156  7
         if ( ( toPath.startsWith( ":", 1 ) && fromPath.startsWith( ":", 1 ) )
 157  
             && ( !toPath.substring( 0, 1 ).equals( fromPath.substring( 0, 1 ) ) ) )
 158  
         {
 159  
             // they both have drive path element but they dont match, no
 160  
             // relative path
 161  0
             return null;
 162  
         }
 163  
 
 164  7
         if ( ( toPath.startsWith( ":", 1 ) && !fromPath.startsWith( ":", 1 ) )
 165  
             || ( !toPath.startsWith( ":", 1 ) && fromPath.startsWith( ":", 1 ) ) )
 166  
         {
 167  
             // one has a drive path element and the other doesnt, no relative
 168  
             // path.
 169  0
             return null;
 170  
         }
 171  
 
 172  7
         String resultPath = buildRelativePath( toPath, fromPath, File.separatorChar );
 173  
 
 174  7
         if ( newPath.endsWith( File.separator ) && !resultPath.endsWith( File.separator ) )
 175  
         {
 176  4
             return resultPath + File.separator;
 177  
         }
 178  
 
 179  3
         return resultPath;
 180  
     }
 181  
 
 182  
     // ----------------------------------------------------------------------
 183  
     // Private methods
 184  
     // ----------------------------------------------------------------------
 185  
 
 186  
     /**
 187  
      * Determines the relative path of a filename.  For each separator
 188  
      * within the filename (except the leading if present), append the
 189  
      * "../" string to the return value.
 190  
      *
 191  
      * @param filename  The filename to parse.
 192  
      * @param separator The separator used within the filename.
 193  
      * @return The relative path of the filename.  This value is not
 194  
      *         terminated with a forward slash.  A zero-length string is
 195  
      *         returned if: the filename is zero-length.
 196  
      */
 197  
     private @Nonnull static String determineRelativePath( @Nonnull String filename, @Nonnull String separator )
 198  
     {
 199  2
         if ( filename.length() == 0 )
 200  
         {
 201  0
             return "";
 202  
         }
 203  
 
 204  
         /*
 205  
         * Count the slashes in the relative filename, but exclude the
 206  
         * leading slash.  If the path has no slashes, then the filename
 207  
         * is relative to the current directory.
 208  
         */
 209  2
         int slashCount = StringUtils.countMatches( filename, separator ) - 1;
 210  2
         if ( slashCount <= 0 )
 211  
         {
 212  0
             return ".";
 213  
         }
 214  
 
 215  
         /*
 216  
          * The relative filename contains one or more slashes indicating
 217  
          * that the file is within one or more directories.  Thus, each
 218  
          * slash represents a "../" in the relative path.
 219  
          */
 220  2
         StringBuilder sb = new StringBuilder();
 221  5
         for ( int i = 0; i < slashCount; i++ )
 222  
         {
 223  3
             sb.append( "../" );
 224  
         }
 225  
 
 226  
         /*
 227  
          * Finally, return the relative path but strip the trailing
 228  
          * slash to mimic Anakia's behavior.
 229  
          */
 230  2
         return StringUtils.chop( sb.toString() );
 231  
     }
 232  
 
 233  
     /**
 234  
      * Helper method to determine the file separator (forward or
 235  
      * backward slash) used in a filename.  The slash that occurs more
 236  
      * often is returned as the separator.
 237  
      *
 238  
      * @param filename The filename parsed to determine the file
 239  
      *                 separator.
 240  
      * @return The file separator used within <code>filename</code>.
 241  
      *         This value is either a forward or backward slash.
 242  
      */
 243  
     private static String determineSeparator( String filename )
 244  
     {
 245  2
         int forwardCount = StringUtils.countMatches( filename, "/" );
 246  2
         int backwardCount = StringUtils.countMatches( filename, "\\" );
 247  
 
 248  2
         return forwardCount >= backwardCount ? "/" : "\\";
 249  
     }
 250  
 
 251  
     /**
 252  
      * Cygwin prefers lowercase drive letters, but other parts of maven use uppercase
 253  
      *
 254  
      * @param path old path
 255  
      * @return String
 256  
      */
 257  
     static String uppercaseDrive( @Nullable String path )
 258  
     {
 259  16
         if ( path == null )
 260  
         {
 261  5
             return null;
 262  
         }
 263  11
         if ( path.length() >= 2 && path.charAt( 1 ) == ':' )
 264  
         {
 265  2
             path = Character.toUpperCase( path.charAt( 0 ) ) + path.substring( 1 );
 266  
         }
 267  11
         return path;
 268  
     }
 269  
 
 270  
     private @Nonnull static String buildRelativePath( @Nonnull String toPath, @Nonnull String fromPath, final char separatorChar )
 271  
     {
 272  
         // use tokeniser to traverse paths and for lazy checking
 273  7
         StringTokenizer toTokeniser = new StringTokenizer( toPath, String.valueOf( separatorChar ) );
 274  7
         StringTokenizer fromTokeniser = new StringTokenizer( fromPath, String.valueOf( separatorChar ) );
 275  
 
 276  7
         int count = 0;
 277  
 
 278  
         // walk along the to path looking for divergence from the from path
 279  17
         while ( toTokeniser.hasMoreTokens() && fromTokeniser.hasMoreTokens() )
 280  
         {
 281  12
             if ( separatorChar == '\\' )
 282  
             {
 283  0
                 if ( !fromTokeniser.nextToken().equalsIgnoreCase( toTokeniser.nextToken() ) )
 284  
                 {
 285  0
                     break;
 286  
                 }
 287  
             }
 288  
             else
 289  
             {
 290  12
                 if ( !fromTokeniser.nextToken().equals( toTokeniser.nextToken() ) )
 291  
                 {
 292  2
                     break;
 293  
                 }
 294  
             }
 295  
 
 296  10
             count++;
 297  
         }
 298  
 
 299  
         // reinitialise the tokenisers to count positions to retrieve the
 300  
         // gobbled token
 301  
 
 302  7
         toTokeniser = new StringTokenizer( toPath, String.valueOf( separatorChar ) );
 303  7
         fromTokeniser = new StringTokenizer( fromPath, String.valueOf( separatorChar ) );
 304  
 
 305  17
         while ( count-- > 0 )
 306  
         {
 307  10
             fromTokeniser.nextToken();
 308  10
             toTokeniser.nextToken();
 309  
         }
 310  
 
 311  7
         StringBuilder relativePath = new StringBuilder();
 312  
 
 313  
         // add back refs for the rest of from location.
 314  15
         while ( fromTokeniser.hasMoreTokens() )
 315  
         {
 316  8
             fromTokeniser.nextToken();
 317  
 
 318  8
             relativePath.append( ".." );
 319  
 
 320  8
             if ( fromTokeniser.hasMoreTokens() )
 321  
             {
 322  4
                 relativePath.append( separatorChar );
 323  
             }
 324  
         }
 325  
 
 326  7
         if ( relativePath.length() != 0 && toTokeniser.hasMoreTokens() )
 327  
         {
 328  2
             relativePath.append( separatorChar );
 329  
         }
 330  
 
 331  
         // add fwd fills for whatevers left of newPath.
 332  17
         while ( toTokeniser.hasMoreTokens() )
 333  
         {
 334  10
             relativePath.append( toTokeniser.nextToken() );
 335  
 
 336  10
             if ( toTokeniser.hasMoreTokens() )
 337  
             {
 338  5
                 relativePath.append( separatorChar );
 339  
             }
 340  
         }
 341  7
         return relativePath.toString();
 342  
     }
 343  
 }