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