From 9f92650ea142f71856cb8b5428a1f54d75211094 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Thu, 4 Jun 2026 13:42:21 +0200 Subject: [PATCH 1/4] DependencyCollectionChecker: new session member Introduce DependencyCollectionChecker, that is able to augment and re-collect the graph. By default, there is only NOOP implementation and this PR causes no change in Resolver behaviour, but this leaves ability to Resolver integrator to use this feature. Also, prepare OptionDependencySelector to be parameterized (by default is not) to be able to ignore or collect optional/excluded artifacts. --- ...ractForwardingRepositorySystemSession.java | 6 + .../DefaultRepositorySystemSession.java | 23 ++ .../aether/RepositorySystemSession.java | 18 ++ .../DependencyCollectionChecker.java | 58 ++++ .../collect/DependencyCollectorDelegate.java | 259 ++++++++++-------- .../scope/OptionalDependencySelector.java | 37 ++- .../impl/session/DefaultCloseableSession.java | 10 + .../impl/session/DefaultSessionBuilder.java | 11 + 8 files changed, 297 insertions(+), 125 deletions(-) create mode 100644 maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyCollectionChecker.java diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/AbstractForwardingRepositorySystemSession.java b/maven-resolver-api/src/main/java/org/eclipse/aether/AbstractForwardingRepositorySystemSession.java index 1fd95e305b..09670fdcc9 100644 --- a/maven-resolver-api/src/main/java/org/eclipse/aether/AbstractForwardingRepositorySystemSession.java +++ b/maven-resolver-api/src/main/java/org/eclipse/aether/AbstractForwardingRepositorySystemSession.java @@ -21,6 +21,7 @@ import java.util.Map; import org.eclipse.aether.artifact.ArtifactTypeRegistry; +import org.eclipse.aether.collection.DependencyCollectionChecker; import org.eclipse.aether.collection.DependencyGraphTransformer; import org.eclipse.aether.collection.DependencyManager; import org.eclipse.aether.collection.DependencySelector; @@ -198,6 +199,11 @@ public ScopeManager getScopeManager() { return getSession().getScopeManager(); } + @Override + public DependencyCollectionChecker getDependencyCollectionChecker() { + return getSession().getDependencyCollectionChecker(); + } + @Override public SystemDependencyScope getSystemDependencyScope() { return getSession().getSystemDependencyScope(); diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/DefaultRepositorySystemSession.java b/maven-resolver-api/src/main/java/org/eclipse/aether/DefaultRepositorySystemSession.java index 5de1a5056b..a068ff8b6d 100644 --- a/maven-resolver-api/src/main/java/org/eclipse/aether/DefaultRepositorySystemSession.java +++ b/maven-resolver-api/src/main/java/org/eclipse/aether/DefaultRepositorySystemSession.java @@ -25,6 +25,7 @@ import org.eclipse.aether.artifact.ArtifactType; import org.eclipse.aether.artifact.ArtifactTypeRegistry; +import org.eclipse.aether.collection.DependencyCollectionChecker; import org.eclipse.aether.collection.DependencyGraphTransformer; import org.eclipse.aether.collection.DependencyManager; import org.eclipse.aether.collection.DependencySelector; @@ -128,6 +129,8 @@ public final class DefaultRepositorySystemSession implements RepositorySystemSes private ScopeManager scopeManager; + private DependencyCollectionChecker dependencyCollectionChecker; + private final Function onSessionEndedRegistrar; /** @@ -165,6 +168,7 @@ public DefaultRepositorySystemSession(Function onSessionEnded proxySelector = PassthroughProxySelector.INSTANCE; authenticationSelector = PassthroughAuthenticationSelector.INSTANCE; artifactTypeRegistry = NullArtifactTypeRegistry.INSTANCE; + dependencyCollectionChecker = DependencyCollectionChecker.NOOP; data = new DefaultSessionData(); this.onSessionEndedRegistrar = requireNonNull(onSessionEndedRegistrar, "onSessionEndedRegistrar"); } @@ -206,6 +210,7 @@ public DefaultRepositorySystemSession(RepositorySystemSession session) { setData(session.getData()); setCache(session.getCache()); setScopeManager(session.getScopeManager()); + setDependencyCollectionChecker(session.getDependencyCollectionChecker()); this.onSessionEndedRegistrar = session::addOnSessionEndedHandler; } @@ -832,6 +837,24 @@ public SystemDependencyScope getSystemDependencyScope() { } } + @Override + public DependencyCollectionChecker getDependencyCollectionChecker() { + return dependencyCollectionChecker; + } + + /** + * Sets the dependency collection checker, may not be {@code null}. + * + * @param dependencyCollectionChecker The dependency collection checker, never {@code null}. + * @return The session for chaining, never {@code null}. + * @since 2.0.19 + */ + public DefaultRepositorySystemSession setDependencyCollectionChecker( + DependencyCollectionChecker dependencyCollectionChecker) { + this.dependencyCollectionChecker = requireNonNull(dependencyCollectionChecker); + return this; + } + /** * Registers onSessionEnded handler, if able to. * diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/RepositorySystemSession.java b/maven-resolver-api/src/main/java/org/eclipse/aether/RepositorySystemSession.java index 0bf8dc56db..80c1c0a494 100644 --- a/maven-resolver-api/src/main/java/org/eclipse/aether/RepositorySystemSession.java +++ b/maven-resolver-api/src/main/java/org/eclipse/aether/RepositorySystemSession.java @@ -25,6 +25,7 @@ import java.util.function.Supplier; import org.eclipse.aether.artifact.ArtifactTypeRegistry; +import org.eclipse.aether.collection.DependencyCollectionChecker; import org.eclipse.aether.collection.DependencyGraphTransformer; import org.eclipse.aether.collection.DependencyManager; import org.eclipse.aether.collection.DependencySelector; @@ -405,6 +406,15 @@ interface SessionBuilder { */ SessionBuilder setScopeManager(ScopeManager scopeManager); + /** + * Sets the dependency collection checker, never {@code null}. + * + * @param dependencyCollectionChecker The checker instance, may not be {@code null}. + * @return The session for chaining, never {@code null}. + * @since 2.0.19 + */ + SessionBuilder setDependencyCollectionChecker(DependencyCollectionChecker dependencyCollectionChecker); + /** * Adds on session ended handler to be immediately registered when this builder creates session. * @@ -782,6 +792,14 @@ interface SessionBuilder { */ SystemDependencyScope getSystemDependencyScope(); + /** + * Returns the dependency collector checker, never {@code null}. + * + * @return The effective {@link DependencyCollectionChecker} instance, never {@code null}. + * @since 2.0.19 + */ + DependencyCollectionChecker getDependencyCollectionChecker(); + /** * Registers a handler to execute when this session closed. *

diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyCollectionChecker.java b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyCollectionChecker.java new file mode 100644 index 0000000000..931ecde480 --- /dev/null +++ b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyCollectionChecker.java @@ -0,0 +1,58 @@ +/* + * 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.eclipse.aether.collection; + +import org.eclipse.aether.RepositorySystemSession; + +/** + * Dependency collector checker. It is able to check dependency collection result, deem it "satisfiable" or + * augment collection and re-execute it. + * + * @since 2.0.19 + */ +public interface DependencyCollectionChecker { + /** + * A default "no op" implementation. + */ + DependencyCollectionChecker NOOP = new DependencyCollectionChecker() {}; + + /** + * Config property for collector checker suppression. Presence of this key will suppress collection checking. + * This key is not meant for users, but to programmatically signal collection suppression. + */ + String COLLECTOR_CHECKER_SUPPRESSED = "aether.dependencyCollector.checker.suppressed"; + + /** + * Prepares for dependency collection. + */ + default RepositorySystemSession prepare(RepositorySystemSession session, CollectRequest request) { + return session; + } + + /** + * Performs checks on finished dependency collection. It should return {@code true} if the collection was deemed + * "satisfactory". If should return {@code false} only, if collection was not satisfactory, and checker + * was able to modify resolution parameters (to not repeat same work). In other cases (not satisfactory + * but no param change would help) it should throw {@link DependencyCollectionException}. + */ + default boolean isSatisfactory(RepositorySystemSession session, CollectRequest request, CollectResult result) + throws DependencyCollectionException { + return true; + } +} diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DependencyCollectorDelegate.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DependencyCollectorDelegate.java index 55b9cabfe1..40693f9a66 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DependencyCollectorDelegate.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DependencyCollectorDelegate.java @@ -25,6 +25,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import org.eclipse.aether.DefaultRepositorySystemSession; import org.eclipse.aether.RepositoryException; @@ -33,6 +34,7 @@ import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.collection.CollectRequest; import org.eclipse.aether.collection.CollectResult; +import org.eclipse.aether.collection.DependencyCollectionChecker; import org.eclipse.aether.collection.DependencyCollectionException; import org.eclipse.aether.collection.DependencyGraphTransformer; import org.eclipse.aether.collection.DependencyTraverser; @@ -121,153 +123,168 @@ protected DependencyCollectorDelegate( @SuppressWarnings("checkstyle:methodlength") @Override - public final CollectResult collectDependencies(RepositorySystemSession session, CollectRequest request) + public final CollectResult collectDependencies( + final RepositorySystemSession originalSession, final CollectRequest request) throws DependencyCollectionException { - requireNonNull(session, "session cannot be null"); + requireNonNull(originalSession, "session cannot be null"); requireNonNull(request, "request cannot be null"); - InternalScopeManager scopeManager = (InternalScopeManager) session.getScopeManager(); - session = setUpSession(session, request, scopeManager); - - RequestTrace trace = RequestTrace.newChild(request.getTrace(), request); - - CollectResult result = new CollectResult(request); - - DependencyTraverser depTraverser = session.getDependencyTraverser(); - VersionFilter verFilter = session.getVersionFilter(); - - Dependency root = request.getRoot(); - List repositories = request.getRepositories(); - List dependencies = request.getDependencies(); - List managedDependencies = request.getManagedDependencies(); - - Map stats = new LinkedHashMap<>(); - long time1 = System.nanoTime(); - - DefaultDependencyNode node; - if (root != null) { - List versions; - VersionRangeResult rangeResult; - try { - VersionRangeRequest rangeRequest = new VersionRangeRequest( - root.getArtifact(), request.getRepositories(), request.getRequestContext()); - rangeRequest.setTrace(trace); - rangeResult = versionRangeResolver.resolveVersionRange(session, rangeRequest); - versions = filterVersions(root, rangeResult, verFilter, new DefaultVersionFilterContext(session)); - } catch (VersionRangeResolutionException e) { - result.addException(e); - throw new DependencyCollectionException(result, e.getMessage()); - } - - Version version = versions.get(versions.size() - 1); - root = root.setArtifact(root.getArtifact().setVersion(version.toString())); + final InternalScopeManager scopeManager = (InternalScopeManager) originalSession.getScopeManager(); + final RepositorySystemSession setUpSession = setUpSession(originalSession, request, scopeManager); + final DependencyCollectionChecker dependencyCollectionChecker = + originalSession.getDependencyCollectionChecker(); + + final RequestTrace trace = RequestTrace.newChild(request.getTrace(), request); + + final Map stats = new LinkedHashMap<>(); + final AtomicInteger runs = new AtomicInteger(0); + + CollectResult result = null; + + boolean finished = false; + while (!finished) { + final long time1 = System.nanoTime(); + runs.incrementAndGet(); + + RepositorySystemSession session = dependencyCollectionChecker.prepare(setUpSession, request); + final DependencyTraverser depTraverser = session.getDependencyTraverser(); + final VersionFilter verFilter = session.getVersionFilter(); + + Dependency root = request.getRoot(); + List repositories = request.getRepositories(); + List dependencies = request.getDependencies(); + List managedDependencies = request.getManagedDependencies(); + + result = new CollectResult(request); + DefaultDependencyNode node; + if (root != null) { + List versions; + VersionRangeResult rangeResult; + try { + VersionRangeRequest rangeRequest = new VersionRangeRequest( + root.getArtifact(), request.getRepositories(), request.getRequestContext()); + rangeRequest.setTrace(trace); + rangeResult = versionRangeResolver.resolveVersionRange(session, rangeRequest); + versions = filterVersions(root, rangeResult, verFilter, new DefaultVersionFilterContext(session)); + } catch (VersionRangeResolutionException e) { + result.addException(e); + throw new DependencyCollectionException(result, e.getMessage()); + } - ArtifactDescriptorResult descriptorResult; - try { - ArtifactDescriptorRequest descriptorRequest = new ArtifactDescriptorRequest(); - descriptorRequest.setArtifact(root.getArtifact()); - descriptorRequest.setRepositories(request.getRepositories()); - descriptorRequest.setRequestContext(request.getRequestContext()); - descriptorRequest.setTrace(trace); - if (isLackingDescriptor(session, root.getArtifact())) { - descriptorResult = new ArtifactDescriptorResult(descriptorRequest); - } else { - descriptorResult = descriptorReader.readArtifactDescriptor(session, descriptorRequest); - for (ArtifactDecorator decorator : - Utils.getArtifactDecorators(session, artifactDecoratorFactories)) { - descriptorResult.setArtifact(decorator.decorateArtifact(descriptorResult)); + Version version = versions.get(versions.size() - 1); + root = root.setArtifact(root.getArtifact().setVersion(version.toString())); + + ArtifactDescriptorResult descriptorResult; + try { + ArtifactDescriptorRequest descriptorRequest = new ArtifactDescriptorRequest(); + descriptorRequest.setArtifact(root.getArtifact()); + descriptorRequest.setRepositories(request.getRepositories()); + descriptorRequest.setRequestContext(request.getRequestContext()); + descriptorRequest.setTrace(trace); + if (isLackingDescriptor(session, root.getArtifact())) { + descriptorResult = new ArtifactDescriptorResult(descriptorRequest); + } else { + descriptorResult = descriptorReader.readArtifactDescriptor(session, descriptorRequest); + for (ArtifactDecorator decorator : + Utils.getArtifactDecorators(session, artifactDecoratorFactories)) { + descriptorResult.setArtifact(decorator.decorateArtifact(descriptorResult)); + } } + } catch (ArtifactDescriptorException e) { + result.addException(e); + throw new DependencyCollectionException(result, e.getMessage()); } - } catch (ArtifactDescriptorException e) { - result.addException(e); - throw new DependencyCollectionException(result, e.getMessage()); - } - root = root.setArtifact(descriptorResult.getArtifact()); + root = root.setArtifact(descriptorResult.getArtifact()); - if (!session.isIgnoreArtifactDescriptorRepositories()) { - repositories = remoteRepositoryManager.aggregateRepositories( - session, repositories, descriptorResult.getRepositories(), true); + if (!session.isIgnoreArtifactDescriptorRepositories()) { + repositories = remoteRepositoryManager.aggregateRepositories( + session, repositories, descriptorResult.getRepositories(), true); + } + dependencies = mergeDeps(dependencies, descriptorResult.getDependencies()); + managedDependencies = mergeDeps(managedDependencies, descriptorResult.getManagedDependencies()); + + node = new DefaultDependencyNode(root); + node.setRequestContext(request.getRequestContext()); + node.setRelocations(descriptorResult.getRelocations()); + node.setVersionConstraint(rangeResult.getVersionConstraint()); + node.setVersion(version); + node.setAliases(descriptorResult.getAliases()); + node.setRepositories(request.getRepositories()); + } else { + node = new DefaultDependencyNode(request.getRootArtifact()); + node.setRequestContext(request.getRequestContext()); + node.setRepositories(request.getRepositories()); } - dependencies = mergeDeps(dependencies, descriptorResult.getDependencies()); - managedDependencies = mergeDeps(managedDependencies, descriptorResult.getManagedDependencies()); - - node = new DefaultDependencyNode(root); - node.setRequestContext(request.getRequestContext()); - node.setRelocations(descriptorResult.getRelocations()); - node.setVersionConstraint(rangeResult.getVersionConstraint()); - node.setVersion(version); - node.setAliases(descriptorResult.getAliases()); - node.setRepositories(request.getRepositories()); - } else { - node = new DefaultDependencyNode(request.getRootArtifact()); - node.setRequestContext(request.getRequestContext()); - node.setRepositories(request.getRepositories()); - } - result.setRoot(node); + result.setRoot(node); - boolean traverse = root == null || depTraverser == null || depTraverser.traverseDependency(root); - String errorPath = null; - if (traverse && !dependencies.isEmpty()) { - DataPool pool = new DataPool(session); + boolean traverse = root == null || depTraverser == null || depTraverser.traverseDependency(root); + String errorPath = null; + if (traverse && !dependencies.isEmpty()) { + DataPool pool = new DataPool(session); - DefaultDependencyCollectionContext context = new DefaultDependencyCollectionContext( - session, request.getRootArtifact(), root, managedDependencies); + DefaultDependencyCollectionContext context = new DefaultDependencyCollectionContext( + session, request.getRootArtifact(), root, managedDependencies); - DefaultVersionFilterContext versionContext = new DefaultVersionFilterContext(session); + DefaultVersionFilterContext versionContext = new DefaultVersionFilterContext(session); - Results results = new Results(result, session); + Results results = new Results(result, session); - doCollectDependencies( - session, - trace, - pool, - context, - versionContext, - request, - node, - repositories, - dependencies, - managedDependencies, - results); + doCollectDependencies( + session, + trace, + pool, + context, + versionContext, + request, + node, + repositories, + dependencies, + managedDependencies, + results); - errorPath = results.getErrorPath(); - } + errorPath = results.getErrorPath(); + } - long time2 = System.nanoTime(); + final long time2 = System.nanoTime(); + + DependencyGraphTransformer transformer = session.getDependencyGraphTransformer(); + if (transformer != null) { + try { + DefaultDependencyGraphTransformationContext context = + new DefaultDependencyGraphTransformationContext(session); + context.put(TransformationContextKeys.STATS, stats); + result.setRoot(transformer.transformGraph(node, context)); + } catch (RepositoryException e) { + result.addException(e); + } + } - DependencyGraphTransformer transformer = session.getDependencyGraphTransformer(); - if (transformer != null) { - try { - DefaultDependencyGraphTransformationContext context = - new DefaultDependencyGraphTransformationContext(session); - context.put(TransformationContextKeys.STATS, stats); - result.setRoot(transformer.transformGraph(node, context)); - } catch (RepositoryException e) { - result.addException(e); + if (errorPath != null) { + throw new DependencyCollectionException(result, "Failed to collect dependencies at " + errorPath); + } + if (!result.getExceptions().isEmpty()) { + throw new DependencyCollectionException(result); } - } - long time3 = System.nanoTime(); - if (logger.isDebugEnabled()) { + if (request.getResolutionScope() != null) { + result = scopeManager.postProcess(session, request.getResolutionScope(), result); + } + + long time3 = System.nanoTime(); stats.put(getClass().getSimpleName() + ".collectTime", time2 - time1); stats.put(getClass().getSimpleName() + ".transformTime", time3 - time2); - logger.debug("Dependency collection stats {}", stats); - } - if (errorPath != null) { - throw new DependencyCollectionException(result, "Failed to collect dependencies at " + errorPath); - } - if (!result.getExceptions().isEmpty()) { - throw new DependencyCollectionException(result); + finished = dependencyCollectionChecker.isSatisfactory(session, request, result); } - if (request.getResolutionScope() != null) { - return scopeManager.postProcess(session, request.getResolutionScope(), result); - } else { - return result; + stats.put(getClass().getSimpleName() + ".runs", runs.get()); + if (logger.isDebugEnabled()) { + logger.debug("Dependency collection stats {}", stats); } + + return result; } /** diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/scope/OptionalDependencySelector.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/scope/OptionalDependencySelector.java index 7c0c324f07..7ac978dadd 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/scope/OptionalDependencySelector.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/scope/OptionalDependencySelector.java @@ -19,10 +19,12 @@ package org.eclipse.aether.internal.impl.scope; import java.util.Objects; +import java.util.Set; import org.eclipse.aether.collection.DependencyCollectionContext; import org.eclipse.aether.collection.DependencySelector; import org.eclipse.aether.graph.Dependency; +import org.eclipse.aether.util.artifact.ArtifactIdUtils; import static java.util.Objects.requireNonNull; @@ -35,6 +37,9 @@ * @see Dependency#isOptional() */ public final class OptionalDependencySelector implements DependencySelector { + public static final String IGNORED_KEYS = OptionalDependencySelector.class.getName() + ".ignored"; + public static final String UNSELECTED_KEYS = OptionalDependencySelector.class.getName() + ".unselected"; + /** * Excludes optional dependencies always (from root). */ @@ -56,29 +61,53 @@ public static OptionalDependencySelector from(int applyFrom) { if (applyFrom < 1) { throw new IllegalArgumentException("applyFrom must be non-zero and positive"); } - return new OptionalDependencySelector(Objects.hash(applyFrom), 0, applyFrom); + return new OptionalDependencySelector(Objects.hash(applyFrom), 0, applyFrom, null, null); } private final int seed; private final int depth; private final int applyFrom; + private final Set ignoredKeys; + private final Set unselectedKeys; - private OptionalDependencySelector(int seed, int depth, int applyFrom) { + private OptionalDependencySelector( + int seed, int depth, int applyFrom, Set ignoredKeys, Set unselectedKeys) { this.seed = seed; this.depth = depth; this.applyFrom = applyFrom; + this.ignoredKeys = ignoredKeys; // nullable + this.unselectedKeys = unselectedKeys; // nullable } @Override public boolean selectDependency(Dependency dependency) { requireNonNull(dependency, "dependency cannot be null"); - return depth < applyFrom || !dependency.isOptional(); + String key = null; + if (ignoredKeys != null || unselectedKeys != null) { + key = ArtifactIdUtils.toId(dependency.getArtifact()); + } + if (ignoredKeys != null) { + if (ignoredKeys.contains(key)) { + return true; + } + } + boolean result = depth < applyFrom || !dependency.isOptional(); + if (!result && unselectedKeys != null) { + unselectedKeys.add(key); + } + return result; } @Override + @SuppressWarnings("unchecked") public DependencySelector deriveChildSelector(DependencyCollectionContext context) { requireNonNull(context, "context cannot be null"); - return new OptionalDependencySelector(seed, depth + 1, applyFrom); + return new OptionalDependencySelector( + seed, + depth + 1, + applyFrom, + (Set) context.getSession().getData().get(IGNORED_KEYS), + (Set) context.getSession().getData().get(UNSELECTED_KEYS)); } @Override diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/session/DefaultCloseableSession.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/session/DefaultCloseableSession.java index ce9494c39e..aaba08a092 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/session/DefaultCloseableSession.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/session/DefaultCloseableSession.java @@ -31,6 +31,7 @@ import org.eclipse.aether.RepositorySystemSession.CloseableSession; import org.eclipse.aether.SessionData; import org.eclipse.aether.artifact.ArtifactTypeRegistry; +import org.eclipse.aether.collection.DependencyCollectionChecker; import org.eclipse.aether.collection.DependencyGraphTransformer; import org.eclipse.aether.collection.DependencyManager; import org.eclipse.aether.collection.DependencySelector; @@ -113,6 +114,8 @@ public final class DefaultCloseableSession implements CloseableSession { private final ScopeManager scopeManager; + private final DependencyCollectionChecker dependencyCollectionChecker; + private final RepositorySystem repositorySystem; private final RepositorySystemLifecycle repositorySystemLifecycle; @@ -147,6 +150,7 @@ public DefaultCloseableSession( SessionData data, RepositoryCache cache, ScopeManager scopeManager, + DependencyCollectionChecker dependencyCollectionChecker, List onSessionEndedHandlers, RepositorySystem repositorySystem, RepositorySystemLifecycle repositorySystemLifecycle) { @@ -177,6 +181,7 @@ public DefaultCloseableSession( this.data = requireNonNull(data); this.cache = cache; this.scopeManager = scopeManager; + this.dependencyCollectionChecker = requireNonNull(dependencyCollectionChecker); this.repositorySystem = requireNonNull(repositorySystem); this.repositorySystemLifecycle = requireNonNull(repositorySystemLifecycle); @@ -343,6 +348,11 @@ public ScopeManager getScopeManager() { return scopeManager; } + @Override + public DependencyCollectionChecker getDependencyCollectionChecker() { + return dependencyCollectionChecker; + } + @Override public SystemDependencyScope getSystemDependencyScope() { if (scopeManager != null) { diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/session/DefaultSessionBuilder.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/session/DefaultSessionBuilder.java index 602f45b44f..7ad2fe6452 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/session/DefaultSessionBuilder.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/session/DefaultSessionBuilder.java @@ -35,6 +35,7 @@ import org.eclipse.aether.RepositorySystemSession.SessionBuilder; import org.eclipse.aether.SessionData; import org.eclipse.aether.artifact.ArtifactTypeRegistry; +import org.eclipse.aether.collection.DependencyCollectionChecker; import org.eclipse.aether.collection.DependencyGraphTransformer; import org.eclipse.aether.collection.DependencyManager; import org.eclipse.aether.collection.DependencySelector; @@ -133,6 +134,8 @@ public final class DefaultSessionBuilder implements SessionBuilder { private ScopeManager scopeManager; + private DependencyCollectionChecker dependencyCollectionChecker = DependencyCollectionChecker.NOOP; + private final ArrayList onSessionCloseHandlers = new ArrayList<>(); /** @@ -363,6 +366,13 @@ public DefaultSessionBuilder setScopeManager(ScopeManager scopeManager) { return this; } + @Override + public SessionBuilder setDependencyCollectionChecker(DependencyCollectionChecker dependencyCollectionChecker) { + requireNonNull(dependencyCollectionChecker, "null dependencyCollectionChecker"); + this.dependencyCollectionChecker = dependencyCollectionChecker; + return null; + } + @Override public SessionBuilder addOnSessionEndedHandler(Runnable handler) { requireNonNull(handler, "null handler"); @@ -485,6 +495,7 @@ public CloseableSession build() { sessionDataSupplier.get(), repositoryCacheSupplier.get(), scopeManager, + dependencyCollectionChecker, onSessionCloseHandlers, repositorySystem, repositorySystemLifecycle); From c5415bd02f8dee82f1d101763f900b6e88c4b1d2 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Thu, 4 Jun 2026 13:46:20 +0200 Subject: [PATCH 2/4] Make it Collection as we do not care --- .../impl/scope/OptionalDependencySelector.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/scope/OptionalDependencySelector.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/scope/OptionalDependencySelector.java index 7ac978dadd..aea51cf2a6 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/scope/OptionalDependencySelector.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/scope/OptionalDependencySelector.java @@ -18,8 +18,8 @@ */ package org.eclipse.aether.internal.impl.scope; +import java.util.Collection; import java.util.Objects; -import java.util.Set; import org.eclipse.aether.collection.DependencyCollectionContext; import org.eclipse.aether.collection.DependencySelector; @@ -67,11 +67,11 @@ public static OptionalDependencySelector from(int applyFrom) { private final int seed; private final int depth; private final int applyFrom; - private final Set ignoredKeys; - private final Set unselectedKeys; + private final Collection ignoredKeys; + private final Collection unselectedKeys; private OptionalDependencySelector( - int seed, int depth, int applyFrom, Set ignoredKeys, Set unselectedKeys) { + int seed, int depth, int applyFrom, Collection ignoredKeys, Collection unselectedKeys) { this.seed = seed; this.depth = depth; this.applyFrom = applyFrom; @@ -106,8 +106,8 @@ public DependencySelector deriveChildSelector(DependencyCollectionContext contex seed, depth + 1, applyFrom, - (Set) context.getSession().getData().get(IGNORED_KEYS), - (Set) context.getSession().getData().get(UNSELECTED_KEYS)); + (Collection) context.getSession().getData().get(IGNORED_KEYS), + (Collection) context.getSession().getData().get(UNSELECTED_KEYS)); } @Override From 7b58067d1c964e5739c8a88baf7f16015be13464 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Thu, 4 Jun 2026 14:10:05 +0200 Subject: [PATCH 3/4] Add limit of re-collection attempts And it is resolver itself enforcing it, as a problematic checker may cause endless loops, so prevent it, but let escape hatch to set the maxRuns as config. --- .../collect/DependencyCollectorDelegate.java | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DependencyCollectorDelegate.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DependencyCollectorDelegate.java index 40693f9a66..169c11d3c1 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DependencyCollectorDelegate.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DependencyCollectorDelegate.java @@ -98,6 +98,19 @@ public abstract class DependencyCollectorDelegate implements DependencyCollector public static final int DEFAULT_MAX_CYCLES = 10; + /** + * The allowed runs of collection (and re-collection). By default, there must be at least 1 run, so values smaller + * than 1 are considered configuration errors and will fail dependency collection. + * + * @configurationSource {@link RepositorySystemSession#getConfigProperties()} + * @configurationType {@link java.lang.Integer} + * @configurationDefaultValue {@link #DEFAULT_MAX_RUNS} + * @since 2.0.19 + */ + public static final String CONFIG_PROP_MAX_RUNS = DefaultDependencyCollector.CONFIG_PROPS_PREFIX + "maxRuns"; + + public static final int DEFAULT_MAX_RUNS = 5; + protected final Logger logger = LoggerFactory.getLogger(getClass()); protected final RemoteRepositoryManager remoteRepositoryManager; @@ -138,13 +151,24 @@ public final CollectResult collectDependencies( final Map stats = new LinkedHashMap<>(); final AtomicInteger runs = new AtomicInteger(0); + final int maxRuns = ConfigUtils.getInteger(originalSession, DEFAULT_MAX_RUNS, CONFIG_PROP_MAX_RUNS); + if (maxRuns < 1) { + throw new DependencyCollectionException( + new CollectResult(request), + "Invalid configuration: '" + CONFIG_PROP_MAX_RUNS + + "' configuration must be equal or grater than 1"); + } CollectResult result = null; boolean finished = false; while (!finished) { final long time1 = System.nanoTime(); - runs.incrementAndGet(); + if (runs.incrementAndGet() > maxRuns) { + throw new DependencyCollectionException( + new CollectResult(request), + "Too many collection attempts (bug of used DependencyCollectionChecker?)"); + } RepositorySystemSession session = dependencyCollectionChecker.prepare(setUpSession, request); final DependencyTraverser depTraverser = session.getDependencyTraverser(); From 0699b8805b9e7c7717889e2649cf9b7b09e7a68f Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Thu, 4 Jun 2026 16:24:36 +0200 Subject: [PATCH 4/4] Make it nullable To be aligned with all other "optional" session stuff, once we will make the Optional<>. --- .../eclipse/aether/DefaultRepositorySystemSession.java | 7 +++---- .../org/eclipse/aether/RepositorySystemSession.java | 10 +++++----- .../impl/collect/DependencyCollectorDelegate.java | 4 +++- .../internal/impl/session/DefaultCloseableSession.java | 2 +- .../internal/impl/session/DefaultSessionBuilder.java | 3 +-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/DefaultRepositorySystemSession.java b/maven-resolver-api/src/main/java/org/eclipse/aether/DefaultRepositorySystemSession.java index a068ff8b6d..b2cf0838cf 100644 --- a/maven-resolver-api/src/main/java/org/eclipse/aether/DefaultRepositorySystemSession.java +++ b/maven-resolver-api/src/main/java/org/eclipse/aether/DefaultRepositorySystemSession.java @@ -168,7 +168,6 @@ public DefaultRepositorySystemSession(Function onSessionEnded proxySelector = PassthroughProxySelector.INSTANCE; authenticationSelector = PassthroughAuthenticationSelector.INSTANCE; artifactTypeRegistry = NullArtifactTypeRegistry.INSTANCE; - dependencyCollectionChecker = DependencyCollectionChecker.NOOP; data = new DefaultSessionData(); this.onSessionEndedRegistrar = requireNonNull(onSessionEndedRegistrar, "onSessionEndedRegistrar"); } @@ -843,15 +842,15 @@ public DependencyCollectionChecker getDependencyCollectionChecker() { } /** - * Sets the dependency collection checker, may not be {@code null}. + * Sets the dependency collection checker, may be {@code null}. * - * @param dependencyCollectionChecker The dependency collection checker, never {@code null}. + * @param dependencyCollectionChecker The dependency collection checker, may be {@code null}. * @return The session for chaining, never {@code null}. * @since 2.0.19 */ public DefaultRepositorySystemSession setDependencyCollectionChecker( DependencyCollectionChecker dependencyCollectionChecker) { - this.dependencyCollectionChecker = requireNonNull(dependencyCollectionChecker); + this.dependencyCollectionChecker = dependencyCollectionChecker; return this; } diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/RepositorySystemSession.java b/maven-resolver-api/src/main/java/org/eclipse/aether/RepositorySystemSession.java index 80c1c0a494..6c552bb01d 100644 --- a/maven-resolver-api/src/main/java/org/eclipse/aether/RepositorySystemSession.java +++ b/maven-resolver-api/src/main/java/org/eclipse/aether/RepositorySystemSession.java @@ -407,10 +407,10 @@ interface SessionBuilder { SessionBuilder setScopeManager(ScopeManager scopeManager); /** - * Sets the dependency collection checker, never {@code null}. + * Sets the dependency collection checker, may be {@code null}. * - * @param dependencyCollectionChecker The checker instance, may not be {@code null}. - * @return The session for chaining, never {@code null}. + * @param dependencyCollectionChecker The checker instance, may be {@code null}. + * @return The session for chaining, may be {@code null}. * @since 2.0.19 */ SessionBuilder setDependencyCollectionChecker(DependencyCollectionChecker dependencyCollectionChecker); @@ -793,9 +793,9 @@ interface SessionBuilder { SystemDependencyScope getSystemDependencyScope(); /** - * Returns the dependency collector checker, never {@code null}. + * Returns the dependency collector checker, may be {@code null}. * - * @return The effective {@link DependencyCollectionChecker} instance, never {@code null}. + * @return The effective {@link DependencyCollectionChecker} instance, may be {@code null}. * @since 2.0.19 */ DependencyCollectionChecker getDependencyCollectionChecker(); diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DependencyCollectorDelegate.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DependencyCollectorDelegate.java index 169c11d3c1..a9d36745f3 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DependencyCollectorDelegate.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DependencyCollectorDelegate.java @@ -145,7 +145,9 @@ public final CollectResult collectDependencies( final InternalScopeManager scopeManager = (InternalScopeManager) originalSession.getScopeManager(); final RepositorySystemSession setUpSession = setUpSession(originalSession, request, scopeManager); final DependencyCollectionChecker dependencyCollectionChecker = - originalSession.getDependencyCollectionChecker(); + originalSession.getDependencyCollectionChecker() == null + ? DependencyCollectionChecker.NOOP + : originalSession.getDependencyCollectionChecker(); final RequestTrace trace = RequestTrace.newChild(request.getTrace(), request); diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/session/DefaultCloseableSession.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/session/DefaultCloseableSession.java index aaba08a092..d5b56c8948 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/session/DefaultCloseableSession.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/session/DefaultCloseableSession.java @@ -181,7 +181,7 @@ public DefaultCloseableSession( this.data = requireNonNull(data); this.cache = cache; this.scopeManager = scopeManager; - this.dependencyCollectionChecker = requireNonNull(dependencyCollectionChecker); + this.dependencyCollectionChecker = dependencyCollectionChecker; this.repositorySystem = requireNonNull(repositorySystem); this.repositorySystemLifecycle = requireNonNull(repositorySystemLifecycle); diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/session/DefaultSessionBuilder.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/session/DefaultSessionBuilder.java index 7ad2fe6452..bdf9fc3c9b 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/session/DefaultSessionBuilder.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/session/DefaultSessionBuilder.java @@ -134,7 +134,7 @@ public final class DefaultSessionBuilder implements SessionBuilder { private ScopeManager scopeManager; - private DependencyCollectionChecker dependencyCollectionChecker = DependencyCollectionChecker.NOOP; + private DependencyCollectionChecker dependencyCollectionChecker; private final ArrayList onSessionCloseHandlers = new ArrayList<>(); @@ -368,7 +368,6 @@ public DefaultSessionBuilder setScopeManager(ScopeManager scopeManager) { @Override public SessionBuilder setDependencyCollectionChecker(DependencyCollectionChecker dependencyCollectionChecker) { - requireNonNull(dependencyCollectionChecker, "null dependencyCollectionChecker"); this.dependencyCollectionChecker = dependencyCollectionChecker; return null; }