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,