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.cling.invoker;
20  
21  import java.io.File;
22  import java.nio.file.Path;
23  import java.util.ArrayList;
24  import java.util.Collections;
25  import java.util.HashSet;
26  import java.util.List;
27  import java.util.Set;
28  import java.util.function.UnaryOperator;
29  
30  import com.google.inject.AbstractModule;
31  import com.google.inject.Module;
32  import org.apache.maven.api.Constants;
33  import org.apache.maven.api.ProtoSession;
34  import org.apache.maven.api.cli.Logger;
35  import org.apache.maven.api.cli.extensions.CoreExtension;
36  import org.apache.maven.api.services.MessageBuilderFactory;
37  import org.apache.maven.api.services.SettingsBuilder;
38  import org.apache.maven.cling.extensions.BootstrapCoreExtensionManager;
39  import org.apache.maven.cling.extensions.ExtensionConfigurationModule;
40  import org.apache.maven.cling.extensions.LoadedCoreExtension;
41  import org.apache.maven.cling.logging.Slf4jLoggerManager;
42  import org.apache.maven.di.Injector;
43  import org.apache.maven.execution.DefaultMavenExecutionRequest;
44  import org.apache.maven.execution.MavenExecutionRequest;
45  import org.apache.maven.execution.MavenExecutionRequestPopulator;
46  import org.apache.maven.execution.scope.internal.MojoExecutionScope;
47  import org.apache.maven.execution.scope.internal.MojoExecutionScopeModule;
48  import org.apache.maven.extension.internal.CoreExports;
49  import org.apache.maven.extension.internal.CoreExtensionEntry;
50  import org.apache.maven.internal.impl.DefaultLookup;
51  import org.apache.maven.session.scope.internal.SessionScope;
52  import org.apache.maven.session.scope.internal.SessionScopeModule;
53  import org.codehaus.plexus.ContainerConfiguration;
54  import org.codehaus.plexus.DefaultContainerConfiguration;
55  import org.codehaus.plexus.DefaultPlexusContainer;
56  import org.codehaus.plexus.PlexusConstants;
57  import org.codehaus.plexus.PlexusContainer;
58  import org.codehaus.plexus.classworlds.ClassWorld;
59  import org.codehaus.plexus.classworlds.realm.ClassRealm;
60  import org.codehaus.plexus.logging.LoggerManager;
61  import org.slf4j.ILoggerFactory;
62  
63  import static java.util.Objects.requireNonNull;
64  import static org.apache.maven.cling.invoker.CliUtils.toPlexusLoggingLevel;
65  
66  /**
67   * Container capsule backed by Plexus Container.
68   *
69   * @param <C> The context type.
70   */
71  public class PlexusContainerCapsuleFactory<C extends LookupContext> implements ContainerCapsuleFactory<C> {
72      @Override
73      public ContainerCapsule createContainerCapsule(
74              LookupInvoker<C> invoker, C context, CoreExtensionSelector<C> coreExtensionSelector) throws Exception {
75          requireNonNull(invoker, "invoker");
76          requireNonNull(context, "context");
77          requireNonNull(coreExtensionSelector, "coreExtensionSelector");
78          return new PlexusContainerCapsule(
79                  context,
80                  Thread.currentThread().getContextClassLoader(),
81                  container(invoker, context, coreExtensionSelector));
82      }
83  
84      protected DefaultPlexusContainer container(
85              LookupInvoker<C> invoker, C context, CoreExtensionSelector<C> coreExtensionSelector) throws Exception {
86          ClassWorld classWorld = requireNonNull(invoker.protoLookup.lookup(ClassWorld.class), "classWorld");
87          ClassRealm coreRealm = classWorld.getClassRealm("plexus.core");
88          List<Path> extClassPath = parseExtClasspath(context);
89          CoreExtensionEntry coreEntry = CoreExtensionEntry.discoverFrom(coreRealm);
90          List<LoadedCoreExtension> loadedExtensions = loadCoreExtensions(
91                  invoker,
92                  context,
93                  coreRealm,
94                  coreEntry.getExportedArtifacts(),
95                  coreExtensionSelector.selectCoreExtensions(invoker, context));
96          List<CoreExtensionEntry> loadedExtensionsEntries =
97                  loadedExtensions.stream().map(LoadedCoreExtension::entry).toList();
98          ClassRealm containerRealm =
99                  setupContainerRealm(context.logger, classWorld, coreRealm, extClassPath, loadedExtensionsEntries);
100         ContainerConfiguration cc = new DefaultContainerConfiguration()
101                 .setClassWorld(classWorld)
102                 .setRealm(containerRealm)
103                 .setClassPathScanning(PlexusConstants.SCANNING_INDEX)
104                 .setAutoWiring(true)
105                 .setJSR250Lifecycle(true)
106                 .setStrictClassPathScanning(false)
107                 .setName("maven");
108         customizeContainerConfiguration(context, cc);
109 
110         CoreExports exports = new CoreExports(
111                 containerRealm,
112                 collectExportedArtifacts(coreEntry, loadedExtensionsEntries),
113                 collectExportedPackages(coreEntry, loadedExtensionsEntries));
114         Thread.currentThread().setContextClassLoader(containerRealm);
115         DefaultPlexusContainer container = new DefaultPlexusContainer(cc, getCustomModule(context, exports));
116 
117         // NOTE: To avoid inconsistencies, we'll use the TCCL exclusively for lookups
118         container.setLookupRealm(null);
119         Thread.currentThread().setContextClassLoader(container.getContainerRealm());
120 
121         container.setLoggerManager(createLoggerManager());
122         ProtoSession protoSession = context.protoSession;
123         UnaryOperator<String> extensionSource = expression -> {
124             String value = protoSession.getUserProperties().get(expression);
125             if (value == null) {
126                 value = protoSession.getSystemProperties().get(expression);
127             }
128             return value;
129         };
130         List<Throwable> failures = new ArrayList<>();
131         for (LoadedCoreExtension extension : loadedExtensions) {
132             container.discoverComponents(
133                     extension.entry().getClassRealm(),
134                     new AbstractModule() {
135                         @Override
136                         protected void configure() {
137                             try {
138                                 container
139                                         .lookup(Injector.class)
140                                         .discover(extension.entry().getClassRealm());
141                             } catch (Throwable e) {
142                                 failures.add(new IllegalStateException(
143                                         "Injection failure in "
144                                                 + extension.coreExtension().getId(),
145                                         e));
146                             }
147                         }
148                     },
149                     new SessionScopeModule(container.lookup(SessionScope.class)),
150                     new MojoExecutionScopeModule(container.lookup(MojoExecutionScope.class)),
151                     new ExtensionConfigurationModule(extension.entry(), extensionSource));
152         }
153         if (!failures.isEmpty()) {
154             IllegalStateException mavenDiFailed = new IllegalStateException(
155                     "Maven dependency injection failed for at least one of the registered core extension");
156             failures.forEach(mavenDiFailed::addSuppressed);
157             throw mavenDiFailed;
158         }
159         container.getLoggerManager().setThresholds(toPlexusLoggingLevel(context.loggerLevel));
160         customizeContainer(context, container);
161 
162         return container;
163     }
164 
165     protected Set<String> collectExportedArtifacts(
166             CoreExtensionEntry coreEntry, List<CoreExtensionEntry> extensionEntries) {
167         Set<String> exportedArtifacts = new HashSet<>(coreEntry.getExportedArtifacts());
168         for (CoreExtensionEntry extension : extensionEntries) {
169             exportedArtifacts.addAll(extension.getExportedArtifacts());
170         }
171         return exportedArtifacts;
172     }
173 
174     protected Set<String> collectExportedPackages(
175             CoreExtensionEntry coreEntry, List<CoreExtensionEntry> extensionEntries) {
176         Set<String> exportedPackages = new HashSet<>(coreEntry.getExportedPackages());
177         for (CoreExtensionEntry extension : extensionEntries) {
178             exportedPackages.addAll(extension.getExportedPackages());
179         }
180         return exportedPackages;
181     }
182 
183     /**
184      * Note: overriding this method should be avoided. Preferred way to replace Maven components is the "normal" way
185      * where the components are on index (are annotated with JSR330 annotations and Sisu index is created) and, they
186      * have priorities set.
187      */
188     protected Module getCustomModule(C context, CoreExports exports) {
189         return new AbstractModule() {
190             @Override
191             protected void configure() {
192                 bind(ILoggerFactory.class).toInstance(context.loggerFactory);
193                 bind(CoreExports.class).toInstance(exports);
194                 bind(MessageBuilderFactory.class).toInstance(context.invokerRequest.messageBuilderFactory());
195             }
196         };
197     }
198 
199     protected LoggerManager createLoggerManager() {
200         return new Slf4jLoggerManager();
201     }
202 
203     protected void customizeContainerConfiguration(C context, ContainerConfiguration configuration) throws Exception {}
204 
205     protected void customizeContainer(C context, PlexusContainer container) throws Exception {}
206 
207     protected List<Path> parseExtClasspath(C context) throws Exception {
208         ProtoSession protoSession = context.protoSession;
209         String extClassPath = protoSession.getUserProperties().get(Constants.MAVEN_EXT_CLASS_PATH);
210         if (extClassPath == null) {
211             extClassPath = protoSession.getSystemProperties().get(Constants.MAVEN_EXT_CLASS_PATH);
212             if (extClassPath != null) {
213                 context.logger.warn("The property '" + Constants.MAVEN_EXT_CLASS_PATH
214                         + "' has been set using a JVM system property which is deprecated. "
215                         + "The property can be passed as a Maven argument or in the Maven project configuration file,"
216                         + "usually located at ${session.rootDirectory}/.mvn/maven.properties.");
217             }
218         }
219         ArrayList<Path> jars = new ArrayList<>();
220         if (extClassPath != null && !extClassPath.isEmpty()) {
221             for (String jar : extClassPath.split(File.pathSeparator)) {
222                 Path file = context.cwd.resolve(jar);
223                 context.logger.debug("  included '" + file + "'");
224                 jars.add(file);
225             }
226         }
227         return jars;
228     }
229 
230     protected ClassRealm setupContainerRealm(
231             Logger logger,
232             ClassWorld classWorld,
233             ClassRealm coreRealm,
234             List<Path> extClassPath,
235             List<CoreExtensionEntry> extensions)
236             throws Exception {
237         if (!extClassPath.isEmpty() || !extensions.isEmpty()) {
238             ClassRealm extRealm = classWorld.newRealm("maven.ext", null);
239 
240             extRealm.setParentRealm(coreRealm);
241 
242             logger.debug("Populating class realm '" + extRealm.getId() + "'");
243 
244             for (Path file : extClassPath) {
245                 logger.debug("  included '" + file + "'");
246                 extRealm.addURL(file.toUri().toURL());
247             }
248 
249             ArrayList<CoreExtensionEntry> reversed = new ArrayList<>(extensions);
250             Collections.reverse(reversed);
251             for (CoreExtensionEntry entry : reversed) {
252                 Set<String> exportedPackages = entry.getExportedPackages();
253                 ClassRealm realm = entry.getClassRealm();
254                 for (String exportedPackage : exportedPackages) {
255                     extRealm.importFrom(realm, exportedPackage);
256                 }
257                 if (exportedPackages.isEmpty()) {
258                     // sisu uses realm imports to establish component visibility
259                     extRealm.importFrom(realm, realm.getId());
260                 }
261             }
262 
263             return extRealm;
264         }
265 
266         return coreRealm;
267     }
268 
269     protected List<LoadedCoreExtension> loadCoreExtensions(
270             LookupInvoker<C> invoker,
271             C context,
272             ClassRealm containerRealm,
273             Set<String> providedArtifacts,
274             List<CoreExtension> extensions)
275             throws Exception {
276         if (extensions.isEmpty()) {
277             return List.of();
278         }
279         ContainerConfiguration cc = new DefaultContainerConfiguration()
280                 .setClassWorld(containerRealm.getWorld())
281                 .setRealm(containerRealm)
282                 .setClassPathScanning(PlexusConstants.SCANNING_INDEX)
283                 .setAutoWiring(true)
284                 .setJSR250Lifecycle(true)
285                 .setStrictClassPathScanning(false)
286                 .setName("maven");
287 
288         DefaultPlexusContainer container = new DefaultPlexusContainer(cc, new AbstractModule() {
289             @Override
290             protected void configure() {
291                 bind(ILoggerFactory.class).toProvider(() -> context.loggerFactory);
292             }
293         });
294 
295         ClassLoader oldCL = Thread.currentThread().getContextClassLoader();
296         Runnable settingsCleaner = null;
297         try {
298             container.setLookupRealm(null);
299             container.setLoggerManager(createLoggerManager());
300             container.getLoggerManager().setThresholds(toPlexusLoggingLevel(context.loggerLevel));
301             Thread.currentThread().setContextClassLoader(container.getContainerRealm());
302 
303             settingsCleaner = invoker.settings(context, false, container.lookup(SettingsBuilder.class));
304 
305             MavenExecutionRequest mer = new DefaultMavenExecutionRequest();
306             invoker.populateRequest(context, new DefaultLookup(container), mer);
307             mer = container.lookup(MavenExecutionRequestPopulator.class).populateDefaults(mer);
308             return Collections.unmodifiableList(container
309                     .lookup(BootstrapCoreExtensionManager.class)
310                     .loadCoreExtensions(mer, providedArtifacts, extensions));
311         } finally {
312             if (settingsCleaner != null) {
313                 settingsCleaner.run();
314             }
315             try {
316                 container.dispose();
317             } finally {
318                 Thread.currentThread().setContextClassLoader(oldCL);
319             }
320         }
321     }
322 }