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