Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> 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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,43 +20,102 @@

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;
import org.junit.jupiter.api.Test;
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");
mavenProject.setVersion("1.0-SNAPSHOT");
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 <abbr>GAV</abbr>.
*
* @param expectedGAV the actual <abbr>GAV</abbr> that the exception message should contain
* @param actualGAV the actual <abbr>GAV</abbr> 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<String> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public DefaultSourceRoot(
@Nonnull Language language,
@Nullable String moduleName,
@Nullable Version targetVersionOrNull,
@Nullable Path directory,
@Nonnull Path directory,
@Nullable List<String> includes,
@Nullable List<String> excludes,
boolean stringFiltering,
Expand Down