diff --git a/api/maven-api-xml/src/main/java/org/apache/maven/api/xml/XmlNode.java b/api/maven-api-xml/src/main/java/org/apache/maven/api/xml/XmlNode.java index a78357a09109..45e2229f2f95 100644 --- a/api/maven-api-xml/src/main/java/org/apache/maven/api/xml/XmlNode.java +++ b/api/maven-api-xml/src/main/java/org/apache/maven/api/xml/XmlNode.java @@ -61,34 +61,66 @@ public interface XmlNode { @Deprecated(since = "4.0.0", forRemoval = true) String CHILDREN_COMBINATION_MODE_ATTRIBUTE = XmlService.CHILDREN_COMBINATION_MODE_ATTRIBUTE; + /** + * @deprecated since 4.0.0. + * Use {@link XmlService#CHILDREN_COMBINATION_MERGE} instead. + */ @Deprecated(since = "4.0.0", forRemoval = true) String CHILDREN_COMBINATION_MERGE = XmlService.CHILDREN_COMBINATION_MERGE; + + /** + * @deprecated since 4.0.0. + * Use {@link XmlService#CHILDREN_COMBINATION_APPEND} instead. + */ @Deprecated(since = "4.0.0", forRemoval = true) String CHILDREN_COMBINATION_APPEND = XmlService.CHILDREN_COMBINATION_APPEND; + /** * This default mode for combining children DOMs during merge means that where element names match, the process will * try to merge the element data, rather than putting the dominant and recessive elements (which share the same * element name) as siblings in the resulting DOM. + * + * @deprecated since 4.0.0. + * Use {@link XmlService#DEFAULT_CHILDREN_COMBINATION_MODE} instead. */ @Deprecated(since = "4.0.0", forRemoval = true) String DEFAULT_CHILDREN_COMBINATION_MODE = XmlService.DEFAULT_CHILDREN_COMBINATION_MODE; + /** + * @deprecated since 4.0.0. + * Use {@link XmlService#SELF_COMBINATION_MODE_ATTRIBUTE} instead. + */ @Deprecated(since = "4.0.0", forRemoval = true) String SELF_COMBINATION_MODE_ATTRIBUTE = XmlService.SELF_COMBINATION_MODE_ATTRIBUTE; - + /** + * @deprecated since 4.0.0. + * Use {@link XmlService#SELF_COMBINATION_OVERRIDE} instead. + */ @Deprecated(since = "4.0.0", forRemoval = true) String SELF_COMBINATION_OVERRIDE = XmlService.SELF_COMBINATION_OVERRIDE; + /** + * @deprecated since 4.0.0. + * Use {@link XmlService#SELF_COMBINATION_MERGE} instead. + */ + @Deprecated(since = "4.0.0", forRemoval = true) String SELF_COMBINATION_MERGE = XmlService.SELF_COMBINATION_MERGE; + /** + * @deprecated since 4.0.0. + * Use {@link XmlService#SELF_COMBINATION_REMOVE} instead. + */ @Deprecated(since = "4.0.0", forRemoval = true) String SELF_COMBINATION_REMOVE = XmlService.SELF_COMBINATION_REMOVE; /** * In case of complex XML structures, combining can be done based on id. + * + * @deprecated since 4.0.0. + * Use {@link XmlService#ID_COMBINATION_MODE_ATTRIBUTE} instead. */ @Deprecated(since = "4.0.0", forRemoval = true) String ID_COMBINATION_MODE_ATTRIBUTE = XmlService.ID_COMBINATION_MODE_ATTRIBUTE; @@ -96,6 +128,9 @@ public interface XmlNode { /** * In case of complex XML structures, combining can be done based on keys. * This is a comma separated list of attribute names. + * + * @deprecated since 4.0.0. + * Use {@link XmlService#KEYS_COMBINATION_MODE_ATTRIBUTE} instead. */ @Deprecated(since = "4.0.0", forRemoval = true) String KEYS_COMBINATION_MODE_ATTRIBUTE = XmlService.KEYS_COMBINATION_MODE_ATTRIBUTE; @@ -105,6 +140,9 @@ public interface XmlNode { * try to merge the element attributes and values, rather than overriding the recessive element completely with the * dominant one. This means that wherever the dominant element doesn't provide the value or a particular attribute, * that value or attribute will be set from the recessive DOM node. + * + * @deprecated since 4.0.0. + * Use {@link XmlService#DEFAULT_SELF_COMBINATION_MODE} instead. */ @Deprecated(since = "4.0.0", forRemoval = true) String DEFAULT_SELF_COMBINATION_MODE = XmlService.DEFAULT_SELF_COMBINATION_MODE; @@ -186,54 +224,89 @@ public interface XmlNode { Object inputLocation(); // Deprecated methods that delegate to new ones + /** + * @deprecated since 4.0.0. + * Use {@link #name()} instead. + */ @Deprecated(since = "4.0.0", forRemoval = true) @Nonnull default String getName() { return name(); } + /** + * @deprecated since 4.0.0. + * Use {@link #namespaceUri()} instead. + */ @Deprecated(since = "4.0.0", forRemoval = true) @Nonnull default String getNamespaceUri() { return namespaceUri(); } + /** + * @deprecated since 4.0.0. + * Use {@link #prefix()} instead. + */ @Deprecated(since = "4.0.0", forRemoval = true) @Nonnull default String getPrefix() { return prefix(); } - + /** + * @deprecated since 4.0.0. + * Use {@link #value()} instead. + */ @Deprecated(since = "4.0.0", forRemoval = true) @Nullable default String getValue() { return value(); } + /** + * @deprecated since 4.0.0. + * Use {@link #attributes()} instead. + */ @Deprecated(since = "4.0.0", forRemoval = true) @Nonnull default Map getAttributes() { return attributes(); } + /** + * @deprecated since 4.0.0. + * Use {@link #attribute(String)} instead. + */ @Deprecated(since = "4.0.0", forRemoval = true) @Nullable default String getAttribute(@Nonnull String name) { return attribute(name); } + /** + * @deprecated since 4.0.0. + * Use {@link #children()} instead. + */ @Deprecated(since = "4.0.0", forRemoval = true) @Nonnull default List getChildren() { return children(); } + /** + * @deprecated since 4.0.0. + * Use {@link #child(String)} instead. + */ @Deprecated(since = "4.0.0", forRemoval = true) @Nullable default XmlNode getChild(String name) { return child(name); } + /** + * @deprecated since 4.0.0. + * Use {@link #inputLocation()} instead. + */ @Deprecated(since = "4.0.0", forRemoval = true) @Nullable default Object getInputLocation() { @@ -241,7 +314,8 @@ default Object getInputLocation() { } /** - * @deprecated use {@link XmlService#merge(XmlNode, XmlNode, Boolean)} instead + * @deprecated since 4.0.0. + * Use {@link XmlService#merge(XmlNode, XmlNode, Boolean)} instead. */ @Deprecated(since = "4.0.0", forRemoval = true) default XmlNode merge(@Nullable XmlNode source) { @@ -249,13 +323,15 @@ default XmlNode merge(@Nullable XmlNode source) { } /** - * @deprecated use {@link XmlService#merge(XmlNode, XmlNode, Boolean)} instead + * @deprecated since 4.0.0. + * Use {@link XmlService#merge(XmlNode, XmlNode, Boolean)} instead. */ @Deprecated(since = "4.0.0", forRemoval = true) default XmlNode merge(@Nullable XmlNode source, @Nullable Boolean childMergeOverride) { return XmlService.merge(this, source, childMergeOverride); } + /** * Merge recessive into dominant and return either {@code dominant} * with merged information or a clone of {@code recessive} if @@ -265,7 +341,8 @@ default XmlNode merge(@Nullable XmlNode source, @Nullable Boolean childMergeOver * @param recessive if {@code null}, nothing will happen * @return the merged node * - * @deprecated use {@link XmlService#merge(XmlNode, XmlNode, Boolean)} instead + * @deprecated since 4.0.0. + * Use {@link XmlService#merge(XmlNode, XmlNode, Boolean)} instead. */ @Deprecated(since = "4.0.0", forRemoval = true) @Nullable diff --git a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultProjectManager.java b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultProjectManager.java index 7c495a7344bc..aae6429912f4 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultProjectManager.java +++ b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultProjectManager.java @@ -119,15 +119,55 @@ public void attachArtifact(@Nonnull Project project, @Nonnull ProducedArtifact a artifact.getExtension(), null); } - if (!Objects.equals(project.getGroupId(), artifact.getGroupId()) - || !Objects.equals(project.getArtifactId(), artifact.getArtifactId()) - || !Objects.equals( - project.getVersion(), artifact.getBaseVersion().toString())) { - throw new IllegalArgumentException( - "The produced artifact must have the same groupId/artifactId/version than the project it is attached to. Expecting " - + project.getGroupId() + ":" + project.getArtifactId() + ":" + project.getVersion() - + " but received " + artifact.getGroupId() + ":" + artifact.getArtifactId() + ":" - + artifact.getBaseVersion()); + // Verify groupId and version, intentionally allow artifactId to differ as Maven project may be + // multi-module with modular sources structure that provide module names used as artifactIds. + String g1 = project.getGroupId(); + String a1 = project.getArtifactId(); + String v1 = project.getVersion(); + String g2 = artifact.getGroupId(); + String a2 = artifact.getArtifactId(); + String v2 = artifact.getBaseVersion().toString(); + + // ArtifactId may differ only for multi-module projects, in which case + // it must match the module name from a source root in modular sources. + boolean isMultiModule = false; + boolean validArtifactId = Objects.equals(a1, a2); + for (SourceRoot sr : getSourceRoots(project)) { + Optional moduleName = sr.module(); + if (moduleName.isPresent()) { + isMultiModule = true; + if (moduleName.get().equals(a2)) { + validArtifactId = true; + break; + } + } + } + boolean isSameGroupAndVersion = Objects.equals(g1, g2) && Objects.equals(v1, v2); + if (!(isSameGroupAndVersion && validArtifactId)) { + String message; + if (isMultiModule) { + // Multi-module project: artifactId may match any declared module name + message = String.format( + "Cannot attach artifact to project: groupId and version must match the project, " + + "and artifactId must match either the project or a declared module name.%n" + + " Project coordinates: %s:%s:%s%n" + + " Artifact coordinates: %s:%s:%s%n", + g1, a1, v1, g2, a2, v2); + if (isSameGroupAndVersion) { + message += String.format( + " Hint: The artifactId '%s' does not match the project artifactId '%s' " + + "nor any declared module name in source roots.", + a2, a1); + } + } else { + // Non-modular project: artifactId must match exactly + message = String.format( + "Cannot attach artifact to project: groupId, artifactId and version must match the project.%n" + + " Project coordinates: %s:%s:%s%n" + + " Artifact coordinates: %s:%s:%s", + g1, a1, v1, g2, a2, v2); + } + throw new IllegalArgumentException(message); } getMavenProject(project) .addAttachedArtifact( diff --git a/impl/maven-core/src/test/java/org/apache/maven/internal/impl/DefaultProjectManagerTest.java b/impl/maven-core/src/test/java/org/apache/maven/internal/impl/DefaultProjectManagerTest.java index 560fd9941b6b..48e87f7cda65 100644 --- a/impl/maven-core/src/test/java/org/apache/maven/internal/impl/DefaultProjectManagerTest.java +++ b/impl/maven-core/src/test/java/org/apache/maven/internal/impl/DefaultProjectManagerTest.java @@ -20,11 +20,15 @@ import java.nio.file.Path; import java.nio.file.Paths; +import java.util.function.Supplier; +import org.apache.maven.api.Language; import org.apache.maven.api.ProducedArtifact; import org.apache.maven.api.Project; +import org.apache.maven.api.ProjectScope; import org.apache.maven.api.services.ArtifactManager; import org.apache.maven.impl.DefaultModelVersionParser; +import org.apache.maven.impl.DefaultSourceRoot; import org.apache.maven.impl.DefaultVersionParser; import org.apache.maven.project.MavenProject; import org.eclipse.aether.util.version.GenericVersionScheme; @@ -32,21 +36,30 @@ import org.mockito.Mockito; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.when; class DefaultProjectManagerTest { + private DefaultProjectManager projectManager; + + private Project project; + + private ProducedArtifact artifact; + + private Path artifactPath; + @Test void attachArtifact() { InternalMavenSession session = Mockito.mock(InternalMavenSession.class); ArtifactManager artifactManager = Mockito.mock(ArtifactManager.class); MavenProject mavenProject = new MavenProject(); - Project project = new DefaultProject(session, mavenProject); - ProducedArtifact artifact = Mockito.mock(ProducedArtifact.class); - Path path = Paths.get(""); + project = new DefaultProject(session, mavenProject); + artifact = Mockito.mock(ProducedArtifact.class); + artifactPath = Paths.get(""); DefaultVersionParser versionParser = new DefaultVersionParser(new DefaultModelVersionParser(new GenericVersionScheme())); - DefaultProjectManager projectManager = new DefaultProjectManager(session, artifactManager); + projectManager = new DefaultProjectManager(session, artifactManager); mavenProject.setGroupId("myGroup"); mavenProject.setArtifactId("myArtifact"); @@ -54,9 +67,55 @@ void attachArtifact() { when(artifact.getGroupId()).thenReturn("myGroup"); when(artifact.getArtifactId()).thenReturn("myArtifact"); when(artifact.getBaseVersion()).thenReturn(versionParser.parseVersion("1.0-SNAPSHOT")); - projectManager.attachArtifact(project, artifact, path); + projectManager.attachArtifact(project, artifact, artifactPath); + // Verify that an exception is thrown when the artifactId differs when(artifact.getArtifactId()).thenReturn("anotherArtifact"); - assertThrows(IllegalArgumentException.class, () -> projectManager.attachArtifact(project, artifact, path)); + assertExceptionMessageContains("myGroup:myArtifact:1.0-SNAPSHOT", "myGroup:anotherArtifact:1.0-SNAPSHOT"); + + // Add a Java module. It should relax the restriction on artifactId. + projectManager.addSourceRoot( + project, + new DefaultSourceRoot( + ProjectScope.MAIN, + Language.JAVA_FAMILY, + "org.foo.bar", + null, + Path.of("myProject"), + null, + null, + false, + null, + true)); + + // Verify that we get the same exception when the artifactId does not match the module name + assertExceptionMessageContains("", "anotherArtifact"); + + // Verify that no exception is thrown when the artifactId is the module name + when(artifact.getArtifactId()).thenReturn("org.foo.bar"); + projectManager.attachArtifact(project, artifact, artifactPath); + + // Verify that an exception is thrown when the groupId differs + when(artifact.getGroupId()).thenReturn("anotherGroup"); + assertExceptionMessageContains("myGroup:myArtifact:1.0-SNAPSHOT", "anotherGroup:org.foo.bar:1.0-SNAPSHOT"); + } + + /** + * Verifies that {@code projectManager.attachArtifact(…)} throws an exception, + * and that the expecption message contains the expected and actual GAV. + * + * @param expectedGAV the actual GAV that the exception message should contain + * @param actualGAV the actual GAV that the exception message should contain + */ + private void assertExceptionMessageContains(String expectedGAV, String actualGAV) { + String cause = assertThrows( + IllegalArgumentException.class, + () -> projectManager.attachArtifact(project, artifact, artifactPath)) + .getMessage(); + Supplier message = () -> + String.format("The exception message does not contain the expected GAV. Message was:%n%s%n", cause); + + assertTrue(cause.contains(expectedGAV), message); + assertTrue(cause.contains(actualGAV), message); } } 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 870b429517f4..d2b0142cfc4e 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 @@ -94,7 +94,7 @@ public DefaultSourceRoot( @Nonnull Language language, @Nullable String moduleName, @Nullable Version targetVersionOrNull, - @Nullable Path directory, + @Nonnull Path directory, @Nullable List includes, @Nullable List excludes, boolean stringFiltering,