diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyManagement.java b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyManagement.java index 71bfd5102..177220dce 100644 --- a/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyManagement.java +++ b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyManagement.java @@ -19,6 +19,7 @@ package org.eclipse.aether.collection; import java.util.Collection; +import java.util.EnumMap; import java.util.Map; import org.eclipse.aether.graph.Exclusion; @@ -29,22 +30,46 @@ * @see DependencyManager#manageDependency(org.eclipse.aether.graph.Dependency) */ public final class DependencyManagement { + /** + * Enumeration of manageable attributes, attributes that can be subjected to dependency management. + * + * @since 2.0.17 + */ + public enum Subject { + VERSION, + SCOPE, + OPTIONAL, + EXCLUSIONS, + PROPERTIES + } - private String version; - - private String scope; - - private Boolean optional; - - private Collection exclusions; - - private Map properties; + private final EnumMap managedValues; + private final EnumMap managedEnforced; /** * Creates an empty management update. */ public DependencyManagement() { - // enables default constructor + this.managedValues = new EnumMap<>(Subject.class); + this.managedEnforced = new EnumMap<>(Subject.class); + } + + /** + * Returns {@code true} if passed in subject is managed. + * + * @since 2.0.17 + */ + public boolean isManagedSubject(Subject subject) { + return managedValues.containsKey(subject); + } + + /** + * Returns {@code true} if passed in subject is managed and is enforced. + * + * @since 2.0.17 + */ + public boolean isManagedSubjectEnforced(Subject subject) { + return isManagedSubject(subject) && managedEnforced.getOrDefault(subject, false); } /** @@ -54,7 +79,7 @@ public DependencyManagement() { * remain unchanged. */ public String getVersion() { - return version; + return (String) managedValues.get(Subject.VERSION); } /** @@ -62,9 +87,29 @@ public String getVersion() { * * @param version The new version, may be {@code null} if the version is not managed. * @return This management update for chaining, never {@code null}. + * @deprecated Use {@link #setVersion(String, boolean)} instead. */ + @Deprecated public DependencyManagement setVersion(String version) { - this.version = version; + return setVersion(version, true); + } + + /** + * Sets the new version to apply to the dependency. + * + * @param version The new version, may be {@code null} if the version is not managed. + * @param enforced The enforcement of new value. + * @return This management update for chaining, never {@code null}. + * @since 2.0.17 + */ + public DependencyManagement setVersion(String version, boolean enforced) { + if (version == null) { + this.managedValues.remove(Subject.VERSION); + this.managedEnforced.remove(Subject.VERSION); + } else { + this.managedValues.put(Subject.VERSION, version); + this.managedEnforced.put(Subject.VERSION, enforced); + } return this; } @@ -75,7 +120,7 @@ public DependencyManagement setVersion(String version) { * unchanged. */ public String getScope() { - return scope; + return (String) managedValues.get(Subject.SCOPE); } /** @@ -83,9 +128,29 @@ public String getScope() { * * @param scope The new scope, may be {@code null} if the scope is not managed. * @return This management update for chaining, never {@code null}. + * @deprecated Use {@link #setScope(String, boolean)} instead. */ + @Deprecated public DependencyManagement setScope(String scope) { - this.scope = scope; + return setScope(scope, true); + } + + /** + * Sets the new scope to apply to the dependency. + * + * @param scope The new scope, may be {@code null} if the scope is not managed. + * @param enforced The enforcement of new value. + * @return This management update for chaining, never {@code null}. + * @since 2.0.17 + */ + public DependencyManagement setScope(String scope, boolean enforced) { + if (scope == null) { + this.managedValues.remove(Subject.SCOPE); + this.managedEnforced.remove(Subject.SCOPE); + } else { + this.managedValues.put(Subject.SCOPE, scope); + this.managedEnforced.put(Subject.SCOPE, enforced); + } return this; } @@ -96,7 +161,7 @@ public DependencyManagement setScope(String scope) { * dependency should remain unchanged. */ public Boolean getOptional() { - return optional; + return (Boolean) managedValues.get(Subject.OPTIONAL); } /** @@ -104,9 +169,29 @@ public Boolean getOptional() { * * @param optional The optional flag, may be {@code null} if the flag is not managed. * @return This management update for chaining, never {@code null}. + * @deprecated Use {@link #setOptional(Boolean, boolean)} instead. */ + @Deprecated public DependencyManagement setOptional(Boolean optional) { - this.optional = optional; + return setOptional(optional, true); + } + + /** + * Sets the new optional flag to apply to the dependency. + * + * @param optional The optional flag, may be {@code null} if the flag is not managed. + * @param enforced The enforcement of new value. + * @return This management update for chaining, never {@code null}. + * @since 2.0.17 + */ + public DependencyManagement setOptional(Boolean optional, boolean enforced) { + if (optional == null) { + this.managedValues.remove(Subject.OPTIONAL); + this.managedEnforced.remove(Subject.OPTIONAL); + } else { + this.managedValues.put(Subject.OPTIONAL, optional); + this.managedEnforced.put(Subject.OPTIONAL, enforced); + } return this; } @@ -118,8 +203,9 @@ public DependencyManagement setOptional(Boolean optional) { * @return The new exclusions or {@code null} if the exclusions are not managed and the existing dependency * exclusions should remain unchanged. */ + @SuppressWarnings("unchecked") public Collection getExclusions() { - return exclusions; + return (Collection) managedValues.get(Subject.EXCLUSIONS); } /** @@ -129,9 +215,31 @@ public Collection getExclusions() { * * @param exclusions The new exclusions, may be {@code null} if the exclusions are not managed. * @return This management update for chaining, never {@code null}. + * @deprecated Use {@link #setExclusions(Collection, boolean)} instead. */ + @Deprecated public DependencyManagement setExclusions(Collection exclusions) { - this.exclusions = exclusions; + return setExclusions(exclusions, true); + } + + /** + * Sets the new exclusions to apply to the dependency. Note that this collection denotes the complete set of + * exclusions for the dependency, i.e. the dependency manager controls whether any existing exclusions get merged + * with information from dependency management or overridden by it. + * + * @param exclusions The new exclusions, may be {@code null} if the exclusions are not managed. + * @param enforced The enforcement of new value. + * @return This management update for chaining, never {@code null}. + * @since 2.0.17 + */ + public DependencyManagement setExclusions(Collection exclusions, boolean enforced) { + if (exclusions == null) { + this.managedValues.remove(Subject.EXCLUSIONS); + this.managedEnforced.remove(Subject.EXCLUSIONS); + } else { + this.managedValues.put(Subject.EXCLUSIONS, exclusions); + this.managedEnforced.put(Subject.EXCLUSIONS, enforced); + } return this; } @@ -143,8 +251,9 @@ public DependencyManagement setExclusions(Collection exclusions) { * @return The new artifact properties or {@code null} if the properties are not managed and the existing properties * should remain unchanged. */ + @SuppressWarnings("unchecked") public Map getProperties() { - return properties; + return (Map) managedValues.get(Subject.PROPERTIES); } /** @@ -154,9 +263,31 @@ public Map getProperties() { * * @param properties The new artifact properties, may be {@code null} if the properties are not managed. * @return This management update for chaining, never {@code null}. + * @deprecated Use {@link #setProperties(Map, boolean)} instead. */ + @Deprecated public DependencyManagement setProperties(Map properties) { - this.properties = properties; + return setProperties(properties, true); + } + + /** + * Sets the new properties to apply to the dependency. Note that this map denotes the complete set of properties, + * i.e. the dependency manager controls whether any existing properties get merged with the information from + * dependency management or overridden by it. + * + * @param properties The new artifact properties, may be {@code null} if the properties are not managed. + * @param enforced The enforcement of new value. + * @return This management update for chaining, never {@code null}. + * @since 2.0.17 + */ + public DependencyManagement setProperties(Map properties, boolean enforced) { + if (properties == null) { + this.managedValues.remove(Subject.PROPERTIES); + this.managedEnforced.remove(Subject.PROPERTIES); + } else { + this.managedValues.put(Subject.PROPERTIES, properties); + this.managedEnforced.put(Subject.PROPERTIES, enforced); + } return this; } } diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DefaultDependencyNode.java b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DefaultDependencyNode.java index 3d7c4fde0..2d7cce810 100644 --- a/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DefaultDependencyNode.java +++ b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DefaultDependencyNode.java @@ -21,11 +21,13 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.EnumMap; import java.util.HashMap; import java.util.List; import java.util.Map; import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.collection.DependencyManagement; import org.eclipse.aether.repository.RemoteRepository; import org.eclipse.aether.version.Version; import org.eclipse.aether.version.VersionConstraint; @@ -36,7 +38,6 @@ * A node within a dependency graph. */ public final class DefaultDependencyNode implements DependencyNode { - private List children; private Dependency dependency; @@ -51,7 +52,7 @@ public final class DefaultDependencyNode implements DependencyNode { private Version version; - private byte managedBits; + private Map managedSubjects; private List repositories; @@ -66,13 +67,14 @@ public final class DefaultDependencyNode implements DependencyNode { */ public DefaultDependencyNode(Dependency dependency) { this.dependency = dependency; - artifact = (dependency != null) ? dependency.getArtifact() : null; - children = new ArrayList<>(0); - aliases = Collections.emptyList(); - relocations = Collections.emptyList(); - repositories = Collections.emptyList(); - context = ""; - data = Collections.emptyMap(); + this.artifact = (dependency != null) ? dependency.getArtifact() : null; + this.children = new ArrayList<>(0); + this.aliases = Collections.emptyList(); + this.relocations = Collections.emptyList(); + this.managedSubjects = null; + this.repositories = Collections.emptyList(); + this.context = ""; + this.data = Collections.emptyMap(); } /** @@ -84,12 +86,13 @@ public DefaultDependencyNode(Dependency dependency) { */ public DefaultDependencyNode(Artifact artifact) { this.artifact = artifact; - children = new ArrayList<>(0); - aliases = Collections.emptyList(); - relocations = Collections.emptyList(); - repositories = Collections.emptyList(); - context = ""; - data = Collections.emptyMap(); + this.children = new ArrayList<>(0); + this.aliases = Collections.emptyList(); + this.relocations = Collections.emptyList(); + this.managedSubjects = null; + this.repositories = Collections.emptyList(); + this.context = ""; + this.data = Collections.emptyMap(); } /** @@ -99,12 +102,25 @@ public DefaultDependencyNode(Artifact artifact) { * @param node The node to copy, must not be {@code null}. */ public DefaultDependencyNode(DependencyNode node) { - dependency = node.getDependency(); - artifact = node.getArtifact(); - children = new ArrayList<>(0); + this.dependency = node.getDependency(); + this.artifact = node.getArtifact(); + this.children = new ArrayList<>(0); setAliases(node.getAliases()); setRequestContext(node.getRequestContext()); - setManagedBits(node.getManagedBits()); + + EnumMap managedSubjects = + new EnumMap<>(DependencyManagement.Subject.class); + for (DependencyManagement.Subject subject : DependencyManagement.Subject.values()) { + if (node.isManagedSubject(subject)) { + managedSubjects.put(subject, node.isManagedSubjectEnforced(subject)); + } + } + if (managedSubjects.isEmpty()) { + setManagedSubjects(null); + } else { + setManagedSubjects(managedSubjects); + } + setRelocations(node.getRelocations()); setRepositories(node.getRepositories()); setVersion(node.getVersion()); @@ -113,10 +129,12 @@ public DefaultDependencyNode(DependencyNode node) { setData(data.isEmpty() ? null : new HashMap<>(data)); } + @Override public List getChildren() { return children; } + @Override public void setChildren(List children) { if (children == null) { this.children = new ArrayList<>(0); @@ -125,14 +143,17 @@ public void setChildren(List children) { } } + @Override public Dependency getDependency() { return dependency; } + @Override public Artifact getArtifact() { return artifact; } + @Override public void setArtifact(Artifact artifact) { if (dependency == null) { throw new IllegalStateException("node does not have a dependency"); @@ -141,6 +162,7 @@ public void setArtifact(Artifact artifact) { this.artifact = dependency.getArtifact(); } + @Override public List getRelocations() { return relocations; } @@ -158,6 +180,7 @@ public void setRelocations(List relocations) { } } + @Override public Collection getAliases() { return aliases; } @@ -175,6 +198,7 @@ public void setAliases(Collection aliases) { } } + @Override public VersionConstraint getVersionConstraint() { return versionConstraint; } @@ -188,6 +212,7 @@ public void setVersionConstraint(VersionConstraint versionConstraint) { this.versionConstraint = versionConstraint; } + @Override public Version getVersion() { return version; } @@ -201,6 +226,7 @@ public void setVersion(Version version) { this.version = version; } + @Override public void setScope(String scope) { if (dependency == null) { throw new IllegalStateException("node does not have a dependency"); @@ -208,6 +234,7 @@ public void setScope(String scope) { dependency = dependency.setScope(scope); } + @Override public void setOptional(Boolean optional) { if (dependency == null) { throw new IllegalStateException("node does not have a dependency"); @@ -215,20 +242,67 @@ public void setOptional(Boolean optional) { dependency = dependency.setOptional(optional); } + @Override public int getManagedBits() { - return managedBits; + byte res = 0; + if (isManagedSubject(DependencyManagement.Subject.VERSION)) { + res |= DependencyNode.MANAGED_VERSION; + } + if (isManagedSubject(DependencyManagement.Subject.SCOPE)) { + res |= DependencyNode.MANAGED_SCOPE; + } + if (isManagedSubject(DependencyManagement.Subject.OPTIONAL)) { + res |= DependencyNode.MANAGED_OPTIONAL; + } + if (isManagedSubject(DependencyManagement.Subject.PROPERTIES)) { + res |= DependencyNode.MANAGED_PROPERTIES; + } + if (isManagedSubject(DependencyManagement.Subject.EXCLUSIONS)) { + res |= DependencyNode.MANAGED_EXCLUSIONS; + } + return res; } - /** - * Sets a bit field indicating which attributes of this node were subject to dependency management. - * - * @param managedBits The bit field indicating the managed attributes or {@code 0} if dependency management wasn't - * applied. - */ + @Deprecated public void setManagedBits(int managedBits) { - this.managedBits = (byte) (managedBits & 0x1F); + EnumMap subjects = new EnumMap<>(DependencyManagement.Subject.class); + if ((managedBits & DependencyNode.MANAGED_VERSION) != 0) { + subjects.put(DependencyManagement.Subject.VERSION, true); + } + if ((managedBits & DependencyNode.MANAGED_SCOPE) != 0) { + subjects.put(DependencyManagement.Subject.SCOPE, true); + } + if ((managedBits & DependencyNode.MANAGED_OPTIONAL) != 0) { + subjects.put(DependencyManagement.Subject.OPTIONAL, true); + } + if ((managedBits & DependencyNode.MANAGED_PROPERTIES) != 0) { + subjects.put(DependencyManagement.Subject.PROPERTIES, true); + } + if ((managedBits & DependencyNode.MANAGED_EXCLUSIONS) != 0) { + subjects.put(DependencyManagement.Subject.EXCLUSIONS, true); + } + setManagedSubjects(subjects.isEmpty() ? null : subjects); + } + + public void setManagedSubjects(Map managedSubjects) { + if (managedSubjects == null || managedSubjects.isEmpty()) { + this.managedSubjects = null; + } else { + this.managedSubjects = managedSubjects; + } + } + + @Override + public boolean isManagedSubject(DependencyManagement.Subject subject) { + return managedSubjects != null && managedSubjects.containsKey(subject); + } + + @Override + public boolean isManagedSubjectEnforced(DependencyManagement.Subject subject) { + return isManagedSubject(subject) && managedSubjects.getOrDefault(subject, false); } + @Override public List getRepositories() { return repositories; } @@ -246,18 +320,22 @@ public void setRepositories(List repositories) { } } + @Override public String getRequestContext() { return context; } + @Override public void setRequestContext(String context) { this.context = (context != null) ? context.intern() : ""; } + @Override public Map getData() { return data; } + @Override public void setData(Map data) { if (data == null) { this.data = Collections.emptyMap(); @@ -266,6 +344,7 @@ public void setData(Map data) { } } + @Override public void setData(Object key, Object value) { requireNonNull(key, "key cannot be null"); @@ -285,6 +364,7 @@ public void setData(Object key, Object value) { } } + @Override public boolean accept(DependencyVisitor visitor) { if (Thread.currentThread().isInterrupted()) { throw new RuntimeException(new InterruptedException("Thread interrupted")); diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DependencyNode.java b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DependencyNode.java index 747163985..fb3c46d1b 100644 --- a/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DependencyNode.java +++ b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DependencyNode.java @@ -23,6 +23,7 @@ import java.util.Map; import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.collection.DependencyManagement; import org.eclipse.aether.repository.RemoteRepository; import org.eclipse.aether.version.Version; import org.eclipse.aether.version.VersionConstraint; @@ -38,40 +39,49 @@ * @noextend This interface is not intended to be extended by clients. */ public interface DependencyNode { - /** * A bit flag indicating the dependency version was subject to dependency management * * @see #getManagedBits() + * @deprecated Use {@link #isManagedSubject(DependencyManagement.Subject)} and {@link #isManagedSubjectEnforced(DependencyManagement.Subject)} instead. */ + @Deprecated int MANAGED_VERSION = 0x01; /** * A bit flag indicating the dependency scope was subject to dependency management * * @see #getManagedBits() + * @deprecated Use {@link #isManagedSubject(DependencyManagement.Subject)} and {@link #isManagedSubjectEnforced(DependencyManagement.Subject)} instead. */ + @Deprecated int MANAGED_SCOPE = 0x02; /** * A bit flag indicating the optional flag was subject to dependency management * * @see #getManagedBits() + * @deprecated Use {@link #isManagedSubject(DependencyManagement.Subject)} and {@link #isManagedSubjectEnforced(DependencyManagement.Subject)} instead. */ + @Deprecated int MANAGED_OPTIONAL = 0x04; /** * A bit flag indicating the artifact properties were subject to dependency management * * @see #getManagedBits() + * @deprecated Use {@link #isManagedSubject(DependencyManagement.Subject)} and {@link #isManagedSubjectEnforced(DependencyManagement.Subject)} instead. */ + @Deprecated int MANAGED_PROPERTIES = 0x08; /** * A bit flag indicating the exclusions were subject to dependency management * * @see #getManagedBits() + * @deprecated Use {@link #isManagedSubject(DependencyManagement.Subject)} and {@link #isManagedSubjectEnforced(DependencyManagement.Subject)} instead. */ + @Deprecated int MANAGED_EXCLUSIONS = 0x10; /** @@ -170,9 +180,46 @@ public interface DependencyNode { * @return A bit field containing any of the bits {@link #MANAGED_VERSION}, {@link #MANAGED_SCOPE}, * {@link #MANAGED_OPTIONAL}, {@link #MANAGED_PROPERTIES} and {@link #MANAGED_EXCLUSIONS} if the * corresponding attribute was set via dependency management. + * @deprecated Use {@link #isManagedSubject(DependencyManagement.Subject)} and {@link #isManagedSubjectEnforced(DependencyManagement.Subject)} instead. */ + @Deprecated int getManagedBits(); + /** + * Returns {@code true} if given subject is managed. + * + * @param subject the {@link org.eclipse.aether.collection.DependencyManagement.Subject}, must not be {@code null}. + * @see org.eclipse.aether.collection.DependencyManagement.Subject + * @since 2.0.17 + */ + default boolean isManagedSubject(DependencyManagement.Subject subject) { + switch (subject) { + case VERSION: + return (getManagedBits() & MANAGED_VERSION) != 0; + case SCOPE: + return (getManagedBits() & MANAGED_SCOPE) != 0; + case OPTIONAL: + return (getManagedBits() & MANAGED_OPTIONAL) != 0; + case PROPERTIES: + return (getManagedBits() & MANAGED_PROPERTIES) != 0; + case EXCLUSIONS: + return (getManagedBits() & MANAGED_EXCLUSIONS) != 0; + default: + throw new IllegalArgumentException("Unknown subject: " + subject.name()); + } + } + + /** + * Returns {@code true} if given subject is managed with enforcing modality on this node. + * + * @param subject the {@link org.eclipse.aether.collection.DependencyManagement.Subject}, must not be {@code null}. + * @see org.eclipse.aether.collection.DependencyManagement.Subject + * @since 2.0.17 + */ + default boolean isManagedSubjectEnforced(DependencyManagement.Subject subject) { + return isManagedSubject(subject); + } + /** * Gets the remote repositories from which this node's artifact shall be resolved. * 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 8e715efdd..8cf4fd602 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 @@ -35,6 +35,7 @@ import org.eclipse.aether.collection.CollectResult; import org.eclipse.aether.collection.DependencyCollectionException; import org.eclipse.aether.collection.DependencyGraphTransformer; +import org.eclipse.aether.collection.DependencyManager; import org.eclipse.aether.collection.DependencyTraverser; import org.eclipse.aether.collection.VersionFilter; import org.eclipse.aether.graph.DefaultDependencyNode; @@ -344,8 +345,16 @@ protected static String getId(Artifact a) { return a.getGroupId() + ':' + a.getArtifactId() + ':' + a.getClassifier() + ':' + a.getExtension(); } + protected PremanagedDependency createPremanagedDependency( + DependencyManager depManager, + Dependency dependency, + boolean disableVersionManagement, + boolean premanagedState) { + return PremanagedDependency.create(depManager, dependency, disableVersionManagement, premanagedState); + } + @SuppressWarnings("checkstyle:parameternumber") - protected static DefaultDependencyNode createDependencyNode( + protected DefaultDependencyNode createDependencyNode( List relocations, PremanagedDependency preManaged, VersionRangeResult rangeResult, @@ -365,7 +374,7 @@ protected static DefaultDependencyNode createDependencyNode( return child; } - protected static DefaultDependencyNode createDependencyNode( + protected DefaultDependencyNode createDependencyNodeCycle( List relocations, PremanagedDependency preManaged, VersionRangeResult rangeResult, diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/PremanagedDependency.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/PremanagedDependency.java index c0a6ef3bb..d5dd73eff 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/PremanagedDependency.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/PremanagedDependency.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.EnumMap; import java.util.HashMap; import java.util.Map; @@ -29,7 +30,6 @@ import org.eclipse.aether.collection.DependencyManager; import org.eclipse.aether.graph.DefaultDependencyNode; import org.eclipse.aether.graph.Dependency; -import org.eclipse.aether.graph.DependencyNode; import org.eclipse.aether.graph.Exclusion; import org.eclipse.aether.util.graph.manager.DependencyManagerUtils; @@ -38,27 +38,30 @@ */ public class PremanagedDependency { - final String premanagedVersion; + private final String premanagedVersion; - final String premanagedScope; + private final String premanagedScope; - final Boolean premanagedOptional; + private final Boolean premanagedOptional; /** * @since 1.1.0 */ - final Collection premanagedExclusions; + private final Collection premanagedExclusions; /** * @since 1.1.0 */ - final Map premanagedProperties; + private final Map premanagedProperties; - final int managedBits; + /** + * @since 2.0.17 + */ + private final EnumMap managedSubjects; - final Dependency managedDependency; + private final Dependency managedDependency; - final boolean premanagedState; + private final boolean premanagedState; @SuppressWarnings("checkstyle:parameternumber") PremanagedDependency( @@ -67,7 +70,7 @@ public class PremanagedDependency { Boolean premanagedOptional, Collection premanagedExclusions, Map premanagedProperties, - int managedBits, + EnumMap managedSubjects, Dependency managedDependency, boolean premanagedState) { this.premanagedVersion = premanagedVersion; @@ -80,7 +83,7 @@ public class PremanagedDependency { this.premanagedProperties = premanagedProperties != null ? Collections.unmodifiableMap(new HashMap<>(premanagedProperties)) : null; - this.managedBits = managedBits; + this.managedSubjects = managedSubjects; this.managedDependency = managedDependency; this.premanagedState = premanagedState; } @@ -92,7 +95,8 @@ public static PremanagedDependency create( boolean premanagedState) { DependencyManagement depMngt = depManager != null ? depManager.manageDependency(dependency) : null; - int managedBits = 0; + EnumMap managedSubjects = + new EnumMap<>(DependencyManagement.Subject.class); String premanagedVersion = null; String premanagedScope = null; Boolean premanagedOptional = null; @@ -104,37 +108,48 @@ public static PremanagedDependency create( Artifact artifact = dependency.getArtifact(); premanagedVersion = artifact.getVersion(); dependency = dependency.setArtifact(artifact.setVersion(depMngt.getVersion())); - managedBits |= DependencyNode.MANAGED_VERSION; + managedSubjects.put( + DependencyManagement.Subject.VERSION, + depMngt.isManagedSubjectEnforced(DependencyManagement.Subject.VERSION)); } if (depMngt.getProperties() != null) { Artifact artifact = dependency.getArtifact(); premanagedProperties = artifact.getProperties(); dependency = dependency.setArtifact(artifact.setProperties(depMngt.getProperties())); - managedBits |= DependencyNode.MANAGED_PROPERTIES; + managedSubjects.put( + DependencyManagement.Subject.PROPERTIES, + depMngt.isManagedSubjectEnforced(DependencyManagement.Subject.PROPERTIES)); } if (depMngt.getScope() != null) { premanagedScope = dependency.getScope(); dependency = dependency.setScope(depMngt.getScope()); - managedBits |= DependencyNode.MANAGED_SCOPE; + managedSubjects.put( + DependencyManagement.Subject.SCOPE, + depMngt.isManagedSubjectEnforced(DependencyManagement.Subject.SCOPE)); } if (depMngt.getOptional() != null) { premanagedOptional = dependency.isOptional(); dependency = dependency.setOptional(depMngt.getOptional()); - managedBits |= DependencyNode.MANAGED_OPTIONAL; + managedSubjects.put( + DependencyManagement.Subject.OPTIONAL, + depMngt.isManagedSubjectEnforced(DependencyManagement.Subject.OPTIONAL)); } if (depMngt.getExclusions() != null) { premanagedExclusions = dependency.getExclusions(); dependency = dependency.setExclusions(depMngt.getExclusions()); - managedBits |= DependencyNode.MANAGED_EXCLUSIONS; + managedSubjects.put( + DependencyManagement.Subject.EXCLUSIONS, + depMngt.isManagedSubjectEnforced(DependencyManagement.Subject.EXCLUSIONS)); } } + return new PremanagedDependency( premanagedVersion, premanagedScope, premanagedOptional, premanagedExclusions, premanagedProperties, - managedBits, + managedSubjects, dependency, premanagedState); } @@ -144,7 +159,7 @@ public Dependency getManagedDependency() { } public void applyTo(DefaultDependencyNode child) { - child.setManagedBits(managedBits); + child.setManagedSubjects(managedSubjects); if (premanagedState) { child.setData(DependencyManagerUtils.NODE_DATA_PREMANAGED_VERSION, premanagedVersion); child.setData(DependencyManagerUtils.NODE_DATA_PREMANAGED_SCOPE, premanagedScope); diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/bf/BfDependencyCollector.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/bf/BfDependencyCollector.java index 759e227e7..c6c77d360 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/bf/BfDependencyCollector.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/bf/BfDependencyCollector.java @@ -209,7 +209,7 @@ protected void doCollectDependencies( managedDependencies, parents, dependency, - PremanagedDependency.create(rootDepManager, dependency, false, args.premanagedState)); + createPremanagedDependency(rootDepManager, dependency, false, args.premanagedState)); if (!filter(processingContext)) { processingContext.withDependency(processingContext.premanagedDependency.getManagedDependency()); resolveArtifactDescriptorAsync(args, processingContext, results); @@ -274,7 +274,7 @@ private void processDependency( results.addCycle(context.parents, cycleEntry, d); DependencyNode cycleNode = context.parents.get(cycleEntry); if (cycleNode.getDependency() != null) { - DefaultDependencyNode child = createDependencyNode( + DefaultDependencyNode child = createDependencyNodeCycle( relocations, preManaged, rangeResult, version, d, descriptorResult, cycleNode); context.getParent().getChildren().add(child); continue; @@ -288,7 +288,7 @@ private void processDependency( .getArtifactId() .equals(d.getArtifact().getArtifactId()); - PremanagedDependency premanagedDependency = PremanagedDependency.create( + PremanagedDependency premanagedDependency = createPremanagedDependency( context.depManager, d, disableVersionManagementSubsequently, args.premanagedState); DependencyProcessingContext relocatedContext = new DependencyProcessingContext( context.depSelector, @@ -406,7 +406,7 @@ private void doRecurse( for (Dependency dependency : descriptorResult.getDependencies()) { RequestTrace childTrace = collectStepTrace( parentContext.trace, args.request.getRequestContext(), parents, dependency); - PremanagedDependency premanagedDependency = PremanagedDependency.create( + PremanagedDependency premanagedDependency = createPremanagedDependency( childManager, dependency, disableVersionManagement, args.premanagedState); DependencyProcessingContext processingContext = new DependencyProcessingContext( childSelector, diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/df/DfDependencyCollector.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/df/DfDependencyCollector.java index 034d6691e..44a9b223f 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/df/DfDependencyCollector.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/df/DfDependencyCollector.java @@ -191,7 +191,7 @@ private void processDependency( RequestTrace trace = collectStepTrace(parent, args.request.getRequestContext(), args.nodes.nodes, dependency); PremanagedDependency preManaged = - PremanagedDependency.create(depManager, dependency, disableVersionManagement, args.premanagedState); + createPremanagedDependency(depManager, dependency, disableVersionManagement, args.premanagedState); dependency = preManaged.getManagedDependency(); boolean noDescriptor = isLackingDescriptor(args.session, dependency.getArtifact()); @@ -231,7 +231,7 @@ private void processDependency( results.addCycle(args.nodes.nodes, cycleEntry, d); DependencyNode cycleNode = args.nodes.get(cycleEntry); if (cycleNode.getDependency() != null) { - DefaultDependencyNode child = createDependencyNode( + DefaultDependencyNode child = createDependencyNodeCycle( relocations, preManaged, rangeResult, version, d, descriptorResult, cycleNode); node.getChildren().add(child); continue; diff --git a/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/DependencyGraphParser.java b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/DependencyGraphParser.java index 18da45bb7..98d66fbfd 100644 --- a/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/DependencyGraphParser.java +++ b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/DependencyGraphParser.java @@ -28,6 +28,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.EnumMap; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; @@ -36,6 +37,7 @@ import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.collection.DependencyManagement; import org.eclipse.aether.graph.DefaultDependencyNode; import org.eclipse.aether.graph.Dependency; import org.eclipse.aether.graph.DependencyNode; @@ -291,16 +293,17 @@ private DependencyNode build(DependencyNode parent, LineContext ctx, boolean isR DefaultArtifact artifact = new DefaultArtifact(def.coords, def.properties); Dependency dependency = new Dependency(artifact, def.scope, def.optional); node = new DefaultDependencyNode(dependency); - int managedBits = 0; + EnumMap managedSubjects = + new EnumMap<>(DependencyManagement.Subject.class); if (def.premanagedScope != null) { - managedBits |= DependencyNode.MANAGED_SCOPE; + managedSubjects.put(DependencyManagement.Subject.SCOPE, true); node.setData("premanaged.scope", def.premanagedScope); } if (def.premanagedVersion != null) { - managedBits |= DependencyNode.MANAGED_VERSION; + managedSubjects.put(DependencyManagement.Subject.VERSION, true); node.setData("premanaged.version", def.premanagedVersion); } - node.setManagedBits(managedBits); + node.setManagedSubjects(managedSubjects); if (def.relocations != null) { List relocations = new ArrayList<>(); for (String relocation : def.relocations) { diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/AbstractDependencyManager.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/AbstractDependencyManager.java index 2b2eeab35..1c67889b9 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/AbstractDependencyManager.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/AbstractDependencyManager.java @@ -86,9 +86,10 @@ *

