Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -841,20 +841,51 @@ void buildEffectiveModel(Collection<String> importIds) throws ModelBuilderExcept
}

Model readParent(Model childModel, Parent parent, DefaultProfileActivationContext profileActivationContext) {
return readParent(childModel, parent, profileActivationContext, new LinkedHashSet<>());
}

Model readParent(
Model childModel,
Parent parent,
DefaultProfileActivationContext profileActivationContext,
Set<String> parentChain) {
Model parentModel;

if (parent != null) {
parentModel = resolveParent(childModel, parent, profileActivationContext);
// Check for circular parent resolution using model IDs
String parentId = parent.getGroupId() + ":" + parent.getArtifactId() + ":" + parent.getVersion();
if (!parentChain.add(parentId)) {
StringBuilder message = new StringBuilder("The parents form a cycle: ");
for (String id : parentChain) {
message.append(id).append(" -> ");
}
message.append(parentId);

if (!"pom".equals(parentModel.getPackaging())) {
add(
Severity.ERROR,
Version.BASE,
"Invalid packaging for parent POM " + ModelProblemUtils.toSourceHint(parentModel)
+ ", must be \"pom\" but is \"" + parentModel.getPackaging() + "\"",
parentModel.getLocation("packaging"));
add(Severity.FATAL, Version.BASE, message.toString());
throw newModelBuilderException();
}

try {
parentModel = resolveParent(childModel, parent, profileActivationContext, parentChain);

if (!"pom".equals(parentModel.getPackaging())) {
add(
Severity.ERROR,
Version.BASE,
"Invalid packaging for parent POM " + ModelProblemUtils.toSourceHint(parentModel)
+ ", must be \"pom\" but is \"" + parentModel.getPackaging() + "\"",
parentModel.getLocation("packaging"));
}
result.setParentModel(parentModel);

// Recursively read the parent's parent
if (parentModel.getParent() != null) {
readParent(parentModel, parentModel.getParent(), profileActivationContext, parentChain);
}
} finally {
// Remove from chain when done processing this parent
parentChain.remove(parentId);
}
result.setParentModel(parentModel);
} else {
String superModelVersion = childModel.getModelVersion();
if (superModelVersion == null || !KNOWN_MODEL_VERSIONS.contains(superModelVersion)) {
Expand All @@ -872,12 +903,21 @@ Model readParent(Model childModel, Parent parent, DefaultProfileActivationContex
private Model resolveParent(
Model childModel, Parent parent, DefaultProfileActivationContext profileActivationContext)
throws ModelBuilderException {
return resolveParent(childModel, parent, profileActivationContext, new LinkedHashSet<>());
}

private Model resolveParent(
Model childModel,
Parent parent,
DefaultProfileActivationContext profileActivationContext,
Set<String> parentChain)
throws ModelBuilderException {
Model parentModel = null;
if (isBuildRequest()) {
parentModel = readParentLocally(childModel, parent, profileActivationContext);
}
if (parentModel == null) {
parentModel = resolveAndReadParentExternally(childModel, parent, profileActivationContext);
parentModel = resolveAndReadParentExternally(childModel, parent, profileActivationContext, parentChain);
}
return parentModel;
}
Expand Down Expand Up @@ -948,7 +988,8 @@ private Model readParentLocally(
return null;
}

// Validate versions aren't inherited when using parent ranges the same way as when read externally.
// Validate versions aren't inherited when using parent ranges the same way as when read
// externally.
String rawChildModelVersion = childModel.getVersion();

if (rawChildModelVersion == null) {
Expand Down Expand Up @@ -1009,6 +1050,15 @@ private void wrongParentRelativePath(Model childModel) {
Model resolveAndReadParentExternally(
Model childModel, Parent parent, DefaultProfileActivationContext profileActivationContext)
throws ModelBuilderException {
return resolveAndReadParentExternally(childModel, parent, profileActivationContext, new LinkedHashSet<>());
}

Model resolveAndReadParentExternally(
Model childModel,
Parent parent,
DefaultProfileActivationContext profileActivationContext,
Set<String> parentChain)
throws ModelBuilderException {
ModelBuilderRequest request = this.request;
setSource(childModel);

Expand Down Expand Up @@ -1581,6 +1631,7 @@ Model readAsParentModel(DefaultProfileActivationContext profileActivationContext
}
// Add the activated profiles from cache to the result
addActivePomProfiles(cached.activatedProfiles());

return cached.model();
}
}
Expand All @@ -1590,6 +1641,7 @@ Model readAsParentModel(DefaultProfileActivationContext profileActivationContext
// that aren't essential to the final result. Only replay the final essential keys
// into the parent recording context to maintain clean cache keys and avoid
// over-recording during parent model processing.

DefaultProfileActivationContext ctx = profileActivationContext.start();
ParentModelWithProfiles modelWithProfiles = doReadAsParentModel(ctx);
DefaultProfileActivationContext.Record record = ctx.stop();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/*
* 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.impl.model;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

import org.apache.maven.api.Session;
import org.apache.maven.api.services.ModelBuilder;
import org.apache.maven.api.services.ModelBuilderException;
import org.apache.maven.api.services.ModelBuilderRequest;
import org.apache.maven.api.services.ModelBuilderResult;
import org.apache.maven.api.services.Sources;
import org.apache.maven.impl.standalone.ApiRunner;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* Test for parent resolution cycle detection.
*/
class ParentCycleDetectionTest {

Session session;
ModelBuilder modelBuilder;

@BeforeEach
void setup() {
session = ApiRunner.createSession();
modelBuilder = session.getService(ModelBuilder.class);
assertNotNull(modelBuilder);
}

@Test
void testParentResolutionCycleDetection(@TempDir Path tempDir) throws IOException {
// Create a parent resolution cycle: A -> B -> A
Path pomA = tempDir.resolve("a").resolve("pom.xml");
Files.createDirectories(pomA.getParent());
Files.writeString(
pomA,
"""
<project xmlns="http://maven.apache.org/POM/4.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 https://maven.apache.org/xsd/maven-4.1.0.xsd" root="true">
<modelVersion>4.1.0</modelVersion>
<groupId>test</groupId>
<artifactId>a</artifactId>
<version>1.0</version>
<parent>
<groupId>test</groupId>
<artifactId>b</artifactId>
<version>1.0</version>
</parent>
</project>
""");

Path pomB = tempDir.resolve("b").resolve("pom.xml");
Files.createDirectories(pomB.getParent());
Files.writeString(
pomB,
"""
<project xmlns="http://maven.apache.org/POM/4.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 https://maven.apache.org/xsd/maven-4.1.0.xsd">
<modelVersion>4.1.0</modelVersion>
<groupId>test</groupId>
<artifactId>b</artifactId>
<version>1.0</version>
<parent>
<groupId>test</groupId>
<artifactId>a</artifactId>
<version>1.0</version>
</parent>
</project>
""");

ModelBuilderRequest request = ModelBuilderRequest.builder()
.session(session)
.source(Sources.buildSource(pomA))
.requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT)
.build();

ModelBuilderException exception = assertThrows(ModelBuilderException.class, () -> {
modelBuilder.newSession().build(request);
});

assertTrue(
exception.getMessage().contains("The parents form a cycle"),
"Expected cycle detection error, but got: " + exception.getMessage());
}

@Test
void testMultipleModulesWithSameParentDoNotCauseCycle(@TempDir Path tempDir) throws IOException {
// Create a scenario like the failing test: multiple modules with the same parent
Path parentPom = tempDir.resolve("parent").resolve("pom.xml");
Files.createDirectories(parentPom.getParent());
Files.writeString(
parentPom,
"""
<project xmlns="http://maven.apache.org/POM/4.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 https://maven.apache.org/xsd/maven-4.1.0.xsd" root="true">
<modelVersion>4.1.0</modelVersion>
<groupId>test</groupId>
<artifactId>parent</artifactId>
<version>1.0</version>
<packaging>pom</packaging>
</project>
""");

Path moduleA = tempDir.resolve("module-a").resolve("pom.xml");
Files.createDirectories(moduleA.getParent());
Files.writeString(
moduleA,
"""
<project xmlns="http://maven.apache.org/POM/4.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 https://maven.apache.org/xsd/maven-4.1.0.xsd">
<modelVersion>4.1.0</modelVersion>
<parent>
<groupId>test</groupId>
<artifactId>parent</artifactId>
<version>1.0</version>
</parent>
<artifactId>module-a</artifactId>
</project>
""");

Path moduleB = tempDir.resolve("module-b").resolve("pom.xml");
Files.createDirectories(moduleB.getParent());
Files.writeString(
moduleB,
"""
<project xmlns="http://maven.apache.org/POM/4.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 https://maven.apache.org/xsd/maven-4.1.0.xsd">
<modelVersion>4.1.0</modelVersion>
<parent>
<groupId>test</groupId>
<artifactId>parent</artifactId>
<version>1.0</version>
</parent>
<artifactId>module-b</artifactId>
</project>
""");

// Both modules should be able to resolve their parent without cycle detection errors
ModelBuilderRequest requestA = ModelBuilderRequest.builder()
.session(session)
.source(Sources.buildSource(moduleA))
.requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT)
.build();

ModelBuilderRequest requestB = ModelBuilderRequest.builder()
.session(session)
.source(Sources.buildSource(moduleB))
.requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT)
.build();

// These should not throw exceptions
ModelBuilderResult resultA = modelBuilder.newSession().build(requestA);
ModelBuilderResult resultB = modelBuilder.newSession().build(requestB);

// Verify that both models were built successfully
assertTrue(resultA.getEffectiveModel().getGroupId().equals("test"));
assertTrue(resultB.getEffectiveModel().getGroupId().equals("test"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* 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.io.File;

import org.junit.jupiter.api.Test;

/**
* This is a test set for <a href="https://github.com/apache/maven/issues/11009">Issue #11009</a>.
*
* @author Guillaume Nodet
*/
public class MavenITmng11009StackOverflowParentResolutionTest extends AbstractMavenIntegrationTestCase {

public MavenITmng11009StackOverflowParentResolutionTest() {
super("[4.0.0-rc-3,)");
}

/**
* Test that circular parent resolution doesn't cause a StackOverflowError during project model building.
* This reproduces the issue where:
* - Root pom.xml has parent with relativePath="parent"
* - parent/pom.xml has parent without relativePath (defaults to "../pom.xml")
* - This creates a circular parent resolution that causes stack overflow in hashCode calculation
*
* @throws Exception in case of failure
*/
@Test
public void testStackOverflowInParentResolution() throws Exception {
File testDir = extractResources("/mng-11009-stackoverflow-parent-resolution");

Verifier verifier = newVerifier(testDir.getAbsolutePath());
verifier.setAutoclean(false);
verifier.deleteArtifacts("org.apache.maven.its.mng11009");

// This should fail gracefully with a meaningful error message, not with StackOverflowError
try {
verifier.addCliArgument("validate");
verifier.execute();
// If we get here without StackOverflowError, the fix is working
// The build may still fail with a different error (circular dependency), but that's expected
} catch (Exception e) {
// Check that it's not a StackOverflowError
String errorMessage = e.getMessage();
if (errorMessage != null && errorMessage.contains("StackOverflowError")) {
throw new AssertionError("Build failed with StackOverflowError, which should be fixed", e);
}
// Other errors are acceptable as the POM structure is intentionally problematic
}

// The main goal is to not get a StackOverflowError
// We expect some kind of circular dependency error instead
}
}
Loading
Loading