diff --git a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultArtifactManager.java b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultArtifactManager.java index 0f2022206015..2b8692a86cf8 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultArtifactManager.java +++ b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultArtifactManager.java @@ -30,17 +30,24 @@ import org.apache.maven.api.Artifact; import org.apache.maven.api.ProducedArtifact; +import org.apache.maven.api.Service; import org.apache.maven.api.annotations.Nonnull; import org.apache.maven.api.di.SessionScoped; import org.apache.maven.api.services.ArtifactManager; import org.apache.maven.impl.DefaultArtifact; +import org.apache.maven.impl.InternalSession; import org.apache.maven.project.MavenProject; import org.eclipse.sisu.Typed; import static java.util.Objects.requireNonNull; +/** + * This implementation of {@code ArtifactManager} is explicitly bound to + * both {@code ArtifactManager} and {@code Service} interfaces so that it can be retrieved using + * {@link InternalSession#getAllServices()}. + */ @Named -@Typed +@Typed({ArtifactManager.class, Service.class}) @SessionScoped public class DefaultArtifactManager implements ArtifactManager { diff --git a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultProjectManager.java b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultProjectManager.java index c7d750572491..7c495a7344bc 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultProjectManager.java +++ b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultProjectManager.java @@ -38,6 +38,7 @@ import org.apache.maven.api.Project; import org.apache.maven.api.ProjectScope; import org.apache.maven.api.RemoteRepository; +import org.apache.maven.api.Service; import org.apache.maven.api.SourceRoot; import org.apache.maven.api.annotations.Nonnull; import org.apache.maven.api.di.SessionScoped; @@ -52,8 +53,13 @@ import static java.util.Objects.requireNonNull; import static org.apache.maven.internal.impl.CoreUtils.map; +/** + * This implementation of {@code ProjectManager} is explicitly bound to + * both {@code ProjectManager} and {@code Service} interfaces so that it can be retrieved using + * {@link InternalSession#getAllServices()}. + */ @Named -@Typed +@Typed({ProjectManager.class, Service.class}) @SessionScoped public class DefaultProjectManager implements ProjectManager { diff --git a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/SisuDiBridgeModule.java b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/SisuDiBridgeModule.java index c1e020b3ed0b..eeb6215f9357 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/SisuDiBridgeModule.java +++ b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/SisuDiBridgeModule.java @@ -25,6 +25,7 @@ import java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -127,7 +128,7 @@ private static com.google.inject.Key toGuiceKey(Key key) { } else if (key.getQualifier() instanceof Annotation a) { return (com.google.inject.Key) com.google.inject.Key.get(key.getType(), a); } else { - return (com.google.inject.Key) com.google.inject.Key.get(key.getType()); + return (com.google.inject.Key) com.google.inject.Key.get(key.getType(), Named.class); } } @@ -203,6 +204,22 @@ private Supplier getBeanSupplier(Dependency dep, Key key) { } } + @Override + public Set> getAllBindings(Class clazz) { + Key key = Key.of(clazz); + Set> bindings = new HashSet<>(); + Set> diBindings = super.getBindings(key); + if (diBindings != null) { + bindings.addAll(diBindings); + } + for (var bean : locator.get().locate(toGuiceKey(key))) { + if (isPlexusBean(bean)) { + bindings.add(new BindingToBeanEntry<>(Key.of(bean.getImplementationClass())).toBeanEntry(bean)); + } + } + return bindings; + } + private Supplier getListSupplier(Key key) { Key elementType = key.getTypeParameter(0); return () -> { diff --git a/impl/maven-core/src/main/java/org/apache/maven/plugin/DefaultBuildPluginManager.java b/impl/maven-core/src/main/java/org/apache/maven/plugin/DefaultBuildPluginManager.java index 6d9c76100d8e..e395d1ed000b 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/plugin/DefaultBuildPluginManager.java +++ b/impl/maven-core/src/main/java/org/apache/maven/plugin/DefaultBuildPluginManager.java @@ -25,8 +25,11 @@ import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.util.List; +import java.util.Map; +import java.util.function.Supplier; import org.apache.maven.api.Project; +import org.apache.maven.api.Service; import org.apache.maven.api.services.MavenException; import org.apache.maven.execution.MavenSession; import org.apache.maven.execution.MojoExecutionEvent; @@ -128,6 +131,10 @@ public void executeMojo(MavenSession session, MojoExecution mojoExecution) scope.seed(org.apache.maven.api.MojoExecution.class, new DefaultMojoExecution(sessionV4, mojoExecution)); if (mojoDescriptor.isV4Api()) { + // For Maven 4 plugins, register a service so that they can be directly injected into plugins + Map, Supplier> services = sessionV4.getAllServices(); + services.forEach((itf, svc) -> scope.seed((Class) itf, (Supplier) svc)); + org.apache.maven.api.plugin.Mojo mojoV4 = mavenPluginManager.getConfiguredMojo( org.apache.maven.api.plugin.Mojo.class, session, mojoExecution); mojo = new MojoWrapper(mojoV4); diff --git a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java index 7d55730f5d96..93c0d5a1237e 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java +++ b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java @@ -37,6 +37,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.function.Supplier; import java.util.jar.JarFile; import java.util.stream.Collectors; import java.util.zip.ZipEntry; @@ -47,6 +48,7 @@ import org.apache.maven.api.PathScope; import org.apache.maven.api.PathType; import org.apache.maven.api.Project; +import org.apache.maven.api.Service; import org.apache.maven.api.Session; import org.apache.maven.api.plugin.descriptor.Resolution; import org.apache.maven.api.services.DependencyResolver; @@ -565,6 +567,10 @@ private T loadV4Mojo( injector.bindInstance(Project.class, project); injector.bindInstance(org.apache.maven.api.MojoExecution.class, execution); injector.bindInstance(org.apache.maven.api.plugin.Log.class, log); + + Map, Supplier> services = sessionV4.getAllServices(); + services.forEach((itf, svc) -> injector.bindSupplier((Class) itf, (Supplier) svc)); + mojo = mojoInterface.cast(injector.getInstance( Key.of(mojoDescriptor.getImplementationClass(), mojoDescriptor.getRoleHint()))); diff --git a/impl/maven-di/src/main/java/org/apache/maven/di/Injector.java b/impl/maven-di/src/main/java/org/apache/maven/di/Injector.java index 8a95f0fd6f03..e908b8e2fe1f 100644 --- a/impl/maven-di/src/main/java/org/apache/maven/di/Injector.java +++ b/impl/maven-di/src/main/java/org/apache/maven/di/Injector.java @@ -123,6 +123,21 @@ static Injector create() { @Nonnull Injector bindInstance(@Nonnull Class cls, @Nonnull T instance); + /** + * Binds a specific instance supplier to a class type. + *

+ * This method allows pre-created instances to be used for injection instead of + * having the injector create new instances. + * + * @param the type of the instance + * @param cls the class to bind to + * @param supplier the supplier to use for injection + * @return this injector instance for method chaining + * @throws NullPointerException if either parameter is null + */ + @Nonnull + Injector bindSupplier(@Nonnull Class cls, @Nonnull Supplier supplier); + /** * Performs field and method injection on an existing instance. *

diff --git a/impl/maven-di/src/main/java/org/apache/maven/di/impl/Binding.java b/impl/maven-di/src/main/java/org/apache/maven/di/impl/Binding.java index a5da997d7554..4caa7311b82a 100644 --- a/impl/maven-di/src/main/java/org/apache/maven/di/impl/Binding.java +++ b/impl/maven-di/src/main/java/org/apache/maven/di/impl/Binding.java @@ -53,6 +53,10 @@ public static Binding toInstance(T instance) { return new BindingToInstance<>(instance); } + public static Binding toSupplier(Supplier supplier) { + return new BindingToSupplier<>(supplier); + } + public static Binding to(Key originalKey, TupleConstructorN constructor, Class[] types) { return Binding.to( originalKey, @@ -168,6 +172,25 @@ public String toString() { } } + public static class BindingToSupplier extends Binding { + final Supplier supplier; + + public BindingToSupplier(Supplier supplier) { + super(null, Collections.emptySet()); + this.supplier = supplier; + } + + @Override + public Supplier compile(Function, Supplier> compiler) { + return supplier; + } + + @Override + public String toString() { + return "BindingToSupplier[" + supplier + "]" + getDependencies(); + } + } + public static class BindingToConstructor extends Binding { final TupleConstructorN constructor; final Dependency[] args; diff --git a/impl/maven-di/src/main/java/org/apache/maven/di/impl/InjectorImpl.java b/impl/maven-di/src/main/java/org/apache/maven/di/impl/InjectorImpl.java index c3a069bca55e..f2963b911b95 100644 --- a/impl/maven-di/src/main/java/org/apache/maven/di/impl/InjectorImpl.java +++ b/impl/maven-di/src/main/java/org/apache/maven/di/impl/InjectorImpl.java @@ -137,6 +137,13 @@ public Injector bindInstance(@Nonnull Class clazz, @Nonnull U instance) { return doBind(key, binding); } + @Override + public Injector bindSupplier(@Nonnull Class clazz, @Nonnull Supplier supplier) { + Key key = Key.of(clazz, ReflectionUtils.qualifierOf(clazz)); + Binding binding = Binding.toSupplier(supplier); + return doBind(key, binding); + } + @Nonnull @Override public Injector bindImplicit(@Nonnull Class clazz) { @@ -195,6 +202,10 @@ public Map, Set>> getBindings() { return bindings; } + public Set> getAllBindings(Class clazz) { + return getBindings(Key.of(clazz)); + } + public Supplier getCompiledBinding(Dependency dep) { Key key = dep.key(); Supplier originalSupplier = doGetCompiledBinding(dep); diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/AbstractSession.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/AbstractSession.java index 605a36b903c0..68e2d6b2a7da 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/AbstractSession.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/AbstractSession.java @@ -23,17 +23,20 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.maven.api.Artifact; import org.apache.maven.api.ArtifactCoordinates; @@ -96,6 +99,10 @@ import org.apache.maven.api.services.VersionRangeResolver; import org.apache.maven.api.services.VersionResolver; import org.apache.maven.api.services.VersionResolverException; +import org.apache.maven.di.Injector; +import org.apache.maven.di.Key; +import org.apache.maven.di.impl.Binding; +import org.apache.maven.di.impl.InjectorImpl; import org.eclipse.aether.DefaultRepositorySystemSession; import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.RepositorySystemSession; @@ -112,6 +119,7 @@ public abstract class AbstractSession implements InternalSession { protected final RepositorySystem repositorySystem; protected final List repositories; protected final Lookup lookup; + protected final Injector injector; private final Map, Service> services = new ConcurrentHashMap<>(); private final List listeners = new CopyOnWriteArrayList<>(); private final Map allNodes = @@ -138,6 +146,24 @@ public AbstractSession( this.repositorySystem = repositorySystem; this.repositories = getRepositories(repositories, resolverRepositories); this.lookup = lookup; + this.injector = lookup != null ? lookup.lookupOptional(Injector.class).orElse(null) : null; + } + + @SuppressWarnings("unchecked") + private static Stream> collectServiceInterfaces(Class clazz) { + if (clazz == null) { + return Stream.empty(); + } else if (clazz.isInterface()) { + return Stream.concat( + Service.class.isAssignableFrom(clazz) ? Stream.of((Class) clazz) : Stream.empty(), + Stream.of(clazz.getInterfaces()).flatMap(AbstractSession::collectServiceInterfaces)) + .filter(itf -> itf != Service.class); + } else { + return Stream.concat( + Stream.of(clazz.getInterfaces()).flatMap(AbstractSession::collectServiceInterfaces), + collectServiceInterfaces(clazz.getSuperclass())) + .filter(itf -> itf != Service.class); + } } @Override @@ -370,6 +396,27 @@ public T getService(Class clazz) throws NoSuchElementExce return t; } + @Override + public Map, Supplier> getAllServices() { + Map, Supplier> allServices = new HashMap<>(services.size()); + // In case the injector is known, lazily populate the map to avoid creating all services upfront. + if (injector instanceof InjectorImpl injector) { + Set> bindings = injector.getAllBindings(Service.class); + bindings.stream() + .map(Binding::getOriginalKey) + .map(Key::getRawType) + .flatMap(AbstractSession::collectServiceInterfaces) + .distinct() + .forEach(itf -> allServices.put(itf, () -> injector.getInstance(Key.of(itf)))); + } else { + List services = this.injector.getInstance(new Key>() {}); + for (Service service : services) { + collectServiceInterfaces(service.getClass()).forEach(itf -> allServices.put(itf, () -> service)); + } + } + return allServices; + } + private Service lookup(Class c) { try { return lookup.lookup(c); diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/InternalSession.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/InternalSession.java index e39836e2552d..b3ce36d47b81 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/InternalSession.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/InternalSession.java @@ -20,7 +20,9 @@ import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.function.Function; +import java.util.function.Supplier; import org.apache.maven.api.Artifact; import org.apache.maven.api.ArtifactCoordinates; @@ -30,6 +32,7 @@ import org.apache.maven.api.Node; import org.apache.maven.api.RemoteRepository; import org.apache.maven.api.Repository; +import org.apache.maven.api.Service; import org.apache.maven.api.Session; import org.apache.maven.api.WorkspaceRepository; import org.apache.maven.api.annotations.Nonnull; @@ -138,4 +141,12 @@ List toDependencies( * @see RequestTraceHelper#enter(Session, Object) For the recommended way to manage traces */ RequestTrace getCurrentTrace(); + + /** + * Retrieves a map of all services. + * + * @see #getService(Class) + */ + @Nonnull + Map, Supplier> getAllServices(); } diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/di/SessionScope.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/di/SessionScope.java index b47c5acde830..cb8e818ea0ec 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/di/SessionScope.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/di/SessionScope.java @@ -119,32 +119,32 @@ protected Object dispatch(Key key, Supplier unscoped, Method method, O protected Class[] getInterfaces(Class superType) { if (superType.isInterface()) { return new Class[] {superType}; - } else { - for (Annotation a : superType.getAnnotations()) { - Class annotationType = a.annotationType(); - if (isTypeAnnotation(annotationType)) { - try { - Class[] value = - (Class[]) annotationType.getMethod("value").invoke(a); - if (value.length == 0) { - value = superType.getInterfaces(); - } - List> nonInterfaces = - Stream.of(value).filter(c -> !c.isInterface()).toList(); - if (!nonInterfaces.isEmpty()) { - throw new IllegalArgumentException( - "The Typed annotation must contain only interfaces but the following types are not: " - + nonInterfaces); - } - return value; - } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { - throw new IllegalStateException(e); + } + for (Annotation a : superType.getAnnotations()) { + Class annotationType = a.annotationType(); + if (isTypeAnnotation(annotationType)) { + try { + Class[] value = + (Class[]) annotationType.getMethod("value").invoke(a); + if (value.length == 0) { + // Only direct interfaces implemented by the class + value = superType.getInterfaces(); + } + List> nonInterfaces = + Stream.of(value).filter(c -> !c.isInterface()).toList(); + if (!nonInterfaces.isEmpty()) { + throw new IllegalArgumentException( + "The Typed annotation must contain only interfaces but the following types are not: " + + nonInterfaces); } + return value; + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException(e); } } - throw new IllegalArgumentException("The use of session scoped proxies require " - + "a org.eclipse.sisu.Typed or javax.enterprise.inject.Typed annotation"); } + throw new IllegalArgumentException( + "The use of session scoped proxies require a org.apache.maven.api.di.Typed, org.eclipse.sisu.Typed or javax.enterprise.inject.Typed annotation"); } protected boolean isTypeAnnotation(Class annotationType) { diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/di/SessionScopeTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/di/SessionScopeTest.java new file mode 100644 index 000000000000..043770e39d54 --- /dev/null +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/di/SessionScopeTest.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.impl.di; + +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.maven.api.di.Typed; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests for SessionScope#getInterfaces behaviour with @Typed and session-scoped proxies. + */ +class SessionScopeTest { + + interface A {} + + interface B extends A {} + + interface C {} + + static class Base implements B {} + + @Typed // no explicit interfaces: should collect all from hierarchy (B, A, C) + static class Impl extends Base implements C {} + + @Typed({C.class}) // explicit interface list + static class ImplExplicit extends Base implements C {} + + static class NoTyped extends Base implements C {} + + static class ExposedSessionScope extends SessionScope { + Class[] interfacesOf(Class type) { + return getInterfaces(type); + } + } + + @Test + void typedWithoutValuesIncludesOnlyDirectInterfaces() { + ExposedSessionScope scope = new ExposedSessionScope(); + Class[] itfs = scope.interfacesOf(Impl.class); + Set> set = Arrays.stream(itfs).collect(Collectors.toSet()); + assertTrue(set.contains(C.class), "Should include only direct interfaces implemented by the class"); + assertFalse(set.contains(B.class), "Should NOT include interfaces from superclass"); + assertFalse(set.contains(A.class), "Should NOT include super-interfaces"); + // Proxy should not include concrete classes + assertFalse(set.contains(Base.class)); + assertFalse(set.contains(Impl.class)); + } + + @Test + void typedWithExplicitValuesRespectsExplicitInterfacesOnly() { + ExposedSessionScope scope = new ExposedSessionScope(); + Class[] itfs = scope.interfacesOf(ImplExplicit.class); + assertArrayEquals(new Class[] {C.class}, itfs, "Only explicitly listed interfaces should be used"); + } + + @Test + void missingTypedAnnotationThrows() { + ExposedSessionScope scope = new ExposedSessionScope(); + IllegalArgumentException ex = + assertThrows(IllegalArgumentException.class, () -> scope.interfacesOf(NoTyped.class)); + assertTrue(ex.getMessage().contains("Typed")); + } +} diff --git a/impl/maven-testing/pom.xml b/impl/maven-testing/pom.xml index 5e8d31ad0286..6bef6bb4b004 100644 --- a/impl/maven-testing/pom.xml +++ b/impl/maven-testing/pom.xml @@ -76,21 +76,19 @@ under the License. org.slf4j slf4j-api + + com.google.inject + guice + classes + org.junit.jupiter junit-jupiter-api - true org.mockito mockito-core - - com.google.inject - guice - classes - test - diff --git a/impl/maven-testing/src/main/java/org/apache/maven/api/plugin/testing/MojoExtension.java b/impl/maven-testing/src/main/java/org/apache/maven/api/plugin/testing/MojoExtension.java index e06ea7c58e7d..1a9bfff9c59d 100644 --- a/impl/maven-testing/src/main/java/org/apache/maven/api/plugin/testing/MojoExtension.java +++ b/impl/maven-testing/src/main/java/org/apache/maven/api/plugin/testing/MojoExtension.java @@ -444,6 +444,19 @@ class Foo { @Singleton @Priority(-10) private InternalSession createSession() { + MojoTest mojoTest = context.getRequiredTestClass().getAnnotation(MojoTest.class); + if (mojoTest != null && mojoTest.realSession()) { + // Try to create a real session using ApiRunner without compile-time dependency + try { + Class apiRunner = Class.forName("org.apache.maven.impl.standalone.ApiRunner"); + Object session = apiRunner.getMethod("createSession").invoke(null); + return (InternalSession) session; + } catch (Throwable t) { + // Explicit request: do not fall back; abort the test with details instead of mocking + throw new org.opentest4j.TestAbortedException( + "@MojoTest(realSession=true) requested but could not create a real session.", t); + } + } return SessionMock.getMockSession(getBasedir()); } diff --git a/impl/maven-testing/src/main/java/org/apache/maven/api/plugin/testing/MojoTest.java b/impl/maven-testing/src/main/java/org/apache/maven/api/plugin/testing/MojoTest.java index 81ff117de6a8..16ced3b9e214 100644 --- a/impl/maven-testing/src/main/java/org/apache/maven/api/plugin/testing/MojoTest.java +++ b/impl/maven-testing/src/main/java/org/apache/maven/api/plugin/testing/MojoTest.java @@ -85,4 +85,10 @@ @Retention(RetentionPolicy.RUNTIME) @ExtendWith(MojoExtension.class) @Target(ElementType.TYPE) -public @interface MojoTest {} +public @interface MojoTest { + /** + * If true, the test harness will provide a real Maven Session created by ApiRunner.createSession(), + * instead of the default mock session. Default is false. + */ + boolean realSession() default false; +} diff --git a/impl/maven-testing/src/main/java/org/apache/maven/api/plugin/testing/SecDispatcherProvider.java b/impl/maven-testing/src/main/java/org/apache/maven/api/plugin/testing/SecDispatcherProvider.java new file mode 100644 index 000000000000..3f7c676f4dfe --- /dev/null +++ b/impl/maven-testing/src/main/java/org/apache/maven/api/plugin/testing/SecDispatcherProvider.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.api.plugin.testing; + +import java.util.Map; + +import org.apache.maven.api.di.Named; +import org.apache.maven.api.di.Provides; +import org.codehaus.plexus.components.secdispatcher.Cipher; +import org.codehaus.plexus.components.secdispatcher.Dispatcher; +import org.codehaus.plexus.components.secdispatcher.MasterSource; +import org.codehaus.plexus.components.secdispatcher.internal.cipher.AESGCMNoPadding; +import org.codehaus.plexus.components.secdispatcher.internal.dispatchers.LegacyDispatcher; +import org.codehaus.plexus.components.secdispatcher.internal.dispatchers.MasterDispatcher; +import org.codehaus.plexus.components.secdispatcher.internal.sources.EnvMasterSource; +import org.codehaus.plexus.components.secdispatcher.internal.sources.GpgAgentMasterSource; +import org.codehaus.plexus.components.secdispatcher.internal.sources.PinEntryMasterSource; +import org.codehaus.plexus.components.secdispatcher.internal.sources.SystemPropertyMasterSource; + +/** + * Delegate that offers just the minimal surface needed to decrypt settings. + */ +@SuppressWarnings("unused") +@Named +public class SecDispatcherProvider { + + @Provides + @Named(LegacyDispatcher.NAME) + public static Dispatcher legacyDispatcher() { + return new LegacyDispatcher(); + } + + @Provides + @Named(MasterDispatcher.NAME) + public static Dispatcher masterDispatcher( + Map masterCiphers, Map masterSources) { + return new MasterDispatcher(masterCiphers, masterSources); + } + + @Provides + @Named(AESGCMNoPadding.CIPHER_ALG) + public static Cipher aesGcmNoPaddingCipher() { + return new AESGCMNoPadding(); + } + + @Provides + @Named(EnvMasterSource.NAME) + public static MasterSource envMasterSource() { + return new EnvMasterSource(); + } + + @Provides + @Named(GpgAgentMasterSource.NAME) + public static MasterSource gpgAgentMasterSource() { + return new GpgAgentMasterSource(); + } + + @Provides + @Named(PinEntryMasterSource.NAME) + public static MasterSource pinEntryMasterSource() { + return new PinEntryMasterSource(); + } + + @Provides + @Named(SystemPropertyMasterSource.NAME) + public static MasterSource systemPropertyMasterSource() { + return new SystemPropertyMasterSource(); + } +} diff --git a/impl/maven-testing/src/test/java/org/apache/maven/api/plugin/testing/MojoRealSessionTest.java b/impl/maven-testing/src/test/java/org/apache/maven/api/plugin/testing/MojoRealSessionTest.java new file mode 100644 index 000000000000..114a649199f8 --- /dev/null +++ b/impl/maven-testing/src/test/java/org/apache/maven/api/plugin/testing/MojoRealSessionTest.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.api.plugin.testing; + +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.apache.maven.api.Session; +import org.apache.maven.api.di.Inject; +import org.apache.maven.api.di.Provides; +import org.apache.maven.api.di.Singleton; +import org.apache.maven.api.plugin.testing.stubs.SessionMock; +import org.apache.maven.impl.standalone.ApiRunner; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests for @MojoTest(realSession=...) support. + */ +class MojoRealSessionTest { + + @Nested + @MojoTest + class DefaultMock { + @Inject + Session session; + + @Test + void hasMockSession() { + assertNotNull(session); + assertTrue(org.mockito.Mockito.mockingDetails(session).isMock()); + } + } + + @Nested + @MojoTest(realSession = true) + class RealSession { + @Inject + Session session; + + @Test + void hasRealSession() { + assertNotNull(session); + // Real session must not be a Mockito mock + assertFalse(Mockito.mockingDetails(session).isMock()); + } + } + + @Nested + @MojoTest + class CustomMock { + @Inject + Session session; + + @Provides + @Singleton + static Session createSession() { + return SessionMock.getMockSession("target/local-repo"); + } + + @Test + void hasCustomMockSession() { + assertNotNull(session); + assertTrue(Mockito.mockingDetails(session).isMock()); + } + } + + @Nested + @MojoTest(realSession = true) + class CustomRealOverridesFlag { + @Inject + Session session; + + @Provides + @Singleton + static Session createSession() { + Path basedir = Paths.get(System.getProperty("basedir", "")); + Path localRepoPath = basedir.resolve("target/local-repo"); + // Rely on DI discovery for SecDispatcherProvider to avoid duplicate bindings + return ApiRunner.createSession(null, localRepoPath); + } + + @Test + void customProviderWinsOverFlag() { + assertNotNull(session); + assertFalse(Mockito.mockingDetails(session).isMock()); + } + } +} diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11055DIServiceInjectionTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11055DIServiceInjectionTest.java new file mode 100644 index 000000000000..199704bb600e --- /dev/null +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11055DIServiceInjectionTest.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.it; + +import java.io.File; + +import org.junit.jupiter.api.Test; + +/** + * This is a test set for gh-11055. + * + * It reproduces the behavior difference between using Session::getService and field injection via @Inject + * for some core services. + */ +class MavenITgh11055DIServiceInjectionTest extends AbstractMavenIntegrationTestCase { + + MavenITgh11055DIServiceInjectionTest() { + super("[4.0.0-rc-4,)"); + } + + @Test + void testGetServiceSucceeds() throws Exception { + File testDir = extractResources("/gh-11055-di-service-injection"); + + Verifier verifier = newVerifier(testDir.getAbsolutePath()); + verifier.addCliArgument("verify"); + verifier.execute(); + verifier.verifyErrorFreeLog(); + } +} diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/TestSuiteOrdering.java b/its/core-it-suite/src/test/java/org/apache/maven/it/TestSuiteOrdering.java index 0f0dcecce7c0..cc0ee65b997e 100644 --- a/its/core-it-suite/src/test/java/org/apache/maven/it/TestSuiteOrdering.java +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/TestSuiteOrdering.java @@ -103,6 +103,7 @@ public TestSuiteOrdering() { * the tests are to finishing. Newer tests are also more likely to fail, so this is * a fail fast technique as well. */ + suite.addTestSuite(MavenITgh11055DIServiceInjectionTest.class); suite.addTestSuite(MavenITgh11084ReactorReaderPreferConsumerPomTest.class); suite.addTestSuite(MavenITgh10312TerminallyDeprecatedMethodInGuiceTest.class); suite.addTestSuite(MavenITgh10937QuotedPipesInMavenOptsTest.class); diff --git a/its/core-it-suite/src/test/resources/gh-11055-di-service-injection/pom.xml b/its/core-it-suite/src/test/resources/gh-11055-di-service-injection/pom.xml new file mode 100644 index 000000000000..040b0b29ac18 --- /dev/null +++ b/its/core-it-suite/src/test/resources/gh-11055-di-service-injection/pom.xml @@ -0,0 +1,101 @@ + + + + 4.1.0 + + com.gitlab.tkslaw + ditests-maven-plugin + 0.1.0-SNAPSHOT + maven-plugin + + ditests-maven-plugin (IT gh-11055) + + + UTF-8 + 4.0.0-SNAPSHOT + 4.0.0-beta-1 + 3.13.0 + 17 + + + + + org.apache.maven + maven-api-core + ${mavenVersion} + provided + + + org.apache.maven + maven-api-di + ${mavenVersion} + provided + + + org.apache.maven + maven-api-annotations + ${mavenVersion} + provided + + + + org.apache.maven + maven-testing + ${mavenVersion} + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${mavenCompilerPluginVersion} + + ${javaVersion} + + + + org.apache.maven.plugins + maven-plugin-plugin + ${mavenPluginPluginVersion} + + + org.apache.maven.plugins + maven-invoker-plugin + + ${project.build.directory}/it + ${project.basedir}/src/it/settings.xml + ${project.build.directory}/local-repo + + + + integration-test + + install + run + + + + + + + diff --git a/its/core-it-suite/src/test/resources/gh-11055-di-service-injection/src/it/inject-service/.mvn/maven.config b/its/core-it-suite/src/test/resources/gh-11055-di-service-injection/src/it/inject-service/.mvn/maven.config new file mode 100644 index 000000000000..db6119c689a5 --- /dev/null +++ b/its/core-it-suite/src/test/resources/gh-11055-di-service-injection/src/it/inject-service/.mvn/maven.config @@ -0,0 +1 @@ +-ntp diff --git a/its/core-it-suite/src/test/resources/gh-11055-di-service-injection/src/it/inject-service/pom.xml b/its/core-it-suite/src/test/resources/gh-11055-di-service-injection/src/it/inject-service/pom.xml new file mode 100644 index 000000000000..d8a2995bfcab --- /dev/null +++ b/its/core-it-suite/src/test/resources/gh-11055-di-service-injection/src/it/inject-service/pom.xml @@ -0,0 +1,50 @@ + + + 4.1.0 + + com.gitlab.tkslaw + inject-service + 0.1.0-SNAPSHOT + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.6.1 + + + + @requiredMavenVersion@ + + + + + + enforce + + enforce + + validate + + + + + + com.gitlab.tkslaw + ditests-maven-plugin + @project.version@ + + + inject-service + + inject-service + + validate + + + + + + + diff --git a/its/core-it-suite/src/test/resources/gh-11055-di-service-injection/src/it/settings.xml b/its/core-it-suite/src/test/resources/gh-11055-di-service-injection/src/it/settings.xml new file mode 100644 index 000000000000..531c1fc24ae1 --- /dev/null +++ b/its/core-it-suite/src/test/resources/gh-11055-di-service-injection/src/it/settings.xml @@ -0,0 +1,35 @@ + + + + + it-repo + + + local.central + @localRepositoryUrl@ + + true + + + true + + + + + + local.central + @localRepositoryUrl@ + + true + + + true + + + + + + + it-repo + + diff --git a/its/core-it-suite/src/test/resources/gh-11055-di-service-injection/src/main/java/com/gitlab/tkslaw/ditests/DITestsMojoBase.java b/its/core-it-suite/src/test/resources/gh-11055-di-service-injection/src/main/java/com/gitlab/tkslaw/ditests/DITestsMojoBase.java new file mode 100644 index 000000000000..813d18ddfdc2 --- /dev/null +++ b/its/core-it-suite/src/test/resources/gh-11055-di-service-injection/src/main/java/com/gitlab/tkslaw/ditests/DITestsMojoBase.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.gitlab.tkslaw.ditests; + +import org.apache.maven.api.Project; +import org.apache.maven.api.Session; +import org.apache.maven.api.di.Inject; +import org.apache.maven.api.plugin.Log; +import org.apache.maven.api.plugin.Mojo; + +public class DITestsMojoBase implements Mojo { + @Inject + protected Log log; + + @Inject + protected Session session; + + @Inject + protected Project project; + + @Override + public void execute() { + log.info(() -> "log = " + log); + log.info(() -> "session = " + session); + log.info(() -> "project = " + project); + } + + protected void logService(String name, Object service) { + log.info(() -> " | %s = %s".formatted(name, service)); + } +} diff --git a/its/core-it-suite/src/test/resources/gh-11055-di-service-injection/src/main/java/com/gitlab/tkslaw/ditests/InjectServiceMojo.java b/its/core-it-suite/src/test/resources/gh-11055-di-service-injection/src/main/java/com/gitlab/tkslaw/ditests/InjectServiceMojo.java new file mode 100644 index 000000000000..7cebc84ff035 --- /dev/null +++ b/its/core-it-suite/src/test/resources/gh-11055-di-service-injection/src/main/java/com/gitlab/tkslaw/ditests/InjectServiceMojo.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.gitlab.tkslaw.ditests; + +import org.apache.maven.api.di.Inject; +import org.apache.maven.api.plugin.annotations.Mojo; +import org.apache.maven.api.services.ArtifactManager; +import org.apache.maven.api.services.DependencyResolver; +import org.apache.maven.api.services.OsService; +import org.apache.maven.api.services.ToolchainManager; + +@Mojo(name = "inject-service") +public class InjectServiceMojo extends DITestsMojoBase { + @Inject + protected ArtifactManager artifactManager; + + @Inject + protected DependencyResolver dependencyResolver; + + @Inject + protected ToolchainManager toolchainManager; + + @Inject + protected OsService osService; + + @Override + public void execute() { + super.execute(); + + log.info("Logging services injected via @Inject"); + logService("artifactManager", artifactManager); + logService("dependencyResolver", dependencyResolver); + logService("toolchainManager", toolchainManager); + logService("osService", osService); + } +} diff --git a/its/core-it-suite/src/test/resources/gh-11055-di-service-injection/src/test/java/com/gitlab/tkslaw/ditests/InjectServiceMojoTests.java b/its/core-it-suite/src/test/resources/gh-11055-di-service-injection/src/test/java/com/gitlab/tkslaw/ditests/InjectServiceMojoTests.java new file mode 100644 index 000000000000..0d667020978b --- /dev/null +++ b/its/core-it-suite/src/test/resources/gh-11055-di-service-injection/src/test/java/com/gitlab/tkslaw/ditests/InjectServiceMojoTests.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.gitlab.tkslaw.ditests; + +import org.apache.maven.api.plugin.testing.InjectMojo; +import org.apache.maven.api.plugin.testing.MojoTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@MojoTest +@DisplayName("Test InjectServiceMojo") +class InjectServiceMojoTests { + + @Test + @InjectMojo(goal = "inject-service") + @DisplayName("had its services injected by the DI container") + void testServicesNotNull(InjectServiceMojo mojo) { + // Preconditions + assertAll( + "Log, Session, and/or Project were not injected. This should not happen!", + () -> assertNotNull(mojo.log, "log"), + () -> assertNotNull(mojo.session, "session"), + () -> assertNotNull(mojo.project, "project")); + + // Actual test + assertDoesNotThrow(mojo::execute, "InjectServiceMojo::execute"); + assertAll( + "Services not injected by DI container", + () -> assertNotNull(mojo.artifactManager, "artifactManager"), + () -> assertNotNull(mojo.dependencyResolver, "dependencyResolver"), + () -> assertNotNull(mojo.toolchainManager, "toolchainManager"), + () -> assertNotNull(mojo.osService, "osService")); + } +} diff --git a/its/core-it-suite/src/test/resources/gh-11055-di-service-injection/src/test/java/com/gitlab/tkslaw/ditests/TestProviders.java b/its/core-it-suite/src/test/resources/gh-11055-di-service-injection/src/test/java/com/gitlab/tkslaw/ditests/TestProviders.java new file mode 100644 index 000000000000..0933a1524828 --- /dev/null +++ b/its/core-it-suite/src/test/resources/gh-11055-di-service-injection/src/test/java/com/gitlab/tkslaw/ditests/TestProviders.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.gitlab.tkslaw.ditests; + +import java.util.Map; + +import org.apache.maven.api.di.Named; +import org.apache.maven.api.di.Priority; +import org.apache.maven.api.di.Provides; +import org.apache.maven.api.di.Singleton; +import org.apache.maven.api.plugin.testing.stubs.SessionMock; +import org.apache.maven.api.services.DependencyResolver; +import org.apache.maven.api.services.OsService; +import org.apache.maven.api.services.ToolchainManager; +import org.apache.maven.impl.DefaultToolchainManager; +import org.apache.maven.impl.InternalSession; +import org.apache.maven.impl.model.DefaultOsService; +import org.mockito.Mockito; + +import static org.apache.maven.api.plugin.testing.MojoExtension.getBasedir; + +@Named +public class TestProviders { + + @Provides + @Singleton + @SuppressWarnings("unused") + private static InternalSession getMockSession( + DependencyResolver dependencyResolver, ToolchainManager toolchainManager, OsService osService) { + + InternalSession session = SessionMock.getMockSession(getBasedir()); + Mockito.when(session.getService(DependencyResolver.class)).thenReturn(dependencyResolver); + Mockito.when(session.getService(ToolchainManager.class)).thenReturn(toolchainManager); + Mockito.when(session.getService(OsService.class)).thenReturn(osService); + return session; + } + + @Provides + @Priority(100) + @Singleton + @SuppressWarnings("unused") + private static DependencyResolver getMockDependencyResolver() { + return Mockito.mock(DependencyResolver.class); + } + + @Provides + @Singleton + @SuppressWarnings("unused") + private static ToolchainManager getToolchainManager() { + return new DefaultToolchainManager(Map.of()); + } + + @Provides + @Singleton + @Priority(100) + @SuppressWarnings("unused") + private static OsService getOsService() { + return new DefaultOsService(); + } +}