Managed Bits and Graph Transformations

*

* When a {@link org.eclipse.aether.graph.DependencyNode} becomes "managed" by any property - * provided from this manager, {@link org.eclipse.aether.graph.DependencyNode#getManagedBits()} + * provided from this manager, {@link org.eclipse.aether.graph.DependencyNode#isManagedSubject(DependencyManagement.Subject)} + * and {@link org.eclipse.aether.graph.DependencyNode#isManagedSubjectEnforced(DependencyManagement.Subject)} * will carry this information for the given property. Later graph transformations will abstain - * from modifying these properties of marked nodes (assuming the node already has the property + * from modifying these properties of marked enforced nodes (assuming the node already has the property * set to what it should have). Sometimes this is unwanted, especially for properties that need * to be inherited in the graph (values derived from parent-child context of the actual node, * like "scope" or "optional"). @@ -206,14 +207,23 @@ private boolean containsManagedVersion(Key key, MMap managedVersion return managedVersions != null && managedVersions.containsKey(key); } - private String getManagedVersion(Key key) { + /** + * Returns {@code true} if this manager represents the root level (factory or root POM level). + * Per the depth model: 0 = factory (seed), 1 = root (project POM), 2+ = descendants. + * Management entries from root level should be enforced, while those from descendants are advised. + */ + private boolean isRootManager() { + return depth <= 1; + } + + private AbstractDependencyManager getManagedVersion(Key key) { for (AbstractDependencyManager ancestor : path) { if (ancestor.managedVersions != null && ancestor.managedVersions.containsKey(key)) { - return ancestor.managedVersions.get(key); + return ancestor; } } if (depth == 1 && managedVersions != null && managedVersions.containsKey(key)) { - return managedVersions.get(key); + return this; } return null; } @@ -227,14 +237,14 @@ private boolean containsManagedScope(Key key, MMap managedScopes) { return managedScopes != null && managedScopes.containsKey(key); } - private String getManagedScope(Key key) { + private AbstractDependencyManager getManagedScope(Key key) { for (AbstractDependencyManager ancestor : path) { if (ancestor.managedScopes != null && ancestor.managedScopes.containsKey(key)) { - return ancestor.managedScopes.get(key); + return ancestor; } } if (depth == 1 && managedScopes != null && managedScopes.containsKey(key)) { - return managedScopes.get(key); + return this; } return null; } @@ -248,14 +258,14 @@ private boolean containsManagedOptional(Key key, MMap managedOptio return managedOptionals != null && managedOptionals.containsKey(key); } - private Boolean getManagedOptional(Key key) { + private AbstractDependencyManager getManagedOptional(Key key) { for (AbstractDependencyManager ancestor : path) { if (ancestor.managedOptionals != null && ancestor.managedOptionals.containsKey(key)) { - return ancestor.managedOptionals.get(key); + return ancestor; } } if (depth == 1 && managedOptionals != null && managedOptionals.containsKey(key)) { - return managedOptionals.get(key); + return this; } return null; } @@ -276,14 +286,14 @@ private boolean containsManagedLocalPath(Key key, MMap managedLocal * @param key the dependency key * @return the managed local path, or null if not managed */ - private String getManagedLocalPath(Key key) { + private AbstractDependencyManager getManagedLocalPath(Key key) { for (AbstractDependencyManager ancestor : path) { if (ancestor.managedLocalPaths != null && ancestor.managedLocalPaths.containsKey(key)) { - return ancestor.managedLocalPaths.get(key); + return ancestor; } } if (managedLocalPaths != null && managedLocalPaths.containsKey(key)) { - return managedLocalPaths.get(key); + return this; } return null; } @@ -334,22 +344,20 @@ public DependencyManager deriveChildManager(DependencyCollectionContext context) managedVersions.put(key, version); } - if (isInheritedDerived()) { - String scope = managedDependency.getScope(); - if (!scope.isEmpty() && !containsManagedScope(key, managedScopes)) { - if (managedScopes == null) { - managedScopes = MMap.emptyNotDone(); - } - managedScopes.put(key, scope); + String scope = managedDependency.getScope(); + if (!scope.isEmpty() && !containsManagedScope(key, managedScopes)) { + if (managedScopes == null) { + managedScopes = MMap.emptyNotDone(); } + managedScopes.put(key, scope); + } - Boolean optional = managedDependency.getOptional(); - if (optional != null && !containsManagedOptional(key, managedOptionals)) { - if (managedOptionals == null) { - managedOptionals = MMap.emptyNotDone(); - } - managedOptionals.put(key, optional); + Boolean optional = managedDependency.getOptional(); + if (optional != null && !containsManagedOptional(key, managedOptionals)) { + if (managedOptionals == null) { + managedOptionals = MMap.emptyNotDone(); } + managedOptionals.put(key, optional); } String localPath = systemDependencyScope == null @@ -394,58 +402,59 @@ public DependencyManagement manageDependency(Dependency dependency) { Key key = new Key(dependency.getArtifact()); if (isApplied()) { - String version = getManagedVersion(key); + AbstractDependencyManager versionOwner = getManagedVersion(key); // is managed locally by model builder // apply only rules coming from "higher" levels - if (version != null) { + if (versionOwner != null) { management = new DependencyManagement(); - management.setVersion(version); + management.setVersion(versionOwner.managedVersions.get(key), versionOwner.isRootManager()); } - String scope = getManagedScope(key); + AbstractDependencyManager scopeOwner = getManagedScope(key); // is managed locally by model builder // apply only rules coming from "higher" levels - if (scope != null) { + if (scopeOwner != null) { if (management == null) { management = new DependencyManagement(); } - management.setScope(scope); + String managedScope = scopeOwner.managedScopes.get(key); + management.setScope(managedScope, scopeOwner.isRootManager()); if (systemDependencyScope != null - && !systemDependencyScope.is(scope) + && !systemDependencyScope.is(managedScope) && systemDependencyScope.getSystemPath(dependency.getArtifact()) != null) { HashMap properties = new HashMap<>(dependency.getArtifact().getProperties()); systemDependencyScope.setSystemPath(properties, null); - management.setProperties(properties); + management.setProperties(properties, false); } } // system scope paths always applied to have them aligned // (same artifact == same path) in whole graph if (systemDependencyScope != null - && (scope != null && systemDependencyScope.is(scope) - || (scope == null && systemDependencyScope.is(dependency.getScope())))) { - String localPath = getManagedLocalPath(key); - if (localPath != null) { + && (scopeOwner != null && systemDependencyScope.is(scopeOwner.managedScopes.get(key)) + || (scopeOwner == null && systemDependencyScope.is(dependency.getScope())))) { + AbstractDependencyManager localPathOwner = getManagedLocalPath(key); + if (localPathOwner != null) { if (management == null) { management = new DependencyManagement(); } HashMap properties = new HashMap<>(dependency.getArtifact().getProperties()); - systemDependencyScope.setSystemPath(properties, localPath); - management.setProperties(properties); + systemDependencyScope.setSystemPath(properties, localPathOwner.managedLocalPaths.get(key)); + management.setProperties(properties, false); } } // optional is not managed by model builder // apply only rules coming from "higher" levels - Boolean optional = getManagedOptional(key); - if (optional != null) { + AbstractDependencyManager optionalOwner = getManagedOptional(key); + if (optionalOwner != null) { if (management == null) { management = new DependencyManagement(); } - management.setOptional(optional); + management.setOptional(optionalOwner.managedOptionals.get(key), optionalOwner.isRootManager()); } } @@ -461,7 +470,7 @@ public DependencyManagement manageDependency(Dependency dependency) { } Collection result = new LinkedHashSet<>(dependency.getExclusions()); result.addAll(exclusions); - management.setExclusions(result); + management.setExclusions(result, true); } return management; @@ -474,34 +483,6 @@ protected boolean isDerived() { return depth < deriveUntil; } - /** - * Manages dependency properties including "version", "scope", "optional", "local path", and "exclusions". - *

- * Property management behavior: - *

    - *
  • Version: Follows {@link #isDerived()} pattern. Management is applied only at higher - * levels to avoid interference with the model builder.
  • - *
  • Scope: Derived from root only due to inheritance in dependency graphs. Special handling - * for "system" scope to align artifact paths.
  • - *
  • Optional: Derived from root only due to inheritance in dependency graphs.
  • - *
  • Local path: Managed only when scope is or was set to "system" to ensure consistent - * artifact path alignment.
  • - *
  • Exclusions: Accumulated additively from root to current level throughout the entire - * dependency path.
  • - *
- *

- * Inheritance handling: Since "scope" and "optional" properties inherit through dependency - * graphs (beyond model builder scope), they are derived only from the root node. The actual manager - * implementation determines specific handling behavior. - *

- * Default behavior: Defaults to {@link #isDerived()} to maintain compatibility with - * "classic" behavior (equivalent to {@code deriveUntil=2}). For custom transitivity management, override - * this method or ensure inherited managed properties are handled during graph transformation. - */ - protected boolean isInheritedDerived() { - return isDerived(); - } - /** * Returns {@code true} if current dependency should be managed according to so far collected/derived rules. */ diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/DependencyManagerUtils.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/DependencyManagerUtils.java index e83fd0669..685d005f4 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/DependencyManagerUtils.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/DependencyManagerUtils.java @@ -23,6 +23,7 @@ import org.eclipse.aether.ConfigurationProperties; import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.collection.DependencyManagement; import org.eclipse.aether.graph.DependencyNode; import org.eclipse.aether.graph.Exclusion; @@ -87,7 +88,7 @@ public final class DependencyManagerUtils { * or if {@link #CONFIG_PROP_VERBOSE} was not enabled */ public static String getPremanagedVersion(DependencyNode node) { - if ((node.getManagedBits() & DependencyNode.MANAGED_VERSION) == 0) { + if (!node.isManagedSubject(DependencyManagement.Subject.VERSION)) { return null; } return cast(node.getData().get(NODE_DATA_PREMANAGED_VERSION), String.class); @@ -101,7 +102,7 @@ public static String getPremanagedVersion(DependencyNode node) { * if {@link #CONFIG_PROP_VERBOSE} was not enabled */ public static String getPremanagedScope(DependencyNode node) { - if ((node.getManagedBits() & DependencyNode.MANAGED_SCOPE) == 0) { + if (!node.isManagedSubject(DependencyManagement.Subject.SCOPE)) { return null; } return cast(node.getData().get(NODE_DATA_PREMANAGED_SCOPE), String.class); @@ -115,7 +116,7 @@ public static String getPremanagedScope(DependencyNode node) { * {@link #CONFIG_PROP_VERBOSE} was not enabled */ public static Boolean getPremanagedOptional(DependencyNode node) { - if ((node.getManagedBits() & DependencyNode.MANAGED_OPTIONAL) == 0) { + if (!node.isManagedSubject(DependencyManagement.Subject.OPTIONAL)) { return null; } return cast(node.getData().get(NODE_DATA_PREMANAGED_OPTIONAL), Boolean.class); @@ -131,7 +132,7 @@ public static Boolean getPremanagedOptional(DependencyNode node) { */ @SuppressWarnings("unchecked") public static Collection getPremanagedExclusions(DependencyNode node) { - if ((node.getManagedBits() & DependencyNode.MANAGED_EXCLUSIONS) == 0) { + if (!node.isManagedSubject(DependencyManagement.Subject.EXCLUSIONS)) { return null; } return cast(node.getData().get(NODE_DATA_PREMANAGED_EXCLUSIONS), Collection.class); @@ -147,7 +148,7 @@ public static Collection getPremanagedExclusions(DependencyNode node) */ @SuppressWarnings("unchecked") public static Map getPremanagedProperties(DependencyNode node) { - if ((node.getManagedBits() & DependencyNode.MANAGED_PROPERTIES) == 0) { + if (!node.isManagedSubject(DependencyManagement.Subject.PROPERTIES)) { return null; } return cast(node.getData().get(NODE_DATA_PREMANAGED_PROPERTIES), Map.class); diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/TransitiveDependencyManager.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/TransitiveDependencyManager.java index b46211be0..460a52c01 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/TransitiveDependencyManager.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/TransitiveDependencyManager.java @@ -145,30 +145,4 @@ protected DependencyManager newInstance( managedExclusions, systemDependencyScope); } - - /** - * Controls inheritance-based property derivation for scope and optional properties. - *

- * Why scope and optional are special: In dependency graphs, these two properties - * are subject to inheritance during graph transformation (which is outside ModelBuilder's scope). - * Therefore, scope and optional are derived only from the root to prevent interference with - * inheritance logic. - *

- *

- * The inheritance problem: If we managed scope/optional from sources below the root, - * we would mark nodes as "managed" in the dependency graph. The "managed" flag means "do not touch it, - * it is as it should be", which would prevent proper inheritance application during later graph - * transformation, causing nodes to end up with incorrect scope or optional states. - *

- *

- * Special case: The "system" scope has special handling due to its unique path requirements. - *

- * - * @return true only at depth 0 (root level) to ensure inheritance-based properties are only - * derived from the root, false otherwise - */ - @Override - protected boolean isInheritedDerived() { - return depth == 0; - } } diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ClassicConflictResolver.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ClassicConflictResolver.java index 4ecd53b2e..4825e433e 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ClassicConflictResolver.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ClassicConflictResolver.java @@ -33,6 +33,7 @@ import org.eclipse.aether.RepositoryException; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.collection.DependencyGraphTransformationContext; +import org.eclipse.aether.collection.DependencyManagement; import org.eclipse.aether.graph.DefaultDependencyNode; import org.eclipse.aether.graph.Dependency; import org.eclipse.aether.graph.DependencyNode; @@ -675,7 +676,7 @@ private DependencyNode parent() { } private String deriveScope(DependencyNode node, String conflictId) throws RepositoryException { - if ((node.getManagedBits() & DependencyNode.MANAGED_SCOPE) != 0 + if (node.isManagedSubjectEnforced(DependencyManagement.Subject.SCOPE) || (conflictId != null && resolvedIds.containsKey(conflictId))) { return scope(node.getDependency()); } @@ -702,7 +703,7 @@ private boolean deriveOptional(DependencyNode node, String conflictId) { Dependency dep = node.getDependency(); boolean optional = (dep != null) && dep.isOptional(); if (optional - || (node.getManagedBits() & DependencyNode.MANAGED_OPTIONAL) != 0 + || node.isManagedSubjectEnforced(DependencyManagement.Subject.OPTIONAL) || (conflictId != null && resolvedIds.containsKey(conflictId))) { return optional; } diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/PathConflictResolver.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/PathConflictResolver.java index 985106a0b..519e60873 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/PathConflictResolver.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/PathConflictResolver.java @@ -32,6 +32,7 @@ import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.collection.DependencyGraphTransformationContext; +import org.eclipse.aether.collection.DependencyManagement; import org.eclipse.aether.graph.DefaultDependencyNode; import org.eclipse.aether.graph.Dependency; import org.eclipse.aether.graph.DependencyNode; @@ -423,12 +424,12 @@ private void pull(int levels) { private void derive(int levels, boolean winner) throws RepositoryException { if (!winner) { if (this.parent != null) { - if ((dn.getManagedBits() & DependencyNode.MANAGED_SCOPE) == 0) { + if (!dn.isManagedSubjectEnforced(DependencyManagement.Subject.SCOPE)) { ScopeContext context = new ScopeContext(this.parent.scope, this.scope); state.scopeDeriver.deriveScope(context); this.scope = context.derivedScope; } - if ((dn.getManagedBits() & DependencyNode.MANAGED_OPTIONAL) == 0) { + if (!dn.isManagedSubjectEnforced(DependencyManagement.Subject.OPTIONAL)) { if (!this.optional && this.parent.optional) { this.optional = true; } diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/manager/DependencyManagerTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/manager/DependencyManagerTest.java index db5c4afe1..0524587bc 100644 --- a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/manager/DependencyManagerTest.java +++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/manager/DependencyManagerTest.java @@ -20,14 +20,18 @@ import java.util.Arrays; import java.util.Collections; +import java.util.EnumMap; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.artifact.DefaultArtifact; import org.eclipse.aether.collection.DependencyCollectionContext; import org.eclipse.aether.collection.DependencyManagement; +import org.eclipse.aether.collection.DependencyManagement.Subject; import org.eclipse.aether.collection.DependencyManager; +import org.eclipse.aether.graph.DefaultDependencyNode; import org.eclipse.aether.graph.Dependency; +import org.eclipse.aether.graph.DependencyNode; import org.eclipse.aether.graph.Exclusion; import org.eclipse.aether.internal.test.util.TestUtils; import org.junit.jupiter.api.BeforeEach; @@ -268,4 +272,175 @@ void testDefault() { // DO NOT APPLY ONTO ITSELF assertNull(mngt); } + + /** + * Tests that root-level management produces enforced results for version, scope, and optional. + */ + @Test + void testTransitiveEnforcementFromRoot() { + DependencyManager manager = new TransitiveDependencyManager(null); + + // depth=1: derive from root with managed dependencies + manager = manager.deriveChildManager(newContext( + new Dependency(A2, null, null), new Dependency(B, null, true), new Dependency(C1, "newscope", null))); + + // depth=2: management is applied — check enforcement flags + manager = manager.deriveChildManager(newContext()); + DependencyManagement mngt = manager.manageDependency(new Dependency(A1, null)); + assertNotNull(mngt); + assertEquals(A2.getVersion(), mngt.getVersion()); + // version management from root should be enforced + assertTrue(mngt.isManagedSubject(Subject.VERSION)); + assertTrue(mngt.isManagedSubjectEnforced(Subject.VERSION)); + + mngt = manager.manageDependency(new Dependency(C1, null)); + assertNotNull(mngt); + assertEquals("newscope", mngt.getScope()); + // scope management from root should be enforced + assertTrue(mngt.isManagedSubject(Subject.SCOPE)); + assertTrue(mngt.isManagedSubjectEnforced(Subject.SCOPE)); + + mngt = manager.manageDependency(new Dependency(B1, null)); + assertNotNull(mngt); + assertEquals(Boolean.TRUE, mngt.getOptional()); + // optional management from root should be enforced + assertTrue(mngt.isManagedSubject(Subject.OPTIONAL)); + assertTrue(mngt.isManagedSubjectEnforced(Subject.OPTIONAL)); + } + + /** + * Tests that transitive (non-root) management produces advised (not enforced) results + * for scope and optional. + */ + @Test + void testTransitiveEnforcementFromNonRoot() { + DependencyManager manager = new TransitiveDependencyManager(null); + + // depth=1: no managed dependencies at root level + manager = manager.deriveChildManager(newContext()); + + // depth=2: managed dependencies introduced at transitive level + manager = manager.deriveChildManager(newContext( + new Dependency(A2, null, null), new Dependency(B, null, true), new Dependency(C1, "newscope", null))); + + // depth=3: management is applied — check enforcement flags + manager = manager.deriveChildManager(newContext()); + DependencyManagement mngt = manager.manageDependency(new Dependency(A1, null)); + assertNotNull(mngt); + assertEquals(A2.getVersion(), mngt.getVersion()); + // version management from non-root should NOT be enforced (advised) + assertTrue(mngt.isManagedSubject(Subject.VERSION)); + assertFalse(mngt.isManagedSubjectEnforced(Subject.VERSION)); + + mngt = manager.manageDependency(new Dependency(C1, null)); + assertNotNull(mngt); + assertEquals("newscope", mngt.getScope()); + // scope management from non-root should NOT be enforced (advised) + assertTrue(mngt.isManagedSubject(Subject.SCOPE)); + assertFalse(mngt.isManagedSubjectEnforced(Subject.SCOPE)); + + mngt = manager.manageDependency(new Dependency(B1, null)); + assertNotNull(mngt); + assertEquals(Boolean.TRUE, mngt.getOptional()); + // optional management from non-root should NOT be enforced (advised) + assertTrue(mngt.isManagedSubject(Subject.OPTIONAL)); + assertFalse(mngt.isManagedSubjectEnforced(Subject.OPTIONAL)); + } + + /** + * Tests that classic dependency manager also produces enforced results from root. + */ + @Test + void testClassicEnforcementFromRoot() { + DependencyManager manager = new ClassicDependencyManager(null); + + // depth=1: derive from root + manager = manager.deriveChildManager( + newContext(new Dependency(A2, null, null), new Dependency(C1, "newscope", null))); + + // depth=2: management is applied + manager = manager.deriveChildManager(newContext()); + DependencyManagement mngt = manager.manageDependency(new Dependency(A1, null)); + assertNotNull(mngt); + assertTrue(mngt.isManagedSubjectEnforced(Subject.VERSION)); + + mngt = manager.manageDependency(new Dependency(C1, null)); + assertNotNull(mngt); + assertTrue(mngt.isManagedSubjectEnforced(Subject.SCOPE)); + } + + /** + * Tests backwards compatibility: setManagedBits maps to isManagedSubject/isManagedSubjectEnforced. + */ + @Test + void testSetManagedBitsBackwardsCompat() { + DefaultDependencyNode node = new DefaultDependencyNode(new Dependency(A1, "compile")); + + // Set via deprecated API + node.setManagedBits(DependencyNode.MANAGED_VERSION | DependencyNode.MANAGED_SCOPE); + + // Check via new API — all mapped as enforced + assertTrue(node.isManagedSubject(Subject.VERSION)); + assertTrue(node.isManagedSubjectEnforced(Subject.VERSION)); + assertTrue(node.isManagedSubject(Subject.SCOPE)); + assertTrue(node.isManagedSubjectEnforced(Subject.SCOPE)); + + // Unset subjects + assertFalse(node.isManagedSubject(Subject.OPTIONAL)); + assertFalse(node.isManagedSubjectEnforced(Subject.OPTIONAL)); + assertFalse(node.isManagedSubject(Subject.PROPERTIES)); + assertFalse(node.isManagedSubject(Subject.EXCLUSIONS)); + + // Round-trip: getManagedBits still works + assertEquals(DependencyNode.MANAGED_VERSION | DependencyNode.MANAGED_SCOPE, node.getManagedBits()); + } + + /** + * Tests that setManagedSubjects with explicit enforcement flags works correctly. + */ + @Test + void testSetManagedSubjectsWithEnforcement() { + DefaultDependencyNode node = new DefaultDependencyNode(new Dependency(A1, "compile")); + + EnumMap subjects = new EnumMap<>(Subject.class); + subjects.put(Subject.VERSION, true); // enforced + subjects.put(Subject.SCOPE, false); // advised + subjects.put(Subject.OPTIONAL, false); // advised + node.setManagedSubjects(subjects); + + // All are managed + assertTrue(node.isManagedSubject(Subject.VERSION)); + assertTrue(node.isManagedSubject(Subject.SCOPE)); + assertTrue(node.isManagedSubject(Subject.OPTIONAL)); + assertFalse(node.isManagedSubject(Subject.PROPERTIES)); + + // Only version is enforced + assertTrue(node.isManagedSubjectEnforced(Subject.VERSION)); + assertFalse(node.isManagedSubjectEnforced(Subject.SCOPE)); + assertFalse(node.isManagedSubjectEnforced(Subject.OPTIONAL)); + assertFalse(node.isManagedSubjectEnforced(Subject.PROPERTIES)); + + // getManagedBits still reports all managed subjects (regardless of enforcement) + assertEquals( + DependencyNode.MANAGED_VERSION | DependencyNode.MANAGED_SCOPE | DependencyNode.MANAGED_OPTIONAL, + node.getManagedBits()); + } + + /** + * Tests that setManagedSubjects with null clears all managed subjects. + */ + @Test + void testSetManagedSubjectsNull() { + DefaultDependencyNode node = new DefaultDependencyNode(new Dependency(A1, "compile")); + + EnumMap subjects = new EnumMap<>(Subject.class); + subjects.put(Subject.VERSION, true); + node.setManagedSubjects(subjects); + assertTrue(node.isManagedSubject(Subject.VERSION)); + + // Clear + node.setManagedSubjects(null); + assertFalse(node.isManagedSubject(Subject.VERSION)); + assertEquals(0, node.getManagedBits()); + } } diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictResolverTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictResolverTest.java index 9d8fca505..a9bb31fa4 100644 --- a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictResolverTest.java +++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictResolverTest.java @@ -20,11 +20,13 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.EnumMap; import java.util.List; import java.util.stream.Stream; import org.eclipse.aether.RepositoryException; import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.collection.DependencyManagement; import org.eclipse.aether.graph.DefaultDependencyNode; import org.eclipse.aether.graph.Dependency; import org.eclipse.aether.graph.DependencyNode; @@ -522,6 +524,84 @@ void winnerCycleRemoved(ConflictResolver conflictResolver) throws RepositoryExce } } + @ParameterizedTest + @MethodSource("conflictResolverSource") + void enforcedScopeNotDerived(ConflictResolver conflictResolver) throws RepositoryException { + // Root -> Parent (test) -> Child (compile, scope enforced) + // Scope derivation would normally narrow Child's scope to "test" (from parent), + // but enforced scope should prevent that. + DependencyNode root = makeDependencyNode("some-group", "root", "1.0"); + DependencyNode parent = makeDependencyNode("some-group", "parent", "1.0", "test"); + DefaultDependencyNode child = + (DefaultDependencyNode) makeDependencyNode("some-group", "child", "1.0", "compile"); + EnumMap enforcedScope = + new EnumMap<>(DependencyManagement.Subject.class); + enforcedScope.put(DependencyManagement.Subject.SCOPE, true); + child.setManagedSubjects(enforcedScope); + root.setChildren(mutableList(parent)); + parent.setChildren(mutableList(child)); + + transform(conflictResolver, root); + assertEquals("compile", child.getDependency().getScope(), "Enforced scope should not be derived from parent"); + } + + @ParameterizedTest + @MethodSource("conflictResolverSource") + void advisedScopeIsDerived(ConflictResolver conflictResolver) throws RepositoryException { + // Root -> Parent (test) -> Child (compile, scope advised) + // Scope derivation should narrow Child's scope to "test" because it's only advised. + DependencyNode root = makeDependencyNode("some-group", "root", "1.0"); + DependencyNode parent = makeDependencyNode("some-group", "parent", "1.0", "test"); + DefaultDependencyNode child = + (DefaultDependencyNode) makeDependencyNode("some-group", "child", "1.0", "compile"); + EnumMap advisedScope = new EnumMap<>(DependencyManagement.Subject.class); + advisedScope.put(DependencyManagement.Subject.SCOPE, false); + child.setManagedSubjects(advisedScope); + root.setChildren(mutableList(parent)); + parent.setChildren(mutableList(child)); + + transform(conflictResolver, root); + assertEquals("test", child.getDependency().getScope(), "Advised scope should be derived from parent"); + } + + @ParameterizedTest + @MethodSource("conflictResolverSource") + void enforcedOptionalNotDerived(ConflictResolver conflictResolver) throws RepositoryException { + // Root -> Parent (optional=true) -> Child (optional=false, optional enforced) + // Optional derivation would normally set Child to optional (from parent), + // but enforced optional should prevent that. + DependencyNode root = makeDependencyNode("some-group", "root", "1.0"); + DependencyNode parent = makeDependencyNode("some-group", "parent", "1.0"); + parent.setOptional(true); + DefaultDependencyNode child = (DefaultDependencyNode) makeDependencyNode("some-group", "child", "1.0"); + child.setOptional(false); + EnumMap enforcedOptional = + new EnumMap<>(DependencyManagement.Subject.class); + enforcedOptional.put(DependencyManagement.Subject.OPTIONAL, true); + child.setManagedSubjects(enforcedOptional); + root.setChildren(mutableList(parent)); + parent.setChildren(mutableList(child)); + + transform(conflictResolver, root); + assertFalse(child.getDependency().isOptional(), "Enforced optional should not be derived from parent"); + } + + @ParameterizedTest + @MethodSource("conflictResolverSource") + void unmanagedScopeIsDerivedAsUsual(ConflictResolver conflictResolver) throws RepositoryException { + // Root -> Parent (test) -> Child (compile, no management) + // Scope derivation should narrow Child's scope to "test" as usual (backwards compat). + DependencyNode root = makeDependencyNode("some-group", "root", "1.0"); + DependencyNode parent = makeDependencyNode("some-group", "parent", "1.0", "test"); + DependencyNode child = makeDependencyNode("some-group", "child", "1.0", "compile"); + root.setChildren(mutableList(parent)); + parent.setChildren(mutableList(child)); + + transform(conflictResolver, root); + assertEquals( + "test", child.getDependency().getScope(), "Unmanaged scope should be derived from parent as usual"); + } + private static DependencyNode makeDependencyNode(String groupId, String artifactId, String version) { return makeDependencyNode(groupId, artifactId, version, "compile"); }