View Javadoc

1   /***
2    * Copyright 2007 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *         http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * For more information visit
11   *         http://72miles.com and
12   *         http://architecturerules.googlecode.com/svn/docs/index.html
13   */
14  
15  package org.seventytwomiles.springframework.core.io;
16  
17  
18  import org.seventytwomiles.springframework.util.ClassUtils;
19  import org.seventytwomiles.springframework.util.ResourceUtils;
20  
21  import java.io.File;
22  import java.io.FileNotFoundException;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.net.URL;
26  import java.util.*;
27  
28  
29  
30  /***
31   * <p>These classes are all extracted from the Spring Framework in order to
32   * remove the dependency on the Spring library.</p>
33   *
34   * @author mikenereson
35   * @noinspection SimplifiableIfStatement
36   */
37  public class ClassPathResource {
38  
39  
40      /***
41       * <p>symbol that separates folders</p>
42       *
43       * @parameter FOLDER_SEPARATOR String
44       */
45      private static final String FOLDER_SEPARATOR = "/";
46  
47      /***
48       * <p>symbol that separates folders on the Windows platform</p>
49       *
50       * @parameter WINDOWS_FOLDER_SEPARATOR String
51       */
52      private static final String WINDOWS_FOLDER_SEPARATOR = "//";
53  
54      /***
55       * <p>Parent directory path</p>
56       *
57       * @parameter TOP_PATH String
58       */
59      private static final String TOP_PATH = "..";
60  
61      /***
62       * <p>Current path</p>
63       *
64       * @parameter CURRENT_PATH String
65       */
66      private static final String CURRENT_PATH = ".";
67  
68      /***
69       * <p>the path that you are in</p>
70       *
71       * @parameter path String
72       */
73      private final String path;
74  
75      /***
76       * <p>instance of ClassLoader</p>
77       *
78       * @parameter classLoader String
79       */
80      private ClassLoader classLoader;
81  
82      /***
83       * <p>instance of Class</p>
84       *
85       * @parameter clazz String
86       */
87      private Class clazz;
88  
89  
90      /***
91       * <p>Create a new <code>ClassPathResource</code> for ClassLoader usage. A
92       * leading slash will be removed, as the ClassLoader resource access methods
93       * will not accept it. The thread context class loader will be used for
94       * loading the resource.</p>
95       *
96       * @param path the absolute path within the class path
97       * @see ClassLoader#getResourceAsStream(String)
98       */
99      public ClassPathResource(final String path) {
100         this(path, (ClassLoader) null);
101     }
102 
103 
104     /***
105      * <p>Create a new <code>ClassPathResource</code> for ClassLoader usage. A
106      * leading slash will be removed, as the ClassLoader resource access methods
107      * will not accept it.</p>
108      *
109      * @param path the absolute path within the classpath
110      * @param classLoader the class loader to load the resource with, or
111      * <code>null</code> for the thread context class loader
112      * @see ClassLoader#getResourceAsStream(String)
113      */
114     public ClassPathResource(String path, final ClassLoader classLoader) {
115 
116         if (null == path || "".equals(path))
117             throw new IllegalArgumentException("path can not be empty or null");
118 
119         if (path.startsWith("/"))
120             path = path.substring(1);
121 
122         this.path = cleanPath(path);
123         this.classLoader
124                 = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
125     }
126 
127 
128     /***
129      * <p>Create a new <code>ClassPathResource</code> for Class usage. The path
130      * can be relative to the given class, or absolute within the classpath via
131      * a leading slash.</p>
132      *
133      * @param path relative or absolute path within the class path
134      * @param clazz the class to load resources with
135      * @see Class#getResourceAsStream
136      */
137     public ClassPathResource(final String path, final Class clazz) {
138 
139         if (null == path || "".equals(path))
140             throw new IllegalArgumentException("path can not be empty or null");
141 
142         this.path = cleanPath(path);
143         this.clazz = clazz;
144     }
145 
146 
147     /***
148      * <p>Create a new <code>ClassPathResource</code> with optional ClassLoader
149      * and Class. Only for internal usage.</p>
150      *
151      * @param path relative or absolute path within the classpath
152      * @param classLoader the class loader to load the resource with, if any
153      * @param clazz the class to load resources with, if any
154      */
155     protected ClassPathResource(final String path,
156                                 final ClassLoader classLoader,
157                                 final Class clazz) {
158         this.path = path;
159         this.classLoader = classLoader;
160         this.clazz = clazz;
161     }
162 
163 
164     /***
165      * <p>This implementation returns a description that includes the class path
166      * location.</p>
167      *
168      * @return String description of File
169      */
170     private String getDescription() {
171         return "class path resource [" + this.path + "]";
172     }
173 
174 
175     /***
176      * <p>This implementation returns a File reference for the underlying class
177      * path resource, provided that it refers to a file in the file system.</p>
178      *
179      * @return File
180      * @throws IOException when file not found
181      * @noinspection RedundantThrows
182      */
183     public File getFile() throws IOException {
184         return ResourceUtils.getFile(getURL(), getDescription());
185     }
186 
187 
188     /***
189      * <p>This implementation returns a URL for the underlying class path
190      * resource.</p>
191      *
192      * @return URL
193      * @throws FileNotFoundException when file not found
194      * @see ClassLoader#getResource(String)
195      * @see Class#getResource(String)
196      */
197     private URL getURL() throws FileNotFoundException {
198 
199         final URL url = clazz != null ? clazz.getResource(
200                 path) : classLoader.getResource(path);
201 
202         if (url == null)
203             throw new FileNotFoundException(
204                     getDescription() + " cannot be resolved to URL because it does not exist");
205 
206         return url;
207     }
208 
209 
210     /***
211      * <p>This implementation compares the underlying class path locations.</p>
212      */
213     @Override
214     public boolean equals(final Object object) {
215 
216         if (object == this)
217             return true;
218 
219         if (!(object instanceof ClassPathResource))
220             return false;
221 
222         final ClassPathResource that = (ClassPathResource) object;
223 
224         return (this.path.equals(that.path) &&
225                 nullSafeEquals(this.classLoader, that.classLoader) &&
226                 nullSafeEquals(this.clazz, that.clazz));
227     }
228 
229 
230     /***
231      * <p>Determine if the given objects are equal, returning <code>true</code>
232      * if both are <code>null</code> or <code>false</code> if only one is
233      * <code>null</code>. <p>Compares arrays with <code>Arrays.equals</code>,
234      * performing an equality check based on the array elements rather than the
235      * array reference.</p>
236      *
237      * @param object1 first Object to compare
238      * @param object2 second Object to compare
239      * @return whether the given objects are equal
240      * @see java.util.Arrays#equals
241      */
242     private boolean nullSafeEquals(final Object object1, final Object object2) {
243 
244         if (object1 == object2)
245             return true;
246 
247         if (object1 == null || object2 == null)
248             return false;
249 
250         if (object1.equals(object2))
251             return true;
252 
253         if (object1 instanceof Object[] && object2 instanceof Object[])
254             return Arrays.equals((Object[]) object1, (Object[]) object2);
255 
256         if (object1 instanceof boolean[] && object2 instanceof boolean[])
257             return Arrays.equals((boolean[]) object1, (boolean[]) object2);
258 
259         if (object1 instanceof byte[] && object2 instanceof byte[])
260             return Arrays.equals((byte[]) object1, (byte[]) object2);
261 
262         if (object1 instanceof char[] && object2 instanceof char[])
263             return Arrays.equals((char[]) object1, (char[]) object2);
264 
265         if (object1 instanceof double[] && object2 instanceof double[])
266             return Arrays.equals((double[]) object1, (double[]) object2);
267 
268         if (object1 instanceof float[] && object2 instanceof float[])
269             return Arrays.equals((float[]) object1, (float[]) object2);
270 
271         if (object1 instanceof int[] && object2 instanceof int[])
272             return Arrays.equals((int[]) object1, (int[]) object2);
273 
274         if (object1 instanceof long[] && object2 instanceof long[])
275             return Arrays.equals((long[]) object1, (long[]) object2);
276 
277         if (object1 instanceof short[] && object2 instanceof short[])
278             return Arrays.equals((short[]) object1, (short[]) object2);
279 
280         return false;
281     }
282 
283 
284     /***
285      * <p>This implementation checks whether a File can be opened, falling back
286      * to whether an InputStream can be opened. This will cover both directories
287      * and content resources.</p>
288      *
289      * @return boolean
290      */
291     public boolean exists() {
292 
293         // Try file existence: can we find the file in the file system?
294         try {
295 
296             return getFile().exists();
297 
298         } catch (final IOException ex) {
299 
300             // Fall back to stream existence: can we open the stream?
301             try {
302 
303                 final InputStream inputStream = getInputStream();
304                 inputStream.close();
305                 return true;
306 
307             } catch (final Throwable throwable) {
308 
309                 return false;
310             }
311         }
312     }
313 
314 
315     /***
316      * <p>This implementation opens an InputStream for the given class path
317      * resource.</p>
318      *
319      * @return InputStream
320      * @throws FileNotFoundException when file not found
321      * @see ClassLoader#getResourceAsStream(String)
322      * @see Class#getResourceAsStream(String)
323      */
324     private InputStream getInputStream() throws FileNotFoundException {
325 
326 
327         final InputStream inputStream
328                 = clazz != null ? clazz.getResourceAsStream(
329                 path) : classLoader.getResourceAsStream(path);
330 
331         if (inputStream == null)
332             throw new FileNotFoundException(
333                     getDescription() + " cannot be opened because it does not exist");
334 
335         return inputStream;
336     }
337 
338 
339     /***
340      * <p>Normalize the path by suppressing sequences like "path/.." and inner
341      * simple dots. <p>The result is convenient for path comparison. For other
342      * uses, notice that Windows separators ("\") are replaced by simple
343      * slashes.</p>
344      *
345      * @param path the original path
346      * @return the normalized path
347      */
348     private String cleanPath(final String path) {
349 
350         String pathToUse = replace(path, WINDOWS_FOLDER_SEPARATOR,
351                 FOLDER_SEPARATOR);
352 
353         // Strip prefix from path to analyze, to not treat it as part of the
354         // first path element. This is necessary to correctly parse paths like
355         // "file:core/../core/io/Resource.class", where the ".." should just
356         // strip the first "core" directory while keeping the "file:" prefix.
357         final int prefixIndex = pathToUse.indexOf(":");
358         String prefix = "";
359 
360         if (prefixIndex != -1) {
361 
362             prefix = pathToUse.substring(0, prefixIndex + 1);
363             pathToUse = pathToUse.substring(prefixIndex + 1);
364         }
365 
366         final String[] pathArray = delimitedListToStringArray(pathToUse,
367                 FOLDER_SEPARATOR);
368 
369         final List pathElements = new LinkedList();
370         int tops = 0;
371 
372         for (int i = pathArray.length - 1; i >= 0; i--) {
373 
374             if (CURRENT_PATH.equals(pathArray[i])) {
375 
376                 // Points to current directory - drop it.
377 
378             } else if (TOP_PATH.equals(pathArray[i])) {
379 
380                 // Registering top path found.
381                 tops++;
382 
383             } else {
384 
385                 if (tops > 0) {
386 
387                     // Merging path element with corresponding to top path.
388                     tops--;
389 
390                 } else {
391 
392                     // Normal path element found.
393                     pathElements.add(0, pathArray[i]);
394                 }
395             }
396         }
397 
398         // Remaining top paths need to be retained.
399         for (int i = 0; i < tops; i++)
400             pathElements.add(0, TOP_PATH);
401 
402         return prefix + collectionToDelimitedString(pathElements,
403                 FOLDER_SEPARATOR);
404     }
405 
406 
407     /***
408      * <p>Replace all occurrences of a substring within a string with another
409      * string.</p>
410      *
411      * @param inString String to examine
412      * @param oldPattern String to replace
413      * @param newPattern String to insert
414      * @return a String with the replacements
415      */
416     private static String replace(final String inString,
417                                   final String oldPattern,
418                                   final String newPattern) {
419 
420         if (inString == null)
421             return null;
422 
423         if (oldPattern == null || newPattern == null)
424             return inString;
425 
426         final StringBuffer stringBuffer = new StringBuffer();
427 
428         // output StringBuffer we'll build up
429         int position = 0; // our position in the old string
430         int index = inString.indexOf(oldPattern);
431 
432         // the index of an occurrence we've found, or -1
433         final int patternLength = oldPattern.length();
434 
435         while (index >= 0) {
436 
437             stringBuffer.append(inString.substring(position, index));
438             stringBuffer.append(newPattern);
439             position = index + patternLength;
440             index = inString.indexOf(oldPattern, position);
441         }
442 
443         stringBuffer.append(inString.substring(position));
444 
445         // remember to append any characters to the right of a matches
446         return stringBuffer.toString();
447     }
448 
449 
450     /***
451      * <p>Convenience method to return a Collection as a delimited (e.g. CSV)
452      * String. E.g. useful for <code>toString()</code> implementations.</p>
453      *
454      * @param collection the Collection to display
455      * @param deliminator the delimiter to use (probably a ",")
456      * @param prefix the String to start each element with
457      * @param suffix the String to end each element with
458      * @return String
459      */
460     private static String collectionToDelimitedString(
461             final Collection collection, final String deliminator,
462             final String prefix, final String suffix) {
463 
464         if (collection.isEmpty())
465             return "";
466 
467         final StringBuffer stringBuffer = new StringBuffer();
468         final Iterator iterator = collection.iterator();
469 
470         while (iterator.hasNext()) {
471             stringBuffer.append(prefix).append(iterator.next()).append(suffix);
472 
473             if (iterator.hasNext())
474                 stringBuffer.append(deliminator);
475         }
476 
477         return stringBuffer.toString();
478     }
479 
480 
481     /***
482      * <p>Convenience method to return a Collection as a delimited (e.g. CSV)
483      * String. E.g. useful for <code>toString()</code> implementations.</p>
484      *
485      * @param collection the Collection to display
486      * @param deliminator the delimiter to use (probably a ",")
487      * @return String
488      */
489     private String collectionToDelimitedString(final Collection collection,
490                                                final String deliminator) {
491         return collectionToDelimitedString(collection, deliminator, "", "");
492     }
493 
494 
495     /***
496      * Take a String which is a delimited list and convert it to a String array.
497      * <p>A single delimiter can consists of more than one character: It will
498      * still be considered as single delimiter string, rather than as bunch of
499      * potential delimiter characters - in contrast to <code>tokenizeToStringArray</code>.
500      *
501      * @param string the input String
502      * @param delimiter the delimiter between elements (this is a single
503      * delimiter, rather than a bunch individual delimiter characters)
504      * @return an array of the tokens in the list
505      */
506     private static String[] delimitedListToStringArray(final String string,
507                                                        final String delimiter) {
508 
509         if (string == null)
510             return new String[0];
511 
512         if (delimiter == null)
513             return new String[]{string};
514 
515         final List result = new ArrayList();
516 
517         if ("".equals(delimiter)) {
518 
519             for (int i = 0; i < string.length(); i++)
520                 result.add(string.substring(i, i + 1));
521 
522         } else {
523 
524             int position = 0;
525             int deletePosition;
526 
527             while ((deletePosition = string.indexOf(delimiter,
528                     position)) != -1) {
529 
530                 result.add(string.substring(position, deletePosition));
531                 position = deletePosition + delimiter.length();
532             }
533 
534             // Add rest of String, but not in case of empty input.
535             if (string.length() > 0 && position <= string.length())
536                 result.add(string.substring(position));
537         }
538 
539         return toStringArray(result);
540     }
541 
542 
543     /***
544      * <p>Copy the given Collection into a String array. The Collection must
545      * contain String elements only.</p>
546      *
547      * @param collection the Collection to copy
548      * @return the String array (<code>null</code> if the passed-in Collection
549      *         was <code>null</code>)
550      */
551     private static String[] toStringArray(final Collection collection) {
552 
553         if (collection == null)
554             return null;
555 
556         return (String[]) collection.toArray(new String[collection.size()]);
557     }
558 
559 }