View Javadoc

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