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