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.api.plugin.testing;
20  
21  import java.io.BufferedReader;
22  import java.io.File;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.Reader;
26  import java.io.StringReader;
27  import java.lang.reflect.AccessibleObject;
28  import java.lang.reflect.AnnotatedElement;
29  import java.lang.reflect.Field;
30  import java.net.URL;
31  import java.nio.file.Files;
32  import java.nio.file.Path;
33  import java.nio.file.Paths;
34  import java.util.ArrayList;
35  import java.util.Arrays;
36  import java.util.HashMap;
37  import java.util.LinkedHashSet;
38  import java.util.List;
39  import java.util.Map;
40  import java.util.Objects;
41  import java.util.Optional;
42  import java.util.Set;
43  import java.util.stream.Collectors;
44  import java.util.stream.Stream;
45  
46  import org.apache.maven.api.MojoExecution;
47  import org.apache.maven.api.Project;
48  import org.apache.maven.api.Session;
49  import org.apache.maven.api.di.Named;
50  import org.apache.maven.api.di.Priority;
51  import org.apache.maven.api.di.Provides;
52  import org.apache.maven.api.di.Singleton;
53  import org.apache.maven.api.di.testing.MavenDIExtension;
54  import org.apache.maven.api.model.Build;
55  import org.apache.maven.api.model.ConfigurationContainer;
56  import org.apache.maven.api.model.Model;
57  import org.apache.maven.api.model.Source;
58  import org.apache.maven.api.plugin.Log;
59  import org.apache.maven.api.plugin.Mojo;
60  import org.apache.maven.api.plugin.descriptor.MojoDescriptor;
61  import org.apache.maven.api.plugin.descriptor.Parameter;
62  import org.apache.maven.api.plugin.descriptor.PluginDescriptor;
63  import org.apache.maven.api.plugin.testing.stubs.MojoExecutionStub;
64  import org.apache.maven.api.plugin.testing.stubs.PluginStub;
65  import org.apache.maven.api.plugin.testing.stubs.ProducedArtifactStub;
66  import org.apache.maven.api.plugin.testing.stubs.ProjectStub;
67  import org.apache.maven.api.plugin.testing.stubs.RepositorySystemSupplier;
68  import org.apache.maven.api.plugin.testing.stubs.SessionMock;
69  import org.apache.maven.api.services.ArtifactDeployer;
70  import org.apache.maven.api.services.ArtifactFactory;
71  import org.apache.maven.api.services.ArtifactInstaller;
72  import org.apache.maven.api.services.ArtifactManager;
73  import org.apache.maven.api.services.LocalRepositoryManager;
74  import org.apache.maven.api.services.ProjectBuilder;
75  import org.apache.maven.api.services.ProjectManager;
76  import org.apache.maven.api.services.RepositoryFactory;
77  import org.apache.maven.api.services.VersionParser;
78  import org.apache.maven.api.services.xml.ModelXmlFactory;
79  import org.apache.maven.api.xml.XmlNode;
80  import org.apache.maven.api.xml.XmlService;
81  import org.apache.maven.configuration.internal.EnhancedComponentConfigurator;
82  import org.apache.maven.di.Injector;
83  import org.apache.maven.di.Key;
84  import org.apache.maven.di.impl.DIException;
85  import org.apache.maven.impl.InternalSession;
86  import org.apache.maven.impl.model.DefaultModelPathTranslator;
87  import org.apache.maven.impl.model.DefaultPathTranslator;
88  import org.apache.maven.internal.impl.DefaultLog;
89  import org.apache.maven.internal.xml.XmlPlexusConfiguration;
90  import org.apache.maven.lifecycle.internal.MojoDescriptorCreator;
91  import org.apache.maven.model.v4.MavenMerger;
92  import org.apache.maven.model.v4.MavenStaxReader;
93  import org.apache.maven.plugin.PluginParameterExpressionEvaluatorV4;
94  import org.apache.maven.plugin.descriptor.io.PluginDescriptorStaxReader;
95  import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
96  import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluator;
97  import org.codehaus.plexus.component.configurator.expression.TypeAwareExpressionEvaluator;
98  import org.codehaus.plexus.util.ReflectionUtils;
99  import org.codehaus.plexus.util.xml.XmlStreamReader;
100 import org.codehaus.plexus.util.xml.Xpp3Dom;
101 import org.codehaus.plexus.util.xml.Xpp3DomBuilder;
102 import org.eclipse.aether.RepositorySystem;
103 import org.junit.jupiter.api.extension.BeforeEachCallback;
104 import org.junit.jupiter.api.extension.ExtensionContext;
105 import org.junit.jupiter.api.extension.ParameterContext;
106 import org.junit.jupiter.api.extension.ParameterResolutionException;
107 import org.junit.jupiter.api.extension.ParameterResolver;
108 import org.junit.platform.commons.support.AnnotationSupport;
109 import org.slf4j.LoggerFactory;
110 
111 import static java.util.Objects.requireNonNull;
112 
113 /**
114  * JUnit Jupiter extension that provides support for testing Maven plugins (Mojos).
115  * This extension handles the lifecycle of Mojo instances in tests, including instantiation,
116  * configuration, and dependency injection.
117  *
118  * <p>The extension is automatically registered when using the {@link MojoTest} annotation
119  * on a test class. It provides the following features:</p>
120  * <ul>
121  *   <li>Automatic Mojo instantiation based on {@link InjectMojo} annotations</li>
122  *   <li>Parameter injection using {@link MojoParameter} annotations</li>
123  *   <li>POM configuration handling</li>
124  *   <li>Project stub creation and configuration</li>
125  *   <li>Maven session and build context setup</li>
126  *   <li>Component dependency injection</li>
127  * </ul>
128  *
129  * <p>Example usage in a test class:</p>
130  * <pre>
131  * {@code
132  * @MojoTest
133  * class MyMojoTest {
134  *     @Test
135  *     @InjectMojo(goal = "my-goal")
136  *     @MojoParameter(name = "outputDirectory", value = "${project.build.directory}/generated")
137  *     void testMojoExecution(MyMojo mojo) throws Exception {
138  *         mojo.execute();
139  *         // verify execution results
140  *     }
141  * }
142  * }
143  * </pre>
144  *
145  * <p>The extension supports two main injection scenarios:</p>
146  * <ol>
147  *   <li>Method parameter injection: Mojo instances can be injected as test method parameters</li>
148  *   <li>Field injection: Components can be injected into test class fields using {@code @Inject}</li>
149  * </ol>
150  *
151  * <p>For custom POM configurations, you can specify a POM file using the {@link InjectMojo#pom()}
152  * attribute. The extension will merge this configuration with default test project settings.</p>
153  *
154  * <p>Base directory handling:</p>
155  * <ul>
156  *   <li>Plugin basedir: The directory containing the plugin project</li>
157  *   <li>Test basedir: The directory containing test resources, configurable via {@link Basedir}</li>
158  * </ul>
159  *
160  * @see MojoTest
161  * @see InjectMojo
162  * @see MojoParameter
163  * @see Basedir
164  * @since 4.0.0
165  */
166 public class MojoExtension extends MavenDIExtension implements ParameterResolver, BeforeEachCallback {
167 
168     /** The base directory of the plugin being tested */
169     protected static String pluginBasedir;
170 
171     /** The base directory for test resources */
172     protected static String basedir;
173 
174     /**
175      * Gets the identifier for the current test method.
176      * The format is "TestClassName-testMethodName".
177      *
178      * @return the test identifier
179      */
180     public static String getTestId() {
181         return context.getRequiredTestClass().getSimpleName() + "-"
182                 + context.getRequiredTestMethod().getName();
183     }
184 
185     /**
186      * Gets the base directory for test resources.
187      * If not explicitly set via {@link Basedir}, returns the plugin base directory.
188      *
189      * @return the base directory path
190      * @throws NullPointerException if neither basedir nor plugin basedir is set
191      */
192     public static String getBasedir() {
193         return requireNonNull(basedir != null ? basedir : MavenDIExtension.basedir);
194     }
195 
196     /**
197      * Gets the base directory of the plugin being tested.
198      *
199      * @return the plugin base directory path
200      * @throws NullPointerException if plugin basedir is not set
201      */
202     public static String getPluginBasedir() {
203         return requireNonNull(pluginBasedir);
204     }
205 
206     /**
207      * Determines if this extension can resolve the given parameter.
208      * Returns true if the parameter is annotated with {@link InjectMojo} or
209      * if its declaring method is annotated with {@link InjectMojo}.
210      *
211      * @param parameterContext the context for the parameter being resolved
212      * @param extensionContext the current extension context
213      * @return true if this extension can resolve the parameter
214      */
215     @Override
216     public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
217             throws ParameterResolutionException {
218         return parameterContext.isAnnotated(InjectMojo.class)
219                 || parameterContext.getDeclaringExecutable().isAnnotationPresent(InjectMojo.class);
220     }
221 
222     @Override
223     public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
224             throws ParameterResolutionException {
225         try {
226             Class<?> holder = parameterContext.getTarget().orElseThrow().getClass();
227             PluginDescriptor descriptor = extensionContext
228                     .getStore(ExtensionContext.Namespace.GLOBAL)
229                     .get(PluginDescriptor.class, PluginDescriptor.class);
230             Model model =
231                     extensionContext.getStore(ExtensionContext.Namespace.GLOBAL).get(Model.class, Model.class);
232             InjectMojo parameterInjectMojo =
233                     parameterContext.getAnnotatedElement().getAnnotation(InjectMojo.class);
234             String goal;
235             if (parameterInjectMojo != null) {
236                 String pom = parameterInjectMojo.pom();
237                 if (pom != null && !pom.isEmpty()) {
238                     try (Reader r = openPomUrl(holder, pom, new Path[1])) {
239                         Model localModel = new MavenStaxReader().read(r);
240                         model = new MavenMerger().merge(localModel, model, false, null);
241                         model = new DefaultModelPathTranslator(new DefaultPathTranslator())
242                                 .alignToBaseDirectory(model, Paths.get(getBasedir()), null);
243                     }
244                 }
245                 goal = parameterInjectMojo.goal();
246             } else {
247                 InjectMojo methodInjectMojo = AnnotationSupport.findAnnotation(
248                                 parameterContext.getDeclaringExecutable(), InjectMojo.class)
249                         .orElse(null);
250                 if (methodInjectMojo != null) {
251                     goal = methodInjectMojo.goal();
252                 } else {
253                     goal = getGoalFromMojoImplementationClass(
254                             parameterContext.getParameter().getType());
255                 }
256             }
257 
258             Set<MojoParameter> mojoParameters = new LinkedHashSet<>();
259             for (AnnotatedElement ae :
260                     Arrays.asList(parameterContext.getDeclaringExecutable(), parameterContext.getAnnotatedElement())) {
261                 mojoParameters.addAll(AnnotationSupport.findRepeatableAnnotations(ae, MojoParameter.class));
262             }
263             String[] coord = mojoCoordinates(goal);
264 
265             XmlNode pluginConfiguration = model.getBuild().getPlugins().stream()
266                     .filter(p ->
267                             Objects.equals(p.getGroupId(), coord[0]) && Objects.equals(p.getArtifactId(), coord[1]))
268                     .map(ConfigurationContainer::getConfiguration)
269                     .findFirst()
270                     .orElseGet(() -> XmlNode.newInstance("config"));
271             List<XmlNode> children = mojoParameters.stream()
272                     .map(mp -> XmlNode.newInstance(mp.name(), mp.value()))
273                     .collect(Collectors.toList());
274             XmlNode config = XmlNode.newInstance("configuration", null, null, children, null);
275             pluginConfiguration = XmlService.merge(config, pluginConfiguration);
276 
277             // load default config
278             // pluginkey = groupId : artifactId : version : goal
279             Mojo mojo = lookup(Mojo.class, coord[0] + ":" + coord[1] + ":" + coord[2] + ":" + coord[3]);
280             for (MojoDescriptor mojoDescriptor : descriptor.getMojos()) {
281                 if (Objects.equals(mojoDescriptor.getGoal(), coord[3])) {
282                     if (pluginConfiguration != null) {
283                         pluginConfiguration = finalizeConfig(pluginConfiguration, mojoDescriptor);
284                     }
285                 }
286             }
287 
288             Session session = getInjector().getInstance(Session.class);
289             Project project = getInjector().getInstance(Project.class);
290             MojoExecution mojoExecution = getInjector().getInstance(MojoExecution.class);
291             ExpressionEvaluator evaluator = new WrapEvaluator(
292                     getInjector(), new PluginParameterExpressionEvaluatorV4(session, project, mojoExecution));
293 
294             EnhancedComponentConfigurator configurator = new EnhancedComponentConfigurator();
295             configurator.configureComponent(
296                     mojo, new XmlPlexusConfiguration(pluginConfiguration), evaluator, null, null);
297             return mojo;
298         } catch (Exception e) {
299             throw new ParameterResolutionException("Unable to resolve mojo", e);
300         }
301     }
302 
303     /**
304      * The @Mojo annotation is only retained in the class file, not at runtime,
305      * so we need to actually read the class file with ASM to find the annotation and
306      * the goal.
307      */
308     private static String getGoalFromMojoImplementationClass(Class<?> cl) throws IOException {
309         return cl.getAnnotation(Named.class).value();
310     }
311 
312     @Override
313     @SuppressWarnings("checkstyle:MethodLength")
314     public void beforeEach(ExtensionContext context) throws Exception {
315         if (pluginBasedir == null) {
316             pluginBasedir = MavenDIExtension.getBasedir();
317         }
318         basedir = AnnotationSupport.findAnnotation(context.getElement().orElseThrow(), Basedir.class)
319                 .map(Basedir::value)
320                 .orElse(pluginBasedir);
321         if (basedir != null) {
322             if (basedir.isEmpty()) {
323                 basedir = pluginBasedir + "/target/tests/"
324                         + context.getRequiredTestClass().getSimpleName() + "/"
325                         + context.getRequiredTestMethod().getName();
326             } else {
327                 basedir = basedir.replace("${basedir}", pluginBasedir);
328             }
329         }
330 
331         setContext(context);
332 
333         /*
334            binder.install(ProviderMethodsModule.forObject(context.getRequiredTestInstance()));
335            binder.requestInjection(context.getRequiredTestInstance());
336            binder.bind(Log.class).toInstance(new DefaultLog(LoggerFactory.getLogger("anonymous")));
337            binder.bind(ExtensionContext.class).toInstance(context);
338            // Load maven 4 api Services interfaces and try to bind them to the (possible) mock instances
339            // returned by the (possibly) mock InternalSession
340            try {
341                for (ClassPath.ClassInfo clazz :
342                        ClassPath.from(getClassLoader()).getAllClasses()) {
343                    if ("org.apache.maven.api.services".equals(clazz.getPackageName())) {
344                        Class<?> load = clazz.load();
345                        if (Service.class.isAssignableFrom(load)) {
346                            Class<Service> svc = (Class) load;
347                            binder.bind(svc).toProvider(() -> {
348                                try {
349                                    return getContainer()
350                                            .lookup(InternalSession.class)
351                                            .getService(svc);
352                                } catch (ComponentLookupException e) {
353                                    throw new RuntimeException("Unable to lookup service " + svc.getName());
354                                }
355                            });
356                        }
357                    }
358                }
359            } catch (Exception e) {
360                throw new RuntimeException("Unable to bind session services", e);
361            }
362 
363         */
364 
365         Path basedirPath = Paths.get(getBasedir());
366 
367         InjectMojo mojo = AnnotationSupport.findAnnotation(context.getElement().get(), InjectMojo.class)
368                 .orElse(null);
369         Model defaultModel = Model.newBuilder()
370                 .groupId("myGroupId")
371                 .artifactId("myArtifactId")
372                 .version("1.0-SNAPSHOT")
373                 .packaging("jar")
374                 .build(Build.newBuilder()
375                         .directory(basedirPath.resolve("target").toString())
376                         .outputDirectory(basedirPath.resolve("target/classes").toString())
377                         .sources(List.of(
378                                 Source.newBuilder()
379                                         .scope("main")
380                                         .lang("java")
381                                         .directory(basedirPath
382                                                 .resolve("src/main/java")
383                                                 .toString())
384                                         .build(),
385                                 Source.newBuilder()
386                                         .scope("test")
387                                         .lang("java")
388                                         .directory(basedirPath
389                                                 .resolve("src/test/java")
390                                                 .toString())
391                                         .build()))
392                         .testOutputDirectory(
393                                 basedirPath.resolve("target/test-classes").toString())
394                         .build())
395                 .build();
396         Path[] modelPath = new Path[] {null};
397         Model tmodel = null;
398         if (mojo != null) {
399             String pom = mojo.pom();
400             if (pom != null && !pom.isEmpty()) {
401                 try (Reader r = openPomUrl(context.getRequiredTestClass(), pom, modelPath)) {
402                     tmodel = new MavenStaxReader().read(r);
403                 }
404             } else {
405                 Path pomPath = basedirPath.resolve("pom.xml");
406                 if (Files.exists(pomPath)) {
407                     try (Reader r = Files.newBufferedReader(pomPath)) {
408                         tmodel = new MavenStaxReader().read(r);
409                         modelPath[0] = pomPath;
410                     }
411                 }
412             }
413         }
414         Model model;
415         if (tmodel == null) {
416             model = defaultModel;
417         } else {
418             model = new MavenMerger().merge(tmodel, defaultModel, false, null);
419         }
420         tmodel = new DefaultModelPathTranslator(new DefaultPathTranslator())
421                 .alignToBaseDirectory(tmodel, Paths.get(getBasedir()), null);
422         context.getStore(ExtensionContext.Namespace.GLOBAL).put(Model.class, tmodel);
423 
424         // mojo execution
425         // Map<Object, Object> map = getInjector().getContext().getContextData();
426         PluginDescriptor pluginDescriptor;
427         ClassLoader classLoader = context.getRequiredTestClass().getClassLoader();
428         try (InputStream is = requireNonNull(
429                         classLoader.getResourceAsStream(getPluginDescriptorLocation()),
430                         "Unable to find plugin descriptor: " + getPluginDescriptorLocation());
431                 Reader reader = new BufferedReader(new XmlStreamReader(is))) {
432             // new InterpolationFilterReader(reader, map, "${", "}");
433             pluginDescriptor = new PluginDescriptorStaxReader().read(reader);
434         }
435         context.getStore(ExtensionContext.Namespace.GLOBAL).put(PluginDescriptor.class, pluginDescriptor);
436         // for (ComponentDescriptor<?> desc : pluginDescriptor.getComponents()) {
437         //    getContainer().addComponentDescriptor(desc);
438         // }
439 
440         @SuppressWarnings({"unused", "MagicNumber"})
441         class Foo {
442 
443             @Provides
444             @Singleton
445             @Priority(-10)
446             private InternalSession createSession() {
447                 return SessionMock.getMockSession(getBasedir());
448             }
449 
450             @Provides
451             @Singleton
452             @Priority(-10)
453             private Project createProject(InternalSession s) {
454                 ProjectStub stub = new ProjectStub();
455                 if (!"pom".equals(model.getPackaging())) {
456                     ProducedArtifactStub artifact = new ProducedArtifactStub(
457                             model.getGroupId(), model.getArtifactId(), "", model.getVersion(), model.getPackaging());
458                     stub.setMainArtifact(artifact);
459                 }
460                 stub.setModel(model);
461                 stub.setBasedir(Paths.get(MojoExtension.getBasedir()));
462                 stub.setPomPath(modelPath[0]);
463                 s.getService(ArtifactManager.class).setPath(stub.getPomArtifact(), modelPath[0]);
464                 return stub;
465             }
466 
467             @Provides
468             @Singleton
469             @Priority(-10)
470             private MojoExecution createMojoExecution() {
471                 MojoExecutionStub mes = new MojoExecutionStub("executionId", null);
472                 if (mojo != null) {
473                     String goal = mojo.goal();
474                     int idx = goal.lastIndexOf(':');
475                     if (idx >= 0) {
476                         goal = goal.substring(idx + 1);
477                     }
478                     mes.setGoal(goal);
479                     for (MojoDescriptor md : pluginDescriptor.getMojos()) {
480                         if (goal.equals(md.getGoal())) {
481                             mes.setDescriptor(md);
482                         }
483                     }
484                     requireNonNull(mes.getDescriptor());
485                 }
486                 PluginStub plugin = new PluginStub();
487                 plugin.setDescriptor(pluginDescriptor);
488                 mes.setPlugin(plugin);
489                 return mes;
490             }
491 
492             @Provides
493             @Singleton
494             @Priority(-10)
495             private Log createLog() {
496                 return new DefaultLog(LoggerFactory.getLogger("anonymous"));
497             }
498 
499             @Provides
500             static RepositorySystemSupplier newRepositorySystemSupplier() {
501                 return new RepositorySystemSupplier();
502             }
503 
504             @Provides
505             static RepositorySystem newRepositorySystem(RepositorySystemSupplier repositorySystemSupplier) {
506                 return repositorySystemSupplier.getRepositorySystem();
507             }
508 
509             @Provides
510             @Priority(10)
511             static RepositoryFactory newRepositoryFactory(Session session) {
512                 return session.getService(RepositoryFactory.class);
513             }
514 
515             @Provides
516             @Priority(10)
517             static VersionParser newVersionParser(Session session) {
518                 return session.getService(VersionParser.class);
519             }
520 
521             @Provides
522             @Priority(10)
523             static LocalRepositoryManager newLocalRepositoryManager(Session session) {
524                 return session.getService(LocalRepositoryManager.class);
525             }
526 
527             @Provides
528             @Priority(10)
529             static ArtifactInstaller newArtifactInstaller(Session session) {
530                 return session.getService(ArtifactInstaller.class);
531             }
532 
533             @Provides
534             @Priority(10)
535             static ArtifactDeployer newArtifactDeployer(Session session) {
536                 return session.getService(ArtifactDeployer.class);
537             }
538 
539             @Provides
540             @Priority(10)
541             static ArtifactManager newArtifactManager(Session session) {
542                 return session.getService(ArtifactManager.class);
543             }
544 
545             @Provides
546             @Priority(10)
547             static ProjectManager newProjectManager(Session session) {
548                 return session.getService(ProjectManager.class);
549             }
550 
551             @Provides
552             @Priority(10)
553             static ArtifactFactory newArtifactFactory(Session session) {
554                 return session.getService(ArtifactFactory.class);
555             }
556 
557             @Provides
558             @Priority(10)
559             static ProjectBuilder newProjectBuilder(Session session) {
560                 return session.getService(ProjectBuilder.class);
561             }
562 
563             @Provides
564             @Priority(10)
565             static ModelXmlFactory newModelXmlFactory(Session session) {
566                 return session.getService(ModelXmlFactory.class);
567             }
568         }
569 
570         getInjector().bindInstance(Foo.class, new Foo());
571 
572         getInjector().injectInstance(context.getRequiredTestInstance());
573 
574         //        SessionScope sessionScope = getInjector().getInstance(SessionScope.class);
575         //        sessionScope.enter();
576         //        sessionScope.seed(Session.class, s);
577         //        sessionScope.seed(InternalSession.class, s);
578 
579         //        MojoExecutionScope mojoExecutionScope = getInjector().getInstance(MojoExecutionScope.class);
580         //        mojoExecutionScope.enter();
581         //        mojoExecutionScope.seed(Project.class, p);
582         //        mojoExecutionScope.seed(MojoExecution.class, me);
583     }
584 
585     private Reader openPomUrl(Class<?> holder, String pom, Path[] modelPath) throws IOException {
586         if (pom.startsWith("file:")) {
587             Path path = Paths.get(getBasedir()).resolve(pom.substring("file:".length()));
588             modelPath[0] = path;
589             return Files.newBufferedReader(path);
590         } else if (pom.startsWith("classpath:")) {
591             URL url = holder.getResource(pom.substring("classpath:".length()));
592             if (url == null) {
593                 throw new IllegalStateException("Unable to find pom on classpath: " + pom);
594             }
595             return new XmlStreamReader(url.openStream());
596         } else if (pom.contains("<project>")) {
597             return new StringReader(pom);
598         } else {
599             Path path = Paths.get(getBasedir()).resolve(pom);
600             modelPath[0] = path;
601             return Files.newBufferedReader(path);
602         }
603     }
604 
605     protected String getPluginDescriptorLocation() {
606         return "META-INF/maven/plugin.xml";
607     }
608 
609     protected String[] mojoCoordinates(String goal) throws Exception {
610         if (goal.matches(".*:.*:.*:.*")) {
611             return goal.split(":");
612         } else {
613             Path pluginPom = Paths.get(getPluginBasedir(), "pom.xml");
614             Xpp3Dom pluginPomDom = Xpp3DomBuilder.build(Files.newBufferedReader(pluginPom));
615             String artifactId = pluginPomDom.getChild("artifactId").getValue();
616             String groupId = resolveFromRootThenParent(pluginPomDom, "groupId");
617             String version = resolveFromRootThenParent(pluginPomDom, "version");
618             return new String[] {groupId, artifactId, version, goal};
619         }
620     }
621 
622     private XmlNode finalizeConfig(XmlNode config, MojoDescriptor mojoDescriptor) {
623         List<XmlNode> children = new ArrayList<>();
624         if (mojoDescriptor != null) {
625             XmlNode defaultConfiguration;
626             defaultConfiguration = MojoDescriptorCreator.convert(mojoDescriptor);
627             for (Parameter parameter : mojoDescriptor.getParameters()) {
628                 XmlNode parameterConfiguration = config.child(parameter.getName());
629                 if (parameterConfiguration == null) {
630                     parameterConfiguration = config.child(parameter.getAlias());
631                 }
632                 XmlNode parameterDefaults = defaultConfiguration.child(parameter.getName());
633                 parameterConfiguration = XmlNode.merge(parameterConfiguration, parameterDefaults, Boolean.TRUE);
634                 if (parameterConfiguration != null) {
635                     Map<String, String> attributes = new HashMap<>(parameterConfiguration.attributes());
636                     // if (isEmpty(parameterConfiguration.getAttribute("implementation"))
637                     //         && !isEmpty(parameter.getImplementation())) {
638                     //     attributes.put("implementation", parameter.getImplementation());
639                     // }
640                     parameterConfiguration = XmlNode.newInstance(
641                             parameter.getName(),
642                             parameterConfiguration.value(),
643                             attributes,
644                             parameterConfiguration.children(),
645                             parameterConfiguration.inputLocation());
646 
647                     children.add(parameterConfiguration);
648                 }
649             }
650         }
651         return XmlNode.newInstance("configuration", null, null, children, null);
652     }
653 
654     private static Optional<Xpp3Dom> child(Xpp3Dom element, String name) {
655         return Optional.ofNullable(element.getChild(name));
656     }
657 
658     private static Stream<Xpp3Dom> children(Xpp3Dom element) {
659         return Stream.of(element.getChildren());
660     }
661 
662     public static XmlNode extractPluginConfiguration(String artifactId, Xpp3Dom pomDom) throws Exception {
663         Xpp3Dom pluginConfigurationElement = child(pomDom, "build")
664                 .flatMap(buildElement -> child(buildElement, "plugins"))
665                 .map(MojoExtension::children)
666                 .orElseGet(Stream::empty)
667                 .filter(e -> e.getChild("artifactId").getValue().equals(artifactId))
668                 .findFirst()
669                 .flatMap(buildElement -> child(buildElement, "configuration"))
670                 .orElse(Xpp3DomBuilder.build(new StringReader("<configuration/>")));
671         return pluginConfigurationElement.getDom();
672     }
673 
674     /**
675      * sometimes the parent element might contain the correct value so generalize that access
676      *
677      * TODO find out where this is probably done elsewhere
678      */
679     private static String resolveFromRootThenParent(Xpp3Dom pluginPomDom, String element) throws Exception {
680         return Optional.ofNullable(child(pluginPomDom, element).orElseGet(() -> child(pluginPomDom, "parent")
681                         .flatMap(e -> child(e, element))
682                         .orElse(null)))
683                 .map(Xpp3Dom::getValue)
684                 .orElseThrow(() -> new Exception("unable to determine " + element));
685     }
686 
687     /**
688      * Convenience method to obtain the value of a variable on a mojo that might not have a getter.
689      * <br>
690      * NOTE: the caller is responsible for casting to what the desired type is.
691      */
692     public static Object getVariableValueFromObject(Object object, String variable) throws IllegalAccessException {
693         Field field = ReflectionUtils.getFieldByNameIncludingSuperclasses(variable, object.getClass());
694         field.setAccessible(true);
695         return field.get(object);
696     }
697 
698     /**
699      * Convenience method to obtain all variables and values from the mojo (including its superclasses)
700      * <br>
701      * Note: the values in the map are of type Object so the caller is responsible for casting to desired types.
702      */
703     public static Map<String, Object> getVariablesAndValuesFromObject(Object object) throws IllegalAccessException {
704         return getVariablesAndValuesFromObject(object.getClass(), object);
705     }
706 
707     /**
708      * Convenience method to obtain all variables and values from the mojo (including its superclasses)
709      * <br>
710      * Note: the values in the map are of type Object so the caller is responsible for casting to desired types.
711      *
712      * @return map of variable names and values
713      */
714     public static Map<String, Object> getVariablesAndValuesFromObject(Class<?> clazz, Object object)
715             throws IllegalAccessException {
716         Map<String, Object> map = new HashMap<>();
717         Field[] fields = clazz.getDeclaredFields();
718         AccessibleObject.setAccessible(fields, true);
719         for (Field field : fields) {
720             map.put(field.getName(), field.get(object));
721         }
722         Class<?> superclass = clazz.getSuperclass();
723         if (!Object.class.equals(superclass)) {
724             map.putAll(getVariablesAndValuesFromObject(superclass, object));
725         }
726         return map;
727     }
728 
729     /**
730      * Convenience method to set values to variables in objects that don't have setters
731      */
732     public static void setVariableValueToObject(Object object, String variable, Object value)
733             throws IllegalAccessException {
734         Field field = ReflectionUtils.getFieldByNameIncludingSuperclasses(variable, object.getClass());
735         requireNonNull(field, "Field " + variable + " not found");
736         field.setAccessible(true);
737         field.set(object, value);
738     }
739 
740     static class WrapEvaluator implements TypeAwareExpressionEvaluator {
741 
742         private final Injector injector;
743         private final TypeAwareExpressionEvaluator evaluator;
744 
745         WrapEvaluator(Injector injector, TypeAwareExpressionEvaluator evaluator) {
746             this.injector = injector;
747             this.evaluator = evaluator;
748         }
749 
750         @Override
751         public Object evaluate(String expression) throws ExpressionEvaluationException {
752             return evaluate(expression, null);
753         }
754 
755         @Override
756         public Object evaluate(String expression, Class<?> type) throws ExpressionEvaluationException {
757             Object value = evaluator.evaluate(expression, type);
758             if (value == null) {
759                 String expr = stripTokens(expression);
760                 if (expr != null) {
761                     try {
762                         value = injector.getInstance(Key.of(type, expr));
763                     } catch (DIException e) {
764                         // nothing
765                     }
766                 }
767             }
768             return value;
769         }
770 
771         private String stripTokens(String expr) {
772             if (expr.startsWith("${") && expr.endsWith("}")) {
773                 return expr.substring(2, expr.length() - 1);
774             }
775             return null;
776         }
777 
778         @Override
779         public File alignToBaseDirectory(File path) {
780             return evaluator.alignToBaseDirectory(path);
781         }
782     }
783 
784     /*
785     private Scope getScopeInstanceOrNull(final Injector injector, final Binding<?> binding) {
786         return binding.acceptScopingVisitor(new DefaultBindingScopingVisitor<Scope>() {
787 
788             @Override
789             public Scope visitScopeAnnotation(Class<? extends Annotation> scopeAnnotation) {
790                 throw new RuntimeException(String.format(
791                         "I don't know how to handle the scopeAnnotation: %s", scopeAnnotation.getCanonicalName()));
792             }
793 
794             @Override
795             public Scope visitNoScoping() {
796                 if (binding instanceof LinkedKeyBinding) {
797                     Binding<?> childBinding = injector.getBinding(((LinkedKeyBinding) binding).getLinkedKey());
798                     return getScopeInstanceOrNull(injector, childBinding);
799                 }
800                 return null;
801             }
802 
803             @Override
804             public Scope visitEagerSingleton() {
805                 return Scopes.SINGLETON;
806             }
807 
808             public Scope visitScope(Scope scope) {
809                 return scope;
810             }
811         });
812     }*/
813 
814 }