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