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