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.plugin.internal;
20  
21  import javax.inject.Inject;
22  import javax.inject.Named;
23  import javax.inject.Singleton;
24  
25  import java.io.ByteArrayOutputStream;
26  import java.io.File;
27  import java.io.IOException;
28  import java.io.PrintStream;
29  import java.lang.annotation.Annotation;
30  import java.lang.reflect.Field;
31  import java.lang.reflect.ParameterizedType;
32  import java.lang.reflect.Type;
33  import java.nio.file.Files;
34  import java.nio.file.Path;
35  import java.util.ArrayList;
36  import java.util.Collection;
37  import java.util.HashMap;
38  import java.util.List;
39  import java.util.Map;
40  import java.util.Objects;
41  import java.util.jar.JarFile;
42  import java.util.stream.Collectors;
43  import java.util.zip.ZipEntry;
44  
45  import org.apache.maven.RepositoryUtils;
46  import org.apache.maven.api.Dependency;
47  import org.apache.maven.api.Node;
48  import org.apache.maven.api.PathScope;
49  import org.apache.maven.api.PathType;
50  import org.apache.maven.api.Project;
51  import org.apache.maven.api.Session;
52  import org.apache.maven.api.plugin.descriptor.Resolution;
53  import org.apache.maven.api.services.DependencyResolver;
54  import org.apache.maven.api.services.DependencyResolverResult;
55  import org.apache.maven.api.services.PathScopeRegistry;
56  import org.apache.maven.api.xml.XmlNode;
57  import org.apache.maven.artifact.Artifact;
58  import org.apache.maven.classrealm.ClassRealmManager;
59  import org.apache.maven.di.Injector;
60  import org.apache.maven.di.Key;
61  import org.apache.maven.execution.MavenSession;
62  import org.apache.maven.execution.scope.internal.MojoExecutionScope;
63  import org.apache.maven.execution.scope.internal.MojoExecutionScopeModule;
64  import org.apache.maven.internal.impl.DefaultLog;
65  import org.apache.maven.internal.impl.DefaultMojoExecution;
66  import org.apache.maven.internal.impl.InternalMavenSession;
67  import org.apache.maven.internal.xml.XmlPlexusConfiguration;
68  import org.apache.maven.model.Plugin;
69  import org.apache.maven.plugin.ContextEnabled;
70  import org.apache.maven.plugin.DebugConfigurationListener;
71  import org.apache.maven.plugin.ExtensionRealmCache;
72  import org.apache.maven.plugin.InvalidPluginDescriptorException;
73  import org.apache.maven.plugin.MavenPluginManager;
74  import org.apache.maven.plugin.MavenPluginPrerequisitesChecker;
75  import org.apache.maven.plugin.Mojo;
76  import org.apache.maven.plugin.MojoExecution;
77  import org.apache.maven.plugin.MojoNotFoundException;
78  import org.apache.maven.plugin.PluginArtifactsCache;
79  import org.apache.maven.plugin.PluginConfigurationException;
80  import org.apache.maven.plugin.PluginContainerException;
81  import org.apache.maven.plugin.PluginDescriptorCache;
82  import org.apache.maven.plugin.PluginDescriptorParsingException;
83  import org.apache.maven.plugin.PluginIncompatibleException;
84  import org.apache.maven.plugin.PluginManagerException;
85  import org.apache.maven.plugin.PluginParameterException;
86  import org.apache.maven.plugin.PluginParameterExpressionEvaluator;
87  import org.apache.maven.plugin.PluginParameterExpressionEvaluatorV4;
88  import org.apache.maven.plugin.PluginRealmCache;
89  import org.apache.maven.plugin.PluginResolutionException;
90  import org.apache.maven.plugin.PluginValidationManager;
91  import org.apache.maven.plugin.descriptor.MojoDescriptor;
92  import org.apache.maven.plugin.descriptor.Parameter;
93  import org.apache.maven.plugin.descriptor.PluginDescriptor;
94  import org.apache.maven.plugin.descriptor.PluginDescriptorBuilder;
95  import org.apache.maven.plugin.version.DefaultPluginVersionRequest;
96  import org.apache.maven.plugin.version.PluginVersionRequest;
97  import org.apache.maven.plugin.version.PluginVersionResolutionException;
98  import org.apache.maven.plugin.version.PluginVersionResolver;
99  import org.apache.maven.project.ExtensionDescriptor;
100 import org.apache.maven.project.ExtensionDescriptorBuilder;
101 import org.apache.maven.project.MavenProject;
102 import org.apache.maven.session.scope.internal.SessionScope;
103 import org.apache.maven.session.scope.internal.SessionScopeModule;
104 import org.codehaus.plexus.DefaultPlexusContainer;
105 import org.codehaus.plexus.PlexusContainer;
106 import org.codehaus.plexus.classworlds.realm.ClassRealm;
107 import org.codehaus.plexus.component.composition.CycleDetectedInComponentGraphException;
108 import org.codehaus.plexus.component.configurator.ComponentConfigurationException;
109 import org.codehaus.plexus.component.configurator.ComponentConfigurator;
110 import org.codehaus.plexus.component.configurator.ConfigurationListener;
111 import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
112 import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluator;
113 import org.codehaus.plexus.component.repository.ComponentDescriptor;
114 import org.codehaus.plexus.component.repository.exception.ComponentLifecycleException;
115 import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
116 import org.codehaus.plexus.configuration.DefaultPlexusConfiguration;
117 import org.codehaus.plexus.configuration.PlexusConfiguration;
118 import org.codehaus.plexus.configuration.PlexusConfigurationException;
119 import org.codehaus.plexus.personality.plexus.lifecycle.phase.Contextualizable;
120 import org.eclipse.aether.RepositorySystemSession;
121 import org.eclipse.aether.graph.DependencyFilter;
122 import org.eclipse.aether.repository.RemoteRepository;
123 import org.eclipse.aether.resolution.DependencyResult;
124 import org.eclipse.aether.util.filter.AndDependencyFilter;
125 import org.slf4j.Logger;
126 import org.slf4j.LoggerFactory;
127 
128 /**
129  * Provides basic services to manage Maven plugins and their mojos. This component is kept general in its design such
130  * that the plugins/mojos can be used in arbitrary contexts. In particular, the mojos can be used for ordinary build
131  * plugins as well as special purpose plugins like reports.
132  *
133  * @since 3.0
134  */
135 @Named
136 @Singleton
137 public class DefaultMavenPluginManager implements MavenPluginManager {
138 
139     /**
140      * <p>
141      * PluginId =&gt; ExtensionRealmCache.CacheRecord map MavenProject context value key. The map is used to ensure the
142      * same class realm is used to load build extensions and load mojos for extensions=true plugins.
143      * </p>
144      * <strong>Note:</strong> This is part of internal implementation and may be changed or removed without notice
145      *
146      * @since 3.3.0
147      */
148     public static final String KEY_EXTENSIONS_REALMS = DefaultMavenPluginManager.class.getName() + "/extensionsRealms";
149 
150     private final Logger logger = LoggerFactory.getLogger(getClass());
151 
152     private final PlexusContainer container;
153     private final ClassRealmManager classRealmManager;
154     private final PluginDescriptorCache pluginDescriptorCache;
155     private final PluginRealmCache pluginRealmCache;
156     private final PluginDependenciesResolver pluginDependenciesResolver;
157     private final ExtensionRealmCache extensionRealmCache;
158     private final PluginVersionResolver pluginVersionResolver;
159     private final PluginArtifactsCache pluginArtifactsCache;
160     private final MavenPluginValidator pluginValidator;
161     private final List<MavenPluginConfigurationValidator> configurationValidators;
162     private final PluginValidationManager pluginValidationManager;
163     private final List<MavenPluginPrerequisitesChecker> prerequisitesCheckers;
164     private final ExtensionDescriptorBuilder extensionDescriptorBuilder = new ExtensionDescriptorBuilder();
165     private final PluginDescriptorBuilder builder = new PluginDescriptorBuilder();
166 
167     @Inject
168     @SuppressWarnings("checkstyle:ParameterNumber")
169     public DefaultMavenPluginManager(
170             PlexusContainer container,
171             ClassRealmManager classRealmManager,
172             PluginDescriptorCache pluginDescriptorCache,
173             PluginRealmCache pluginRealmCache,
174             PluginDependenciesResolver pluginDependenciesResolver,
175             ExtensionRealmCache extensionRealmCache,
176             PluginVersionResolver pluginVersionResolver,
177             PluginArtifactsCache pluginArtifactsCache,
178             MavenPluginValidator pluginValidator,
179             List<MavenPluginConfigurationValidator> configurationValidators,
180             PluginValidationManager pluginValidationManager,
181             List<MavenPluginPrerequisitesChecker> prerequisitesCheckers) {
182         this.container = container;
183         this.classRealmManager = classRealmManager;
184         this.pluginDescriptorCache = pluginDescriptorCache;
185         this.pluginRealmCache = pluginRealmCache;
186         this.pluginDependenciesResolver = pluginDependenciesResolver;
187         this.extensionRealmCache = extensionRealmCache;
188         this.pluginVersionResolver = pluginVersionResolver;
189         this.pluginArtifactsCache = pluginArtifactsCache;
190         this.pluginValidator = pluginValidator;
191         this.configurationValidators = configurationValidators;
192         this.pluginValidationManager = pluginValidationManager;
193         this.prerequisitesCheckers = prerequisitesCheckers;
194     }
195 
196     public PluginDescriptor getPluginDescriptor(
197             Plugin plugin, List<RemoteRepository> repositories, RepositorySystemSession session)
198             throws PluginResolutionException, PluginDescriptorParsingException, InvalidPluginDescriptorException {
199         PluginDescriptorCache.Key cacheKey = pluginDescriptorCache.createKey(plugin, repositories, session);
200 
201         PluginDescriptor pluginDescriptor = pluginDescriptorCache.get(cacheKey, () -> {
202             org.eclipse.aether.artifact.Artifact artifact =
203                     pluginDependenciesResolver.resolve(plugin, repositories, session);
204 
205             Artifact pluginArtifact = RepositoryUtils.toArtifact(artifact);
206 
207             PluginDescriptor descriptor = extractPluginDescriptor(pluginArtifact, plugin);
208 
209             boolean isBlankVersion = descriptor.getRequiredMavenVersion() == null
210                     || descriptor.getRequiredMavenVersion().trim().isEmpty();
211 
212             if (isBlankVersion) {
213                 // only take value from underlying POM if plugin descriptor has no explicit Maven requirement
214                 descriptor.setRequiredMavenVersion(artifact.getProperty("requiredMavenVersion", null));
215             }
216 
217             return descriptor;
218         });
219 
220         pluginDescriptor.setPlugin(plugin);
221 
222         return pluginDescriptor;
223     }
224 
225     private PluginDescriptor extractPluginDescriptor(Artifact pluginArtifact, Plugin plugin)
226             throws PluginDescriptorParsingException, InvalidPluginDescriptorException {
227         PluginDescriptor pluginDescriptor = null;
228 
229         File pluginFile = pluginArtifact.getFile();
230 
231         try {
232             if (pluginFile.isFile()) {
233                 try (JarFile pluginJar = new JarFile(pluginFile, false)) {
234                     ZipEntry pluginDescriptorEntry = pluginJar.getEntry(getPluginDescriptorLocation());
235 
236                     if (pluginDescriptorEntry != null) {
237                         pluginDescriptor = parsePluginDescriptor(
238                                 () -> pluginJar.getInputStream(pluginDescriptorEntry),
239                                 plugin,
240                                 pluginFile.getAbsolutePath());
241                     }
242                 }
243             } else {
244                 File pluginXml = new File(pluginFile, getPluginDescriptorLocation());
245 
246                 if (pluginXml.isFile()) {
247                     pluginDescriptor = parsePluginDescriptor(
248                             () -> Files.newInputStream(pluginXml.toPath()), plugin, pluginXml.getAbsolutePath());
249                 }
250             }
251 
252             if (pluginDescriptor == null) {
253                 throw new IOException("No plugin descriptor found at " + getPluginDescriptorLocation());
254             }
255         } catch (IOException e) {
256             throw new PluginDescriptorParsingException(plugin, pluginFile.getAbsolutePath(), e);
257         }
258 
259         List<String> errors = new ArrayList<>();
260         pluginValidator.validate(pluginArtifact, pluginDescriptor, errors);
261 
262         if (!errors.isEmpty()) {
263             throw new InvalidPluginDescriptorException(
264                     "Invalid plugin descriptor for " + plugin.getId() + " (" + pluginFile + ")", errors);
265         }
266 
267         pluginDescriptor.setPluginArtifact(pluginArtifact);
268 
269         return pluginDescriptor;
270     }
271 
272     private String getPluginDescriptorLocation() {
273         return "META-INF/maven/plugin.xml";
274     }
275 
276     private PluginDescriptor parsePluginDescriptor(
277             PluginDescriptorBuilder.StreamSupplier is, Plugin plugin, String descriptorLocation)
278             throws PluginDescriptorParsingException {
279         try {
280             return builder.build(is, descriptorLocation);
281         } catch (PlexusConfigurationException e) {
282             throw new PluginDescriptorParsingException(plugin, descriptorLocation, e);
283         }
284     }
285 
286     public MojoDescriptor getMojoDescriptor(
287             Plugin plugin, String goal, List<RemoteRepository> repositories, RepositorySystemSession session)
288             throws MojoNotFoundException, PluginResolutionException, PluginDescriptorParsingException,
289                     InvalidPluginDescriptorException {
290         PluginDescriptor pluginDescriptor = getPluginDescriptor(plugin, repositories, session);
291 
292         MojoDescriptor mojoDescriptor = pluginDescriptor.getMojo(goal);
293 
294         if (mojoDescriptor == null) {
295             throw new MojoNotFoundException(goal, pluginDescriptor);
296         }
297 
298         return mojoDescriptor;
299     }
300 
301     @Override
302     public void checkPrerequisites(PluginDescriptor pluginDescriptor) throws PluginIncompatibleException {
303         List<IllegalStateException> prerequisiteExceptions = new ArrayList<>();
304         prerequisitesCheckers.forEach(c -> {
305             try {
306                 c.accept(pluginDescriptor);
307             } catch (IllegalStateException e) {
308                 prerequisiteExceptions.add(e);
309             }
310         });
311         // aggregate all exceptions
312         if (!prerequisiteExceptions.isEmpty()) {
313             String messages = prerequisiteExceptions.stream()
314                     .map(IllegalStateException::getMessage)
315                     .collect(Collectors.joining(", "));
316             PluginIncompatibleException pie = new PluginIncompatibleException(
317                     pluginDescriptor.getPlugin(),
318                     "The plugin " + pluginDescriptor.getId() + " has unmet prerequisites: " + messages,
319                     prerequisiteExceptions.get(0));
320             // the first exception is added as cause, all other ones as suppressed exceptions
321             prerequisiteExceptions.stream().skip(1).forEach(pie::addSuppressed);
322             throw pie;
323         }
324     }
325 
326     @Override
327     @Deprecated
328     public void checkRequiredMavenVersion(PluginDescriptor pluginDescriptor) throws PluginIncompatibleException {
329         checkPrerequisites(pluginDescriptor);
330     }
331 
332     public void setupPluginRealm(
333             PluginDescriptor pluginDescriptor,
334             MavenSession session,
335             ClassLoader parent,
336             List<String> imports,
337             DependencyFilter filter)
338             throws PluginResolutionException, PluginContainerException {
339         Plugin plugin = pluginDescriptor.getPlugin();
340         MavenProject project = session.getCurrentProject();
341 
342         if (plugin.isExtensions()) {
343             ExtensionRealmCache.CacheRecord extensionRecord;
344             try {
345                 RepositorySystemSession repositorySession = session.getRepositorySession();
346                 extensionRecord = setupExtensionsRealm(project, plugin, repositorySession);
347             } catch (PluginManagerException e) {
348                 // extensions realm is expected to be fully setup at this point
349                 // any exception means a problem in maven code, not a user error
350                 throw new IllegalStateException(e);
351             }
352 
353             ClassRealm pluginRealm = extensionRecord.getRealm();
354             List<Artifact> pluginArtifacts = extensionRecord.getArtifacts();
355 
356             for (ComponentDescriptor<?> componentDescriptor : pluginDescriptor.getComponents()) {
357                 componentDescriptor.setRealm(pluginRealm);
358             }
359 
360             pluginDescriptor.setClassRealm(pluginRealm);
361             pluginDescriptor.setArtifacts(pluginArtifacts);
362         } else {
363             boolean v4api = pluginDescriptor.getMojos().stream().anyMatch(MojoDescriptor::isV4Api);
364             Map<String, ClassLoader> foreignImports = calcImports(project, parent, imports, v4api);
365 
366             PluginRealmCache.Key cacheKey = pluginRealmCache.createKey(
367                     plugin,
368                     parent,
369                     foreignImports,
370                     filter,
371                     project.getRemotePluginRepositories(),
372                     session.getRepositorySession());
373 
374             PluginRealmCache.CacheRecord cacheRecord = pluginRealmCache.get(cacheKey, () -> {
375                 createPluginRealm(pluginDescriptor, session, parent, foreignImports, filter);
376 
377                 return new PluginRealmCache.CacheRecord(
378                         pluginDescriptor.getClassRealm(), pluginDescriptor.getArtifacts());
379             });
380 
381             pluginDescriptor.setClassRealm(cacheRecord.getRealm());
382             pluginDescriptor.setArtifacts(new ArrayList<>(cacheRecord.getArtifacts()));
383             for (ComponentDescriptor<?> componentDescriptor : pluginDescriptor.getComponents()) {
384                 componentDescriptor.setRealm(cacheRecord.getRealm());
385             }
386 
387             pluginRealmCache.register(project, cacheKey, cacheRecord);
388         }
389     }
390 
391     private void createPluginRealm(
392             PluginDescriptor pluginDescriptor,
393             MavenSession session,
394             ClassLoader parent,
395             Map<String, ClassLoader> foreignImports,
396             DependencyFilter filter)
397             throws PluginResolutionException, PluginContainerException {
398         Plugin plugin = Objects.requireNonNull(pluginDescriptor.getPlugin(), "pluginDescriptor.plugin cannot be null");
399 
400         Artifact pluginArtifact = Objects.requireNonNull(
401                 pluginDescriptor.getPluginArtifact(), "pluginDescriptor.pluginArtifact cannot be null");
402 
403         MavenProject project = session.getCurrentProject();
404 
405         final ClassRealm pluginRealm;
406         final List<Artifact> pluginArtifacts;
407 
408         RepositorySystemSession repositorySession = session.getRepositorySession();
409         DependencyFilter dependencyFilter = project.getExtensionDependencyFilter();
410         dependencyFilter = AndDependencyFilter.newInstance(dependencyFilter, filter);
411 
412         DependencyResult result = pluginDependenciesResolver.resolvePlugin(
413                 plugin,
414                 RepositoryUtils.toArtifact(pluginArtifact),
415                 dependencyFilter,
416                 project.getRemotePluginRepositories(),
417                 repositorySession);
418 
419         pluginArtifacts = toMavenArtifacts(result);
420 
421         pluginRealm = classRealmManager.createPluginRealm(
422                 plugin, parent, null, foreignImports, toAetherArtifacts(pluginArtifacts));
423 
424         discoverPluginComponents(pluginRealm, plugin, pluginDescriptor);
425 
426         pluginDescriptor.setDependencyNode(result.getRoot());
427         pluginDescriptor.setClassRealm(pluginRealm);
428         pluginDescriptor.setArtifacts(pluginArtifacts);
429     }
430 
431     private void discoverPluginComponents(
432             final ClassRealm pluginRealm, Plugin plugin, PluginDescriptor pluginDescriptor)
433             throws PluginContainerException {
434         try {
435             if (pluginDescriptor != null) {
436                 for (MojoDescriptor mojo : pluginDescriptor.getMojos()) {
437                     if (!mojo.isV4Api()) {
438                         mojo.setRealm(pluginRealm);
439                         container.addComponentDescriptor(mojo);
440                     }
441                 }
442             }
443 
444             ((DefaultPlexusContainer) container)
445                     .discoverComponents(
446                             pluginRealm,
447                             new SessionScopeModule(container.lookup(SessionScope.class)),
448                             new MojoExecutionScopeModule(container.lookup(MojoExecutionScope.class)),
449                             new PluginConfigurationModule(plugin.getDelegate()));
450         } catch (ComponentLookupException | CycleDetectedInComponentGraphException e) {
451             throw new PluginContainerException(
452                     plugin,
453                     pluginRealm,
454                     "Error in component graph of plugin " + plugin.getId() + ": " + e.getMessage(),
455                     e);
456         }
457     }
458 
459     private List<org.eclipse.aether.artifact.Artifact> toAetherArtifacts(final List<Artifact> pluginArtifacts) {
460         return new ArrayList<>(RepositoryUtils.toArtifacts(pluginArtifacts));
461     }
462 
463     private List<Artifact> toMavenArtifacts(DependencyResult dependencyResult) {
464         return dependencyResult.getDependencyNodeResults().stream()
465                 .filter(n -> n.getArtifact().getPath() != null)
466                 .map(n -> RepositoryUtils.toArtifact(n.getDependency()))
467                 .collect(Collectors.toUnmodifiableList());
468     }
469 
470     private Map<String, ClassLoader> calcImports(
471             MavenProject project, ClassLoader parent, List<String> imports, boolean v4api) {
472         Map<String, ClassLoader> foreignImports = new HashMap<>();
473 
474         ClassLoader projectRealm = project.getClassRealm();
475         if (projectRealm != null) {
476             foreignImports.put("", projectRealm);
477         } else {
478             foreignImports.put(
479                     "", v4api ? classRealmManager.getMaven4ApiRealm() : classRealmManager.getMavenApiRealm());
480         }
481 
482         if (parent != null && imports != null) {
483             for (String parentImport : imports) {
484                 foreignImports.put(parentImport, parent);
485             }
486         }
487 
488         return foreignImports;
489     }
490 
491     public <T> T getConfiguredMojo(Class<T> mojoInterface, MavenSession session, MojoExecution mojoExecution)
492             throws PluginConfigurationException, PluginContainerException {
493         MojoDescriptor mojoDescriptor = mojoExecution.getMojoDescriptor();
494 
495         PluginDescriptor pluginDescriptor = mojoDescriptor.getPluginDescriptor();
496 
497         ClassRealm pluginRealm = pluginDescriptor.getClassRealm();
498 
499         if (pluginRealm == null) {
500             try {
501                 setupPluginRealm(pluginDescriptor, session, null, null, null);
502             } catch (PluginResolutionException e) {
503                 String msg = "Cannot setup plugin realm [mojoDescriptor=" + mojoDescriptor.getId()
504                         + ", pluginDescriptor=" + pluginDescriptor.getId() + "]";
505                 throw new PluginConfigurationException(pluginDescriptor, msg, e);
506             }
507             pluginRealm = pluginDescriptor.getClassRealm();
508         }
509 
510         if (logger.isDebugEnabled()) {
511             logger.debug("Loading mojo " + mojoDescriptor.getId() + " from plugin realm " + pluginRealm);
512         }
513 
514         // We are forcing the use of the plugin realm for all lookups that might occur during
515         // the lifecycle that is part of the lookup. Here we are specifically trying to keep
516         // lookups that occur in contextualize calls in line with the right realm.
517         ClassRealm oldLookupRealm = container.setLookupRealm(pluginRealm);
518 
519         ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader();
520         Thread.currentThread().setContextClassLoader(pluginRealm);
521 
522         try {
523             if (mojoDescriptor.isV4Api()) {
524                 return loadV4Mojo(mojoInterface, session, mojoExecution, mojoDescriptor, pluginDescriptor, pluginRealm);
525             } else {
526                 return loadV3Mojo(mojoInterface, session, mojoExecution, mojoDescriptor, pluginDescriptor, pluginRealm);
527             }
528         } finally {
529             Thread.currentThread().setContextClassLoader(oldClassLoader);
530             container.setLookupRealm(oldLookupRealm);
531         }
532     }
533 
534     private <T> T loadV4Mojo(
535             Class<T> mojoInterface,
536             MavenSession session,
537             MojoExecution mojoExecution,
538             MojoDescriptor mojoDescriptor,
539             PluginDescriptor pluginDescriptor,
540             ClassRealm pluginRealm)
541             throws PluginContainerException, PluginConfigurationException {
542         T mojo;
543 
544         InternalMavenSession sessionV4 = InternalMavenSession.from(session.getSession());
545         Project project = sessionV4.getProject(session.getCurrentProject());
546 
547         org.apache.maven.api.MojoExecution execution = new DefaultMojoExecution(sessionV4, mojoExecution);
548         org.apache.maven.api.plugin.Log log = new DefaultLog(
549                 LoggerFactory.getLogger(mojoExecution.getMojoDescriptor().getFullGoalName()));
550         try {
551             Injector injector = Injector.create();
552             injector.discover(pluginRealm);
553             // Add known classes
554             // TODO: get those from the existing plexus scopes ?
555             injector.bindInstance(Session.class, sessionV4);
556             injector.bindInstance(Project.class, project);
557             injector.bindInstance(org.apache.maven.api.MojoExecution.class, execution);
558             injector.bindInstance(org.apache.maven.api.plugin.Log.class, log);
559             mojo = mojoInterface.cast(injector.getInstance(
560                     Key.of(mojoDescriptor.getImplementationClass(), mojoDescriptor.getRoleHint())));
561 
562         } catch (Exception e) {
563             throw new PluginContainerException(mojoDescriptor, pluginRealm, "Unable to lookup Mojo", e);
564         }
565 
566         XmlNode dom = mojoExecution.getConfiguration() != null
567                 ? mojoExecution.getConfiguration().getDom()
568                 : null;
569 
570         PlexusConfiguration pomConfiguration;
571 
572         if (dom == null) {
573             pomConfiguration = new DefaultPlexusConfiguration("configuration");
574         } else {
575             pomConfiguration = XmlPlexusConfiguration.toPlexusConfiguration(dom);
576         }
577 
578         ExpressionEvaluator expressionEvaluator =
579                 new PluginParameterExpressionEvaluatorV4(sessionV4, project, execution);
580 
581         for (MavenPluginConfigurationValidator validator : configurationValidators) {
582             validator.validate(session, mojoDescriptor, mojo.getClass(), pomConfiguration, expressionEvaluator);
583         }
584 
585         populateMojoExecutionFields(
586                 mojo,
587                 mojoExecution.getExecutionId(),
588                 mojoDescriptor,
589                 pluginRealm,
590                 pomConfiguration,
591                 expressionEvaluator);
592 
593         for (Resolution resolution : mojoDescriptor.getMojoDescriptorV4().getResolutions()) {
594             Field field = null;
595             for (Class<?> clazz = mojo.getClass(); clazz != Object.class; clazz = clazz.getSuperclass()) {
596                 try {
597                     field = clazz.getDeclaredField(resolution.getField());
598                     break;
599                 } catch (NoSuchFieldException e) {
600                     // continue
601                 }
602             }
603             if (field == null) {
604                 throw new PluginConfigurationException(
605                         pluginDescriptor,
606                         "Unable to find field '" + resolution.getField() + "' annotated with @Resolution");
607             }
608             field.setAccessible(true);
609             String pathScope = resolution.getPathScope();
610             Object result = null;
611             if (pathScope != null && !pathScope.isEmpty()) {
612                 // resolution
613                 PathScope ps = sessionV4.getService(PathScopeRegistry.class).require(pathScope);
614                 DependencyResolverResult res =
615                         sessionV4.getService(DependencyResolver.class).resolve(sessionV4, project, ps);
616                 if (field.getType() == DependencyResolverResult.class) {
617                     result = res;
618                 } else if (field.getType() == Node.class) {
619                     result = res.getRoot();
620                 } else if (field.getType() == List.class && field.getGenericType() instanceof ParameterizedType pt) {
621                     Type t = pt.getActualTypeArguments()[0];
622                     if (t == Node.class) {
623                         result = res.getNodes();
624                     } else if (t == Path.class) {
625                         result = res.getPaths();
626                     }
627                 } else if (field.getType() == Map.class && field.getGenericType() instanceof ParameterizedType pt) {
628                     Type k = pt.getActualTypeArguments()[0];
629                     Type v = pt.getActualTypeArguments()[1];
630                     if (k == PathType.class
631                             && v instanceof ParameterizedType ptv
632                             && ptv.getRawType() == List.class
633                             && ptv.getActualTypeArguments()[0] == Path.class) {
634                         result = res.getDispatchedPaths();
635                     } else if (k == Dependency.class && v == Path.class) {
636                         result = res.getDependencies();
637                     }
638                 }
639             } else {
640                 // collection
641                 DependencyResolverResult res = sessionV4
642                         .getService(DependencyResolver.class)
643                         .collect(sessionV4, project, PathScope.MAIN_RUNTIME);
644                 if (field.getType() == DependencyResolverResult.class) {
645                     result = res;
646                 } else if (field.getType() == Node.class) {
647                     result = res.getRoot();
648                 }
649             }
650             if (result == null) {
651                 throw new PluginConfigurationException(
652                         pluginDescriptor,
653                         "Unable to inject field '" + resolution.getField()
654                                 + "' annotated with @Dependencies. Unsupported type " + field.getGenericType());
655             }
656             try {
657                 field.set(mojo, result);
658             } catch (IllegalAccessException e) {
659                 throw new PluginConfigurationException(
660                         pluginDescriptor,
661                         "Unable to inject field '" + resolution.getField() + "' annotated with @Dependencies",
662                         e);
663             }
664         }
665 
666         return mojo;
667     }
668 
669     private <T> T loadV3Mojo(
670             Class<T> mojoInterface,
671             MavenSession session,
672             MojoExecution mojoExecution,
673             MojoDescriptor mojoDescriptor,
674             PluginDescriptor pluginDescriptor,
675             ClassRealm pluginRealm)
676             throws PluginContainerException, PluginConfigurationException {
677         T mojo;
678 
679         try {
680             mojo = container.lookup(mojoInterface, mojoDescriptor.getRoleHint());
681         } catch (ComponentLookupException e) {
682             Throwable cause = e.getCause();
683             while (cause != null && !(cause instanceof LinkageError) && !(cause instanceof ClassNotFoundException)) {
684                 cause = cause.getCause();
685             }
686 
687             if ((cause instanceof NoClassDefFoundError) || (cause instanceof ClassNotFoundException)) {
688                 ByteArrayOutputStream os = new ByteArrayOutputStream(1024);
689                 PrintStream ps = new PrintStream(os);
690                 ps.println("Unable to load the mojo '" + mojoDescriptor.getGoal() + "' in the plugin '"
691                         + pluginDescriptor.getId() + "'. A required class is missing: "
692                         + cause.getMessage());
693                 pluginRealm.display(ps);
694 
695                 throw new PluginContainerException(mojoDescriptor, pluginRealm, os.toString(), cause);
696             } else if (cause instanceof LinkageError) {
697                 ByteArrayOutputStream os = new ByteArrayOutputStream(1024);
698                 PrintStream ps = new PrintStream(os);
699                 ps.println("Unable to load the mojo '" + mojoDescriptor.getGoal() + "' in the plugin '"
700                         + pluginDescriptor.getId() + "' due to an API incompatibility: "
701                         + e.getClass().getName() + ": " + cause.getMessage());
702                 pluginRealm.display(ps);
703 
704                 throw new PluginContainerException(mojoDescriptor, pluginRealm, os.toString(), cause);
705             }
706 
707             throw new PluginContainerException(
708                     mojoDescriptor,
709                     pluginRealm,
710                     "Unable to load the mojo '" + mojoDescriptor.getGoal()
711                             + "' (or one of its required components) from the plugin '"
712                             + pluginDescriptor.getId() + "'",
713                     e);
714         }
715 
716         if (mojo instanceof ContextEnabled) {
717             MavenProject project = session.getCurrentProject();
718 
719             Map<String, Object> pluginContext = session.getPluginContext(pluginDescriptor, project);
720 
721             if (pluginContext != null) {
722                 pluginContext.put("project", project);
723 
724                 pluginContext.put("pluginDescriptor", pluginDescriptor);
725 
726                 ((ContextEnabled) mojo).setPluginContext(pluginContext);
727             }
728         }
729 
730         if (mojo instanceof Mojo) {
731             Logger mojoLogger = LoggerFactory.getLogger(mojoDescriptor.getImplementation());
732             ((Mojo) mojo).setLog(new MojoLogWrapper(mojoLogger));
733         }
734 
735         if (mojo instanceof Contextualizable) {
736             pluginValidationManager.reportPluginMojoValidationIssue(
737                     PluginValidationManager.IssueLocality.EXTERNAL,
738                     session,
739                     mojoDescriptor,
740                     mojo.getClass(),
741                     "Mojo implements `Contextualizable` interface from Plexus Container, which is EOL.");
742         }
743 
744         XmlNode dom = mojoExecution.getConfiguration() != null
745                 ? mojoExecution.getConfiguration().getDom()
746                 : null;
747 
748         PlexusConfiguration pomConfiguration;
749 
750         if (dom == null) {
751             pomConfiguration = new DefaultPlexusConfiguration("configuration");
752         } else {
753             pomConfiguration = XmlPlexusConfiguration.toPlexusConfiguration(dom);
754         }
755 
756         ExpressionEvaluator expressionEvaluator = new PluginParameterExpressionEvaluator(session, mojoExecution);
757 
758         for (MavenPluginConfigurationValidator validator : configurationValidators) {
759             validator.validate(session, mojoDescriptor, mojo.getClass(), pomConfiguration, expressionEvaluator);
760         }
761 
762         populateMojoExecutionFields(
763                 mojo,
764                 mojoExecution.getExecutionId(),
765                 mojoDescriptor,
766                 pluginRealm,
767                 pomConfiguration,
768                 expressionEvaluator);
769 
770         return mojo;
771     }
772 
773     private void populateMojoExecutionFields(
774             Object mojo,
775             String executionId,
776             MojoDescriptor mojoDescriptor,
777             ClassRealm pluginRealm,
778             PlexusConfiguration configuration,
779             ExpressionEvaluator expressionEvaluator)
780             throws PluginConfigurationException {
781         ComponentConfigurator configurator = null;
782 
783         String configuratorId = mojoDescriptor.getComponentConfigurator();
784 
785         if (configuratorId == null || configuratorId.isEmpty()) {
786             configuratorId = mojoDescriptor.isV4Api() ? "enhanced" : "basic";
787         }
788 
789         try {
790             // TODO could the configuration be passed to lookup and the configurator known to plexus via the descriptor
791             // so that this method could entirely be handled by a plexus lookup?
792             configurator = container.lookup(ComponentConfigurator.class, configuratorId);
793 
794             ConfigurationListener listener = new DebugConfigurationListener(logger);
795 
796             ValidatingConfigurationListener validator =
797                     new ValidatingConfigurationListener(mojo, mojoDescriptor, listener);
798 
799             if (logger.isDebugEnabled()) {
800                 logger.debug("Configuring mojo execution '" + mojoDescriptor.getId() + ':' + executionId + "' with "
801                         + configuratorId + " configurator -->");
802             }
803 
804             configurator.configureComponent(mojo, configuration, expressionEvaluator, pluginRealm, validator);
805 
806             logger.debug("-- end configuration --");
807 
808             Collection<Parameter> missingParameters = validator.getMissingParameters();
809             if (!missingParameters.isEmpty()) {
810                 if ("basic".equals(configuratorId)) {
811                     throw new PluginParameterException(mojoDescriptor, new ArrayList<>(missingParameters));
812                 } else {
813                     /*
814                      * NOTE: Other configurators like the map-oriented one don't call into the listener, so do it the
815                      * hard way.
816                      */
817                     validateParameters(mojoDescriptor, configuration, expressionEvaluator);
818                 }
819             }
820 
821         } catch (ComponentConfigurationException e) {
822             String message = "Unable to parse configuration of mojo " + mojoDescriptor.getId();
823             if (e.getFailedConfiguration() != null) {
824                 message += " for parameter " + e.getFailedConfiguration().getName();
825             }
826             message += ": " + e.getMessage();
827 
828             throw new PluginConfigurationException(mojoDescriptor.getPluginDescriptor(), message, e);
829         } catch (ComponentLookupException e) {
830             throw new PluginConfigurationException(
831                     mojoDescriptor.getPluginDescriptor(),
832                     "Unable to retrieve component configurator " + configuratorId + " for configuration of mojo "
833                             + mojoDescriptor.getId(),
834                     e);
835         } catch (NoClassDefFoundError e) {
836             ByteArrayOutputStream os = new ByteArrayOutputStream(1024);
837             PrintStream ps = new PrintStream(os);
838             ps.println("A required class was missing during configuration of mojo " + mojoDescriptor.getId() + ": "
839                     + e.getMessage());
840             pluginRealm.display(ps);
841 
842             throw new PluginConfigurationException(mojoDescriptor.getPluginDescriptor(), os.toString(), e);
843         } catch (LinkageError e) {
844             ByteArrayOutputStream os = new ByteArrayOutputStream(1024);
845             PrintStream ps = new PrintStream(os);
846             ps.println("An API incompatibility was encountered during configuration of mojo " + mojoDescriptor.getId()
847                     + ": " + e.getClass().getName() + ": " + e.getMessage());
848             pluginRealm.display(ps);
849 
850             throw new PluginConfigurationException(mojoDescriptor.getPluginDescriptor(), os.toString(), e);
851         } finally {
852             if (configurator != null) {
853                 try {
854                     container.release(configurator);
855                 } catch (ComponentLifecycleException e) {
856                     logger.debug("Failed to release mojo configurator - ignoring.");
857                 }
858             }
859         }
860     }
861 
862     private void validateParameters(
863             MojoDescriptor mojoDescriptor, PlexusConfiguration configuration, ExpressionEvaluator expressionEvaluator)
864             throws ComponentConfigurationException, PluginParameterException {
865         if (mojoDescriptor.getParameters() == null) {
866             return;
867         }
868 
869         List<Parameter> invalidParameters = new ArrayList<>();
870 
871         for (Parameter parameter : mojoDescriptor.getParameters()) {
872             if (!parameter.isRequired()) {
873                 continue;
874             }
875 
876             Object value = null;
877 
878             PlexusConfiguration config = configuration.getChild(parameter.getName(), false);
879             if (config != null) {
880                 String expression = config.getValue(null);
881 
882                 try {
883                     value = expressionEvaluator.evaluate(expression);
884 
885                     if (value == null) {
886                         value = config.getAttribute("default-value", null);
887                     }
888                 } catch (ExpressionEvaluationException e) {
889                     String msg = "Error evaluating the expression '" + expression + "' for configuration value '"
890                             + configuration.getName() + "'";
891                     throw new ComponentConfigurationException(configuration, msg, e);
892                 }
893             }
894 
895             if (value == null && (config == null || config.getChildCount() <= 0)) {
896                 invalidParameters.add(parameter);
897             }
898         }
899 
900         if (!invalidParameters.isEmpty()) {
901             throw new PluginParameterException(mojoDescriptor, invalidParameters);
902         }
903     }
904 
905     public void releaseMojo(Object mojo, MojoExecution mojoExecution) {
906         if (mojo != null) {
907             try {
908                 container.release(mojo);
909             } catch (ComponentLifecycleException e) {
910                 String goalExecId = mojoExecution.getGoal();
911 
912                 if (mojoExecution.getExecutionId() != null) {
913                     logger.debug(
914                             "Error releasing mojo for {} {execution: {}}",
915                             goalExecId,
916                             mojoExecution.getExecutionId(),
917                             e);
918                 } else {
919                     logger.debug("Error releasing mojo for {}", goalExecId, e);
920                 }
921             }
922         }
923     }
924 
925     public ExtensionRealmCache.CacheRecord setupExtensionsRealm(
926             MavenProject project, Plugin plugin, RepositorySystemSession session) throws PluginManagerException {
927         @SuppressWarnings("unchecked")
928         Map<String, ExtensionRealmCache.CacheRecord> pluginRealms =
929                 (Map<String, ExtensionRealmCache.CacheRecord>) project.getContextValue(KEY_EXTENSIONS_REALMS);
930         if (pluginRealms == null) {
931             pluginRealms = new HashMap<>();
932             project.setContextValue(KEY_EXTENSIONS_REALMS, pluginRealms);
933         }
934 
935         final String pluginKey = plugin.getId();
936 
937         ExtensionRealmCache.CacheRecord extensionRecord = pluginRealms.get(pluginKey);
938         if (extensionRecord != null) {
939             return extensionRecord;
940         }
941 
942         final List<RemoteRepository> repositories = project.getRemotePluginRepositories();
943 
944         // resolve plugin version as necessary
945         if (plugin.getVersion() == null) {
946             PluginVersionRequest versionRequest = new DefaultPluginVersionRequest(plugin, session, repositories);
947             try {
948                 plugin.setVersion(pluginVersionResolver.resolve(versionRequest).getVersion());
949             } catch (PluginVersionResolutionException e) {
950                 throw new PluginManagerException(plugin, e.getMessage(), e);
951             }
952         }
953 
954         // TODO: store plugin version
955 
956         // resolve plugin artifacts
957         List<Artifact> artifacts;
958         PluginArtifactsCache.Key cacheKey = pluginArtifactsCache.createKey(plugin, null, repositories, session);
959         PluginArtifactsCache.CacheRecord recordArtifacts;
960         try {
961             recordArtifacts = pluginArtifactsCache.get(cacheKey);
962         } catch (PluginResolutionException e) {
963             throw new PluginManagerException(plugin, e.getMessage(), e);
964         }
965         if (recordArtifacts != null) {
966             artifacts = recordArtifacts.getArtifacts();
967         } else {
968             try {
969                 artifacts = resolveExtensionArtifacts(plugin, repositories, session);
970                 recordArtifacts = pluginArtifactsCache.put(cacheKey, artifacts);
971             } catch (PluginResolutionException e) {
972                 pluginArtifactsCache.put(cacheKey, e);
973                 pluginArtifactsCache.register(project, cacheKey, recordArtifacts);
974                 throw new PluginManagerException(plugin, e.getMessage(), e);
975             }
976         }
977         pluginArtifactsCache.register(project, cacheKey, recordArtifacts);
978 
979         // create and cache extensions realms
980         final ExtensionRealmCache.Key extensionKey = extensionRealmCache.createKey(artifacts);
981         extensionRecord = extensionRealmCache.get(extensionKey);
982         if (extensionRecord == null) {
983             ClassRealm extensionRealm = classRealmManager.createExtensionRealm(plugin, toAetherArtifacts(artifacts));
984 
985             // TODO figure out how to use the same PluginDescriptor when running mojos
986 
987             PluginDescriptor pluginDescriptor = null;
988             if (plugin.isExtensions() && !artifacts.isEmpty()) {
989                 // ignore plugin descriptor parsing errors at this point
990                 // these errors will reported during calculation of project build execution plan
991                 try {
992                     pluginDescriptor = extractPluginDescriptor(artifacts.get(0), plugin);
993                 } catch (PluginDescriptorParsingException | InvalidPluginDescriptorException e) {
994                     // ignore, see above
995                 }
996             }
997 
998             discoverPluginComponents(extensionRealm, plugin, pluginDescriptor);
999 
1000             ExtensionDescriptor extensionDescriptor = null;
1001             Artifact extensionArtifact = artifacts.get(0);
1002             try {
1003                 extensionDescriptor = extensionDescriptorBuilder.build(extensionArtifact.getFile());
1004             } catch (IOException e) {
1005                 String message = "Invalid extension descriptor for " + plugin.getId() + ": " + e.getMessage();
1006                 if (logger.isDebugEnabled()) {
1007                     logger.error(message, e);
1008                 } else {
1009                     logger.error(message);
1010                 }
1011             }
1012             extensionRecord = extensionRealmCache.put(extensionKey, extensionRealm, extensionDescriptor, artifacts);
1013         }
1014         extensionRealmCache.register(project, extensionKey, extensionRecord);
1015         pluginRealms.put(pluginKey, extensionRecord);
1016 
1017         return extensionRecord;
1018     }
1019 
1020     private List<Artifact> resolveExtensionArtifacts(
1021             Plugin extensionPlugin, List<RemoteRepository> repositories, RepositorySystemSession session)
1022             throws PluginResolutionException {
1023         DependencyResult root =
1024                 pluginDependenciesResolver.resolvePlugin(extensionPlugin, null, null, repositories, session);
1025         return toMavenArtifacts(root);
1026     }
1027 
1028     static class NamedImpl implements Named {
1029         private final String value;
1030 
1031         NamedImpl(String value) {
1032             this.value = value;
1033         }
1034 
1035         public String value() {
1036             return this.value;
1037         }
1038 
1039         public int hashCode() {
1040             return 127 * "value".hashCode() ^ this.value.hashCode();
1041         }
1042 
1043         public boolean equals(Object o) {
1044             return o instanceof Named && this.value.equals(((Named) o).value());
1045         }
1046 
1047         public Class<? extends Annotation> annotationType() {
1048             return Named.class;
1049         }
1050     }
1051 }