1   /*
2    * AdaptiveClassLoader.java
3    *
4    * Created on July 29, 2003, 8:25 PM
5    */
6   
7   package main;
8   
9   /**
10   * Borrowed class off the internet
11   * @author  UNKNOWN, Modified by: Simon Zienkiewicz
12   */
13  import java.io.*;
14  import java.net.*;
15  import java.text.*;
16  import java.util.*;
17  import java.util.zip.*;
18   
19  public class AdaptiveClassLoader extends ClassLoader {
20  
21      /**
22       * Generation counter, incremented for each classloader as they are
23       * created.
24       */
25      static private int generationCounter = 0;
26  
27      /**
28       * Generation number of the classloader, used to distinguish between
29       * different instances.
30       */
31      private int generation;
32  
33      /**
34       * Cache of the loaded classes. This contains ClassCacheEntry keyed
35       * by class names.
36       */
37      private Hashtable cache;
38  
39      /**
40       * The classpath which this classloader searches for class definitions.
41       * Each element of the vector should be either a directory, a .zip
42       * file, or a .jar file.
43       * <p>
44       * It may be empty when only system classes are controlled.
45       */
46      private Vector repository;
47  
48      /**
49       * Private class used to maintain information about the classes that
50       * we loaded.
51       */
52      private static class ClassCacheEntry {
53  
54          /**
55           * The actual loaded class
56           */
57          Class loadedClass;
58  
59          /**
60           * The file from which this class was loaded; or null if
61           * it was loaded from the system.
62           */
63          File origin;
64  
65          /**
66           * The time at which the class was loaded from the origin
67           * file, in ms since the epoch.
68           */
69          long lastModified;
70  
71          /**
72           * Check whether this class was loaded from the system.
73           */
74          public boolean isSystemClass() {
75              return origin == null;
76          }
77      }
78  
79      //------------------------------------------------------- Constructors
80  
81      /**
82       * Creates a new class loader that will load classes from specified
83       * class repositories.
84       *
85       * @param classRepository An set of File classes indicating
86       *        directories and/or zip/jar files. It may be empty when
87       *        only system classes are loaded.
88       * @throw java.lang.IllegalArgumentException if the objects contained
89       *        in the vector are not a file instance or the file is not
90       *        a valid directory or a zip/jar file.
91       */
92      public AdaptiveClassLoader(Vector classRepository)
93          throws IllegalArgumentException
94      {
95          // Create the cache of loaded classes
96          cache = new Hashtable();
97  
98          // Verify that all the repository are valid.
99          Enumeration e = classRepository.elements();
100         while(e.hasMoreElements()) {
101             Object o = e.nextElement();
102             File file;
103 
104             // Check to see if element is a File instance.
105             try {
106                 file = (File) o;
107             } catch (ClassCastException objectIsNotFile) {
108                 throw new IllegalArgumentException("Object " + o
109                     + "is not a valid \"File\" instance");
110             }
111 
112             // Check to see if we have proper access.
113             if (!file.exists()) {
114                 throw new IllegalArgumentException("Repository "
115                     + file.getAbsolutePath() + " doesn't exist!");
116             } else if (!file.canRead()) {
117                 throw new IllegalArgumentException(
118                     "Don't have read access for file "
119                      + file.getAbsolutePath());
120             }
121 
122             // Check that it is a directory or zip/jar file
123             if (!(file.isDirectory() || isZipOrJarArchive(file))) {
124                 throw new IllegalArgumentException(file.getAbsolutePath()
125                     + " is not a directory or zip/jar file"
126                     + " or if it's a zip/jar file then it is corrupted.");
127             }
128         }
129 
130         // Store the class repository for use
131         this.repository = classRepository;
132 
133         // Increment and store generation counter
134         this.generation = generationCounter++;
135     }
136 
137     //------------------------------------------------------- Methods
138 
139     /**
140      * Test if a file is a ZIP or JAR archive.
141      *
142      * @param file the file to be tested.
143      * @return true if the file is a ZIP/JAR archive, false otherwise.
144      */
145     private boolean isZipOrJarArchive(File file) {
146         boolean isArchive = true;
147         ZipFile zipFile = null;
148 
149         try {
150             zipFile = new ZipFile(file);
151         } catch (ZipException zipCurrupted) {
152             isArchive = false;
153         } catch (IOException anyIOError) {
154             isArchive = false;
155         } finally {
156             if (zipFile != null) {
157                 try {
158                     zipFile.close();
159                 } catch (IOException ignored) {}
160             }
161         }
162 
163         return isArchive;
164     }
165 
166     /**
167      * Check to see if a given class should be reloaded because of a
168      * modification to the original class.
169      *
170      * @param className The name of the class to check for modification.
171      */
172     public synchronized boolean shouldReload(String classname) {
173 
174         ClassCacheEntry entry = (ClassCacheEntry) cache.get(classname);
175         
176         if (entry == null) {
177             // class wasn't even loaded
178             return false;
179         } 
180         
181         else if (entry.isSystemClass()) {
182             //System classes cannot be reloaded
183             return false;
184         }
185         
186         else {
187             boolean reload = (entry.origin.lastModified() != entry.lastModified);
188             return reload;
189         }
190     }
191 
192     /**
193      * Check whether the classloader should be reinstantiated.
194      * <P>
195      * The classloader must be replaced if there is any class whose
196         * origin file has changed since it was last loaded.
197      */
198     public synchronized boolean shouldReload() {
199 
200         // Check whether any class has changed
201         Enumeration e = cache.elements();
202         while (e.hasMoreElements()) {
203             ClassCacheEntry entry = (ClassCacheEntry) e.nextElement();
204 
205         if (entry.isSystemClass()) continue;
206 
207             // XXX: Because we want the classloader to be an accurate
208             // reflection of the contents of the repository, we also
209             // reload if a class origin file is now missing.  This
210             // probably makes things a bit more fragile, but is OK in
211             // a servlet development situation. <mbp@pharos.com.au>
212 
213             long msOrigin = entry.origin.lastModified();
214 
215             if (msOrigin == 0) {
216                 // class no longer exists
217                 return true;
218             }
219 
220             if (msOrigin != entry.lastModified) {
221                 // class is modified
222                 return true;
223             }
224         }
225 
226         // No changes, no need to reload
227         return false;
228     }
229 
230     /**
231      * Re-instantiate this class loader.
232      * <p>
233      * This method creates a new instance
234      * of the class loader that will load classes form the same path
235      * as this one.
236      */
237     public AdaptiveClassLoader reinstantiate() {
238         return new AdaptiveClassLoader(repository);
239     }
240 
241     //------------------------------------ Implementation of Classloader
242 
243     /*
244      * XXX: The javadoc for java.lang.ClassLoader says that the
245      * ClassLoader should cache classes so that it can handle repeated
246      * requests for the same class.  On the other hand, the JLS seems
247      * to imply that each classloader is only asked to load each class
248      * once.  Is this a contradiction?
249      *
250      * Perhaps the second call only applies to classes which have been
251      * garbage-collected?
252      */
253 
254     /**
255      * Resolves the specified name to a Class. The method loadClass()
256      * is called by the virtual machine.  As an abstract method,
257      * loadClass() must be defined in a subclass of ClassLoader.
258      * Modified By: Simon Zienkiewicz
259      *
260      * @param      file the class file
261      * @param      resolve true if the Class needs to be resolved;
262      *             false if the virtual machine just wants to determine
263      *             whether the class exists or not
264      * @return     the resulting Class.
265      * @exception  ClassNotFoundException  if the class loader cannot
266      *             find a the requested class.
267      */
268     protected synchronized Class loadClass(File file, boolean resolve) throws ClassNotFoundException
269     {
270         // The class object that will be returned.
271         Class c = null;
272 
273         // Use the cached value, if this class is already loaded into
274         // this classloader.
275         String name = file.getName();
276         name=name.substring(0,file.getName().length() - 6);
277                 
278         ClassCacheEntry entry = (ClassCacheEntry) cache.get(name);
279         
280         if (entry != null) {
281             // Class found in our cache
282             c = entry.loadedClass;
283             
284             if (resolve) resolveClass(c);
285             return c;
286         }
287 
288         // Try to load it from each repository
289         Enumeration repEnum = repository.elements();
290 
291         // Cache entry.
292         ClassCacheEntry classCache = new ClassCacheEntry();
293                
294         byte[] classData=null;
295 
296         file = new File((file.getAbsolutePath()).substring(0,(file.getAbsolutePath()).length()-(name.length()+6)));
297         
298         try {
299             if (file.isDirectory()) {
300                 classData = loadClassFromDirectory(file, name,classCache);
301             } 
302 
303         } 
304         
305         catch(IOException ioe) {
306             // Error while reading in data, consider it as not found
307             classData = null;
308         }
309 
310         if (classData != null) {
311             // Define the class
312             c = defineClass(name, classData, 0, classData.length);
313             
314             // Cache the result;
315             classCache.loadedClass = c;
316             
317             // Origin is set by the specific loader
318             classCache.lastModified = classCache.origin.lastModified();
319             cache.put(name, classCache);
320 
321             // Resolve it if necessary
322             if (resolve) resolveClass(c);
323             return c;
324         }
325         //}
326         
327         return c;
328         // If not found in any repositor
329     }
330 
331     /**
332      * Load a class using the system classloader.
333      *
334      * @exception  ClassNotFoundException  if the class loader cannot
335      *             find a the requested class.
336      * @exception  NoClassDefFoundError  if the class loader cannot
337      *             find a definition for the class.
338      */
339     private Class loadSystemClass(String name, boolean resolve)
340         throws NoClassDefFoundError, ClassNotFoundException
341     {
342         Class c = findSystemClass(name);
343         // Throws if not found.
344 
345         // Add cache entry
346         ClassCacheEntry cacheEntry = new ClassCacheEntry();
347         cacheEntry.origin = null;
348         cacheEntry.loadedClass = c;
349         cacheEntry.lastModified = Long.MAX_VALUE;
350         cache.put(name, cacheEntry);
351 
352         if (resolve) resolveClass(c);
353 
354         return c;
355     }
356 
357     /**
358      * Checks whether a classloader is allowed to define a given class,
359      * within the security manager restrictions.
360      */
361     // XXX: Should we perhaps also not allow classes to be dynamically
362     // loaded from org.apache.jserv.*?  Would it introduce security
363     // problems if people could override classes here?
364     // <mbp@humbug.org.au 1998-07-29>
365     private boolean securityAllowsClass(String className) {
366         try {
367             SecurityManager security = System.getSecurityManager();
368 
369             if (security == null) {
370                 // if there's no security manager then all classes
371                 // are allowed to be loaded
372                 return true;
373             }
374 
375             int lastDot = className.lastIndexOf('.');
376             // Check if we are allowed to load the class' package
377             security.checkPackageDefinition((lastDot > -1)
378                 ? className.substring(0, lastDot) : "");
379             // Throws if not allowed
380             return true;
381         } catch (SecurityException e) {
382             return false;
383         }
384     }
385 
386     /**
387      * Tries to load the class from a directory.
388      * Modified by: Simon Zienkiewicz
389      *
390      * @param dir The directory that contains classes.
391      * @param name The classname
392      * @param cache The cache entry to set the file if successful.
393      */
394     private byte[] loadClassFromDirectory(File dir, String name, ClassCacheEntry cache)
395             throws IOException
396     {
397         // Translate class name to file name
398         String classFileName =
399             name.replace('.', File.separatorChar) + ".class";
400 
401         // Check for garbage input at beginning of file name
402         // i.e. ../ or similar
403         if (!Character.isJavaIdentifierStart(classFileName.charAt(0))) {
404             // Find real beginning of class name
405             int start = 1;
406             while (!Character.isJavaIdentifierStart(
407                 classFileName.charAt(start++)));
408             classFileName = classFileName.substring(start);
409         }
410 
411         File classFile = new File(dir, classFileName);
412         
413         if (classFile.exists()) {
414             cache.origin = classFile;
415             InputStream in = new FileInputStream(classFile);
416             try {
417                 return loadBytesFromStream(in, (int) classFile.length());
418             } finally {
419                 in.close();
420             }
421         } else {
422             // Not found
423             return null;
424         }
425         
426     }
427 
428     /**
429      * Tries to load the class from a zip file.
430      *
431      * @param file The zipfile that contains classes.
432      * @param name The classname
433      * @param cache The cache entry to set the file if successful.
434      */
435     private byte[] loadClassFromZipfile(File file, String name,
436             ClassCacheEntry cache)
437         throws IOException
438     {
439         // Translate class name to file name
440         String classFileName = name.replace('.', '/') + ".class";
441 
442         ZipFile zipfile = new ZipFile(file);
443 
444         try {
445             ZipEntry entry = zipfile.getEntry(classFileName);
446             if (entry != null) {
447                 cache.origin = file;
448                 return loadBytesFromStream(zipfile.getInputStream(entry),
449                     (int) entry.getSize());
450             } else {
451                 // Not found
452                 return null;
453             }
454         } finally {
455             zipfile.close();
456         }
457     }
458 
459     /**
460      * Loads all the bytes of an InputStream.
461      */
462     private byte[] loadBytesFromStream(InputStream in, int length)
463         throws IOException
464     {
465         byte[] buf = new byte[length];
466         int nRead, count = 0;
467 
468         while ((length > 0) && ((nRead = in.read(buf,count,length)) != -1)) {
469             count += nRead;
470             length -= nRead;
471         }
472 
473         return buf;
474     }
475 
476     /**
477      * Get an InputStream on a given resource.  Will return null if no
478      * resource with this name is found.
479      * <p>
480      * The JServClassLoader translate the resource's name to a file
481      * or a zip entry. It looks for the resource in all its repository
482      * entry.
483      *
484      * @see     java.lang.Class#getResourceAsStream(String)
485      * @param   name    the name of the resource, to be used as is.
486      * @return  an InputStream on the resource, or null if not found.
487      */
488     public InputStream getResourceAsStream(String name) {
489         // Try to load it from the system class
490         InputStream s = getSystemResourceAsStream(name);
491 
492         if (s == null) {
493             // Try to find it from every repository
494             Enumeration repEnum = repository.elements();
495             while (repEnum.hasMoreElements()) {
496                 File file = (File) repEnum.nextElement();
497                 if (file.isDirectory()) {
498                     s = loadResourceFromDirectory(file, name);
499                 } else {
500                     s = loadResourceFromZipfile(file, name);
501                 }
502 
503                 if (s != null) {
504                     break;
505                 }
506             }
507         }
508 
509         return s;
510     }
511 
512     /**
513      * Loads resource from a directory.
514      */
515     private InputStream loadResourceFromDirectory(File dir, String name) {
516         // Name of resources are always separated by /
517         String fileName = name.replace('/', File.separatorChar);
518         File resFile = new File(dir, fileName);
519 
520         if (resFile.exists()) {
521             try {
522                 return new FileInputStream(resFile);
523             } catch (FileNotFoundException shouldnothappen) {
524                 return null;
525             }
526         } else {
527             return null;
528         }
529     }
530 
531     /**
532      * Loads resource from a zip file
533      */
534     private InputStream loadResourceFromZipfile(File file, String name) {
535         try {
536             ZipFile zipfile = new ZipFile(file);
537             ZipEntry entry = zipfile.getEntry(name);
538 
539             if (entry != null) {
540                 return zipfile.getInputStream(entry);
541             } else {
542                 return null;
543             }
544         } catch(IOException e) {
545             return null;
546         }
547     }
548 
549     /**
550      * Find a resource with a given name.  The return is a URL to the
551      * resource. Doing a getContent() on the URL may return an Image,
552      * an AudioClip,or an InputStream.
553      * <p>
554      * This classloader looks for the resource only in the directory
555      * repository for this resource.
556      *
557      * @param   name    the name of the resource, to be used as is.
558      * @return  an URL on the resource, or null if not found.
559      */
560     public URL getResource(String name) {
561 
562         URL u = getSystemResource(name);
563         if (u != null) {
564             return u;
565         }
566 
567         // Load for it only in directories since no URL can point into
568         // a zip file.
569         Enumeration repEnum = repository.elements();
570         while (repEnum.hasMoreElements()) {
571             File file = (File) repEnum.nextElement();
572             if (file.isDirectory()) {
573                 String fileName = name.replace('/', File.separatorChar);
574                 File resFile = new File(file, fileName);
575                 if (resFile.exists()) {
576                     // Build a file:// URL form the file name
577                     try {
578                         return new URL("file://"
579                             + resFile.getAbsolutePath());
580                     } catch(java.net.MalformedURLException badurl) {
581                         badurl.printStackTrace();
582                         return null;
583                     }
584                 }
585             }
586         }
587 
588         // Not found
589         return null;
590     }
591 }
592