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.UnsupportedEncodingException;
27 import java.net.MalformedURLException;
28 import java.net.URL;
29 import java.net.URLClassLoader;
30 import java.net.URLDecoder;
31 import java.util.ArrayList;
32 import java.util.Collections;
33 import java.util.List;
34
35 import static org.codehaus.mojo.jaxb2.shared.environment.classloading.ThreadContextClassLoaderBuilder.SupportedURLProtocols.*;
36
37 /**
38 * <p>Utility class which assists in synthesizing a URLClassLoader for use as a ThreadLocal ClassLoader.
39 * Typical use:</p>
40 * <pre>
41 * <code>
42 * // Create and set the ThreadContext ClassLoader
43 * ThreadContextClassLoaderHolder holder = null;
44 *
45 * try {
46 *
47 * holder = ThreadContextClassLoaderBuilder.createFor(getClass())
48 * .addPath("some/path")
49 * .addURL(someURL)
50 * .addPaths(aPathList)
51 * .buildAndSet();
52 *
53 * // ... perform operations using the newly set ThreadContext ClassLoader...
54 *
55 * } finally {
56 * // Restore the original ClassLoader
57 * holder.restoreClassLoaderAndReleaseThread();
58 * }
59 * </code>
60 * </pre>
61 *
62 * @author <a href="mailto:lj@jguru.se">Lennart Jörelid</a>, jGuru Europe AB
63 * @since 2.0
64 */
65 public final class ThreadContextClassLoaderBuilder {
66
67 /**
68 * Simple enumeration of supported classpath URL protocols.
69 */
70 enum SupportedURLProtocols {
71
72 FILE,
73
74 JAR,
75
76 HTTP,
77
78 HTTPS,
79
80 BUNDLERESOURCE;
81
82 /**
83 * Filter function, indicating if this {@link SupportedURLProtocols} instance can handle the supplied protocol.
84 * @param protocol The protocol to support.
85 * @return <code>true</code> if this {@link SupportedURLProtocols} instance supports the given protocol.
86 */
87 public boolean supports(final String protocol) {
88 return protocol != null && this.name().equalsIgnoreCase(protocol.trim());
89 }
90 }
91
92 // Internal state
93 private ClassLoader originalClassLoader;
94 private List<URL> urlList;
95 private Log log;
96 private String encoding;
97
98 private ThreadContextClassLoaderBuilder(final ClassLoader classLoader, final Log aLog, final String encoding) {
99 log = aLog;
100 originalClassLoader = classLoader;
101 urlList = new ArrayList<URL>();
102 this.encoding = encoding;
103 }
104
105 /**
106 * Adds the supplied anURL to the list of internal URLs which should be used to build an URLClassLoader.
107 * Will only add an URL once, and warns about trying to re-add an URL.
108 *
109 * @param anURL The URL to add.
110 * @return This ThreadContextClassLoaderBuilder, for builder pattern chaining.
111 */
112 public ThreadContextClassLoaderBuilder addURL(final URL anURL) {
113
114 // Check sanity
115 Validate.notNull(anURL, "anURL");
116
117 // Add the segment unless already added.
118 for (URL current : urlList) {
119 if (current.toString().equalsIgnoreCase(anURL.toString())) {
120
121 if (log.isWarnEnabled()) {
122 log.warn("Not adding URL [" + anURL.toString() + "] twice. Check your plugin configuration.");
123 }
124
125 // Don't re-add the supplied URL.
126 return this;
127 }
128 }
129
130 // Add the supplied URL to the urlList
131 if (log.isDebugEnabled()) {
132 log.debug("Adding URL [" + anURL.toString() + "]");
133 }
134
135 //
136 // According to the URLClassLoader's documentation:
137 // "Any URL that ends with a '/' is assumed to refer to a directory.
138 // Otherwise, the URL is assumed to refer to a JAR file which will be downloaded and opened as needed."
139 //
140 // ... uhm ... instead of using the 'protocol' property of the URL itself?
141 //
142 // So ... we need to ensure that any file-protocol URLs which point to directories are actually
143 // terminated with a '/'. Otherwise the URLClassLoader treats those URLs as JARs - and hence ignores them.
144 //
145 urlList.add(addSlashToDirectoryUrlIfRequired(anURL));
146
147 return this;
148 }
149
150 /**
151 * Converts the supplied path to an URL and adds it to this ThreadContextClassLoaderBuilder.
152 *
153 * @param path A path to convert to an URL and add.
154 * @return This ThreadContextClassLoaderBuilder, for builder pattern chaining.
155 * @see #addURL(java.net.URL)
156 */
157 public ThreadContextClassLoaderBuilder addPath(final String path) {
158
159 // Check sanity
160 Validate.notEmpty(path, "path");
161
162 // Convert to an URL, and delegate.
163 final URL anUrl;
164 try {
165 anUrl = new File(path).toURI().toURL();
166 } catch (MalformedURLException e) {
167 throw new IllegalArgumentException("Could not convert path [" + path + "] to an URL.", e);
168 }
169
170 // Delegate
171 return addURL(anUrl);
172 }
173
174 /**
175 * Converts the supplied path to an URL and adds it to this ThreadContextClassLoaderBuilder.
176 *
177 * @param paths A List of path to convert to URLs and add.
178 * @return This ThreadContextClassLoaderBuilder, for builder pattern chaining.
179 * @see #addPath(String)
180 */
181 public ThreadContextClassLoaderBuilder addPaths(final List<String> paths) {
182
183 // Check sanity
184 Validate.notNull(paths, "paths");
185
186 // Delegate
187 for (String path : paths) {
188 addPath(path);
189 }
190
191 return this;
192 }
193
194 /**
195 * <p>This method performs 2 things in order:</p>
196 * <ol>
197 * <li>Builds a ThreadContext ClassLoader from the URLs supplied to this Builder, and assigns the
198 * newly built ClassLoader to the current Thread.</li>
199 * <li>Stores the ThreadContextClassLoaderHolder for later restoration.</li>
200 * </ol>
201 * References to the original ThreadContextClassLoader and the currentThread are stored within the returned
202 * ThreadContextClassLoaderHolder, and can be restored by a call to
203 * {@code ThreadContextClassLoaderHolder.restoreClassLoaderAndReleaseThread()}.
204 *
205 * @return A fully set up ThreadContextClassLoaderHolder which is used to set the
206 */
207 public ThreadContextClassLoaderHolder buildAndSet() {
208
209 // Create the URLClassLoader from the supplied URLs
210 final URL[] allURLs = new URL[urlList.size()];
211 urlList.toArray(allURLs);
212 final URLClassLoader classLoader = new URLClassLoader(allURLs, originalClassLoader);
213
214 // Assign the ThreadContext ClassLoader
215 final Thread currentThread = Thread.currentThread();
216 currentThread.setContextClassLoader(classLoader);
217
218 // Build the classpath argument
219 StringBuilder builder = new StringBuilder();
220 try {
221 for (URL current : Collections.list(classLoader.getResources(""))) {
222
223 final String toAppend = getClassPathElement(current, encoding);
224 if (toAppend != null) {
225 builder.append(toAppend).append(File.pathSeparator);
226 }
227 }
228 } catch (Exception e) {
229 // Restore the original classloader to the active thread before failing.
230 currentThread.setContextClassLoader(originalClassLoader);
231 throw new IllegalStateException("Could not synthesize classpath from original classloader.", e);
232 }
233
234 final String classPathString = builder.length() > 0
235 ? builder.toString().substring(0, builder.length() - File.pathSeparator.length())
236 : "";
237
238 // All done.
239 return new DefaultHolder(currentThread, this.originalClassLoader, classPathString);
240 }
241
242 /**
243 * Creates a new ThreadContextClassLoaderBuilder using the supplied original classLoader, as well
244 * as the supplied Maven Log.
245 *
246 * @param classLoader The original ClassLoader which should be used as the parent for the ThreadContext
247 * ClassLoader produced by the ThreadContextClassLoaderBuilder generated by this builder method.
248 * Cannot be null.
249 * @param log The active Maven Log. Cannot be null.
250 * @param encoding The encoding used by Maven. Cannot be null.
251 * @return A ThreadContextClassLoaderBuilder wrapping the supplied members.
252 */
253 public static ThreadContextClassLoaderBuilder createFor(final ClassLoader classLoader,
254 final Log log,
255 final String encoding) {
256
257 // Check sanity
258 Validate.notNull(classLoader, "classLoader");
259 Validate.notNull(log, "log");
260
261 // All done.
262 return new ThreadContextClassLoaderBuilder(classLoader, log, encoding);
263 }
264
265 /**
266 * Creates a new ThreadContextClassLoaderBuilder using the original ClassLoader from the supplied Class, as well
267 * as the given Maven Log.
268 *
269 * @param aClass A non-null class from which to extract the original ClassLoader.
270 * @param log The active Maven Log. Cannot be null.
271 * @param encoding The encoding used by Maven. Cannot be null.
272 * @return A ThreadContextClassLoaderBuilder wrapping the supplied members.
273 */
274 public static ThreadContextClassLoaderBuilder createFor(final Class<?> aClass,
275 final Log log,
276 final String encoding) {
277
278 // Check sanity
279 Validate.notNull(aClass, "aClass");
280
281 // Delegate
282 return createFor(aClass.getClassLoader(), log, encoding);
283 }
284
285 /**
286 * Converts the supplied URL to a class path element.
287 *
288 * @param anURL The non-null URL for which to acquire a classPath element.
289 * @param encoding The encoding used by Maven.
290 * @return The full (i.e. non-chopped) classpath element corresponding to the supplied URL.
291 * @throws java.lang.IllegalArgumentException if the supplied URL had an unknown protocol.
292 */
293 public static String getClassPathElement(final URL anURL, final String encoding) throws IllegalArgumentException {
294
295 // Check sanity
296 Validate.notNull(anURL, "anURL");
297
298 final String protocol = anURL.getProtocol();
299 String toReturn = null;
300
301 if (FILE.supports(protocol)) {
302
303 final String originalPath = anURL.getPath();
304 try {
305 return URLDecoder.decode(anURL.getPath(), encoding);
306 } catch (UnsupportedEncodingException e) {
307 throw new IllegalArgumentException("Could not URLDecode path [" + originalPath
308 + "] using encoding [" + encoding + "]", e);
309 }
310 } else if (JAR.supports(protocol)) {
311 toReturn = anURL.getPath();
312 } else if (HTTP.supports(protocol) || HTTPS.supports(protocol)) {
313 toReturn = anURL.toString();
314 } else if (BUNDLERESOURCE.supports(protocol)) { // e.g. when used in Eclipse/m2e
315 toReturn = anURL.toString();
316 } else {
317 throw new IllegalArgumentException("Unknown protocol [" + protocol + "]; could not handle URL ["
318 + anURL + "]");
319 }
320
321 return toReturn;
322 }
323
324 //
325 // Private helpers
326 //
327
328 private URL addSlashToDirectoryUrlIfRequired(final URL anURL) {
329
330 // Check sanity
331 Validate.notNull(anURL, "anURL");
332
333 URL toReturn = anURL;
334 if ("file".equalsIgnoreCase(anURL.getProtocol())) {
335
336 final File theFile = new File(anURL.getPath());
337 if (theFile.isDirectory()) {
338 try {
339
340 // This ensures that an URL pointing to a File directory
341 // actually is terminated by a '/', which is required by
342 // the URLClassLoader to operate properly.
343 toReturn = theFile.toURI().toURL();
344 } catch (MalformedURLException e) {
345 // This should never happen
346 throw new IllegalArgumentException("Could not convert a File to an URL", e);
347 }
348 }
349 }
350
351 // All done.
352 return toReturn;
353 }
354
355 /**
356 * Default implementation of the ThreadContextClassLoaderCleaner specification,
357 * with added finalizer to ensure we release the Thread reference no matter
358 * what happens with any DefaultCleaner objects.
359 */
360 @SuppressWarnings("all")
361 class DefaultHolder implements ThreadContextClassLoaderHolder {
362
363 // Internal state
364 private Thread affectedThread;
365 private ClassLoader originalClassLoader;
366 private String classPathArgument;
367
368 /**
369 * Compound constructor creating a default-implementation {@link ThreadContextClassLoaderHolder} which
370 * wraps references to the {@link Thread} affected as well as the original ClassLoader to restore during
371 * the call to {@link #restoreClassLoaderAndReleaseThread()} method.
372 *
373 * @param affectedThread The non-null Thread for which a new ClassLoader should be constructed.
374 * @param originalClassLoader The non-null original ClassLoader.
375 * @param classPathArgument The non-null classpath argument, to be returned
376 * from the method call to {@link #getClassPathAsArgument()}.
377 */
378 public DefaultHolder(final Thread affectedThread,
379 final ClassLoader originalClassLoader,
380 final String classPathArgument) {
381
382 // Check sanity
383 Validate.notNull(affectedThread, "affectedThread");
384 Validate.notNull(originalClassLoader, "originalClassLoader");
385 Validate.notNull(classPathArgument, "classPathArgument");
386
387 // Assign internal state
388 this.affectedThread = affectedThread;
389 this.originalClassLoader = originalClassLoader;
390 this.classPathArgument = classPathArgument;
391 }
392
393 /**
394 * {@inheritDoc}
395 */
396 @Override
397 public void restoreClassLoaderAndReleaseThread() {
398
399 if (affectedThread != null) {
400
401 // Restore original state
402 affectedThread.setContextClassLoader(originalClassLoader);
403
404 // Null out the internal state
405 affectedThread = null;
406 originalClassLoader = null;
407 classPathArgument = null;
408 }
409 }
410
411 /**
412 * {@inheritDoc}
413 */
414 @Override
415 public String getClassPathAsArgument() {
416 return classPathArgument;
417 }
418
419 /**
420 * {@inheritDoc}
421 */
422 @Override
423 protected void finalize() throws Throwable {
424 try {
425 // First, release all resources held by this object.
426 restoreClassLoaderAndReleaseThread();
427
428 } finally {
429
430 // Now, perform standard finalization.
431 super.finalize();
432 }
433 }
434 }
435 }