Skip to content
Merged
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 @@ -266,6 +266,10 @@ protected class ModelBuilderSessionState implements ModelProblemCollector {
List<RemoteRepository> externalRepositories;
List<RemoteRepository> repositories;

// Cycle detection chain shared across all derived sessions
// Contains both GAV coordinates (groupId:artifactId:version) and file paths
final Set<String> parentChain;

ModelBuilderSessionState(ModelBuilderRequest request) {
this(
request.getSession(),
Expand All @@ -275,7 +279,8 @@ protected class ModelBuilderSessionState implements ModelProblemCollector {
new ConcurrentHashMap<>(64),
List.of(),
repos(request),
repos(request));
repos(request),
new LinkedHashSet<>());
}

static List<RemoteRepository> repos(ModelBuilderRequest request) {
Expand All @@ -294,7 +299,8 @@ private ModelBuilderSessionState(
Map<GAKey, Set<ModelSource>> mappedSources,
List<RemoteRepository> pomRepositories,
List<RemoteRepository> externalRepositories,
List<RemoteRepository> repositories) {
List<RemoteRepository> repositories,
Set<String> parentChain) {
this.session = session;
this.request = request;
this.result = result;
Expand All @@ -303,6 +309,7 @@ private ModelBuilderSessionState(
this.pomRepositories = pomRepositories;
this.externalRepositories = externalRepositories;
this.repositories = repositories;
this.parentChain = parentChain;
this.result.setSource(this.request.getSource());
}

Expand All @@ -325,8 +332,18 @@ ModelBuilderSessionState derive(ModelBuilderRequest request, DefaultModelBuilder
if (session != request.getSession()) {
throw new IllegalArgumentException("Session mismatch");
}
// Create a new parentChain for each derived session to prevent cycle detection issues
// The parentChain now contains both GAV coordinates and file paths
return new ModelBuilderSessionState(
session, request, result, dag, mappedSources, pomRepositories, externalRepositories, repositories);
session,
request,
result,
dag,
mappedSources,
pomRepositories,
externalRepositories,
repositories,
new LinkedHashSet<>());
}

@Override
Expand Down Expand Up @@ -645,6 +662,13 @@ private void buildBuildPom() throws ModelBuilderException {
mbs.buildEffectiveModel(new LinkedHashSet<>());
} catch (ModelBuilderException e) {
// gathered with problem collector
// Propagate problems from child session to parent session
for (var problem : e.getResult()
.getProblemCollector()
.problems()
.toList()) {
getProblemCollector().reportProblem(problem);
}
} catch (RuntimeException t) {
exceptions.add(t);
} finally {
Expand Down Expand Up @@ -843,22 +867,48 @@ void buildEffectiveModel(Collection<String> importIds) throws ModelBuilderExcept
}
}

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

Parent parent = childModel.getParent();
if (parent != null) {
parentModel = resolveParent(childModel, 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 @@ -873,23 +923,29 @@ Model readParent(Model childModel, DefaultProfileActivationContext profileActiva
return parentModel;
}

private Model resolveParent(Model childModel, DefaultProfileActivationContext profileActivationContext)
private Model resolveParent(
Model childModel,
Parent parent,
DefaultProfileActivationContext profileActivationContext,
Set<String> parentChain)
throws ModelBuilderException {
Model parentModel = null;
if (isBuildRequest()) {
parentModel = readParentLocally(childModel, profileActivationContext);
parentModel = readParentLocally(childModel, parent, profileActivationContext, parentChain);
}
if (parentModel == null) {
parentModel = resolveAndReadParentExternally(childModel, profileActivationContext);
parentModel = resolveAndReadParentExternally(childModel, parent, profileActivationContext, parentChain);
}
return parentModel;
}

private Model readParentLocally(Model childModel, DefaultProfileActivationContext profileActivationContext)
private Model readParentLocally(
Model childModel,
Parent parent,
DefaultProfileActivationContext profileActivationContext,
Set<String> parentChain)
throws ModelBuilderException {
ModelSource candidateSource;

Parent parent = childModel.getParent();
String parentPath = parent.getRelativePath();
if (request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_PROJECT) {
if (parentPath != null && !parentPath.isEmpty()) {
Expand Down Expand Up @@ -921,56 +977,77 @@ private Model readParentLocally(Model childModel, DefaultProfileActivationContex
return null;
}

ModelBuilderSessionState derived = derive(candidateSource);
Model candidateModel = derived.readAsParentModel(profileActivationContext);
addActivePomProfiles(derived.result.getActivePomProfiles());
// Check for circular parent resolution using source locations (file paths)
// This must be done BEFORE calling derive() to prevent StackOverflowError
String sourceLocation = candidateSource.getLocation();

String groupId = getGroupId(candidateModel);
String artifactId = candidateModel.getArtifactId();
String version = getVersion(candidateModel);
if (!parentChain.add(sourceLocation)) {
StringBuilder message = new StringBuilder("The parents form a cycle: ");
for (String location : parentChain) {
message.append(location).append(" -> ");
}
message.append(sourceLocation);

// Ensure that relative path and GA match, if both are provided
if (groupId == null
|| !groupId.equals(parent.getGroupId())
|| artifactId == null
|| !artifactId.equals(parent.getArtifactId())) {
mismatchRelativePathAndGA(childModel, groupId, artifactId);
return null;
add(Severity.FATAL, Version.BASE, message.toString());
throw newModelBuilderException();
}

if (version != null && parent.getVersion() != null && !version.equals(parent.getVersion())) {
try {
VersionRange parentRange = versionParser.parseVersionRange(parent.getVersion());
if (!parentRange.contains(versionParser.parseVersion(version))) {
// version skew drop back to resolution from the repository
return null;
}
try {
ModelBuilderSessionState derived = derive(candidateSource);
Model candidateModel = derived.readAsParentModel(profileActivationContext, parentChain);
addActivePomProfiles(derived.result.getActivePomProfiles());

String groupId = getGroupId(candidateModel);
String artifactId = candidateModel.getArtifactId();
String version = getVersion(candidateModel);

// Ensure that relative path and GA match, if both are provided
if (groupId == null
|| !groupId.equals(parent.getGroupId())
|| artifactId == null
|| !artifactId.equals(parent.getArtifactId())) {
mismatchRelativePathAndGA(childModel, groupId, artifactId);
return null;
}

// Validate versions aren't inherited when using parent ranges the same way as when read externally.
String rawChildModelVersion = childModel.getVersion();
if (version != null && parent.getVersion() != null && !version.equals(parent.getVersion())) {
try {
VersionRange parentRange = versionParser.parseVersionRange(parent.getVersion());
if (!parentRange.contains(versionParser.parseVersion(version))) {
// version skew drop back to resolution from the repository
return null;
}

if (rawChildModelVersion == null) {
// Message below is checked for in the MNG-2199 core IT.
add(Severity.FATAL, Version.V31, "Version must be a constant", childModel.getLocation(""));
// Validate versions aren't inherited when using parent ranges the same way as when read
// externally.
String rawChildModelVersion = childModel.getVersion();

} else {
if (rawChildVersionReferencesParent(rawChildModelVersion)) {
if (rawChildModelVersion == null) {
// Message below is checked for in the MNG-2199 core IT.
add(
Severity.FATAL,
Version.V31,
"Version must be a constant",
childModel.getLocation("version"));
add(Severity.FATAL, Version.V31, "Version must be a constant", childModel.getLocation(""));

} else {
if (rawChildVersionReferencesParent(rawChildModelVersion)) {
// Message below is checked for in the MNG-2199 core IT.
add(
Severity.FATAL,
Version.V31,
"Version must be a constant",
childModel.getLocation("version"));
}
}
}

// MNG-2199: What else to check here ?
} catch (VersionParserException e) {
// invalid version range, so drop back to resolution from the repository
return null;
// MNG-2199: What else to check here ?
} catch (VersionParserException e) {
// invalid version range, so drop back to resolution from the repository
return null;
}
}
return candidateModel;
} finally {
// Remove the source location from the chain when we're done processing this parent
parentChain.remove(sourceLocation);
}
return candidateModel;
}

private void mismatchRelativePathAndGA(Model childModel, String groupId, String artifactId) {
Expand Down Expand Up @@ -1005,13 +1082,15 @@ private void wrongParentRelativePath(Model childModel) {
add(Severity.FATAL, Version.BASE, buffer.toString(), parent.getLocation(""));
}

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

Parent parent = childModel.getParent();

String groupId = parent.getGroupId();
String artifactId = parent.getArtifactId();
String version = parent.getVersion();
Expand Down Expand Up @@ -1064,7 +1143,7 @@ Model resolveAndReadParentExternally(Model childModel, DefaultProfileActivationC
.source(modelSource)
.build();

Model parentModel = derive(lenientRequest).readAsParentModel(profileActivationContext);
Model parentModel = derive(lenientRequest).readAsParentModel(profileActivationContext, parentChain);

if (!parent.getVersion().equals(version)) {
String rawChildModelVersion = childModel.getVersion();
Expand Down Expand Up @@ -1153,7 +1232,8 @@ private Model readEffectiveModel() throws ModelBuilderException {
profileActivationContext.setUserProperties(profileProps);
}

Model parentModel = readParent(activatedFileModel, profileActivationContext);
Model parentModel = readParent(
activatedFileModel, activatedFileModel.getParent(), profileActivationContext, parentChain);

// Now that we have read the parent, we can set the relative
// path correctly if it was not set in the input model
Expand Down Expand Up @@ -1597,9 +1677,10 @@ private Model doReadRawModel() throws ModelBuilderException {
private record ParentModelWithProfiles(Model model, List<Profile> activatedProfiles) {}

/**
* Reads the request source's parent.
* Reads the request source's parent with cycle detection.
*/
Model readAsParentModel(DefaultProfileActivationContext profileActivationContext) throws ModelBuilderException {
Model readAsParentModel(DefaultProfileActivationContext profileActivationContext, Set<String> parentChain)
throws ModelBuilderException {
Map<DefaultProfileActivationContext.Record, ParentModelWithProfiles> parentsPerContext =
cache(request.getSource(), PARENT, ConcurrentHashMap::new);

Expand All @@ -1626,7 +1707,7 @@ Model readAsParentModel(DefaultProfileActivationContext profileActivationContext
// 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);
ParentModelWithProfiles modelWithProfiles = doReadAsParentModel(ctx, parentChain);
DefaultProfileActivationContext.Record record = ctx.stop();
replayRecordIntoContext(record, profileActivationContext);

Expand All @@ -1636,9 +1717,10 @@ Model readAsParentModel(DefaultProfileActivationContext profileActivationContext
}

private ParentModelWithProfiles doReadAsParentModel(
DefaultProfileActivationContext childProfileActivationContext) throws ModelBuilderException {
DefaultProfileActivationContext childProfileActivationContext, Set<String> parentChain)
throws ModelBuilderException {
Model raw = readRawModel();
Model parentData = readParent(raw, childProfileActivationContext);
Model parentData = readParent(raw, raw.getParent(), childProfileActivationContext, parentChain);
Model parent = new DefaultInheritanceAssembler(new DefaultInheritanceAssembler.InheritanceModelMerger() {
@Override
protected void mergeModel_Modules(
Expand Down
Loading