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 IllegalArgumentException if container setup fails
88       */
89      @SuppressWarnings("unchecked")
90      protected void setupContainer() {
91          try {
92              injector = Injector.create();
93              injector.bindInstance(ExtensionContext.class, context);
94              injector.discover(context.getRequiredTestClass().getClassLoader());
95              injector.bindInstance(Injector.class, injector);
96              injector.bindInstance((Class) context.getRequiredTestClass(), context.getRequiredTestInstance());
97          } catch (Exception e) {
98              throw new IllegalArgumentException("Failed to create DI injector.", e);
99          }
100     }
101 
102     /**
103      * Cleans up resources after each test method execution.
104      * Currently a placeholder for future cleanup implementation.
105      *
106      * @param context The extension context provided by JUnit
107      */
108     @Override
109     public void afterEach(ExtensionContext context) throws Exception {
110         if (injector != null) {
111             // TODO: implement
112             // injector.dispose();
113             injector = null;
114         }
115     }
116 
117     /**
118      * Returns the DI injector, creating it if necessary.
119      *
120      * @return The configured injector instance
121      */
122     public Injector getInjector() {
123         if (injector == null) {
124             setupContainer();
125         }
126         return injector;
127     }
128 
129     /**
130      * Looks up a component of the specified type from the container.
131      *
132      * @param <T> The component type
133      * @param componentClass The class of the component to look up
134      * @return The component instance
135      * @throws DIException if lookup fails
136      */
137     protected <T> T lookup(Class<T> componentClass) throws DIException {
138         return getInjector().getInstance(componentClass);
139     }
140 
141     /**
142      * Looks up a component of the specified type and role hint from the container.
143      *
144      * @param <T> The component type
145      * @param componentClass The class of the component to look up
146      * @param roleHint The role hint for the component
147      * @return The component instance
148      * @throws DIException if lookup fails
149      */
150     protected <T> T lookup(Class<T> componentClass, String roleHint) throws DIException {
151         return getInjector().getInstance(Key.ofType(componentClass, roleHint));
152     }
153 
154     /**
155      * Looks up a component of the specified type and qualifier from the container.
156      *
157      * @param <T> The component type
158      * @param componentClass The class of the component to look up
159      * @param qualifier The qualifier for the component
160      * @return The component instance
161      * @throws DIException if lookup fails
162      */
163     protected <T> T lookup(Class<T> componentClass, Object qualifier) throws DIException {
164         return getInjector().getInstance(Key.ofType(componentClass, qualifier));
165     }
166 
167     /**
168      * Releases a component back to the container.
169      * Currently a placeholder for future implementation.
170      *
171      * @param component The component to release
172      * @throws DIException if release fails
173      */
174     protected void release(Object component) throws DIException {
175         // TODO: implement
176         // getInjector().release(component);
177     }
178 
179     /**
180      * Creates a File object for a path relative to the base directory.
181      *
182      * @param path The relative path
183      * @return A File object representing the path
184      */
185     public static File getTestFile(String path) {
186         return new File(getBasedir(), path);
187     }
188 
189     /**
190      * Creates a File object for a path relative to a specified base directory.
191      *
192      * @param basedir The base directory path
193      * @param path The relative path
194      * @return A File object representing the path
195      */
196     public static File getTestFile(String basedir, String path) {
197         File basedirFile = new File(basedir);
198 
199         if (!basedirFile.isAbsolute()) {
200             basedirFile = getTestFile(basedir);
201         }
202 
203         return new File(basedirFile, path);
204     }
205 
206     /**
207      * Returns the absolute path for a path relative to the base directory.
208      *
209      * @param path The relative path
210      * @return The absolute path
211      */
212     public static String getTestPath(String path) {
213         return getTestFile(path).getAbsolutePath();
214     }
215 
216     /**
217      * Returns the absolute path for a path relative to a specified base directory.
218      *
219      * @param basedir The base directory path
220      * @param path The relative path
221      * @return The absolute path
222      */
223     public static String getTestPath(String basedir, String path) {
224         return getTestFile(basedir, path).getAbsolutePath();
225     }
226 
227     /**
228      * Returns the base directory for test execution.
229      * Uses the "basedir" system property if set, otherwise uses the current directory.
230      *
231      * @return The base directory path
232      */
233     public static String getBasedir() {
234         if (basedir != null) {
235             return basedir;
236         }
237 
238         basedir = System.getProperty("basedir");
239 
240         if (basedir == null) {
241             basedir = new File("").getAbsolutePath();
242         }
243 
244         return basedir;
245     }
246 }