View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  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,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.myfaces.util;
20  
21  import java.io.File;
22  
23  /**
24   * General filename and filepath manipulation utilities.
25   * <p>
26   * When dealing with filenames you can hit problems when moving from a Windows
27   * based development machine to a Unix based production machine.
28   * This class aims to help avoid those problems.
29   * <p>
30   * <b>NOTE</b>: You may be able to avoid using this class entirely simply by
31   * using JDK {@link java.io.File File} objects and the two argument constructor
32   * {@link java.io.File#File(java.io.File, java.lang.String) File(File,String)}.
33   * <p>
34   * Most methods on this class are designed to work the same on both Unix and Windows.
35   * Those that don't include 'System', 'Unix' or 'Windows' in their name.
36   * <p>
37   * Most methods recognise both separators (forward and back), and both
38   * sets of prefixes. See the javadoc of each method for details.
39   * <p>
40   * This class defines six components within a filename
41   * (example C:\dev\project\file.txt):
42   * <ul>
43   * <li>the prefix - C:\</li>
44   * <li>the path - dev\project\</li>
45   * <li>the full path - C:\dev\project\</li>
46   * <li>the name - file.txt</li>
47   * <li>the base name - file</li>
48   * <li>the extension - txt</li>
49   * </ul>
50   * Note that this class works best if directory filenames end with a separator.
51   * If you omit the last separator, it is impossible to determine if the filename
52   * corresponds to a file or a directory. As a result, we have chosen to say
53   * it corresponds to a file.
54   * <p>
55   * This class only supports Unix and Windows style names.
56   * Prefixes are matched as follows:
57   * <pre>
58   * Windows:
59   * a\b\c.txt           --> ""          --> relative
60   * \a\b\c.txt          --> "\"         --> current drive absolute
61   * C:a\b\c.txt         --> "C:"        --> drive relative
62   * C:\a\b\c.txt        --> "C:\"       --> absolute
63   * \\server\a\b\c.txt  --> "\\server\" --> UNC
64   *
65   * Unix:
66   * a/b/c.txt           --> ""          --> relative
67   * /a/b/c.txt          --> "/"         --> absolute
68   * ~/a/b/c.txt         --> "~/"        --> current user
69   * ~                   --> "~/"        --> current user (slash added)
70   * ~user/a/b/c.txt     --> "~user/"    --> named user
71   * ~user               --> "~user/"    --> named user (slash added)
72   * </pre>
73   * Both prefix styles are matched always, irrespective of the machine that you are
74   * currently running on.
75   * <p>
76   * Origin of code: Excalibur, Alexandria, Tomcat, Commons-Utils.
77   * 
78   * NOTE: Taken from Commons-IO package
79   *
80   * @version $Id$
81   * @since 1.1
82   */
83  public class FilenameUtils
84  {
85  
86      /**
87       * The extension separator character.
88       *
89       * @since 1.4
90       */
91      public static final char EXTENSION_SEPARATOR = '.';
92  
93      /**
94       * The extension separator String.
95       *
96       * @since 1.4
97       */
98      public static final String EXTENSION_SEPARATOR_STR = Character.toString(EXTENSION_SEPARATOR);
99  
100     /**
101      * The Unix separator character.
102      */
103     private static final char UNIX_SEPARATOR = '/';
104 
105     /**
106      * The Windows separator character.
107      */
108     private static final char WINDOWS_SEPARATOR = '\\';
109 
110     /**
111      * The system separator character.
112      */
113     private static final char SYSTEM_SEPARATOR = File.separatorChar;
114 
115     /**
116      * The separator character that is the opposite of the system separator.
117      */
118     private static final char OTHER_SEPARATOR;
119 
120     static
121     {
122         if (isSystemWindows())
123         {
124             OTHER_SEPARATOR = UNIX_SEPARATOR;
125         }
126         else
127         {
128             OTHER_SEPARATOR = WINDOWS_SEPARATOR;
129         }
130     }
131 
132     /**
133      * Instances should NOT be constructed in standard programming.
134      */
135     public FilenameUtils()
136     {
137         super();
138     }
139 
140     //-----------------------------------------------------------------------
141     /**
142      * Determines if Windows file system is in use.
143      *
144      * @return true if the system is Windows
145      */
146     static boolean isSystemWindows()
147     {
148         return SYSTEM_SEPARATOR == WINDOWS_SEPARATOR;
149     }
150 
151     //-----------------------------------------------------------------------
152     /**
153      * Checks if the character is a separator.
154      *
155      * @param ch the character to check
156      * @return true if it is a separator character
157      */
158     private static boolean isSeparator(final char ch)
159     {
160         return ch == UNIX_SEPARATOR || ch == WINDOWS_SEPARATOR;
161     }
162 
163     //-----------------------------------------------------------------------
164     /**
165      * Normalizes a path, removing double and single dot path steps.
166      * <p>
167      * This method normalizes a path to a standard format.
168      * The input may contain separators in either Unix or Windows format.
169      * The output will contain separators in the format of the system.
170      * <p>
171      * A trailing slash will be retained.
172      * A double slash will be merged to a single slash (but UNC names are handled).
173      * A single dot path segment will be removed.
174      * A double dot will cause that path segment and the one before to be removed.
175      * If the double dot has no parent path segment to work with, {@code null}
176      * is returned.
177      * <p>
178      * The output will be the same on both Unix and Windows except
179      * for the separator character.
180      * <pre>
181      * /foo//               -->   /foo/
182      * /foo/./              -->   /foo/
183      * /foo/../bar          -->   /bar
184      * /foo/../bar/         -->   /bar/
185      * /foo/../bar/../baz   -->   /baz
186      * //foo//./bar         -->   /foo/bar
187      * /../                 -->   null
188      * ../foo               -->   null
189      * foo/bar/..           -->   foo/
190      * foo/../../bar        -->   null
191      * foo/../bar           -->   bar
192      * //server/foo/../bar  -->   //server/bar
193      * //server/../bar      -->   null
194      * C:\foo\..\bar        -->   C:\bar
195      * C:\..\bar            -->   null
196      * ~/foo/../bar/        -->   ~/bar/
197      * ~/../bar             -->   null
198      * </pre>
199      * (Note the file separator returned will be correct for Windows/Unix)
200      *
201      * @param filename  the filename to normalize, null returns null
202      * @return the normalized filename, or null if invalid
203      */
204     public static String normalize(final String filename)
205     {
206         return doNormalize(filename, SYSTEM_SEPARATOR, true);
207     }
208 
209     /**
210      * Normalizes a path, removing double and single dot path steps.
211      * <p>
212      * This method normalizes a path to a standard format.
213      * The input may contain separators in either Unix or Windows format.
214      * The output will contain separators in the format specified.
215      * <p>
216      * A trailing slash will be retained.
217      * A double slash will be merged to a single slash (but UNC names are handled).
218      * A single dot path segment will be removed.
219      * A double dot will cause that path segment and the one before to be removed.
220      * If the double dot has no parent path segment to work with, {@code null}
221      * is returned.
222      * <p>
223      * The output will be the same on both Unix and Windows except
224      * for the separator character.
225      * <pre>
226      * /foo//               -->   /foo/
227      * /foo/./              -->   /foo/
228      * /foo/../bar          -->   /bar
229      * /foo/../bar/         -->   /bar/
230      * /foo/../bar/../baz   -->   /baz
231      * //foo//./bar         -->   /foo/bar
232      * /../                 -->   null
233      * ../foo               -->   null
234      * foo/bar/..           -->   foo/
235      * foo/../../bar        -->   null
236      * foo/../bar           -->   bar
237      * //server/foo/../bar  -->   //server/bar
238      * //server/../bar      -->   null
239      * C:\foo\..\bar        -->   C:\bar
240      * C:\..\bar            -->   null
241      * ~/foo/../bar/        -->   ~/bar/
242      * ~/../bar             -->   null
243      * </pre>
244      * The output will be the same on both Unix and Windows including
245      * the separator character.
246      *
247      * @param filename  the filename to normalize, null returns null
248      * @param unixSeparator {@code true} if a unix separator should
249      * be used or {@code false} if a windows separator should be used.
250      * @return the normalized filename, or null if invalid
251      * @since 2.0
252      */
253     public static String normalize(final String filename, final boolean unixSeparator)
254     {
255         final char separator = unixSeparator ? UNIX_SEPARATOR : WINDOWS_SEPARATOR;
256         return doNormalize(filename, separator, true);
257     }
258 
259     //-----------------------------------------------------------------------
260     /**
261      * Normalizes a path, removing double and single dot path steps,
262      * and removing any final directory separator.
263      * <p>
264      * This method normalizes a path to a standard format.
265      * The input may contain separators in either Unix or Windows format.
266      * The output will contain separators in the format of the system.
267      * <p>
268      * A trailing slash will be removed.
269      * A double slash will be merged to a single slash (but UNC names are handled).
270      * A single dot path segment will be removed.
271      * A double dot will cause that path segment and the one before to be removed.
272      * If the double dot has no parent path segment to work with, {@code null}
273      * is returned.
274      * <p>
275      * The output will be the same on both Unix and Windows except
276      * for the separator character.
277      * <pre>
278      * /foo//               -->   /foo
279      * /foo/./              -->   /foo
280      * /foo/../bar          -->   /bar
281      * /foo/../bar/         -->   /bar
282      * /foo/../bar/../baz   -->   /baz
283      * //foo//./bar         -->   /foo/bar
284      * /../                 -->   null
285      * ../foo               -->   null
286      * foo/bar/..           -->   foo
287      * foo/../../bar        -->   null
288      * foo/../bar           -->   bar
289      * //server/foo/../bar  -->   //server/bar
290      * //server/../bar      -->   null
291      * C:\foo\..\bar        -->   C:\bar
292      * C:\..\bar            -->   null
293      * ~/foo/../bar/        -->   ~/bar
294      * ~/../bar             -->   null
295      * </pre>
296      * (Note the file separator returned will be correct for Windows/Unix)
297      *
298      * @param filename  the filename to normalize, null returns null
299      * @return the normalized filename, or null if invalid
300      */
301     public static String normalizeNoEndSeparator(final String filename)
302     {
303         return doNormalize(filename, SYSTEM_SEPARATOR, false);
304     }
305 
306     /**
307      * Normalizes a path, removing double and single dot path steps,
308      * and removing any final directory separator.
309      * <p>
310      * This method normalizes a path to a standard format.
311      * The input may contain separators in either Unix or Windows format.
312      * The output will contain separators in the format specified.
313      * <p>
314      * A trailing slash will be removed.
315      * A double slash will be merged to a single slash (but UNC names are handled).
316      * A single dot path segment will be removed.
317      * A double dot will cause that path segment and the one before to be removed.
318      * If the double dot has no parent path segment to work with, {@code null}
319      * is returned.
320      * <p>
321      * The output will be the same on both Unix and Windows including
322      * the separator character.
323      * <pre>
324      * /foo//               -->   /foo
325      * /foo/./              -->   /foo
326      * /foo/../bar          -->   /bar
327      * /foo/../bar/         -->   /bar
328      * /foo/../bar/../baz   -->   /baz
329      * //foo//./bar         -->   /foo/bar
330      * /../                 -->   null
331      * ../foo               -->   null
332      * foo/bar/..           -->   foo
333      * foo/../../bar        -->   null
334      * foo/../bar           -->   bar
335      * //server/foo/../bar  -->   //server/bar
336      * //server/../bar      -->   null
337      * C:\foo\..\bar        -->   C:\bar
338      * C:\..\bar            -->   null
339      * ~/foo/../bar/        -->   ~/bar
340      * ~/../bar             -->   null
341      * </pre>
342      *
343      * @param filename  the filename to normalize, null returns null
344      * @param unixSeparator {@code true} if a unix separator should
345      * be used or {@code false} if a windows separtor should be used.
346      * @return the normalized filename, or null if invalid
347      * @since 2.0
348      */
349     public static String normalizeNoEndSeparator(final String filename, final boolean unixSeparator)
350     {
351         final char separator = unixSeparator ? UNIX_SEPARATOR : WINDOWS_SEPARATOR;
352         return doNormalize(filename, separator, false);
353     }
354 
355     /**
356      * Internal method to perform the normalization.
357      *
358      * @param filename  the filename
359      * @param separator The separator character to use
360      * @param keepSeparator  true to keep the final separator
361      * @return the normalized filename
362      */
363     private static String doNormalize(final String filename, final char separator, final boolean keepSeparator)
364     {
365         if (filename == null)
366         {
367             return null;
368         }
369         int size = filename.length();
370         if (size == 0)
371         {
372             return filename;
373         }
374         final int prefix = getPrefixLength(filename);
375         if (prefix < 0)
376         {
377             return null;
378         }
379 
380         final char[] array = new char[size + 2];  // +1 for possible extra slash, +2 for arraycopy
381         filename.getChars(0, filename.length(), array, 0);
382 
383         // fix separators throughout
384         final char otherSeparator = separator == SYSTEM_SEPARATOR ? OTHER_SEPARATOR : SYSTEM_SEPARATOR;
385         for (int i = 0; i < array.length; i++)
386         {
387             if (array[i] == otherSeparator)
388             {
389                 array[i] = separator;
390             }
391         }
392 
393         // add extra separator on the end to simplify code below
394         boolean lastIsDirectory = true;
395         if (array[size - 1] != separator)
396         {
397             array[size++] = separator;
398             lastIsDirectory = false;
399         }
400 
401         // adjoining slashes
402         for (int i = prefix + 1; i < size; i++)
403         {
404             if (array[i] == separator && array[i - 1] == separator)
405             {
406                 System.arraycopy(array, i, array, i - 1, size - i);
407                 size--;
408                 i--;
409             }
410         }
411 
412         // dot slash
413         for (int i = prefix + 1; i < size; i++)
414         {
415             if (array[i] == separator && array[i - 1] == '.'
416                     && (i == prefix + 1 || array[i - 2] == separator))
417             {
418                 if (i == size - 1)
419                 {
420                     lastIsDirectory = true;
421                 }
422                 System.arraycopy(array, i + 1, array, i - 1, size - i);
423                 size -= 2;
424                 i--;
425             }
426         }
427 
428         // double dot slash
429         outer:
430         for (int i = prefix + 2; i < size; i++)
431         {
432             if (array[i] == separator && array[i - 1] == '.' && array[i - 2] == '.'
433                     && (i == prefix + 2 || array[i - 3] == separator))
434             {
435                 if (i == prefix + 2)
436                 {
437                     return null;
438                 }
439                 if (i == size - 1)
440                 {
441                     lastIsDirectory = true;
442                 }
443                 int j;
444                 for (j = i - 4; j >= prefix; j--)
445                 {
446                     if (array[j] == separator)
447                     {
448                         // remove b/../ from a/b/../c
449                         System.arraycopy(array, i + 1, array, j + 1, size - i);
450                         size -= i - j;
451                         i = j + 1;
452                         continue outer;
453                     }
454                 }
455                 // remove a/../ from a/../c
456                 System.arraycopy(array, i + 1, array, prefix, size - i);
457                 size -= i + 1 - prefix;
458                 i = prefix + 1;
459             }
460         }
461 
462         if (size <= 0)
463         {  // should never be less than 0
464             return "";
465         }
466         if (size <= prefix)
467         {  // should never be less than prefix
468             return new String(array, 0, size);
469         }
470         if (lastIsDirectory && keepSeparator)
471         {
472             return new String(array, 0, size);  // keep trailing separator
473         }
474         return new String(array, 0, size - 1);  // lose trailing separator
475     }
476 
477     //-----------------------------------------------------------------------
478     /**
479      * Concatenates a filename to a base path using normal command line style rules.
480      * <p>
481      * The effect is equivalent to resultant directory after changing
482      * directory to the first argument, followed by changing directory to
483      * the second argument.
484      * <p>
485      * The first argument is the base path, the second is the path to concatenate.
486      * The returned path is always normalized via {@link #normalize(String)},
487      * thus <code>..</code> is handled.
488      * <p>
489      * If <code>pathToAdd</code> is absolute (has an absolute prefix), then
490      * it will be normalized and returned.
491      * Otherwise, the paths will be joined, normalized and returned.
492      * <p>
493      * The output will be the same on both Unix and Windows except
494      * for the separator character.
495      * <pre>
496      * /foo/ + bar          -->   /foo/bar
497      * /foo + bar           -->   /foo/bar
498      * /foo + /bar          -->   /bar
499      * /foo + C:/bar        -->   C:/bar
500      * /foo + C:bar         -->   C:bar (*)
501      * /foo/a/ + ../bar     -->   foo/bar
502      * /foo/ + ../../bar    -->   null
503      * /foo/ + /bar         -->   /bar
504      * /foo/.. + /bar       -->   /bar
505      * /foo + bar/c.txt     -->   /foo/bar/c.txt
506      * /foo/c.txt + bar     -->   /foo/c.txt/bar (!)
507      * </pre>
508      * (*) Note that the Windows relative drive prefix is unreliable when
509      * used with this method.
510      * (!) Note that the first parameter must be a path. If it ends with a name, then
511      * the name will be built into the concatenated path. If this might be a problem,
512      * use {@link #getFullPath(String)} on the base path argument.
513      *
514      * @param basePath  the base path to attach to, always treated as a path
515      * @param fullFilenameToAdd  the filename (or path) to attach to the base
516      * @return the concatenated path, or null if invalid
517      */
518     public static String concat(final String basePath, final String fullFilenameToAdd)
519     {
520         final int prefix = getPrefixLength(fullFilenameToAdd);
521         if (prefix < 0)
522         {
523             return null;
524         }
525         if (prefix > 0)
526         {
527             return normalize(fullFilenameToAdd);
528         }
529         if (basePath == null)
530         {
531             return null;
532         }
533         final int len = basePath.length();
534         if (len == 0)
535         {
536             return normalize(fullFilenameToAdd);
537         }
538         final char ch = basePath.charAt(len - 1);
539         if (isSeparator(ch))
540         {
541             return normalize(basePath + fullFilenameToAdd);
542         }
543         else
544         {
545             return normalize(basePath + '/' + fullFilenameToAdd);
546         }
547     }
548 
549     //-----------------------------------------------------------------------
550     /**
551      * Converts all separators to the Unix separator of forward slash.
552      *
553      * @param path the path to be changed, null ignored
554      * @return the updated path
555      */
556     public static String separatorsToUnix(final String path)
557     {
558         if (path == null || path.indexOf(WINDOWS_SEPARATOR) == -1)
559         {
560             return path;
561         }
562         return path.replace(WINDOWS_SEPARATOR, UNIX_SEPARATOR);
563     }
564 
565     /**
566      * Converts all separators to the Windows separator of backslash.
567      *
568      * @param path the path to be changed, null ignored
569      * @return the updated path
570      */
571     public static String separatorsToWindows(final String path)
572     {
573         if (path == null || path.indexOf(UNIX_SEPARATOR) == -1)
574         {
575             return path;
576         }
577         return path.replace(UNIX_SEPARATOR, WINDOWS_SEPARATOR);
578     }
579 
580     /**
581      * Converts all separators to the system separator.
582      *
583      * @param path the path to be changed, null ignored
584      * @return the updated path
585      */
586     public static String separatorsToSystem(final String path)
587     {
588         if (path == null)
589         {
590             return null;
591         }
592         if (isSystemWindows())
593         {
594             return separatorsToWindows(path);
595         }
596         else
597         {
598             return separatorsToUnix(path);
599         }
600     }
601 
602     //-----------------------------------------------------------------------
603     /**
604      * Returns the length of the filename prefix, such as <code>C:/</code> or <code>~/</code>.
605      * <p>
606      * This method will handle a file in either Unix or Windows format.
607      * <p>
608      * The prefix length includes the first slash in the full filename
609      * if applicable. Thus, it is possible that the length returned is greater
610      * than the length of the input string.
611      * <pre>
612      * Windows:
613      * a\b\c.txt           --> ""          --> relative
614      * \a\b\c.txt          --> "\"         --> current drive absolute
615      * C:a\b\c.txt         --> "C:"        --> drive relative
616      * C:\a\b\c.txt        --> "C:\"       --> absolute
617      * \\server\a\b\c.txt  --> "\\server\" --> UNC
618      * \\\a\b\c.txt        -->  error, length = -1
619      *
620      * Unix:
621      * a/b/c.txt           --> ""          --> relative
622      * /a/b/c.txt          --> "/"         --> absolute
623      * ~/a/b/c.txt         --> "~/"        --> current user
624      * ~                   --> "~/"        --> current user (slash added)
625      * ~user/a/b/c.txt     --> "~user/"    --> named user
626      * ~user               --> "~user/"    --> named user (slash added)
627      * //server/a/b/c.txt  --> "//server/"
628      * ///a/b/c.txt        --> error, length = -1
629      * </pre>
630      * <p>
631      * The output will be the same irrespective of the machine that the code is running on.
632      * ie. both Unix and Windows prefixes are matched regardless.
633      *
634      * Note that a leading // (or \\) is used to indicate a UNC name on Windows.
635      * These must be followed by a server name, so double-slashes are not collapsed
636      * to a single slash at the start of the filename.
637      *
638      * @param filename  the filename to find the prefix in, null returns -1
639      * @return the length of the prefix, -1 if invalid or null
640      */
641     public static int getPrefixLength(final String filename)
642     {
643         if (filename == null)
644         {
645             return -1;
646         }
647         final int len = filename.length();
648         if (len == 0)
649         {
650             return 0;
651         }
652         char ch0 = filename.charAt(0);
653         if (ch0 == ':')
654         {
655             return -1;
656         }
657         if (len == 1)
658         {
659             if (ch0 == '~')
660             {
661                 return 2;  // return a length greater than the input
662             }
663             return isSeparator(ch0) ? 1 : 0;
664         }
665         else
666         {
667             if (ch0 == '~')
668             {
669                 int posUnix = filename.indexOf(UNIX_SEPARATOR, 1);
670                 int posWin = filename.indexOf(WINDOWS_SEPARATOR, 1);
671                 if (posUnix == -1 && posWin == -1)
672                 {
673                     return len + 1;  // return a length greater than the input
674                 }
675                 posUnix = posUnix == -1 ? posWin : posUnix;
676                 posWin = posWin == -1 ? posUnix : posWin;
677                 return Math.min(posUnix, posWin) + 1;
678             }
679             final char ch1 = filename.charAt(1);
680             if (ch1 == ':')
681             {
682                 ch0 = Character.toUpperCase(ch0);
683                 if (ch0 >= 'A' && ch0 <= 'Z')
684                 {
685                     if (len == 2 || isSeparator(filename.charAt(2)) == false)
686                     {
687                         return 2;
688                     }
689                     return 3;
690                 }
691                 return -1;
692 
693             }
694             else if (isSeparator(ch0) && isSeparator(ch1))
695             {
696                 int posUnix = filename.indexOf(UNIX_SEPARATOR, 2);
697                 int posWin = filename.indexOf(WINDOWS_SEPARATOR, 2);
698                 if (posUnix == -1 && posWin == -1 || posUnix == 2 || posWin == 2)
699                 {
700                     return -1;
701                 }
702                 posUnix = posUnix == -1 ? posWin : posUnix;
703                 posWin = posWin == -1 ? posUnix : posWin;
704                 return Math.min(posUnix, posWin) + 1;
705             }
706             else
707             {
708                 return isSeparator(ch0) ? 1 : 0;
709             }
710         }
711     }
712 
713     /**
714      * Returns the index of the last directory separator character.
715      * <p>
716      * This method will handle a file in either Unix or Windows format.
717      * The position of the last forward or backslash is returned.
718      * <p>
719      * The output will be the same irrespective of the machine that the code is running on.
720      *
721      * @param filename  the filename to find the last path separator in, null returns -1
722      * @return the index of the last separator character, or -1 if there
723      * is no such character
724      */
725     public static int indexOfLastSeparator(final String filename)
726     {
727         if (filename == null)
728         {
729             return -1;
730         }
731         final int lastUnixPos = filename.lastIndexOf(UNIX_SEPARATOR);
732         final int lastWindowsPos = filename.lastIndexOf(WINDOWS_SEPARATOR);
733         return Math.max(lastUnixPos, lastWindowsPos);
734     }
735 
736     /**
737      * Returns the index of the last extension separator character, which is a dot.
738      * <p>
739      * This method also checks that there is no directory separator after the last dot.
740      * To do this it uses {@link #indexOfLastSeparator(String)} which will
741      * handle a file in either Unix or Windows format.
742      * <p>
743      * The output will be the same irrespective of the machine that the code is running on.
744      *
745      * @param filename  the filename to find the last path separator in, null returns -1
746      * @return the index of the last separator character, or -1 if there
747      * is no such character
748      */
749     public static int indexOfExtension(final String filename)
750     {
751         if (filename == null)
752         {
753             return -1;
754         }
755         final int extensionPos = filename.lastIndexOf(EXTENSION_SEPARATOR);
756         final int lastSeparator = indexOfLastSeparator(filename);
757         return lastSeparator > extensionPos ? -1 : extensionPos;
758     }
759 
760     //-----------------------------------------------------------------------
761     /**
762      * Gets the prefix from a full filename, such as <code>C:/</code>
763      * or <code>~/</code>.
764      * <p>
765      * This method will handle a file in either Unix or Windows format.
766      * The prefix includes the first slash in the full filename where applicable.
767      * <pre>
768      * Windows:
769      * a\b\c.txt           --> ""          --> relative
770      * \a\b\c.txt          --> "\"         --> current drive absolute
771      * C:a\b\c.txt         --> "C:"        --> drive relative
772      * C:\a\b\c.txt        --> "C:\"       --> absolute
773      * \\server\a\b\c.txt  --> "\\server\" --> UNC
774      *
775      * Unix:
776      * a/b/c.txt           --> ""          --> relative
777      * /a/b/c.txt          --> "/"         --> absolute
778      * ~/a/b/c.txt         --> "~/"        --> current user
779      * ~                   --> "~/"        --> current user (slash added)
780      * ~user/a/b/c.txt     --> "~user/"    --> named user
781      * ~user               --> "~user/"    --> named user (slash added)
782      * </pre>
783      * <p>
784      * The output will be the same irrespective of the machine that the code is running on.
785      * ie. both Unix and Windows prefixes are matched regardless.
786      *
787      * @param filename  the filename to query, null returns null
788      * @return the prefix of the file, null if invalid
789      */
790     public static String getPrefix(final String filename)
791     {
792         if (filename == null)
793         {
794             return null;
795         }
796         final int len = getPrefixLength(filename);
797         if (len < 0)
798         {
799             return null;
800         }
801         if (len > filename.length())
802         {
803             return filename + UNIX_SEPARATOR;  // we know this only happens for unix
804         }
805         return filename.substring(0, len);
806     }
807 
808     /**
809      * Gets the path from a full filename, which excludes the prefix.
810      * <p>
811      * This method will handle a file in either Unix or Windows format.
812      * The method is entirely text based, and returns the text before and
813      * including the last forward or backslash.
814      * <pre>
815      * C:\a\b\c.txt --> a\b\
816      * ~/a/b/c.txt  --> a/b/
817      * a.txt        --> ""
818      * a/b/c        --> a/b/
819      * a/b/c/       --> a/b/c/
820      * </pre>
821      * <p>
822      * The output will be the same irrespective of the machine that the code is running on.
823      * <p>
824      * This method drops the prefix from the result.
825      * See {@link #getFullPath(String)} for the method that retains the prefix.
826      *
827      * @param filename  the filename to query, null returns null
828      * @return the path of the file, an empty string if none exists, null if invalid
829      */
830     public static String getPath(final String filename)
831     {
832         return doGetPath(filename, 1);
833     }
834 
835     /**
836      * Gets the path from a full filename, which excludes the prefix, and
837      * also excluding the final directory separator.
838      * <p>
839      * This method will handle a file in either Unix or Windows format.
840      * The method is entirely text based, and returns the text before the
841      * last forward or backslash.
842      * <pre>
843      * C:\a\b\c.txt --> a\b
844      * ~/a/b/c.txt  --> a/b
845      * a.txt        --> ""
846      * a/b/c        --> a/b
847      * a/b/c/       --> a/b/c
848      * </pre>
849      * <p>
850      * The output will be the same irrespective of the machine that the code is running on.
851      * <p>
852      * This method drops the prefix from the result.
853      * See {@link #getFullPathNoEndSeparator(String)} for the method that retains the prefix.
854      *
855      * @param filename  the filename to query, null returns null
856      * @return the path of the file, an empty string if none exists, null if invalid
857      */
858     public static String getPathNoEndSeparator(final String filename)
859     {
860         return doGetPath(filename, 0);
861     }
862 
863     /**
864      * Does the work of getting the path.
865      *
866      * @param filename  the filename
867      * @param separatorAdd  0 to omit the end separator, 1 to return it
868      * @return the path
869      */
870     private static String doGetPath(final String filename, final int separatorAdd)
871     {
872         if (filename == null)
873         {
874             return null;
875         }
876         final int prefix = getPrefixLength(filename);
877         if (prefix < 0)
878         {
879             return null;
880         }
881         final int index = indexOfLastSeparator(filename);
882         final int endIndex = index + separatorAdd;
883         if (prefix >= filename.length() || index < 0 || prefix >= endIndex)
884         {
885             return "";
886         }
887         return filename.substring(prefix, endIndex);
888     }
889 
890     /**
891      * Gets the full path from a full filename, which is the prefix + path.
892      * <p>
893      * This method will handle a file in either Unix or Windows format.
894      * The method is entirely text based, and returns the text before and
895      * including the last forward or backslash.
896      * <pre>
897      * C:\a\b\c.txt --> C:\a\b\
898      * ~/a/b/c.txt  --> ~/a/b/
899      * a.txt        --> ""
900      * a/b/c        --> a/b/
901      * a/b/c/       --> a/b/c/
902      * C:           --> C:
903      * C:\          --> C:\
904      * ~            --> ~/
905      * ~/           --> ~/
906      * ~user        --> ~user/
907      * ~user/       --> ~user/
908      * </pre>
909      * <p>
910      * The output will be the same irrespective of the machine that the code is running on.
911      *
912      * @param filename  the filename to query, null returns null
913      * @return the path of the file, an empty string if none exists, null if invalid
914      */
915     public static String getFullPath(final String filename)
916     {
917         return doGetFullPath(filename, true);
918     }
919 
920     /**
921      * Gets the full path from a full filename, which is the prefix + path,
922      * and also excluding the final directory separator.
923      * <p>
924      * This method will handle a file in either Unix or Windows format.
925      * The method is entirely text based, and returns the text before the
926      * last forward or backslash.
927      * <pre>
928      * C:\a\b\c.txt --> C:\a\b
929      * ~/a/b/c.txt  --> ~/a/b
930      * a.txt        --> ""
931      * a/b/c        --> a/b
932      * a/b/c/       --> a/b/c
933      * C:           --> C:
934      * C:\          --> C:\
935      * ~            --> ~
936      * ~/           --> ~
937      * ~user        --> ~user
938      * ~user/       --> ~user
939      * </pre>
940      * <p>
941      * The output will be the same irrespective of the machine that the code is running on.
942      *
943      * @param filename  the filename to query, null returns null
944      * @return the path of the file, an empty string if none exists, null if invalid
945      */
946     public static String getFullPathNoEndSeparator(final String filename)
947     {
948         return doGetFullPath(filename, false);
949     }
950 
951     /**
952      * Does the work of getting the path.
953      *
954      * @param filename the filename
955      * @param includeSeparator true to include the end separator
956      * @return the path
957      */
958     private static String doGetFullPath(final String filename, final boolean includeSeparator)
959     {
960         if (filename == null)
961         {
962             return null;
963         }
964         final int prefix = getPrefixLength(filename);
965         if (prefix < 0)
966         {
967             return null;
968         }
969         if (prefix >= filename.length())
970         {
971             if (includeSeparator)
972             {
973                 return getPrefix(filename);  // add end slash if necessary
974             }
975             else
976             {
977                 return filename;
978             }
979         }
980         final int index = indexOfLastSeparator(filename);
981         if (index < 0)
982         {
983             return filename.substring(0, prefix);
984         }
985         int end = index + (includeSeparator ? 1 : 0);
986         if (end == 0)
987         {
988             end++;
989         }
990         return filename.substring(0, end);
991     }
992 
993     /**
994      * Gets the name minus the path from a full filename.
995      * <p>
996      * This method will handle a file in either Unix or Windows format.
997      * The text after the last forward or backslash is returned.
998      * <pre>
999      * a/b/c.txt --> c.txt
1000      * a.txt     --> a.txt
1001      * a/b/c     --> c
1002      * a/b/c/    --> ""
1003      * </pre>
1004      * <p>
1005      * The output will be the same irrespective of the machine that the code is running on.
1006      *
1007      * @param filename  the filename to query, null returns null
1008      * @return the name of the file without the path, or an empty string if none exists
1009      */
1010     public static String getName(final String filename)
1011     {
1012         if (filename == null)
1013         {
1014             return null;
1015         }
1016         final int index = indexOfLastSeparator(filename);
1017         return filename.substring(index + 1);
1018     }
1019 
1020     /**
1021      * Gets the base name, minus the full path and extension, from a full filename.
1022      * <p>
1023      * This method will handle a file in either Unix or Windows format.
1024      * The text after the last forward or backslash and before the last dot is returned.
1025      * <pre>
1026      * a/b/c.txt --> c
1027      * a.txt     --> a
1028      * a/b/c     --> c
1029      * a/b/c/    --> ""
1030      * </pre>
1031      * <p>
1032      * The output will be the same irrespective of the machine that the code is running on.
1033      *
1034      * @param filename  the filename to query, null returns null
1035      * @return the name of the file without the path, or an empty string if none exists
1036      */
1037     public static String getBaseName(final String filename)
1038     {
1039         return removeExtension(getName(filename));
1040     }
1041 
1042     /**
1043      * Gets the extension of a filename.
1044      * <p>
1045      * This method returns the textual part of the filename after the last dot.
1046      * There must be no directory separator after the dot.
1047      * <pre>
1048      * foo.txt      --> "txt"
1049      * a/b/c.jpg    --> "jpg"
1050      * a/b.txt/c    --> ""
1051      * a/b/c        --> ""
1052      * </pre>
1053      * <p>
1054      * The output will be the same irrespective of the machine that the code is running on.
1055      *
1056      * @param filename the filename to retrieve the extension of.
1057      * @return the extension of the file or an empty string if none exists or {@code null}
1058      * if the filename is {@code null}.
1059      */
1060     public static String getExtension(final String filename)
1061     {
1062         if (filename == null)
1063         {
1064             return null;
1065         }
1066         final int index = indexOfExtension(filename);
1067         if (index == -1)
1068         {
1069             return "";
1070         }
1071         else
1072         {
1073             return filename.substring(index + 1);
1074         }
1075     }
1076 
1077     //-----------------------------------------------------------------------
1078     /**
1079      * Removes the extension from a filename.
1080      * <p>
1081      * This method returns the textual part of the filename before the last dot.
1082      * There must be no directory separator after the dot.
1083      * <pre>
1084      * foo.txt    --> foo
1085      * a\b\c.jpg  --> a\b\c
1086      * a\b\c      --> a\b\c
1087      * a.b\c      --> a.b\c
1088      * </pre>
1089      * <p>
1090      * The output will be the same irrespective of the machine that the code is running on.
1091      *
1092      * @param filename  the filename to query, null returns null
1093      * @return the filename minus the extension
1094      */
1095     public static String removeExtension(final String filename)
1096     {
1097         if (filename == null)
1098         {
1099             return null;
1100         }
1101         final int index = indexOfExtension(filename);
1102         if (index == -1)
1103         {
1104             return filename;
1105         }
1106         else
1107         {
1108             return filename.substring(0, index);
1109         }
1110     }
1111 
1112 }