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