1 package org.codehaus.mojo.jaxb2.shared.environment.classloading;
2
3 /*
4 * Licensed to the Apache Software Foundation (ASF) under one
5 * or more contributor license agreements. See the NOTICE file
6 * distributed with this work for additional information
7 * regarding copyright ownership. The ASF licenses this file
8 * to you under the Apache License, Version 2.0 (the
9 * "License"); you may not use this file except in compliance
10 * with the License. You may obtain a copy of the License at
11 *
12 * http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing,
15 * software distributed under the License is distributed on an
16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17 * KIND, either express or implied. See the License for the
18 * specific language governing permissions and limitations
19 * under the License.
20 */
21
22 import org.apache.maven.plugin.logging.Log;
23 import org.codehaus.mojo.jaxb2.shared.Validate;
24
25 import java.io.File;
26 import java.io.IOException;
27 import java.net.MalformedURLException;
28 import java.net.URL;
29 import java.net.URLClassLoader;
30 import java.util.ArrayList;
31 import java.util.Collections;
32 import java.util.List;
33
34 /**
35 * <p>Utility class which assists in synthesizing a URLClassLoader for use as a ThreadLocal ClassLoader.
36 * Typical use:</p>
37 * <pre>
38 * <code>
39 * // Create and set the ThreadContext ClassLoader
40 * ThreadContextClassLoaderHolder holder = null;
41 *
42 * try {
43 *
44 * holder = ThreadContextClassLoaderBuilder.createFor(getClass())
45 * .addPath("some/path")
46 * .addURL(someURL)
47 * .addPaths(aPathList)
48 * .buildAndSet();
49 *
50 * // ... perform operations using the newly set ThreadContext ClassLoader...
51 *
52 * } finally {
53 * // Restore the original ClassLoader
54 * holder.restoreClassLoaderAndReleaseThread();
55 * }
56 * </code>
57 * </pre>
58 *
59 * @author <a href="mailto:lj@jguru.se">Lennart Jörelid</a>, jGuru Europe AB
60 * @since 2.0
61 */
62 public class ThreadContextClassLoaderBuilder {
63
64 // Internal state
65 private ClassLoader originalClassLoader;
66 private List<URL> urlList;
67 private Log log;
68
69 private ThreadContextClassLoaderBuilder(final ClassLoader classLoader, final Log aLog) {
70 log = aLog;
71 originalClassLoader = classLoader;
72 urlList = new ArrayList<URL>();
73 }
74
75 /**
76 * Adds the supplied anURL to the list of internal URLs which should be used to build an URLClassLoader.
77 * Will only add an URL once, and warns about trying to re-add an URL.
78 *
79 * @param anURL The URL to add.
80 * @return This ThreadContextClassLoaderBuilder, for builder pattern chaining.
81 */
82 public ThreadContextClassLoaderBuilder addURL(final URL anURL) {
83
84 // Check sanity
85 Validate.notNull(anURL, "anURL");
86
87 // Add the segment unless already added.
88 for (URL current : urlList) {
89 if (current.toString().equalsIgnoreCase(anURL.toString())) {
90
91 if (log.isWarnEnabled()) {
92 log.warn("Not adding URL [" + anURL.toString() + "] twice. Check your plugin configuration.");
93 }
94
95 // Don't re-add the supplied URL.
96 return this;
97 }
98 }
99
100 // Add the supplied URL to the urlList
101 if (log.isDebugEnabled()) {
102 log.debug("Adding URL [" + anURL.toString() + "]");
103 }
104
105 //
106 // According to the URLClassLoader's documentation:
107 // "Any URL that ends with a '/' is assumed to refer to a directory.
108 // Otherwise, the URL is assumed to refer to a JAR file which will be downloaded and opened as needed."
109 //
110 // ... uhm ... instead of using the 'protocol' property of the URL itself?
111 //
112 // So ... we need to ensure that any file-protocol URLs which point to directories are actually
113 // terminated with a '/'. Otherwise the URLClassLoader treats those URLs as JARs - and hence ignores them.
114 //
115 urlList.add(addSlashToDirectoryUrlIfRequired(anURL));
116
117 return this;
118 }
119
120 /**
121 * Converts the supplied path to an URL and adds it to this ThreadContextClassLoaderBuilder.
122 *
123 * @param path A path to convert to an URL and add.
124 * @return This ThreadContextClassLoaderBuilder, for builder pattern chaining.
125 * @see #addURL(java.net.URL)
126 */
127 public ThreadContextClassLoaderBuilder addPath(final String path) {
128
129 // Check sanity
130 Validate.notEmpty(path, "path");
131
132 // Convert to an URL, and delegate.
133 final URL anUrl;
134 try {
135 anUrl = new File(path).toURI().toURL();
136 } catch (MalformedURLException e) {
137 throw new IllegalArgumentException("Could not convert path [" + path + "] to an URL.", e);
138 }
139
140 // Delegate
141 return addURL(anUrl);
142 }
143
144 /**
145 * Converts the supplied path to an URL and adds it to this ThreadContextClassLoaderBuilder.
146 *
147 * @param paths A List of path to convert to URLs and add.
148 * @return This ThreadContextClassLoaderBuilder, for builder pattern chaining.
149 * @see #addPath(String)
150 */
151 public ThreadContextClassLoaderBuilder addPaths(final List<String> paths) {
152
153 // Check sanity
154 Validate.notNull(paths, "paths");
155
156 // Delegate
157 for (String path : paths) {
158 addPath(path);
159 }
160
161 return this;
162 }
163
164 /**
165 * <p>This method performs 2 things in order:</p>
166 * <ol>
167 * <li>Builds a ThreadContext ClassLoader from the URLs supplied to this Builder, and assigns the
168 * newly built ClassLoader to the current Thread.</li>
169 * <li>Stores the ThreadContextClassLoaderHolder for later restoration.</li>
170 * </ol>
171 * References to the original ThreadContextClassLoader and the currentThread are stored within the returned
172 * ThreadContextClassLoaderHolder, and can be restored by a call to
173 * {@code ThreadContextClassLoaderHolder.restoreClassLoaderAndReleaseThread()}.
174 *
175 * @return A fully set up ThreadContextClassLoaderHolder which is used to set the
176 */
177 public ThreadContextClassLoaderHolder buildAndSet() {
178
179 // Create the URLClassLoader from the supplied URLs
180 final URL[] allURLs = new URL[urlList.size()];
181 urlList.toArray(allURLs);
182 final URLClassLoader classLoader = new URLClassLoader(allURLs, originalClassLoader);
183
184 // Assign the ThreadContext ClassLoader
185 final Thread currentThread = Thread.currentThread();
186 currentThread.setContextClassLoader(classLoader);
187
188 // Build the classpath argument
189 StringBuilder builder = new StringBuilder();
190 try {
191 for (URL current : Collections.list(classLoader.getResources(""))) {
192
193 final String toAppend = getClassPathElement(current);
194 if (toAppend != null) {
195 builder.append(toAppend).append(File.pathSeparator);
196 }
197 }
198 } catch (IOException e) {
199
200 // Restore the original ClassLoader to the active thread before failing.
201 currentThread.setContextClassLoader(originalClassLoader);
202 throw new IllegalStateException("Could not synthesize classPath from original ClassLoader.", e);
203 }
204
205 final String classPathString = builder.length() > 0
206 ? builder.toString().substring(0, builder.length() - File.pathSeparator.length())
207 : "";
208
209 // All done.
210 return new DefaultHolder(currentThread, this.originalClassLoader, classPathString);
211 }
212
213 /**
214 * Creates a new ThreadContextClassLoaderBuilder using the supplied original classLoader, as well
215 * as the supplied Maven Log.
216 *
217 * @param classLoader The original ClassLoader which should be used as the parent for the ThreadContext
218 * ClassLoader produced by the ThreadContextClassLoaderBuilder generated by this builder method.
219 * Cannot be null.
220 * @param log The active Maven Log. Cannot be null.
221 * @return A ThreadContextClassLoaderBuilder wrapping the supplied members.
222 */
223 public static ThreadContextClassLoaderBuilder createFor(final ClassLoader classLoader, final Log log) {
224
225 // Check sanity
226 Validate.notNull(classLoader, "classLoader");
227 Validate.notNull(log, "log");
228
229 // All done.
230 return new ThreadContextClassLoaderBuilder(classLoader, log);
231 }
232
233 /**
234 * Creates a new ThreadContextClassLoaderBuilder using the original ClassLoader from the supplied Class, as well
235 * as the given Maven Log.
236 *
237 * @param aClass A non-null class from which to extract the original ClassLoader.
238 * @param log The active Maven Log. Cannot be null.
239 * @return A ThreadContextClassLoaderBuilder wrapping the supplied members.
240 */
241 public static ThreadContextClassLoaderBuilder createFor(final Class<?> aClass, final Log log) {
242
243 // Check sanity
244 Validate.notNull(aClass, "aClass");
245
246 // Delegate
247 return createFor(aClass.getClassLoader(), log);
248 }
249
250 /**
251 * Converts the supplied URL to a class path element.
252 *
253 * @param anURL The non-null URL for which to acquire a classPath element.
254 * @return The full (i.e. non-chopped) classpath element corresponding to the supplied URL.
255 * @throws java.lang.IllegalArgumentException if the supplied URL had an unknown protocol.
256 */
257 public static String getClassPathElement(final URL anURL) throws IllegalArgumentException {
258
259 // Check sanity
260 Validate.notNull(anURL, "anURL");
261
262 final String protocol = anURL.getProtocol();
263 String toReturn = null;
264
265 if ("file".equalsIgnoreCase(protocol)) {
266 toReturn = anURL.getPath();
267 } else if ("jar".equalsIgnoreCase(protocol)) {
268 toReturn = anURL.getPath();
269 } else if ("http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol)) {
270 toReturn = anURL.toString();
271 } else {
272 throw new IllegalArgumentException("Unknown protocol [" + protocol + "]; could not handle URL ["
273 + anURL + "]");
274 }
275
276 return toReturn;
277 }
278
279 //
280 // Private helpers
281 //
282
283 private URL addSlashToDirectoryUrlIfRequired(final URL anURL) {
284
285 // Check sanity
286 Validate.notNull(anURL, "anURL");
287
288 URL toReturn = anURL;
289 if ("file".equalsIgnoreCase(anURL.getProtocol())) {
290
291 final File theFile = new File(anURL.getPath());
292 if (theFile.isDirectory()) {
293 try {
294
295 // This ensures that an URL pointing to a File directory
296 // actually is terminated by a '/', which is required by
297 // the URLClassLoader to operate properly.
298 toReturn = theFile.toURI().toURL();
299 } catch (MalformedURLException e) {
300 // This should never happen
301 throw new IllegalArgumentException("Could not convert a File to an URL", e);
302 }
303 }
304 }
305
306 // All done.
307 return toReturn;
308 }
309
310 /**
311 * Default implementation of the ThreadContextClassLoaderCleaner specification,
312 * with added finalizer to ensure we release the Thread reference no matter
313 * what happens with any DefaultCleaner objects.
314 */
315 class DefaultHolder implements ThreadContextClassLoaderHolder {
316
317 // Internal state
318 private Thread affectedThread;
319 private ClassLoader originalClassLoader;
320 private String classPathArgument;
321
322 public DefaultHolder(final Thread affectedThread,
323 final ClassLoader originalClassLoader,
324 final String classPathArgument) {
325
326 // Check sanity
327 Validate.notNull(affectedThread, "affectedThread");
328 Validate.notNull(originalClassLoader, "originalClassLoader");
329 Validate.notNull(classPathArgument, "classPathArgument");
330
331 // Assign internal state
332 this.affectedThread = affectedThread;
333 this.originalClassLoader = originalClassLoader;
334 this.classPathArgument = classPathArgument;
335 }
336
337 /**
338 * {@inheritDoc}
339 */
340 @Override
341 public void restoreClassLoaderAndReleaseThread() {
342 if (affectedThread != null) {
343
344 // Restore original state
345 affectedThread.setContextClassLoader(originalClassLoader);
346
347 // Null out the internal state
348 affectedThread = null;
349 originalClassLoader = null;
350 classPathArgument = null;
351 }
352 }
353
354 /**
355 * {@inheritDoc}
356 */
357 @Override
358 public String getClassPathAsArgument() {
359 return classPathArgument;
360 }
361
362 /**
363 * {@inheritDoc}
364 */
365 @Override
366 protected void finalize() throws Throwable {
367 try {
368 // First, release all resources held by this object.
369 restoreClassLoaderAndReleaseThread();
370 } finally {
371 // Now, perform standard finalization.
372 super.finalize();
373 }
374 }
375 }
376 }