View Javadoc
1   package org.andromda.core.common;
2   
3   import java.io.File;
4   import java.io.FileNotFoundException;
5   import java.io.IOException;
6   import java.io.InputStream;
7   import java.io.Reader;
8   import java.net.MalformedURLException;
9   import java.net.URL;
10  import java.net.URLConnection;
11  import java.net.URLDecoder;
12  import java.util.ArrayList;
13  import java.util.Collection;
14  import java.util.Collections;
15  import java.util.Enumeration;
16  import java.util.List;
17  import java.util.ListIterator;
18  import java.util.zip.ZipEntry;
19  import java.util.zip.ZipFile;
20  
21  import org.apache.commons.io.FileUtils;
22  import org.apache.commons.io.IOUtils;
23  import org.apache.commons.io.filefilter.TrueFileFilter;
24  import org.apache.commons.lang.StringUtils;
25  import org.apache.log4j.Logger;
26  
27  /**
28   * Provides utilities for loading resources.
29   *
30   * @author Chad Brandon
31   * @author Bob Fields
32   * @author Michail Plushnikov
33   */
34  public class ResourceUtils
35  {
36      private static final Logger logger = Logger.getLogger(ResourceUtils.class);
37  
38      /**
39       * All archive files start with this prefix.
40       */
41      private static final String ARCHIVE_PREFIX = "jar:";
42  
43      /**
44       * The prefix for URL file resources.
45       */
46      private static final String FILE_PREFIX = "file:";
47  
48      /**
49       * The prefix to use for searching the classpath for a resource.
50       */
51      private static final String CLASSPATH_PREFIX = "classpath:";
52  
53      /**
54       * Retrieves a resource from the current classpath.
55       *
56       * @param resourceName the name of the resource
57       * @return the resource url
58       */
59      public static URL getResource(final String resourceName)
60      {
61          ExceptionUtils.checkEmpty(
62              "resourceName",
63              resourceName);
64          final ClassLoader loader = ClassUtils.getClassLoader();
65          URL resource = loader != null ? loader.getResource(resourceName) : null;
66          //if (resource==null)
67          return resource;
68      }
69  
70      /**
71       * Loads the resource and returns the contents as a String.
72       *
73       * @param resource the name of the resource.
74       * @return String
75       */
76      public static String getContents(final URL resource)
77      {
78          String result = null;
79  
80          InputStream in = null;
81          try
82          {
83              if(null!=resource)
84              {
85                  in = resource.openStream();
86                  result = IOUtils.toString(in);
87              }
88          }
89          catch (final IOException ex) {
90              throw new RuntimeException(ex);
91          }finally {
92              IOUtils.closeQuietly(in);
93          }
94          return result;
95      }
96  
97      /**
98       * Loads the resource and returns the contents as a String.
99       *
100      * @param resource the name of the resource.
101      * @return the contents of the resource as a string.
102      */
103     public static String getContents(final Reader resource)
104     {
105         String result;
106         try {
107             result = IOUtils.toString(resource);
108         }catch (IOException ex) {
109             throw new RuntimeException(ex);
110         }finally {
111             IOUtils.closeQuietly(resource);
112         }
113         return StringUtils.trimToEmpty(result);
114     }
115 
116     /**
117      * If the <code>resource</code> represents a classpath archive (i.e. jar, zip, etc), this method will retrieve all
118      * contents from that resource as a List of relative paths (relative to the archive base). Otherwise an empty List
119      * will be returned.
120      *
121      * @param resource the resource from which to retrieve the contents
122      * @return a list of Strings containing the names of every nested resource found in this resource.
123      */
124     public static List<String> getClassPathArchiveContents(final URL resource)
125     {
126         final List<String> contents = new ArrayList<String>();
127         if (isArchive(resource))
128         {
129             final ZipFile archive = getArchive(resource);
130             if (archive != null)
131             {
132                 for (final Enumeration<? extends ZipEntry> entries = archive.entries(); entries.hasMoreElements();)
133                 {
134                     final ZipEntry entry = entries.nextElement();
135                     contents.add(entry.getName());
136                 }
137                 try
138                 {
139                     archive.close();
140                 }
141                 catch (IOException ex)
142                 {
143                     // Ignore
144                 }
145             }
146         }
147         return contents;
148     }
149 
150     /**
151      * If this <code>resource</code> happens to be a directory, it will load the contents of that directory into a
152      * List and return the list of names relative to the given <code>resource</code> (otherwise it will return an empty
153      * List).
154      *
155      * @param resource the resource from which to retrieve the contents
156      * @param levels the number of levels to step down if the resource ends up being a directory (if its an artifact,
157      *               levels will be ignored).
158      * @return a list of Strings containing the names of every nested resource found in this resource.
159      */
160     public static List<String> getDirectoryContents(
161         final URL resource,
162         final int levels)
163     {
164         return getDirectoryContents(resource, levels, true);
165     }
166 
167     /**
168      * The character used for substituting whitespace in paths.
169      */
170     private static final String PATH_WHITESPACE_CHARACTER = "%20";
171 
172     /**
173      * Replaces any escape characters in the given file path with their
174      * counterparts.
175      *
176      * @param filePath the path of the file to unescape.
177      * @return the unescaped path.
178      */
179     public static String unescapeFilePath(String filePath)
180     {
181         if (StringUtils.isNotBlank(filePath))
182         {
183             filePath = filePath.replaceAll(PATH_WHITESPACE_CHARACTER, " ");
184         }
185         return filePath;
186     }
187 
188     /**
189      * If this <code>resource</code> happens to be a directory, it will load the contents of that directory into a
190      * List and return the list of names relative to the given <code>resource</code> (otherwise it will return an empty
191      * List).
192      *
193      * @param resource the resource from which to retrieve the contents
194      * @param levels the number of levels to step down if the resource ends up being a directory (if its an artifact,
195      *               levels will be ignored).
196      * @param includeSubdirectories whether or not to include subdirectories in the contents.
197      * @return a list of Strings containing the names of every nested resource found in this resource.
198      */
199     public static List<String> getDirectoryContents(
200         final URL resource,
201         final int levels,
202         boolean includeSubdirectories)
203     {
204         final List<String> contents = new ArrayList<String>();
205         if (resource != null)
206         {
207             // - create the file and make sure we remove any path white space characters
208             final File fileResource = new File(unescapeFilePath(resource.getFile()));
209             if (fileResource.isDirectory())
210             {
211                 File rootDirectory = fileResource;
212                 for (int ctr = 0; ctr < levels; ctr++)
213                 {
214                     rootDirectory = rootDirectory.getParentFile();
215                 }
216                 final File pluginDirectory = rootDirectory;
217                 loadFiles(
218                     pluginDirectory,
219                     contents,
220                     includeSubdirectories);
221 
222                 // - remove the root path from each file
223                 for (final ListIterator<String> iterator = contents.listIterator(); iterator.hasNext();)
224                 {
225                     final String filePath = iterator.next();
226                     iterator.set(
227                         StringUtils.replace(
228                             filePath.replace(
229                                 '\\',
230                                 '/'),
231                             pluginDirectory.getPath().replace(
232                                 '\\',
233                                 '/') + '/',
234                             ""));
235                 }
236             }
237         }
238         return contents;
239     }
240 
241     /**
242      * Loads all files find in the <code>directory</code> and adds them to the <code>fileList</code>.
243      *
244      * @param directory the directory from which to load all files.
245      * @param fileList  the Collection of files to which we'll add the found files.
246      * @param includeSubdirectories whether or not to include sub directories when loading the files.
247      */
248     private static void loadFiles(
249         final File directory,
250         final Collection<String> fileList,
251         boolean includeSubdirectories)
252     {
253         final Collection<File> lAllFiles = loadFiles(directory, includeSubdirectories);
254         for (File file : lAllFiles)
255         {
256             fileList.add(file.getPath());
257         }
258     }
259 
260     /**
261      * Loads all files find in the <code>directory</code> and returns them as Collection
262      *
263      * @param directory the directory from which to load all files.
264      * @param includeSubdirectories whether or not to include sub directories when loading the files.
265      * @return Collection with all files found in the directory
266      */
267     private static Collection<File> loadFiles(
268         final File directory,
269         boolean includeSubdirectories)
270     {
271         Collection<File> result = Collections.emptyList();
272         if(null!=directory && directory.exists())
273         {
274             result = FileUtils.listFiles(
275                     directory.isDirectory()? directory : directory.getParentFile(),
276                     TrueFileFilter.INSTANCE,
277                     includeSubdirectories ? TrueFileFilter.INSTANCE : null);
278         }
279         return result;
280     }
281 
282     /**
283      * Returns true/false on whether or not this <code>resource</code> represents an archive or not (i.e. jar, or zip,
284      * etc).
285      *
286      * @param resource
287      * @return true if its an archive, false otherwise.
288      */
289     public static boolean isArchive(final URL resource)
290     {
291         return resource != null && resource.toString().startsWith(ARCHIVE_PREFIX);
292     }
293 
294     private static final String URL_DECODE_ENCODING = "UTF-8";
295 
296     /**
297      * If this <code>resource</code> is an archive file, it will return the resource as an archive.
298      *
299      * @param resource
300      * @return the archive as a ZipFile
301      */
302     public static ZipFile getArchive(final URL resource)
303     {
304         try
305         {
306             ZipFile archive = null;
307             if (resource != null)
308             {
309                 String resourceUrl = resource.toString();
310                 resourceUrl = resourceUrl.replaceFirst(
311                         ARCHIVE_PREFIX,
312                         "");
313                 final int entryPrefixIndex = resourceUrl.indexOf('!');
314                 if (entryPrefixIndex != -1)
315                 {
316                     resourceUrl = resourceUrl.substring(
317                             0,
318                             entryPrefixIndex);
319                 }
320                 resourceUrl = URLDecoder.decode(new URL(resourceUrl).getFile(), URL_DECODE_ENCODING);
321                 File zipFile = new File(resourceUrl);
322                 if (zipFile.exists())
323                 {
324                     archive = new ZipFile(resourceUrl);
325                 }
326                 else
327                 {
328                     // ZipFile doesn't give enough detail about missing file
329                     throw new FileNotFoundException("ResourceUtils.getArchive " + resourceUrl + " NOT FOUND.");
330                 }
331             }
332             return archive;
333         }
334         // Don't unnecessarily wrap RuntimeException
335         catch (final RuntimeException ex)
336         {
337             throw ex;
338         }
339         // But don't require Exception declaration either.
340         catch (final Throwable throwable)
341         {
342             throw new RuntimeException(throwable);
343         }
344     }
345 
346     /**
347      * Loads the file resource and returns the contents as a String.
348      *
349      * @param resourceName the name of the resource.
350      * @return String
351      */
352     public static String getContents(final String resourceName)
353     {
354         return getContents(getResource(resourceName));
355     }
356 
357     /**
358      * Takes a className as an argument and returns the URL for the class.
359      *
360      * @param className name of class
361      * @return java.net.URL
362      */
363     public static URL getClassResource(final String className)
364     {
365         ExceptionUtils.checkEmpty(
366             "className",
367             className);
368         return getResource(getClassNameAsResource(className));
369     }
370 
371     /**
372      * Gets the class name as a resource.
373      *
374      * @param className the name of the class.
375      * @return the class name as a resource path.
376      */
377     private static String getClassNameAsResource(final String className)
378     {
379         return className.replace('.','/') + ".class";
380     }
381 
382     /**
383      * <p>
384      * Retrieves a resource from an optionally given <code>directory</code> or from the package on the classpath. </p>
385      * <p>
386      * If the directory is specified and is a valid directory then an attempt at finding the resource by appending the
387      * <code>resourceName</code> to the given <code>directory</code> will be made, otherwise an attempt to find the
388      * <code>resourceName</code> directly on the classpath will be initiated. </p>
389      *
390      * @param resourceName the name of a resource
391      * @param directory the directory location
392      * @return the resource url
393      */
394     public static URL getResource(
395         final String resourceName,
396         final String directory)
397     {
398         ExceptionUtils.checkEmpty(
399             "resourceName",
400             resourceName);
401 
402         if (directory != null)
403         {
404             final File file = new File(directory, resourceName);
405             if (file.exists())
406             {
407                 try
408                 {
409                     return file.toURI().toURL();
410                 }
411                 catch (final MalformedURLException exception)
412                 {
413                     // - ignore, we just try to find the resource on the classpath
414                 }
415             }
416         }
417         return getResource(resourceName);
418     }
419 
420     /**
421      * Makes the directory for the given location if it doesn't exist.
422      *
423      * @param location the location to make the directory.
424      */
425     public static void makeDirectories(final String location)
426     {
427         final File file = new File(location);
428         makeDirectories(file);
429     }
430 
431     /**
432      * Makes the directory for the given location if it doesn't exist.
433      *
434      * @param location the location to make the directory.
435      */
436     public static void makeDirectories(final File location)
437     {
438         final File parent = location.getParentFile();
439         if (parent != null)
440         {
441             parent.mkdirs();
442         }
443     }
444 
445     /**
446      * Gets the time as a <code>long</code> when this <code>resource</code> was last modified.
447      * If it can not be determined <code>0</code> is returned.
448      *
449      * @param resource the resource from which to retrieve
450      *        the last modified time.
451      * @return the last modified time or 0 if it couldn't be retrieved.
452      */
453     public static long getLastModifiedTime(final URL resource)
454     {
455         long lastModified;
456         try
457         {
458             final File file = new File(resource.getFile());
459             if (file.exists())
460             {
461                 lastModified = file.lastModified();
462             }
463             else
464             {
465                 URLConnection uriConnection = resource.openConnection();
466                 lastModified = uriConnection.getLastModified();
467 
468                 // - we need to set the urlConnection to null and explicitly
469                 //   call garbage collection, otherwise the JVM won't let go
470                 //   of the URL resource
471 //                uriConnection = null;
472 //                System.gc();
473                 IOUtils.closeQuietly(uriConnection.getInputStream());
474             }
475         }
476         catch (final Exception exception)
477         {
478             lastModified = 0;
479         }
480         return lastModified;
481     }
482 
483     /**
484      * <p>
485      * Retrieves a resource from an optionally given <code>directory</code> or from the package on the classpath.
486      * </p>
487      * <p>
488      * If the directory is specified and is a valid directory then an attempt at finding the resource by appending the
489      * <code>resourceName</code> to the given <code>directory</code> will be made, otherwise an attempt to find the
490      * <code>resourceName</code> directly on the classpath will be initiated. </p>
491      *
492      * @param resourceName the name of a resource
493      * @param directory the directory location
494      * @return the resource url
495      */
496     public static URL getResource(
497         final String resourceName,
498         final URL directory)
499     {
500         String directoryLocation = null;
501         if (directory != null)
502         {
503             directoryLocation = directory.getFile();
504         }
505         return getResource(
506             resourceName,
507             directoryLocation);
508     }
509 
510     /**
511      * Attempts to construct the given <code>path</code>
512      * to a URL instance. If the argument cannot be resolved as a resource
513      * on the file system this method will attempt to locate it on the
514      * classpath.
515      *
516      * @param path the path from which to construct the URL.
517      * @return the constructed URL or null if one couldn't be constructed.
518      */
519     public static URL toURL(String path)
520     {
521         URL url = null;
522         if (path != null)
523         {
524             path = ResourceUtils.normalizePath(path);
525 
526             try
527             {
528                 if (path.startsWith(CLASSPATH_PREFIX))
529                 {
530                     url = ResourceUtils.resolveClasspathResource(path);
531                 }
532                 else
533                 {
534                     final File file = new File(path);
535                     url = file.exists() ? file.toURI().toURL() : new URL(path);
536                 }
537             }
538             catch (MalformedURLException exception)
539             {
540                 // ignore means no protocol was specified
541             }
542         }
543         return url;
544     }
545 
546     /**
547      * Resolves a URL to a classpath resource, this method will treat occurrences of the exclamation mark
548      * similar to what {@link URL} does with the <code>jar:file</code> protocol.
549      * <p>
550      * Example: <code>my/path/to/some.zip!/file.xml</code> represents a resource <code>file.xml</code>
551      * that is located in a ZIP file on the classpath called <code>my/path/to/some.zip</code>
552      * <p>
553      * It is possible to have nested ZIP files, example:
554      * <code>my/path/to/first.zip!/subdir/second.zip!/file.xml</code>.
555      * <p>
556      * <i>Please note that the extension of the ZIP file can be anything,
557      * but in the case the extension is <code>.jar</code> the JVM will automatically unpack resources
558      * one level deep and put them all on the classpath</i>
559      *
560      * @param path the name of the resource to resolve to a URL, potentially nested in ZIP archives
561      * @return a URL pointing the resource resolved from the argument path
562      *      or <code>null</code> if the argument is <code>null</code> or impossible to resolve
563      */
564     public static URL resolveClasspathResource(String path)
565     {
566         URL urlResource = null;
567         if (path.startsWith(CLASSPATH_PREFIX))
568         {
569             path = path.substring(CLASSPATH_PREFIX.length(), path.length());
570 
571             // - the value of the following constant is -1 of no nested resources were specified,
572             //   otherwise it points to the location of the first occurrence
573             final int nestedPathOffset = path.indexOf("!/");
574 
575             // - take the part of the path that is not nested (may be all of it)
576             final String resourcePath = nestedPathOffset == -1 ? path : path.substring(0, nestedPathOffset);
577             final String nestingPath = nestedPathOffset == -1 ? "" : path.substring(nestedPathOffset);
578 
579             // - use the new path to load a URL from the classpath using the context class loader for this thread
580             urlResource = Thread.currentThread().getContextClassLoader().getResource(resourcePath);
581 
582             // - at this point the URL might be null in case the resource was not found
583             if (urlResource == null)
584             {
585                 if (logger.isDebugEnabled())
586                 {
587                     logger.debug("Resource could not be located on the classpath: " + resourcePath);
588                 }
589             }
590             else
591             {
592                 try
593                 {
594                     // - extract the filename from the entire resource path
595                     final int fileNameOffset = resourcePath.lastIndexOf('/');
596                     final String resourceFileName =
597                         fileNameOffset == -1 ? resourcePath : resourcePath.substring(fileNameOffset + 1);
598 
599                     if (logger.isDebugEnabled())
600                     {
601                         logger.debug("Creating temporary copy on the file system of the classpath resource");
602                     }
603                     final File fileSystemResource = File.createTempFile(resourceFileName, null);
604                     if (logger.isDebugEnabled())
605                     {
606                         logger.debug("Temporary file will be deleted on VM exit: " + fileSystemResource.getAbsolutePath());
607                     }
608                     fileSystemResource.deleteOnExit();
609                     if (logger.isDebugEnabled())
610                     {
611                         logger.debug("Copying classpath resource contents into temporary file");
612                     }
613                     writeUrlToFile(urlResource, fileSystemResource.toString());
614 
615                     // - count the times the actual resource to resolve has been nested
616                     final int nestingCount = StringUtils.countMatches(path, "!/");
617                     // - this buffer is used to construct the URL spec to that specific resource
618                     final StringBuilder buffer = new StringBuilder();
619                     for (int ctr = 0; ctr < nestingCount; ctr++)
620                     {
621                         buffer.append(ARCHIVE_PREFIX);
622                     }
623                     buffer.append(FILE_PREFIX).append(fileSystemResource.getAbsolutePath()).append(nestingPath);
624                     if (logger.isDebugEnabled())
625                     {
626                         logger.debug("Constructing URL to " +
627                             (nestingCount > 0 ? "nested" : "") + " resource in temporary file");
628                     }
629 
630                     urlResource = new URL(buffer.toString());
631                 }
632                 catch (final IOException exception)
633                 {
634                     logger.warn("Unable to resolve classpath resource", exception);
635                     // - impossible to properly resolve the path into a URL
636                     urlResource = null;
637                 }
638             }
639         }
640         return urlResource;
641     }
642 
643     /**
644      * Writes the URL contents to a file specified by the fileLocation argument.
645      *
646      * @param url the URL to read
647      * @param fileLocation the location which to write.
648      * @throws IOException if error writing file
649      */
650     public static void writeUrlToFile(final URL url, final String fileLocation)
651         throws IOException
652     {
653         ExceptionUtils.checkNull(
654             "url",
655             url);
656         ExceptionUtils.checkEmpty(
657             "fileLocation",
658             fileLocation);
659 
660         final File lOutputFile = new File(fileLocation);
661         makeDirectories(lOutputFile);
662         FileUtils.copyURLToFile(url, lOutputFile);
663     }
664 
665     /**
666      * Indicates whether or not the given <code>url</code> is a file.
667      *
668      * @param url the URL to check.
669      * @return true/false
670      */
671     public static boolean isFile(final URL url)
672     {
673         return url != null && new File(url.getFile()).isFile();
674     }
675 
676     /**
677      * The forward slash character.
678      */
679     private static final String FORWARD_SLASH = "/";
680 
681     /**
682      * Gets the contents of this directory and any of its sub directories based on the given <code>patterns</code>.
683      * And returns absolute or relative paths depending on the value of <code>absolute</code>.
684      *
685      * @param url the URL of the directory.
686      * @param absolute whether or not the returned content paths should be absolute (if
687      *        false paths will be relative to URL).
688      * @param patterns
689      * @return a collection of paths.
690      */
691     public static List<String> getDirectoryContents(
692         final URL url,
693         boolean absolute,
694         final String[] patterns)
695     {
696         List<String> contents = ResourceUtils.getDirectoryContents(
697                 url,
698                 0,
699                 true);
700 
701         // - first see if it's a directory
702         if (!contents.isEmpty())
703         {
704             for (final ListIterator<String> iterator = contents.listIterator(); iterator.hasNext();)
705             {
706                 String path = iterator.next();
707                 if (!matchesAtLeastOnePattern(
708                         path,
709                         patterns))
710                 {
711                     iterator.remove();
712                 }
713                 else if (absolute)
714                 {
715                     path = url.toString().endsWith(FORWARD_SLASH) ? path : FORWARD_SLASH + path;
716                     final URL resource = ResourceUtils.toURL(url + path);
717                     if (resource != null)
718                     {
719                         iterator.set(resource.toString());
720                     }
721                 }
722             }
723         }
724         else // - otherwise handle archives (i.e. jars, etc).
725         {
726             final String urlAsString = url.toString();
727             final String delimiter = "!/";
728             final String archivePath = urlAsString.replaceAll(
729                     delimiter + ".*",
730                     delimiter);
731             contents = ResourceUtils.getClassPathArchiveContents(url);
732             for (final ListIterator<String> iterator = contents.listIterator(); iterator.hasNext();)
733             {
734                 final String relativePath = iterator.next();
735                 final String fullPath = archivePath + relativePath;
736                 if (!fullPath.startsWith(urlAsString) || fullPath.equals(urlAsString + FORWARD_SLASH))
737                 {
738                     iterator.remove();
739                 }
740                 else if (!matchesAtLeastOnePattern(
741                         relativePath,
742                         patterns))
743                 {
744                     iterator.remove();
745                 }
746                 else if (absolute)
747                 {
748                     iterator.set(fullPath);
749                 }
750             }
751         }
752         return contents;
753     }
754 
755     /**
756      * Indicates whether or not the given <code>path</code> matches on
757      * one or more of the patterns defined within this class
758      * returns true if no patterns are defined.
759      *
760      * @param path the path to match on.
761      * @param patterns
762      * @return true/false
763      */
764     public static boolean matchesAtLeastOnePattern(
765         final String path,
766         final String[] patterns)
767     {
768         boolean matches = (patterns == null || patterns.length == 0);
769         if (!matches && patterns != null && patterns.length > 0)
770         {
771             final int patternNumber = patterns.length;
772             for (int ctr = 0; ctr < patternNumber; ctr++)
773             {
774                 final String pattern = patterns[ctr];
775                 if (PathMatcher.wildcardMatch(
776                         path,
777                         pattern))
778                 {
779                     matches = true;
780                     break;
781                 }
782             }
783         }
784         return matches;
785     }
786 
787     /**
788      * Indicates whether or not the contents of the given <code>directory</code>
789      * and any of its sub directories have been modified after the given <code>time</code>.
790      *
791      * @param directory the directory to check
792      * @param time the time to check against
793      * @return true/false
794      */
795     public static boolean modifiedAfter(
796         long time,
797         final File directory)
798     {
799         final Collection<File> files = ResourceUtils.loadFiles(directory, true);
800         boolean changed = files.isEmpty();
801         for (File file : files)
802         {
803             changed = file.lastModified() < time;
804             if (changed)
805             {
806                 break;
807             }
808         }
809         return changed;
810     }
811 
812     /**
813      * The pattern used for normalizing paths paths with more than one back slash.
814      */
815     private static final String BACK_SLASH_NORMALIZATION_PATTERN = "\\\\+";
816 
817     /**
818      * The pattern used for normalizing paths with more than one forward slash.
819      */
820     private static final String FORWARD_SLASH_NORMALIZATION_PATTERN = FORWARD_SLASH + '+';
821 
822     /**
823      * Removes any extra path separators and converts all from back slashes
824      * to forward slashes.
825      *
826      * @param path the path to normalize.
827      * @return the normalizd path
828      */
829     public static String normalizePath(final String path)
830     {
831         return path != null
832         ? path.replaceAll(
833             BACK_SLASH_NORMALIZATION_PATTERN,
834             FORWARD_SLASH).replaceAll(
835             FORWARD_SLASH_NORMALIZATION_PATTERN,
836             FORWARD_SLASH) : null;
837     }
838 
839     /**
840      * Takes a path and replaces the oldException with the newExtension.
841      *
842      * @param path the path to rename.
843      * @param oldExtension the extension to rename from.
844      * @param newExtension the extension to rename to.
845      * @return the path with the new extension.
846      */
847     public static String renameExtension(
848         final String path,
849         final String oldExtension,
850         final String newExtension)
851     {
852         ExceptionUtils.checkEmpty(
853             "path",
854             path);
855         ExceptionUtils.checkNull(
856             "oldExtension",
857             oldExtension);
858         ExceptionUtils.checkNull(
859             "newExtension",
860             newExtension);
861         String newPath = path;
862         final int oldExtensionIndex = path.lastIndexOf(oldExtension);
863         if (oldExtensionIndex != -1)
864         {
865             newPath = path.substring(
866                     0,
867                     oldExtensionIndex) + newExtension;
868         }
869         return newPath;
870     }
871 }