View Javadoc
1   package com.atlassian.plugin.classloader;
2   
3   import com.google.common.annotations.VisibleForTesting;
4   import org.apache.commons.io.FileUtils;
5   import org.apache.commons.io.IOUtils;
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  import static com.google.common.base.Preconditions.checkNotNull;
23  import static com.google.common.base.Preconditions.checkState;
24  
25  /**
26   * A class loader used to load classes and resources from a given plugin.
27   *
28   * @see PluginsClassLoader
29   */
30  public final class PluginClassLoader extends ClassLoader {
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<>();
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          this(pluginFile, null);
50      }
51  
52      /**
53       * @param pluginFile file reference to the jar for this plugin
54       * @param parent     the parent class loader
55       */
56      public PluginClassLoader(final File pluginFile, final ClassLoader parent) {
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          super(parent);
68          this.tempDirectory = checkNotNull(tempDirectory);
69          checkState(tempDirectory.exists(), "Temp directory should exist, %s", tempDirectory);
70          try {
71              if ((pluginFile == null) || !pluginFile.exists()) {
72                  throw new IllegalArgumentException("Plugin jar file must not be null and must exist.");
73              }
74              pluginInnerJars = new ArrayList<>();
75              initialiseOuterJar(pluginFile);
76          } catch (final IOException e) {
77              throw new IllegalStateException(e);
78          }
79      }
80  
81      /**
82       * Go through all entries in the given JAR, and recursively populate entryMappings by providing
83       * resource or Class name to URL mappings.
84       *
85       * @param file the file to scan
86       * @throws IOException if the plugin jar can not be read
87       */
88      private void initialiseOuterJar(final File file) throws IOException {
89          final JarFile jarFile = new JarFile(file);
90          try {
91              for (final Enumeration<JarEntry> entries = jarFile.entries(); entries.hasMoreElements(); ) {
92                  final JarEntry jarEntry = entries.nextElement();
93                  if (isInnerJarPath(jarEntry.getName())) {
94                      initialiseInnerJar(jarFile, jarEntry);
95                  } else {
96                      addEntryMapping(jarEntry, file, true);
97                  }
98              }
99          } finally {
100             jarFile.close();
101         }
102     }
103 
104     private boolean isInnerJarPath(final String name) {
105         return name.startsWith("META-INF/lib/") && name.endsWith(".jar");
106     }
107 
108     private void initialiseInnerJar(final JarFile jarFile, final JarEntry jarEntry) throws IOException {
109         InputStream inputStream = null;
110         FileOutputStream fileOutputStream = null;
111         try {
112             final File innerJarFile = File.createTempFile(PLUGIN_INNER_JAR_PREFIX, ".jar", tempDirectory);
113             inputStream = jarFile.getInputStream(jarEntry);
114             fileOutputStream = new FileOutputStream(innerJarFile);
115             IOUtils.copy(inputStream, fileOutputStream);
116             IOUtils.closeQuietly(fileOutputStream);
117 
118             final JarFile innerJarJarFile = new JarFile(innerJarFile);
119             try {
120                 for (final Enumeration<JarEntry> entries = innerJarJarFile.entries(); entries.hasMoreElements(); ) {
121                     final JarEntry innerJarEntry = entries.nextElement();
122                     addEntryMapping(innerJarEntry, innerJarFile, false);
123                 }
124             } finally {
125                 innerJarJarFile.close();
126             }
127 
128             pluginInnerJars.add(innerJarFile);
129         } finally {
130             IOUtils.closeQuietly(inputStream);
131             IOUtils.closeQuietly(fileOutputStream);
132         }
133     }
134 
135     /**
136      * This implementation of loadClass uses a child first delegation model rather than the standard parent first. If the
137      * requested class cannot be found in this class loader, the parent class loader will be consulted via the standard
138      * {@link ClassLoader#loadClass(String, boolean)} mechanism.
139      *
140      * @param name    Class to load
141      * @param resolve true to resolve all class dependencies when loaded
142      * @return Class for the provided name
143      * @throws ClassNotFoundException if the class cannot be found in this class loader or its parent
144      */
145     @Override
146     protected synchronized Class<?> loadClass(final String name, final boolean resolve) throws ClassNotFoundException {
147         // First check if it's already been loaded
148         final Class<?> c = findLoadedClass(name);
149         if (c != null) {
150             return c;
151         }
152 
153         // If not, look inside the plugin before searching the parent.
154         final String path = name.replace('.', '/').concat(".class");
155         if (isEntryInPlugin(path)) {
156             try {
157                 return loadClassFromPlugin(name, path);
158             } catch (final IOException e) {
159                 throw new ClassNotFoundException("Unable to load class [ " + name + " ] from PluginClassLoader", e);
160             }
161         }
162         return super.loadClass(name, resolve);
163     }
164 
165     /**
166      * Load the named resource from this plugin. This implementation checks the plugin's contents first
167      * then delegates to the system loaders.
168      *
169      * @param name the name of the resource.
170      * @return the URL to the resource, <code>null</code> if the resource was not found.
171      */
172     @Override
173     public URL getResource(final String name) {
174         if (isEntryInPlugin(name)) {
175             return entryMappings.get(name);
176         } else {
177             return super.getResource(name);
178         }
179     }
180 
181     /**
182      * Gets the resource from this classloader only
183      *
184      * @param name the name of the resource
185      * @return the URL to the resource, <code>null</code> if the resource was not found
186      */
187     public URL getLocalResource(final String name) {
188         if (isEntryInPlugin(name)) {
189             return getResource(name);
190         } else {
191             return null;
192         }
193     }
194 
195     public void close() {
196         for (final File pluginInnerJar : pluginInnerJars) {
197             FileUtils.deleteQuietly(pluginInnerJar);
198         }
199     }
200 
201     @VisibleForTesting
202     public List<File> getPluginInnerJars() {
203         return new ArrayList<File>(pluginInnerJars);
204     }
205 
206     /**
207      * This is based on part of the defineClass method in URLClassLoader (minus the package security checks).
208      * See java.lang.ClassLoader.packages.
209      *
210      * @param className to derive the package from
211      */
212     private void initializePackage(final String className) {
213         final int i = className.lastIndexOf('.');
214         if (i != -1) {
215             final String pkgname = className.substring(0, i);
216             // Check if package already loaded.
217             final Package pkg = getPackage(pkgname);
218             if (pkg == null) {
219                 definePackage(pkgname, null, null, null, null, null, null, null);
220             }
221         }
222     }
223 
224     private Class<?> loadClassFromPlugin(final String className, final String path) throws IOException {
225         InputStream inputStream = null;
226         try {
227             final URL resourceURL = entryMappings.get(path);
228             inputStream = resourceURL.openStream();
229             final byte[] bytez = IOUtils.toByteArray(inputStream);
230             initializePackage(className);
231             return defineClass(className, bytez, 0, bytez.length);
232         } finally {
233             IOUtils.closeQuietly(inputStream);
234         }
235     }
236 
237     private URL getUrlOfResourceInJar(final String name, final File jarFile) {
238         try {
239             return new URL(new URL("jar:file:" + jarFile.getAbsolutePath() + "!/"), name, NonLockingJarHandler.getInstance());
240         } catch (final MalformedURLException e) {
241             throw new RuntimeException(e);
242         }
243     }
244 
245     private boolean isEntryInPlugin(final String name) {
246         return entryMappings.containsKey(name);
247     }
248 
249     private void addEntryMapping(final JarEntry jarEntry, final File jarFile, final boolean overrideExistingEntries) {
250         if (overrideExistingEntries) {
251             addEntryUrl(jarEntry, jarFile);
252         } else {
253             if (!entryMappings.containsKey(jarEntry.getName())) {
254                 addEntryUrl(jarEntry, jarFile);
255             }
256         }
257     }
258 
259     private void addEntryUrl(final JarEntry jarEntry, final File jarFile) {
260         entryMappings.put(jarEntry.getName(), getUrlOfResourceInJar(jarEntry.getName(), jarFile));
261     }
262 }