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