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