001package org.andromda.core.common;
002
003import java.io.File;
004import java.io.FileNotFoundException;
005import java.io.IOException;
006import java.io.InputStream;
007import java.io.Reader;
008import java.net.MalformedURLException;
009import java.net.URL;
010import java.net.URLConnection;
011import java.net.URLDecoder;
012import java.util.ArrayList;
013import java.util.Collection;
014import java.util.Collections;
015import java.util.Enumeration;
016import java.util.List;
017import java.util.ListIterator;
018import java.util.zip.ZipEntry;
019import java.util.zip.ZipFile;
020
021import org.apache.commons.io.FileUtils;
022import org.apache.commons.io.IOUtils;
023import org.apache.commons.io.filefilter.TrueFileFilter;
024import org.apache.commons.lang.StringUtils;
025import org.apache.log4j.Logger;
026
027/**
028 * Provides utilities for loading resources.
029 *
030 * @author Chad Brandon
031 * @author Bob Fields
032 * @author Michail Plushnikov
033 */
034public class ResourceUtils
035{
036    private static final Logger logger = Logger.getLogger(ResourceUtils.class);
037
038    /**
039     * All archive files start with this prefix.
040     */
041    private static final String ARCHIVE_PREFIX = "jar:";
042
043    /**
044     * The prefix for URL file resources.
045     */
046    private static final String FILE_PREFIX = "file:";
047
048    /**
049     * The prefix to use for searching the classpath for a resource.
050     */
051    private static final String CLASSPATH_PREFIX = "classpath:";
052
053    /**
054     * Retrieves a resource from the current classpath.
055     *
056     * @param resourceName the name of the resource
057     * @return the resource url
058     */
059    public static URL getResource(final String resourceName)
060    {
061        ExceptionUtils.checkEmpty(
062            "resourceName",
063            resourceName);
064        final ClassLoader loader = ClassUtils.getClassLoader();
065        URL resource = loader != null ? loader.getResource(resourceName) : null;
066        //if (resource==null)
067        return resource;
068    }
069
070    /**
071     * Loads the resource and returns the contents as a String.
072     *
073     * @param resource the name of the resource.
074     * @return String
075     */
076    public static String getContents(final URL resource)
077    {
078        String result = null;
079
080        InputStream in = null;
081        try
082        {
083            if(null!=resource)
084            {
085                in = resource.openStream();
086                result = IOUtils.toString(in);
087            }
088        }
089        catch (final IOException ex) {
090            throw new RuntimeException(ex);
091        }finally {
092            IOUtils.closeQuietly(in);
093        }
094        return result;
095    }
096
097    /**
098     * Loads the resource and returns the contents as a String.
099     *
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}