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.File;
021import java.io.IOException;
022import java.io.InputStream;
023import java.net.HttpURLConnection;
024import java.net.JarURLConnection;
025import java.net.MalformedURLException;
026import java.net.URL;
027import java.net.URLConnection;
028import java.net.URLDecoder;
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: 936567 $ $Date: 2010-04-22 01:41:32 +0200 (jeu. 22 avril 2010) $
044 */
045public class ResourceFinder {
046
047    private final URL[] urls;
048    private final String path;
049    private final ClassLoader classLoader;
050    private final List<String> resourcesNotLoaded = new ArrayList<String>();
051
052    public ResourceFinder(URL... urls) {
053        this(null, Thread.currentThread().getContextClassLoader(), urls);
054    }
055
056    public ResourceFinder(String path) {
057        this(path, Thread.currentThread().getContextClassLoader(), null);
058    }
059
060    public ResourceFinder(String path, URL... urls) {
061        this(path, Thread.currentThread().getContextClassLoader(), urls);
062    }
063
064    public ResourceFinder(String path, ClassLoader classLoader) {
065        this(path, classLoader, null);
066    }
067
068    public ResourceFinder(String path, ClassLoader classLoader, URL... urls) {
069        if (path == null){
070            path = "";
071        } else if (path.length() > 0 && !path.endsWith("/")) {
072            path += "/";
073        }
074        this.path = path;
075
076        if (classLoader == null) {
077            classLoader = Thread.currentThread().getContextClassLoader();
078        }
079        this.classLoader = classLoader;
080
081        for (int i = 0; urls != null && i < urls.length; i++) {
082            URL url = urls[i];
083            if (url == null || isDirectory(url) || url.getProtocol().equals("jar")) {
084                continue;
085            }
086            try {
087                urls[i] = new URL("jar", "", -1, url.toString() + "!/");
088            } catch (MalformedURLException e) {
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) == '/');
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 '" + fullUri + "'");
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 (Class) 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 (Iterator iterator = map.entrySet().iterator(); iterator.hasNext();) {
394            Map.Entry entry = (Map.Entry) iterator.next();
395            String string = (String) entry.getKey();
396            String className = (String) entry.getValue();
397            Class clazz = classLoader.loadClass(className);
398            classes.put(string, clazz);
399        }
400        return classes;
401    }
402
403    /**
404     * Executes mapAvailableStrings assuming the value of each entry in the
405     * map is the name of a class that should be loaded.
406     * <p/>
407     * Any class that cannot be loaded will be skipped and placed in the
408     * 'resourcesNotLoaded' collection.
409     * <p/>
410     * Example classpath:
411     * <p/>
412     * META-INF/xmlparsers/xerces
413     * META-INF/xmlparsers/crimson
414     * <p/>
415     * ResourceFinder finder = new ResourceFinder("META-INF/");
416     * Map map = finder.mapAvailableStrings("xmlparsers");
417     * map.contains("xerces");  // true
418     * map.contains("crimson");  // true
419     * Class xercesClass = map.get("xerces");
420     * Class crimsonClass = map.get("crimson");
421     *
422     * @param uri
423     * @return
424     * @throws IOException if classLoader.getResources throws an exception
425     */
426    public Map<String, Class> mapAvailableClasses(String uri) throws IOException {
427        resourcesNotLoaded.clear();
428        Map<String, Class> classes = new HashMap<String, Class>();
429        Map<String, String> map = mapAvailableStrings(uri);
430        for (Iterator iterator = map.entrySet().iterator(); iterator.hasNext();) {
431            Map.Entry entry = (Map.Entry) iterator.next();
432            String string = (String) entry.getKey();
433            String className = (String) entry.getValue();
434            try {
435                Class clazz = classLoader.loadClass(className);
436                classes.put(string, clazz);
437            } catch (Exception notAvailable) {
438                resourcesNotLoaded.add(className);
439            }
440        }
441        return classes;
442    }
443
444    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
445    //
446    //   Find Implementation
447    //
448    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
449
450    /**
451     * Assumes the class specified points to a file in the classpath that contains
452     * the name of a class that implements or is a subclass of the specfied class.
453     * <p/>
454     * Any class that cannot be loaded will be cause an exception to be thrown.
455     * <p/>
456     * Example classpath:
457     * <p/>
458     * META-INF/java.io.InputStream    # contains the classname org.acme.AcmeInputStream
459     * META-INF/java.io.OutputStream
460     * <p/>
461     * ResourceFinder finder = new ResourceFinder("META-INF/");
462     * Class clazz = finder.findImplementation(java.io.InputStream.class);
463     * clazz.getName();  // returns "org.acme.AcmeInputStream"
464     *
465     * @param interfase a superclass or interface
466     * @return
467     * @throws IOException            if the URL cannot be read
468     * @throws ClassNotFoundException if the class found is not loadable
469     * @throws ClassCastException     if the class found is not assignable to the specified superclass or interface
470     */
471    public Class findImplementation(Class interfase) throws IOException, ClassNotFoundException {
472        String className = findString(interfase.getName());
473        Class impl = classLoader.loadClass(className);
474        if (!interfase.isAssignableFrom(impl)) {
475            throw new ClassCastException("Class not of type: " + interfase.getName());
476        }
477        return impl;
478    }
479
480    /**
481     * Assumes the class specified points to a file in the classpath that contains
482     * the name of a class that implements or is a subclass of the specfied class.
483     * <p/>
484     * Any class that cannot be loaded or assigned to the specified interface will be cause
485     * an exception to be thrown.
486     * <p/>
487     * Example classpath:
488     * <p/>
489     * META-INF/java.io.InputStream    # contains the classname org.acme.AcmeInputStream
490     * META-INF/java.io.InputStream    # contains the classname org.widget.NeatoInputStream
491     * META-INF/java.io.InputStream    # contains the classname com.foo.BarInputStream
492     * <p/>
493     * ResourceFinder finder = new ResourceFinder("META-INF/");
494     * List classes = finder.findAllImplementations(java.io.InputStream.class);
495     * classes.contains("org.acme.AcmeInputStream");  // true
496     * classes.contains("org.widget.NeatoInputStream");  // true
497     * classes.contains("com.foo.BarInputStream");  // true
498     *
499     * @param interfase a superclass or interface
500     * @return
501     * @throws IOException            if the URL cannot be read
502     * @throws ClassNotFoundException if the class found is not loadable
503     * @throws ClassCastException     if the class found is not assignable to the specified superclass or interface
504     */
505    public List<Class> findAllImplementations(Class interfase) throws IOException, ClassNotFoundException {
506        List<Class> implementations = new ArrayList<Class>();
507        List<String> strings = findAllStrings(interfase.getName());
508        for (String className : strings) {
509            Class impl = classLoader.loadClass(className);
510            if (!interfase.isAssignableFrom(impl)) {
511                throw new ClassCastException("Class not of type: " + interfase.getName());
512            }
513            implementations.add(impl);
514        }
515        return implementations;
516    }
517
518    /**
519     * Assumes the class specified points to a file in the classpath that contains
520     * the name of a class that implements or is a subclass of the specfied class.
521     * <p/>
522     * Any class that cannot be loaded or are not assignable to the specified class will be
523     * skipped and placed in the 'resourcesNotLoaded' collection.
524     * <p/>
525     * Example classpath:
526     * <p/>
527     * META-INF/java.io.InputStream    # contains the classname org.acme.AcmeInputStream
528     * META-INF/java.io.InputStream    # contains the classname org.widget.NeatoInputStream
529     * META-INF/java.io.InputStream    # contains the classname com.foo.BarInputStream
530     * <p/>
531     * ResourceFinder finder = new ResourceFinder("META-INF/");
532     * List classes = finder.findAllImplementations(java.io.InputStream.class);
533     * classes.contains("org.acme.AcmeInputStream");  // true
534     * classes.contains("org.widget.NeatoInputStream");  // true
535     * classes.contains("com.foo.BarInputStream");  // true
536     *
537     * @param interfase a superclass or interface
538     * @return
539     * @throws IOException if classLoader.getResources throws an exception
540     */
541    public List<Class> findAvailableImplementations(Class interfase) throws IOException {
542        resourcesNotLoaded.clear();
543        List<Class> implementations = new ArrayList<Class>();
544        List<String> strings = findAvailableStrings(interfase.getName());
545        for (String className : strings) {
546            try {
547                Class impl = classLoader.loadClass(className);
548                if (interfase.isAssignableFrom(impl)) {
549                    implementations.add(impl);
550                } else {
551                    resourcesNotLoaded.add(className);
552                }
553            } catch (Exception notAvailable) {
554                resourcesNotLoaded.add(className);
555            }
556        }
557        return implementations;
558    }
559
560    /**
561     * Assumes the class specified points to a directory in the classpath that holds files
562     * containing the name of a class that implements or is a subclass of the specfied class.
563     * <p/>
564     * Any class that cannot be loaded or assigned to the specified interface will be cause
565     * an exception to be thrown.
566     * <p/>
567     * Example classpath:
568     * <p/>
569     * META-INF/java.net.URLStreamHandler/jar
570     * META-INF/java.net.URLStreamHandler/file
571     * META-INF/java.net.URLStreamHandler/http
572     * <p/>
573     * ResourceFinder finder = new ResourceFinder("META-INF/");
574     * Map map = finder.mapAllImplementations(java.net.URLStreamHandler.class);
575     * Class jarUrlHandler = map.get("jar");
576     * Class fileUrlHandler = map.get("file");
577     * Class httpUrlHandler = map.get("http");
578     *
579     * @param interfase a superclass or interface
580     * @return
581     * @throws IOException            if the URL cannot be read
582     * @throws ClassNotFoundException if the class found is not loadable
583     * @throws ClassCastException     if the class found is not assignable to the specified superclass or interface
584     */
585    public Map<String, Class> mapAllImplementations(Class interfase) throws IOException, ClassNotFoundException {
586        Map<String, Class> implementations = new HashMap<String, Class>();
587        Map<String, String> map = mapAllStrings(interfase.getName());
588        for (Iterator iterator = map.entrySet().iterator(); iterator.hasNext();) {
589            Map.Entry entry = (Map.Entry) iterator.next();
590            String string = (String) entry.getKey();
591            String className = (String) entry.getValue();
592            Class impl = classLoader.loadClass(className);
593            if (!interfase.isAssignableFrom(impl)) {
594                throw new ClassCastException("Class not of type: " + interfase.getName());
595            }
596            implementations.put(string, impl);
597        }
598        return implementations;
599    }
600
601    /**
602     * Assumes the class specified points to a directory in the classpath that holds files
603     * containing the name of a class that implements or is a subclass of the specfied class.
604     * <p/>
605     * Any class that cannot be loaded or are not assignable to the specified class will be
606     * skipped and placed in the 'resourcesNotLoaded' collection.
607     * <p/>
608     * Example classpath:
609     * <p/>
610     * META-INF/java.net.URLStreamHandler/jar
611     * META-INF/java.net.URLStreamHandler/file
612     * META-INF/java.net.URLStreamHandler/http
613     * <p/>
614     * ResourceFinder finder = new ResourceFinder("META-INF/");
615     * Map map = finder.mapAllImplementations(java.net.URLStreamHandler.class);
616     * Class jarUrlHandler = map.get("jar");
617     * Class fileUrlHandler = map.get("file");
618     * Class httpUrlHandler = map.get("http");
619     *
620     * @param interfase a superclass or interface
621     * @return
622     * @throws IOException if classLoader.getResources throws an exception
623     */
624    public Map<String, Class> mapAvailableImplementations(Class interfase) throws IOException {
625        resourcesNotLoaded.clear();
626        Map<String, Class> implementations = new HashMap<String, Class>();
627        Map<String, String> map = mapAvailableStrings(interfase.getName());
628        for (Iterator iterator = map.entrySet().iterator(); iterator.hasNext();) {
629            Map.Entry entry = (Map.Entry) iterator.next();
630            String string = (String) entry.getKey();
631            String className = (String) entry.getValue();
632            try {
633                Class impl = classLoader.loadClass(className);
634                if (interfase.isAssignableFrom(impl)) {
635                    implementations.put(string, impl);
636                } else {
637                    resourcesNotLoaded.add(className);
638                }
639            } catch (Exception notAvailable) {
640                resourcesNotLoaded.add(className);
641            }
642        }
643        return implementations;
644    }
645
646    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
647    //
648    //   Find Properties
649    //
650    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
651
652    /**
653     * Finds the corresponding resource and reads it in as a properties file
654     * <p/>
655     * Example classpath:
656     * <p/>
657     * META-INF/widget.properties
658     * <p/>
659     * ResourceFinder finder = new ResourceFinder("META-INF/");
660     * Properties widgetProps = finder.findProperties("widget.properties");
661     *
662     * @param uri
663     * @return
664     * @throws IOException if the URL cannot be read or is not in properties file format
665     */
666    public Properties findProperties(String uri) throws IOException {
667        String fulluri = path + uri;
668
669        URL resource = getResource(fulluri);
670        if (resource == null) {
671            throw new IOException("Could not find resource: " + fulluri);
672        }
673
674        return loadProperties(resource);
675    }
676
677    /**
678     * Finds the corresponding resources and reads them in as a properties files
679     * <p/>
680     * Any URL that cannot be read in as a properties file will cause an exception to be thrown.
681     * <p/>
682     * Example classpath:
683     * <p/>
684     * META-INF/app.properties
685     * META-INF/app.properties
686     * META-INF/app.properties
687     * <p/>
688     * ResourceFinder finder = new ResourceFinder("META-INF/");
689     * List<Properties> appProps = finder.findAllProperties("app.properties");
690     *
691     * @param uri
692     * @return
693     * @throws IOException if the URL cannot be read or is not in properties file format
694     */
695    public List<Properties> findAllProperties(String uri) throws IOException {
696        String fulluri = path + uri;
697
698        List<Properties> properties = new ArrayList<Properties>();
699
700        Enumeration<URL> resources = getResources(fulluri);
701        while (resources.hasMoreElements()) {
702            URL url = resources.nextElement();
703            Properties props = loadProperties(url);
704            properties.add(props);
705        }
706        return properties;
707    }
708
709    /**
710     * Finds the corresponding resources and reads them in as a properties files
711     * <p/>
712     * Any URL that cannot be read in as a properties file will be added to the
713     * 'resourcesNotLoaded' collection.
714     * <p/>
715     * Example classpath:
716     * <p/>
717     * META-INF/app.properties
718     * META-INF/app.properties
719     * META-INF/app.properties
720     * <p/>
721     * ResourceFinder finder = new ResourceFinder("META-INF/");
722     * List<Properties> appProps = finder.findAvailableProperties("app.properties");
723     *
724     * @param uri
725     * @return
726     * @throws IOException if classLoader.getResources throws an exception
727     */
728    public List<Properties> findAvailableProperties(String uri) throws IOException {
729        resourcesNotLoaded.clear();
730        String fulluri = path + uri;
731
732        List<Properties> properties = new ArrayList<Properties>();
733
734        Enumeration<URL> resources = getResources(fulluri);
735        while (resources.hasMoreElements()) {
736            URL url = resources.nextElement();
737            try {
738                Properties props = loadProperties(url);
739                properties.add(props);
740            } catch (Exception notAvailable) {
741                resourcesNotLoaded.add(url.toExternalForm());
742            }
743        }
744        return properties;
745    }
746
747    /**
748     * Finds the corresponding resources and reads them in as a properties files
749     * <p/>
750     * Any URL that cannot be read in as a properties file will cause an exception to be thrown.
751     * <p/>
752     * Example classpath:
753     * <p/>
754     * META-INF/jdbcDrivers/oracle.properties
755     * META-INF/jdbcDrivers/mysql.props
756     * META-INF/jdbcDrivers/derby
757     * <p/>
758     * ResourceFinder finder = new ResourceFinder("META-INF/");
759     * List<Properties> driversList = finder.findAvailableProperties("jdbcDrivers");
760     * Properties oracleProps = driversList.get("oracle.properties");
761     * Properties mysqlProps = driversList.get("mysql.props");
762     * Properties derbyProps = driversList.get("derby");
763     *
764     * @param uri
765     * @return
766     * @throws IOException if the URL cannot be read or is not in properties file format
767     */
768    public Map<String, Properties> mapAllProperties(String uri) throws IOException {
769        Map<String, Properties> propertiesMap = new HashMap<String, Properties>();
770        Map<String, URL> map = getResourcesMap(uri);
771        for (Iterator iterator = map.entrySet().iterator(); iterator.hasNext();) {
772            Map.Entry entry = (Map.Entry) iterator.next();
773            String string = (String) entry.getKey();
774            URL url = (URL) entry.getValue();
775            Properties properties = loadProperties(url);
776            propertiesMap.put(string, properties);
777        }
778        return propertiesMap;
779    }
780
781    /**
782     * Finds the corresponding resources and reads them in as a properties files
783     * <p/>
784     * Any URL that cannot be read in as a properties file will be added to the
785     * 'resourcesNotLoaded' collection.
786     * <p/>
787     * Example classpath:
788     * <p/>
789     * META-INF/jdbcDrivers/oracle.properties
790     * META-INF/jdbcDrivers/mysql.props
791     * META-INF/jdbcDrivers/derby
792     * <p/>
793     * ResourceFinder finder = new ResourceFinder("META-INF/");
794     * List<Properties> driversList = finder.findAvailableProperties("jdbcDrivers");
795     * Properties oracleProps = driversList.get("oracle.properties");
796     * Properties mysqlProps = driversList.get("mysql.props");
797     * Properties derbyProps = driversList.get("derby");
798     *
799     * @param uri
800     * @return
801     * @throws IOException if classLoader.getResources throws an exception
802     */
803    public Map<String, Properties> mapAvailableProperties(String uri) throws IOException {
804        resourcesNotLoaded.clear();
805        Map<String, Properties> propertiesMap = new HashMap<String, Properties>();
806        Map<String, URL> map = getResourcesMap(uri);
807        for (Iterator iterator = map.entrySet().iterator(); iterator.hasNext();) {
808            Map.Entry entry = (Map.Entry) iterator.next();
809            String string = (String) entry.getKey();
810            URL url = (URL) entry.getValue();
811            try {
812                Properties properties = loadProperties(url);
813                propertiesMap.put(string, properties);
814            } catch (Exception notAvailable) {
815                resourcesNotLoaded.add(url.toExternalForm());
816            }
817        }
818        return propertiesMap;
819    }
820
821    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
822    //
823    //   Map Resources
824    //
825    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
826
827    public Map<String, URL> getResourcesMap(String uri) throws IOException {
828        String basePath = path + uri;
829
830        Map<String, URL> resources = new HashMap<String, URL>();
831        if (!basePath.endsWith("/")) {
832            basePath += "/";
833        }
834        Enumeration<URL> urls = getResources(basePath);
835
836        while (urls.hasMoreElements()) {
837            URL location = urls.nextElement();
838
839            try {
840                if (location.getProtocol().equals("jar")) {
841
842                    readJarEntries(location, basePath, resources);
843
844                } else if (location.getProtocol().equals("file")) {
845
846                    readDirectoryEntries(location, resources);
847
848                }
849            } catch (Exception e) {
850            }
851        }
852
853        return resources;
854    }
855
856    private static void readDirectoryEntries(URL location, Map<String, URL> resources) throws MalformedURLException {
857        File dir = new File(URLDecoder.decode(location.getPath()));
858        if (dir.isDirectory()) {
859            File[] files = dir.listFiles();
860            for (File file : files) {
861                if (!file.isDirectory()) {
862                    String name = file.getName();
863                    URL url = file.toURI().toURL();
864                    resources.put(name, url);
865                }
866            }
867        }
868    }
869
870    private static void readJarEntries(URL location, String basePath, Map<String, URL> resources) throws IOException {
871        JarURLConnection conn = (JarURLConnection) location.openConnection();
872        JarFile jarfile = null;
873        jarfile = conn.getJarFile();
874
875        Enumeration<JarEntry> entries = jarfile.entries();
876        while (entries != null && entries.hasMoreElements()) {
877            JarEntry entry = entries.nextElement();
878            String name = entry.getName();
879
880            if (entry.isDirectory() || !name.startsWith(basePath) || name.length() == basePath.length()) {
881                continue;
882            }
883
884            name = name.substring(basePath.length());
885
886            if (name.contains("/")) {
887                continue;
888            }
889
890            URL resource = new URL(location, name);
891            resources.put(name, resource);
892        }
893    }
894
895    private Properties loadProperties(URL resource) throws IOException {
896        InputStream in = resource.openStream();
897
898        BufferedInputStream reader = null;
899        try {
900            reader = new BufferedInputStream(in);
901            Properties properties = new Properties();
902            properties.load(reader);
903
904            return properties;
905        } finally {
906            try {
907                in.close();
908                reader.close();
909            } catch (Exception e) {
910            }
911        }
912    }
913
914    private String readContents(URL resource) throws IOException {
915        InputStream in = resource.openStream();
916        BufferedInputStream reader = null;
917        StringBuffer sb = new StringBuffer();
918
919        try {
920            reader = new BufferedInputStream(in);
921
922            int b = reader.read();
923            while (b != -1) {
924                sb.append((char) b);
925                b = reader.read();
926            }
927
928            return sb.toString().trim();
929        } finally {
930            try {
931                in.close();
932                reader.close();
933            } catch (Exception e) {
934            }
935        }
936    }
937
938    private URL getResource(String fullUri) {
939        if (urls == null){
940            return classLoader.getResource(fullUri);
941        }
942        return findResource(fullUri, urls);
943    }
944
945    private Enumeration<URL> getResources(String fulluri) throws IOException {
946        if (urls == null) {
947            return classLoader.getResources(fulluri);
948        }
949        Vector<URL> resources = new Vector();
950        for (URL url : urls) {
951            URL resource = findResource(fulluri, url);
952            if (resource != null){
953                resources.add(resource);
954            }
955        }
956        return resources.elements();
957    }
958
959    private URL findResource(String resourceName, URL... search) {
960        for (int i = 0; i < search.length; i++) {
961            URL currentUrl = search[i];
962            if (currentUrl == null) {
963                continue;
964            }            
965
966            try {
967                String protocol = currentUrl.getProtocol();
968                if (protocol.equals("jar")) {
969                    /*
970                    * If the connection for currentUrl or resURL is
971                    * used, getJarFile() will throw an exception if the
972                    * entry doesn't exist.
973                    */
974                    URL jarURL = ((JarURLConnection) currentUrl.openConnection()).getJarFileURL();
975                    JarFile jarFile;
976                    JarURLConnection juc;
977                    try {
978                        juc = (JarURLConnection) new URL("jar", "", jarURL.toExternalForm() + "!/").openConnection();
979                        jarFile = juc.getJarFile();
980                    } catch (IOException e) {
981                        // Don't look for this jar file again
982                        search[i] = null;
983                        throw e;
984                    }
985
986                    try {
987                        juc = (JarURLConnection) new URL("jar", "", jarURL.toExternalForm() + "!/").openConnection();                        
988                        jarFile = juc.getJarFile();
989                        String entryName;
990                        if (currentUrl.getFile().endsWith("!/")) {
991                            entryName = resourceName;
992                        } else {
993                            String file = currentUrl.getFile();
994                            int sepIdx = file.lastIndexOf("!/");
995                            if (sepIdx == -1) {
996                                // Invalid URL, don't look here again
997                                search[i] = null;
998                                continue;
999                            }
1000                            sepIdx += 2;
1001                            StringBuffer sb = new StringBuffer(file.length() - sepIdx + resourceName.length());
1002                            sb.append(file.substring(sepIdx));
1003                            sb.append(resourceName);
1004                            entryName = sb.toString();
1005                        }
1006                        if (entryName.equals("META-INF/") && jarFile.getEntry("META-INF/MANIFEST.MF") != null) {
1007                            return targetURL(currentUrl, "META-INF/MANIFEST.MF");
1008                        }
1009                        if (jarFile.getEntry(entryName) != null) {
1010                            return targetURL(currentUrl, resourceName);
1011                        }
1012                    } finally {
1013                        if (!juc.getUseCaches()) {
1014                            try {
1015                                jarFile.close();
1016                            } catch (Exception e) {
1017                            }
1018                        }
1019                    }
1020                    
1021                } else if (protocol.equals("file")) {
1022                    String baseFile = currentUrl.getFile();
1023                    String host = currentUrl.getHost();
1024                    int hostLength = 0;
1025                    if (host != null) {
1026                        hostLength = host.length();
1027                    }
1028                    StringBuffer buf = new StringBuffer(2 + hostLength + baseFile.length() + resourceName.length());
1029
1030                    if (hostLength > 0) {
1031                        buf.append("//").append(host);
1032                    }
1033                    // baseFile always ends with '/'
1034                    buf.append(baseFile);
1035                    String fixedResName = resourceName;
1036                    // Do not create a UNC path, i.e. \\host
1037                    while (fixedResName.startsWith("/") || fixedResName.startsWith("\\")) {
1038                        fixedResName = fixedResName.substring(1);
1039                    }
1040                    buf.append(fixedResName);
1041                    String filename = buf.toString();
1042                    File file = new File(filename);
1043                    File file2 = new File(URLDecoder.decode(filename));
1044
1045                    if (file.exists() || file2.exists()) {
1046                        return targetURL(currentUrl, fixedResName);
1047                    }
1048                } else {
1049                    URL resourceURL = targetURL(currentUrl, resourceName);
1050                    URLConnection urlConnection = resourceURL.openConnection();
1051
1052                    try {
1053                        urlConnection.getInputStream().close();
1054                    } catch (SecurityException e) {
1055                        return null;
1056                    }
1057                    // HTTP can return a stream on a non-existent file
1058                    // So check for the return code;
1059                    if (!resourceURL.getProtocol().equals("http")) {
1060                        return resourceURL;
1061                    }
1062
1063                    int code = ((HttpURLConnection) urlConnection).getResponseCode();
1064                    if (code >= 200 && code < 300) {
1065                        return resourceURL;
1066                    }
1067                }
1068            } catch (MalformedURLException e) {
1069                // Keep iterating through the URL list
1070            } catch (IOException e) {
1071            } catch (SecurityException e) {
1072            }
1073        }
1074        return null;
1075    }
1076
1077    private URL targetURL(URL base, String name) throws MalformedURLException {
1078        StringBuffer sb = new StringBuffer(base.getFile().length() + name.length());
1079        sb.append(base.getFile());
1080        sb.append(name);
1081        String file = sb.toString();
1082        return new URL(base.getProtocol(), base.getHost(), base.getPort(), file, null);
1083    }
1084}