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

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


The following commit(s) were added to refs/heads/master by this push:
     new 34e68c8935 [MNG-8561] SourceRoot should be more lenient wrt duplicates 
(#2085)
34e68c8935 is described below

commit 34e68c8935984e3beee9494276c9a8ac813c8987
Author: Guillaume Nodet <gno...@gmail.com>
AuthorDate: Thu Feb 6 10:17:29 2025 +0100

    [MNG-8561] SourceRoot should be more lenient wrt duplicates (#2085)
---
 .../maven/project/DefaultProjectBuilder.java       |   9 +-
 .../org/apache/maven/project/MavenProject.java     | 113 ++++++++++++---------
 .../org/apache/maven/impl/DefaultSourceRoot.java   |  49 +++++++--
 .../maven/it/MavenITmng8561SourceRootTest.java     |  46 +++++++++
 .../org/apache/maven/it/TestSuiteOrdering.java     |   1 +
 .../src/test/resources/mng-8561/.mvn/.gitkeep      |   0
 .../src/test/resources/mng-8561/pom.xml            |  46 +++++++++
 .../src/test/resources/mng-8561/src/res/test.a     |   0
 .../src/test/resources/mng-8561/src/res/test.xml   |   0
 9 files changed, 206 insertions(+), 58 deletions(-)

diff --git 
a/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java
 
b/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java
index 182504a135..71b6818390 100644
--- 
a/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java
+++ 
b/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java
@@ -60,6 +60,7 @@
 import org.apache.maven.api.model.Plugin;
 import org.apache.maven.api.model.Profile;
 import org.apache.maven.api.model.ReportPlugin;
+import org.apache.maven.api.model.Resource;
 import org.apache.maven.api.services.BuilderProblem.Severity;
 import org.apache.maven.api.services.ModelBuilder;
 import org.apache.maven.api.services.ModelBuilderException;
@@ -616,7 +617,7 @@ private void initProject(MavenProject project, 
ModelBuilderResult result) {
                 /*
                  * `sourceDirectory`, `testSourceDirectory` and 
`scriptSourceDirectory`
                  * are ignored if the POM file contains at least one <source> 
element
-                 * for the corresponding scope and langiage. This rule exists 
because
+                 * for the corresponding scope and language. This rule exists 
because
                  * Maven provides default values for those elements which may 
conflict
                  * with user's configuration.
                  */
@@ -629,6 +630,12 @@ private void initProject(MavenProject project, 
ModelBuilderResult result) {
                 if (!hasTest) {
                     
project.addTestCompileSourceRoot(build.getTestSourceDirectory());
                 }
+                for (Resource resource : 
project.getBuild().getDelegate().getResources()) {
+                    project.addSourceRoot(new DefaultSourceRoot(baseDir, 
ProjectScope.MAIN, resource));
+                }
+                for (Resource resource : 
project.getBuild().getDelegate().getTestResources()) {
+                    project.addSourceRoot(new DefaultSourceRoot(baseDir, 
ProjectScope.TEST, resource));
+                }
             }
 
             project.setActiveProfiles(
diff --git 
a/impl/maven-core/src/main/java/org/apache/maven/project/MavenProject.java 
b/impl/maven-core/src/main/java/org/apache/maven/project/MavenProject.java
index 2b5b199eb5..c67ad5804d 100644
--- a/impl/maven-core/src/main/java/org/apache/maven/project/MavenProject.java
+++ b/impl/maven-core/src/main/java/org/apache/maven/project/MavenProject.java
@@ -22,6 +22,7 @@
 import java.io.IOException;
 import java.io.Writer;
 import java.nio.file.Path;
+import java.util.AbstractList;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -33,6 +34,7 @@
 import java.util.Objects;
 import java.util.Properties;
 import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.function.Predicate;
 import java.util.stream.Stream;
 
@@ -143,40 +145,10 @@ public class MavenProject implements Cloneable {
 
     private List<MavenProject> collectedProjects;
 
-    /**
-     * A tuple of {@link SourceRoot} properties for which we decide that no 
duplicated value should exist in a project.
-     * The set of properties that we choose to put in this record may be 
modified in any future Maven version.
-     * The intent is to detect some configuration errors.
-     */
-    private record SourceKey(ProjectScope scope, Language language, Path 
directory) {
-        /**
-         * Converts this key into a source root.
-         * Used for adding a new source when no other information is available.
-         *
-         * @return the source root for the properties of this key.
-         */
-        SourceRoot createSource() {
-            return new DefaultSourceRoot(scope, language, directory);
-        }
-
-        /**
-         * {@return an error message to report when a conflict is detected}.
-         *
-         * @param baseDir value of {@link #getBaseDirectory()}, in order to 
make the message shorter
-         */
-        String conflictMessage(Path baseDir) {
-            return "Directory " + baseDir.relativize(directory)
-                    + " is specified twice for the scope \"" + scope.id()
-                    + "\" and language \"" + language.id() + "\".";
-        }
-    }
-
     /**
      * All sources of this project. The map includes main and test codes for 
all languages.
-     * However, we put some restrictions on what information can be repeated.
-     * Those restrictions are expressed in {@link SourceKey}.
      */
-    private HashMap<SourceKey, SourceRoot> sources = new LinkedHashMap<>(); // 
Need access to the `clone()` method.
+    private List<SourceRoot> sources = new CopyOnWriteArrayList<>();
 
     @Deprecated
     private ArtifactRepository releaseArtifactRepository;
@@ -358,10 +330,8 @@ public DependencyManagement getDependencyManagement() {
      * @since 4.0.0
      */
     public void addSourceRoot(SourceRoot source) {
-        var key = new SourceKey(source.scope(), source.language(), 
source.directory());
-        SourceRoot current = sources.putIfAbsent(key, source);
-        if (current != null && !current.equals(source)) {
-            throw new 
IllegalArgumentException(key.conflictMessage(getBaseDirectory()));
+        if (!sources.contains(source)) {
+            sources.add(source);
         }
     }
 
@@ -381,8 +351,7 @@ public void addSourceRoot(SourceRoot source) {
      */
     public void addSourceRoot(ProjectScope scope, Language language, Path 
directory) {
         directory = getBaseDirectory().resolve(directory).normalize();
-        var key = new SourceKey(scope, language, directory);
-        sources.computeIfAbsent(key, SourceKey::createSource);
+        addSourceRoot(new DefaultSourceRoot(scope, language, directory));
     }
 
     /**
@@ -402,8 +371,7 @@ public void addSourceRoot(ProjectScope scope, Language 
language, String director
             directory = directory.trim();
             if (!directory.isBlank()) {
                 Path path = getBaseDirectory().resolve(directory).normalize();
-                var key = new SourceKey(scope, language, path);
-                sources.computeIfAbsent(key, SourceKey::createSource);
+                addSourceRoot(scope, language, path);
             }
         }
     }
@@ -432,7 +400,7 @@ public void addTestCompileSourceRoot(String path) {
      * @see #addSourceRoot(SourceRoot)
      */
     public Collection<SourceRoot> getSourceRoots() {
-        return Collections.unmodifiableCollection(sources.values());
+        return Collections.unmodifiableCollection(sources);
     }
 
     /**
@@ -449,14 +417,14 @@ public Collection<SourceRoot> getSourceRoots() {
      * @since 4.0.0
      */
     public Stream<SourceRoot> getEnabledSourceRoots(ProjectScope scope, 
Language language) {
-        Stream<SourceRoot> s = 
sources.values().stream().filter(SourceRoot::enabled);
+        Stream<SourceRoot> stream = 
sources.stream().filter(SourceRoot::enabled);
         if (scope != null) {
-            s = s.filter((source) -> scope.equals(source.scope()));
+            stream = stream.filter(source -> scope.equals(source.scope()));
         }
         if (language != null) {
-            s = s.filter((source) -> language.equals(source.language()));
+            stream = stream.filter(source -> 
language.equals(source.language()));
         }
-        return s;
+        return stream;
     }
 
     /**
@@ -770,7 +738,26 @@ public Build getBuild() {
      */
     @Deprecated(since = "4.0.0")
     public List<Resource> getResources() {
-        return getBuild().getResources();
+        return new AbstractList<>() {
+            @Override
+            public Resource get(int index) {
+                return toResource(getEnabledSourceRoots(ProjectScope.MAIN, 
Language.RESOURCES)
+                        .toList()
+                        .get(index));
+            }
+
+            @Override
+            public int size() {
+                return (int) getEnabledSourceRoots(ProjectScope.MAIN, 
Language.RESOURCES)
+                        .count();
+            }
+
+            @Override
+            public boolean add(Resource resource) {
+                addResource(resource);
+                return true;
+            }
+        };
     }
 
     /**
@@ -778,7 +765,35 @@ public List<Resource> getResources() {
      */
     @Deprecated(since = "4.0.0")
     public List<Resource> getTestResources() {
-        return getBuild().getTestResources();
+        return new AbstractList<>() {
+            @Override
+            public Resource get(int index) {
+                return toResource(getEnabledSourceRoots(ProjectScope.TEST, 
Language.RESOURCES)
+                        .toList()
+                        .get(index));
+            }
+
+            @Override
+            public int size() {
+                return (int) getEnabledSourceRoots(ProjectScope.TEST, 
Language.RESOURCES)
+                        .count();
+            }
+
+            @Override
+            public boolean add(Resource resource) {
+                addTestResource(resource);
+                return true;
+            }
+        };
+    }
+
+    private Resource toResource(SourceRoot sourceRoot) {
+        return new Resource(org.apache.maven.api.model.Resource.newBuilder()
+                .directory(sourceRoot.directory().toString())
+                
.includes(sourceRoot.includes().stream().map(Object::toString).toList())
+                
.excludes(sourceRoot.excludes().stream().map(Object::toString).toList())
+                .filtering(Boolean.toString(sourceRoot.stringFiltering()))
+                .build());
     }
 
     /**
@@ -786,7 +801,6 @@ public List<Resource> getTestResources() {
      */
     @Deprecated(since = "4.0.0")
     public void addResource(Resource resource) {
-        getBuild().addResource(resource);
         addSourceRoot(new DefaultSourceRoot(getBaseDirectory(), 
ProjectScope.MAIN, resource.getDelegate()));
     }
 
@@ -795,7 +809,6 @@ public void addResource(Resource resource) {
      */
     @Deprecated(since = "4.0.0")
     public void addTestResource(Resource testResource) {
-        getBuild().addTestResource(testResource);
         addSourceRoot(new DefaultSourceRoot(getBaseDirectory(), 
ProjectScope.TEST, testResource.getDelegate()));
     }
 
@@ -1238,7 +1251,7 @@ protected void setAttachedArtifacts(List<Artifact> 
attachedArtifacts) {
      */
     @Deprecated
     private void setSourceRootDirs(ProjectScope scope, Language language, 
List<String> roots) {
-        sources.values().removeIf((source) -> scope.equals(source.scope()) && 
language.equals(source.language()));
+        sources.removeIf((source) -> scope.equals(source.scope()) && 
language.equals(source.language()));
         Path directory = getBaseDirectory();
         for (String root : roots) {
             addSourceRoot(new DefaultSourceRoot(scope, language, 
directory.resolve(root)));
@@ -1324,7 +1337,7 @@ private void deepCopy(MavenProject project) {
         // This property is not handled like others as we don't use public API.
         // The whole implementation of this `deepCopy` method may need 
revision,
         // but it would be the topic for a separated commit.
-        sources = (HashMap<SourceKey, SourceRoot>) project.sources.clone();
+        sources = new CopyOnWriteArrayList<>(project.sources);
 
         if (project.getModel() != null) {
             setModel(project.getModel().clone());
diff --git 
a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSourceRoot.java 
b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSourceRoot.java
index 8dadf02f05..1225ad538c 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSourceRoot.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSourceRoot.java
@@ -117,7 +117,7 @@ public DefaultSourceRoot(final Path baseDir, ProjectScope 
scope, Resource resour
     }
 
     /**
-     * Creates a new instance for the given directory and scope. The language 
is assumed Java.
+     * Creates a new instance for the given directory and scope.
      *
      * @param scope scope of source code (main or test)
      * @param language language of the source code
@@ -125,7 +125,7 @@ public DefaultSourceRoot(final Path baseDir, ProjectScope 
scope, Resource resour
      */
     public DefaultSourceRoot(final ProjectScope scope, final Language 
language, final Path directory) {
         this.scope = Objects.requireNonNull(scope);
-        this.language = language;
+        this.language = Objects.requireNonNull(language);
         this.directory = Objects.requireNonNull(directory);
         includes = List.of();
         excludes = List.of();
@@ -136,6 +136,31 @@ public DefaultSourceRoot(final ProjectScope scope, final 
Language language, fina
         enabled = true;
     }
 
+    /**
+     * Creates a new instance for the given directory and scope.
+     *
+     * @param scope scope of source code (main or test)
+     * @param language language of the source code
+     * @param directory directory of the source code
+     */
+    public DefaultSourceRoot(
+            final ProjectScope scope,
+            final Language language,
+            final Path directory,
+            List<PathMatcher> includes,
+            List<PathMatcher> excludes) {
+        this.scope = Objects.requireNonNull(scope);
+        this.language = language;
+        this.directory = Objects.requireNonNull(directory);
+        this.includes = includes != null ? List.copyOf(includes) : List.of();
+        this.excludes = excludes != null ? List.copyOf(excludes) : List.of();
+        moduleName = null;
+        targetVersion = null;
+        targetPath = null;
+        stringFiltering = false;
+        enabled = true;
+    }
+
     /**
      * {@return the given value as a trimmed non-blank string, or null 
otherwise}.
      */
@@ -162,11 +187,21 @@ private static String nonBlank(String value) {
     private static List<PathMatcher> matchers(FileSystem fs, List<String> 
patterns) {
         final var matchers = new PathMatcher[patterns.size()];
         for (int i = 0; i < matchers.length; i++) {
-            String pattern = patterns.get(i);
-            if (pattern.indexOf(':') < 0) {
-                pattern = "glob:" + pattern;
-            }
-            matchers[i] = fs.getPathMatcher(pattern);
+            String rawPattern = patterns.get(i);
+            String pattern = rawPattern.contains(":") ? rawPattern : "glob:" + 
rawPattern;
+            matchers[i] = new PathMatcher() {
+                final PathMatcher delegate = fs.getPathMatcher(pattern);
+
+                @Override
+                public boolean matches(Path path) {
+                    return delegate.matches(path);
+                }
+
+                @Override
+                public String toString() {
+                    return rawPattern;
+                }
+            };
         }
         return List.of(matchers);
     }
diff --git 
a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8561SourceRootTest.java
 
b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8561SourceRootTest.java
new file mode 100644
index 0000000000..fbe3422ab2
--- /dev/null
+++ 
b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8561SourceRootTest.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.it;
+
+import java.nio.file.Path;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * This is a test set for <a 
href="https://issues.apache.org/jira/browse/MNG-8561";>MNG-8561</a>.
+ */
+class MavenITmng8561SourceRootTest extends AbstractMavenIntegrationTestCase {
+
+    MavenITmng8561SourceRootTest() {
+        super("[4.0.0-rc-3-SNAPSHOT,)");
+    }
+
+    /**
+     *  Verify project is buildable.
+     */
+    @Test
+    void testIt() throws Exception {
+        Path basedir = 
extractResources("/mng-8561").getAbsoluteFile().toPath();
+
+        Verifier verifier = newVerifier(basedir.toString());
+        verifier.addCliArgument("validate");
+        verifier.execute();
+        verifier.verifyErrorFreeLog();
+    }
+}
diff --git 
a/its/core-it-suite/src/test/java/org/apache/maven/it/TestSuiteOrdering.java 
b/its/core-it-suite/src/test/java/org/apache/maven/it/TestSuiteOrdering.java
index 76f939dd0b..a303f19ca3 100644
--- a/its/core-it-suite/src/test/java/org/apache/maven/it/TestSuiteOrdering.java
+++ b/its/core-it-suite/src/test/java/org/apache/maven/it/TestSuiteOrdering.java
@@ -100,6 +100,7 @@ public TestSuiteOrdering() {
          * the tests are to finishing. Newer tests are also more likely to 
fail, so this is
          * a fail fast technique as well.
          */
+        suite.addTestSuite(MavenITmng8561SourceRootTest.class);
         suite.addTestSuite(MavenITmng8523ModelPropertiesTest.class);
         suite.addTestSuite(MavenITmng8527ConsumerPomTest.class);
         suite.addTestSuite(MavenITmng8525MavenDIPlugin.class);
diff --git a/its/core-it-suite/src/test/resources/mng-8561/.mvn/.gitkeep 
b/its/core-it-suite/src/test/resources/mng-8561/.mvn/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/its/core-it-suite/src/test/resources/mng-8561/pom.xml 
b/its/core-it-suite/src/test/resources/mng-8561/pom.xml
new file mode 100644
index 0000000000..20140a73bc
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/mng-8561/pom.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0";>
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>org.apache.maven.its.mng-8561</groupId>
+  <artifactId>parent</artifactId>
+  <version>1.0.0-SNAPSHOT</version>
+  <packaging>jar</packaging>
+
+  <build>
+    <resources>
+      <resource>
+        <directory>src/res</directory>
+        <includes>
+          <include>*.xml</include>
+        </includes>
+      </resource>
+      <resource>
+        <directory>src/res</directory>
+        <excludes>
+          <exclude>*.xml</exclude>
+        </excludes>
+      </resource>
+    </resources>
+  </build>
+
+</project>
diff --git a/its/core-it-suite/src/test/resources/mng-8561/src/res/test.a 
b/its/core-it-suite/src/test/resources/mng-8561/src/res/test.a
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/its/core-it-suite/src/test/resources/mng-8561/src/res/test.xml 
b/its/core-it-suite/src/test/resources/mng-8561/src/res/test.xml
new file mode 100644
index 0000000000..e69de29bb2

Reply via email to