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
24
25
26
27 public final class PluginClassLoader extends ClassLoader
28 {
29 private static final String PLUGIN_INNER_JAR_PREFIX = "atlassian-plugins-innerjar";
30
31
32
33 private final List<File> pluginInnerJars;
34
35
36
37 private final Map<String, URL> entryMappings = new HashMap<String, URL>();
38
39
40
41 private final File tempDirectory;
42
43
44
45
46 public PluginClassLoader(final File pluginFile)
47 {
48 this(pluginFile, null);
49 }
50
51
52
53
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
62
63
64
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
88
89
90
91
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
159
160
161
162
163
164
165
166
167 @Override
168 protected synchronized Class<?> loadClass(final String name, final boolean resolve) throws ClassNotFoundException
169 {
170
171 final Class<?> c = findLoadedClass(name);
172 if (c != null)
173 {
174 return c;
175 }
176
177
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
195
196
197
198
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
215
216
217
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
246
247
248
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
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 }