001package org.apache.maven.classrealm;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *   http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import java.io.File;
023import java.net.MalformedURLException;
024import java.util.ArrayList;
025import java.util.Collections;
026import java.util.LinkedHashSet;
027import java.util.List;
028import java.util.Map;
029import java.util.Random;
030import java.util.Set;
031import java.util.TreeMap;
032
033import javax.inject.Inject;
034import javax.inject.Named;
035import javax.inject.Singleton;
036
037import org.apache.commons.lang3.Validate;
038import org.apache.maven.artifact.ArtifactUtils;
039import org.apache.maven.classrealm.ClassRealmRequest.RealmType;
040import org.apache.maven.extension.internal.CoreExportsProvider;
041import org.apache.maven.model.Model;
042import org.apache.maven.model.Plugin;
043import org.codehaus.plexus.MutablePlexusContainer;
044import org.codehaus.plexus.PlexusContainer;
045import org.codehaus.plexus.classworlds.ClassWorld;
046import org.codehaus.plexus.classworlds.realm.ClassRealm;
047import org.codehaus.plexus.classworlds.realm.DuplicateRealmException;
048import org.codehaus.plexus.logging.Logger;
049import org.codehaus.plexus.util.StringUtils;
050import org.eclipse.aether.artifact.Artifact;
051
052/**
053 * Manages the class realms used by Maven. <strong>Warning:</strong> This is an internal utility class that is only
054 * public for technical reasons, it is not part of the public API. In particular, this class can be changed or deleted
055 * without prior notice.
056 *
057 * @author Benjamin Bentmann
058 */
059@Named
060@Singleton
061public class DefaultClassRealmManager
062    implements ClassRealmManager
063{
064    public static final String API_REALMID = "maven.api";
065
066    /**
067     * During normal command line build, ClassWorld is loaded by jvm system classloader, which only includes
068     * plexus-classworlds jar and possibly javaagent classes, see https://issues.apache.org/jira/browse/MNG-4747.
069     * <p>
070     * Using ClassWorld to determine plugin/extensions realm parent classloaders gives m2e and integration test harness
071     * flexibility to load multiple version of maven into dedicated classloaders without assuming state of jvm system
072     * classloader.
073     */
074    private static final ClassLoader PARENT_CLASSLOADER = ClassWorld.class.getClassLoader();
075
076    private final Logger logger;
077
078    private final ClassWorld world;
079
080    private final ClassRealm containerRealm;
081
082    // this is a live injected collection
083    private final List<ClassRealmManagerDelegate> delegates;
084
085    private final ClassRealm mavenApiRealm;
086
087    /**
088     * Patterns of artifacts provided by maven core and exported via maven api realm. These artifacts are filtered from
089     * plugin and build extensions realms to avoid presence of duplicate and possibly conflicting classes on classpath.
090     */
091    private final Set<String> providedArtifacts;
092
093    @Inject
094    public DefaultClassRealmManager( Logger logger, PlexusContainer container,
095                                     List<ClassRealmManagerDelegate> delegates, CoreExportsProvider exports )
096    {
097        this.logger = logger;
098        this.world = ( (MutablePlexusContainer) container ).getClassWorld();
099        this.containerRealm = container.getContainerRealm();
100        this.delegates = delegates;
101
102        Map<String, ClassLoader> foreignImports = exports.get().getExportedPackages();
103
104        this.mavenApiRealm =
105            createRealm( API_REALMID, RealmType.Core, null /* parent */, null /* parentImports */,
106                         foreignImports, null /* artifacts */ );
107
108        this.providedArtifacts = exports.get().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        Validate.notNull( 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        Validate.notNull( 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        Validate.notNull( 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}