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.di.testing;
20  
21  import java.io.File;
22  
23  import org.apache.maven.di.Injector;
24  import org.apache.maven.di.Key;
25  import org.apache.maven.di.impl.DIException;
26  import org.junit.jupiter.api.extension.AfterEachCallback;
27  import org.junit.jupiter.api.extension.BeforeEachCallback;
28  import org.junit.jupiter.api.extension.ExtensionContext;
29  
30  /**
31   * JUnit Jupiter extension that provides dependency injection support for Maven tests.
32   * This extension manages the lifecycle of a DI container for each test method execution,
33   * automatically performing injection into test instances and cleanup.
34   *
35   * <p>This is a modernized version of the original Plexus test support, adapted for
36   * Maven's new DI framework and JUnit Jupiter.</p>
37   *
38   * <p>Usage example:</p>
39   * <pre>
40   * {@code
41   * @ExtendWith(MavenDIExtension.class)
42   * class MyTest {
43   *     @Inject
44   *     private MyComponent component;
45   *
46   *     @Test
47   *     void testSomething() {
48   *         // component is automatically injected
49   *     }
50   * }
51   * }
52   * </pre>
53   */
54  public class MavenDIExtension implements BeforeEachCallback, AfterEachCallback {
55      protected static ExtensionContext context;
56      protected Injector injector;
57      protected static String basedir;
58  
59      /**
60       * Initializes the test environment before each test method execution.
61       * Sets up the base directory and DI container, then performs injection into the test instance.
62       *
63       * @param context The extension context provided by JUnit
64       * @throws Exception if initialization fails
65       */
66      @Override
67      public void beforeEach(ExtensionContext context) throws Exception {
68          basedir = getBasedir();
69          setContext(context);
70          getInjector().bindInstance((Class<Object>) context.getRequiredTestClass(), context.getRequiredTestInstance());
71          getInjector().injectInstance(context.getRequiredTestInstance());
72      }
73  
74      /**
75       * Stores the extension context for use during test execution.
76       *
77       * @param context The extension context to store
78       */
79      protected void setContext(ExtensionContext context) {
80          MavenDIExtension.context = context;
81      }
82  
83      /**
84       * Creates and configures the DI container for test execution.
85       * Performs component discovery and sets up basic bindings.
86       *
87       * @throws IllegalStateException if the ExtensionContext is null, the required test class is unavailable,
88       *         the required test instance is unavailable, or if container setup fails
89       */
90      protected void setupContainer() {
91          if (context == null) {
92              throw new IllegalStateException("ExtensionContext must not be null");
93          }
94          final Class<?> testClass = context.getRequiredTestClass();
95          if (testClass == null) {
96              throw new IllegalStateException("Required test class is not available in ExtensionContext");
97          }
98          final Object testInstance = context.getRequiredTestInstance();
99          if (testInstance == null) {
100             throw new IllegalStateException("Required test instance is not available in ExtensionContext");
101         }
102 
103         try {
104             injector = Injector.create();
105             injector.bindInstance(ExtensionContext.class, context);
106             injector.discover(testClass.getClassLoader());
107             injector.bindInstance(Injector.class, injector);
108             injector.bindInstance(testClass.asSubclass(Object.class), (Object) testInstance); // Safe generics handling
109         } catch (final Exception e) {
110             throw new IllegalStateException(
111                     String.format(
112                             "Failed to set up DI injector for test class '%s': %s",
113                             testClass.getName(), e.getMessage()),
114                     e);
115         }
116     }
117 
118     /**
119      * Cleans up resources after each test method execution.
120      * Currently a placeholder for future cleanup implementation.
121      *
122      * @param context The extension context provided by JUnit
123      */
124     @Override
125     public void afterEach(ExtensionContext context) throws Exception {
126         if (injector != null) {
127             // TODO: implement
128             // injector.dispose();
129             injector = null;
130         }
131     }
132 
133     /**
134      * Returns the DI injector, creating it if necessary.
135      *
136      * @return The configured injector instance
137      */
138     public Injector getInjector() {
139         if (injector == null) {
140             setupContainer();
141         }
142         return injector;
143     }
144 
145     /**
146      * Looks up a component of the specified type from the container.
147      *
148      * @param <T> The component type
149      * @param componentClass The class of the component to look up
150      * @return The component instance
151      * @throws DIException if lookup fails
152      */
153     protected <T> T lookup(Class<T> componentClass) throws DIException {
154         return getInjector().getInstance(componentClass);
155     }
156 
157     /**
158      * Looks up a component of the specified type and role hint from the container.
159      *
160      * @param <T> The component type
161      * @param componentClass The class of the component to look up
162      * @param roleHint The role hint for the component
163      * @return The component instance
164      * @throws DIException if lookup fails
165      */
166     protected <T> T lookup(Class<T> componentClass, String roleHint) throws DIException {
167         return getInjector().getInstance(Key.ofType(componentClass, roleHint));
168     }
169 
170     /**
171      * Looks up a component of the specified type and qualifier from the container.
172      *
173      * @param <T> The component type
174      * @param componentClass The class of the component to look up
175      * @param qualifier The qualifier for the component
176      * @return The component instance
177      * @throws DIException if lookup fails
178      */
179     protected <T> T lookup(Class<T> componentClass, Object qualifier) throws DIException {
180         return getInjector().getInstance(Key.ofType(componentClass, qualifier));
181     }
182 
183     /**
184      * Releases a component back to the container.
185      * Currently a placeholder for future implementation.
186      *
187      * @param component The component to release
188      * @throws DIException if release fails
189      */
190     protected void release(Object component) throws DIException {
191         // TODO: implement
192         // getInjector().release(component);
193     }
194 
195     /**
196      * Creates a File object for a path relative to the base directory.
197      *
198      * @param path The relative path
199      * @return A File object representing the path
200      */
201     public static File getTestFile(String path) {
202         return new File(getBasedir(), path);
203     }
204 
205     /**
206      * Creates a File object for a path relative to a specified base directory.
207      *
208      * @param basedir The base directory path
209      * @param path The relative path
210      * @return A File object representing the path
211      */
212     public static File getTestFile(String basedir, String path) {
213         File basedirFile = new File(basedir);
214 
215         if (!basedirFile.isAbsolute()) {
216             basedirFile = getTestFile(basedir);
217         }
218 
219         return new File(basedirFile, path);
220     }
221 
222     /**
223      * Returns the absolute path for a path relative to the base directory.
224      *
225      * @param path The relative path
226      * @return The absolute path
227      */
228     public static String getTestPath(String path) {
229         return getTestFile(path).getAbsolutePath();
230     }
231 
232     /**
233      * Returns the absolute path for a path relative to a specified base directory.
234      *
235      * @param basedir The base directory path
236      * @param path The relative path
237      * @return The absolute path
238      */
239     public static String getTestPath(String basedir, String path) {
240         return getTestFile(basedir, path).getAbsolutePath();
241     }
242 
243     /**
244      * Returns the base directory for test execution.
245      * Uses the "basedir" system property if set, otherwise uses the current directory.
246      *
247      * @return The base directory path
248      */
249     public static String getBasedir() {
250         if (basedir != null) {
251             return basedir;
252         }
253 
254         basedir = System.getProperty("basedir");
255 
256         if (basedir == null) {
257             basedir = new File("").getAbsolutePath();
258         }
259 
260         return basedir;
261     }
262 }