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 (IOException e) {
203
204 // Restore the original ClassLoader to the active thread before failing.
205 currentThread.setContextClassLoader(originalClassLoader);
206 throw new IllegalStateException("Could not synthesize classPath from original ClassLoader.", e);
207 }
208
209 final String classPathString = builder.length() > 0
210 ? builder.toString().substring(0, builder.length() - File.pathSeparator.length())
211 : "";
212
213 // All done.
214 return new DefaultHolder(currentThread, this.originalClassLoader, classPathString);
215 }
216
217 /**
218 * Creates a new ThreadContextClassLoaderBuilder using the supplied original classLoader, as well
219 * as the supplied Maven Log.
220 *
221 * @param classLoader The original ClassLoader which should be used as the parent for the ThreadContext
222 * ClassLoader produced by the ThreadContextClassLoaderBuilder generated by this builder method.
223 * Cannot be null.
224 * @param log The active Maven Log. Cannot be null.
225 * @param encoding The encoding used by Maven. Cannot be null.
226 * @return A ThreadContextClassLoaderBuilder wrapping the supplied members.
227 */
228 public static ThreadContextClassLoaderBuilder createFor(final ClassLoader classLoader,
229 final Log log,
230 final String encoding) {
231
232 // Check sanity
233 Validate.notNull(classLoader, "classLoader");
234 Validate.notNull(log, "log");
235
236 // All done.
237 return new ThreadContextClassLoaderBuilder(classLoader, log, encoding);
238 }
239
240 /**
241 * Creates a new ThreadContextClassLoaderBuilder using the original ClassLoader from the supplied Class, as well
242 * as the given Maven Log.
243 *
244 * @param aClass A non-null class from which to extract the original ClassLoader.
245 * @param log The active Maven Log. Cannot be null.
246 * @param encoding The encoding used by Maven. Cannot be null.
247 * @return A ThreadContextClassLoaderBuilder wrapping the supplied members.
248 */
249 public static ThreadContextClassLoaderBuilder createFor(final Class<?> aClass,
250 final Log log,
251 final String encoding) {
252
253 // Check sanity
254 Validate.notNull(aClass, "aClass");
255
256 // Delegate
257 return createFor(aClass.getClassLoader(), log, encoding);
258 }
259
260 /**
261 * Converts the supplied URL to a class path element.
262 *
263 * @param anURL The non-null URL for which to acquire a classPath element.
264 * @param encoding The encoding used by Maven.
265 * @return The full (i.e. non-chopped) classpath element corresponding to the supplied URL.
266 * @throws java.lang.IllegalArgumentException if the supplied URL had an unknown protocol.
267 */
268 public static String getClassPathElement(final URL anURL, final String encoding) throws IllegalArgumentException {
269
270 // Check sanity
271 Validate.notNull(anURL, "anURL");
272
273 final String protocol = anURL.getProtocol();
274 String toReturn = null;
275
276 if ("file".equalsIgnoreCase(protocol)) {
277
278 final String originalPath = anURL.getPath();
279 try {
280 return URLDecoder.decode(anURL.getPath(), encoding);
281 } catch (UnsupportedEncodingException e) {
282 throw new IllegalArgumentException("Could not URLDecode path [" + originalPath
283 + "] using encoding [" + encoding + "]", e);
284 }
285 } else if ("jar".equalsIgnoreCase(protocol)) {
286 toReturn = anURL.getPath();
287 } else if ("http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol)) {
288 toReturn = anURL.toString();
289 } else {
290 throw new IllegalArgumentException("Unknown protocol [" + protocol + "]; could not handle URL ["
291 + anURL + "]");
292 }
293
294 return toReturn;
295 }
296
297 //
298 // Private helpers
299 //
300
301 private URL addSlashToDirectoryUrlIfRequired(final URL anURL) {
302
303 // Check sanity
304 Validate.notNull(anURL, "anURL");
305
306 URL toReturn = anURL;
307 if ("file".equalsIgnoreCase(anURL.getProtocol())) {
308
309 final File theFile = new File(anURL.getPath());
310 if (theFile.isDirectory()) {
311 try {
312
313 // This ensures that an URL pointing to a File directory
314 // actually is terminated by a '/', which is required by
315 // the URLClassLoader to operate properly.
316 toReturn = theFile.toURI().toURL();
317 } catch (MalformedURLException e) {
318 // This should never happen
319 throw new IllegalArgumentException("Could not convert a File to an URL", e);
320 }
321 }
322 }
323
324 // All done.
325 return toReturn;
326 }
327
328 /**
329 * Default implementation of the ThreadContextClassLoaderCleaner specification,
330 * with added finalizer to ensure we release the Thread reference no matter
331 * what happens with any DefaultCleaner objects.
332 */
333 class DefaultHolder implements ThreadContextClassLoaderHolder {
334
335 // Internal state
336 private Thread affectedThread;
337 private ClassLoader originalClassLoader;
338 private String classPathArgument;
339
340 public DefaultHolder(final Thread affectedThread,
341 final ClassLoader originalClassLoader,
342 final String classPathArgument) {
343
344 // Check sanity
345 Validate.notNull(affectedThread, "affectedThread");
346 Validate.notNull(originalClassLoader, "originalClassLoader");
347 Validate.notNull(classPathArgument, "classPathArgument");
348
349 // Assign internal state
350 this.affectedThread = affectedThread;
351 this.originalClassLoader = originalClassLoader;
352 this.classPathArgument = classPathArgument;
353 }
354
355 /**
356 * {@inheritDoc}
357 */
358 @Override
359 public void restoreClassLoaderAndReleaseThread() {
360 if (affectedThread != null) {
361
362 // Restore original state
363 affectedThread.setContextClassLoader(originalClassLoader);
364
365 // Null out the internal state
366 affectedThread = null;
367 originalClassLoader = null;
368 classPathArgument = null;
369 }
370 }
371
372 /**
373 * {@inheritDoc}
374 */
375 @Override
376 public String getClassPathAsArgument() {
377 return classPathArgument;
378 }
379
380 /**
381 * {@inheritDoc}
382 */
383 @Override
384 protected void finalize() throws Throwable {
385 try {
386 // First, release all resources held by this object.
387 restoreClassLoaderAndReleaseThread();
388 } finally {
389 // Now, perform standard finalization.
390 super.finalize();
391 }
392 }
393 }
394 }