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
26
27
28
29 public final class PluginClassLoader extends ClassLoader
30 {
31 private static final String PLUGIN_INNER_JAR_PREFIX = "atlassian-plugins-innerjar";
32
33
34
35 private final List<File> pluginInnerJars;
36
37
38
39 private final Map<String, URL> entryMappings = new HashMap<String, URL>();
40
41
42
43 private final File tempDirectory;
44
45
46
47
48 public PluginClassLoader(final File pluginFile)
49 {
50 this(pluginFile, null);
51 }
52
53
54
55
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
64
65
66
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
90
91
92
93
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
161
162
163
164
165
166
167
168
169 @Override
170 protected synchronized Class<?> loadClass(final String name, final boolean resolve) throws ClassNotFoundException
171 {
172
173 final Class<?> c = findLoadedClass(name);
174 if (c != null)
175 {
176 return c;
177 }
178
179
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
197
198
199
200
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
217
218
219
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
248
249
250
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
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 }