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.HashMap;
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  import java.util.stream.Collectors;
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.internal.CoreRealm;
42  import org.apache.maven.model.Model;
43  import org.apache.maven.model.Plugin;
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   */
57  @Named
58  @Singleton
59  public class DefaultClassRealmManager implements ClassRealmManager {
60      public static final String API_REALMID = "maven.api";
61  
62      public static final String API_V4_REALMID = "maven.api.v4";
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      private final ClassRealm maven4ApiRealm;
86  
87      /**
88       * Patterns of artifacts provided by maven core and exported via maven api realm. These artifacts are filtered from
89       * plugin and build extensions realms to avoid presence of duplicate and possibly conflicting classes on classpath.
90       */
91      private final Set<String> providedArtifacts;
92  
93      private final Set<String> providedArtifactsV4;
94  
95      @Inject
96      public DefaultClassRealmManager(
97              CoreRealm coreRealm, List<ClassRealmManagerDelegate> delegates, CoreExports exports) {
98          this.world = coreRealm.getClassWorld();
99          this.containerRealm = coreRealm.getRealm();
100         this.delegates = delegates;
101 
102         Map<String, ClassLoader> foreignImports = exports.getExportedPackages();
103 
104         this.mavenApiRealm = createRealm(
105                 API_REALMID,
106                 RealmType.Core,
107                 null /* parent */,
108                 null /* parentImports */,
109                 foreignImports,
110                 null /* artifacts */);
111 
112         Map<String, ClassLoader> apiV4Imports = new HashMap<>();
113         apiV4Imports.put("org.apache.maven.api", containerRealm);
114         apiV4Imports.put("org.slf4j", containerRealm);
115         this.maven4ApiRealm = createRealm(API_V4_REALMID, RealmType.Core, null, null, apiV4Imports, null);
116 
117         this.providedArtifacts = exports.getExportedArtifacts();
118 
119         this.providedArtifactsV4 = providedArtifacts.stream()
120                 .filter(ga -> ga.startsWith("org.apache.maven:maven-api-"))
121                 .collect(Collectors.toSet());
122     }
123 
124     private ClassRealm newRealm(String id) {
125         synchronized (world) {
126             String realmId = id;
127 
128             Random random = new Random();
129 
130             while (true) {
131                 try {
132                     ClassRealm classRealm = world.newRealm(realmId, null);
133 
134                     logger.debug("Created new class realm {}", realmId);
135 
136                     return classRealm;
137                 } catch (DuplicateRealmException e) {
138                     realmId = id + '-' + random.nextInt();
139                 }
140             }
141         }
142     }
143 
144     public ClassRealm getMavenApiRealm() {
145         return mavenApiRealm;
146     }
147 
148     @Override
149     public ClassRealm getMaven4ApiRealm() {
150         return maven4ApiRealm;
151     }
152 
153     /**
154      * Creates a new class realm with the specified parent and imports.
155      *
156      * @param baseRealmId The base id to use for the new realm, must not be {@code null}.
157      * @param type The type of the class realm, must not be {@code null}.
158      * @param parent The parent realm for the new realm, may be {@code null}.
159      * @param parentImports The packages/types to import from the parent realm, may be {@code null}.
160      * @param foreignImports The packages/types to import from foreign realms, may be {@code null}.
161      * @param artifacts The artifacts to add to the realm, may be {@code null}. Unresolved artifacts (i.e. with a
162      *            missing file) will automatically be excluded from the realm.
163      * @return The created class realm, never {@code null}.
164      */
165     private ClassRealm createRealm(
166             String baseRealmId,
167             RealmType type,
168             ClassLoader parent,
169             List<String> parentImports,
170             Map<String, ClassLoader> foreignImports,
171             List<Artifact> artifacts) {
172         List<ClassRealmConstituent> constituents = new ArrayList<>(artifacts == null ? 0 : artifacts.size());
173 
174         if (artifacts != null && !artifacts.isEmpty()) {
175             boolean v4api = foreignImports != null && foreignImports.containsValue(maven4ApiRealm);
176             for (Artifact artifact : artifacts) {
177                 if (!isProvidedArtifact(artifact, v4api) && artifact.getFile() != null) {
178                     constituents.add(new ArtifactClassRealmConstituent(artifact));
179                 } else if (logger.isDebugEnabled()) {
180                     logger.debug("  Excluded: {}", getId(artifact));
181                 }
182             }
183         }
184 
185         if (parentImports != null) {
186             parentImports = new ArrayList<>(parentImports);
187         } else {
188             parentImports = new ArrayList<>();
189         }
190 
191         if (foreignImports != null) {
192             foreignImports = new TreeMap<>(foreignImports);
193         } else {
194             foreignImports = new TreeMap<>();
195         }
196 
197         ClassRealm classRealm = newRealm(baseRealmId);
198 
199         if (parent != null) {
200             classRealm.setParentClassLoader(parent);
201         }
202 
203         callDelegates(classRealm, type, parent, parentImports, foreignImports, constituents);
204 
205         wireRealm(classRealm, parentImports, foreignImports);
206 
207         populateRealm(classRealm, constituents);
208 
209         return classRealm;
210     }
211 
212     public ClassRealm getCoreRealm() {
213         return containerRealm;
214     }
215 
216     public ClassRealm createProjectRealm(Model model, List<Artifact> artifacts) {
217         Objects.requireNonNull(model, "model cannot be null");
218 
219         ClassLoader parent = getMavenApiRealm();
220 
221         return createRealm(getKey(model), RealmType.Project, parent, null, null, artifacts);
222     }
223 
224     private static String getKey(Model model) {
225         return "project>" + model.getGroupId() + ":" + model.getArtifactId() + ":" + model.getVersion();
226     }
227 
228     public ClassRealm createExtensionRealm(Plugin plugin, List<Artifact> artifacts) {
229         Objects.requireNonNull(plugin, "plugin cannot be null");
230 
231         Map<String, ClassLoader> foreignImports = Collections.singletonMap("", getMavenApiRealm());
232 
233         return createRealm(
234                 getKey(plugin, true), RealmType.Extension, PARENT_CLASSLOADER, null, foreignImports, artifacts);
235     }
236 
237     private boolean isProvidedArtifact(Artifact artifact, boolean v4api) {
238         Set<String> provided = v4api ? providedArtifactsV4 : providedArtifacts;
239         return provided.contains(artifact.getGroupId() + ":" + artifact.getArtifactId());
240     }
241 
242     public ClassRealm createPluginRealm(
243             Plugin plugin,
244             ClassLoader parent,
245             List<String> parentImports,
246             Map<String, ClassLoader> foreignImports,
247             List<Artifact> artifacts) {
248         Objects.requireNonNull(plugin, "plugin cannot be null");
249 
250         if (parent == null) {
251             parent = PARENT_CLASSLOADER;
252         }
253 
254         return createRealm(getKey(plugin, false), RealmType.Plugin, parent, parentImports, foreignImports, artifacts);
255     }
256 
257     private static String getKey(Plugin plugin, boolean extension) {
258         String version = ArtifactUtils.toSnapshotVersion(plugin.getVersion());
259         return (extension ? "extension>" : "plugin>") + plugin.getGroupId() + ":" + plugin.getArtifactId() + ":"
260                 + version;
261     }
262 
263     private static String getId(Artifact artifact) {
264         return getId(
265                 artifact.getGroupId(),
266                 artifact.getArtifactId(),
267                 artifact.getExtension(),
268                 artifact.getClassifier(),
269                 artifact.getBaseVersion());
270     }
271 
272     private static String getId(ClassRealmConstituent constituent) {
273         return getId(
274                 constituent.getGroupId(),
275                 constituent.getArtifactId(),
276                 constituent.getType(),
277                 constituent.getClassifier(),
278                 constituent.getVersion());
279     }
280 
281     private static String getId(String gid, String aid, String type, String cls, String ver) {
282         return gid + ':' + aid + ':' + type + ((cls != null && !cls.isEmpty()) ? ':' + cls : "") + ':' + ver;
283     }
284 
285     private void callDelegates(
286             ClassRealm classRealm,
287             RealmType type,
288             ClassLoader parent,
289             List<String> parentImports,
290             Map<String, ClassLoader> foreignImports,
291             List<ClassRealmConstituent> constituents) {
292         List<ClassRealmManagerDelegate> delegates = new ArrayList<>(this.delegates);
293 
294         if (!delegates.isEmpty()) {
295             ClassRealmRequest request =
296                     new DefaultClassRealmRequest(type, parent, parentImports, foreignImports, constituents);
297 
298             for (ClassRealmManagerDelegate delegate : delegates) {
299                 try {
300                     delegate.setupRealm(classRealm, request);
301                 } catch (Exception e) {
302                     logger.error(
303                             delegate.getClass().getName() + " failed to setup class realm " + classRealm + ": "
304                                     + e.getMessage(),
305                             e);
306                 }
307             }
308         }
309     }
310 
311     private void populateRealm(ClassRealm classRealm, List<ClassRealmConstituent> constituents) {
312         logger.debug("Populating class realm {}", classRealm.getId());
313 
314         for (ClassRealmConstituent constituent : constituents) {
315             File file = constituent.getFile();
316 
317             if (logger.isDebugEnabled()) {
318                 String id = getId(constituent);
319                 logger.debug("  Included: {}", id);
320             }
321 
322             try {
323                 classRealm.addURL(file.toURI().toURL());
324             } catch (MalformedURLException e) {
325                 // Not going to happen
326                 logger.error(e.getMessage(), e);
327             }
328         }
329     }
330 
331     private void wireRealm(ClassRealm classRealm, List<String> parentImports, Map<String, ClassLoader> foreignImports) {
332         if (foreignImports != null && !foreignImports.isEmpty()) {
333             logger.debug("Importing foreign packages into class realm {}", classRealm.getId());
334 
335             for (Map.Entry<String, ClassLoader> entry : foreignImports.entrySet()) {
336                 ClassLoader importedRealm = entry.getValue();
337                 String imp = entry.getKey();
338 
339                 logger.debug("  Imported: {} < {}", imp, getId(importedRealm));
340 
341                 classRealm.importFrom(importedRealm, imp);
342             }
343         }
344 
345         if (parentImports != null && !parentImports.isEmpty()) {
346             logger.debug("Importing parent packages into class realm {}", classRealm.getId());
347 
348             for (String imp : parentImports) {
349                 logger.debug("  Imported: {} < {}", imp, getId(classRealm.getParentClassLoader()));
350 
351                 classRealm.importFromParent(imp);
352             }
353         }
354     }
355 
356     private static Object getId(ClassLoader classLoader) {
357         if (classLoader instanceof ClassRealm) {
358             return ((ClassRealm) classLoader).getId();
359         }
360         return classLoader;
361     }
362 }