View Javadoc

1   package com.atlassian.plugin.classloader;
2   
3   import org.apache.commons.io.FileUtils;
4   import org.apache.commons.io.IOUtils;
5   import org.codehaus.classworlds.uberjar.protocol.jar.NonLockingJarHandler;
6   
7   import java.io.File;
8   import java.io.FileOutputStream;
9   import java.io.IOException;
10  import java.io.InputStream;
11  import java.net.URL;
12  import java.net.MalformedURLException;
13  import java.util.*;
14  import java.util.jar.JarEntry;
15  import java.util.jar.JarFile;
16  
17  /**
18   * A class loader used to load classes and resources from a given plugin.
19   */
20  public final class PluginClassLoader extends ClassLoader
21  {
22      private static final String PLUGIN_INNER_JAR_PREFIX = "atlassian-plugins-innerjar";
23      /**
24       * the list of inner jars
25       */
26      private final List/*<File>*/ pluginInnerJars;
27      /**
28       * Mapping of <String> names (resource, or class name) to the <URL>s where the resource or class can be found.
29       */
30      private final Map entryMappings = new HashMap();
31  
32      public PluginClassLoader(final File pluginFile)
33      {
34          this(pluginFile, null);
35      }
36  
37      public PluginClassLoader(final File pluginFile, ClassLoader parent)
38      {
39          super(parent);
40          try
41          {
42              if (pluginFile == null || !pluginFile.exists())
43              {
44                  throw new IllegalArgumentException("Plugin jar file must not be null and must exist.");
45              }
46              this.pluginInnerJars = new ArrayList();
47              initializeFromJar(pluginFile, true);
48          }
49          catch (IOException e)
50          {
51              throw new IllegalStateException(e.getMessage());
52          }
53      }
54  
55      /**
56       * Go through all entries in the given JAR, and recursively populate entryMappings by providing
57       * resource or Class name to URL mappings. 
58       *
59       * @param file the file to scan
60       * @throws IOException
61       */
62      private void initializeFromJar(File file, boolean isOuterJar) throws IOException
63      {
64          final JarFile jarFile = new JarFile(file);
65          try
66          {
67              for (Enumeration entries = jarFile.entries(); entries.hasMoreElements();)
68              {
69                  final JarEntry jarEntry = (JarEntry) entries.nextElement();
70                  if(isOuterJar && isInnerJarPath(jarEntry.getName()))
71                      initialiseInnerJar(jarFile, jarEntry);
72                  else
73                      addEntryMapping(jarEntry, file, isOuterJar);
74              }
75          }
76          finally
77          {
78              jarFile.close();
79          }
80      }
81  
82      private boolean isInnerJarPath(String name){
83           return name.startsWith("META-INF/lib/") && name.endsWith(".jar");
84      }
85  
86      private void initialiseInnerJar(JarFile jarFile, JarEntry jarEntry) throws IOException
87      {
88          InputStream inputStream = null;
89          FileOutputStream fileOutputStream = null;
90          try
91          {
92              final File innerJarTmpFile = File.createTempFile(PLUGIN_INNER_JAR_PREFIX, ".jar");
93              inputStream = jarFile.getInputStream(jarEntry);
94              fileOutputStream = new FileOutputStream(innerJarTmpFile);
95              IOUtils.copy(inputStream, fileOutputStream);
96              initializeFromJar(innerJarTmpFile, false);
97              pluginInnerJars.add(innerJarTmpFile);
98          }
99          finally
100         {
101             IOUtils.closeQuietly(inputStream);
102             IOUtils.closeQuietly(fileOutputStream);
103         }
104     }
105 
106     /**
107      * This implementation of loadClass uses a child first delegation model rather than the standard parent first. If the
108      * requested class cannot be found in this class loader, the parent class loader will be consulted via the standard
109      * {@link ClassLoader#loadClass(String, boolean)} mechanism.
110      *
111      * @param name Class to load
112      * @param resolve true to resolve all class dependencies when loaded
113      * @return Class for the provided name
114      * @throws ClassNotFoundException if the class cannot be found in this class loader or its parent
115      */
116     protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException
117     {
118         // First check if it's already been loaded
119         Class c = findLoadedClass(name);
120         if (c != null)
121             return c;
122 
123         // If not, look inside the plugin before searching the parent.
124         String path = name.replace('.', '/').concat(".class");
125         if (isEntryInPlugin(path))
126         {
127             try
128             {
129                 return loadClassFromPlugin(name, path);
130             }
131             catch (IOException e)
132             {
133                 throw new ClassNotFoundException("Unable to load class [ " + name + " ] from PluginClassLoader", e);
134             }
135         }
136         return super.loadClass(name, resolve);
137     }
138 
139     /**
140      * Load the named resource from this plugin. This implementation checks the plugin's contents first
141      * then delegates to the system loaders.
142      *
143      * @param name the name of the resource.
144      * @return the URL to the resource, <code>null</code> if the resource was not found.
145      */
146     public URL getResource(String name) {
147         if (isEntryInPlugin(name))
148         {
149             return (URL) entryMappings.get(name);
150         }
151         else
152         {
153             return super.getResource(name);
154         }
155     }
156 
157     /**
158      * Gets the resource from this classloader only
159      *
160      * @param name the name of the resource
161      * @return the URL to the resource, <code>null</code> if the resource was not found
162      */
163     public URL getLocalResource(String name)
164     {
165         if (isEntryInPlugin(name))
166             return getResource(name);
167          else
168             return null;
169     }
170 
171     public void close()
172     {
173         for (final Iterator innerJars = pluginInnerJars.iterator(); innerJars.hasNext();)
174         {
175             FileUtils.deleteQuietly((File) innerJars.next());
176         }
177     }
178 
179     List getPluginInnerJars()
180     {
181         return new ArrayList(pluginInnerJars);
182     }
183 
184     /**
185      * This is based on part of the defineClass method in URLClassLoader (minus the package security checks).
186      * See java.lang.ClassLoader.packages.
187      * @param className to derive the package from
188      */
189     private void initializePackage(String className)
190     {
191         int i = className.lastIndexOf('.');
192         if (i != -1)
193         {
194             String pkgname = className.substring(0, i);
195             // Check if package already loaded.
196             Package pkg = getPackage(pkgname);
197             if (pkg == null)
198             {
199                 definePackage(pkgname, null, null, null, null, null, null, null);
200             }
201         }
202     }
203 
204     private Class loadClassFromPlugin(String className, String path) throws IOException
205     {
206         InputStream inputStream = null;
207         try
208         {
209             URL resourceURL = (URL) entryMappings.get(path);
210             inputStream = resourceURL.openStream();
211             byte[] bytez = IOUtils.toByteArray(inputStream);
212             initializePackage(className);
213             return defineClass(className, bytez, 0, bytez.length);
214         }
215         finally
216         {
217             IOUtils.closeQuietly(inputStream);
218         }
219     }
220 
221     private URL getUrlOfResourceInJar(String name, File jarFile)
222     {
223         try
224         {
225             URL url = new URL(new URL("jar:file:" + jarFile.getAbsolutePath() + "!/"), name, NonLockingJarHandler.getInstance());
226             return url;
227         }
228         catch (MalformedURLException e)
229         {
230             throw new RuntimeException(e);
231         }
232     }
233 
234     private boolean isEntryInPlugin(String name)
235     {
236         return entryMappings.containsKey(name);
237     }
238 
239     private void addEntryMapping(JarEntry jarEntry, File jarFile, boolean overrideExistingEntries)
240     {
241         if(overrideExistingEntries)
242         {
243             addEntryUrl(jarEntry, jarFile);
244         }
245         else
246         {
247             if(!entryMappings.containsKey(jarEntry.getName()))
248             {
249                 addEntryUrl(jarEntry, jarFile);
250             }
251         }
252     }
253 
254     private void addEntryUrl(JarEntry jarEntry, File jarFile)
255     {
256         entryMappings.put(jarEntry.getName(), getUrlOfResourceInJar(jarEntry.getName(), jarFile));
257     }
258 }