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