This is an automated email from the ASF dual-hosted git repository.

cstamas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-resolver.git


The following commit(s) were added to refs/heads/master by this push:
     new 63e4736b3 Bug: GH-1711 make sure last wins (#1712)
63e4736b3 is described below

commit 63e4736b333c5d4ae5a8afb2a4728b2074a59cb5
Author: Tamas Cservenak <[email protected]>
AuthorDate: Fri Dec 12 13:50:35 2025 +0100

    Bug: GH-1711 make sure last wins (#1712)
    
    Nodes once created were immutable, now structure is made mutable for 
parsing, with "last wins" strategy. Also, split and simplify things, as prefix 
tree does not even need stop/allow, is groupTree only thing.
    
    Fixes #1711
---
 .../GroupIdRemoteRepositoryFilterSource.java       |  9 +--
 .../internal/impl/filter/ruletree/GroupTree.java   | 72 ++++++++++++++++------
 .../aether/internal/impl/filter/ruletree/Node.java | 40 +++---------
 .../internal/impl/filter/ruletree/PrefixTree.java  | 14 +++--
 .../impl/filter/ruletree/GroupTreeTest.java        | 63 ++++++++++++++++---
 src/site/markdown/remote-repository-filtering.md   |  2 +-
 6 files changed, 128 insertions(+), 72 deletions(-)

diff --git 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/GroupIdRemoteRepositoryFilterSource.java
 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/GroupIdRemoteRepositoryFilterSource.java
index dcf25d268..c98953af0 100644
--- 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/GroupIdRemoteRepositoryFilterSource.java
+++ 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/GroupIdRemoteRepositoryFilterSource.java
@@ -254,7 +254,7 @@ public final class GroupIdRemoteRepositoryFilterSource 
extends RemoteRepositoryF
                         rules(session)
                                 .compute(normalized, (k, v) -> {
                                     if (v == null || v == DISABLED || v == 
ENABLED_NO_INPUT) {
-                                        v = new GroupTree("");
+                                        v = GroupTree.create("record");
                                     }
                                     return v;
                                 })
@@ -277,15 +277,16 @@ public final class GroupIdRemoteRepositoryFilterSource 
extends RemoteRepositoryF
                         normalizeRemoteRepository(session, remoteRepository), 
r -> loadRepositoryRules(session, r));
     }
 
