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