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
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
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
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
441
442
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
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
542
543 final int nestedPathOffset = path.indexOf("!/");
544
545
546 final String resourcePath = nestedPathOffset == -1 ? path : path.substring(0, nestedPathOffset);
547 final String nestingPath = nestedPathOffset == -1 ? "" : path.substring(nestedPathOffset);
548
549
550 urlResource = Thread.currentThread().getContextClassLoader().getResource(resourcePath);
551
552
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
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
586 final int nestingCount = StringUtils.countMatches(path, "!/");
587
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
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
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
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 }