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