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 }