-    private static final GroupTree DISABLED = new GroupTree("disabled");
-    private static final GroupTree ENABLED_NO_INPUT = new 
GroupTree("enabled-no-input");
+    private static final GroupTree DISABLED = GroupTree.create("disabled");
+    private static final GroupTree ENABLED_NO_INPUT = 
GroupTree.create("enabled-no-input");
 
     private GroupTree loadRepositoryRules(RepositorySystemSession session, 
RemoteRepository remoteRepository) {
         if (isRepositoryFilteringEnabled(session, remoteRepository)) {
             Path filePath = ruleFile(session, remoteRepository);
             if (Files.isReadable(filePath)) {
                 try (Stream<String> lines = Files.lines(filePath, 
StandardCharsets.UTF_8)) {
-                    GroupTree groupTree = new GroupTree("");
+                    GroupTree groupTree =
+                            
GroupTree.create(filePath.getFileName().toString());
                     int rules = groupTree.loadNodes(lines);
                     logger.info("Loaded {} group rules for remote repository 
{}", rules, remoteRepository.getId());
                     if (logger.isDebugEnabled()) {
diff --git 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/ruletree/GroupTree.java
 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/ruletree/GroupTree.java
index 7aaf74610..15d2e0510 100644
--- 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/ruletree/GroupTree.java
+++ 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/ruletree/GroupTree.java
@@ -41,6 +41,7 @@ import static java.util.stream.Collectors.toList;
  * means "by default deny" (same effect as when this character is not present 
in file). Use of limiter modifier
  * on "root" like {@code "=*"} has no effect, is simply ignored.
  *
+ * <p>
  * Examples:
  * <pre>
  * {@code
@@ -58,10 +59,19 @@ import static java.util.stream.Collectors.toList;
  * and "allow {@code org.apache.bar} groupID ONLY".
  *
  * <p>
- * In case of conflicting rules, parsing happens by "first wins", so line 
closer to first line in file "wins", and conflicting
- * line is ignored.
+ * In case of conflicting rules, parsing happens by "last wins", so line 
closer to last line in file "wins", and conflicting
+ * line value is lost.
  */
-public class GroupTree extends Node {
+public class GroupTree extends Node<GroupTree> {
+    /**
+     * Creates root, that is special: accept is never null.
+     */
+    public static GroupTree create(String name) {
+        GroupTree result = new GroupTree(name);
+        result.accept = false;
+        return result;
+    }
+
     private static final String ROOT = "*";
     private static final String MOD_EXCLUSION = "!";
     private static final String MOD_STOP = "=";
@@ -70,8 +80,11 @@ public class GroupTree extends Node {
         return Arrays.stream(groupId.split("\\.")).filter(e -> 
!e.isEmpty()).collect(toList());
     }
 
-    public GroupTree(String name) {
-        super(name, false, false);
+    private boolean stop;
+    private Boolean accept;
+
+    private GroupTree(String name) {
+        super(name);
     }
 
     public int loadNodes(Stream<String> linesStream) {
@@ -86,10 +99,10 @@ public class GroupTree extends Node {
 
     public boolean loadNode(String line) {
         if (!line.startsWith("#") && !line.trim().isEmpty()) {
-            Node currentNode = this;
-            boolean allow = true;
+            GroupTree currentNode = this;
+            boolean accept = true;
             if (line.startsWith(MOD_EXCLUSION)) {
-                allow = false;
+                accept = false;
                 line = line.substring(MOD_EXCLUSION.length());
             }
             boolean stop = false;
@@ -98,14 +111,17 @@ public class GroupTree extends Node {
                 line = line.substring(MOD_STOP.length());
             }
             if (ROOT.equals(line)) {
-                this.setAllow(allow);
+                this.accept = accept;
                 return true;
             }
             List<String> groupElements = elementsOfGroup(line);
             for (String groupElement : groupElements.subList(0, 
groupElements.size() - 1)) {
-                currentNode = currentNode.addSibling(groupElement, false, 
null);
+                currentNode = 
currentNode.siblings.computeIfAbsent(groupElement, GroupTree::new);
             }
-            currentNode.addSibling(groupElements.get(groupElements.size() - 
1), stop, allow);
+            String lastElement = groupElements.get(groupElements.size() - 1);
+            currentNode = currentNode.siblings.computeIfAbsent(lastElement, 
GroupTree::new);
+            currentNode.stop = stop;
+            currentNode.accept = accept;
             return true;
         }
         return false;
@@ -115,20 +131,36 @@ public class GroupTree extends Node {
         final List<String> current = new ArrayList<>();
         final List<String> groupElements = elementsOfGroup(groupId);
         Boolean accepted = null;
-        Node currentNode = this;
+        GroupTree currentNode = this;
         for (String groupElement : groupElements) {
             current.add(groupElement);
-            currentNode = currentNode.getSibling(groupElement);
+            currentNode = currentNode.siblings.get(groupElement);
             if (currentNode == null) {
+                // we stepped off the tree; use value we got so far
                 break;
+            } else if (currentNode.stop && groupElements.equals(current)) {
+                // exact match
+                accepted = currentNode.accept;
+                break;
+            } else if (!currentNode.stop && currentNode.accept != null) {
+                // "inherit" if not STOP and allow is set; and most probably 
we loop more
+                accepted = currentNode.accept;
             }
-            if (currentNode.isStop() && groupElements.equals(current)) {
-                accepted = currentNode.isAllow();
-            } else if (!currentNode.isStop() && currentNode.isAllow() != null) 
{
-                accepted = currentNode.isAllow();
-            }
         }
-        // use `accepted`, if defined; otherwise fallback to root (it always 
has `allow` set)
-        return accepted != null ? accepted : this.isAllow();
+        // use 'accepted', if defined; otherwise fallback to root (it always 
has 'allow' set)
+        return accepted != null ? accepted : this.accept;
+    }
+
+    @Override
+    public String toString() {
+        return (accept != null ? (accept ? "+" : "-") : "?") + (stop ? "=" : 
"") + name;
+    }
+
+    @Override
+    public void dump(String prefix) {
+        System.out.println(prefix + this);
+        for (GroupTree node : siblings.values()) {
+            node.dump(prefix + "  ");
+        }
     }
 }
diff --git 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/ruletree/Node.java
 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/ruletree/Node.java
index beccf29c2..1006666cf 100644
--- 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/ruletree/Node.java
+++ 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/ruletree/Node.java
@@ -18,22 +18,18 @@
  */
 package org.eclipse.aether.internal.impl.filter.ruletree;
 
-import java.util.HashMap;
+import java.util.concurrent.ConcurrentHashMap;
 
 /**
  * A tree structure with rules.
  */
-class Node {
-    private final String name;
-    private final boolean stop;
-    private Boolean allow;
-    private final HashMap<String, Node> siblings;
+abstract class Node<N extends Node<?>> {
+    protected final String name;
+    protected final ConcurrentHashMap<String, N> siblings;
 
-    protected Node(String name, boolean stop, Boolean allow) {
+    protected Node(String name) {
         this.name = name;
-        this.stop = stop;
-        this.allow = allow;
-        this.siblings = new HashMap<>();
+        this.siblings = new ConcurrentHashMap<>();
     }
 
     public String getName() {
@@ -44,34 +40,14 @@ class Node {
         return siblings.isEmpty();
     }
 
-    public boolean isStop() {
-        return stop;
-    }
-
-    public Boolean isAllow() {
-        return allow;
-    }
-
-    public void setAllow(Boolean allow) {
-        this.allow = allow;
-    }
-
-    protected Node addSibling(String name, boolean stop, Boolean allow) {
-        return siblings.computeIfAbsent(name, n -> new Node(n, stop, allow));
-    }
-
-    protected Node getSibling(String name) {
-        return siblings.get(name);
-    }
-
     @Override
     public String toString() {
-        return (allow != null ? (allow ? "+" : "-") : "?") + (stop ? "=" : "") 
+ name;
+        return name;
     }
 
     public void dump(String prefix) {
         System.out.println(prefix + this);
-        for (Node node : siblings.values()) {
+        for (N node : siblings.values()) {
             node.dump(prefix + "  ");
         }
     }
diff --git 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/ruletree/PrefixTree.java
 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/ruletree/PrefixTree.java
index da11d65d4..f5f510ad2 100644
--- 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/ruletree/PrefixTree.java
+++ 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/ruletree/PrefixTree.java
@@ -34,6 +34,7 @@ import static java.util.stream.Collectors.toList;
  * </ul>
  * By default, artifact is allowed if layout converted path of it has a 
matching prefix in this file.
  *
+ * <p>
  * Example prefix files:
  * <ul>
  *     <li><a 
href="https://repo.maven.apache.org/maven2/.meta/prefixes.txt";>Maven 
Central</a></li>
@@ -41,13 +42,13 @@ import static java.util.stream.Collectors.toList;
  *     <li><a 
href="https://repo.eclipse.org/content/repositories/tycho/.meta/prefixes.txt";>Eclipse
 Tycho</a></li>
  * </ul>
  */
-public class PrefixTree extends Node {
+public class PrefixTree extends Node<PrefixTree> {
     private static List<String> elementsOfPath(final String path) {
         return Arrays.stream(path.split("/")).filter(e -> 
!e.isEmpty()).collect(toList());
     }
 
     public PrefixTree(String name) {
-        super(name, false, null);
+        super(name);
     }
 
     public int loadNodes(Stream<String> linesStream) {
@@ -62,9 +63,9 @@ public class PrefixTree extends Node {
 
     public boolean loadNode(String line) {
         if (!line.startsWith("#") && !line.trim().isEmpty()) {
-            Node currentNode = this;
+            PrefixTree currentNode = this;
             for (String element : elementsOfPath(line)) {
-                currentNode = currentNode.addSibling(element, false, null);
+                currentNode = currentNode.siblings.computeIfAbsent(element, 
PrefixTree::new);
             }
             return true;
         }
@@ -73,13 +74,14 @@ public class PrefixTree extends Node {
 
     public boolean acceptedPath(String path) {
         final List<String> pathElements = elementsOfPath(path);
-        Node currentNode = this;
+        PrefixTree currentNode = this;
         for (String pathElement : pathElements) {
-            currentNode = currentNode.getSibling(pathElement);
+            currentNode = currentNode.siblings.get(pathElement);
             if (currentNode == null || currentNode.isLeaf()) {
                 break;
             }
         }
+        // path is accepted if its elements lead to an existing node that is a 
leaf node
         return currentNode != null && currentNode.isLeaf();
     }
 }
diff --git 
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/filter/ruletree/GroupTreeTest.java
 
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/filter/ruletree/GroupTreeTest.java
index b47d54d9a..d7ccc571a 100644
--- 
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/filter/ruletree/GroupTreeTest.java
+++ 
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/filter/ruletree/GroupTreeTest.java
@@ -34,7 +34,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 public class GroupTreeTest {
     @Test
     void smoke() {
-        GroupTree groupTree = new GroupTree("root");
+        GroupTree groupTree = GroupTree.create("root");
         groupTree.loadNodes(Stream.of(
                 "# comment",
                 "",
@@ -42,6 +42,8 @@ public class GroupTreeTest {
                 "!=org.apache.maven.foo",
                 "!org.apache.maven.bar",
                 "=org.apache.baz"));
+        groupTree.dump("");
+
         assertTrue(groupTree.acceptedGroupId("org.apache.maven"));
         assertTrue(groupTree.acceptedGroupId("org.apache.maven.aaa"));
 
@@ -59,7 +61,7 @@ public class GroupTreeTest {
 
     @Test
     void smokeWithPositiveDefault() {
-        GroupTree groupTree = new GroupTree("root");
+        GroupTree groupTree = GroupTree.create("root");
         groupTree.loadNodes(Stream.of(
                 "# comment",
                 "",
@@ -68,6 +70,8 @@ public class GroupTreeTest {
                 "!org.apache.maven.bar",
                 "=org.apache.baz",
                 "*"));
+        groupTree.dump("");
+
         assertTrue(groupTree.acceptedGroupId("org.apache.maven"));
         assertTrue(groupTree.acceptedGroupId("org.apache.maven.aaa"));
 
@@ -85,8 +89,10 @@ public class GroupTreeTest {
 
     @Test
     void smokeWithPositiveDefaultExclusionsOnly() {
-        GroupTree groupTree = new GroupTree("root");
+        GroupTree groupTree = GroupTree.create("root");
         groupTree.loadNodes(Stream.of("# comment", "*", 
"!org.apache.maven.foo", "!=org.apache.maven.bar"));
+        groupTree.dump("");
+
         // no applicable rule; root=ALLOW
         assertTrue(groupTree.acceptedGroupId("org.apache.maven"));
         assertTrue(groupTree.acceptedGroupId("org.apache.maven.aaa"));
@@ -105,7 +111,7 @@ public class GroupTreeTest {
 
     @Test
     void smokeWithNegativeDefault() {
-        GroupTree groupTree = new GroupTree("root");
+        GroupTree groupTree = GroupTree.create("root");
         groupTree.loadNodes(Stream.of(
                 "# comment",
                 "",
@@ -114,6 +120,8 @@ public class GroupTreeTest {
                 "!org.apache.maven.bar",
                 "=org.apache.baz",
                 "!*"));
+        groupTree.dump("");
+
         assertTrue(groupTree.acceptedGroupId("org.apache.maven"));
         assertTrue(groupTree.acceptedGroupId("org.apache.maven.aaa"));
 
@@ -132,7 +140,7 @@ public class GroupTreeTest {
     @Test
     void implicitAndExplicitDefaultIsSame() {
         // w/o asterisk: uses coded defaults
-        GroupTree implicitTree = new GroupTree("root");
+        GroupTree implicitTree = GroupTree.create("root");
         implicitTree.loadNodes(Stream.of(
                 "# comment",
                 "",
@@ -140,9 +148,11 @@ public class GroupTreeTest {
                 "!=org.apache.maven.foo",
                 "!org.apache.maven.bar",
                 "=org.apache.baz"));
+        implicitTree.dump("");
+
         HashMap<String, Boolean> implicitResults = new HashMap<>();
         // w/ asterisk: set to same value as default (should cause no change)
-        GroupTree explicitTree = new GroupTree("root");
+        GroupTree explicitTree = GroupTree.create("root");
         explicitTree.loadNodes(Stream.of(
                 "# comment",
                 "",
@@ -175,9 +185,21 @@ public class GroupTreeTest {
         GroupTree groupTree;
 
         // REPRODUCER as given
-        groupTree = new GroupTree("root");
+        groupTree = GroupTree.create("root");
         // this is redundant, as 'org.apache' IMPLIES 
'org.apache.maven.plugins'
         groupTree.loadNodes(Stream.of("# comment", "", "org.apache", 
"org.apache.maven.plugins"));
+        groupTree.dump("");
+
+        assertTrue(groupTree.acceptedGroupId("org.apache")); // this is given
+        assertTrue(groupTree.acceptedGroupId("org.apache.maven")); // implied 
by first
+        assertTrue(groupTree.acceptedGroupId("org.apache.maven.plugins")); // 
implied by first (line is redundant)
+        assertTrue(groupTree.acceptedGroupId("org.apache.maven.plugins.foo")); 
// implied by first
+
+        // INVERTED REPRODUCER as given
+        groupTree = GroupTree.create("root");
+        // reproducer in opposite order
+        groupTree.loadNodes(Stream.of("# comment", "", 
"org.apache.maven.plugins", "org.apache"));
+        groupTree.dump("");
 
         assertTrue(groupTree.acceptedGroupId("org.apache")); // this is given
         assertTrue(groupTree.acceptedGroupId("org.apache.maven")); // implied 
by first
@@ -185,8 +207,9 @@ public class GroupTreeTest {
         assertTrue(groupTree.acceptedGroupId("org.apache.maven.plugins.foo")); 
// implied by first
 
         // FIXED
-        groupTree = new GroupTree("root");
+        groupTree = GroupTree.create("root");
         groupTree.loadNodes(Stream.of("# comment", "", "=org.apache", 
"org.apache.maven.plugins"));
+        groupTree.dump("");
 
         assertTrue(groupTree.acceptedGroupId("org.apache")); // this is given 
(=)
         assertFalse(groupTree.acceptedGroupId("org.apache.maven")); // not 
allowed
@@ -194,12 +217,34 @@ public class GroupTreeTest {
         assertTrue(groupTree.acceptedGroupId("org.apache.maven.plugins.foo")); 
// implied by above
 
         // MIXED
-        groupTree = new GroupTree("root");
+        groupTree = GroupTree.create("root");
         groupTree.loadNodes(Stream.of("# comment", "", "org.apache", 
"!=org.apache.maven.plugins"));
+        groupTree.dump("");
 
         assertTrue(groupTree.acceptedGroupId("org.apache")); // this is given
         assertTrue(groupTree.acceptedGroupId("org.apache.maven")); // implied 
by first
         assertFalse(groupTree.acceptedGroupId("org.apache.maven.plugins")); // 
this is given (!=)
         assertTrue(groupTree.acceptedGroupId("org.apache.maven.plugins.foo")); 
// implied by first
     }
+
+    @Test
+    void gh1711() {
+        GroupTree groupTree;
+
+        // "last wins" 1
+        groupTree = GroupTree.create("root");
+        groupTree.loadNodes(Stream.of("org.apache", "!org.apache"));
+        groupTree.dump("");
+
+        assertFalse(groupTree.acceptedGroupId("org.apache")); // last wins
+        assertFalse(groupTree.acceptedGroupId("org.apache.maven")); // last 
wins
+
+        // "last wins" 2
+        groupTree = GroupTree.create("root");
+        groupTree.loadNodes(Stream.of("org.apache", "!org.apache", 
"org.apache"));
+        groupTree.dump("");
+
+        assertTrue(groupTree.acceptedGroupId("org.apache")); // last wins
+        assertTrue(groupTree.acceptedGroupId("org.apache.maven")); // last wins
+    }
 }
diff --git a/src/site/markdown/remote-repository-filtering.md 
b/src/site/markdown/remote-repository-filtering.md
index 8012895fd..7e9dbd1c3 100644
--- a/src/site/markdown/remote-repository-filtering.md
+++ b/src/site/markdown/remote-repository-filtering.md
@@ -188,7 +188,7 @@ Be aware: In case a line with single asterisk `*` is 
present, the whole logic of
 hence there is no need to add "allowed entries" (they are allowed by default), 
but one can add "disallowed entries" by 
 adding `!com.foo` and alike.
 
-Conflicting rules: rule parser is intentionally trivial, so in case of 
conflicting rules the "first wins" strategy is 
+Conflicting rules: rule parser is intentionally trivial, so in case of 
conflicting rules the "last wins" strategy is 
 applied. Ideally, user should keep files sorted or handle them in a way one 
can detect conflicts in it.
 
 ## Operation

Reply via email to