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
27
28
29
30 public final class PluginClassLoader extends ClassLoader {
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<>();
40
41
42
43 private final File tempDirectory;
44
45
46
47
48 public PluginClassLoader(final File pluginFile) {
49 this(pluginFile, null);
50 }
51
52
53
54
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
62
63
64
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
83
84
85
86
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
137
138
139
140
141
142
143
144
145 @Override
146 protected synchronized Class<?> loadClass(final String name, final boolean resolve) throws ClassNotFoundException {
147
148 final Class<?> c = findLoadedClass(name);
149 if (c != null) {
150 return c;
151 }
152
153
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
167
168
169
170
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
183
184
185
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
208
209
210
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
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 }