001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.xbean.finder;
018
019import java.io.BufferedInputStream;
020import java.io.ByteArrayOutputStream;
021import java.io.File;
022import java.io.IOException;
023import java.io.InputStream;
024import java.net.HttpURLConnection;
025import java.net.JarURLConnection;
026import java.net.MalformedURLException;
027import java.net.URL;
028import java.net.URLConnection;
029import java.util.ArrayList;
030import java.util.Collections;
031import java.util.Enumeration;
032import java.util.HashMap;
033import java.util.Iterator;
034import java.util.List;
035import java.util.Map;
036import java.util.Properties;
037import java.util.Vector;
038import java.util.jar.JarEntry;
039import java.util.jar.JarFile;
040
041/**
042 * @author David Blevins
043 * @version $Rev: 1484013 $ $Date: 2013-05-18 00:09:26 +0200 (Sat, 18 May 2013) $
044 */
045public class ResourceFinder {
046    private final URL[] urls;
047    private final String path;
048    private final ClassLoader classLoader;
049    private final List<String> resourcesNotLoaded = new ArrayList<String>();
050
051    public ResourceFinder(URL... urls) {
052        this(null, Thread.currentThread().getContextClassLoader(), urls);
053    }
054
055    public ResourceFinder(String path) {
056        this(path, Thread.currentThread().getContextClassLoader(), null);
057    }
058
059    public ResourceFinder(String path, URL... urls) {
060        this(path, Thread.currentThread().getContextClassLoader(), urls);
061    }
062
063    public ResourceFinder(String path, ClassLoader classLoader) {
064        this(path, classLoader, null);
065    }
066
067    public ResourceFinder(String path, ClassLoader classLoader, URL... urls) {
068        if (path == null){
069            path = "";
070        } else if (path.length() > 0 && !path.endsWith("/")) {
071            path += "/";
072        }
073        this.path = path;
074
075        if (classLoader == null) {
076            classLoader = Thread.currentThread().getContextClassLoader();
077        }
078        this.classLoader = classLoader;
079
080        for (int i = 0; urls != null && i < urls.length; i++) {
081            URL url = urls[i];
082            if (url == null || "jar".equals(url.getProtocol()) || isDirectory(url)) { // test directory last since it is the longer in time
083                continue;
084            }
085            try {
086                urls[i] = new URL("jar", "", -1, url.toString() + "!/");
087            } catch (MalformedURLException e) {
088                // no-op
089            }
090        }
091        this.urls = (urls == null || urls.length == 0)? null : urls;
092    }
093
094    private static boolean isDirectory(URL url) {
095        String file = url.getFile();
096        return (file.length() > 0 && file.charAt(file.length() - 1) == '/') || new File(file).isDirectory(); // with surefire first test can easily fail
097    }
098
099    /**
100     * Returns a list of resources that could not be loaded in the last invoked findAvailable* or
101     * mapAvailable* methods.
102     * <p/>
103     * The list will only contain entries of resources that match the requirements
104     * of the last invoked findAvailable* or mapAvailable* methods, but were unable to be
105     * loaded and included in their results.
106     * <p/>
107     * The list returned is unmodifiable and the results of this method will change
108     * after each invocation of a findAvailable* or mapAvailable* methods.
109     * <p/>
110     * This method is not thread safe.
111     */
112    public List<String> getResourcesNotLoaded() {
113        return Collections.unmodifiableList(resourcesNotLoaded);
114    }
115
116    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
117    //
118    //   Find
119    //
120    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
121
122    public URL find(String uri) throws IOException {
123        String fullUri = path + uri;
124
125        URL resource = getResource(fullUri);
126        if (resource == null) {
127            throw new IOException("Could not find resource '" + path + uri + "'");
128        }
129
130        return resource;
131    }
132
133    public List<URL> findAll(String uri) throws IOException {
134        String fullUri = path + uri;
135
136        Enumeration<URL> resources = getResources(fullUri);
137        List<URL> list = new ArrayList();
138        while (resources.hasMoreElements()) {
139            URL url = resources.nextElement();
140            list.add(url);
141        }
142        return list;
143    }
144
145
146    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
147    //
148    //   Find String
149    //
150    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
151
152    /**
153     * Reads the contents of the URL as a {@link String}'s and returns it.
154     *
155     * @param uri
156     * @return a stringified content of a resource
157     * @throws IOException if a resource pointed out by the uri param could not be find
158     * @see ClassLoader#getResource(String)
159     */
160    public String findString(String uri) throws IOException {
161        String fullUri = path + uri;
162
163        URL resource = getResource(fullUri);
164        if (resource == null) {
165            throw new IOException("Could not find a resource in : " + fullUri);
166        }
167
168        return readContents(resource);
169    }
170
171    /**
172     * Reads the contents of the found URLs as a list of {@link String}'s and returns them.
173     *
174     * @param uri
175     * @return a list of the content of each resource URL found
176     * @throws IOException if any of the found URLs are unable to be read.
177     */
178    public List<String> findAllStrings(String uri) throws IOException {
179        String fulluri = path + uri;
180
181        List<String> strings = new ArrayList<String>();
182
183        Enumeration<URL> resources = getResources(fulluri);
184        while (resources.hasMoreElements()) {
185            URL url = resources.nextElement();
186            String string = readContents(url);
187            strings.add(string);
188        }
189        return strings;
190    }
191
192    /**
193     * Reads the contents of the found URLs as a Strings and returns them.
194     * Individual URLs that cannot be read are skipped and added to the
195     * list of 'resourcesNotLoaded'
196     *
197     * @param uri
198     * @return a list of the content of each resource URL found
199     * @throws IOException if classLoader.getResources throws an exception
200     */
201    public List<String> findAvailableStrings(String uri) throws IOException {
202        resourcesNotLoaded.clear();
203        String fulluri = path + uri;
204
205        List<String> strings = new ArrayList<String>();
206
207        Enumeration<URL> resources = getResources(fulluri);
208        while (resources.hasMoreElements()) {
209            URL url = resources.nextElement();
210            try {
211                String string = readContents(url);
212                strings.add(string);
213            } catch (IOException notAvailable) {
214                resourcesNotLoaded.add(url.toExternalForm());
215            }
216        }
217        return strings;
218    }
219
220    /**
221     * Reads the contents of all non-directory URLs immediately under the specified
222     * location and returns them in a map keyed by the file name.
223     * <p/>
224     * Any URLs that cannot be read will cause an exception to be thrown.
225     * <p/>
226     * Example classpath:
227     * <p/>
228     * META-INF/serializables/one
229     * META-INF/serializables/two
230     * META-INF/serializables/three
231     * META-INF/serializables/four/foo.txt
232     * <p/>
233     * ResourceFinder finder = new ResourceFinder("META-INF/");
234     * Map map = finder.mapAvailableStrings("serializables");
235     * map.contains("one");  // true
236     * map.contains("two");  // true
237     * map.contains("three");  // true
238     * map.contains("four");  // false
239     *
240     * @param uri
241     * @return a list of the content of each resource URL found
242     * @throws IOException if any of the urls cannot be read
243     */
244    public Map<String, String> mapAllStrings(String uri) throws IOException {
245        Map<String, String> strings = new HashMap<String, String>();
246        Map<String, URL> resourcesMap = getResourcesMap(uri);
247        for (Iterator iterator = resourcesMap.entrySet().iterator(); iterator.hasNext();) {
248            Map.Entry entry = (Map.Entry) iterator.next();
249            String name = (String) entry.getKey();
250            URL url = (URL) entry.getValue();
251            String value = readContents(url);
252            strings.put(name, value);
253        }
254        return strings;
255    }
256
257    /**
258     * Reads the contents of all non-directory URLs immediately under the specified
259     * location and returns them in a map keyed by the file name.
260     * <p/>
261     * Individual URLs that cannot be read are skipped and added to the
262     * list of 'resourcesNotLoaded'
263     * <p/>
264     * Example classpath:
265     * <p/>
266     * META-INF/serializables/one
267     * META-INF/serializables/two      # not readable
268     * META-INF/serializables/three
269     * META-INF/serializables/four/foo.txt
270     * <p/>
271     * ResourceFinder finder = new ResourceFinder("META-INF/");
272     * Map map = finder.mapAvailableStrings("serializables");
273     * map.contains("one");  // true
274     * map.contains("two");  // false
275     * map.contains("three");  // true
276     * map.contains("four");  // false
277     *
278     * @param uri
279     * @return a list of the content of each resource URL found
280     * @throws IOException if classLoader.getResources throws an exception
281     */
282    public Map<String, String> mapAvailableStrings(String uri) throws IOException {
283        resourcesNotLoaded.clear();
284        Map<String, String> strings = new HashMap<String, String>();
285        Map<String, URL> resourcesMap = getResourcesMap(uri);
286        for (Iterator iterator = resourcesMap.entrySet().iterator(); iterator.hasNext();) {
287            Map.Entry entry = (Map.Entry) iterator.next();
288            String name = (String) entry.getKey();
289            URL url = (URL) entry.getValue();
290            try {
291                String value = readContents(url);
292                strings.put(name, value);
293            } catch (IOException notAvailable) {
294                resourcesNotLoaded.add(url.toExternalForm());
295            }
296        }
297        return strings;
298    }
299
300    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
301    //
302    //   Find Class
303    //
304    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
305
306    /**
307     * Executes {@link #findString(String)} assuming the contents URL found is the name of
308     * a class that should be loaded and returned.
309     *
310     * @param uri
311     * @return
312     * @throws IOException
313     * @throws ClassNotFoundException
314     */
315    public Class<?> findClass(String uri) throws IOException, ClassNotFoundException {
316        String className = findString(uri);
317        return classLoader.loadClass(className);
318    }
319
320    /**
321     * Executes findAllStrings assuming the strings are
322     * the names of a classes that should be loaded and returned.
323     * <p/>
324     * Any URL or class that cannot be loaded will cause an exception to be thrown.
325     *
326     * @param uri
327     * @return
328     * @throws IOException
329     * @throws ClassNotFoundException
330     */
331    public List<Class<?>> findAllClasses(String uri) throws IOException, ClassNotFoundException {
332        List<Class<?>> classes = new ArrayList<Class<?>>();
333        List<String> strings = findAllStrings(uri);
334        for (String className : strings) {
335            Class<?> clazz = classLoader.loadClass(className);
336            classes.add(clazz);
337        }
338        return classes;
339    }
340
341    /**
342     * Executes findAvailableStrings assuming the strings are
343     * the names of a classes that should be loaded and returned.
344     * <p/>
345     * Any class that cannot be loaded will be skipped and placed in the
346     * 'resourcesNotLoaded' collection.
347     *
348     * @param uri
349     * @return
350     * @throws IOException if classLoader.getResources throws an exception
351     */
352    public List<Class<?>> findAvailableClasses(String uri) throws IOException {
353        resourcesNotLoaded.clear();
354        List<Class<?>> classes = new ArrayList<Class<?>>();
355        List<String> strings = findAvailableStrings(uri);
356        for (String className : strings) {
357            try {
358                Class<?> clazz = classLoader.loadClass(className);
359                classes.add(clazz);
360            } catch (Exception notAvailable) {
361                resourcesNotLoaded.add(className);
362            }
363        }
364        return classes;
365    }
366
367    /**
368     * Executes mapAllStrings assuming the value of each entry in the
369     * map is the name of a class that should be loaded.
370     * <p/>
371     * Any class that cannot be loaded will be cause an exception to be thrown.
372     * <p/>
373     * Example classpath:
374     * <p/>
375     * META-INF/xmlparsers/xerces
376     * META-INF/xmlparsers/crimson
377     * <p/>
378     * ResourceFinder finder = new ResourceFinder("META-INF/");
379     * Map map = finder.mapAvailableStrings("xmlparsers");
380     * map.contains("xerces");  // true
381     * map.contains("crimson");  // true
382     * Class xercesClass = map.get("xerces");
383     * Class crimsonClass = map.get("crimson");
384     *
385     * @param uri
386     * @return
387     * @throws IOException
388     * @throws ClassNotFoundException
389     */
390    public Map<String, Class<?>> mapAllClasses(String uri) throws IOException, ClassNotFoundException {
391        Map<String, Class<?>> classes = new HashMap<String, Class<?>>();
392        Map<String, String> map = mapAllStrings(uri);
393        for (Map.Entry<String, String> entry : map.entrySet()) {
394            String string = entry.getKey();
395            String className = entry.getValue();
396            Class<?> clazz = classLoader.loadClass(className);
397            classes.put(string, clazz);
398        }
399        return classes;
400    }
401
402    /**
403     * Executes mapAvailableStrings assuming the value of each entry in the
404     * map is the name of a class that should be loaded.
405     * <p/>
406     * Any class that cannot be loaded will be skipped and placed in the
407     * 'resourcesNotLoaded' collection.
408     * <p/>
409     * Example classpath:
410     * <p/>
411     * META-INF/xmlparsers/xerces
412     * META-INF/xmlparsers/crimson
413     * <p/>
414     * ResourceFinder finder = new ResourceFinder("META-INF/");
415     * Map map = finder.mapAvailableStrings("xmlparsers");
416     * map.contains("xerces");  // true
417     * map.contains("crimson");  // true
418     * Class xercesClass = map.get("xerces");
419     * Class crimsonClass = map.get("crimson");
420     *
421     * @param uri
422     * @return
423     * @throws IOException if classLoader.getResources throws an exception
424     */
425    public Map<String, Class<?>> mapAvailableClasses(String uri) throws IOException {
426        resourcesNotLoaded.clear();
427        Map<String, Class<?>> classes = new HashMap<String, Class<?>>();
428        Map<String, String> map = mapAvailableStrings(uri);
429        for (Map.Entry<String, String> entry : map.entrySet()) {
430            String string = entry.getKey();
431            String className = entry.getValue();
432            try {
433                Class<?> clazz = classLoader.loadClass(className);
434                classes.put(string, clazz);
435            } catch (Exception notAvailable) {
436                resourcesNotLoaded.add(className);
437            }
438        }
439        return classes;
440    }
441
442    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
443    //
444    //   Find Implementation
445    //
446    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
447
448    /**
449     * Assumes the class specified points to a file in the classpath that contains
450     * the name of a class that implements or is a subclass of the specfied class.
451     * <p/>
452     * Any class that cannot be loaded will be cause an exception to be thrown.
453     * <p/>
454     * Example classpath:
455     * <p/>
456     * META-INF/java.io.InputStream    # contains the classname org.acme.AcmeInputStream
457     * META-INF/java.io.OutputStream
458     * <p/>
459     * ResourceFinder finder = new ResourceFinder("META-INF/");
460     * Class clazz = finder.findImplementation(java.io.InputStream.class);
461     * clazz.getName();  // returns "org.acme.AcmeInputStream"
462     *
463     * @param interfase a superclass or interface
464     * @return
465     * @throws IOException            if the URL cannot be read
466     * @throws ClassNotFoundException if the class found is not loadable
467     * @throws ClassCastException     if the class found is not assignable to the specified superclass or interface
468     */
469    public Class<?> findImplementation(Class<?> interfase) throws IOException, ClassNotFoundException {
470        String className = findString(interfase.getName());
471        Class<?> impl = classLoader.loadClass(className);
472        if (!interfase.isAssignableFrom(impl)) {
473            throw new ClassCastException("Class not of type: " + interfase.getName());
474        }
475        return impl;
476    }
477
478    /**
479     * Assumes the class specified points to a file in the classpath that contains
480     * the name of a class that implements or is a subclass of the specfied class.
481     * <p/>
482     * Any class that cannot be loaded or assigned to the specified interface will be cause
483     * an exception to be thrown.
484     * <p/>
485     * Example classpath:
486     * <p/>
487     * META-INF/java.io.InputStream    # contains the classname org.acme.AcmeInputStream
488     * META-INF/java.io.InputStream    # contains the classname org.widget.NeatoInputStream
489     * META-INF/java.io.InputStream    # contains the classname com.foo.BarInputStream
490     * <p/>
491     * ResourceFinder finder = new ResourceFinder("META-INF/");
492     * List classes = finder.findAllImplementations(java.io.InputStream.class);
493     * classes.contains("org.acme.AcmeInputStream");  // true
494     * classes.contains("org.widget.NeatoInputStream");  // true
495     * classes.contains("com.foo.BarInputStream");  // true
496     *
497     * @param interfase a superclass or interface
498     * @return
499     * @throws IOException            if the URL cannot be read
500     * @throws ClassNotFoundException if the class found is not loadable
501     * @throws ClassCastException     if the class found is not assignable to the specified superclass or interface
502     */
503    public <T> List<Class<? extends T>> findAllImplementations(Class<T> interfase) throws IOException, ClassNotFoundException {
504        List<Class<? extends T>> implementations = new ArrayList<Class<? extends T>>();
505        List<String> strings = findAllStrings(interfase.getName());
506        for (String className : strings) {
507            Class<? extends T> impl = classLoader.loadClass(className).asSubclass(interfase);
508            implementations.add(impl);
509        }
510        return implementations;
511    }
512
513    /**
514     * Assumes the class specified points to a file in the classpath that contains
515     * the name of a class that implements or is a subclass of the specfied class.
516     * <p/>
517     * Any class that cannot be loaded or are not assignable to the specified class will be
518     * skipped and placed in the 'resourcesNotLoaded' collection.
519     * <p/>
520     * Example classpath:
521     * <p/>
522     * META-INF/java.io.InputStream    # contains the classname org.acme.AcmeInputStream
523     * META-INF/java.io.InputStream    # contains the classname org.widget.NeatoInputStream
524     * META-INF/java.io.InputStream    # contains the classname com.foo.BarInputStream
525     * <p/>
526     * ResourceFinder finder = new ResourceFinder("META-INF/");
527     * List classes = finder.findAllImplementations(java.io.InputStream.class);
528     * classes.contains("org.acme.AcmeInputStream");  // true
529     * classes.contains("org.widget.NeatoInputStream");  // true
530     * classes.contains("com.foo.BarInputStream");  // true
531     *
532     * @param interfase a superclass or interface
533     * @return
534     * @throws IOException if classLoader.getResources throws an exception
535     */
536    public <T> List<Class<? extends T>> findAvailableImplementations(Class<T> interfase) throws IOException {
537        resourcesNotLoaded.clear();
538        List<Class<? extends T>> implementations = new ArrayList<Class<? extends T>>();
539        List<String> strings = findAvailableStrings(interfase.getName());
540        for (String className : strings) {
541            try {
542                Class<?> impl = classLoader.loadClass(className);
543                if (interfase.isAssignableFrom(impl)) {
544                    implementations.add(impl.asSubclass(interfase));
545                } else {
546                    resourcesNotLoaded.add(className);
547                }
548            } catch (Exception notAvailable) {
549                resourcesNotLoaded.add(className);
550            }
551        }
552        return implementations;
553    }
554
555    /**
556     * Assumes the class specified points to a directory in the classpath that holds files
557     * containing the name of a class that implements or is a subclass of the specfied class.
558     * <p/>
559     * Any class that cannot be loaded or assigned to the specified interface will be cause
560     * an exception to be thrown.
561     * <p/>
562     * Example classpath:
563     * <p/>
564     * META-INF/java.net.URLStreamHandler/jar
565     * META-INF/java.net.URLStreamHandler/file
566     * META-INF/java.net.URLStreamHandler/http
567     * <p/>
568     * ResourceFinder finder = new ResourceFinder("META-INF/");
569     * Map map = finder.mapAllImplementations(java.net.URLStreamHandler.class);
570     * Class jarUrlHandler = map.get("jar");
571     * Class fileUrlHandler = map.get("file");
572     * Class httpUrlHandler = map.get("http");
573     *
574     * @param interfase a superclass or interface
575     * @return
576     * @throws IOException            if the URL cannot be read
577     * @throws ClassNotFoundException if the class found is not loadable
578     * @throws ClassCastException     if the class found is not assignable to the specified superclass or interface
579     */
580    public <T> Map<String, Class<? extends T>> mapAllImplementations(Class<T> interfase) throws IOException, ClassNotFoundException {
581        Map<String, Class<? extends T>> implementations = new HashMap<String, Class<? extends T>>();
582        Map<String, String> map = mapAllStrings(interfase.getName());
583        for (Map.Entry<String, String> entry : map.entrySet()) {
584            String string = entry.getKey();
585            String className = entry.getValue();
586            Class<? extends T> impl = classLoader.loadClass(className).asSubclass(interfase);
587            implementations.put(string, impl);
588        }
589        return implementations;
590    }
591
592    /**
593     * Assumes the class specified points to a directory in the classpath that holds files
594     * containing the name of a class that implements or is a subclass of the specfied class.
595     * <p/>
596     * Any class that cannot be loaded or are not assignable to the specified class will be
597     * skipped and placed in the 'resourcesNotLoaded' collection.
598     * <p/>
599     * Example classpath:
600     * <p/>
601     * META-INF/java.net.URLStreamHandler/jar
602     * META-INF/java.net.URLStreamHandler/file
603     * META-INF/java.net.URLStreamHandler/http
604     * <p/>
605     * ResourceFinder finder = new ResourceFinder("META-INF/");
606     * Map map = finder.mapAllImplementations(java.net.URLStreamHandler.class);
607     * Class jarUrlHandler = map.get("jar");
608     * Class fileUrlHandler = map.get("file");
609     * Class httpUrlHandler = map.get("http");
610     *
611     * @param interfase a superclass or interface
612     * @return
613     * @throws IOException if classLoader.getResources throws an exception
614     */
615    public <T> Map<String, Class<? extends T>> mapAvailableImplementations(Class<T> interfase) throws IOException {
616        resourcesNotLoaded.clear();
617        Map<String, Class<? extends T>> implementations = new HashMap<String, Class<? extends T>>();
618        Map<String, String> map = mapAvailableStrings(interfase.getName());
619        for (Map.Entry<String, String> entry : map.entrySet()) {
620            String string = entry.getKey();
621            String className = entry.getValue();
622            try {
623                Class<?> impl = classLoader.loadClass(className);
624                if (interfase.isAssignableFrom(impl)) {
625                    implementations.put(string, impl.asSubclass(interfase));
626                } else {
627                    resourcesNotLoaded.add(className);
628                }
629            } catch (Exception notAvailable) {
630                resourcesNotLoaded.add(className);
631            }
632        }
633        return implementations;
634    }
635
636    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
637    //
638    //   Find Properties
639    //
640    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
641
642    /**
643     * Finds the corresponding resource and reads it in as a properties file
644     * <p/>
645     * Example classpath:
646     * <p/>
647     * META-INF/widget.properties
648     * <p/>
649     * ResourceFinder finder = new ResourceFinder("META-INF/");
650     * Properties widgetProps = finder.findProperties("widget.properties");
651     *
652     * @param uri
653     * @return
654     * @throws IOException if the URL cannot be read or is not in properties file format
655     */
656    public Properties findProperties(String uri) throws IOException {
657        String fulluri = path + uri;
658
659        URL resource = getResource(fulluri);
660        if (resource == null) {
661            throw new IOException("Could not find resource: " + fulluri);
662        }
663
664        return loadProperties(resource);
665    }
666
667    /**
668     * Finds the corresponding resources and reads them in as a properties files
669     * <p/>
670     * Any URL that cannot be read in as a properties file will cause an exception to be thrown.
671     * <p/>
672     * Example classpath:
673     * <p/>
674     * META-INF/app.properties
675     * META-INF/app.properties
676     * META-INF/app.properties
677     * <p/>
678     * ResourceFinder finder = new ResourceFinder("META-INF/");
679     * List<Properties> appProps = finder.findAllProperties("app.properties");
680     *
681     * @param uri
682     * @return
683     * @throws IOException if the URL cannot be read or is not in properties file format
684     */
685    public List<Properties> findAllProperties(String uri) throws IOException {
686        String fulluri = path + uri;
687
688        List<Properties> properties = new ArrayList<Properties>();
689
690        Enumeration<URL> resources = getResources(fulluri);
691        while (resources.hasMoreElements()) {
692            URL url = resources.nextElement();
693            Properties props = loadProperties(url);
694            properties.add(props);
695        }
696        return properties;
697    }
698
699    /**
700     * Finds the corresponding resources and reads them in as a properties files
701     * <p/>
702     * Any URL that cannot be read in as a properties file will be added to the
703     * 'resourcesNotLoaded' collection.
704     * <p/>
705     * Example classpath:
706     * <p/>
707     * META-INF/app.properties
708     * META-INF/app.properties
709     * META-INF/app.properties
710     * <p/>
711     * ResourceFinder finder = new ResourceFinder("META-INF/");
712     * List<Properties> appProps = finder.findAvailableProperties("app.properties");
713     *
714     * @param uri
715     * @return
716     * @throws IOException if classLoader.getResources throws an exception
717     */
718    public List<Properties> findAvailableProperties(String uri) throws IOException {
719        resourcesNotLoaded.clear();
720        String fulluri = path + uri;
721
722        List<Properties> properties = new ArrayList<Properties>();
723
724        Enumeration<URL> resources = getResources(fulluri);
725        while (resources.hasMoreElements()) {
726            URL url = resources.nextElement();
727            try {
728                Properties props = loadProperties(url);
729                properties.add(props);
730            } catch (Exception notAvailable) {
731                resourcesNotLoaded.add(url.toExternalForm());
732            }
733        }
734        return properties;
735    }
736
737    /**
738     * Finds the corresponding resources and reads them in as a properties files
739     * <p/>
740     * Any URL that cannot be read in as a properties file will cause an exception to be thrown.
741     * <p/>
742     * Example classpath:
743     * <p/>
744 - META-INF/jdbcDrivers/oracle.properties
745 - META-INF/jdbcDrivers/mysql.props
746 - META-INF/jdbcDrivers/derby
747     * <p/>
748     * ResourceFinder finder = new ResourceFinder("META-INF/");
749     * List<Properties> driversList = finder.findAvailableProperties("jdbcDrivers");
750     * Properties oracleProps = driversList.get("oracle.properties");
751     * Properties mysqlProps = driversList.get("mysql.props");
752     * Properties derbyProps = driversList.get("derby");
753     *
754     * @param uri
755     * @return
756     * @throws IOException if the URL cannot be read or is not in properties file format
757     */
758    public Map<String, Properties> mapAllProperties(String uri) throws IOException {
759        Map<String, Properties> propertiesMap = new HashMap<String, Properties>();
760        Map<String, URL> map = getResourcesMap(uri);
761        for (Iterator iterator = map.entrySet().iterator(); iterator.hasNext();) {
762            Map.Entry entry = (Map.Entry) iterator.next();
763            String string = (String) entry.getKey();
764            URL url = (URL) entry.getValue();
765            Properties properties = loadProperties(url);
766            propertiesMap.put(string, properties);
767        }
768        return propertiesMap;
769    }
770
771    /**
772     * Finds the corresponding resources and reads them in as a properties files
773     * <p/>
774     * Any URL that cannot be read in as a properties file will be added to the
775     * 'resourcesNotLoaded' collection.
776     * <p/>
777     * Example classpath:
778     * <p/>
779     * META-INF/jdbcDrivers/oracle.properties
780     * META-INF/jdbcDrivers/mysql.props
781     * META-INF/jdbcDrivers/derby
782     * <p/>
783     * ResourceFinder finder = new ResourceFinder("META-INF/");
784     * List<Properties> driversList = finder.findAvailableProperties("jdbcDrivers");
785     * Properties oracleProps = driversList.get("oracle.properties");
786     * Properties mysqlProps = driversList.get("mysql.props");
787     * Properties derbyProps = driversList.get("derby");
788     *
789     * @param uri
790     * @return
791     * @throws IOException if classLoader.getResources throws an exception
792     */
793    public Map<String, Properties> mapAvailableProperties(String uri) throws IOException {
794        resourcesNotLoaded.clear();
795        Map<String, Properties> propertiesMap = new HashMap<String, Properties>();
796        Map<String, URL> map = getResourcesMap(uri);
797        for (Iterator iterator = map.entrySet().iterator(); iterator.hasNext();) {
798            Map.Entry entry = (Map.Entry) iterator.next();
799            String string = (String) entry.getKey();
800            URL url = (URL) entry.getValue();
801            try {
802                Properties properties = loadProperties(url);
803                propertiesMap.put(string, properties);
804            } catch (Exception notAvailable) {
805                resourcesNotLoaded.add(url.toExternalForm());
806            }
807        }
808        return propertiesMap;
809    }
810
811    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
812    //
813    //   Map Resources
814    //
815    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
816
817    public Map<String, URL> getResourcesMap(String uri) throws IOException {
818        String basePath = path + uri;
819
820        Map<String, URL> resources = new HashMap<String, URL>();
821        if (!basePath.endsWith("/")) {
822            basePath += "/";
823        }
824        Enumeration<URL> urls = getResources(basePath);
825
826        while (urls.hasMoreElements()) {
827            URL location = urls.nextElement();
828
829            try {
830                if (location.getProtocol().equals("jar")) {
831
832                    readJarEntries(location, basePath, resources);
833
834                } else if (location.getProtocol().equals("file")) {
835
836                    readDirectoryEntries(location, resources);
837
838                }
839            } catch (Exception e) {
840            }
841        }
842
843        return resources;
844    }
845
846    private static void readDirectoryEntries(URL location, Map<String, URL> resources) throws MalformedURLException {
847        File dir = new File(decode(location.getPath()));
848        if (dir.isDirectory()) {
849            File[] files = dir.listFiles();
850            for (File file : files) {
851                if (!file.isDirectory()) {
852                    String name = file.getName();
853                    URL url = file.toURI().toURL();
854                    resources.put(name, url);
855                }
856            }
857        }
858    }
859
860    private static void readJarEntries(URL location, String basePath, Map<String, URL> resources) throws IOException {
861        JarURLConnection conn = (JarURLConnection) location.openConnection();
862        JarFile jarfile = null;
863        jarfile = conn.getJarFile();
864
865        Enumeration<JarEntry> entries = jarfile.entries();
866        while (entries != null && entries.hasMoreElements()) {
867            JarEntry entry = entries.nextElement();
868            String name = entry.getName();
869
870            if (entry.isDirectory() || !name.startsWith(basePath) || name.length() == basePath.length()) {
871                continue;
872            }
873
874            name = name.substring(basePath.length());
875
876            if (name.contains("/")) {
877                continue;
878            }
879
880            URL resource = new URL(location, name);
881            resources.put(name, resource);
882        }
883    }
884
885    private Properties loadProperties(URL resource) throws IOException {
886        InputStream in = resource.openStream();
887
888        BufferedInputStream reader = null;
889        try {
890            reader = new BufferedInputStream(in);
891            Properties properties = new Properties();
892            properties.load(reader);
893
894            return properties;
895        } finally {
896            try {
897                in.close();
898                reader.close();
899            } catch (Exception e) {
900            }
901        }
902    }
903
904    private String readContents(URL resource) throws IOException {
905        InputStream in = resource.openStream();
906        BufferedInputStream reader = null;
907        StringBuffer sb = new StringBuffer();
908
909        try {
910            reader = new BufferedInputStream(in);
911
912            int b = reader.read();
913            while (b != -1) {
914                sb.append((char) b);
915                b = reader.read();
916            }
917
918            return sb.toString().trim();
919        } finally {
920            try {
921                in.close();
922                reader.close();
923            } catch (Exception e) {
924            }
925        }
926    }
927
928    public URL getResource(String fullUri) {
929        if (urls == null){
930            return classLoader.getResource(fullUri);
931        }
932        return findResource(fullUri, urls);
933    }
934
935    private Enumeration<URL> getResources(String fulluri) throws IOException {
936        if (urls == null) {
937            return classLoader.getResources(fulluri);
938        }
939        Vector<URL> resources = new Vector();
940        for (URL url : urls) {
941            URL resource = findResource(fulluri, url);
942            if (resource != null){
943                resources.add(resource);
944            }
945        }
946        return resources.elements();
947    }
948
949    private URL findResource(String resourceName, URL... search) {
950        for (int i = 0; i < search.length; i++) {
951            URL currentUrl = search[i];
952            if (currentUrl == null) {
953                continue;
954            }            
955
956            try {
957                String protocol = currentUrl.getProtocol();
958                if (protocol.equals("jar")) {
959                    /*
960                    * If the connection for currentUrl or resURL is
961                    * used, getJarFile() will throw an exception if the
962                    * entry doesn't exist.
963                    */
964                    URL jarURL = ((JarURLConnection) currentUrl.openConnection()).getJarFileURL();
965                    JarFile jarFile;
966                    JarURLConnection juc;
967                    try {
968                        juc = (JarURLConnection) new URL("jar", "", jarURL.toExternalForm() + "!/").openConnection();
969                        jarFile = juc.getJarFile();
970                    } catch (IOException e) {
971                        // Don't look for this jar file again
972                        search[i] = null;
973                        throw e;
974                    }
975
976                    try {
977                        juc = (JarURLConnection) new URL("jar", "", jarURL.toExternalForm() + "!/").openConnection();                        
978                        jarFile = juc.getJarFile();
979                        String entryName;
980                        if (currentUrl.getFile().endsWith("!/")) {
981                            entryName = resourceName;
982                        } else {
983                            String file = currentUrl.getFile();
984                            int sepIdx = file.lastIndexOf("!/");
985                            if (sepIdx == -1) {
986                                // Invalid URL, don't look here again
987                                search[i] = null;
988                                continue;
989                            }
990                            sepIdx += 2;
991                            StringBuffer sb = new StringBuffer(file.length() - sepIdx + resourceName.length());
992                            sb.append(file.substring(sepIdx));
993                            sb.append(resourceName);
994                            entryName = sb.toString();
995                        }
996                        if (entryName.equals("META-INF/") && jarFile.getEntry("META-INF/MANIFEST.MF") != null) {
997                            return targetURL(currentUrl, "META-INF/MANIFEST.MF");
998                        }
999                        if (jarFile.getEntry(entryName) != null) {
1000                            return targetURL(currentUrl, resourceName);
1001                        }
1002                    } finally {
1003                        if (!juc.getUseCaches()) {
1004                            try {
1005                                jarFile.close();
1006                            } catch (Exception e) {
1007                            }
1008                        }
1009                    }
1010                    
1011                } else if (protocol.equals("file")) {
1012                    String baseFile = currentUrl.getFile();
1013                    String host = currentUrl.getHost();
1014                    int hostLength = 0;
1015                    if (host != null) {
1016                        hostLength = host.length();
1017                    }
1018                    StringBuffer buf = new StringBuffer(2 + hostLength + baseFile.length() + resourceName.length());
1019
1020                    if (hostLength > 0) {
1021                        buf.append("//").append(host);
1022                    }
1023                    // baseFile should always ends with '/'
1024                    buf.append(baseFile);
1025                    if (!baseFile.endsWith("/")) {
1026                        buf.append("/");
1027                    }
1028
1029                    String fixedResName = resourceName;
1030                    // Do not create a UNC path, i.e. \\host
1031                    while (fixedResName.startsWith("/") || fixedResName.startsWith("\\")) {
1032                        fixedResName = fixedResName.substring(1);
1033                    }
1034                    buf.append(fixedResName);
1035                    String filename = buf.toString();
1036                    File file = new File(filename);
1037                    File file2 = new File(decode(filename));
1038
1039                    if (file.exists() || file2.exists()) {
1040                        return targetURL(currentUrl, fixedResName);
1041                    }
1042                } else {
1043                    URL resourceURL = targetURL(currentUrl, resourceName);
1044                    URLConnection urlConnection = resourceURL.openConnection();
1045
1046                    try {
1047                        urlConnection.getInputStream().close();
1048                    } catch (SecurityException e) {
1049                        return null;
1050                    }
1051                    // HTTP can return a stream on a non-existent file
1052                    // So check for the return code;
1053                    if (!resourceURL.getProtocol().equals("http")) {
1054                        return resourceURL;
1055                    }
1056
1057                    int code = ((HttpURLConnection) urlConnection).getResponseCode();
1058                    if (code >= 200 && code < 300) {
1059                        return resourceURL;
1060                    }
1061                }
1062            } catch (MalformedURLException e) {
1063                // Keep iterating through the URL list
1064            } catch (IOException e) {
1065            } catch (SecurityException e) {
1066            }
1067        }
1068        return null;
1069    }
1070
1071    private URL targetURL(URL base, String name) throws MalformedURLException {
1072        final String baseFile = base.getFile();
1073        final StringBuffer sb = new StringBuffer(baseFile.length() + name.length());
1074        sb.append(baseFile);
1075        if (!baseFile.endsWith("/")) {
1076            sb.append("/");
1077        }
1078        sb.append(name);
1079        return new URL(base.getProtocol(), base.getHost(), base.getPort(), sb.toString(), null);
1080    }
1081
1082    public static String decode(String fileName) {
1083        if (fileName.indexOf('%') == -1) return fileName;
1084
1085        StringBuilder result = new StringBuilder(fileName.length());
1086        ByteArrayOutputStream out = new ByteArrayOutputStream();
1087
1088        for (int i = 0; i < fileName.length();) {
1089            char c = fileName.charAt(i);
1090
1091            if (c == '%') {
1092                out.reset();
1093                do {
1094                    if (i + 2 >= fileName.length()) {
1095                        throw new IllegalArgumentException("Incomplete % sequence at: " + i);
1096                    }
1097
1098                    int d1 = Character.digit(fileName.charAt(i + 1), 16);
1099                    int d2 = Character.digit(fileName.charAt(i + 2), 16);
1100
1101                    if (d1 == -1 || d2 == -1) {
1102                        throw new IllegalArgumentException("Invalid % sequence (" + fileName.substring(i, i + 3) + ") at: " + String.valueOf(i));
1103                    }
1104
1105                    out.write((byte) ((d1 << 4) + d2));
1106
1107                    i += 3;
1108
1109                } while (i < fileName.length() && fileName.charAt(i) == '%');
1110
1111
1112                result.append(out.toString());
1113
1114                continue;
1115            } else {
1116                result.append(c);
1117            }
1118
1119            i++;
1120        }
1121        return result.toString();
1122    }
1123
1124}