View Javadoc
1   package org.codehaus.mojo.exec;
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  
24  import java.io.ByteArrayOutputStream;
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.nio.file.Files;
31  import java.nio.file.Path;
32  import java.util.ArrayList;
33  import java.util.Collection;
34  import java.util.Enumeration;
35  import java.util.Iterator;
36  import java.util.List;
37  
38  import static java.util.Arrays.asList;
39  
40  /**
41   * 
42   * @author Robert Scholte
43   * @since 3.0.0
44   */
45  class URLClassLoaderBuilder
46  {
47      private Log logger;
48      private Collection<Path> paths;
49      private Collection<String> exclusions;
50  
51      private URLClassLoaderBuilder()
52      {
53      }
54  
55      static URLClassLoaderBuilder builder()
56      {
57          return new URLClassLoaderBuilder();
58      }
59  
60      URLClassLoaderBuilder setLogger(Log logger )
61      {
62          this.logger = logger;
63          return this;
64      }
65  
66      URLClassLoaderBuilder setExclusions(Collection<String> exclusions )
67      {
68          this.exclusions = exclusions;
69          return this;
70      }
71  
72      URLClassLoaderBuilder setPaths(Collection<Path> paths )
73      {
74          this.paths = paths;
75          return this;
76      }
77  
78      URLClassLoader build() throws IOException
79      {
80          List<URL> urls = new ArrayList<>( paths.size() );
81  
82          for ( Path dependency : paths )
83          {
84              if ( exclusions != null && exclusions.contains( dependency.getFileName().toString() ) )
85              {
86                  if ( logger != null )
87                  {
88                      logger.debug("Excluding as requested '" + dependency + "'" );
89                  }
90                  continue;
91              }
92              try
93              {
94                  urls.add( dependency.toUri().toURL() );
95              }
96              catch ( MalformedURLException e )
97              {
98                  throw new IOException( "Error during setting up classpath", e );
99              }
100         }
101 
102         return new ExecJavaClassLoader( urls.toArray( new URL[0] ) );
103     }
104 
105     // child first strategy
106     private static class ExecJavaClassLoader extends URLClassLoader
107     {
108         static
109         {
110             try
111             {
112                 registerAsParallelCapable();
113             }
114             catch ( Exception e )
115             {
116                 // no-op, not that important
117             }
118         }
119 
120         private final String jre;
121 
122         public ExecJavaClassLoader(URL[] urls )
123         {
124             super(urls);
125             jre = getJre();
126         }
127 
128         @Override
129         public Class<?> loadClass(final String name, final boolean resolve) throws ClassNotFoundException {
130             if (name == null) {
131                 throw new ClassNotFoundException();
132             }
133 
134             synchronized ( getClassLoadingLock( name ) )
135             {
136                 Class<?> clazz;
137 
138                 // if in the JVM, never override them
139                 if ( isDirectJvmClass( name ) )
140                 {
141                     try
142                     {
143                         clazz = getSystemClassLoader().loadClass( name );
144                         if ( postLoad( resolve, clazz ) )
145                         {
146                             return clazz;
147                         }
148                     }
149                     catch ( NoClassDefFoundError | ClassNotFoundException ignored )
150                     {
151                         // no-op
152                     }
153                 }
154 
155                 // already loaded?
156                 clazz = findLoadedClass( name );
157                 if ( postLoad( resolve, clazz ) )
158                 {
159                     return clazz;
160                 }
161 
162                 // look for it in this classloader
163                 try
164                 {
165                     clazz = super.findClass( name );
166                     if (clazz != null)
167                     {
168                         if ( postLoad( resolve, clazz ) )
169                         {
170                             return clazz;
171                         }
172                         return clazz;
173                     }
174                 }
175                 catch ( ClassNotFoundException | NoClassDefFoundError ignored )
176                 {
177                     // try next
178                 }
179 
180                 // load from parent - todo: exclude some classes?
181                 ClassLoader parent = getParent();
182                 if (parent == null) {
183                     parent = getSystemClassLoader();
184                 }
185                 try
186                 {
187                     clazz = Class.forName( name, false, parent );
188                     if ( postLoad( resolve, clazz ) )
189                     {
190                         return clazz;
191                     }
192                 }
193                 catch ( ClassNotFoundException ignored )
194                 {
195                     // no-op
196                 }
197 
198                 throw new ClassNotFoundException(name);
199             }
200         }
201 
202         @Override
203         public Enumeration<URL> getResources( String name ) throws IOException
204         {
205             final Enumeration<URL> selfResources = findResources(name);
206             final Enumeration<URL> parentResources = getParent().getResources(name);
207             if (!parentResources.hasMoreElements())
208             {
209                 return selfResources;
210             }
211             if (!selfResources.hasMoreElements())
212             {
213                 return new FilteringUrlEnum(parentResources);
214             }
215             return new ChainedEnumerations(
216                     asList( selfResources, new FilteringUrlEnum(parentResources) ).iterator());
217         }
218 
219         private boolean isInJvm( URL resource )
220         {
221             final Path path = toPath(resource);
222             if (path == null)
223             {
224                 return false;
225             }
226             return path.normalize().toAbsolutePath().toString().startsWith(jre);
227         }
228 
229         private String getJre()
230         {
231             final Path home = new File( System.getProperty("java.home", "") ).toPath();
232             if ( "jre".equals( home.getFileName().toString() ) && home.getParent() != null &&
233                     Files.exists(home.getParent().resolve( "lib/tools.jar" ) ) )
234             {
235                 return home.getParent().toAbsolutePath().toString();
236             }
237             return home.toAbsolutePath().toString();
238         }
239 
240         private Path toPath(final URL url)
241         {
242             if ( "jar".equals( url.getProtocol()) )
243             {
244                 try
245                 {
246                     String spec = url.getFile();
247                     int separator = spec.indexOf( '!' );
248                     if ( separator == -1 ) {
249                         return null;
250                     }
251                     return toPath( new URL( spec.substring( 0, separator + 1 ) ) ) ;
252                 }
253                 catch ( MalformedURLException e )
254                 {
255                     // no-op
256                 }
257             }
258             else if ( "file".equals( url.getProtocol() ) )
259             {
260                 String path = decode( url.getFile() );
261                 if (path.endsWith( "!" ))
262                 {
263                     path = path.substring( 0, path.length() - 1 );
264                 }
265                 return new File(path).getAbsoluteFile().toPath();
266             }
267             return null;
268         }
269 
270         private String decode( String fileName )
271         {
272             if ( fileName.indexOf( '%' ) == -1 )
273             {
274                 return fileName;
275             }
276 
277             final StringBuilder result = new StringBuilder( fileName.length() );
278             final ByteArrayOutputStream out = new ByteArrayOutputStream();
279 
280             for ( int i = 0; i < fileName.length(); )
281             {
282                 final char c = fileName.charAt( i );
283 
284                 if ( c == '%' )
285                 {
286                     out.reset();
287                     do
288                     {
289                         if ( i + 2 >= fileName.length() )
290                         {
291                             throw new IllegalArgumentException( "Incomplete % sequence at: " + i );
292                         }
293 
294                         final int d1 = Character.digit( fileName.charAt( i + 1 ), 16 );
295                         final int d2 = Character.digit (fileName.charAt( i + 2 ), 16 );
296 
297                         if ( d1 == -1 || d2 == -1 )
298                         {
299                             throw new IllegalArgumentException(
300                                     "Invalid % sequence (" + fileName.substring(i, i + 3) + ") at: " + i );
301                         }
302 
303                         out.write( (byte) ( ( d1 << 4 ) + d2 ) );
304 
305                         i += 3;
306 
307                     }
308                     while ( i < fileName.length() && fileName.charAt(i) == '%' );
309 
310                     result.append( out.toString() );
311 
312                     continue;
313                 }
314                 else
315                 {
316                     result.append( c );
317                 }
318 
319                 i++;
320             }
321             return result.toString();
322         }
323 
324         private boolean postLoad( boolean resolve, Class<?> clazz )
325         {
326             if ( clazz != null )
327             {
328                 if (resolve)
329                 {
330                     resolveClass( clazz );
331                 }
332                 return true;
333             }
334             return false;
335         }
336 
337         // not all jvm classes, for ex "javax" can be overriden so don't list it here
338         private boolean isDirectJvmClass(final String name) {
339             if (name.startsWith( "java." ) )
340             {
341                 return true;
342             }
343             if ( name.startsWith( "sun." ) )
344             {
345                 return true;
346             }
347             if ( name.startsWith( "jdk." ) )
348             {
349                 return true;
350             }
351             if ( name.startsWith( "oracle." ) )
352             {
353                 return true;
354             }
355             if ( name.startsWith( "javafx." ) )
356             {
357                 return true;
358             }
359             if ( name.startsWith( "netscape." ) )
360             {
361                 return true;
362             }
363             if ( name.startsWith( "org." ) )
364             {
365                 final String sub = name.substring( "org.".length() );
366                 if ( sub.startsWith( "w3c.dom." ) )
367                 {
368                     return true;
369                 }
370                 if ( sub.startsWith( "omg." ) )
371                 {
372                     return true;
373                 }
374                 if ( sub.startsWith( "xml.sax." ) )
375                 {
376                     return true;
377                 }
378                 if ( sub.startsWith( "ietf.jgss." ) )
379                 {
380                     return true;
381                 }
382                 if ( sub.startsWith( "jcp.xml.dsig.internal." ) )
383                 {
384                     return true;
385                 }
386             }
387             if ( name.startsWith( "com." ) )
388             {
389                 final String sub = name.substring( "com.".length() );
390                 if ( sub.startsWith( "oracle." ) )
391                 {
392                     return true;
393                 }
394                 if ( sub.startsWith( "sun." ) )
395                 {
396                     final String subSun = sub.substring( "sun.".length() );
397                     if ( subSun.startsWith( "accessibility." ) )
398                     {
399                         return true;
400                     }
401                     if ( subSun.startsWith( "activation." ) )
402                     {
403                         return true;
404                     }
405                     if ( subSun.startsWith( "awt." ) )
406                     {
407                         return true;
408                     }
409                     if ( subSun.startsWith( "beans." ) )
410                     {
411                         return true;
412                     }
413                     if ( subSun.startsWith( "corba.se." ) )
414                     {
415                         return true;
416                     }
417                     if ( subSun.startsWith( "demo.jvmti." ) )
418                     {
419                         return true;
420                     }
421                     if ( subSun.startsWith( "image.codec.jpeg." ) )
422                     {
423                         return true;
424                     }
425                     if ( subSun.startsWith( "imageio." ) )
426                     {
427                         return true;
428                     }
429                     if ( subSun.startsWith( "istack.internal." ) )
430                     {
431                         return true;
432                     }
433                     if ( subSun.startsWith( "java." ) )
434                     {
435                         return true;
436                     }
437                     if ( subSun.startsWith( "java_cup." ) )
438                     {
439                         return true;
440                     }
441                     if ( subSun.startsWith( "jmx." ) )
442                     {
443                         return true;
444                     }
445                     if ( subSun.startsWith( "jndi." ) )
446                     {
447                         return true;
448                     }
449                     if ( subSun.startsWith( "management." ) )
450                     {
451                         return true;
452                     }
453                     if ( subSun.startsWith( "media.sound." ) )
454                     {
455                         return true;
456                     }
457                     if ( subSun.startsWith( "naming.internal." ) )
458                     {
459                         return true;
460                     }
461                     if ( subSun.startsWith( "net." ) )
462                     {
463                         return true;
464                     }
465                     if ( subSun.startsWith( "nio." ) )
466                     {
467                         return true;
468                     }
469                     if ( subSun.startsWith( "org." ) )
470                     {
471                         return true;
472                     }
473                     if ( subSun.startsWith( "rmi.rmid." ) )
474                     {
475                         return true;
476                     }
477                     if ( subSun.startsWith( "rowset." ) )
478                     {
479                         return true;
480                     }
481                     if ( subSun.startsWith( "security." ) )
482                     {
483                         return true;
484                     }
485                     if ( subSun.startsWith( "swing." ) )
486                     {
487                         return true;
488                     }
489                     if ( subSun.startsWith( "tracing." ) )
490                     {
491                         return true;
492                     }
493                     if ( subSun.startsWith( "xml.internal." ) )
494                     {
495                         return true;
496                     }
497                     return false;
498                 }
499             }
500             return false;
501         }
502 
503         private class FilteringUrlEnum implements Enumeration<URL>
504         {
505             private final Enumeration<URL> delegate;
506             private URL next;
507 
508             private FilteringUrlEnum( Enumeration<URL> delegate )
509             {
510                 this.delegate = delegate;
511             }
512 
513             @Override
514             public boolean hasMoreElements()
515             {
516                 while (delegate.hasMoreElements())
517                 {
518                     next = delegate.nextElement();
519                     if (isInJvm(next))
520                     {
521                         return true;
522                     }
523                 }
524                 return false;
525             }
526 
527             @Override
528             public URL nextElement()
529             {
530                 return next;
531             }
532         }
533 
534         private static class ChainedEnumerations implements Enumeration<URL>
535         {
536             private final Iterator<Enumeration<URL>> enumerations;
537             private Enumeration<URL> current;
538 
539             private ChainedEnumerations( Iterator<Enumeration<URL>> enumerations )
540             {
541                 this.enumerations = enumerations;
542             }
543 
544             @Override
545             public boolean hasMoreElements()
546             {
547                 if (current == null || !current.hasMoreElements())
548                 {
549                     if (!enumerations.hasNext())
550                     {
551                         return false;
552                     }
553                     current = enumerations.next();
554                 }
555                 return current.hasMoreElements();
556             }
557 
558             @Override
559             public URL nextElement()
560             {
561                 return current.nextElement();
562             }
563         }
564     }
565 }