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