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.util.StringUtils;
49  import org.eclipse.aether.artifact.Artifact;
50  import org.slf4j.Logger;
51  import org.slf4j.LoggerFactory;
52  
53  /**
54   * Manages the class realms used by Maven. <strong>Warning:</strong> This is an internal utility class that is only
55   * public for technical reasons, it is not part of the public API. In particular, this class can be changed or deleted
56   * without prior notice.
57   *
58   * @author Benjamin Bentmann
59   */
60  @Named
61  @Singleton
62  public class DefaultClassRealmManager
63      implements ClassRealmManager
64  {
65      public static final String API_REALMID = "maven.api";
66  
67      /**
68       * During normal command line build, ClassWorld is loaded by jvm system classloader, which only includes
69       * plexus-classworlds jar and possibly javaagent classes, see https://issues.apache.org/jira/browse/MNG-4747.
70       * <p>
71       * Using ClassWorld to determine plugin/extensions realm parent classloaders gives m2e and integration test harness
72       * flexibility to load multiple version of maven into dedicated classloaders without assuming state of jvm system
73       * classloader.
74       */
75      private static final ClassLoader PARENT_CLASSLOADER = ClassWorld.class.getClassLoader();
76  
77      private final Logger logger = LoggerFactory.getLogger( getClass() );
78  
79      private final ClassWorld world;
80  
81      private final ClassRealm containerRealm;
82  
83      // this is a live injected collection
84      private final List<ClassRealmManagerDelegate> delegates;
85  
86      private final ClassRealm mavenApiRealm;
87  
88      /**
89       * Patterns of artifacts provided by maven core and exported via maven api realm. These artifacts are filtered from
90       * plugin and build extensions realms to avoid presence of duplicate and possibly conflicting classes on classpath.
91       */
92      private final Set<String> providedArtifacts;
93  
94      @Inject
95      public DefaultClassRealmManager( PlexusContainer container,
96                                       List<ClassRealmManagerDelegate> delegates,
97                                       CoreExports exports )
98      {
99          this.world = ( (MutablePlexusContainer) container ).getClassWorld();
100         this.containerRealm = container.getContainerRealm();
101         this.delegates = delegates;
102 
103         Map<String, ClassLoader> foreignImports = exports.getExportedPackages();
104 
105         this.mavenApiRealm =
106             createRealm( API_REALMID, RealmType.Core, null /* parent */, null /* parentImports */,
107                          foreignImports, null /* artifacts */ );
108 
109         this.providedArtifacts = exports.getExportedArtifacts();
110     }
111 
112     private ClassRealm newRealm( String id )
113     {
114         synchronized ( world )
115         {
116             String realmId = id;
117 
118             Random random = new Random();
119 
120             while ( true )
121             {
122                 try
123                 {
124                     ClassRealm classRealm = world.newRealm( realmId, null );
125 
126                     if ( logger.isDebugEnabled() )
127                     {
128                         logger.debug( "Created new class realm " + realmId );
129                     }
130 
131                     return classRealm;
132                 }
133                 catch ( DuplicateRealmException e )
134                 {
135                     realmId = id + '-' + random.nextInt();
136                 }
137             }
138         }
139     }
140 
141     public ClassRealm getMavenApiRealm()
142     {
143         return mavenApiRealm;
144     }
145 
146     /**
147      * Creates a new class realm with the specified parent and imports.
148      *
149      * @param baseRealmId The base id to use for the new realm, must not be {@code null}.
150      * @param type The type of the class realm, must not be {@code null}.
151      * @param parent The parent realm for the new realm, may be {@code null}.
152      * @param parentImports The packages/types to import from the parent realm, may be {@code null}.
153      * @param foreignImports The packages/types to import from foreign realms, may be {@code null}.
154      * @param artifacts The artifacts to add to the realm, may be {@code null}. Unresolved artifacts (i.e. with a
155      *            missing file) will automatically be excluded from the realm.
156      * @return The created class realm, never {@code null}.
157      */
158     private ClassRealm createRealm( String baseRealmId, RealmType type, ClassLoader parent, List<String> parentImports,
159                                     Map<String, ClassLoader> foreignImports, List<Artifact> artifacts )
160     {
161         Set<String> artifactIds = new LinkedHashSet<>();
162 
163         List<ClassRealmConstituent> constituents = new ArrayList<>();
164 
165         if ( artifacts != null )
166         {
167             for ( Artifact artifact : artifacts )
168             {
169                 if ( !isProvidedArtifact( artifact ) )
170                 {
171                     artifactIds.add( getId( artifact ) );
172                     if ( artifact.getFile() != null )
173                     {
174                         constituents.add( new ArtifactClassRealmConstituent( artifact ) );
175                     }
176                 }
177             }
178         }
179 
180         if ( parentImports != null )
181         {
182             parentImports = new ArrayList<>( parentImports );
183         }
184         else
185         {
186             parentImports = new ArrayList<>();
187         }
188 
189         if ( foreignImports != null )
190         {
191             foreignImports = new TreeMap<>( foreignImports );
192         }
193         else
194         {
195             foreignImports = new TreeMap<>();
196         }
197 
198         ClassRealm classRealm = newRealm( baseRealmId );
199 
200         if ( parent != null )
201         {
202             classRealm.setParentClassLoader( parent );
203         }
204 
205         callDelegates( classRealm, type, parent, parentImports, foreignImports, constituents );
206 
207         wireRealm( classRealm, parentImports, foreignImports );
208 
209         Set<String> includedIds = populateRealm( classRealm, constituents );
210 
211         if ( logger.isDebugEnabled() )
212         {
213             artifactIds.removeAll( includedIds );
214 
215             for ( String id : artifactIds )
216             {
217                 logger.debug( "  Excluded: " + id );
218             }
219         }
220 
221         return classRealm;
222     }
223 
224     public ClassRealm getCoreRealm()
225     {
226         return containerRealm;
227     }
228 
229     public ClassRealm createProjectRealm( Model model, List<Artifact> artifacts )
230     {
231         Objects.requireNonNull( model, "model cannot be null" );
232 
233         ClassLoader parent = getMavenApiRealm();
234 
235         return createRealm( getKey( model ), RealmType.Project, parent, null, null, artifacts );
236     }
237 
238     private static String getKey( Model model )
239     {
240         return "project>" + model.getGroupId() + ":" + model.getArtifactId() + ":" + model.getVersion();
241     }
242 
243     public ClassRealm createExtensionRealm( Plugin plugin, List<Artifact> artifacts )
244     {
245         Objects.requireNonNull( plugin, "plugin cannot be null" );
246 
247         Map<String, ClassLoader> foreignImports =
248             Collections.singletonMap( "", getMavenApiRealm() );
249 
250         return createRealm( getKey( plugin, true ), RealmType.Extension, PARENT_CLASSLOADER, null,
251                 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 }