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 }