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