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             injector.dispose();
128             injector = null;
129         }
130     }
131 
132     /**
133      * Returns the DI injector, creating it if necessary.
134      *
135      * @return The configured injector instance
136      */
137     public Injector getInjector() {
138         if (injector == null) {
139             setupContainer();
140         }
141         return injector;
142     }
143 
144     /**
145      * Looks up a component of the specified type from the container.
146      *
147      * @param <T> The component type
148      * @param componentClass The class of the component to look up
149      * @return The component instance
150      * @throws DIException if lookup fails
151      */
152     protected <T> T lookup(Class<T> componentClass) throws DIException {
153         return getInjector().getInstance(componentClass);
154     }
155 
156     /**
157      * Looks up a component of the specified type and role hint from the container.
158      *
159      * @param <T> The component type
160      * @param componentClass The class of the component to look up
161      * @param roleHint The role hint for the component
162      * @return The component instance
163      * @throws DIException if lookup fails
164      */
165     protected <T> T lookup(Class<T> componentClass, String roleHint) throws DIException {
166         return getInjector().getInstance(Key.ofType(componentClass, roleHint));
167     }
168 
169     /**
170      * Looks up a component of the specified type and qualifier from the container.
171      *
172      * @param <T> The component type
173      * @param componentClass The class of the component to look up
174      * @param qualifier The qualifier for the component
175      * @return The component instance
176      * @throws DIException if lookup fails
177      */
178     protected <T> T lookup(Class<T> componentClass, Object qualifier) throws DIException {
179         return getInjector().getInstance(Key.ofType(componentClass, qualifier));
180     }
181 
182     /**
183      * Releases a component back to the container.
184      * Currently a placeholder for future implementation.
185      *
186      * @param component The component to release
187      * @throws DIException if release fails
188      */
189     protected void release(Object component) throws DIException {
190         // TODO: implement
191         // getInjector().release(component);
192     }
193 
194     /**
195      * Creates a File object for a path relative to the base directory.
196      *
197      * @param path The relative path
198      * @return A File object representing the path
199      */
200     public static File getTestFile(String path) {
201         return new File(getBasedir(), path);
202     }
203 
204     /**
205      * Creates a File object for a path relative to a specified base directory.
206      *
207      * @param basedir The base directory path
208      * @param path The relative path
209      * @return A File object representing the path
210      */
211     public static File getTestFile(String basedir, String path) {
212         File basedirFile = new File(basedir);
213 
214         if (!basedirFile.isAbsolute()) {
215             basedirFile = getTestFile(basedir);
216         }
217 
218         return new File(basedirFile, path);
219     }
220 
221     /**
222      * Returns the absolute path for a path relative to the base directory.
223      *
224      * @param path The relative path
225      * @return The absolute path
226      */
227     public static String getTestPath(String path) {
228         return getTestFile(path).getAbsolutePath();
229     }
230 
231     /**
232      * Returns the absolute path for a path relative to a specified base directory.
233      *
234      * @param basedir The base directory path
235      * @param path The relative path
236      * @return The absolute path
237      */
238     public static String getTestPath(String basedir, String path) {
239         return getTestFile(basedir, path).getAbsolutePath();
240     }
241 
242     /**
243      * Returns the base directory for test execution.
244      * Uses the "basedir" system property if set, otherwise uses the current directory.
245      *
246      * @return The base directory path
247      */
248     public static String getBasedir() {
249         if (basedir != null) {
250             return basedir;
251         }
252 
253         basedir = System.getProperty("basedir");
254 
255         if (basedir == null) {
256             basedir = new File("").getAbsolutePath();
257         }
258 
259         return basedir;
260     }
261 }