Coverage Report - org.codehaus.plexus.util.PathTool
 
Classes in this File Line Coverage Branch Coverage Complexity
PathTool
74%
86/115
62%
75/120
9
 
 1  
 package org.codehaus.plexus.util;
 2  
 
 3  
 /*
 4  
  * Copyright The Codehaus Foundation.
 5  
  *
 6  
  * Licensed under the Apache License, Version 2.0 (the "License");
 7  
  * you may not use this file except in compliance with the License.
 8  
  * You may obtain a copy of the License at
 9  
  *
 10  
  *     http://www.apache.org/licenses/LICENSE-2.0
 11  
  *
 12  
  * Unless required by applicable law or agreed to in writing, software
 13  
  * distributed under the License is distributed on an "AS IS" BASIS,
 14  
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 15  
  * See the License for the specific language governing permissions and
 16  
  * limitations under the License.
 17  
  */
 18  
 
 19  
 import java.io.File;
 20  
 import java.util.StringTokenizer;
 21  
 
 22  
 /**
 23  
  * Path tool contains static methods to assist in determining path-related information such as relative paths.
 24  
  *
 25  
  * @author <a href="mailto:pete-apache-dev@kazmier.com">Pete Kazmier</a>
 26  
  * @author <a href="mailto:vmassol@apache.org">Vincent Massol</a>
 27  
  * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
 28  
  * @version $Id$
 29  
  */
 30  0
 public class PathTool
 31  
 {
 32  
     /**
 33  
      * Determines the relative path of a filename from a base directory. This method is useful in building relative
 34  
      * links within pages of a web site. It provides similar functionality to Anakia's <code>$relativePath</code>
 35  
      * context variable. The arguments to this method may contain either forward or backward slashes as file separators.
 36  
      * The relative path returned is formed using forward slashes as it is expected this path is to be used as a link in
 37  
      * a web page (again mimicking Anakia's behavior).
 38  
      * <p/>
 39  
      * This method is thread-safe. <br/>
 40  
      * 
 41  
      * <pre>
 42  
      * PathTool.getRelativePath( null, null )                                   = ""
 43  
      * PathTool.getRelativePath( null, "/usr/local/java/bin" )                  = ""
 44  
      * PathTool.getRelativePath( "/usr/local/", null )                          = ""
 45  
      * PathTool.getRelativePath( "/usr/local/", "/usr/local/java/bin" )         = ".."
 46  
      * PathTool.getRelativePath( "/usr/local/", "/usr/local/java/bin/java.sh" ) = "../.."
 47  
      * PathTool.getRelativePath( "/usr/local/java/bin/java.sh", "/usr/local/" ) = ""
 48  
      * </pre>
 49  
      *
 50  
      * @param basedir The base directory.
 51  
      * @param filename The filename that is relative to the base directory.
 52  
      * @return The relative path of the filename from the base directory. This value is not terminated with a forward
 53  
      *         slash. A zero-length string is returned if: the filename is not relative to the base directory,
 54  
      *         <code>basedir</code> is null or zero-length, or <code>filename</code> is null or zero-length.
 55  
      */
 56  
     public static final String getRelativePath( String basedir, String filename )
 57  
     {
 58  6
         basedir = uppercaseDrive( basedir );
 59  6
         filename = uppercaseDrive( filename );
 60  
 
 61  
         /*
 62  
          * Verify the arguments and make sure the filename is relative to the base directory.
 63  
          */
 64  6
         if ( basedir == null || basedir.length() == 0 || filename == null || filename.length() == 0
 65  
             || !filename.startsWith( basedir ) )
 66  
         {
 67  4
             return "";
 68  
         }
 69  
 
 70  
         /*
 71  
          * Normalize the arguments. First, determine the file separator that is being used, then strip that off the end
 72  
          * of both the base directory and filename.
 73  
          */
 74  2
         String separator = determineSeparator( filename );
 75  2
         basedir = StringUtils.chompLast( basedir, separator );
 76  2
         filename = StringUtils.chompLast( filename, separator );
 77  
 
 78  
         /*
 79  
          * Remove the base directory from the filename to end up with a relative filename (relative to the base
 80  
          * directory). This filename is then used to determine the relative path.
 81  
          */
 82  2
         String relativeFilename = filename.substring( basedir.length() );
 83  
 
 84  2
         return determineRelativePath( relativeFilename, separator );
 85  
     }
 86  
 
 87  
     /**
 88  
      * Determines the relative path of a filename. This method is useful in building relative links within pages of a
 89  
      * web site. It provides similar functionality to Anakia's <code>$relativePath</code> context variable. The argument
 90  
      * to this method may contain either forward or backward slashes as file separators. The relative path returned is
 91  
      * formed using forward slashes as it is expected this path is to be used as a link in a web page (again mimicking
 92  
      * Anakia's behavior).
 93  
      * <p/>
 94  
      * This method is thread-safe.
 95  
      *
 96  
      * @param filename The filename to be parsed.
 97  
      * @return The relative path of the filename. This value is not terminated with a forward slash. A zero-length
 98  
      *         string is returned if: <code>filename</code> is null or zero-length.
 99  
      * @see #getRelativeFilePath(String, String)
 100  
      */
 101  
     public static final String getRelativePath( String filename )
 102  
     {
 103  0
         filename = uppercaseDrive( filename );
 104  
 
 105  0
         if ( filename == null || filename.length() == 0 )
 106  
         {
 107  0
             return "";
 108  
         }
 109  
 
 110  
         /*
 111  
          * Normalize the argument. First, determine the file separator that is being used, then strip that off the end
 112  
          * of the filename. Then, if the filename doesn't begin with a separator, add one.
 113  
          */
 114  
 
 115  0
         String separator = determineSeparator( filename );
 116  0
         filename = StringUtils.chompLast( filename, separator );
 117  0
         if ( !filename.startsWith( separator ) )
 118  
         {
 119  0
             filename = separator + filename;
 120  
         }
 121  
 
 122  0
         return determineRelativePath( filename, separator );
 123  
     }
 124  
 
 125  
     /**
 126  
      * Determines the directory component of a filename. This is useful within DVSL templates when used in conjunction
 127  
      * with the DVSL's <code>$context.getAppValue("infilename")</code> to get the current directory that is currently
 128  
      * being processed.
 129  
      * <p/>
 130  
      * This method is thread-safe. <br/>
 131  
      * 
 132  
      * <pre>
 133  
      * PathTool.getDirectoryComponent( null )                                   = ""
 134  
      * PathTool.getDirectoryComponent( "/usr/local/java/bin" )                  = "/usr/local/java"
 135  
      * PathTool.getDirectoryComponent( "/usr/local/java/bin/" )                 = "/usr/local/java/bin"
 136  
      * PathTool.getDirectoryComponent( "/usr/local/java/bin/java.sh" )          = "/usr/local/java/bin"
 137  
      * </pre>
 138  
      *
 139  
      * @param filename The filename to be parsed.
 140  
      * @return The directory portion of the <code>filename</code>. If the filename does not contain a directory
 141  
      *         component, "." is returned.
 142  
      */
 143  
     public static final String getDirectoryComponent( String filename )
 144  
     {
 145  4
         if ( filename == null || filename.length() == 0 )
 146  
         {
 147  1
             return "";
 148  
         }
 149  
 
 150  3
         String separator = determineSeparator( filename );
 151  3
         String directory = StringUtils.chomp( filename, separator );
 152  
 
 153  3
         if ( filename.equals( directory ) )
 154  
         {
 155  0
             return ".";
 156  
         }
 157  
 
 158  3
         return directory;
 159  
     }
 160  
 
 161  
     /**
 162  
      * Calculates the appropriate link given the preferred link and the relativePath of the document. <br/>
 163  
      * 
 164  
      * <pre>
 165  
      * PathTool.calculateLink( "/index.html", "../.." )                                        = "../../index.html"
 166  
      * PathTool.calculateLink( "http://plexus.codehaus.org/plexus-utils/index.html", "../.." ) = "http://plexus.codehaus.org/plexus-utils/index.html"
 167  
      * PathTool.calculateLink( "/usr/local/java/bin/java.sh", "../.." )                        = "../../usr/local/java/bin/java.sh"
 168  
      * PathTool.calculateLink( "../index.html", "/usr/local/java/bin" )                        = "/usr/local/java/bin/../index.html"
 169  
      * PathTool.calculateLink( "../index.html", "http://plexus.codehaus.org/plexus-utils" )    = "http://plexus.codehaus.org/plexus-utils/../index.html"
 170  
      * </pre>
 171  
      *
 172  
      * @param link
 173  
      * @param relativePath
 174  
      * @return String
 175  
      */
 176  
     public static final String calculateLink( String link, String relativePath )
 177  
     {
 178  5
         if ( link == null )
 179  
         {
 180  0
             link = "";
 181  
         }
 182  5
         if ( relativePath == null )
 183  
         {
 184  0
             relativePath = "";
 185  
         }
 186  
         // This must be some historical feature
 187  5
         if ( link.startsWith( "/site/" ) )
 188  
         {
 189  0
             return link.substring( 5 );
 190  
         }
 191  
 
 192  
         // Allows absolute links in nav-bars etc
 193  5
         if ( link.startsWith( "/absolute/" ) )
 194  
         {
 195  0
             return link.substring( 10 );
 196  
         }
 197  
 
 198  
         // This traps urls like http://
 199  5
         if ( link.contains( ":" ) )
 200  
         {
 201  1
             return link;
 202  
         }
 203  
 
 204  
         // If relativepath is current directory, just pass the link through
 205  4
         if ( StringUtils.equals( relativePath, "." ) )
 206  
         {
 207  0
             if ( link.startsWith( "/" ) )
 208  
             {
 209  0
                 return link.substring( 1 );
 210  
             }
 211  
 
 212  0
             return link;
 213  
         }
 214  
 
 215  
         // If we don't do this, you can end up with ..//bob.html rather than ../bob.html
 216  4
         if ( relativePath.endsWith( "/" ) && link.startsWith( "/" ) )
 217  
         {
 218  0
             return relativePath + "." + link.substring( 1 );
 219  
         }
 220  
 
 221  4
         if ( relativePath.endsWith( "/" ) || link.startsWith( "/" ) )
 222  
         {
 223  2
             return relativePath + link;
 224  
         }
 225  
 
 226  2
         return relativePath + "/" + link;
 227  
     }
 228  
 
 229  
     /**
 230  
      * This method can calculate the relative path between two paths on a web site. <br/>
 231  
      * 
 232  
      * <pre>
 233  
      * PathTool.getRelativeWebPath( null, null )                                          = ""
 234  
      * PathTool.getRelativeWebPath( null, "http://plexus.codehaus.org/" )                 = ""
 235  
      * PathTool.getRelativeWebPath( "http://plexus.codehaus.org/", null )                 = ""
 236  
      * PathTool.getRelativeWebPath( "http://plexus.codehaus.org/",
 237  
      *                      "http://plexus.codehaus.org/plexus-utils/index.html" )        = "plexus-utils/index.html"
 238  
      * PathTool.getRelativeWebPath( "http://plexus.codehaus.org/plexus-utils/index.html",
 239  
      *                      "http://plexus.codehaus.org/"                                 = "../../"
 240  
      * </pre>
 241  
      *
 242  
      * @param oldPath
 243  
      * @param newPath
 244  
      * @return a relative web path from <code>oldPath</code>.
 245  
      */
 246  
     public static final String getRelativeWebPath( final String oldPath, final String newPath )
 247  
     {
 248  5
         if ( StringUtils.isEmpty( oldPath ) || StringUtils.isEmpty( newPath ) )
 249  
         {
 250  3
             return "";
 251  
         }
 252  
 
 253  2
         String resultPath = buildRelativePath( newPath, oldPath, '/' );
 254  
 
 255  2
         if ( newPath.endsWith( "/" ) && !resultPath.endsWith( "/" ) )
 256  
         {
 257  1
             return resultPath + "/";
 258  
         }
 259  
 
 260  1
         return resultPath;
 261  
     }
 262  
 
 263  
     /**
 264  
      * This method can calculate the relative path between two paths on a file system. <br/>
 265  
      * 
 266  
      * <pre>
 267  
      * PathTool.getRelativeFilePath( null, null )                                   = ""
 268  
      * PathTool.getRelativeFilePath( null, "/usr/local/java/bin" )                  = ""
 269  
      * PathTool.getRelativeFilePath( "/usr/local", null )                           = ""
 270  
      * PathTool.getRelativeFilePath( "/usr/local", "/usr/local/java/bin" )          = "java/bin"
 271  
      * PathTool.getRelativeFilePath( "/usr/local", "/usr/local/java/bin/" )         = "java/bin"
 272  
      * PathTool.getRelativeFilePath( "/usr/local/java/bin", "/usr/local/" )         = "../.."
 273  
      * PathTool.getRelativeFilePath( "/usr/local/", "/usr/local/java/bin/java.sh" ) = "java/bin/java.sh"
 274  
      * PathTool.getRelativeFilePath( "/usr/local/java/bin/java.sh", "/usr/local/" ) = "../../.."
 275  
      * PathTool.getRelativeFilePath( "/usr/local/", "/bin" )                        = "../../bin"
 276  
      * PathTool.getRelativeFilePath( "/bin", "/usr/local/" )                        = "../usr/local"
 277  
      * </pre>
 278  
      * 
 279  
      * Note: On Windows based system, the <code>/</code> character should be replaced by <code>\</code> character.
 280  
      *
 281  
      * @param oldPath
 282  
      * @param newPath
 283  
      * @return a relative file path from <code>oldPath</code>.
 284  
      */
 285  
     public static final String getRelativeFilePath( final String oldPath, final String newPath )
 286  
     {
 287  11
         if ( StringUtils.isEmpty( oldPath ) || StringUtils.isEmpty( newPath ) )
 288  
         {
 289  3
             return "";
 290  
         }
 291  
 
 292  
         // normalise the path delimiters
 293  8
         String fromPath = new File( oldPath ).getPath();
 294  8
         String toPath = new File( newPath ).getPath();
 295  
 
 296  
         // strip any leading slashes if its a windows path
 297  8
         if ( toPath.matches( "^\\[a-zA-Z]:" ) )
 298  
         {
 299  0
             toPath = toPath.substring( 1 );
 300  
         }
 301  8
         if ( fromPath.matches( "^\\[a-zA-Z]:" ) )
 302  
         {
 303  0
             fromPath = fromPath.substring( 1 );
 304  
         }
 305  
 
 306  
         // lowercase windows drive letters.
 307  8
         if ( fromPath.startsWith( ":", 1 ) )
 308  
         {
 309  0
             fromPath = Character.toLowerCase( fromPath.charAt( 0 ) ) + fromPath.substring( 1 );
 310  
         }
 311  8
         if ( toPath.startsWith( ":", 1 ) )
 312  
         {
 313  0
             toPath = Character.toLowerCase( toPath.charAt( 0 ) ) + toPath.substring( 1 );
 314  
         }
 315  
 
 316  
         // check for the presence of windows drives. No relative way of
 317  
         // traversing from one to the other.
 318  8
         if ( ( toPath.startsWith( ":", 1 ) && fromPath.startsWith( ":", 1 ) )
 319  
             && ( !toPath.substring( 0, 1 ).equals( fromPath.substring( 0, 1 ) ) ) )
 320  
         {
 321  
             // they both have drive path element but they dont match, no
 322  
             // relative path
 323  0
             return null;
 324  
         }
 325  
 
 326  8
         if ( ( toPath.startsWith( ":", 1 ) && !fromPath.startsWith( ":", 1 ) )
 327  
             || ( !toPath.startsWith( ":", 1 ) && fromPath.startsWith( ":", 1 ) ) )
 328  
         {
 329  
             // one has a drive path element and the other doesnt, no relative
 330  
             // path.
 331  0
             return null;
 332  
         }
 333  
 
 334  8
         String resultPath = buildRelativePath( toPath, fromPath, File.separatorChar );
 335  
 
 336  8
         if ( newPath.endsWith( File.separator ) && !resultPath.endsWith( File.separator ) )
 337  
         {
 338  3
             return resultPath + File.separator;
 339  
         }
 340  
 
 341  5
         return resultPath;
 342  
     }
 343  
 
 344  
     // ----------------------------------------------------------------------
 345  
     // Private methods
 346  
     // ----------------------------------------------------------------------
 347  
 
 348  
     /**
 349  
      * Determines the relative path of a filename. For each separator within the filename (except the leading if
 350  
      * present), append the "../" string to the return value.
 351  
      *
 352  
      * @param filename The filename to parse.
 353  
      * @param separator The separator used within the filename.
 354  
      * @return The relative path of the filename. This value is not terminated with a forward slash. A zero-length
 355  
      *         string is returned if: the filename is zero-length.
 356  
      */
 357  
     private static final String determineRelativePath( String filename, String separator )
 358  
     {
 359  2
         if ( filename.length() == 0 )
 360  
         {
 361  0
             return "";
 362  
         }
 363  
 
 364  
         /*
 365  
          * Count the slashes in the relative filename, but exclude the leading slash. If the path has no slashes, then
 366  
          * the filename is relative to the current directory.
 367  
          */
 368  2
         int slashCount = StringUtils.countMatches( filename, separator ) - 1;
 369  2
         if ( slashCount <= 0 )
 370  
         {
 371  0
             return ".";
 372  
         }
 373  
 
 374  
         /*
 375  
          * The relative filename contains one or more slashes indicating that the file is within one or more
 376  
          * directories. Thus, each slash represents a "../" in the relative path.
 377  
          */
 378  2
         StringBuilder sb = new StringBuilder();
 379  5
         for ( int i = 0; i < slashCount; i++ )
 380  
         {
 381  3
             sb.append( "../" );
 382  
         }
 383  
 
 384  
         /*
 385  
          * Finally, return the relative path but strip the trailing slash to mimic Anakia's behavior.
 386  
          */
 387  2
         return StringUtils.chop( sb.toString() );
 388  
     }
 389  
 
 390  
     /**
 391  
      * Helper method to determine the file separator (forward or backward slash) used in a filename. The slash that
 392  
      * occurs more often is returned as the separator.
 393  
      *
 394  
      * @param filename The filename parsed to determine the file separator.
 395  
      * @return The file separator used within <code>filename</code>. This value is either a forward or backward slash.
 396  
      */
 397  
     private static final String determineSeparator( String filename )
 398  
     {
 399  5
         int forwardCount = StringUtils.countMatches( filename, "/" );
 400  5
         int backwardCount = StringUtils.countMatches( filename, "\\" );
 401  
 
 402  5
         return forwardCount >= backwardCount ? "/" : "\\";
 403  
     }
 404  
 
 405  
     /**
 406  
      * Cygwin prefers lowercase drive letters, but other parts of maven use uppercase
 407  
      *
 408  
      * @param path
 409  
      * @return String
 410  
      */
 411  
     static final String uppercaseDrive( String path )
 412  
     {
 413  12
         if ( path == null )
 414  
         {
 415  4
             return null;
 416  
         }
 417  8
         if ( path.length() >= 2 && path.charAt( 1 ) == ':' )
 418  
         {
 419  0
             path = Character.toUpperCase( path.charAt( 0 ) ) + path.substring( 1 );
 420  
         }
 421  8
         return path;
 422  
     }
 423  
 
 424  
     private static final String buildRelativePath( String toPath, String fromPath, final char separatorChar )
 425  
     {
 426  
         // use tokeniser to traverse paths and for lazy checking
 427  10
         StringTokenizer toTokeniser = new StringTokenizer( toPath, String.valueOf( separatorChar ) );
 428  10
         StringTokenizer fromTokeniser = new StringTokenizer( fromPath, String.valueOf( separatorChar ) );
 429  
 
 430  10
         int count = 0;
 431  
 
 432  
         // walk along the to path looking for divergence from the from path
 433  25
         while ( toTokeniser.hasMoreTokens() && fromTokeniser.hasMoreTokens() )
 434  
         {
 435  17
             if ( separatorChar == '\\' )
 436  
             {
 437  0
                 if ( !fromTokeniser.nextToken().equalsIgnoreCase( toTokeniser.nextToken() ) )
 438  
                 {
 439  0
                     break;
 440  
                 }
 441  
             }
 442  
             else
 443  
             {
 444  17
                 if ( !fromTokeniser.nextToken().equals( toTokeniser.nextToken() ) )
 445  
                 {
 446  2
                     break;
 447  
                 }
 448  
             }
 449  
 
 450  15
             count++;
 451  
         }
 452  
 
 453  
         // reinitialise the tokenisers to count positions to retrieve the
 454  
         // gobbled token
 455  
 
 456  10
         toTokeniser = new StringTokenizer( toPath, String.valueOf( separatorChar ) );
 457  10
         fromTokeniser = new StringTokenizer( fromPath, String.valueOf( separatorChar ) );
 458  
 
 459  25
         while ( count-- > 0 )
 460  
         {
 461  15
             fromTokeniser.nextToken();
 462  15
             toTokeniser.nextToken();
 463  
         }
 464  
 
 465  10
         String relativePath = "";
 466  
 
 467  
         // add back refs for the rest of from location.
 468  20
         while ( fromTokeniser.hasMoreTokens() )
 469  
         {
 470  10
             fromTokeniser.nextToken();
 471  
 
 472  10
             relativePath += "..";
 473  
 
 474  10
             if ( fromTokeniser.hasMoreTokens() )
 475  
             {
 476  5
                 relativePath += separatorChar;
 477  
             }
 478  
         }
 479  
 
 480  10
         if ( relativePath.length() != 0 && toTokeniser.hasMoreTokens() )
 481  
         {
 482  2
             relativePath += separatorChar;
 483  
         }
 484  
 
 485  
         // add fwd fills for whatevers left of newPath.
 486  22
         while ( toTokeniser.hasMoreTokens() )
 487  
         {
 488  12
             relativePath += toTokeniser.nextToken();
 489  
 
 490  12
             if ( toTokeniser.hasMoreTokens() )
 491  
             {
 492  6
                 relativePath += separatorChar;
 493  
             }
 494  
         }
 495  10
         return relativePath;
 496  
     }
 497  
 }