1   /**
2    * Distribution License:
3    * JSword is free software; you can redistribute it and/or modify it under
4    * the terms of the GNU Lesser General Public License, version 2.1 or later
5    * as published by the Free Software Foundation. This program is distributed
6    * in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
7    * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
8    * See the GNU Lesser General Public License for more details.
9    *
10   * The License is available on the internet at:
11   *      http://www.gnu.org/copyleft/lgpl.html
12   * or by writing to:
13   *      Free Software Foundation, Inc.
14   *      59 Temple Place - Suite 330
15   *      Boston, MA 02111-1307, USA
16   *
17   * © CrossWire Bible Society, 2005 - 2016
18   *
19   */
20  package org.crosswire.common.util;
21  
22  import java.net.URI;
23  import java.net.URL;
24  import java.security.AccessController;
25  import java.security.PrivilegedAction;
26  
27  /**
28   * CWClassLoader extends the regular class loader by using looking in more
29   * places. This is needed so that ResourceBundle can find resources that are not
30   * held in the same package as the class. This is expressed as a list of
31   * locations, called homes, that the program will look in.
32   * 
33   * @see gnu.lgpl.License The GNU Lesser General Public License for details.
34   * @author DM Smith
35   */
36  public final class CWClassLoader extends ClassLoader {
37      /**
38       * Creates a class loader that finds resources for the supplied class that
39       * may not be in the class' package. You can use this within base classes by
40       * passing getClass() to load resources for a derived class.
41       * 
42       * @param resourceOwner
43       *            is the owner of the resource
44       */
45      CWClassLoader(Class<?> resourceOwner) {
46          owner = resourceOwner;
47      }
48  
49      /**
50       * Creates a class loader that finds resources for the calling class that
51       * may not be in the class' package. Use this only within classes that are
52       * directly looking up their resources.
53       */
54      CWClassLoader() {
55          owner = CallContext.getCallingClass();
56      }
57  
58      /**
59       * Creates a privileged class loader that finds resources for the supplied
60       * class that may not be in the class' package. You can use this within base
61       * classes by passing getClass() to load resources for a derived class.
62       * 
63       * @param resourceOwner
64       *            is the owner of the resource
65       * @return the CrossWire Class Loader
66       */
67      public static CWClassLoader instance(Class<?> resourceOwner) {
68          return AccessController.doPrivileged(new PrivilegedLoader<CWClassLoader>(resourceOwner));
69      }
70  
71      /**
72       * Creates a privileged class loader that finds resources for the calling
73       * class that may not be in the class' package. Use this only within classes
74       * that are directly looking up their resources.
75       * 
76       * @return the CrossWire Class Loader
77       */
78      public static CWClassLoader instance() {
79          Class<? extends Object> resourceOwner = CallContext.getCallingClass();
80          return instance(resourceOwner);
81      }
82  
83      /* (non-Javadoc)
84       * @see java.lang.ClassLoader#findResource(java.lang.String)
85       */
86      @Override
87      public URL findResource(String search) {
88          URL resource = null;
89          if (search == null || search.length() == 0) {
90              return resource;
91          }
92  
93          // First look for it with an absolute path
94          // This allows developer overrides
95          if (search.charAt(0) != '/') {
96              resource = findResource('/' + search);
97          }
98  
99          if (resource == null) {
100             // Look for it in the class's package.
101             String newsearch = adjustPackageSearch(search);
102             if (!search.equals(newsearch)) {
103                 resource = findResource(newsearch);
104             }
105         }
106 
107         // Sometimes it comes in with '/' inside of it.
108         // So look for it as a file with '.' in the name
109         // This is the form that will find files in the resource.jar
110         if (resource == null) {
111             // Look for it in the class's package.
112             String newsearch = adjustPathSearch(search);
113             if (!search.equals(newsearch)) {
114                 resource = findResource(newsearch);
115             }
116         }
117 
118         // See if it can be found in a home directory
119         if (resource == null) {
120             URI homeResource = CWClassLoader.findHomeResource(search);
121             if (homeResource != null) {
122                 resource = NetUtil.toURL(homeResource);
123             }
124         }
125 
126         // See if it can be found by its own class.
127         if (resource == null) {
128             resource = owner.getResource(search);
129         }
130 
131         // Try the appropriate class loader
132         if (resource == null) {
133             resource = getClassLoader().getResource(search);
134         }
135 
136         // Try the bootstrap and the system loader
137         if (resource == null) {
138             resource = ClassLoader.getSystemResource(search);
139         }
140 
141         // For good measure let the super class try to find it.
142         if (resource == null) {
143             resource = super.findResource(search);
144         }
145 
146         return resource;
147     }
148 
149     /**
150      * Prefix the search with a package prefix, if not already. Skip a leading
151      * '/' if present.
152      * 
153      * @param aSearch the search to adjust
154      * @return the adjusted search
155      */
156     private String adjustPackageSearch(String aSearch) {
157         String search = aSearch;
158         // If it has embedded '/' there is nothing to do.
159         if (search.indexOf('/', 1) == -1) {
160             String className = owner.getName();
161             String pkgPrefix = className.substring(0, className.lastIndexOf('.') + 1);
162 
163             if (search.charAt(0) == '/') {
164                 String part = search.substring(1);
165                 if (!part.startsWith(pkgPrefix)) {
166                     search = '/' + pkgPrefix + part;
167                 }
168             } else {
169                 if (!search.startsWith(pkgPrefix)) {
170                     search = pkgPrefix + search;
171                 }
172             }
173         }
174 
175         return search;
176     }
177 
178     /**
179      * Change all but a leading '/' to '.'
180      * 
181      * @param aSearch the search to adjust
182      * @return the adjusted search
183      */
184     private String adjustPathSearch(String aSearch) {
185         String search = aSearch;
186         if (search.indexOf('/', 1) != -1) {
187             // Change all but a leading '/' to '.'
188             if (search.charAt(0) == '/') {
189                 search = '/' + search.substring(1).replace('/', '.');
190             } else {
191                 search = search.replace('/', '.');
192             }
193         }
194         return search;
195     }
196 
197     /**
198      * Pick the best class loader
199      * 
200      * @return the class loader
201      */
202     public ClassLoader getClassLoader() {
203         // Choose the child loader as it will use the parent if need be
204         // If they are not related then choose the context loader
205         ClassLoader loader = pickLoader(Thread.currentThread().getContextClassLoader(), owner.getClassLoader());
206         return pickLoader(loader, ClassLoader.getSystemClassLoader());
207     }
208 
209     /**
210      * Returns 'true' if 'loader2' is a delegation child of 'loader1' [or if
211      * 'loader1'=='loader2']. Of course, this works only for classloaders that
212      * set their parent pointers correctly. 'null' is interpreted as the
213      * primordial loader [i.e., everybody's parent].
214      * 
215      * @param loader1 a class loader to consider
216      * @param loader2 a class loader to consider
217      * @return one of the class loaders
218      */
219     private static ClassLoader pickLoader(ClassLoader loader1, ClassLoader loader2) {
220         ClassLoader loader = loader2;
221         if (loader1 != loader2) {
222             loader = loader1;
223             if (loader1 == null) {
224                 loader = loader2;
225             } else {
226                 // Is loader2 a descendant of loader1?
227                 // It is if we can walk up to the top and find it.
228                 for (ClassLoader curloader = loader2; curloader != null; curloader = curloader.getParent()) {
229                     if (curloader == loader1) {
230                         loader = loader2;
231                         break;
232                     }
233                 }
234             }
235         }
236         return loader;
237     }
238 
239     /**
240      * If the application has set the homes, it will return the application's
241      * requested home directory, otherwise it returns null.
242      * 
243      * @param i get the i-th home
244      * @return Returns the home.
245      */
246     public static synchronized URI getHome(int i) {
247         if (i > 0 && i < homes.length) {
248             return homes[i];
249         }
250         return null;
251     }
252 
253     /**
254      * Establish the applications home directory from where additional resources
255      * can be found. URL is expected to end with the directory name, not '/'.
256      * 
257      * @param newHomes
258      *            The home to set.
259      */
260     public static synchronized void setHome(URI[] newHomes) {
261         homes = new URI[newHomes.length];
262         System.arraycopy(newHomes, 0, homes, 0, newHomes.length);
263     }
264 
265     /**
266      * Look for the resource in the home directories, returning the first
267      * readable file.
268      * 
269      * @param search
270      *            must be non-null, non-empty
271      * @return the URI of the home of the resource
272      */
273     public static URI findHomeResource(String search) {
274         if (homes == null) {
275             return null;
276         }
277 
278         for (int i = 0; i < homes.length; i++) {
279             URI homeURI = homes[i];
280 
281             URI override = NetUtil.lengthenURI(homeURI, search);
282 
283             // Make sure the file exists and can be read
284             if (NetUtil.canRead(override)) {
285                 return override;
286             }
287         }
288 
289         return null;
290     }
291 
292     /**
293      * PrivilegedLoader creates a CWClassLoader if it is able to obtain java
294      * security permissions to do so.
295      * 
296      * @param <T> the type
297      */
298     private static class PrivilegedLoader<T> implements PrivilegedAction<T> {
299         /**
300          * Creates a privileged class loader that finds resources for the
301          * supplied class that may not be in the class' package. You can use
302          * this within base classes by passing getClass() to load resources for
303          * a derived class.
304          * 
305          * @param resourceOwner
306          *            is the owner of the resource
307          */
308         PrivilegedLoader(Class<?> resourceOwner) {
309             owningClass = resourceOwner;
310         }
311 
312         /* (non-Javadoc)
313          * @see java.security.PrivilegedAction#run()
314          */
315         public T run() {
316             return (T) new CWClassLoader(owningClass);
317         }
318 
319         private Class<?> owningClass;
320     }
321 
322     /**
323      * The class to which the resources belong
324      */
325     private Class<?> owner;
326 
327     /**
328      * Notion of a project's home from where additional resources can be found.
329      */
330     private static URI[] homes;
331 }
332