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