diff --git a/api/maven-api-di/src/main/java/org/apache/maven/di/tool/DiIndexProcessor.java b/api/maven-api-di/src/main/java/org/apache/maven/di/tool/DiIndexProcessor.java index 1341ea6dd09b..b2232348d117 100644 --- a/api/maven-api-di/src/main/java/org/apache/maven/di/tool/DiIndexProcessor.java +++ b/api/maven-api-di/src/main/java/org/apache/maven/di/tool/DiIndexProcessor.java @@ -24,6 +24,7 @@ import javax.annotation.processing.SupportedSourceVersion; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; +import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; import javax.tools.Diagnostic; import javax.tools.FileObject; @@ -71,14 +72,19 @@ public boolean process(Set annotations, RoundEnvironment } private String getFullClassName(TypeElement typeElement) { - String className = typeElement.getQualifiedName().toString(); + StringBuilder className = new StringBuilder(typeElement.getSimpleName()); Element enclosingElement = typeElement.getEnclosingElement(); - if (enclosingElement instanceof TypeElement) { - String enclosingClassName = - ((TypeElement) enclosingElement).getQualifiedName().toString(); - className = enclosingClassName + "$" + typeElement.getSimpleName(); + + while (enclosingElement instanceof TypeElement) { + className.insert(0, "$").insert(0, ((TypeElement) enclosingElement).getSimpleName()); + enclosingElement = enclosingElement.getEnclosingElement(); + } + + if (enclosingElement instanceof PackageElement) { + className.insert(0, ".").insert(0, ((PackageElement) enclosingElement).getQualifiedName()); } - return className; + + return className.toString(); } private void updateFileIfChanged() throws IOException { diff --git a/maven-api-impl/pom.xml b/maven-api-impl/pom.xml index 8cc4830f86fa..69b7355e57e2 100644 --- a/maven-api-impl/pom.xml +++ b/maven-api-impl/pom.xml @@ -71,6 +71,10 @@ under the License. org.apache.maven maven-api-settings + + org.apache.maven + maven-di + org.apache.maven.resolver maven-resolver-api @@ -132,11 +136,6 @@ under the License. org.assertj assertj-core - - org.apache.maven - maven-di - test - org.apache.maven.resolver maven-resolver-named-locks diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/di/MojoExecutionScope.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/di/MojoExecutionScope.java new file mode 100644 index 000000000000..2128db3ac0ce --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/di/MojoExecutionScope.java @@ -0,0 +1,124 @@ +/* + * 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.internal.impl.di; + +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.function.Supplier; + +import org.apache.maven.api.annotations.Nonnull; +import org.apache.maven.di.Key; +import org.apache.maven.di.Scope; +import org.apache.maven.di.impl.DIException; + +/** + * MojoExecutionScope + */ +public class MojoExecutionScope implements Scope { + + protected static final class ScopeState { + private final Map, Supplier> seeded = new HashMap<>(); + + private final Map, Object> provided = new HashMap<>(); + + public void seed(Class clazz, Supplier value) { + seeded.put(Key.of(clazz), value); + } + + public Collection provided() { + return provided.values(); + } + } + + private final ThreadLocal> values = new ThreadLocal<>(); + + public MojoExecutionScope() {} + + public static Supplier seededKeySupplier(Class clazz) { + return () -> { + throw new IllegalStateException( + "No instance of " + clazz.getName() + " is bound to the mojo execution scope."); + }; + } + + public void enter() { + LinkedList stack = values.get(); + if (stack == null) { + stack = new LinkedList<>(); + values.set(stack); + } + stack.addFirst(new ScopeState()); + } + + protected ScopeState getScopeState() { + LinkedList stack = values.get(); + if (stack == null || stack.isEmpty()) { + throw new IllegalStateException(); + } + return stack.getFirst(); + } + + public void exit() { + final LinkedList stack = values.get(); + if (stack == null || stack.isEmpty()) { + throw new IllegalStateException(); + } + stack.removeFirst(); + if (stack.isEmpty()) { + values.remove(); + } + } + + public void seed(Class clazz, Supplier value) { + getScopeState().seed(clazz, value); + } + + public void seed(Class clazz, final T value) { + seed(clazz, (Supplier) () -> value); + } + + @SuppressWarnings("unchecked") + @Nonnull + public Supplier scope(@Nonnull Key key, @Nonnull Supplier unscoped) { + return () -> { + LinkedList stack = values.get(); + if (stack == null || stack.isEmpty()) { + throw new DIException("Cannot access " + key + " outside of a scoping block"); + } + + ScopeState state = stack.getFirst(); + + Supplier seeded = state.seeded.get(key); + + if (seeded != null) { + return (T) seeded.get(); + } + + T provided = (T) state.provided.get(key); + if (provided == null && unscoped != null) { + provided = unscoped.get(); + state.provided.put(key, provided); + } + + return provided; + }; + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/di/OutOfScopeException.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/di/OutOfScopeException.java new file mode 100644 index 000000000000..c437425415d2 --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/di/OutOfScopeException.java @@ -0,0 +1,31 @@ +/* + * 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.internal.impl.di; + +import org.apache.maven.di.impl.DIException; + +public class OutOfScopeException extends DIException { + public OutOfScopeException(String message) { + super(message); + } + + public OutOfScopeException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/di/SessionScope.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/di/SessionScope.java new file mode 100644 index 000000000000..f1ebcc22fc6a --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/di/SessionScope.java @@ -0,0 +1,186 @@ +/* + * 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.internal.impl.di; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.apache.maven.di.Key; +import org.apache.maven.di.Scope; +import org.apache.maven.di.impl.Types; + +public class SessionScope implements Scope { + + /** + * ScopeState + */ + protected static final class ScopeState { + private final Map, CachingProvider> provided = new ConcurrentHashMap<>(); + + public void seed(Class clazz, Supplier value) { + provided.put(Key.of(clazz), new CachingProvider<>(value)); + } + + @SuppressWarnings("unchecked") + public Supplier scope(Key key, Supplier unscoped) { + Supplier provider = provided.computeIfAbsent(key, k -> new CachingProvider<>(unscoped)); + return (Supplier) provider; + } + + public Collection> providers() { + return provided.values(); + } + } + + protected final List values = new CopyOnWriteArrayList<>(); + + public void enter() { + values.add(0, new ScopeState()); + } + + protected ScopeState getScopeState() { + if (values.isEmpty()) { + throw new OutOfScopeException("Cannot access session scope outside of a scoping block"); + } + return values.get(0); + } + + public void exit() { + if (values.isEmpty()) { + throw new IllegalStateException(); + } + values.remove(0); + } + + public void seed(Class clazz, Supplier value) { + getScopeState().seed(clazz, value); + } + + public void seed(Class clazz, T value) { + seed(clazz, (Supplier) () -> value); + } + + @Override + public Supplier scope(Key key, Supplier unscoped) { + // Lazy evaluating provider + return () -> { + if (values.isEmpty()) { + return createProxy(key, unscoped); + } else { + return getScopeState().scope(key, unscoped).get(); + } + }; + } + + @SuppressWarnings("unchecked") + protected T createProxy(Key key, Supplier unscoped) { + InvocationHandler dispatcher = (proxy, method, args) -> dispatch(key, unscoped, method, args); + Class superType = (Class) Types.getRawType(key.getType()); + Class[] interfaces = getInterfaces(superType); + return (T) java.lang.reflect.Proxy.newProxyInstance(superType.getClassLoader(), interfaces, dispatcher); + } + + protected Object dispatch(Key key, Supplier unscoped, Method method, Object[] args) throws Throwable { + method.setAccessible(true); + try { + return method.invoke(getScopeState().scope(key, unscoped).get(), args); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } + + 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); + } + } + } + throw new IllegalArgumentException("The use of session scoped proxies require " + + "a org.eclipse.sisu.Typed or javax.enterprise.inject.Typed annotation"); + } + } + + protected boolean isTypeAnnotation(Class annotationType) { + return "org.apache.maven.api.di.Typed".equals(annotationType.getName()); + } + + /** + * A provider wrapping an existing provider with a cache + * @param the provided type + */ + protected static class CachingProvider implements Supplier { + private final Supplier provider; + private volatile T value; + + CachingProvider(Supplier provider) { + this.provider = provider; + } + + public T value() { + return value; + } + + @Override + public T get() { + if (value == null) { + synchronized (this) { + if (value == null) { + value = provider.get(); + } + } + } + return value; + } + } + + public static Supplier seededKeySupplier(Class clazz) { + return () -> { + throw new IllegalStateException("No instance of " + clazz.getName() + " is bound to the session scope."); + }; + } +} diff --git a/maven-api-impl/src/test/java/org/apache/maven/internal/impl/standalone/ApiRunner.java b/maven-api-impl/src/test/java/org/apache/maven/internal/impl/standalone/ApiRunner.java index 1f141fbbbe8d..15a605ea20ef 100644 --- a/maven-api-impl/src/test/java/org/apache/maven/internal/impl/standalone/ApiRunner.java +++ b/maven-api-impl/src/test/java/org/apache/maven/internal/impl/standalone/ApiRunner.java @@ -41,6 +41,7 @@ import org.apache.maven.api.Type; import org.apache.maven.api.Version; import org.apache.maven.api.di.Provides; +import org.apache.maven.api.di.SessionScoped; import org.apache.maven.api.model.PluginContainer; import org.apache.maven.api.model.Profile; import org.apache.maven.api.services.ArtifactManager; @@ -58,6 +59,7 @@ import org.apache.maven.di.impl.DIException; import org.apache.maven.internal.impl.AbstractSession; import org.apache.maven.internal.impl.InternalSession; +import org.apache.maven.internal.impl.di.SessionScope; import org.eclipse.aether.DefaultRepositorySystemSession; import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.RepositorySystemSession; @@ -75,7 +77,12 @@ public static Session createSession() { injector.bindInstance(Injector.class, injector); injector.bindImplicit(ApiRunner.class); injector.discover(ApiRunner.class.getClassLoader()); - return injector.getInstance(Session.class); + Session session = injector.getInstance(Session.class); + SessionScope scope = new SessionScope(); + scope.enter(); + scope.seed(Session.class, session); + injector.bindScope(SessionScoped.class, scope); + return session; } static class DefaultSession extends AbstractSession { diff --git a/maven-core/src/main/java/org/apache/maven/execution/scope/internal/MojoExecutionScope.java b/maven-core/src/main/java/org/apache/maven/execution/scope/internal/MojoExecutionScope.java index a9d9feb65f3d..1233557b3747 100644 --- a/maven-core/src/main/java/org/apache/maven/execution/scope/internal/MojoExecutionScope.java +++ b/maven-core/src/main/java/org/apache/maven/execution/scope/internal/MojoExecutionScope.java @@ -18,20 +18,13 @@ */ package org.apache.maven.execution.scope.internal; -import java.lang.annotation.Annotation; import java.util.Collection; -import java.util.HashMap; import java.util.IdentityHashMap; -import java.util.LinkedList; -import java.util.Map; -import java.util.function.Supplier; import com.google.inject.Key; -import com.google.inject.OutOfScopeException; import com.google.inject.Provider; import com.google.inject.Scope; -import com.google.inject.name.Names; -import com.google.inject.util.Providers; +import com.google.inject.name.Named; import org.apache.maven.execution.MojoExecutionEvent; import org.apache.maven.execution.MojoExecutionListener; import org.apache.maven.execution.scope.WeakMojoExecutionListener; @@ -40,96 +33,22 @@ /** * MojoExecutionScope */ -public class MojoExecutionScope implements Scope, org.apache.maven.di.Scope, MojoExecutionListener { - private static final Provider SEEDED_KEY_PROVIDER = () -> { - throw new IllegalStateException(); - }; - - private static final class ScopeState { - private final Map, Provider> seeded = new HashMap<>(); - - private final Map, Object> provided = new HashMap<>(); - } - - private final ThreadLocal> values = new ThreadLocal<>(); - - public MojoExecutionScope() {} - - public void enter() { - LinkedList stack = values.get(); - if (stack == null) { - stack = new LinkedList<>(); - values.set(stack); - } - stack.addFirst(new ScopeState()); - } - - private ScopeState getScopeState() { - LinkedList stack = values.get(); - if (stack == null || stack.isEmpty()) { - throw new IllegalStateException(); - } - return stack.getFirst(); - } - - public void exit() throws MojoExecutionException { - final LinkedList stack = values.get(); - if (stack == null || stack.isEmpty()) { - throw new IllegalStateException(); - } - stack.removeFirst(); - if (stack.isEmpty()) { - values.remove(); - } - } +public class MojoExecutionScope extends org.apache.maven.internal.impl.di.MojoExecutionScope + implements Scope, MojoExecutionListener { public void seed(Class clazz, Provider value) { - getScopeState().seeded.put(Key.get(clazz), value); - } - - public void seed(Class clazz, final T value) { - getScopeState().seeded.put(Key.get(clazz), Providers.of(value)); - } - - public Provider scope(final Key key, final Provider unscoped) { - return () -> { - LinkedList stack = values.get(); - if (stack == null || stack.isEmpty()) { - throw new OutOfScopeException("Cannot access " + key + " outside of a scoping block"); - } - - ScopeState state = stack.getFirst(); - - Provider seeded = state.seeded.get(key); - - if (seeded != null) { - return (T) seeded.get(); - } - - T provided = (T) state.provided.get(key); - if (provided == null && unscoped != null) { - provided = unscoped.get(); - state.provided.put(key, provided); - } - - return provided; - }; + getScopeState().seed(clazz, value::get); } - @Override - public Supplier scope(org.apache.maven.di.Key key, Annotation scope, Supplier unscoped) { - Object qualifier = key.getQualifier(); - Key k = qualifier != null - ? Key.get(key.getType(), qualifier instanceof String s ? Names.named(s) : (Annotation) qualifier) - : Key.get(key.getType()); - Provider up = unscoped::get; - Provider p = scope(k, up); - return p::get; + public Provider scope(final Key key, Provider unscoped) { + Object qualifier = key.getAnnotation() instanceof Named n ? n.value() : key.getAnnotation(); + org.apache.maven.di.Key k = + org.apache.maven.di.Key.ofType(key.getTypeLiteral().getType(), qualifier); + return scope(k, unscoped::get)::get; } - @SuppressWarnings({"unchecked"}) - public static Provider seededKeyProvider() { - return (Provider) SEEDED_KEY_PROVIDER; + public static Provider seededKeyProvider(Class clazz) { + return MojoExecutionScope.seededKeySupplier(clazz)::get; } public void beforeMojoExecution(MojoExecutionEvent event) throws MojoExecutionException { @@ -154,7 +73,7 @@ private Collection getProvidedListeners() { // the same instance can be provided multiple times under different Key's // deduplicate instances to avoid redundant beforeXXX/afterXXX callbacks IdentityHashMap listeners = new IdentityHashMap<>(); - for (Object provided : getScopeState().provided.values()) { + for (Object provided : getScopeState().provided()) { if (provided instanceof WeakMojoExecutionListener) { listeners.put((WeakMojoExecutionListener) provided, null); } diff --git a/maven-core/src/main/java/org/apache/maven/execution/scope/internal/MojoExecutionScopeModule.java b/maven-core/src/main/java/org/apache/maven/execution/scope/internal/MojoExecutionScopeModule.java index 149b94f97c5d..a477ec868cbb 100644 --- a/maven-core/src/main/java/org/apache/maven/execution/scope/internal/MojoExecutionScopeModule.java +++ b/maven-core/src/main/java/org/apache/maven/execution/scope/internal/MojoExecutionScopeModule.java @@ -40,17 +40,19 @@ protected void configure() { // bindScope(org.apache.maven.api.di.MojoExecutionScoped.class, scope); bind(MojoExecutionScope.class).toInstance(scope); bind(MavenProject.class) - .toProvider(MojoExecutionScope.seededKeyProvider()) + .toProvider(MojoExecutionScope.seededKeyProvider(MavenProject.class)) .in(scope); bind(MojoExecution.class) - .toProvider(MojoExecutionScope.seededKeyProvider()) + .toProvider(MojoExecutionScope.seededKeyProvider(MojoExecution.class)) + .in(scope); + bind(Log.class) + .toProvider(MojoExecutionScope.seededKeyProvider(Log.class)) .in(scope); - bind(Log.class).toProvider(MojoExecutionScope.seededKeyProvider()).in(scope); bind(org.apache.maven.api.Project.class) - .toProvider(MojoExecutionScope.seededKeyProvider()) + .toProvider(MojoExecutionScope.seededKeyProvider(org.apache.maven.api.Project.class)) .in(scope); bind(org.apache.maven.api.MojoExecution.class) - .toProvider(MojoExecutionScope.seededKeyProvider()) + .toProvider(MojoExecutionScope.seededKeyProvider(org.apache.maven.api.MojoExecution.class)) .in(scope); } } diff --git a/maven-core/src/main/java/org/apache/maven/graph/DefaultGraphBuilder.java b/maven-core/src/main/java/org/apache/maven/graph/DefaultGraphBuilder.java index 02d935df4fee..93c81d830df0 100644 --- a/maven-core/src/main/java/org/apache/maven/graph/DefaultGraphBuilder.java +++ b/maven-core/src/main/java/org/apache/maven/graph/DefaultGraphBuilder.java @@ -120,8 +120,10 @@ private Result sessionDependencyGraph(final MavenSession Result result = null; if (session.getProjectDependencyGraph() != null || session.getProjects() != null) { - final ProjectDependencyGraph graph = - new DefaultProjectDependencyGraph(session.getAllProjects(), session.getProjects()); + ProjectDependencyGraph graph = new DefaultProjectDependencyGraph(session.getAllProjects()); + if (session.getProjects() != null) { + graph = new FilteredProjectDependencyGraph(graph, session.getProjects()); + } result = Result.success(graph); } diff --git a/maven-core/src/main/java/org/apache/maven/graph/FilteredProjectDependencyGraph.java b/maven-core/src/main/java/org/apache/maven/graph/FilteredProjectDependencyGraph.java index 2d6612e3aa4f..feb06ec7c2cb 100644 --- a/maven-core/src/main/java/org/apache/maven/graph/FilteredProjectDependencyGraph.java +++ b/maven-core/src/main/java/org/apache/maven/graph/FilteredProjectDependencyGraph.java @@ -34,11 +34,11 @@ */ class FilteredProjectDependencyGraph implements ProjectDependencyGraph { - private ProjectDependencyGraph projectDependencyGraph; + private final ProjectDependencyGraph projectDependencyGraph; - private Map whiteList; + private final Map whiteList; - private List sortedProjects; + private final List sortedProjects; /** * Creates a new project dependency graph from the specified graph. @@ -50,46 +50,58 @@ class FilteredProjectDependencyGraph implements ProjectDependencyGraph { ProjectDependencyGraph projectDependencyGraph, Collection whiteList) { this.projectDependencyGraph = Objects.requireNonNull(projectDependencyGraph, "projectDependencyGraph cannot be null"); - this.whiteList = new IdentityHashMap<>(); - for (MavenProject project : whiteList) { this.whiteList.put(project, null); } + this.sortedProjects = projectDependencyGraph.getSortedProjects().stream() + .filter(this.whiteList::containsKey) + .toList(); } /** * @since 3.5.0 */ + @Override public List getAllProjects() { return this.projectDependencyGraph.getAllProjects(); } + @Override public List getSortedProjects() { - if (sortedProjects == null) { - sortedProjects = applyFilter(projectDependencyGraph.getSortedProjects()); - } - return new ArrayList<>(sortedProjects); } + @Override public List getDownstreamProjects(MavenProject project, boolean transitive) { - return applyFilter(projectDependencyGraph.getDownstreamProjects(project, transitive)); + return applyFilter(projectDependencyGraph.getDownstreamProjects(project, transitive), transitive, false); } + @Override public List getUpstreamProjects(MavenProject project, boolean transitive) { - return applyFilter(projectDependencyGraph.getUpstreamProjects(project, transitive)); + return applyFilter(projectDependencyGraph.getUpstreamProjects(project, transitive), transitive, true); } - private List applyFilter(Collection projects) { + /** + * Filter out whitelisted projects with a big twist: + * Assume we have all projects {@code a, b, c} while active are {@code a, c} and relation among all projects + * is {@code a -> b -> c}. This method handles well the case for transitive list. But, for non-transitive we need + * to "pull in" transitive dependencies of eliminated projects, as for case above, the properly filtered list would + * be {@code a -> c}. + *

+ * Original code would falsely report {@code a} project as "without dependencies", basically would lose link due + * filtering. This causes build ordering issues in concurrent builders. + */ + private List applyFilter( + Collection projects, boolean transitive, boolean upstream) { List filtered = new ArrayList<>(projects.size()); - for (MavenProject project : projects) { if (whiteList.containsKey(project)) { filtered.add(project); + } else if (!transitive) { + filtered.addAll(upstream ? getUpstreamProjects(project, false) : getDownstreamProjects(project, false)); } } - return filtered; } diff --git a/maven-core/src/main/java/org/apache/maven/internal/impl/SisuDiBridgeModule.java b/maven-core/src/main/java/org/apache/maven/internal/impl/SisuDiBridgeModule.java index eb6789c12bff..6cc9b4973324 100644 --- a/maven-core/src/main/java/org/apache/maven/internal/impl/SisuDiBridgeModule.java +++ b/maven-core/src/main/java/org/apache/maven/internal/impl/SisuDiBridgeModule.java @@ -21,31 +21,27 @@ import javax.inject.Named; import javax.inject.Provider; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; import java.lang.annotation.Annotation; -import java.net.URL; -import java.nio.charset.StandardCharsets; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Comparator; -import java.util.HashSet; -import java.util.Iterator; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; import com.google.inject.AbstractModule; -import com.google.inject.binder.AnnotatedBindingBuilder; +import com.google.inject.Binder; import com.google.inject.name.Names; +import com.google.inject.spi.ProviderInstanceBinding; import org.apache.maven.api.di.MojoExecutionScoped; import org.apache.maven.api.di.SessionScoped; -import org.apache.maven.api.services.MavenException; import org.apache.maven.di.Injector; import org.apache.maven.di.Key; +import org.apache.maven.di.Scope; import org.apache.maven.di.impl.Binding; import org.apache.maven.di.impl.DIException; import org.apache.maven.di.impl.Dependency; @@ -54,76 +50,147 @@ import org.apache.maven.session.scope.internal.SessionScope; import org.codehaus.plexus.PlexusContainer; import org.codehaus.plexus.component.repository.exception.ComponentLookupException; +import org.eclipse.sisu.BeanEntry; +import org.eclipse.sisu.inject.BeanLocator; @Named public class SisuDiBridgeModule extends AbstractModule { - InjectorImpl injector; - final Set loaded = new HashSet<>(); + protected final boolean discover; + protected InjectorImpl injector; + + public SisuDiBridgeModule() { + this(true); + } + + public SisuDiBridgeModule(boolean discover) { + this.discover = discover; + } @Override protected void configure() { Provider containerProvider = getProvider(PlexusContainer.class); + Provider beanLocatorProvider = getProvider(BeanLocator.class); + injector = new BridgeInjectorImpl(beanLocatorProvider, binder()); + bindScope(injector, containerProvider, SessionScoped.class, SessionScope.class); + bindScope(injector, containerProvider, MojoExecutionScoped.class, MojoExecutionScope.class); + injector.bindInstance(Injector.class, injector); + bind(Injector.class).toInstance(injector); + bind(SisuDiBridgeModule.class).toInstance(this); + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + if (classLoader == null) { + classLoader = getClass().getClassLoader(); + } + if (discover) { + injector.discover(classLoader); + } + } + + private void bindScope( + InjectorImpl injector, + Provider containerProvider, + Class sa, + Class ss) { + injector.bindScope(sa, () -> { + try { + return containerProvider.get().lookup(ss); + } catch (ComponentLookupException e) { + throw new RuntimeException(e); + } + }); + } + + static class BridgeInjectorImpl extends InjectorImpl { + final Provider locator; + final Binder binder; + + BridgeInjectorImpl(Provider locator, Binder binder) { + this.locator = locator; + this.binder = binder; + } + + @Override + protected Injector bind(Key key, Binding binding) { + super.bind(key, binding); + if (key.getQualifier() != null) { + com.google.inject.Key k = toGuiceKey(key); + this.binder.bind(k).toProvider(new BridgeProvider<>(binding)); + } + return this; + } + + @SuppressWarnings("unchecked") + private static com.google.inject.Key toGuiceKey(Key key) { + if (key.getQualifier() instanceof String s) { + return (com.google.inject.Key) com.google.inject.Key.get(key.getType(), Names.named(s)); + } 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()); + } + } + + static class BindingToBeanEntry extends Binding { + private BeanEntry beanEntry; + + BindingToBeanEntry(Key elementType) { + super(elementType, Set.of()); + } + + public BindingToBeanEntry toBeanEntry(BeanEntry beanEntry) { + this.beanEntry = beanEntry; + return this; + } - injector = new InjectorImpl() { @Override - public Supplier getCompiledBinding(Dependency dep) { - Key key = dep.key(); - Set> res = getBindings(key); - if (res != null && !res.isEmpty()) { - List> bindingList = new ArrayList<>(res); - Comparator> comparing = Comparator.comparing(Binding::getPriority); - bindingList.sort(comparing.reversed()); - Binding binding = bindingList.get(0); - return compile(binding); - } - if (key.getRawType() == List.class) { - Set> res2 = getBindings(key.getTypeParameter(0)); - Set> res3 = res2 != null ? new HashSet<>(res2) : new HashSet<>(); - try { - List l = containerProvider - .get() - .lookupList(key.getTypeParameter(0).getRawType()); - l.forEach(o -> res3.add(new Binding.BindingToInstance<>(o))); - } catch (Throwable e) { - // ignore - e.printStackTrace(); - } - List> list = - res3.stream().map(this::compile).collect(Collectors.toList()); - //noinspection unchecked - return () -> (Q) list(list); - } - if (key.getRawType() == Map.class) { - Key k = key.getTypeParameter(0); - Key v = key.getTypeParameter(1); - if (k.getRawType() == String.class) { - Set> res2 = getBindings(v); - Set> res3 = res2 != null ? new HashSet<>(res2) : new HashSet<>(); - Map> map = res3.stream() - .filter(b -> b.getOriginalKey() == null - || b.getOriginalKey().getQualifier() == null - || b.getOriginalKey().getQualifier() instanceof String) - .collect(Collectors.toMap( - b -> (String) - (b.getOriginalKey() != null - ? b.getOriginalKey().getQualifier() - : null), - this::compile)); - //noinspection unchecked - return () -> (Q) map(map); - } - } - try { - Q t = containerProvider.get().lookup(key.getRawType()); - return compile(new Binding.BindingToInstance<>(t)); - } catch (Throwable e) { - // ignore - e.printStackTrace(); - } - if (dep.optional()) { - return () -> null; + public Supplier compile(Function, Supplier> compiler) { + return beanEntry.getProvider()::get; + } + } + + class BridgeProvider implements Provider { + final Binding binding; + + BridgeProvider(Binding binding) { + this.binding = binding; + } + + @Override + public T get() { + return compile(binding).get(); + } + } + + @Override + public Supplier getCompiledBinding(Dependency dep) { + Key key = dep.key(); + Class rawType = key.getRawType(); + if (rawType == List.class) { + return getListSupplier(key); + } else if (rawType == Map.class) { + return getMapSupplier(key); + } else { + return getBeanSupplier(dep, key); + } + } + + private Supplier getBeanSupplier(Dependency dep, Key key) { + List> list = new ArrayList<>(); + // Add DI bindings + list.addAll(getBindings().getOrDefault(key, Set.of())); + // Add Plexus bindings + for (var bean : locator.get().locate(toGuiceKey(key))) { + if (isPlexusBean(bean)) { + list.add(new BindingToBeanEntry<>(key).toBeanEntry(bean)); } + } + if (!list.isEmpty()) { + list.sort(getBindingComparator()); + //noinspection unchecked + return () -> (Q) getInstance(list.iterator().next()); + } else if (dep.optional()) { + return () -> null; + } else { throw new DIException("No binding to construct an instance for key " + key.getDisplayString() + ". Existing bindings:\n" + getBoundKeys().stream() @@ -133,80 +200,78 @@ public Supplier getCompiledBinding(Dependency dep) { .distinct() .collect(Collectors.joining("\n - ", " - ", ""))); } - }; - injector.bindScope(SessionScoped.class, () -> { - try { - return containerProvider.get().lookup(SessionScope.class); - } catch (ComponentLookupException e) { - throw new RuntimeException(e); - } - }); - injector.bindScope(MojoExecutionScoped.class, () -> { - try { - return containerProvider.get().lookup(MojoExecutionScope.class); - } catch (ComponentLookupException e) { - throw new RuntimeException(e); - } - }); - injector.bindInstance(Injector.class, injector); - bind(Injector.class).toInstance(injector); - bind(SisuDiBridgeModule.class).toInstance(this); - ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); - if (classLoader == null) { - classLoader = getClass().getClassLoader(); } - loadFromClassLoader(classLoader); - injector.getBindings().keySet().stream() - .filter(k -> k.getQualifier() != null) - .sorted(Comparator.comparing(k -> k.getRawType().getName())) - .distinct() - .forEach(key -> { - Class clazz = key.getRawType(); - Class itf = (clazz.isInterface() - ? null - : (Class) (clazz.getInterfaces().length > 0 ? clazz.getInterfaces()[0] : clazz)); - if (itf != null) { - AnnotatedBindingBuilder binder = bind(itf); - if (key.getQualifier() instanceof String s && !s.isEmpty()) { - binder.annotatedWith(Names.named(s)); - } else if (key.getQualifier() instanceof Annotation a) { - binder.annotatedWith(a); - } - binder.toProvider(() -> injector.getInstance(clazz)); - } - }); - } - public void loadFromClassLoader(ClassLoader classLoader) { - try { - for (Iterator it = classLoader - .getResources("META-INF/maven/org.apache.maven.api.di.Inject") - .asIterator(); - it.hasNext(); ) { - URL url = it.next(); - if (loaded.add(url.toExternalForm())) { - List lines; - try (InputStream is = url.openStream(); - BufferedReader reader = - new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { - lines = reader.lines() - .map(String::trim) - .filter(s -> !s.isEmpty() && !s.startsWith("#")) - .toList(); + private Supplier getListSupplier(Key key) { + Key elementType = key.getTypeParameter(0); + return () -> { + List> list = new ArrayList<>(); + // Add DI bindings + list.addAll(getBindings().getOrDefault(elementType, Set.of())); + // Add Plexus bindings + for (var bean : locator.get().locate(toGuiceKey(elementType))) { + if (isPlexusBean(bean)) { + list.add(new BindingToBeanEntry<>(elementType).toBeanEntry(bean)); } - for (String className : lines) { - try { - Class clazz = classLoader.loadClass(className); - injector.bindImplicit(clazz); - } catch (ClassNotFoundException e) { - // ignore - e.printStackTrace(); - } + } + //noinspection unchecked + return (Q) list(list.stream().sorted(getBindingComparator()).toList(), this::getInstance); + }; + } + + private Supplier getMapSupplier(Key key) { + Key keyType = key.getTypeParameter(0); + Key valueType = key.getTypeParameter(1); + if (keyType.getRawType() != String.class) { + throw new DIException("Only String keys are supported for maps: " + key); + } + return () -> { + var comparator = getBindingComparator(); + Map> map = new HashMap<>(); + for (Binding b : getBindings().getOrDefault(valueType, Set.of())) { + String name = + b.getOriginalKey() != null && b.getOriginalKey().getQualifier() instanceof String s + ? s + : ""; + map.compute(name, (n, ob) -> ob == null || comparator.compare(ob, b) < 0 ? b : ob); + } + for (var bean : locator.get().locate(toGuiceKey(valueType))) { + if (isPlexusBean(bean)) { + Binding b = new BindingToBeanEntry<>(valueType) + .toBeanEntry(bean) + .prioritize(bean.getRank()); + String name = bean.getKey() instanceof com.google.inject.name.Named n ? n.value() : ""; + map.compute(name, (n, ob) -> ob == null || ob.getPriority() < b.getPriority() ? b : ob); } } + //noinspection unchecked + return (Q) map(map, this::getInstance); + }; + } + + private Q getInstance(Binding binding) { + return compile(binding).get(); + } + + private static Comparator> getBindingComparator() { + Comparator> comparing = Comparator.comparing(Binding::getPriority); + return comparing.reversed(); + } + + private boolean isPlexusBean(BeanEntry entry) { + try { + if ("org.eclipse.sisu.inject.LazyBeanEntry" + .equals(entry.getClass().getName())) { + Field f = entry.getClass().getDeclaredField("binding"); + f.setAccessible(true); + Object b = f.get(entry); + return !(b instanceof ProviderInstanceBinding pib) + || !(pib.getUserSuppliedProvider() instanceof BridgeProvider); + } + } catch (Exception e) { + // ignore } - } catch (IOException e) { - throw new MavenException(e); + return true; } } } diff --git a/maven-core/src/main/java/org/apache/maven/session/scope/internal/SessionScope.java b/maven-core/src/main/java/org/apache/maven/session/scope/internal/SessionScope.java index 284fafd6aea0..9938a9c46cac 100644 --- a/maven-core/src/main/java/org/apache/maven/session/scope/internal/SessionScope.java +++ b/maven-core/src/main/java/org/apache/maven/session/scope/internal/SessionScope.java @@ -19,190 +19,36 @@ package org.apache.maven.session.scope.internal; import java.lang.annotation.Annotation; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.InvocationTargetException; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.Stream; import com.google.inject.Key; -import com.google.inject.OutOfScopeException; import com.google.inject.Provider; import com.google.inject.Scope; -import com.google.inject.name.Names; +import com.google.inject.name.Named; /** * SessionScope */ -public class SessionScope implements Scope, org.apache.maven.di.Scope { - - private static final Provider SEEDED_KEY_PROVIDER = () -> { - throw new IllegalStateException(); - }; - - /** - * ScopeState - */ - protected static final class ScopeState { - private final Map, CachingProvider> provided = new ConcurrentHashMap<>(); - - public void seed(Class clazz, Provider value) { - provided.put(Key.get(clazz), new CachingProvider<>(value)); - } - - @SuppressWarnings("unchecked") - public Provider scope(Key key, Provider unscoped) { - Provider provider = provided.computeIfAbsent(key, k -> new CachingProvider<>(unscoped)); - return (Provider) provider; - } - - public Collection> providers() { - return provided.values(); - } - } - - private final List values = new CopyOnWriteArrayList<>(); - - public void enter() { - values.add(0, new ScopeState()); - } - - protected ScopeState getScopeState() { - if (values.isEmpty()) { - throw new OutOfScopeException("Cannot access session scope outside of a scoping block"); - } - return values.get(0); - } - - public void exit() { - if (values.isEmpty()) { - throw new IllegalStateException(); - } - values.remove(0); - } +public class SessionScope extends org.apache.maven.internal.impl.di.SessionScope implements Scope { public void seed(Class clazz, Provider value) { - getScopeState().seed(clazz, value); - } - - public void seed(Class clazz, final T value) { - seed(clazz, (Provider) () -> value); + getScopeState().seed(clazz, value::get); } public Provider scope(final Key key, final Provider unscoped) { - // Lazy evaluating provider - return () -> { - if (values.isEmpty()) { - return createProxy(key, unscoped); - } else { - return getScopeState().scope(key, unscoped).get(); - } - }; - } - - @SuppressWarnings("unchecked") - @Override - public Supplier scope(org.apache.maven.di.Key key, Annotation scope, Supplier unscoped) { - Object qualifier = key.getQualifier(); - Key k = qualifier != null - ? Key.get(key.getType(), qualifier instanceof String s ? Names.named(s) : (Annotation) qualifier) - : Key.get(key.getType()); - Provider up = unscoped::get; - Provider p = scope((Key) k, up); - return p::get; - } - - @SuppressWarnings("unchecked") - private T createProxy(Key key, Provider unscoped) { - InvocationHandler dispatcher = (proxy, method, args) -> { - method.setAccessible(true); - try { - return method.invoke(getScopeState().scope(key, unscoped).get(), args); - } catch (InvocationTargetException e) { - throw e.getCause(); - } - }; - Class superType = (Class) key.getTypeLiteral().getRawType(); - Class[] interfaces = getInterfaces(superType); - return (T) java.lang.reflect.Proxy.newProxyInstance(superType.getClassLoader(), interfaces, dispatcher); - } - - private Class[] getInterfaces(Class superType) { - if (superType.isInterface()) { - return new Class[] {superType}; - } else { - for (Annotation a : superType.getAnnotations()) { - Class annotationType = a.annotationType(); - if ("org.apache.maven.api.di.Typed".equals(annotationType.getName()) - || "org.eclipse.sisu.Typed".equals(annotationType.getName()) - || "javax.enterprise.inject.Typed".equals(annotationType.getName()) - || "jakarta.enterprise.inject.Typed".equals(annotationType.getName())) { - 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()).collect(Collectors.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"); - } + Object qualifier = key.getAnnotation() instanceof Named n ? n.value() : key.getAnnotation(); + org.apache.maven.di.Key k = + org.apache.maven.di.Key.ofType(key.getTypeLiteral().getType(), qualifier); + return scope(k, unscoped::get)::get; } - /** - * A provider wrapping an existing provider with a cache - * @param the provided type - */ - protected static class CachingProvider implements Provider { - private final Provider provider; - private volatile T value; - - CachingProvider(Provider provider) { - this.provider = provider; - } - - public T value() { - return value; - } - - @Override - public T get() { - if (value == null) { - synchronized (this) { - if (value == null) { - value = provider.get(); - } - } - } - return value; - } - } - - @SuppressWarnings({"unchecked"}) - public static Provider seededKeyProvider() { - return (Provider) SEEDED_KEY_PROVIDER; + public static Provider seededKeyProvider(Class clazz) { + return SessionScope.seededKeySupplier(clazz)::get; } - public static Provider seededKeyProvider(Class clazz) { - return () -> { - throw new IllegalStateException("No instance of " + clazz.getName() + " is bound to the session scope."); - }; + protected boolean isTypeAnnotation(Class annotationType) { + return "org.apache.maven.api.di.Typed".equals(annotationType.getName()) + || "org.eclipse.sisu.Typed".equals(annotationType.getName()) + || "javax.enterprise.inject.Typed".equals(annotationType.getName()) + || "jakarta.enterprise.inject.Typed".equals(annotationType.getName()); } } diff --git a/maven-core/src/test/java/org/apache/maven/di/DiTest.java b/maven-core/src/test/java/org/apache/maven/di/DiTest.java new file mode 100644 index 000000000000..62b84bca3b6d --- /dev/null +++ b/maven-core/src/test/java/org/apache/maven/di/DiTest.java @@ -0,0 +1,271 @@ +/* + * 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.di; + +import javax.inject.Named; +import javax.inject.Singleton; + +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import com.google.inject.AbstractModule; +import com.google.inject.Binding; +import com.google.inject.Injector; +import com.google.inject.TypeLiteral; +import org.apache.maven.api.model.Model; +import org.apache.maven.api.services.Source; +import org.apache.maven.api.spi.ModelParser; +import org.apache.maven.api.spi.ModelParserException; +import org.apache.maven.internal.impl.SisuDiBridgeModule; +import org.codehaus.plexus.DefaultContainerConfiguration; +import org.codehaus.plexus.DefaultPlexusContainer; +import org.codehaus.plexus.PlexusContainer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIf; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class DiTest { + + // return true to run the test + static boolean testShouldNotHaveDuplicates() { + return true; + } + + @Nested + class DiTest1 { + + PlexusContainer container; + + @BeforeEach + void setup() throws Exception { + container = new DefaultPlexusContainer( + new DefaultContainerConfiguration(), + new AbstractModule() { + @Override + protected void configure() { + bind(ModelParser.class).to(TestModelParser.class); + } + }, + new SisuDiBridgeModule(false)); + } + + @Test + void testPlexus() throws Exception { + List parsers = container.lookupList(ModelParser.class); + assertNotNull(parsers); + assertEquals(1, parsers.size()); + Map parsersMap = container.lookupMap(ModelParser.class); + assertNotNull(parsersMap); + assertEquals(1, parsersMap.size()); + } + + @Test + void testGuice() throws Exception { + List> parsers = + container.lookup(Injector.class).findBindingsByType(TypeLiteral.get(ModelParser.class)); + assertNotNull(parsers); + assertEquals(1, parsers.size()); + } + + @Test + void testDI() throws Exception { + DiInjected diInjected = new DiInjected(); + container.lookup(org.apache.maven.di.Injector.class).injectInstance(diInjected); + assertNotNull(diInjected.parser); + assertNotNull(diInjected.parsers); + assertEquals(1, diInjected.parsers.size()); + assertNotNull(diInjected.parsersMap); + assertEquals(1, diInjected.parsersMap.size()); + } + + static class DiInjected { + @org.apache.maven.api.di.Inject + ModelParser parser; + + @org.apache.maven.api.di.Inject + List parsers; + + @org.apache.maven.api.di.Inject + Map parsersMap; + } + + @Named + @Singleton + static class TestModelParser implements ModelParser { + @Override + public Optional locate(Path dir) { + return Optional.empty(); + } + + @Override + public Model parse(Source source, Map options) throws ModelParserException { + return null; + } + } + } + + @Nested + class DiTest2 { + + PlexusContainer container; + + @BeforeEach + void setup() throws Exception { + container = new DefaultPlexusContainer(new DefaultContainerConfiguration(), new SisuDiBridgeModule(false) { + @Override + protected void configure() { + super.configure(); + injector.bindImplicit(TestModelParser.class); + } + }); + } + + @Test + void testPlexus() throws Exception { + List parsers = container.lookupList(ModelParser.class); + assertNotNull(parsers); + assertEquals(1, parsers.size()); + Map parsersMap = container.lookupMap(ModelParser.class); + assertNotNull(parsersMap); + assertEquals(1, parsersMap.size()); + } + + @Test + void testGuice() throws Exception { + List> parsers2 = + container.lookup(Injector.class).findBindingsByType(TypeLiteral.get(ModelParser.class)); + assertNotNull(parsers2); + assertEquals(1, parsers2.size()); + } + + @Test + @EnabledIf("org.apache.maven.di.DiTest#testShouldNotHaveDuplicates") + void testDI() throws Exception { + DiInjected diInjected = new DiInjected(); + container.lookup(org.apache.maven.di.Injector.class).injectInstance(diInjected); + assertNotNull(diInjected.parser); + assertNotNull(diInjected.parsers); + assertEquals(1, diInjected.parsers.size()); + assertNotNull(diInjected.parsersMap); + assertEquals(1, diInjected.parsersMap.size()); + } + + static class DiInjected { + @org.apache.maven.api.di.Inject + ModelParser parser; + + @org.apache.maven.api.di.Inject + List parsers; + + @org.apache.maven.api.di.Inject + Map parsersMap; + } + + @org.apache.maven.api.di.Named + @org.apache.maven.api.di.Singleton + static class TestModelParser implements ModelParser { + @Override + public Optional locate(Path dir) { + return Optional.empty(); + } + + @Override + public Model parse(Source source, Map options) throws ModelParserException { + return null; + } + } + } + + @Nested + class DiTest3 { + + PlexusContainer container; + + @BeforeEach + void setup() throws Exception { + container = new DefaultPlexusContainer(new DefaultContainerConfiguration(), new SisuDiBridgeModule(false) { + @Override + protected void configure() { + super.configure(); + injector.bindImplicit(TestModelParser.class); + } + }); + } + + @Test + void testPlexus() throws Exception { + List parsers = container.lookupList(ModelParser.class); + assertNotNull(parsers); + assertEquals(1, parsers.size()); + Map parsersMap = container.lookupMap(ModelParser.class); + assertNotNull(parsersMap); + assertEquals(1, parsersMap.size()); + } + + @Test + void testGuice() throws Exception { + List> parsers = + container.lookup(Injector.class).findBindingsByType(TypeLiteral.get(ModelParser.class)); + assertNotNull(parsers); + assertEquals(1, parsers.size()); + } + + @Test + @EnabledIf("org.apache.maven.di.DiTest#testShouldNotHaveDuplicates") + void testDI() throws Exception { + DiInjected diInjected = new DiInjected(); + container.lookup(org.apache.maven.di.Injector.class).injectInstance(diInjected); + assertNotNull(diInjected.parser); + assertNotNull(diInjected.parsers); + assertEquals(1, diInjected.parsers.size()); + assertNotNull(diInjected.parsersMap); + assertEquals(1, diInjected.parsersMap.size()); + } + + static class DiInjected { + @org.apache.maven.api.di.Inject + ModelParser parser; + + @org.apache.maven.api.di.Inject + List parsers; + + @org.apache.maven.api.di.Inject + Map parsersMap; + } + + @org.apache.maven.api.di.Named + static class TestModelParser implements ModelParser { + @Override + public Optional locate(Path dir) { + return Optional.empty(); + } + + @Override + public Model parse(Source source, Map options) throws ModelParserException { + return null; + } + } + } +} diff --git a/maven-core/src/test/java/org/apache/maven/execution/scope/internal/MojoExecutionScopeTest.java b/maven-core/src/test/java/org/apache/maven/execution/scope/internal/MojoExecutionScopeTest.java index 5e53789a47f1..09af78fb6e58 100644 --- a/maven-core/src/test/java/org/apache/maven/execution/scope/internal/MojoExecutionScopeTest.java +++ b/maven-core/src/test/java/org/apache/maven/execution/scope/internal/MojoExecutionScopeTest.java @@ -39,15 +39,15 @@ void testNestedEnter() throws Exception { Object o1 = new Object(); scope.seed(Object.class, o1); - assertSame(o1, scope.scope(Key.get(Object.class), null).get()); + assertSame(o1, scope.scope(Key.get(Object.class), () -> null).get()); scope.enter(); Object o2 = new Object(); scope.seed(Object.class, o2); - assertSame(o2, scope.scope(Key.get(Object.class), null).get()); + assertSame(o2, scope.scope(Key.get(Object.class), () -> null).get()); scope.exit(); - assertSame(o1, scope.scope(Key.get(Object.class), null).get()); + assertSame(o1, scope.scope(Key.get(Object.class), () -> null).get()); scope.exit(); diff --git a/maven-core/src/test/java/org/apache/maven/graph/DefaultProjectDependencyGraphTest.java b/maven-core/src/test/java/org/apache/maven/graph/DefaultProjectDependencyGraphTest.java index 9d68de019004..fab4c1cfb134 100644 --- a/maven-core/src/test/java/org/apache/maven/graph/DefaultProjectDependencyGraphTest.java +++ b/maven-core/src/test/java/org/apache/maven/graph/DefaultProjectDependencyGraphTest.java @@ -29,6 +29,7 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; /** */ @@ -36,6 +37,10 @@ class DefaultProjectDependencyGraphTest { private final MavenProject aProject = createA(); + private final MavenProject bProject = createProject(Arrays.asList(toDependency(aProject)), "bProject"); + + private final MavenProject cProject = createProject(Arrays.asList(toDependency(bProject)), "cProject"); + private final MavenProject depender1 = createProject(Arrays.asList(toDependency(aProject)), "depender1"); private final MavenProject depender2 = createProject(Arrays.asList(toDependency(aProject)), "depender2"); @@ -47,6 +52,18 @@ class DefaultProjectDependencyGraphTest { private final MavenProject transitiveOnly = createProject(Arrays.asList(toDependency(depender3)), "depender5"); + @Test + void testNonTransitiveFiltering() throws DuplicateProjectException, CycleDetectedException { + ProjectDependencyGraph graph = new FilteredProjectDependencyGraph( + new DefaultProjectDependencyGraph(Arrays.asList(aProject, bProject, cProject)), + Arrays.asList(aProject, cProject)); + final List sortedProjects = graph.getSortedProjects(); + assertEquals(aProject, sortedProjects.get(0)); + assertEquals(cProject, sortedProjects.get(1)); + + assertTrue(graph.getDownstreamProjects(aProject, false).contains(cProject)); + } + @Test void testGetSortedProjects() throws DuplicateProjectException, CycleDetectedException { ProjectDependencyGraph graph = new DefaultProjectDependencyGraph(Arrays.asList(depender1, aProject)); diff --git a/maven-core/src/test/java/org/apache/maven/session/scope/SessionScopeProxyTest.java b/maven-core/src/test/java/org/apache/maven/session/scope/SessionScopeProxyTest.java index 00bc450b14a8..8db44f55d977 100644 --- a/maven-core/src/test/java/org/apache/maven/session/scope/SessionScopeProxyTest.java +++ b/maven-core/src/test/java/org/apache/maven/session/scope/SessionScopeProxyTest.java @@ -22,9 +22,9 @@ import javax.inject.Named; import javax.inject.Singleton; -import com.google.inject.OutOfScopeException; import org.apache.maven.SessionScoped; import org.apache.maven.api.Session; +import org.apache.maven.internal.impl.di.OutOfScopeException; import org.apache.maven.session.scope.internal.SessionScope; import org.codehaus.plexus.PlexusContainer; import org.codehaus.plexus.component.repository.exception.ComponentLookupException; diff --git a/maven-di/src/main/java/org/apache/maven/di/Injector.java b/maven-di/src/main/java/org/apache/maven/di/Injector.java index d4eea9862526..56b2212650d2 100644 --- a/maven-di/src/main/java/org/apache/maven/di/Injector.java +++ b/maven-di/src/main/java/org/apache/maven/di/Injector.java @@ -36,27 +36,29 @@ static Injector create() { } @Nonnull - Injector discover(ClassLoader classLoader); + Injector discover(@Nonnull ClassLoader classLoader); @Nonnull - Injector bindScope(Class scopeAnnotation, Scope scope); + Injector bindScope(@Nonnull Class scopeAnnotation, @Nonnull Scope scope); @Nonnull - Injector bindScope(Class scopeAnnotation, Supplier scope); + Injector bindScope(@Nonnull Class scopeAnnotation, @Nonnull Supplier scope); @Nonnull - Injector bindImplicit(Class cls); + Injector bindImplicit(@Nonnull Class cls); @Nonnull - Injector bindInstance(Class cls, T instance); + Injector bindInstance(@Nonnull Class cls, @Nonnull T instance); // // Bean access // - void injectInstance(T instance); + void injectInstance(@Nonnull T instance); - T getInstance(Class key); + @Nonnull + T getInstance(@Nonnull Class key); - T getInstance(Key key); + @Nonnull + T getInstance(@Nonnull Key key); } diff --git a/maven-di/src/main/java/org/apache/maven/di/Key.java b/maven-di/src/main/java/org/apache/maven/di/Key.java index 3e922bf0a4f5..6f13be353107 100644 --- a/maven-di/src/main/java/org/apache/maven/di/Key.java +++ b/maven-di/src/main/java/org/apache/maven/di/Key.java @@ -31,7 +31,7 @@ * The key defines an identity of a binding. In any DI, a key is usually a type of the object along * with some optional tag to distinguish between bindings which make objects of the same type. *

- * In ActiveJ Inject, a key is also a type token - special abstract class that can store type information + * In Maven Inject, a key is also a type token - special abstract class that can store type information * with the shortest syntax possible in Java. *

* For example, to create a key of type Map<String, List<Integer>>, you can just use diff --git a/maven-di/src/main/java/org/apache/maven/di/Scope.java b/maven-di/src/main/java/org/apache/maven/di/Scope.java index c05334f319f2..5da978b64a8f 100644 --- a/maven-di/src/main/java/org/apache/maven/di/Scope.java +++ b/maven-di/src/main/java/org/apache/maven/di/Scope.java @@ -18,10 +18,34 @@ */ package org.apache.maven.di; -import java.lang.annotation.Annotation; import java.util.function.Supplier; +import org.apache.maven.api.annotations.Experimental; +import org.apache.maven.api.annotations.Nonnull; + +/** + * A {@code Scope} defines how visible instances are when managed by a {@link org.apache.maven.di.Injector}. + * Typically, instances are created with no scope, meaning they don’t retain any state from the + * framework’s perspective: the {@code Injector} generates the instance, injects it into the necessary class, + * and then immediately forgets it. By linking a scope to a specific binding, the created instance can be + * “remembered” and reused for future injections. + *

+ * Instances are associated to a given scope by means of a {@link org.apache.maven.api.di.Scope @Scope} + * annotation, usually put on another annotation. For example, the {@code @Singleton} annotation is used + * to indicate that a given binding should be scoped as a singleton. + *

+ * The following scopes are currently supported: + *

    + *
  • {@link org.apache.maven.api.di.Singleton @Singleton}
  • + *
  • {@link org.apache.maven.api.di.SessionScoped @SessionScoped}
  • + *
  • {@link org.apache.maven.api.di.MojoExecutionScoped @MojoExecutionScoped}
  • + *
+ * + * @since 4.0.0 + */ +@Experimental public interface Scope { - Supplier scope(Key key, Annotation scope, Supplier unscoped); + @Nonnull + Supplier scope(@Nonnull Key key, @Nonnull Supplier unscoped); } diff --git a/maven-di/src/main/java/org/apache/maven/di/impl/DIException.java b/maven-di/src/main/java/org/apache/maven/di/impl/DIException.java index 4aca38c16e97..b5e39bbfe792 100644 --- a/maven-di/src/main/java/org/apache/maven/di/impl/DIException.java +++ b/maven-di/src/main/java/org/apache/maven/di/impl/DIException.java @@ -25,7 +25,7 @@ * (missing or cyclic dependencies, incorrect annotations etc.) or in runtime when * you ask an {@link Injector} for an instance it does not have a {@link Binding binding} for. */ -public final class DIException extends RuntimeException { +public class DIException extends RuntimeException { public DIException(String message) { super(message); } diff --git a/maven-di/src/main/java/org/apache/maven/di/impl/InjectorImpl.java b/maven-di/src/main/java/org/apache/maven/di/impl/InjectorImpl.java index 1e047f55c211..720a095d8b8d 100644 --- a/maven-di/src/main/java/org/apache/maven/di/impl/InjectorImpl.java +++ b/maven-di/src/main/java/org/apache/maven/di/impl/InjectorImpl.java @@ -59,6 +59,7 @@ public class InjectorImpl implements Injector { private final Map, Set>> bindings = new HashMap<>(); private final Map, Supplier> scopes = new HashMap<>(); + private final Set loadedUrls = new HashSet<>(); public InjectorImpl() { bindScope(Singleton.class, new SingletonScope()); @@ -87,12 +88,16 @@ public Injector discover(ClassLoader classLoader) { try { Enumeration enumeration = classLoader.getResources("META-INF/maven/org.apache.maven.api.di.Inject"); while (enumeration.hasMoreElements()) { - try (InputStream is = enumeration.nextElement().openStream(); - BufferedReader reader = new BufferedReader(new InputStreamReader(Objects.requireNonNull(is)))) { - for (String line : - reader.lines().filter(l -> !l.startsWith("#")).toList()) { - Class clazz = classLoader.loadClass(line); - bindImplicit(clazz); + URL url = enumeration.nextElement(); + if (loadedUrls.add(url.toExternalForm())) { + try (InputStream is = url.openStream(); + BufferedReader reader = + new BufferedReader(new InputStreamReader(Objects.requireNonNull(is)))) { + for (String line : + reader.lines().filter(l -> !l.startsWith("#")).toList()) { + Class clazz = classLoader.loadClass(line); + bindImplicit(clazz); + } } } } @@ -195,7 +200,7 @@ public Supplier getCompiledBinding(Dependency dep) { if (res2 != null) { List> list = res2.stream().map(this::compile).collect(Collectors.toList()); //noinspection unchecked - return () -> (Q) list(list); + return () -> (Q) list(list, Supplier::get); } } if (key.getRawType() == Map.class) { @@ -214,7 +219,7 @@ public Supplier getCompiledBinding(Dependency dep) { : null), this::compile)); //noinspection unchecked - return () -> (Q) map(map); + return () -> (Q) map(map, Supplier::get); } } if (dep.optional()) { @@ -241,7 +246,7 @@ protected Supplier compile(Binding binding) { .orElseThrow(() -> new DIException("Scope not bound for annotation " + binding.getScope().annotationType())) .get(); - compiled = scope.scope((Key) binding.getOriginalKey(), binding.getScope(), compiled); + compiled = scope.scope((Key) binding.getOriginalKey(), compiled); } return compiled; } @@ -307,8 +312,8 @@ private static Set> getBoundTypes(Typed typed, Class clazz) { } } - protected Map map(Map> map) { - return new WrappingMap<>(map, Supplier::get); + protected Map map(Map map, Function mapper) { + return new WrappingMap<>(map, mapper); } private static class WrappingMap extends AbstractMap { @@ -349,8 +354,8 @@ public int size() { } } - protected List list(List> bindingList) { - return new WrappingList<>(bindingList, Supplier::get); + protected List list(List bindingList, Function mapper) { + return new WrappingList<>(bindingList, mapper); } private static class WrappingList extends AbstractList { @@ -379,8 +384,7 @@ private static class SingletonScope implements Scope { @SuppressWarnings("unchecked") @Override - public java.util.function.Supplier scope( - Key key, Annotation scope, java.util.function.Supplier unscoped) { + public java.util.function.Supplier scope(Key key, java.util.function.Supplier unscoped) { return (java.util.function.Supplier) cache.computeIfAbsent(key, k -> new java.util.function.Supplier() { volatile T instance; diff --git a/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java b/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java index 28d898c6ad8a..faf3c5235b99 100644 --- a/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java +++ b/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java @@ -79,6 +79,7 @@ import org.apache.maven.cli.transfer.QuietMavenTransferListener; import org.apache.maven.cli.transfer.SimplexTransferListener; import org.apache.maven.cli.transfer.Slf4jMavenTransferListener; +import org.apache.maven.di.Injector; import org.apache.maven.eventspy.internal.EventSpyDispatcher; import org.apache.maven.exception.DefaultExceptionHandler; import org.apache.maven.exception.ExceptionHandler; @@ -95,7 +96,6 @@ import org.apache.maven.execution.scope.internal.MojoExecutionScopeModule; import org.apache.maven.extension.internal.CoreExports; import org.apache.maven.extension.internal.CoreExtensionEntry; -import org.apache.maven.internal.impl.SisuDiBridgeModule; import org.apache.maven.jline.JLineMessageBuilderFactory; import org.apache.maven.jline.MessageUtils; import org.apache.maven.lifecycle.LifecycleExecutionException; @@ -732,10 +732,20 @@ public Object getValue(String expression) { for (CoreExtensionEntry extension : extensions) { container.discoverComponents( extension.getClassRealm(), + new AbstractModule() { + @Override + protected void configure() { + try { + container.lookup(Injector.class).discover(extension.getClassRealm()); + } catch (Throwable e) { + // ignore + e.printStackTrace(); + } + } + }, new SessionScopeModule(container.lookup(SessionScope.class)), new MojoExecutionScopeModule(container.lookup(MojoExecutionScope.class)), new ExtensionConfigurationModule(extension, extensionSource)); - container.lookup(SisuDiBridgeModule.class).loadFromClassLoader(extension.getClassRealm()); } customizeContainer(container);