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