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 (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 }