View Javadoc
1   package org.apache.maven.classrealm;
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 java.io.File;
23  import java.net.MalformedURLException;
24  import java.util.ArrayList;
25  import java.util.Collections;
26  import java.util.LinkedHashSet;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.Objects;
30  import java.util.Random;
31  import java.util.Set;
32  import java.util.TreeMap;
33  
34  import javax.inject.Inject;
35  import javax.inject.Named;
36  import javax.inject.Singleton;
37  
38  import org.apache.maven.artifact.ArtifactUtils;
39  import org.apache.maven.classrealm.ClassRealmRequest.RealmType;
40  import org.apache.maven.extension.internal.CoreExports;
41  import org.apache.maven.model.Model;
42  import org.apache.maven.model.Plugin;
43  import org.codehaus.plexus.MutablePlexusContainer;
44  import org.codehaus.plexus.PlexusContainer;
45  import org.codehaus.plexus.classworlds.ClassWorld;
46  import org.codehaus.plexus.classworlds.realm.ClassRealm;
47  import org.codehaus.plexus.classworlds.realm.DuplicateRealmException;
48  import org.codehaus.plexus.logging.Logger;
49  import org.codehaus.plexus.util.StringUtils;
50  import org.eclipse.aether.artifact.Artifact;
51  
52  /**
53   * Manages the class realms used by Maven. <strong>Warning:</strong> This is an internal utility class that is only
54   * public for technical reasons, it is not part of the public API. In particular, this class can be changed or deleted
55   * without prior notice.
56   *
57   * @author Benjamin Bentmann
58   */
59  @Named
60  @Singleton
61  public class DefaultClassRealmManager
62      implements ClassRealmManager
63  {
64      public static final String API_REALMID = "maven.api";
65  
66      /**
67       * During normal command line build, ClassWorld is loaded by jvm system classloader, which only includes
68       * plexus-classworlds jar and possibly javaagent classes, see https://issues.apache.org/jira/browse/MNG-4747.
69       * <p>
70       * Using ClassWorld to determine plugin/extensions realm parent classloaders gives m2e and integration test harness
71       * flexibility to load multiple version of maven into dedicated classloaders without assuming state of jvm system
72       * classloader.
73       */
74      private static final ClassLoader PARENT_CLASSLOADER = ClassWorld.class.getClassLoader();
75  
76      private final Logger logger;
77  
78      private final ClassWorld world;
79  
80      private final ClassRealm containerRealm;
81  
82      // this is a live injected collection
83      private final List<ClassRealmManagerDelegate> delegates;
84  
85      private final ClassRealm mavenApiRealm;
86  
87      /**
88       * Patterns of artifacts provided by maven core and exported via maven api realm. These artifacts are filtered from
89       * plugin and build extensions realms to avoid presence of duplicate and possibly conflicting classes on classpath.
90       */
91      private final Set<String> providedArtifacts;
92  
93      @Inject
94      public DefaultClassRealmManager( Logger logger, PlexusContainer container,
95                                       List<ClassRealmManagerDelegate> delegates, CoreExports exports )
96      {
97          this.logger = logger;
98          this.world = ( (MutablePlexusContainer) container ).getClassWorld();
99          this.containerRealm = container.getContainerRealm();
100         this.delegates = delegates;
101 
102         Map<String, ClassLoader> foreignImports = exports.getExportedPackages();
103 
104         this.mavenApiRealm =
105             createRealm( API_REALMID, RealmType.Core, null /* parent */, null /* parentImports */,
106                          foreignImports, null /* artifacts */ );
107 
108         this.providedArtifacts = exports.getExportedArtifacts();
109     }
110 
111     private ClassRealm newRealm( String id )
112     {
113         synchronized ( world )
114         {
115             String realmId = id;
116 
117             Random random = new Random();
118 
119             while ( true )
120             {
121                 try
122                 {
123                     ClassRealm classRealm = world.newRealm( realmId, null );
124 
125                     if ( logger.isDebugEnabled() )
126                     {
127                         logger.debug( "Created new class realm " + realmId );
128                     }
129 
130                     return classRealm;
131                 }
132                 catch ( DuplicateRealmException e )
133                 {
134                     realmId = id + '-' + random.nextInt();
135                 }
136             }
137         }
138     }
139 
140     public ClassRealm getMavenApiRealm()
141     {
142         return mavenApiRealm;
143     }
144 
145     /**
146      * Creates a new class realm with the specified parent and imports.
147      *
148      * @param baseRealmId The base id to use for the new realm, must not be {@code null}.
149      * @param type The type of the class realm, must not be {@code null}.
150      * @param parent The parent realm for the new realm, may be {@code null}.
151      * @param parentImports The packages/types to import from the parent realm, may be {@code null}.
152      * @param foreignImports The packages/types to import from foreign realms, may be {@code null}.
153      * @param artifacts The artifacts to add to the realm, may be {@code null}. Unresolved artifacts (i.e. with a
154      *            missing file) will automatically be excluded from the realm.
155      * @return The created class realm, never {@code null}.
156      */
157     private ClassRealm createRealm( String baseRealmId, RealmType type, ClassLoader parent, List<String> parentImports,
158                                     Map<String, ClassLoader> foreignImports, List<Artifact> artifacts )
159     {
160         Set<String> artifactIds = new LinkedHashSet<>();
161 
162         List<ClassRealmConstituent> constituents = new ArrayList<>();
163 
164         if ( artifacts != null )
165         {
166             for ( Artifact artifact : artifacts )
167             {
168                 if ( !isProvidedArtifact( artifact ) )
169                 {
170                     artifactIds.add( getId( artifact ) );
171                     if ( artifact.getFile() != null )
172                     {
173                         constituents.add( new ArtifactClassRealmConstituent( artifact ) );
174                     }
175                 }
176             }
177         }
178 
179         if ( parentImports != null )
180         {
181             parentImports = new ArrayList<>( parentImports );
182         }
183         else
184         {
185             parentImports = new ArrayList<>();
186         }
187 
188         if ( foreignImports != null )
189         {
190             foreignImports = new TreeMap<>( foreignImports );
191         }
192         else
193         {
194             foreignImports = new TreeMap<>();
195         }
196 
197         ClassRealm classRealm = newRealm( baseRealmId );
198 
199         if ( parent != null )
200         {
201             classRealm.setParentClassLoader( parent );
202         }
203 
204         callDelegates( classRealm, type, parent, parentImports, foreignImports, constituents );
205 
206         wireRealm( classRealm, parentImports, foreignImports );
207 
208         Set<String> includedIds = populateRealm( classRealm, constituents );
209 
210         if ( logger.isDebugEnabled() )
211         {
212             artifactIds.removeAll( includedIds );
213 
214             for ( String id : artifactIds )
215             {
216                 logger.debug( "  Excluded: " + id );
217             }
218         }
219 
220         return classRealm;
221     }
222 
223     public ClassRealm getCoreRealm()
224     {
225         return containerRealm;
226     }
227 
228     public ClassRealm createProjectRealm( Model model, List<Artifact> artifacts )
229     {
230         Objects.requireNonNull( model, "model cannot be null" );
231 
232         ClassLoader parent = getMavenApiRealm();
233 
234         return createRealm( getKey( model ), RealmType.Project, parent, null, null, artifacts );
235     }
236 
237     private static String getKey( Model model )
238     {
239         return "project>" + model.getGroupId() + ":" + model.getArtifactId() + ":" + model.getVersion();
240     }
241 
242     public ClassRealm createExtensionRealm( Plugin plugin, List<Artifact> artifacts )
243     {
244         Objects.requireNonNull( plugin, "plugin cannot be null" );
245 
246         ClassLoader parent = PARENT_CLASSLOADER;
247 
248         Map<String, ClassLoader> foreignImports =
249             Collections.<String, ClassLoader>singletonMap( "", getMavenApiRealm() );
250 
251         return createRealm( getKey( plugin, true ), RealmType.Extension, parent, null, foreignImports, artifacts );
252     }
253 
254     private boolean isProvidedArtifact( Artifact artifact )
255     {
256         return providedArtifacts.contains( artifact.getGroupId() + ":" + artifact.getArtifactId() );
257     }
258 
259     public ClassRealm createPluginRealm( Plugin plugin, ClassLoader parent, List<String> parentImports,
260                                          Map<String, ClassLoader> foreignImports, List<Artifact> artifacts )
261     {
262         Objects.requireNonNull( plugin, "plugin cannot be null" );
263 
264         if ( parent == null )
265         {
266             parent = PARENT_CLASSLOADER;
267         }
268 
269         return createRealm( getKey( plugin, false ), RealmType.Plugin, parent, parentImports, foreignImports,
270                             artifacts );
271     }
272 
273     private static String getKey( Plugin plugin, boolean extension )
274     {
275         String version = ArtifactUtils.toSnapshotVersion( plugin.getVersion() );
276         return ( extension ? "extension>" : "plugin>" ) + plugin.getGroupId() + ":" + plugin.getArtifactId() + ":"
277             + version;
278     }
279 
280     private static String getId( Artifact artifact )
281     {
282         return getId( artifact.getGroupId(), artifact.getArtifactId(), artifact.getExtension(),
283                       artifact.getClassifier(), artifact.getBaseVersion() );
284     }
285 
286     private static String getId( ClassRealmConstituent constituent )
287     {
288         return getId( constituent.getGroupId(), constituent.getArtifactId(), constituent.getType(),
289                       constituent.getClassifier(), constituent.getVersion() );
290     }
291 
292     private static String getId( String gid, String aid, String type, String cls, String ver )
293     {
294         return gid + ':' + aid + ':' + type + ( StringUtils.isNotEmpty( cls ) ? ':' + cls : "" ) + ':' + ver;
295     }
296 
297     private void callDelegates( ClassRealm classRealm, RealmType type, ClassLoader parent, List<String> parentImports,
298                                 Map<String, ClassLoader> foreignImports, List<ClassRealmConstituent> constituents )
299     {
300         List<ClassRealmManagerDelegate> delegates = new ArrayList<>( this.delegates );
301 
302         if ( !delegates.isEmpty() )
303         {
304             ClassRealmRequest request =
305                 new DefaultClassRealmRequest( type, parent, parentImports, foreignImports, constituents );
306 
307             for ( ClassRealmManagerDelegate delegate : delegates )
308             {
309                 try
310                 {
311                     delegate.setupRealm( classRealm, request );
312                 }
313                 catch ( Exception e )
314                 {
315                     logger.error( delegate.getClass().getName() + " failed to setup class realm " + classRealm + ": "
316                         + e.getMessage(), e );
317                 }
318             }
319         }
320     }
321 
322     private Set<String> populateRealm( ClassRealm classRealm, List<ClassRealmConstituent> constituents )
323     {
324         Set<String> includedIds = new LinkedHashSet<>();
325 
326         if ( logger.isDebugEnabled() )
327         {
328             logger.debug( "Populating class realm " + classRealm.getId() );
329         }
330 
331         for ( ClassRealmConstituent constituent : constituents )
332         {
333             File file = constituent.getFile();
334 
335             String id = getId( constituent );
336             includedIds.add( id );
337 
338             if ( logger.isDebugEnabled() )
339             {
340                 logger.debug( "  Included: " + id );
341             }
342 
343             try
344             {
345                 classRealm.addURL( file.toURI().toURL() );
346             }
347             catch ( MalformedURLException e )
348             {
349                 // Not going to happen
350                 logger.error( e.getMessage(), e );
351             }
352         }
353 
354         return includedIds;
355     }
356 
357     private void wireRealm( ClassRealm classRealm, List<String> parentImports, Map<String, ClassLoader> foreignImports )
358     {
359         if ( foreignImports != null && !foreignImports.isEmpty() )
360         {
361             if ( logger.isDebugEnabled() )
362             {
363                 logger.debug( "Importing foreign packages into class realm " + classRealm.getId() );
364             }
365 
366             for ( Map.Entry<String, ClassLoader> entry : foreignImports.entrySet() )
367             {
368                 ClassLoader importedRealm = entry.getValue();
369                 String imp = entry.getKey();
370 
371                 if ( logger.isDebugEnabled() )
372                 {
373                     logger.debug( "  Imported: " + imp + " < " + getId( importedRealm ) );
374                 }
375 
376                 classRealm.importFrom( importedRealm, imp );
377             }
378         }
379 
380         if ( parentImports != null && !parentImports.isEmpty() )
381         {
382             if ( logger.isDebugEnabled() )
383             {
384                 logger.debug( "Importing parent packages into class realm " + classRealm.getId() );
385             }
386 
387             for ( String imp : parentImports )
388             {
389                 if ( logger.isDebugEnabled() )
390                 {
391                     logger.debug( "  Imported: " + imp + " < " + getId( classRealm.getParentClassLoader() ) );
392                 }
393 
394                 classRealm.importFromParent( imp );
395             }
396         }
397     }
398 
399     private String getId( ClassLoader classLoader )
400     {
401         if ( classLoader instanceof ClassRealm )
402         {
403             return ( (ClassRealm) classLoader ).getId();
404         }
405         return String.valueOf( classLoader );
406     }
407 
408 }