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