This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch maven-4.0.x
in repository https://gitbox.apache.org/repos/asf/maven.git
The following commit(s) were added to refs/heads/maven-4.0.x by this push:
new f4058491cb Fix #2486: Make Resource.addInclude() persist in project
model (#2534) (#2565)
f4058491cb is described below
commit f4058491cb1ae75d8e375d153cac56b9ef3836ee
Author: Guillaume Nodet <[email protected]>
AuthorDate: Fri Jul 4 21:45:12 2025 +0200
Fix #2486: Make Resource.addInclude() persist in project model (#2534)
(#2565)
When plugin developers tried to modify project resources using:
project.getResources().get(0).addInclude(\test\);
The addInclude() call had no effect because Resource objects were
disconnected from the underlying project model.
This fix implements a ConnectedResource pattern:
1. Created ConnectedResource class that extends Resource and maintains
references to the original SourceRoot, ProjectScope, and MavenProject
2. Override modification methods (addInclude/removeInclude/setIncludes/
setExcludes) to update both the Resource and underlying project model
3. Preserve SourceRoot ordering by replacing at the same index position
4. Modified getResources() to return ConnectedResource instances
Key benefits:
- Fixes the original issue: addInclude() now works correctly
- Preserves SourceRoot ordering during modifications
- Backward compatible: existing code continues to work
- Comprehensive: handles all modification methods
- Well-tested: includes tests for functionality and ordering
Files changed:
- MavenProject.java: Core fix implementation, made sources field
package-private
- ConnectedResource.java: New class extracted to meet file length limits
- ResourceIncludeTest.java: Comprehensive test suite
Closes #2486
(cherry picked from commit 0d7b61a4b4e0eaca83ef0517eac2e4fd9fef8fe0)
---
.../apache/maven/project/ConnectedResource.java | 130 ++++++++++++++
.../org/apache/maven/project/MavenProject.java | 11 +-
.../apache/maven/project/ResourceIncludeTest.java | 191 +++++++++++++++++++++
3 files changed, 330 insertions(+), 2 deletions(-)
diff --git
a/impl/maven-core/src/main/java/org/apache/maven/project/ConnectedResource.java
b/impl/maven-core/src/main/java/org/apache/maven/project/ConnectedResource.java
new file mode 100644
index 0000000000..ba1afa8472
--- /dev/null
+++
b/impl/maven-core/src/main/java/org/apache/maven/project/ConnectedResource.java
@@ -0,0 +1,130 @@
+/*
+ * 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.project;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.maven.api.ProjectScope;
+import org.apache.maven.api.SourceRoot;
+import org.apache.maven.impl.DefaultSourceRoot;
+import org.apache.maven.model.Resource;
+
+/**
+ * A Resource wrapper that maintains a connection to the underlying project
model.
+ * When includes/excludes are modified, the changes are propagated back to the
project's SourceRoots.
+ */
+class ConnectedResource extends Resource {
+ private final SourceRoot originalSourceRoot;
+ private final ProjectScope scope;
+ private final MavenProject project;
+
+ ConnectedResource(SourceRoot sourceRoot, ProjectScope scope, MavenProject
project) {
+ super(org.apache.maven.api.model.Resource.newBuilder()
+ .directory(sourceRoot.directory().toString())
+ .includes(sourceRoot.includes())
+ .excludes(sourceRoot.excludes())
+ .filtering(Boolean.toString(sourceRoot.stringFiltering()))
+ .build());
+ this.originalSourceRoot = sourceRoot;
+ this.scope = scope;
+ this.project = project;
+ }
+
+ @Override
+ public void addInclude(String include) {
+ // Update the underlying Resource model
+ super.addInclude(include);
+
+ // Update the project's SourceRoots
+ updateProjectSourceRoot();
+ }
+
+ @Override
+ public void removeInclude(String include) {
+ // Update the underlying Resource model
+ super.removeInclude(include);
+
+ // Update the project's SourceRoots
+ updateProjectSourceRoot();
+ }
+
+ @Override
+ public void addExclude(String exclude) {
+ // Update the underlying Resource model
+ super.addExclude(exclude);
+
+ // Update the project's SourceRoots
+ updateProjectSourceRoot();
+ }
+
+ @Override
+ public void removeExclude(String exclude) {
+ // Update the underlying Resource model
+ super.removeExclude(exclude);
+
+ // Update the project's SourceRoots
+ updateProjectSourceRoot();
+ }
+
+ @Override
+ public void setIncludes(List<String> includes) {
+ // Update the underlying Resource model
+ super.setIncludes(includes);
+
+ // Update the project's SourceRoots
+ updateProjectSourceRoot();
+ }
+
+ @Override
+ public void setExcludes(List<String> excludes) {
+ // Update the underlying Resource model
+ super.setExcludes(excludes);
+
+ // Update the project's SourceRoots
+ updateProjectSourceRoot();
+ }
+
+ private void updateProjectSourceRoot() {
+ // Convert the LinkedHashSet to a List to maintain order
+ List<SourceRoot> sourcesList = new ArrayList<>(project.sources);
+
+ // Find the index of the original SourceRoot
+ int index = -1;
+ for (int i = 0; i < sourcesList.size(); i++) {
+ SourceRoot source = sourcesList.get(i);
+ if (source.scope() == originalSourceRoot.scope()
+ && source.language() == originalSourceRoot.language()
+ &&
source.directory().equals(originalSourceRoot.directory())) {
+ index = i;
+ break;
+ }
+ }
+
+ if (index >= 0) {
+ // Replace the SourceRoot at the same position
+ SourceRoot newSourceRoot = new
DefaultSourceRoot(project.getBaseDirectory(), scope, this.getDelegate());
+ sourcesList.set(index, newSourceRoot);
+
+ // Update the project's sources, preserving order
+ project.sources.clear();
+ project.sources.addAll(sourcesList);
+ }
+ }
+}
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 029a1680fe..3b934c922e 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
@@ -149,7 +149,7 @@ public class MavenProject implements Cloneable {
/**
* All sources of this project, in the order they were added.
*/
- private Set<SourceRoot> sources = new LinkedHashSet<>();
+ Set<SourceRoot> sources = new LinkedHashSet<>();
@Deprecated
private ArtifactRepository releaseArtifactRepository;
@@ -798,7 +798,10 @@ private Stream<SourceRoot> sources() {
@Override
public ListIterator<Resource> listIterator(int index) {
- return
sources().map(MavenProject::toResource).toList().listIterator(index);
+ return sources()
+ .map(sourceRoot -> toConnectedResource(sourceRoot,
scope))
+ .toList()
+ .listIterator(index);
}
@Override
@@ -828,6 +831,10 @@ private static Resource toResource(SourceRoot sourceRoot) {
.build());
}
+ private Resource toConnectedResource(SourceRoot sourceRoot, ProjectScope
scope) {
+ return new ConnectedResource(sourceRoot, scope, this);
+ }
+
private void addResource(ProjectScope scope, Resource resource) {
addSourceRoot(new DefaultSourceRoot(getBaseDirectory(), scope,
resource.getDelegate()));
}
diff --git
a/impl/maven-core/src/test/java/org/apache/maven/project/ResourceIncludeTest.java
b/impl/maven-core/src/test/java/org/apache/maven/project/ResourceIncludeTest.java
new file mode 100644
index 0000000000..cf3144a002
--- /dev/null
+++
b/impl/maven-core/src/test/java/org/apache/maven/project/ResourceIncludeTest.java
@@ -0,0 +1,191 @@
+/*
+ * 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.project;
+
+import java.nio.file.Path;
+import java.util.List;
+
+import org.apache.maven.api.Language;
+import org.apache.maven.api.ProjectScope;
+import org.apache.maven.impl.DefaultSourceRoot;
+import org.apache.maven.model.Resource;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Test for the fix of issue #2486: Includes are not added to existing project
resource.
+ */
+class ResourceIncludeTest {
+
+ private MavenProject project;
+
+ @BeforeEach
+ void setUp() {
+ project = new MavenProject();
+ // Set a dummy pom file to establish the base directory
+ project.setFile(new java.io.File("./pom.xml"));
+
+ // Add a resource source root to the project
+ project.addSourceRoot(
+ new DefaultSourceRoot(ProjectScope.MAIN, Language.RESOURCES,
Path.of("src/main/resources")));
+ }
+
+ @Test
+ void testAddIncludeToExistingResource() {
+ // Get the first resource
+ List<Resource> resources = project.getResources();
+ assertEquals(1, resources.size(), "Should have one resource");
+
+ Resource resource = resources.get(0);
+ assertEquals(Path.of("src/main/resources").toString(),
resource.getDirectory());
+ assertTrue(resource.getIncludes().isEmpty(), "Initially should have no
includes");
+
+ // Add an include - this should work now
+ resource.addInclude("test");
+
+ // Verify the include was added
+ assertEquals(1, resource.getIncludes().size(), "Should have one
include");
+ assertEquals("test", resource.getIncludes().get(0), "Include should be
'test'");
+
+ // Verify that getting resources again still shows the include
+ List<Resource> resourcesAfter = project.getResources();
+ assertEquals(1, resourcesAfter.size(), "Should still have one
resource");
+ Resource resourceAfter = resourcesAfter.get(0);
+ assertEquals(1, resourceAfter.getIncludes().size(), "Should still have
one include");
+ assertEquals("test", resourceAfter.getIncludes().get(0), "Include
should still be 'test'");
+ }
+
+ @Test
+ void testAddMultipleIncludes() {
+ Resource resource = project.getResources().get(0);
+
+ // Add multiple includes
+ resource.addInclude("*.xml");
+ resource.addInclude("*.properties");
+
+ // Verify both includes are present
+ assertEquals(2, resource.getIncludes().size(), "Should have two
includes");
+ assertTrue(resource.getIncludes().contains("*.xml"), "Should contain
*.xml");
+ assertTrue(resource.getIncludes().contains("*.properties"), "Should
contain *.properties");
+
+ // Verify persistence
+ Resource resourceAfter = project.getResources().get(0);
+ assertEquals(2, resourceAfter.getIncludes().size(), "Should still have
two includes");
+ assertTrue(resourceAfter.getIncludes().contains("*.xml"), "Should
still contain *.xml");
+ assertTrue(resourceAfter.getIncludes().contains("*.properties"),
"Should still contain *.properties");
+ }
+
+ @Test
+ void testRemoveInclude() {
+ Resource resource = project.getResources().get(0);
+
+ // Add includes
+ resource.addInclude("*.xml");
+ resource.addInclude("*.properties");
+ assertEquals(2, resource.getIncludes().size());
+
+ // Remove one include
+ resource.removeInclude("*.xml");
+
+ // Verify only one include remains
+ assertEquals(1, resource.getIncludes().size(), "Should have one
include");
+ assertEquals("*.properties", resource.getIncludes().get(0), "Should
only have *.properties");
+
+ // Verify persistence
+ Resource resourceAfter = project.getResources().get(0);
+ assertEquals(1, resourceAfter.getIncludes().size(), "Should still have
one include");
+ assertEquals("*.properties", resourceAfter.getIncludes().get(0),
"Should still only have *.properties");
+ }
+
+ @Test
+ void testSetIncludes() {
+ Resource resource = project.getResources().get(0);
+
+ // Set includes directly
+ resource.setIncludes(List.of("*.txt", "*.md"));
+
+ // Verify includes were set
+ assertEquals(2, resource.getIncludes().size(), "Should have two
includes");
+ assertTrue(resource.getIncludes().contains("*.txt"), "Should contain
*.txt");
+ assertTrue(resource.getIncludes().contains("*.md"), "Should contain
*.md");
+
+ // Verify persistence
+ Resource resourceAfter = project.getResources().get(0);
+ assertEquals(2, resourceAfter.getIncludes().size(), "Should still have
two includes");
+ assertTrue(resourceAfter.getIncludes().contains("*.txt"), "Should
still contain *.txt");
+ assertTrue(resourceAfter.getIncludes().contains("*.md"), "Should still
contain *.md");
+ }
+
+ @Test
+ void testSourceRootOrderingPreserved() {
+ // Add multiple resource source roots
+ project.addSourceRoot(
+ new DefaultSourceRoot(ProjectScope.MAIN, Language.RESOURCES,
Path.of("src/main/resources2")));
+ project.addSourceRoot(
+ new DefaultSourceRoot(ProjectScope.MAIN, Language.RESOURCES,
Path.of("src/main/resources3")));
+
+ // Verify initial order
+ List<Resource> resources = project.getResources();
+ assertEquals(3, resources.size(), "Should have three resources");
+ assertEquals(Path.of("src/main/resources").toString(),
resources.get(0).getDirectory());
+ assertEquals(Path.of("src/main/resources2").toString(),
resources.get(1).getDirectory());
+ assertEquals(Path.of("src/main/resources3").toString(),
resources.get(2).getDirectory());
+
+ // Modify the middle resource
+ resources.get(1).addInclude("*.properties");
+
+ // Verify order is preserved after modification
+ List<Resource> resourcesAfter = project.getResources();
+ assertEquals(3, resourcesAfter.size(), "Should still have three
resources");
+ assertEquals(
+ Path.of("src/main/resources").toString(),
resourcesAfter.get(0).getDirectory());
+ assertEquals(
+ Path.of("src/main/resources2").toString(),
resourcesAfter.get(1).getDirectory());
+ assertEquals(
+ Path.of("src/main/resources3").toString(),
resourcesAfter.get(2).getDirectory());
+
+ // Verify the modification was applied to the correct resource
+ assertTrue(
+ resourcesAfter.get(1).getIncludes().contains("*.properties"),
+ "Middle resource should have the include");
+ assertTrue(resourcesAfter.get(0).getIncludes().isEmpty(), "First
resource should not have includes");
+ assertTrue(resourcesAfter.get(2).getIncludes().isEmpty(), "Third
resource should not have includes");
+ }
+
+ @Test
+ void testUnderlyingSourceRootsUpdated() {
+ Resource resource = project.getResources().get(0);
+
+ // Add an include
+ resource.addInclude("*.xml");
+
+ // Verify that the underlying SourceRoot collection was updated
+ java.util.stream.Stream<org.apache.maven.api.SourceRoot>
resourceSourceRoots =
+ project.getEnabledSourceRoots(ProjectScope.MAIN,
Language.RESOURCES);
+
+ java.util.List<org.apache.maven.api.SourceRoot> sourceRootsList =
resourceSourceRoots.toList();
+ assertEquals(1, sourceRootsList.size(), "Should have one resource
source root");
+
+ org.apache.maven.api.SourceRoot sourceRoot = sourceRootsList.get(0);
+ assertTrue(sourceRoot.includes().contains("*.xml"), "Underlying
SourceRoot should contain the include");
+ }
+}