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 e3d4ed55f Fix for ChainedWorkspaceReader (#1909)
e3d4ed55f is described below

commit e3d4ed55fe19ffaa0463c111484eda4740478e0a
Author: Tamas Cservenak <[email protected]>
AuthorDate: Sun Jun 14 11:10:31 2026 +0200

    Fix for ChainedWorkspaceReader (#1909)
    
    The reader instances once set, are immutable, but repository instances 
returned by readers may change (their key).
---
 .../util/repository/ChainedWorkspaceReader.java    |  38 +++--
 .../repository/ChainedWorkspaceReaderTest.java     | 175 +++++++++++++++++++++
 2 files changed, 198 insertions(+), 15 deletions(-)

diff --git 
a/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/ChainedWorkspaceReader.java
 
b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/ChainedWorkspaceReader.java
index c34a60e60..2ecfd872d 100644
--- 
a/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/ChainedWorkspaceReader.java
+++ 
b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/ChainedWorkspaceReader.java
@@ -20,10 +20,13 @@ package org.eclipse.aether.util.repository;
 
 import java.io.File;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicReference;
 
 import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.repository.WorkspaceReader;
@@ -35,10 +38,8 @@ import static java.util.Objects.requireNonNull;
  * A workspace reader that delegates to a chain of other readers, effectively 
aggregating their contents.
  */
 public final class ChainedWorkspaceReader implements WorkspaceReader {
-
-    private final List<WorkspaceReader> readers = new ArrayList<>();
-
-    private WorkspaceRepository repository;
+    private final List<WorkspaceReader> readers;
+    private final AtomicReference<WorkspaceRepository> repository;
 
     /**
      * Creates a new workspace reader by chaining the specified readers.
@@ -47,19 +48,19 @@ public final class ChainedWorkspaceReader implements 
WorkspaceReader {
      * @see #newInstance(WorkspaceReader, WorkspaceReader)
      */
     public ChainedWorkspaceReader(WorkspaceReader... readers) {
+        ArrayList<WorkspaceReader> list = new ArrayList<>();
         if (readers != null) {
-            Collections.addAll(this.readers, readers);
+            Arrays.stream(readers).filter(Objects::nonNull).forEach(list::add);
         }
-
         StringBuilder buffer = new StringBuilder();
-        for (WorkspaceReader reader : this.readers) {
+        for (WorkspaceReader reader : list) {
             if (buffer.length() > 0) {
                 buffer.append('+');
             }
             buffer.append(reader.getRepository().getContentType());
         }
-
-        repository = new WorkspaceRepository(buffer.toString(), new 
Key(this.readers));
+        this.readers = Collections.unmodifiableList(list);
+        this.repository = new AtomicReference<>(new 
WorkspaceRepository(buffer.toString(), new Key(list)));
     }
 
     /**
@@ -79,6 +80,7 @@ public final class ChainedWorkspaceReader implements 
WorkspaceReader {
         return new ChainedWorkspaceReader(reader1, reader2);
     }
 
+    @Override
     public File findArtifact(Artifact artifact) {
         requireNonNull(artifact, "artifact cannot be null");
         File file = null;
@@ -93,6 +95,7 @@ public final class ChainedWorkspaceReader implements 
WorkspaceReader {
         return file;
     }
 
+    @Override
     public List<String> findVersions(Artifact artifact) {
         requireNonNull(artifact, "artifact cannot be null");
         Collection<String> versions = new LinkedHashSet<>();
@@ -104,22 +107,27 @@ public final class ChainedWorkspaceReader implements 
WorkspaceReader {
         return Collections.unmodifiableList(new ArrayList<>(versions));
     }
 
+    @Override
     public WorkspaceRepository getRepository() {
         Key key = new Key(readers);
-        if (!key.equals(repository.getKey())) {
-            repository = new WorkspaceRepository(repository.getContentType(), 
key);
-        }
-        return repository;
+        return repository.updateAndGet(r -> {
+            if (!key.equals(r.getKey())) {
+                return new WorkspaceRepository(r.getContentType(), key);
+            } else {
+                return r;
+            }
+        });
     }
 
     private static class Key {
-
-        private final List<Object> keys = new ArrayList<>();
+        private final List<Object> keys;
 
         Key(List<WorkspaceReader> readers) {
+            ArrayList<Object> keys = new ArrayList<>();
             for (WorkspaceReader reader : readers) {
                 keys.add(reader.getRepository().getKey());
             }
+            this.keys = keys;
         }
 
         @Override
diff --git 
a/maven-resolver-util/src/test/java/org/eclipse/aether/util/repository/ChainedWorkspaceReaderTest.java
 
b/maven-resolver-util/src/test/java/org/eclipse/aether/util/repository/ChainedWorkspaceReaderTest.java
new file mode 100644
index 000000000..2de109c1e
--- /dev/null
+++ 
b/maven-resolver-util/src/test/java/org/eclipse/aether/util/repository/ChainedWorkspaceReaderTest.java
@@ -0,0 +1,175 @@
+/*
+ * 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.util.repository;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.repository.WorkspaceReader;
+import org.eclipse.aether.repository.WorkspaceRepository;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+public class ChainedWorkspaceReaderTest {
+    /**
+     * Stable reader, returns always same instance for {@link 
#getRepository()}.
+     */
+    private static class StableWorkspaceReader implements WorkspaceReader {
+        private final WorkspaceRepository repository = new 
WorkspaceRepository("stable", "stableKey");
+
+        @Override
+        public WorkspaceRepository getRepository() {
+            return repository;
+        }
+
+        @Override
+        public File findArtifact(Artifact artifact) {
+            return null;
+        }
+
+        @Override
+        public List<String> findVersions(Artifact artifact) {
+            return Collections.emptyList();
+        }
+    }
+
+    /**
+     * Unstable reader, returns always different instance with different key 
for {@link #getRepository()}.
+     */
+    private static class UnstableWorkspaceReader implements WorkspaceReader {
+        private final AtomicInteger count = new AtomicInteger();
+
+        @Override
+        public WorkspaceRepository getRepository() {
+            return new WorkspaceRepository("unstable", 
count.getAndIncrement());
+        }
+
+        @Override
+        public File findArtifact(Artifact artifact) {
+            return null;
+        }
+
+        @Override
+        public List<String> findVersions(Artifact artifact) {
+            return Collections.emptyList();
+        }
+    }
+
+    @Test
+    void constructEmpty() {
+        ChainedWorkspaceReader reader = new ChainedWorkspaceReader();
+        WorkspaceRepository repository = reader.getRepository();
+        assertNotNull(repository);
+        assertEquals("", repository.getContentType());
+    }
+
+    @Test
+    void constructNullReader() {
+        ChainedWorkspaceReader reader = new 
ChainedWorkspaceReader((WorkspaceReader) null);
+        WorkspaceRepository repository = reader.getRepository();
+        assertNotNull(repository);
+        assertEquals("", repository.getContentType());
+    }
+
+    @Test
+    void constructNullVararg() {
+        ChainedWorkspaceReader reader = new 
ChainedWorkspaceReader((WorkspaceReader[]) null);
+        WorkspaceRepository repository = reader.getRepository();
+        assertNotNull(repository);
+        assertEquals("", repository.getContentType());
+    }
+
+    @Test
+    void constructOne() {
+        ChainedWorkspaceReader reader = new ChainedWorkspaceReader(new 
StableWorkspaceReader());
+        WorkspaceRepository repository = reader.getRepository();
+        assertNotNull(repository);
+        assertEquals("stable", repository.getContentType());
+    }
+
+    @Test
+    void constructTwo() {
+        ChainedWorkspaceReader reader =
+                new ChainedWorkspaceReader(new StableWorkspaceReader(), new 
StableWorkspaceReader());
+        WorkspaceRepository repository = reader.getRepository();
+        assertNotNull(repository);
+        assertEquals("stable+stable", repository.getContentType());
+    }
+
+    @Test
+    void constructMultipleWithNulls() {
+        ChainedWorkspaceReader reader1 =
+                new ChainedWorkspaceReader(null, new StableWorkspaceReader(), 
null, new StableWorkspaceReader(), null);
+        ChainedWorkspaceReader reader2 =
+                new ChainedWorkspaceReader(new StableWorkspaceReader(), new 
StableWorkspaceReader());
+        WorkspaceRepository repository1 = reader1.getRepository();
+        assertNotNull(repository1);
+        WorkspaceRepository repository2 = reader2.getRepository();
+        assertNotNull(repository2);
+        assertEquals("stable+stable", repository1.getContentType());
+        assertEquals("stable+stable", repository2.getContentType());
+        assertEquals(repository1.getKey(), repository2.getKey());
+    }
+
+    @Test
+    void keyChange() {
+        WorkspaceReader reader =
+                ChainedWorkspaceReader.newInstance(new 
StableWorkspaceReader(), new UnstableWorkspaceReader());
+        WorkspaceRepository repository = reader.getRepository();
+        assertNotNull(reader);
+        assertEquals("stable+unstable", repository.getContentType());
+        assertNotEquals(repository, reader.getRepository());
+    }
+
+    @Test
+    void concurrentKeyChange() throws InterruptedException {
+        WorkspaceReader reader =
+                ChainedWorkspaceReader.newInstance(new 
StableWorkspaceReader(), new UnstableWorkspaceReader());
+        final int threads = 100;
+        ConcurrentLinkedQueue<WorkspaceRepository> results = new 
ConcurrentLinkedQueue<>();
+        CountDownLatch latch = new CountDownLatch(threads);
+        for (int i = 0; i < threads; i++) {
+            new Thread(() -> {
+                        try {
+                            results.add(reader.getRepository());
+                        } finally {
+                            latch.countDown();
+                        }
+                    })
+                    .start();
+        }
+        latch.await();
+        // assert no same element present in results
+        assertEquals(new HashSet<>(results).size(), results.size());
+        for (int i = 0; i < threads; i++) {
+            assertEquals("stable+unstable", results.remove().getContentType());
+        }
+        // foolproof: queue must be empty after we checked all elements of it
+        assertEquals(0, results.size());
+    }
+}

Reply via email to