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