Coverage Report - org.apache.commons.io.FilenameUtils
 
Classes in this File Line Coverage Branch Coverage Complexity
FilenameUtils
97%
299/307
96%
287/296
6.256
 
 1  
 /*
 2  
  * Licensed to the Apache Software Foundation (ASF) under one or more
 3  
  * contributor license agreements.  See the NOTICE file distributed with
 4  
  * this work for additional information regarding copyright ownership.
 5  
  * The ASF licenses this file to You under the Apache License, Version 2.0
 6  
  * (the "License"); you may not use this file except in compliance with
 7  
  * the License.  You may obtain a copy of the License at
 8  
  *
 9  
  *      http://www.apache.org/licenses/LICENSE-2.0
 10  
  *
 11  
  * Unless required by applicable law or agreed to in writing, software
 12  
  * distributed under the License is distributed on an "AS IS" BASIS,
 13  
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 14  
  * See the License for the specific language governing permissions and
 15  
  * limitations under the License.
 16  
  */
 17  
 package org.apache.commons.io;
 18  
 
 19  
 import java.io.File;
 20  
 import java.io.IOException;
 21  
 import java.util.ArrayList;
 22  
 import java.util.Collection;
 23  
 import java.util.Stack;
 24  
 
 25  
 /**
 26  
  * General filename and filepath manipulation utilities.
 27  
  * <p>
 28  
  * When dealing with filenames you can hit problems when moving from a Windows
 29  
  * based development machine to a Unix based production machine.
 30  
  * This class aims to help avoid those problems.
 31  
  * <p>
 32  
  * <b>NOTE</b>: You may be able to avoid using this class entirely simply by
 33  
  * using JDK {@link java.io.File File} objects and the two argument constructor
 34  
  * {@link java.io.File#File(java.io.File, java.lang.String) File(File,String)}.
 35  
  * <p>
 36  
  * Most methods on this class are designed to work the same on both Unix and Windows.
 37  
  * Those that don't include 'System', 'Unix' or 'Windows' in their name.
 38  
  * <p>
 39  
  * Most methods recognise both separators (forward and back), and both
 40  
  * sets of prefixes. See the javadoc of each method for details.
 41  
  * <p>
 42  
  * This class defines six components within a filename
 43  
  * (example C:\dev\project\file.txt):
 44  
  * <ul>
 45  
  * <li>the prefix - C:\</li>
 46  
  * <li>the path - dev\project\</li>
 47  
  * <li>the full path - C:\dev\project\</li>
 48  
  * <li>the name - file.txt</li>
 49  
  * <li>the base name - file</li>
 50  
  * <li>the extension - txt</li>
 51  
  * </ul>
 52  
  * Note that this class works best if directory filenames end with a separator.
 53  
  * If you omit the last separator, it is impossible to determine if the filename
 54  
  * corresponds to a file or a directory. As a result, we have chosen to say
 55  
  * it corresponds to a file.
 56  
  * <p>
 57  
  * This class only supports Unix and Windows style names.
 58  
  * Prefixes are matched as follows:
 59  
  * <pre>
 60  
  * Windows:
 61  
  * a\b\c.txt           --&gt; ""          --&gt; relative
 62  
  * \a\b\c.txt          --&gt; "\"         --&gt; current drive absolute
 63  
  * C:a\b\c.txt         --&gt; "C:"        --&gt; drive relative
 64  
  * C:\a\b\c.txt        --&gt; "C:\"       --&gt; absolute
 65  
  * \\server\a\b\c.txt  --&gt; "\\server\" --&gt; UNC
 66  
  *
 67  
  * Unix:
 68  
  * a/b/c.txt           --&gt; ""          --&gt; relative
 69  
  * /a/b/c.txt          --&gt; "/"         --&gt; absolute
 70  
  * ~/a/b/c.txt         --&gt; "~/"        --&gt; current user
 71  
  * ~                   --&gt; "~/"        --&gt; current user (slash added)
 72  
  * ~user/a/b/c.txt     --&gt; "~user/"    --&gt; named user
 73  
  * ~user               --&gt; "~user/"    --&gt; named user (slash added)
 74  
  * </pre>
 75  
  * Both prefix styles are matched always, irrespective of the machine that you are
 76  
  * currently running on.
 77  
  * <p>
 78  
  * Origin of code: Excalibur, Alexandria, Tomcat, Commons-Utils.
 79  
  *
 80  
  * @version $Id: FilenameUtils.java 1563227 2014-01-31 19:45:30Z ggregory $
 81  
  * @since 1.1
 82  
  */
 83  
 public class FilenameUtils {
 84  
 
 85  
     /**
 86  
      * The extension separator character.
 87  
      * @since 1.4
 88  
      */
 89  
     public static final char EXTENSION_SEPARATOR = '.';
 90  
 
 91  
     /**
 92  
      * The extension separator String.
 93  
      * @since 1.4
 94  
      */
 95  62
     public static final String EXTENSION_SEPARATOR_STR = Character.toString(EXTENSION_SEPARATOR);
 96  
 
 97  
     /**
 98  
      * The Unix separator character.
 99  
      */
 100  
     private static final char UNIX_SEPARATOR = '/';
 101  
 
 102  
     /**
 103  
      * The Windows separator character.
 104  
      */
 105  
     private static final char WINDOWS_SEPARATOR = '\\';
 106  
 
 107  
     /**
 108  
      * The system separator character.
 109  
      */
 110  62
     private static final char SYSTEM_SEPARATOR = File.separatorChar;
 111  
 
 112  
     /**
 113  
      * The separator character that is the opposite of the system separator.
 114  
      */
 115  
     private static final char OTHER_SEPARATOR;
 116  
     static {
 117  62
         if (isSystemWindows()) {
 118  62
             OTHER_SEPARATOR = UNIX_SEPARATOR;
 119  
         } else {
 120  0
             OTHER_SEPARATOR = WINDOWS_SEPARATOR;
 121  
         }
 122  62
     }
 123  
 
 124  
     /**
 125  
      * Instances should NOT be constructed in standard programming.
 126  
      */
 127  
     public FilenameUtils() {
 128  0
         super();
 129  0
     }
 130  
 
 131  
     //-----------------------------------------------------------------------
 132  
     /**
 133  
      * Determines if Windows file system is in use.
 134  
      *
 135  
      * @return true if the system is Windows
 136  
      */
 137  
     static boolean isSystemWindows() {
 138  2532
         return SYSTEM_SEPARATOR == WINDOWS_SEPARATOR;
 139  
     }
 140  
 
 141  
     //-----------------------------------------------------------------------
 142  
     /**
 143  
      * Checks if the character is a separator.
 144  
      *
 145  
      * @param ch  the character to check
 146  
      * @return true if it is a separator character
 147  
      */
 148  
     private static boolean isSeparator(final char ch) {
 149  1426
         return ch == UNIX_SEPARATOR || ch == WINDOWS_SEPARATOR;
 150  
     }
 151  
 
 152  
     //-----------------------------------------------------------------------
 153  
     /**
 154  
      * Normalizes a path, removing double and single dot path steps.
 155  
      * <p>
 156  
      * This method normalizes a path to a standard format.
 157  
      * The input may contain separators in either Unix or Windows format.
 158  
      * The output will contain separators in the format of the system.
 159  
      * <p>
 160  
      * A trailing slash will be retained.
 161  
      * A double slash will be merged to a single slash (but UNC names are handled).
 162  
      * A single dot path segment will be removed.
 163  
      * A double dot will cause that path segment and the one before to be removed.
 164  
      * If the double dot has no parent path segment to work with, {@code null}
 165  
      * is returned.
 166  
      * <p>
 167  
      * The output will be the same on both Unix and Windows except
 168  
      * for the separator character.
 169  
      * <pre>
 170  
      * /foo//               --&gt;   /foo/
 171  
      * /foo/./              --&gt;   /foo/
 172  
      * /foo/../bar          --&gt;   /bar
 173  
      * /foo/../bar/         --&gt;   /bar/
 174  
      * /foo/../bar/../baz   --&gt;   /baz
 175  
      * //foo//./bar         --&gt;   /foo/bar
 176  
      * /../                 --&gt;   null
 177  
      * ../foo               --&gt;   null
 178  
      * foo/bar/..           --&gt;   foo/
 179  
      * foo/../../bar        --&gt;   null
 180  
      * foo/../bar           --&gt;   bar
 181  
      * //server/foo/../bar  --&gt;   //server/bar
 182  
      * //server/../bar      --&gt;   null
 183  
      * C:\foo\..\bar        --&gt;   C:\bar
 184  
      * C:\..\bar            --&gt;   null
 185  
      * ~/foo/../bar/        --&gt;   ~/bar/
 186  
      * ~/../bar             --&gt;   null
 187  
      * </pre>
 188  
      * (Note the file separator returned will be correct for Windows/Unix)
 189  
      *
 190  
      * @param filename  the filename to normalize, null returns null
 191  
      * @return the normalized filename, or null if invalid
 192  
      */
 193  
     public static String normalize(final String filename) {
 194  392
         return doNormalize(filename, SYSTEM_SEPARATOR, true);
 195  
     }
 196  
     /**
 197  
      * Normalizes a path, removing double and single dot path steps.
 198  
      * <p>
 199  
      * This method normalizes a path to a standard format.
 200  
      * The input may contain separators in either Unix or Windows format.
 201  
      * The output will contain separators in the format specified.
 202  
      * <p>
 203  
      * A trailing slash will be retained.
 204  
      * A double slash will be merged to a single slash (but UNC names are handled).
 205  
      * A single dot path segment will be removed.
 206  
      * A double dot will cause that path segment and the one before to be removed.
 207  
      * If the double dot has no parent path segment to work with, {@code null}
 208  
      * is returned.
 209  
      * <p>
 210  
      * The output will be the same on both Unix and Windows except
 211  
      * for the separator character.
 212  
      * <pre>
 213  
      * /foo//               --&gt;   /foo/
 214  
      * /foo/./              --&gt;   /foo/
 215  
      * /foo/../bar          --&gt;   /bar
 216  
      * /foo/../bar/         --&gt;   /bar/
 217  
      * /foo/../bar/../baz   --&gt;   /baz
 218  
      * //foo//./bar         --&gt;   /foo/bar
 219  
      * /../                 --&gt;   null
 220  
      * ../foo               --&gt;   null
 221  
      * foo/bar/..           --&gt;   foo/
 222  
      * foo/../../bar        --&gt;   null
 223  
      * foo/../bar           --&gt;   bar
 224  
      * //server/foo/../bar  --&gt;   //server/bar
 225  
      * //server/../bar      --&gt;   null
 226  
      * C:\foo\..\bar        --&gt;   C:\bar
 227  
      * C:\..\bar            --&gt;   null
 228  
      * ~/foo/../bar/        --&gt;   ~/bar/
 229  
      * ~/../bar             --&gt;   null
 230  
      * </pre>
 231  
      * The output will be the same on both Unix and Windows including
 232  
      * the separator character.
 233  
      *
 234  
      * @param filename  the filename to normalize, null returns null
 235  
      * @param unixSeparator {@code true} if a unix separator should
 236  
      * be used or {@code false} if a windows separator should be used.
 237  
      * @return the normalized filename, or null if invalid
 238  
      * @since 2.0
 239  
      */
 240  
     public static String normalize(final String filename, final boolean unixSeparator) {
 241  30
         final char separator = unixSeparator ? UNIX_SEPARATOR : WINDOWS_SEPARATOR;
 242  30
         return doNormalize(filename, separator, true);
 243  
     }
 244  
 
 245  
     //-----------------------------------------------------------------------
 246  
     /**
 247  
      * Normalizes a path, removing double and single dot path steps,
 248  
      * and removing any final directory separator.
 249  
      * <p>
 250  
      * This method normalizes a path to a standard format.
 251  
      * The input may contain separators in either Unix or Windows format.
 252  
      * The output will contain separators in the format of the system.
 253  
      * <p>
 254  
      * A trailing slash will be removed.
 255  
      * A double slash will be merged to a single slash (but UNC names are handled).
 256  
      * A single dot path segment will be removed.
 257  
      * A double dot will cause that path segment and the one before to be removed.
 258  
      * If the double dot has no parent path segment to work with, {@code null}
 259  
      * is returned.
 260  
      * <p>
 261  
      * The output will be the same on both Unix and Windows except
 262  
      * for the separator character.
 263  
      * <pre>
 264  
      * /foo//               --&gt;   /foo
 265  
      * /foo/./              --&gt;   /foo
 266  
      * /foo/../bar          --&gt;   /bar
 267  
      * /foo/../bar/         --&gt;   /bar
 268  
      * /foo/../bar/../baz   --&gt;   /baz
 269  
      * //foo//./bar         --&gt;   /foo/bar
 270  
      * /../                 --&gt;   null
 271  
      * ../foo               --&gt;   null
 272  
      * foo/bar/..           --&gt;   foo
 273  
      * foo/../../bar        --&gt;   null
 274  
      * foo/../bar           --&gt;   bar
 275  
      * //server/foo/../bar  --&gt;   //server/bar
 276  
      * //server/../bar      --&gt;   null
 277  
      * C:\foo\..\bar        --&gt;   C:\bar
 278  
      * C:\..\bar            --&gt;   null
 279  
      * ~/foo/../bar/        --&gt;   ~/bar
 280  
      * ~/../bar             --&gt;   null
 281  
      * </pre>
 282  
      * (Note the file separator returned will be correct for Windows/Unix)
 283  
      *
 284  
      * @param filename  the filename to normalize, null returns null
 285  
      * @return the normalized filename, or null if invalid
 286  
      */
 287  
     public static String normalizeNoEndSeparator(final String filename) {
 288  276
         return doNormalize(filename, SYSTEM_SEPARATOR, false);
 289  
     }
 290  
 
 291  
     /**
 292  
      * Normalizes a path, removing double and single dot path steps,
 293  
      * and removing any final directory separator.
 294  
      * <p>
 295  
      * This method normalizes a path to a standard format.
 296  
      * The input may contain separators in either Unix or Windows format.
 297  
      * The output will contain separators in the format specified.
 298  
      * <p>
 299  
      * A trailing slash will be removed.
 300  
      * A double slash will be merged to a single slash (but UNC names are handled).
 301  
      * A single dot path segment will be removed.
 302  
      * A double dot will cause that path segment and the one before to be removed.
 303  
      * If the double dot has no parent path segment to work with, {@code null}
 304  
      * is returned.
 305  
      * <p>
 306  
      * The output will be the same on both Unix and Windows including
 307  
      * the separator character.
 308  
      * <pre>
 309  
      * /foo//               --&gt;   /foo
 310  
      * /foo/./              --&gt;   /foo
 311  
      * /foo/../bar          --&gt;   /bar
 312  
      * /foo/../bar/         --&gt;   /bar
 313  
      * /foo/../bar/../baz   --&gt;   /baz
 314  
      * //foo//./bar         --&gt;   /foo/bar
 315  
      * /../                 --&gt;   null
 316  
      * ../foo               --&gt;   null
 317  
      * foo/bar/..           --&gt;   foo
 318  
      * foo/../../bar        --&gt;   null
 319  
      * foo/../bar           --&gt;   bar
 320  
      * //server/foo/../bar  --&gt;   //server/bar
 321  
      * //server/../bar      --&gt;   null
 322  
      * C:\foo\..\bar        --&gt;   C:\bar
 323  
      * C:\..\bar            --&gt;   null
 324  
      * ~/foo/../bar/        --&gt;   ~/bar
 325  
      * ~/../bar             --&gt;   null
 326  
      * </pre>
 327  
      *
 328  
      * @param filename  the filename to normalize, null returns null
 329  
      * @param unixSeparator {@code true} if a unix separator should
 330  
      * be used or {@code false} if a windows separtor should be used.
 331  
      * @return the normalized filename, or null if invalid
 332  
      * @since 2.0
 333  
      */
 334  
     public static String normalizeNoEndSeparator(final String filename, final boolean unixSeparator) {
 335  8
          final char separator = unixSeparator ? UNIX_SEPARATOR : WINDOWS_SEPARATOR;
 336  8
         return doNormalize(filename, separator, false);
 337  
     }
 338  
 
 339  
     /**
 340  
      * Internal method to perform the normalization.
 341  
      *
 342  
      * @param filename  the filename
 343  
      * @param separator The separator character to use
 344  
      * @param keepSeparator  true to keep the final separator
 345  
      * @return the normalized filename
 346  
      */
 347  
     private static String doNormalize(final String filename, final char separator, final boolean keepSeparator) {
 348  706
         if (filename == null) {
 349  4
             return null;
 350  
         }
 351  702
         int size = filename.length();
 352  702
         if (size == 0) {
 353  20
             return filename;
 354  
         }
 355  682
         final int prefix = getPrefixLength(filename);
 356  682
         if (prefix < 0) {
 357  34
             return null;
 358  
         }
 359  
 
 360  648
         final char[] array = new char[size + 2];  // +1 for possible extra slash, +2 for arraycopy
 361  648
         filename.getChars(0, filename.length(), array, 0);
 362  
 
 363  
         // fix separators throughout
 364  648
         final char otherSeparator = separator == SYSTEM_SEPARATOR ? OTHER_SEPARATOR : SYSTEM_SEPARATOR;
 365  7818
         for (int i = 0; i < array.length; i++) {
 366  7170
             if (array[i] == otherSeparator) {
 367  1786
                 array[i] = separator;
 368  
             }
 369  
         }
 370  
 
 371  
         // add extra separator on the end to simplify code below
 372  648
         boolean lastIsDirectory = true;
 373  648
         if (array[size - 1] != separator) {
 374  518
             array[size++] = separator;
 375  518
             lastIsDirectory = false;
 376  
         }
 377  
 
 378  
         // adjoining slashes
 379  4648
         for (int i = prefix + 1; i < size; i++) {
 380  4000
             if (array[i] == separator && array[i - 1] == separator) {
 381  28
                 System.arraycopy(array, i, array, i - 1, size - i);
 382  28
                 size--;
 383  28
                 i--;
 384  
             }
 385  
         }
 386  
 
 387  
         // dot slash
 388  4524
         for (int i = prefix + 1; i < size; i++) {
 389  3876
             if (array[i] == separator && array[i - 1] == '.' &&
 390  
                     (i == prefix + 1 || array[i - 2] == separator)) {
 391  184
                 if (i == size - 1) {
 392  88
                     lastIsDirectory = true;
 393  
                 }
 394  184
                 System.arraycopy(array, i + 1, array, i - 1, size - i);
 395  184
                 size -=2;
 396  184
                 i--;
 397  
             }
 398  
         }
 399  
 
 400  
         // double dot slash
 401  
         outer:
 402  3122
         for (int i = prefix + 2; i < size; i++) {
 403  2586
             if (array[i] == separator && array[i - 1] == '.' && array[i - 2] == '.' &&
 404  
                     (i == prefix + 2 || array[i - 3] == separator)) {
 405  504
                 if (i == prefix + 2) {
 406  112
                     return null;
 407  
                 }
 408  392
                 if (i == size - 1) {
 409  64
                     lastIsDirectory = true;
 410  
                 }
 411  
                 int j;
 412  784
                 for (j = i - 4 ; j >= prefix; j--) {
 413  664
                     if (array[j] == separator) {
 414  
                         // remove b/../ from a/b/../c
 415  272
                         System.arraycopy(array, i + 1, array, j + 1, size - i);
 416  272
                         size -= i - j;
 417  272
                         i = j + 1;
 418  272
                         continue outer;
 419  
                     }
 420  
                 }
 421  
                 // remove a/../ from a/../c
 422  120
                 System.arraycopy(array, i + 1, array, prefix, size - i);
 423  120
                 size -= i + 1 - prefix;
 424  120
                 i = prefix + 1;
 425  
             }
 426  
         }
 427  
 
 428  536
         if (size <= 0) {  // should never be less than 0
 429  16
             return "";
 430  
         }
 431  520
         if (size <= prefix) {  // should never be less than prefix
 432  104
             return new String(array, 0, size);
 433  
         }
 434  416
         if (lastIsDirectory && keepSeparator) {
 435  76
             return new String(array, 0, size);  // keep trailing separator
 436  
         }
 437  340
         return new String(array, 0, size - 1);  // lose trailing separator
 438  
     }
 439  
 
 440  
     //-----------------------------------------------------------------------
 441  
     /**
 442  
      * Concatenates a filename to a base path using normal command line style rules.
 443  
      * <p>
 444  
      * The effect is equivalent to resultant directory after changing
 445  
      * directory to the first argument, followed by changing directory to
 446  
      * the second argument.
 447  
      * <p>
 448  
      * The first argument is the base path, the second is the path to concatenate.
 449  
      * The returned path is always normalized via {@link #normalize(String)},
 450  
      * thus <code>..</code> is handled.
 451  
      * <p>
 452  
      * If <code>pathToAdd</code> is absolute (has an absolute prefix), then
 453  
      * it will be normalized and returned.
 454  
      * Otherwise, the paths will be joined, normalized and returned.
 455  
      * <p>
 456  
      * The output will be the same on both Unix and Windows except
 457  
      * for the separator character.
 458  
      * <pre>
 459  
      * /foo/ + bar          --&gt;   /foo/bar
 460  
      * /foo + bar           --&gt;   /foo/bar
 461  
      * /foo + /bar          --&gt;   /bar
 462  
      * /foo + C:/bar        --&gt;   C:/bar
 463  
      * /foo + C:bar         --&gt;   C:bar (*)
 464  
      * /foo/a/ + ../bar     --&gt;   foo/bar
 465  
      * /foo/ + ../../bar    --&gt;   null
 466  
      * /foo/ + /bar         --&gt;   /bar
 467  
      * /foo/.. + /bar       --&gt;   /bar
 468  
      * /foo + bar/c.txt     --&gt;   /foo/bar/c.txt
 469  
      * /foo/c.txt + bar     --&gt;   /foo/c.txt/bar (!)
 470  
      * </pre>
 471  
      * (*) Note that the Windows relative drive prefix is unreliable when
 472  
      * used with this method.
 473  
      * (!) Note that the first parameter must be a path. If it ends with a name, then
 474  
      * the name will be built into the concatenated path. If this might be a problem,
 475  
      * use {@link #getFullPath(String)} on the base path argument.
 476  
      *
 477  
      * @param basePath  the base path to attach to, always treated as a path
 478  
      * @param fullFilenameToAdd  the filename (or path) to attach to the base
 479  
      * @return the concatenated path, or null if invalid
 480  
      */
 481  
     public static String concat(final String basePath, final String fullFilenameToAdd) {
 482  58
         final int prefix = getPrefixLength(fullFilenameToAdd);
 483  58
         if (prefix < 0) {
 484  6
             return null;
 485  
         }
 486  52
         if (prefix > 0) {
 487  24
             return normalize(fullFilenameToAdd);
 488  
         }
 489  28
         if (basePath == null) {
 490  4
             return null;
 491  
         }
 492  24
         final int len = basePath.length();
 493  24
         if (len == 0) {
 494  4
             return normalize(fullFilenameToAdd);
 495  
         }
 496  20
         final char ch = basePath.charAt(len - 1);
 497  20
         if (isSeparator(ch)) {
 498  8
             return normalize(basePath + fullFilenameToAdd);
 499  
         } else {
 500  12
             return normalize(basePath + '/' + fullFilenameToAdd);
 501  
         }
 502  
     }
 503  
 
 504  
     /**
 505  
      * Determines whether the {@code parent} directory contains the {@code child} element (a file or directory).
 506  
      * <p>
 507  
      * The files names are expected to be normalized.
 508  
      * </p>
 509  
      *
 510  
      * Edge cases:
 511  
      * <ul>
 512  
      * <li>A {@code directory} must not be null: if null, throw IllegalArgumentException</li>
 513  
      * <li>A directory does not contain itself: return false</li>
 514  
      * <li>A null child file is not contained in any parent: return false</li>
 515  
      * </ul>
 516  
      *
 517  
      * @param canonicalParent
 518  
      *            the file to consider as the parent.
 519  
      * @param canonicalChild
 520  
      *            the file to consider as the child.
 521  
      * @return true is the candidate leaf is under by the specified composite. False otherwise.
 522  
      * @throws IOException
 523  
      *             if an IO error occurs while checking the files.
 524  
      * @since 2.2
 525  
      * @see FileUtils#directoryContains(File, File)
 526  
      */
 527  
     public static boolean directoryContains(final String canonicalParent, final String canonicalChild)
 528  
             throws IOException {
 529  
 
 530  
         // Fail fast against NullPointerException
 531  36
         if (canonicalParent == null) {
 532  0
             throw new IllegalArgumentException("Directory must not be null");
 533  
         }
 534  
 
 535  36
         if (canonicalChild == null) {
 536  0
             return false;
 537  
         }
 538  
 
 539  36
         if (IOCase.SYSTEM.checkEquals(canonicalParent, canonicalChild)) {
 540  0
             return false;
 541  
         }
 542  
 
 543  36
         return IOCase.SYSTEM.checkStartsWith(canonicalChild, canonicalParent);
 544  
     }
 545  
 
 546  
     //-----------------------------------------------------------------------
 547  
     /**
 548  
      * Converts all separators to the Unix separator of forward slash.
 549  
      *
 550  
      * @param path  the path to be changed, null ignored
 551  
      * @return the updated path
 552  
      */
 553  
     public static String separatorsToUnix(final String path) {
 554  12
         if (path == null || path.indexOf(WINDOWS_SEPARATOR) == -1) {
 555  6
             return path;
 556  
         }
 557  6
         return path.replace(WINDOWS_SEPARATOR, UNIX_SEPARATOR);
 558  
     }
 559  
 
 560  
     /**
 561  
      * Converts all separators to the Windows separator of backslash.
 562  
      *
 563  
      * @param path  the path to be changed, null ignored
 564  
      * @return the updated path
 565  
      */
 566  
     public static String separatorsToWindows(final String path) {
 567  22
         if (path == null || path.indexOf(UNIX_SEPARATOR) == -1) {
 568  10
             return path;
 569  
         }
 570  12
         return path.replace(UNIX_SEPARATOR, WINDOWS_SEPARATOR);
 571  
     }
 572  
 
 573  
     /**
 574  
      * Converts all separators to the system separator.
 575  
      *
 576  
      * @param path  the path to be changed, null ignored
 577  
      * @return the updated path
 578  
      */
 579  
     public static String separatorsToSystem(final String path) {
 580  12
         if (path == null) {
 581  2
             return null;
 582  
         }
 583  10
         if (isSystemWindows()) {
 584  10
             return separatorsToWindows(path);
 585  
         } else {
 586  0
             return separatorsToUnix(path);
 587  
         }
 588  
     }
 589  
 
 590  
     //-----------------------------------------------------------------------
 591  
     /**
 592  
      * Returns the length of the filename prefix, such as <code>C:/</code> or <code>~/</code>.
 593  
      * <p>
 594  
      * This method will handle a file in either Unix or Windows format.
 595  
      * <p>
 596  
      * The prefix length includes the first slash in the full filename
 597  
      * if applicable. Thus, it is possible that the length returned is greater
 598  
      * than the length of the input string.
 599  
      * <pre>
 600  
      * Windows:
 601  
      * a\b\c.txt           --&gt; ""          --&gt; relative
 602  
      * \a\b\c.txt          --&gt; "\"         --&gt; current drive absolute
 603  
      * C:a\b\c.txt         --&gt; "C:"        --&gt; drive relative
 604  
      * C:\a\b\c.txt        --&gt; "C:\"       --&gt; absolute
 605  
      * \\server\a\b\c.txt  --&gt; "\\server\" --&gt; UNC
 606  
      * \\\a\b\c.txt        --&gt;  error, length = -1
 607  
      *
 608  
      * Unix:
 609  
      * a/b/c.txt           --&gt; ""          --&gt; relative
 610  
      * /a/b/c.txt          --&gt; "/"         --&gt; absolute
 611  
      * ~/a/b/c.txt         --&gt; "~/"        --&gt; current user
 612  
      * ~                   --&gt; "~/"        --&gt; current user (slash added)
 613  
      * ~user/a/b/c.txt     --&gt; "~user/"    --&gt; named user
 614  
      * ~user               --&gt; "~user/"    --&gt; named user (slash added)
 615  
      * //server/a/b/c.txt  --&gt; "//server/"
 616  
      * ///a/b/c.txt        --&gt; error, length = -1
 617  
      * </pre>
 618  
      * <p>
 619  
      * The output will be the same irrespective of the machine that the code is running on.
 620  
      * ie. both Unix and Windows prefixes are matched regardless.
 621  
      *
 622  
      * Note that a leading // (or \\) is used to indicate a UNC name on Windows.
 623  
      * These must be followed by a server name, so double-slashes are not collapsed
 624  
      * to a single slash at the start of the filename.
 625  
      *
 626  
      * @param filename  the filename to find the prefix in, null returns -1
 627  
      * @return the length of the prefix, -1 if invalid or null
 628  
      */
 629  
     public static int getPrefixLength(final String filename) {
 630  1118
         if (filename == null) {
 631  6
             return -1;
 632  
         }
 633  1112
         final int len = filename.length();
 634  1112
         if (len == 0) {
 635  18
             return 0;
 636  
         }
 637  1094
         char ch0 = filename.charAt(0);
 638  1094
         if (ch0 == ':') {
 639  20
             return -1;
 640  
         }
 641  1074
         if (len == 1) {
 642  50
             if (ch0 == '~') {
 643  22
                 return 2;  // return a length greater than the input
 644  
             }
 645  28
             return isSeparator(ch0) ? 1 : 0;
 646  
         } else {
 647  1024
             if (ch0 == '~') {
 648  234
                 int posUnix = filename.indexOf(UNIX_SEPARATOR, 1);
 649  234
                 int posWin = filename.indexOf(WINDOWS_SEPARATOR, 1);
 650  234
                 if (posUnix == -1 && posWin == -1) {
 651  22
                     return len + 1;  // return a length greater than the input
 652  
                 }
 653  212
                 posUnix = posUnix == -1 ? posWin : posUnix;
 654  212
                 posWin = posWin == -1 ? posUnix : posWin;
 655  212
                 return Math.min(posUnix, posWin) + 1;
 656  
             }
 657  790
             final char ch1 = filename.charAt(1);
 658  790
             if (ch1 == ':') {
 659  270
                 ch0 = Character.toUpperCase(ch0);
 660  270
                 if (ch0 >= 'A' && ch0 <= 'Z') {
 661  222
                     if (len == 2 || isSeparator(filename.charAt(2)) == false) {
 662  114
                         return 2;
 663  
                     }
 664  108
                     return 3;
 665  
                 }
 666  48
                 return -1;
 667  
 
 668  520
             } else if (isSeparator(ch0) && isSeparator(ch1)) {
 669  148
                 int posUnix = filename.indexOf(UNIX_SEPARATOR, 2);
 670  148
                 int posWin = filename.indexOf(WINDOWS_SEPARATOR, 2);
 671  148
                 if (posUnix == -1 && posWin == -1 || posUnix == 2 || posWin == 2) {
 672  44
                     return -1;
 673  
                 }
 674  104
                 posUnix = posUnix == -1 ? posWin : posUnix;
 675  104
                 posWin = posWin == -1 ? posUnix : posWin;
 676  104
                 return Math.min(posUnix, posWin) + 1;
 677  
             } else {
 678  372
                 return isSeparator(ch0) ? 1 : 0;
 679  
             }
 680  
         }
 681  
     }
 682  
 
 683  
     /**
 684  
      * Returns the index of the last directory separator character.
 685  
      * <p>
 686  
      * This method will handle a file in either Unix or Windows format.
 687  
      * The position of the last forward or backslash is returned.
 688  
      * <p>
 689  
      * The output will be the same irrespective of the machine that the code is running on.
 690  
      *
 691  
      * @param filename  the filename to find the last path separator in, null returns -1
 692  
      * @return the index of the last separator character, or -1 if there
 693  
      * is no such character
 694  
      */
 695  
     public static int indexOfLastSeparator(final String filename) {
 696  522
         if (filename == null) {
 697  2
             return -1;
 698  
         }
 699  520
         final int lastUnixPos = filename.lastIndexOf(UNIX_SEPARATOR);
 700  520
         final int lastWindowsPos = filename.lastIndexOf(WINDOWS_SEPARATOR);
 701  520
         return Math.max(lastUnixPos, lastWindowsPos);
 702  
     }
 703  
 
 704  
     /**
 705  
      * Returns the index of the last extension separator character, which is a dot.
 706  
      * <p>
 707  
      * This method also checks that there is no directory separator after the last dot.
 708  
      * To do this it uses {@link #indexOfLastSeparator(String)} which will
 709  
      * handle a file in either Unix or Windows format.
 710  
      * <p>
 711  
      * The output will be the same irrespective of the machine that the code is running on.
 712  
      *
 713  
      * @param filename  the filename to find the last path separator in, null returns -1
 714  
      * @return the index of the last separator character, or -1 if there
 715  
      * is no such character
 716  
      */
 717  
     public static int indexOfExtension(final String filename) {
 718  342
         if (filename == null) {
 719  2
             return -1;
 720  
         }
 721  340
         final int extensionPos = filename.lastIndexOf(EXTENSION_SEPARATOR);
 722  340
         final int lastSeparator = indexOfLastSeparator(filename);
 723  340
         return lastSeparator > extensionPos ? -1 : extensionPos;
 724  
     }
 725  
 
 726  
     //-----------------------------------------------------------------------
 727  
     /**
 728  
      * Gets the prefix from a full filename, such as <code>C:/</code>
 729  
      * or <code>~/</code>.
 730  
      * <p>
 731  
      * This method will handle a file in either Unix or Windows format.
 732  
      * The prefix includes the first slash in the full filename where applicable.
 733  
      * <pre>
 734  
      * Windows:
 735  
      * a\b\c.txt           --&gt; ""          --&gt; relative
 736  
      * \a\b\c.txt          --&gt; "\"         --&gt; current drive absolute
 737  
      * C:a\b\c.txt         --&gt; "C:"        --&gt; drive relative
 738  
      * C:\a\b\c.txt        --&gt; "C:\"       --&gt; absolute
 739  
      * \\server\a\b\c.txt  --&gt; "\\server\" --&gt; UNC
 740  
      *
 741  
      * Unix:
 742  
      * a/b/c.txt           --&gt; ""          --&gt; relative
 743  
      * /a/b/c.txt          --&gt; "/"         --&gt; absolute
 744  
      * ~/a/b/c.txt         --&gt; "~/"        --&gt; current user
 745  
      * ~                   --&gt; "~/"        --&gt; current user (slash added)
 746  
      * ~user/a/b/c.txt     --&gt; "~user/"    --&gt; named user
 747  
      * ~user               --&gt; "~user/"    --&gt; named user (slash added)
 748  
      * </pre>
 749  
      * <p>
 750  
      * The output will be the same irrespective of the machine that the code is running on.
 751  
      * ie. both Unix and Windows prefixes are matched regardless.
 752  
      *
 753  
      * @param filename  the filename to query, null returns null
 754  
      * @return the prefix of the file, null if invalid
 755  
      */
 756  
     public static String getPrefix(final String filename) {
 757  76
         if (filename == null) {
 758  2
             return null;
 759  
         }
 760  74
         final int len = getPrefixLength(filename);
 761  74
         if (len < 0) {
 762  12
             return null;
 763  
         }
 764  62
         if (len > filename.length()) {
 765  8
             return filename + UNIX_SEPARATOR;  // we know this only happens for unix
 766  
         }
 767  54
         return filename.substring(0, len);
 768  
     }
 769  
 
 770  
     /**
 771  
      * Gets the path from a full filename, which excludes the prefix.
 772  
      * <p>
 773  
      * This method will handle a file in either Unix or Windows format.
 774  
      * The method is entirely text based, and returns the text before and
 775  
      * including the last forward or backslash.
 776  
      * <pre>
 777  
      * C:\a\b\c.txt --&gt; a\b\
 778  
      * ~/a/b/c.txt  --&gt; a/b/
 779  
      * a.txt        --&gt; ""
 780  
      * a/b/c        --&gt; a/b/
 781  
      * a/b/c/       --&gt; a/b/c/
 782  
      * </pre>
 783  
      * <p>
 784  
      * The output will be the same irrespective of the machine that the code is running on.
 785  
      * <p>
 786  
      * This method drops the prefix from the result.
 787  
      * See {@link #getFullPath(String)} for the method that retains the prefix.
 788  
      *
 789  
      * @param filename  the filename to query, null returns null
 790  
      * @return the path of the file, an empty string if none exists, null if invalid
 791  
      */
 792  
     public static String getPath(final String filename) {
 793  64
         return doGetPath(filename, 1);
 794  
     }
 795  
 
 796  
     /**
 797  
      * Gets the path from a full filename, which excludes the prefix, and
 798  
      * also excluding the final directory separator.
 799  
      * <p>
 800  
      * This method will handle a file in either Unix or Windows format.
 801  
      * The method is entirely text based, and returns the text before the
 802  
      * last forward or backslash.
 803  
      * <pre>
 804  
      * C:\a\b\c.txt --&gt; a\b
 805  
      * ~/a/b/c.txt  --&gt; a/b
 806  
      * a.txt        --&gt; ""
 807  
      * a/b/c        --&gt; a/b
 808  
      * a/b/c/       --&gt; a/b/c
 809  
      * </pre>
 810  
      * <p>
 811  
      * The output will be the same irrespective of the machine that the code is running on.
 812  
      * <p>
 813  
      * This method drops the prefix from the result.
 814  
      * See {@link #getFullPathNoEndSeparator(String)} for the method that retains the prefix.
 815  
      *
 816  
      * @param filename  the filename to query, null returns null
 817  
      * @return the path of the file, an empty string if none exists, null if invalid
 818  
      */
 819  
     public static String getPathNoEndSeparator(final String filename) {
 820  56
         return doGetPath(filename, 0);
 821  
     }
 822  
 
 823  
     /**
 824  
      * Does the work of getting the path.
 825  
      *
 826  
      * @param filename  the filename
 827  
      * @param separatorAdd  0 to omit the end separator, 1 to return it
 828  
      * @return the path
 829  
      */
 830  
     private static String doGetPath(final String filename, final int separatorAdd) {
 831  120
         if (filename == null) {
 832  4
             return null;
 833  
         }
 834  116
         final int prefix = getPrefixLength(filename);
 835  116
         if (prefix < 0) {
 836  24
             return null;
 837  
         }
 838  92
         final int index = indexOfLastSeparator(filename);
 839  92
         final int endIndex = index+separatorAdd;
 840  92
         if (prefix >= filename.length() || index < 0 || prefix >= endIndex) {
 841  48
             return "";
 842  
         }
 843  44
         return filename.substring(prefix, endIndex);
 844  
     }
 845  
 
 846  
     /**
 847  
      * Gets the full path from a full filename, which is the prefix + path.
 848  
      * <p>
 849  
      * This method will handle a file in either Unix or Windows format.
 850  
      * The method is entirely text based, and returns the text before and
 851  
      * including the last forward or backslash.
 852  
      * <pre>
 853  
      * C:\a\b\c.txt --&gt; C:\a\b\
 854  
      * ~/a/b/c.txt  --&gt; ~/a/b/
 855  
      * a.txt        --&gt; ""
 856  
      * a/b/c        --&gt; a/b/
 857  
      * a/b/c/       --&gt; a/b/c/
 858  
      * C:           --&gt; C:
 859  
      * C:\          --&gt; C:\
 860  
      * ~            --&gt; ~/
 861  
      * ~/           --&gt; ~/
 862  
      * ~user        --&gt; ~user/
 863  
      * ~user/       --&gt; ~user/
 864  
      * </pre>
 865  
      * <p>
 866  
      * The output will be the same irrespective of the machine that the code is running on.
 867  
      *
 868  
      * @param filename  the filename to query, null returns null
 869  
      * @return the path of the file, an empty string if none exists, null if invalid
 870  
      */
 871  
     public static String getFullPath(final String filename) {
 872  56
         return doGetFullPath(filename, true);
 873  
     }
 874  
 
 875  
     /**
 876  
      * Gets the full path from a full filename, which is the prefix + path,
 877  
      * and also excluding the final directory separator.
 878  
      * <p>
 879  
      * This method will handle a file in either Unix or Windows format.
 880  
      * The method is entirely text based, and returns the text before the
 881  
      * last forward or backslash.
 882  
      * <pre>
 883  
      * C:\a\b\c.txt --&gt; C:\a\b
 884  
      * ~/a/b/c.txt  --&gt; ~/a/b
 885  
      * a.txt        --&gt; ""
 886  
      * a/b/c        --&gt; a/b
 887  
      * a/b/c/       --&gt; a/b/c
 888  
      * C:           --&gt; C:
 889  
      * C:\          --&gt; C:\
 890  
      * ~            --&gt; ~
 891  
      * ~/           --&gt; ~
 892  
      * ~user        --&gt; ~user
 893  
      * ~user/       --&gt; ~user
 894  
      * </pre>
 895  
      * <p>
 896  
      * The output will be the same irrespective of the machine that the code is running on.
 897  
      *
 898  
      * @param filename  the filename to query, null returns null
 899  
      * @return the path of the file, an empty string if none exists, null if invalid
 900  
      */
 901  
     public static String getFullPathNoEndSeparator(final String filename) {
 902  68
         return doGetFullPath(filename, false);
 903  
     }
 904  
 
 905  
     /**
 906  
      * Does the work of getting the path.
 907  
      *
 908  
      * @param filename  the filename
 909  
      * @param includeSeparator  true to include the end separator
 910  
      * @return the path
 911  
      */
 912  
     private static String doGetFullPath(final String filename, final boolean includeSeparator) {
 913  124
         if (filename == null) {
 914  4
             return null;
 915  
         }
 916  120
         final int prefix = getPrefixLength(filename);
 917  120
         if (prefix < 0) {
 918  24
             return null;
 919  
         }
 920  96
         if (prefix >= filename.length()) {
 921  36
             if (includeSeparator) {
 922  16
                 return getPrefix(filename);  // add end slash if necessary
 923  
             } else {
 924  20
                 return filename;
 925  
             }
 926  
         }
 927  60
         final int index = indexOfLastSeparator(filename);
 928  60
         if (index < 0) {
 929  8
             return filename.substring(0, prefix);
 930  
         }
 931  52
         int end = index + (includeSeparator ?  1 : 0);
 932  52
         if (end == 0) {
 933  4
             end++;
 934  
         }
 935  52
         return filename.substring(0, end);
 936  
     }
 937  
 
 938  
     /**
 939  
      * Gets the name minus the path from a full filename.
 940  
      * <p>
 941  
      * This method will handle a file in either Unix or Windows format.
 942  
      * The text after the last forward or backslash is returned.
 943  
      * <pre>
 944  
      * a/b/c.txt --&gt; c.txt
 945  
      * a.txt     --&gt; a.txt
 946  
      * a/b/c     --&gt; c
 947  
      * a/b/c/    --&gt; ""
 948  
      * </pre>
 949  
      * <p>
 950  
      * The output will be the same irrespective of the machine that the code is running on.
 951  
      *
 952  
      * @param filename  the filename to query, null returns null
 953  
      * @return the name of the file without the path, or an empty string if none exists
 954  
      */
 955  
     public static String getName(final String filename) {
 956  26
         if (filename == null) {
 957  4
             return null;
 958  
         }
 959  22
         final int index = indexOfLastSeparator(filename);
 960  22
         return filename.substring(index + 1);
 961  
     }
 962  
 
 963  
     /**
 964  
      * Gets the base name, minus the full path and extension, from a full filename.
 965  
      * <p>
 966  
      * This method will handle a file in either Unix or Windows format.
 967  
      * The text after the last forward or backslash and before the last dot is returned.
 968  
      * <pre>
 969  
      * a/b/c.txt --&gt; c
 970  
      * a.txt     --&gt; a
 971  
      * a/b/c     --&gt; c
 972  
      * a/b/c/    --&gt; ""
 973  
      * </pre>
 974  
      * <p>
 975  
      * The output will be the same irrespective of the machine that the code is running on.
 976  
      *
 977  
      * @param filename  the filename to query, null returns null
 978  
      * @return the name of the file without the path, or an empty string if none exists
 979  
      */
 980  
     public static String getBaseName(final String filename) {
 981  14
         return removeExtension(getName(filename));
 982  
     }
 983  
 
 984  
     /**
 985  
      * Gets the extension of a filename.
 986  
      * <p>
 987  
      * This method returns the textual part of the filename after the last dot.
 988  
      * There must be no directory separator after the dot.
 989  
      * <pre>
 990  
      * foo.txt      --&gt; "txt"
 991  
      * a/b/c.jpg    --&gt; "jpg"
 992  
      * a/b.txt/c    --&gt; ""
 993  
      * a/b/c        --&gt; ""
 994  
      * </pre>
 995  
      * <p>
 996  
      * The output will be the same irrespective of the machine that the code is running on.
 997  
      *
 998  
      * @param filename the filename to retrieve the extension of.
 999  
      * @return the extension of the file or an empty string if none exists or {@code null}
 1000  
      * if the filename is {@code null}.
 1001  
      */
 1002  
     public static String getExtension(final String filename) {
 1003  224
         if (filename == null) {
 1004  2
             return null;
 1005  
         }
 1006  222
         final int index = indexOfExtension(filename);
 1007  222
         if (index == -1) {
 1008  16
             return "";
 1009  
         } else {
 1010  206
             return filename.substring(index + 1);
 1011  
         }
 1012  
     }
 1013  
 
 1014  
     //-----------------------------------------------------------------------
 1015  
     /**
 1016  
      * Removes the extension from a filename.
 1017  
      * <p>
 1018  
      * This method returns the textual part of the filename before the last dot.
 1019  
      * There must be no directory separator after the dot.
 1020  
      * <pre>
 1021  
      * foo.txt    --&gt; foo
 1022  
      * a\b\c.jpg  --&gt; a\b\c
 1023  
      * a\b\c      --&gt; a\b\c
 1024  
      * a.b\c      --&gt; a.b\c
 1025  
      * </pre>
 1026  
      * <p>
 1027  
      * The output will be the same irrespective of the machine that the code is running on.
 1028  
      *
 1029  
      * @param filename  the filename to query, null returns null
 1030  
      * @return the filename minus the extension
 1031  
      */
 1032  
     public static String removeExtension(final String filename) {
 1033  40
         if (filename == null) {
 1034  4
             return null;
 1035  
         }
 1036  36
         final int index = indexOfExtension(filename);
 1037  36
         if (index == -1) {
 1038  18
             return filename;
 1039  
         } else {
 1040  18
             return filename.substring(0, index);
 1041  
         }
 1042  
     }
 1043  
 
 1044  
     //-----------------------------------------------------------------------
 1045  
     /**
 1046  
      * Checks whether two filenames are equal exactly.
 1047  
      * <p>
 1048  
      * No processing is performed on the filenames other than comparison,
 1049  
      * thus this is merely a null-safe case-sensitive equals.
 1050  
      *
 1051  
      * @param filename1  the first filename to query, may be null
 1052  
      * @param filename2  the second filename to query, may be null
 1053  
      * @return true if the filenames are equal, null equals null
 1054  
      * @see IOCase#SENSITIVE
 1055  
      */
 1056  
     public static boolean equals(final String filename1, final String filename2) {
 1057  14
         return equals(filename1, filename2, false, IOCase.SENSITIVE);
 1058  
     }
 1059  
 
 1060  
     /**
 1061  
      * Checks whether two filenames are equal using the case rules of the system.
 1062  
      * <p>
 1063  
      * No processing is performed on the filenames other than comparison.
 1064  
      * The check is case-sensitive on Unix and case-insensitive on Windows.
 1065  
      *
 1066  
      * @param filename1  the first filename to query, may be null
 1067  
      * @param filename2  the second filename to query, may be null
 1068  
      * @return true if the filenames are equal, null equals null
 1069  
      * @see IOCase#SYSTEM
 1070  
      */
 1071  
     public static boolean equalsOnSystem(final String filename1, final String filename2) {
 1072  14
         return equals(filename1, filename2, false, IOCase.SYSTEM);
 1073  
     }
 1074  
 
 1075  
     //-----------------------------------------------------------------------
 1076  
     /**
 1077  
      * Checks whether two filenames are equal after both have been normalized.
 1078  
      * <p>
 1079  
      * Both filenames are first passed to {@link #normalize(String)}.
 1080  
      * The check is then performed in a case-sensitive manner.
 1081  
      *
 1082  
      * @param filename1  the first filename to query, may be null
 1083  
      * @param filename2  the second filename to query, may be null
 1084  
      * @return true if the filenames are equal, null equals null
 1085  
      * @see IOCase#SENSITIVE
 1086  
      */
 1087  
     public static boolean equalsNormalized(final String filename1, final String filename2) {
 1088  16
         return equals(filename1, filename2, true, IOCase.SENSITIVE);
 1089  
     }
 1090  
 
 1091  
     /**
 1092  
      * Checks whether two filenames are equal after both have been normalized
 1093  
      * and using the case rules of the system.
 1094  
      * <p>
 1095  
      * Both filenames are first passed to {@link #normalize(String)}.
 1096  
      * The check is then performed case-sensitive on Unix and
 1097  
      * case-insensitive on Windows.
 1098  
      *
 1099  
      * @param filename1  the first filename to query, may be null
 1100  
      * @param filename2  the second filename to query, may be null
 1101  
      * @return true if the filenames are equal, null equals null
 1102  
      * @see IOCase#SYSTEM
 1103  
      */
 1104  
     public static boolean equalsNormalizedOnSystem(final String filename1, final String filename2) {
 1105  22
         return equals(filename1, filename2, true, IOCase.SYSTEM);
 1106  
     }
 1107  
 
 1108  
     /**
 1109  
      * Checks whether two filenames are equal, optionally normalizing and providing
 1110  
      * control over the case-sensitivity.
 1111  
      *
 1112  
      * @param filename1  the first filename to query, may be null
 1113  
      * @param filename2  the second filename to query, may be null
 1114  
      * @param normalized  whether to normalize the filenames
 1115  
      * @param caseSensitivity  what case sensitivity rule to use, null means case-sensitive
 1116  
      * @return true if the filenames are equal, null equals null
 1117  
      * @since 1.3
 1118  
      */
 1119  
     public static boolean equals(
 1120  
             String filename1, String filename2,
 1121  
             final boolean normalized, IOCase caseSensitivity) {
 1122  
 
 1123  74
         if (filename1 == null || filename2 == null) {
 1124  24
             return filename1 == null && filename2 == null;
 1125  
         }
 1126  50
         if (normalized) {
 1127  34
             filename1 = normalize(filename1);
 1128  34
             filename2 = normalize(filename2);
 1129  34
             if (filename1 == null || filename2 == null) {
 1130  6
                 throw new NullPointerException(
 1131  
                     "Error normalizing one or both of the file names");
 1132  
             }
 1133  
         }
 1134  44
         if (caseSensitivity == null) {
 1135  2
             caseSensitivity = IOCase.SENSITIVE;
 1136  
         }
 1137  44
         return caseSensitivity.checkEquals(filename1, filename2);
 1138  
     }
 1139  
 
 1140  
     //-----------------------------------------------------------------------
 1141  
     /**
 1142  
      * Checks whether the extension of the filename is that specified.
 1143  
      * <p>
 1144  
      * This method obtains the extension as the textual part of the filename
 1145  
      * after the last dot. There must be no directory separator after the dot.
 1146  
      * The extension check is case-sensitive on all platforms.
 1147  
      *
 1148  
      * @param filename  the filename to query, null returns false
 1149  
      * @param extension  the extension to check for, null or empty checks for no extension
 1150  
      * @return true if the filename has the specified extension
 1151  
      */
 1152  
     public static boolean isExtension(final String filename, final String extension) {
 1153  48
         if (filename == null) {
 1154  2
             return false;
 1155  
         }
 1156  46
         if (extension == null || extension.length() == 0) {
 1157  24
             return indexOfExtension(filename) == -1;
 1158  
         }
 1159  22
         final String fileExt = getExtension(filename);
 1160  22
         return fileExt.equals(extension);
 1161  
     }
 1162  
 
 1163  
     /**
 1164  
      * Checks whether the extension of the filename is one of those specified.
 1165  
      * <p>
 1166  
      * This method obtains the extension as the textual part of the filename
 1167  
      * after the last dot. There must be no directory separator after the dot.
 1168  
      * The extension check is case-sensitive on all platforms.
 1169  
      *
 1170  
      * @param filename  the filename to query, null returns false
 1171  
      * @param extensions  the extensions to check for, null checks for no extension
 1172  
      * @return true if the filename is one of the extensions
 1173  
      */
 1174  
     public static boolean isExtension(final String filename, final String[] extensions) {
 1175  60
         if (filename == null) {
 1176  2
             return false;
 1177  
         }
 1178  58
         if (extensions == null || extensions.length == 0) {
 1179  22
             return indexOfExtension(filename) == -1;
 1180  
         }
 1181  36
         final String fileExt = getExtension(filename);
 1182  64
         for (final String extension : extensions) {
 1183  50
             if (fileExt.equals(extension)) {
 1184  22
                 return true;
 1185  
             }
 1186  
         }
 1187  14
         return false;
 1188  
     }
 1189  
 
 1190  
     /**
 1191  
      * Checks whether the extension of the filename is one of those specified.
 1192  
      * <p>
 1193  
      * This method obtains the extension as the textual part of the filename
 1194  
      * after the last dot. There must be no directory separator after the dot.
 1195  
      * The extension check is case-sensitive on all platforms.
 1196  
      *
 1197  
      * @param filename  the filename to query, null returns false
 1198  
      * @param extensions  the extensions to check for, null checks for no extension
 1199  
      * @return true if the filename is one of the extensions
 1200  
      */
 1201  
     public static boolean isExtension(final String filename, final Collection<String> extensions) {
 1202  60
         if (filename == null) {
 1203  2
             return false;
 1204  
         }
 1205  58
         if (extensions == null || extensions.isEmpty()) {
 1206  22
             return indexOfExtension(filename) == -1;
 1207  
         }
 1208  36
         final String fileExt = getExtension(filename);
 1209  36
         for (final String extension : extensions) {
 1210  50
             if (fileExt.equals(extension)) {
 1211  22
                 return true;
 1212  
             }
 1213  28
         }
 1214  14
         return false;
 1215  
     }
 1216  
 
 1217  
     //-----------------------------------------------------------------------
 1218  
     /**
 1219  
      * Checks a filename to see if it matches the specified wildcard matcher,
 1220  
      * always testing case-sensitive.
 1221  
      * <p>
 1222  
      * The wildcard matcher uses the characters '?' and '*' to represent a
 1223  
      * single or multiple (zero or more) wildcard characters.
 1224  
      * This is the same as often found on Dos/Unix command lines.
 1225  
      * The check is case-sensitive always.
 1226  
      * <pre>
 1227  
      * wildcardMatch("c.txt", "*.txt")      --&gt; true
 1228  
      * wildcardMatch("c.txt", "*.jpg")      --&gt; false
 1229  
      * wildcardMatch("a/b/c.txt", "a/b/*")  --&gt; true
 1230  
      * wildcardMatch("c.txt", "*.???")      --&gt; true
 1231  
      * wildcardMatch("c.txt", "*.????")     --&gt; false
 1232  
      * </pre>
 1233  
      * N.B. the sequence "*?" does not work properly at present in match strings.
 1234  
      *
 1235  
      * @param filename  the filename to match on
 1236  
      * @param wildcardMatcher  the wildcard string to match against
 1237  
      * @return true if the filename matches the wilcard string
 1238  
      * @see IOCase#SENSITIVE
 1239  
      */
 1240  
     public static boolean wildcardMatch(final String filename, final String wildcardMatcher) {
 1241  200
         return wildcardMatch(filename, wildcardMatcher, IOCase.SENSITIVE);
 1242  
     }
 1243  
 
 1244  
     /**
 1245  
      * Checks a filename to see if it matches the specified wildcard matcher
 1246  
      * using the case rules of the system.
 1247  
      * <p>
 1248  
      * The wildcard matcher uses the characters '?' and '*' to represent a
 1249  
      * single or multiple (zero or more) wildcard characters.
 1250  
      * This is the same as often found on Dos/Unix command lines.
 1251  
      * The check is case-sensitive on Unix and case-insensitive on Windows.
 1252  
      * <pre>
 1253  
      * wildcardMatch("c.txt", "*.txt")      --&gt; true
 1254  
      * wildcardMatch("c.txt", "*.jpg")      --&gt; false
 1255  
      * wildcardMatch("a/b/c.txt", "a/b/*")  --&gt; true
 1256  
      * wildcardMatch("c.txt", "*.???")      --&gt; true
 1257  
      * wildcardMatch("c.txt", "*.????")     --&gt; false
 1258  
      * </pre>
 1259  
      * N.B. the sequence "*?" does not work properly at present in match strings.
 1260  
      *
 1261  
      * @param filename  the filename to match on
 1262  
      * @param wildcardMatcher  the wildcard string to match against
 1263  
      * @return true if the filename matches the wilcard string
 1264  
      * @see IOCase#SYSTEM
 1265  
      */
 1266  
     public static boolean wildcardMatchOnSystem(final String filename, final String wildcardMatcher) {
 1267  40
         return wildcardMatch(filename, wildcardMatcher, IOCase.SYSTEM);
 1268  
     }
 1269  
 
 1270  
     /**
 1271  
      * Checks a filename to see if it matches the specified wildcard matcher
 1272  
      * allowing control over case-sensitivity.
 1273  
      * <p>
 1274  
      * The wildcard matcher uses the characters '?' and '*' to represent a
 1275  
      * single or multiple (zero or more) wildcard characters.
 1276  
      * N.B. the sequence "*?" does not work properly at present in match strings.
 1277  
      *
 1278  
      * @param filename  the filename to match on
 1279  
      * @param wildcardMatcher  the wildcard string to match against
 1280  
      * @param caseSensitivity  what case sensitivity rule to use, null means case-sensitive
 1281  
      * @return true if the filename matches the wilcard string
 1282  
      * @since 1.3
 1283  
      */
 1284  
     public static boolean wildcardMatch(final String filename, final String wildcardMatcher, IOCase caseSensitivity) {
 1285  2628
         if (filename == null && wildcardMatcher == null) {
 1286  6
             return true;
 1287  
         }
 1288  2622
         if (filename == null || wildcardMatcher == null) {
 1289  12
             return false;
 1290  
         }
 1291  2610
         if (caseSensitivity == null) {
 1292  0
             caseSensitivity = IOCase.SENSITIVE;
 1293  
         }
 1294  2610
         final String[] wcs = splitOnTokens(wildcardMatcher);
 1295  2610
         boolean anyChars = false;
 1296  2610
         int textIdx = 0;
 1297  2610
         int wcsIdx = 0;
 1298  2610
         final Stack<int[]> backtrack = new Stack<int[]>();
 1299  
 
 1300  
         // loop around a backtrack stack, to handle complex * matching
 1301  
         do {
 1302  2630
             if (backtrack.size() > 0) {
 1303  20
                 final int[] array = backtrack.pop();
 1304  20
                 wcsIdx = array[0];
 1305  20
                 textIdx = array[1];
 1306  20
                 anyChars = true;
 1307  
             }
 1308  
 
 1309  
             // loop whilst tokens and text left to process
 1310  5698
             while (wcsIdx < wcs.length) {
 1311  
 
 1312  3238
                 if (wcs[wcsIdx].equals("?")) {
 1313  
                     // ? so move to next text char
 1314  134
                     textIdx++;
 1315  134
                     if (textIdx > filename.length()) {
 1316  18
                         break;
 1317  
                     }
 1318  116
                     anyChars = false;
 1319  
 
 1320  3104
                 } else if (wcs[wcsIdx].equals("*")) {
 1321  
                     // set any chars status
 1322  404
                     anyChars = true;
 1323  404
                     if (wcsIdx == wcs.length - 1) {
 1324  108
                         textIdx = filename.length();
 1325  
                     }
 1326  
 
 1327  
                 } else {
 1328  
                     // matching text token
 1329  2700
                     if (anyChars) {
 1330  
                         // any chars then try to locate text token
 1331  314
                         textIdx = caseSensitivity.checkIndexOf(filename, textIdx, wcs[wcsIdx]);
 1332  314
                         if (textIdx == -1) {
 1333  
                             // token not found
 1334  124
                             break;
 1335  
                         }
 1336  190
                         final int repeat = caseSensitivity.checkIndexOf(filename, textIdx + 1, wcs[wcsIdx]);
 1337  190
                         if (repeat >= 0) {
 1338  22
                             backtrack.push(new int[] {wcsIdx, repeat});
 1339  
                         }
 1340  190
                     } else {
 1341  
                         // matching from current position
 1342  2386
                         if (!caseSensitivity.checkRegionMatches(filename, textIdx, wcs[wcsIdx])) {
 1343  
                             // couldnt match token
 1344  28
                             break;
 1345  
                         }
 1346  
                     }
 1347  
 
 1348  
                     // matched text token, move text index to end of matched token
 1349  2548
                     textIdx += wcs[wcsIdx].length();
 1350  2548
                     anyChars = false;
 1351  
                 }
 1352  
 
 1353  3068
                 wcsIdx++;
 1354  
             }
 1355  
 
 1356  
             // full match
 1357  2630
             if (wcsIdx == wcs.length && textIdx == filename.length()) {
 1358  2444
                 return true;
 1359  
             }
 1360  
 
 1361  186
         } while (backtrack.size() > 0);
 1362  
 
 1363  166
         return false;
 1364  
     }
 1365  
 
 1366  
     /**
 1367  
      * Splits a string into a number of tokens.
 1368  
      * The text is split by '?' and '*'.
 1369  
      * Where multiple '*' occur consecutively they are collapsed into a single '*'.
 1370  
      *
 1371  
      * @param text  the text to split
 1372  
      * @return the array of tokens, never null
 1373  
      */
 1374  
     static String[] splitOnTokens(final String text) {
 1375  
         // used by wildcardMatch
 1376  
         // package level so a unit test may run on this
 1377  
 
 1378  2630
         if (text.indexOf('?') == -1 && text.indexOf('*') == -1) {
 1379  2208
             return new String[] { text };
 1380  
         }
 1381  
 
 1382  422
         final char[] array = text.toCharArray();
 1383  422
         final ArrayList<String> list = new ArrayList<String>();
 1384  422
         final StringBuilder buffer = new StringBuilder();
 1385  422
         char prevChar = 0;
 1386  2558
         for (int i = 0; i < array.length; i++) {
 1387  2136
             final char ch = array[i];
 1388  2136
             if (ch == '?' || ch == '*') {
 1389  628
                 if (buffer.length() != 0) {
 1390  230
                     list.add(buffer.toString());
 1391  230
                     buffer.setLength(0);
 1392  
                 }
 1393  628
                 if (ch == '?') {
 1394  150
                     list.add("?");
 1395  478
                 } else if (prevChar != '*' ) {// ch == '*' here; check if previous char was '*'
 1396  456
                     list.add("*");
 1397  
                 }
 1398  
             } else {
 1399  1508
                 buffer.append(ch);
 1400  
             }
 1401  2136
             prevChar = ch;
 1402  
         }
 1403  422
         if (buffer.length() != 0) {
 1404  254
             list.add(buffer.toString());
 1405  
         }
 1406  
 
 1407  422
         return list.toArray( new String[ list.size() ] );
 1408  
     }
 1409  
 
 1410  
 }