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