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