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
28
29
30
31 public final class PluginClassLoader extends ClassLoader
32 {
33 private static final String PLUGIN_INNER_JAR_PREFIX = "atlassian-plugins-innerjar";
34
35
36
37 private final List<File> pluginInnerJars;
38
39
40
41 private final Map<String, URL> entryMappings = new HashMap<String, URL>();
42
43
44
45 private final File tempDirectory;
46
47
48
49
50 public PluginClassLoader(final File pluginFile)
51 {
52 this(pluginFile, null);
53 }
54
55
56
57
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
66
67
68
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
92
93
94
95
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
163
164
165
166
167
168
169
170
171 @Override
172 protected synchronized Class<?> loadClass(final String name, final boolean resolve) throws ClassNotFoundException
173 {
174
175 final Class<?> c = findLoadedClass(name);
176 if (c != null)
177 {
178 return c;
179 }
180
181
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
199
200
201
202
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
219
220
221
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
251
252
253
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
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 }