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.maven.artifact.ArtifactUtils;
038import org.apache.maven.classrealm.ClassRealmRequest.RealmType;
039import org.apache.maven.extension.internal.CoreExportsProvider;
040import org.apache.maven.model.Model;
041import org.apache.maven.model.Plugin;
042import org.codehaus.plexus.MutablePlexusContainer;
043import org.codehaus.plexus.PlexusContainer;
044import org.codehaus.plexus.classworlds.ClassWorld;
045import org.codehaus.plexus.classworlds.realm.ClassRealm;
046import org.codehaus.plexus.classworlds.realm.DuplicateRealmException;
047import org.codehaus.plexus.logging.Logger;
048import org.codehaus.plexus.util.StringUtils;
049import org.eclipse.aether.artifact.Artifact;
050
051/**
052 * Manages the class realms used by Maven. <strong>Warning:</strong> This is an internal utility class that is only
053 * public for technical reasons, it is not part of the public API. In particular, this class can be changed or deleted
054 * without prior notice.
055 *
056 * @author Benjamin Bentmann
057 */
058@Named
059@Singleton
060public class DefaultClassRealmManager
061    implements ClassRealmManager
062{
063    public static final String API_REALMID = "maven.api";
064
065    /**
066     * During normal command line build, ClassWorld is loaded by jvm system classloader, which only includes
067     * plexus-classworlds jar and possibly javaagent classes, see http://jira.codehaus.org/browse/MNG-4747.
068     * <p>
069     * Using ClassWorld to determine plugin/extensions realm parent classloaders gives m2e and integration test harness
070     * flexibility to load multiple version of maven into dedicated classloaders without assuming state of jvm system
071     * classloader.
072     */
073    private static final ClassLoader PARENT_CLASSLOADER = ClassWorld.class.getClassLoader();
074
075    private final Logger logger;
076
077    private final ClassWorld world;
078
079    private final ClassRealm containerRealm;
080
081    // this is a live injected collection
082    private final List<ClassRealmManagerDelegate> delegates;
083
084    private final ClassRealm mavenApiRealm;
085
086    /**
087     * Patterns of artifacts provided by maven core and exported via maven api realm. These artifacts are filtered from
088     * plugin and build extensions realms to avoid presence of duplicate and possibly conflicting classes on classpath.
089     */
090    private final Set<String> providedArtifacts;
091
092    @Inject
093    public DefaultClassRealmManager( Logger logger, PlexusContainer container,
094                                     List<ClassRealmManagerDelegate> delegates, CoreExportsProvider exports )
095    {
096        this.logger = logger;
097        this.world = ( (MutablePlexusContainer) container ).getClassWorld();
098        this.containerRealm = container.getContainerRealm();
099        this.delegates = delegates;
100
101        Map<String, ClassLoader> foreignImports = exports.get().getExportedPackages();
102
103        this.mavenApiRealm =
104            createRealm( API_REALMID, RealmType.Core, null /* parent */, null /* parentImports */, 
105                         foreignImports, null /* artifacts */ );
106
107        this.providedArtifacts = exports.get().getExportedArtifacts();
108    }
109
110    private ClassRealm newRealm( String id )
111    {
112        synchronized ( world )
113        {
114            String realmId = id;
115
116            Random random = new Random();
117
118            while ( true )
119            {
120                try
121                {
122                    ClassRealm classRealm = world.newRealm( realmId, null );
123
124                    if ( logger.isDebugEnabled() )
125                    {
126                        logger.debug( "Created new class realm " + realmId );
127                    }
128
129                    return classRealm;
130                }
131                catch ( DuplicateRealmException e )
132                {
133                    realmId = id + '-' + random.nextInt();
134                }
135            }
136        }
137    }
138
139    public ClassRealm getMavenApiRealm()
140    {
141        return mavenApiRealm;
142    }
143
144    /**
145     * Creates a new class realm with the specified parent and imports.
146     *
147     * @param baseRealmId The base id to use for the new realm, must not be {@code null}.
148     * @param type The type of the class realm, must not be {@code null}.
149     * @param parent The parent realm for the new realm, may be {@code null}.
150     * @param parentImports The packages/types to import from the parent realm, may be {@code null}.
151     * @param foreignImports The packages/types to import from foreign realms, may be {@code null}.
152     * @param artifacts The artifacts to add to the realm, may be {@code null}. Unresolved artifacts (i.e. with a
153     *            missing file) will automatically be excluded from the realm.
154     * @return The created class realm, never {@code null}.
155     */
156    private ClassRealm createRealm( String baseRealmId, RealmType type, ClassLoader parent, List<String> parentImports,
157                                    Map<String, ClassLoader> foreignImports, List<Artifact> artifacts )
158    {
159        Set<String> artifactIds = new LinkedHashSet<String>();
160
161        List<ClassRealmConstituent> constituents = new ArrayList<ClassRealmConstituent>();
162
163        if ( artifacts != null )
164        {
165            for ( Artifact artifact : artifacts )
166            {
167                if ( !isProvidedArtifact( artifact ) )
168                {
169                    artifactIds.add( getId( artifact ) );
170                    if ( artifact.getFile() != null )
171                    {
172                        constituents.add( new ArtifactClassRealmConstituent( artifact ) );
173                    }
174                }
175            }
176        }
177
178        if ( parentImports != null )
179        {
180            parentImports = new ArrayList<String>( parentImports );
181        }
182        else
183        {
184            parentImports = new ArrayList<String>();
185        }
186
187        if ( foreignImports != null )
188        {
189            foreignImports = new TreeMap<String, ClassLoader>( foreignImports );
190        }
191        else
192        {
193            foreignImports = new TreeMap<String, ClassLoader>();
194        }
195
196        ClassRealm classRealm = newRealm( baseRealmId );
197
198        if ( parent != null )
199        {
200            classRealm.setParentClassLoader( parent );
201        }
202
203        callDelegates( classRealm, type, parent, parentImports, foreignImports, constituents );
204
205        wireRealm( classRealm, parentImports, foreignImports );
206
207        Set<String> includedIds = populateRealm( classRealm, constituents );
208
209        if ( logger.isDebugEnabled() )
210        {
211            artifactIds.removeAll( includedIds );
212
213            for ( String id : artifactIds )
214            {
215                logger.debug( "  Excluded: " + id );
216            }
217        }
218
219        return classRealm;
220    }
221
222    public ClassRealm getCoreRealm()
223    {
224        return containerRealm;
225    }
226
227    public ClassRealm createProjectRealm( Model model, List<Artifact> artifacts )
228    {
229        if ( model == null )
230        {
231            throw new IllegalArgumentException( "model missing" );
232        }
233
234        ClassLoader parent = getMavenApiRealm();
235
236        return createRealm( getKey( model ), RealmType.Project, parent, null, null, artifacts );
237    }
238
239    private static String getKey( Model model )
240    {
241        return "project>" + model.getGroupId() + ":" + model.getArtifactId() + ":" + model.getVersion();
242    }
243
244    public ClassRealm createExtensionRealm( Plugin plugin, List<Artifact> artifacts )
245    {
246        if ( plugin == null )
247        {
248            throw new IllegalArgumentException( "extension plugin missing" );
249        }
250
251        ClassLoader parent = PARENT_CLASSLOADER;
252
253        Map<String, ClassLoader> foreignImports =
254            Collections.<String, ClassLoader>singletonMap( "", getMavenApiRealm() );
255
256        return createRealm( getKey( plugin, true ), RealmType.Extension, parent, null, foreignImports, artifacts );
257    }
258
259    private boolean isProvidedArtifact( Artifact artifact )
260    {
261        return providedArtifacts.contains( artifact.getGroupId() + ":" + artifact.getArtifactId() );
262    }
263
264    public ClassRealm createPluginRealm( Plugin plugin, ClassLoader parent, List<String> parentImports,
265                                         Map<String, ClassLoader> foreignImports, List<Artifact> artifacts )
266    {
267        if ( plugin == null )
268        {
269            throw new IllegalArgumentException( "plugin missing" );
270        }
271
272        if ( parent == null )
273        {
274            parent = PARENT_CLASSLOADER;
275        }
276
277        return createRealm( getKey( plugin, false ), RealmType.Plugin, parent, parentImports, foreignImports,
278                            artifacts );
279    }
280
281    private static String getKey( Plugin plugin, boolean extension )
282    {
283        String version = ArtifactUtils.toSnapshotVersion( plugin.getVersion() );
284        return ( extension ? "extension>" : "plugin>" ) + plugin.getGroupId() + ":" + plugin.getArtifactId() + ":"
285            + version;
286    }
287
288    private static String getId( Artifact artifact )
289    {
290        return getId( artifact.getGroupId(), artifact.getArtifactId(), artifact.getExtension(),
291                      artifact.getClassifier(), artifact.getBaseVersion() );
292    }
293
294    private static String getId( ClassRealmConstituent constituent )
295    {
296        return getId( constituent.getGroupId(), constituent.getArtifactId(), constituent.getType(),
297                      constituent.getClassifier(), constituent.getVersion() );
298    }
299
300    private static String getId( String gid, String aid, String type, String cls, String ver )
301    {
302        return gid + ':' + aid + ':' + type + ( StringUtils.isNotEmpty( cls ) ? ':' + cls : "" ) + ':' + ver;
303    }
304
305    private void callDelegates( ClassRealm classRealm, RealmType type, ClassLoader parent, List<String> parentImports,
306                                Map<String, ClassLoader> foreignImports, List<ClassRealmConstituent> constituents )
307    {
308        List<ClassRealmManagerDelegate> delegates = new ArrayList<ClassRealmManagerDelegate>( this.delegates );
309
310        if ( !delegates.isEmpty() )
311        {
312            ClassRealmRequest request =
313                new DefaultClassRealmRequest( type, parent, parentImports, foreignImports, constituents );
314
315            for ( ClassRealmManagerDelegate delegate : delegates )
316            {
317                try
318                {
319                    delegate.setupRealm( classRealm, request );
320                }
321                catch ( Exception e )
322                {
323                    logger.error( delegate.getClass().getName() + " failed to setup class realm " + classRealm + ": "
324                        + e.getMessage(), e );
325                }
326            }
327        }
328    }
329
330    private Set<String> populateRealm( ClassRealm classRealm, List<ClassRealmConstituent> constituents )
331    {
332        Set<String> includedIds = new LinkedHashSet<String>();
333
334        if ( logger.isDebugEnabled() )
335        {
336            logger.debug( "Populating class realm " + classRealm.getId() );
337        }
338
339        for ( ClassRealmConstituent constituent : constituents )
340        {
341            File file = constituent.getFile();
342
343            String id = getId( constituent );
344            includedIds.add( id );
345
346            if ( logger.isDebugEnabled() )
347            {
348                logger.debug( "  Included: " + id );
349            }
350
351            try
352            {
353                classRealm.addURL( file.toURI().toURL() );
354            }
355            catch ( MalformedURLException e )
356            {
357                // Not going to happen
358                logger.error( e.getMessage(), e );
359            }
360        }
361
362        return includedIds;
363    }
364
365    private void wireRealm( ClassRealm classRealm, List<String> parentImports, Map<String, ClassLoader> foreignImports )
366    {
367        if ( foreignImports != null && !foreignImports.isEmpty() )
368        {
369            if ( logger.isDebugEnabled() )
370            {
371                logger.debug( "Importing foreign packages into class realm " + classRealm.getId() );
372            }
373
374            for ( Map.Entry<String, ClassLoader> entry : foreignImports.entrySet() )
375            {
376                ClassLoader importedRealm = entry.getValue();
377                String imp = entry.getKey();
378
379                if ( logger.isDebugEnabled() )
380                {
381                    logger.debug( "  Imported: " + imp + " < " + getId( importedRealm ) );
382                }
383
384                classRealm.importFrom( importedRealm, imp );
385            }
386        }
387
388        if ( parentImports != null && !parentImports.isEmpty() )
389        {
390            if ( logger.isDebugEnabled() )
391            {
392                logger.debug( "Importing parent packages into class realm " + classRealm.getId() );
393            }
394
395            for ( String imp : parentImports )
396            {
397                if ( logger.isDebugEnabled() )
398                {
399                    logger.debug( "  Imported: " + imp + " < " + getId( classRealm.getParentClassLoader() ) );
400                }
401
402                classRealm.importFromParent( imp );
403            }
404        }
405    }
406
407    private String getId( ClassLoader classLoader )
408    {
409        if ( classLoader instanceof ClassRealm )
410        {
411            return ( (ClassRealm) classLoader ).getId();
412        }
413        return String.valueOf( classLoader );
414    }
415
416}