View Javadoc
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&ouml;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 }