From fee69f2f89c6f32dbef0ec55a7f6e8c071d4b6a9 Mon Sep 17 00:00:00 2001
From: Guillaume Nodet <gnodet@gmail.com>
Date: Wed, 9 Oct 2024 13:07:31 +0200
Subject: [PATCH] [MNG-8294] Consistency checks when loading parent (#1784)

---
 .../api/services/ModelBuilderRequest.java     |   4 +
 api/maven-api-model/src/main/mdo/maven.mdo    |  18 +-
 .../impl/model/DefaultModelBuilder.java       | 206 +++++++++++-------
 .../impl/model/DefaultModelValidator.java     |  18 ++
 .../impl/DefaultConsumerPomBuilder.java       | 191 ++++++----------
 .../maven/project/DefaultProjectBuilder.java  |  42 ++--
 .../impl/ConsumerPomBuilderTest.java          |  75 ++++---
 .../model/building/DefaultModelBuilder.java   |   6 +-
 8 files changed, 296 insertions(+), 264 deletions(-)

diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelBuilderRequest.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelBuilderRequest.java
index ccd39248b1..b9b3ebed9e 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelBuilderRequest.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelBuilderRequest.java
@@ -52,6 +52,10 @@ public interface ModelBuilderRequest {
          * The request is for building a model from a POM file in a project on the filesystem.
          */
         BUILD_POM,
+        /**
+         * The request is for building the consumer POM.
+         */
+        CONSUMER_POM,
         /**
          * The request is for building a model from a parent POM file from a downloaded artifact.
          */
diff --git a/api/maven-api-model/src/main/mdo/maven.mdo b/api/maven-api-model/src/main/mdo/maven.mdo
index 7df64f342b..4e29c4ffaf 100644
--- a/api/maven-api-model/src/main/mdo/maven.mdo
+++ b/api/maven-api-model/src/main/mdo/maven.mdo
@@ -1744,20 +1744,16 @@
           <name>relativePath</name>
           <version>4.0.0+</version>
           <description>
-            The relative path of the parent {@code pom.xml} file within the checkout.
-            If not specified, it defaults to {@code ../pom.xml}.
+            The relative path of the parent subproject POM file or directory within the checkout.
+            If not specified, it defaults to {@code ..}, i.e. the parent directory.
             Maven looks for the parent POM first in this location on
-            the filesystem, then the local repository, and lastly in the remote repo.
-            {@code relativePath} allows you to select a different location,
-            for example when your structure is flat, or deeper without an intermediate parent POM.
-            However, the group ID, artifact ID and version are still required,
-            and must match the file in the location given, or it will revert to the repository for the POM.
-            This feature is only for enhancing the development in a local checkout of that project.
-            Set the value to an empty string in case you want to disable the feature and always resolve
-            the parent POM from the repositories.
+            the filesystem if explicitly provided, then in the reactor if groupId and artifactId are provided,
+            then in the default parent directory, then the local repository, and lastly in the remote repo.
+            However, if the both relative path and the group ID / artifact ID are provided,
+            they must match the file in the location given.
+            Specify either the {@code relativePath} or the {@code groupId}/{@code artifactId}, not both.
           </description>
           <type>String</type>
-          <defaultValue>..</defaultValue>
         </field>
       </fields>
       <codeSegments>
diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelBuilder.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelBuilder.java
index 4de108f1ff..bb1a7e23b7 100644
--- a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelBuilder.java
+++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelBuilder.java
@@ -34,7 +34,6 @@ import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Optional;
 import java.util.Properties;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
@@ -829,6 +828,14 @@ public class DefaultModelBuilder implements ModelBuilder {
             }
 
             result.setEffectiveModel(resultModel);
+            // Set the default relative path for the parent in the file model
+            if (result.getFileModel().getParent() != null
+                    && result.getFileModel().getParent().getRelativePath() == null) {
+                result.setFileModel(result.getFileModel()
+                        .withParent(result.getFileModel()
+                                .getParent()
+                                .withRelativePath(resultModel.getParent().getRelativePath())));
+            }
 
             // effective model validation
             modelValidator.validateEffectiveModel(
@@ -849,10 +856,7 @@ public class DefaultModelBuilder implements ModelBuilder {
 
             Parent parent = childModel.getParent();
             if (parent != null) {
-                parentModel = readParentLocally(childModel);
-                if (parentModel == null) {
-                    parentModel = resolveAndReadParentExternally(childModel);
-                }
+                parentModel = resolveParent(childModel);
 
                 if (!"pom".equals(parentModel.getPackaging())) {
                     add(
@@ -877,43 +881,56 @@ public class DefaultModelBuilder implements ModelBuilder {
             return parentModel;
         }
 
+        private Model resolveParent(Model childModel) {
+            Model parentModel = null;
+            if (request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_POM
+                    || request.getRequestType() == ModelBuilderRequest.RequestType.CONSUMER_POM) {
+                parentModel = readParentLocally(childModel);
+            }
+            if (parentModel == null) {
+                parentModel = resolveAndReadParentExternally(childModel);
+            }
+            return parentModel;
+        }
+
         private Model readParentLocally(Model childModel) throws ModelBuilderException {
-            ModelSource candidateSource = getParentPomFile(childModel, request.getSource());
+            ModelSource candidateSource = null;
+
+            Parent parent = childModel.getParent();
+            String parentPath = parent.getRelativePath();
+            if (parentPath != null && !parentPath.isEmpty()) {
+                candidateSource = request.getSource().resolve(modelProcessor::locateExistingPom, parentPath);
+                if (candidateSource == null) {
+                    wrongParentRelativePath(childModel);
+                    return null;
+                }
+            }
+            if (candidateSource == null) {
+                candidateSource = resolveReactorModel(parent.getGroupId(), parent.getArtifactId(), parent.getVersion());
+            }
+            if (candidateSource == null) {
+                candidateSource = request.getSource().resolve(modelProcessor::locateExistingPom, "..");
+            }
+
             if (candidateSource == null) {
                 return null;
             }
 
             Model candidateModel = derive(candidateSource).readAsParentModel();
 
-            //
-            // TODO jvz Why isn't all this checking the job of the duty of the workspace resolver, we know that we
-            // have a model that is suitable, yet more checks are done here and the one for the version is problematic
-            // before because with parents as ranges it will never work in this scenario.
-            //
-
             String groupId = getGroupId(candidateModel);
             String artifactId = candidateModel.getArtifactId();
+            String version = getVersion(candidateModel);
 
-            Parent parent = childModel.getParent();
+            // Ensure that relative path and GA match, if both are provided
             if (groupId == null
                     || !groupId.equals(parent.getGroupId())
                     || artifactId == null
                     || !artifactId.equals(parent.getArtifactId())) {
-                StringBuilder buffer = new StringBuilder(256);
-                buffer.append("'parent.relativePath'");
-                if (childModel != getRootModel()) {
-                    buffer.append(" of POM ").append(ModelProblemUtils.toSourceHint(childModel));
-                }
-                buffer.append(" points at ").append(groupId).append(':').append(artifactId);
-                buffer.append(" instead of ").append(parent.getGroupId()).append(':');
-                buffer.append(parent.getArtifactId()).append(", please verify your project structure");
-
-                setSource(childModel);
-                add(Severity.WARNING, Version.BASE, buffer.toString(), parent.getLocation(""));
+                mismatchRelativePathAndGA(childModel, groupId, artifactId);
                 return null;
             }
 
-            String version = getVersion(candidateModel);
             if (version != null && parent.getVersion() != null && !version.equals(parent.getVersion())) {
                 try {
                     VersionRange parentRange = versionParser.parseVersionRange(parent.getVersion());
@@ -946,15 +963,41 @@ public class DefaultModelBuilder implements ModelBuilder {
                     return null;
                 }
             }
-
-            //
-            // Here we just need to know that a version is fine to use but this validation we can do in our workspace
-            // resolver.
-            //
-
             return candidateModel;
         }
 
+        private void mismatchRelativePathAndGA(Model childModel, String groupId, String artifactId) {
+            Parent parent = childModel.getParent();
+            StringBuilder buffer = new StringBuilder(256);
+            buffer.append("'parent.relativePath'");
+            if (childModel != getRootModel()) {
+                buffer.append(" of POM ").append(ModelProblemUtils.toSourceHint(childModel));
+            }
+            buffer.append(" points at ").append(groupId).append(':').append(artifactId);
+            buffer.append(" instead of ").append(parent.getGroupId()).append(':');
+            buffer.append(parent.getArtifactId()).append(", please verify your project structure");
+
+            setSource(childModel);
+            boolean warn = MODEL_VERSION_4_0_0.equals(childModel.getModelVersion())
+                    || childModel.getParent().getRelativePath() == null;
+            add(warn ? Severity.WARNING : Severity.FATAL, Version.BASE, buffer.toString(), parent.getLocation(""));
+        }
+
+        private void wrongParentRelativePath(Model childModel) {
+            Parent parent = childModel.getParent();
+            String parentPath = parent.getRelativePath();
+            StringBuilder buffer = new StringBuilder(256);
+            buffer.append("'parent.relativePath'");
+            if (childModel != getRootModel()) {
+                buffer.append(" of POM ").append(ModelProblemUtils.toSourceHint(childModel));
+            }
+            buffer.append(" points at '").append(parentPath);
+            buffer.append("' but no POM could be found, please verify your project structure");
+
+            setSource(childModel);
+            add(Severity.FATAL, Version.BASE, buffer.toString(), parent.getLocation(""));
+        }
+
         Model resolveAndReadParentExternally(Model childModel) throws ModelBuilderException {
             ModelBuilderRequest request = this.request;
             setSource(childModel);
@@ -981,7 +1024,7 @@ public class DefaultModelBuilder implements ModelBuilder {
 
             ModelSource modelSource;
             try {
-                modelSource = resolveReactorModel(groupId, artifactId, version);
+                modelSource = resolveReactorModel(parent.getGroupId(), parent.getArtifactId(), parent.getVersion());
                 if (modelSource == null) {
                     AtomicReference<Parent> modified = new AtomicReference<>();
                     modelSource = modelResolver.resolveModel(request.getSession(), repositories, parent, modified);
@@ -1000,13 +1043,8 @@ public class DefaultModelBuilder implements ModelBuilder {
                     buffer.append(" for ").append(ModelProblemUtils.toId(childModel));
                 }
                 buffer.append(": ").append(e.getMessage());
-                if (childModel.getProjectDirectory() != null) {
-                    if (parent.getRelativePath() == null
-                            || parent.getRelativePath().isEmpty()) {
-                        buffer.append(" and 'parent.relativePath' points at no local POM");
-                    } else {
-                        buffer.append(" and 'parent.relativePath' points at wrong local POM");
-                    }
+                if (request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_POM) {
+                    buffer.append(" and parent could not be found in reactor");
                 }
 
                 add(Severity.FATAL, Version.BASE, buffer.toString(), parent.getLocation(""), e);
@@ -1050,8 +1088,7 @@ public class DefaultModelBuilder implements ModelBuilder {
             DefaultProfileActivationContext profileActivationContext = getProfileActivationContext(request, inputModel);
 
             setSource("(external profiles)");
-            List<Profile> activeExternalProfiles =
-                    profileSelector.getActiveProfiles(request.getProfiles(), profileActivationContext, this);
+            List<Profile> activeExternalProfiles = getActiveProfiles(request.getProfiles(), profileActivationContext);
 
             result.setActiveExternalProfiles(activeExternalProfiles);
 
@@ -1066,8 +1103,7 @@ public class DefaultModelBuilder implements ModelBuilder {
 
             profileActivationContext.setProjectProperties(inputModel.getProperties());
             setSource(inputModel);
-            List<Profile> activePomProfiles =
-                    profileSelector.getActiveProfiles(inputModel.getProfiles(), profileActivationContext, this);
+            List<Profile> activePomProfiles = getActiveProfiles(inputModel.getProfiles(), profileActivationContext);
 
             // model normalization
             setSource(inputModel);
@@ -1109,12 +1145,29 @@ public class DefaultModelBuilder implements ModelBuilder {
             }
 
             Model parentModel = readParent(inputModel);
+            // Now that we have read the parent, we can set the relative
+            // path correctly if it was not set in the input model
+            if (inputModel.getParent() != null && inputModel.getParent().getRelativePath() == null) {
+                String relPath;
+                if (parentModel.getPomFile() != null
+                        && (request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_POM
+                                || request.getRequestType() == ModelBuilderRequest.RequestType.CONSUMER_POM)) {
+                    relPath = inputModel
+                            .getPomFile()
+                            .getParent()
+                            .relativize(parentModel.getPomFile().getParent())
+                            .toString();
+                } else {
+                    relPath = "..";
+                }
+                inputModel = inputModel.withParent(inputModel.getParent().withRelativePath(relPath));
+            }
 
             List<Profile> parentInterpolatedProfiles =
                     interpolateActivations(parentModel.getProfiles(), profileActivationContext, this);
             // profile injection
             List<Profile> parentActivePomProfiles =
-                    profileSelector.getActiveProfiles(parentInterpolatedProfiles, profileActivationContext, this);
+                    getActiveProfiles(parentInterpolatedProfiles, profileActivationContext);
             Model injectedParentModel = profileInjector
                     .injectProfiles(parentModel, parentActivePomProfiles, request, this)
                     .withProfiles(List.of());
@@ -1131,8 +1184,7 @@ public class DefaultModelBuilder implements ModelBuilder {
                     interpolateActivations(model.getProfiles(), profileActivationContext, this);
 
             // profile injection
-            List<Profile> activePomProfiles =
-                    profileSelector.getActiveProfiles(interpolatedProfiles, profileActivationContext, this);
+            List<Profile> activePomProfiles = getActiveProfiles(interpolatedProfiles, profileActivationContext);
             result.setActivePomProfiles(activePomProfiles);
             model = profileInjector.injectProfiles(model, activePomProfiles, request, this);
             model = profileInjector.injectProfiles(model, activeExternalProfiles, request, this);
@@ -1160,6 +1212,15 @@ public class DefaultModelBuilder implements ModelBuilder {
             return resultModel;
         }
 
+        private List<Profile> getActiveProfiles(
+                Collection<Profile> interpolatedProfiles, DefaultProfileActivationContext profileActivationContext) {
+            if (request.getRequestType() != ModelBuilderRequest.RequestType.CONSUMER_POM) {
+                return profileSelector.getActiveProfiles(interpolatedProfiles, profileActivationContext, this);
+            } else {
+                return List.of();
+            }
+        }
+
         Model readFileModel() throws ModelBuilderException {
             Model model = cache(request.getSource(), FILE, this::doReadFileModel);
             // set the file model in the result outside the cache
@@ -1248,7 +1309,8 @@ public class DefaultModelBuilder implements ModelBuilder {
                 }
             }
 
-            if (request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_POM) {
+            if (request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_POM
+                    || request.getRequestType() == ModelBuilderRequest.RequestType.CONSUMER_POM) {
                 model = model.withPomFile(modelSource.getPath());
 
                 Parent parent = model.getParent();
@@ -1256,10 +1318,11 @@ public class DefaultModelBuilder implements ModelBuilder {
                     String groupId = parent.getGroupId();
                     String artifactId = parent.getArtifactId();
                     String version = parent.getVersion();
-                    String path = Optional.ofNullable(parent.getRelativePath()).orElse("..");
-                    if (version == null && !path.isEmpty()) {
+                    String path = parent.getRelativePath();
+                    if ((groupId == null || artifactId == null || version == null)
+                            && (path == null || !path.isEmpty())) {
                         Path pomFile = model.getPomFile();
-                        Path relativePath = Paths.get(path);
+                        Path relativePath = Paths.get(path != null ? path : "..");
                         Path pomPath = pomFile.resolveSibling(relativePath).normalize();
                         if (Files.isDirectory(pomPath)) {
                             pomPath = modelProcessor.locateExistingPom(pomPath);
@@ -1267,18 +1330,23 @@ public class DefaultModelBuilder implements ModelBuilder {
                         if (pomPath != null && Files.isRegularFile(pomPath)) {
                             Model parentModel =
                                     derive(ModelSource.fromPath(pomPath)).readFileModel();
-                            if (parentModel != null) {
-                                String parentGroupId = getGroupId(parentModel);
-                                String parentArtifactId = parentModel.getArtifactId();
-                                String parentVersion = getVersion(parentModel);
-                                if ((groupId == null || groupId.equals(parentGroupId))
-                                        && (artifactId == null || artifactId.equals(parentArtifactId))) {
-                                    model = model.withParent(parent.with()
-                                            .groupId(parentGroupId)
-                                            .artifactId(parentArtifactId)
-                                            .version(parentVersion)
-                                            .build());
-                                }
+                            String parentGroupId = getGroupId(parentModel);
+                            String parentArtifactId = parentModel.getArtifactId();
+                            String parentVersion = getVersion(parentModel);
+                            if ((groupId == null || groupId.equals(parentGroupId))
+                                    && (artifactId == null || artifactId.equals(parentArtifactId))
+                                    && (version == null || version.equals(parentVersion))) {
+                                model = model.withParent(parent.with()
+                                        .groupId(parentGroupId)
+                                        .artifactId(parentArtifactId)
+                                        .version(parentVersion)
+                                        .build());
+                            } else {
+                                mismatchRelativePathAndGA(model, parentGroupId, parentArtifactId);
+                            }
+                        } else {
+                            if (!MODEL_VERSION_4_0_0.equals(model.getModelVersion()) && path != null) {
+                                wrongParentRelativePath(model);
                             }
                         }
                     }
@@ -1369,7 +1437,8 @@ public class DefaultModelBuilder implements ModelBuilder {
             Model rawModel = readFileModel();
 
             if (!MODEL_VERSION_4_0_0.equals(rawModel.getModelVersion())
-                    && request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_POM) {
+                    && (request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_POM
+                            || request.getRequestType() == ModelBuilderRequest.RequestType.CONSUMER_POM)) {
                 rawModel = transformFileToRaw(rawModel);
             }
 
@@ -1600,7 +1669,7 @@ public class DefaultModelBuilder implements ModelBuilder {
                         .source(importSource)
                         .repositories(repositories)
                         .build();
-                DefaultModelBuilderSession modelBuilderSession = new DefaultModelBuilderSession(importRequest);
+                DefaultModelBuilderSession modelBuilderSession = derive(importRequest);
                 // build the effective model
                 modelBuilderSession.buildEffectiveModel(importIds);
                 importResult = modelBuilderSession.result;
@@ -1812,15 +1881,6 @@ public class DefaultModelBuilder implements ModelBuilder {
                 || rawChildModelVersion.equals("${project.parent.version}");
     }
 
-    private ModelSource getParentPomFile(Model childModel, ModelSource source) {
-        String parentPath = childModel.getParent().getRelativePath();
-        if (parentPath == null || parentPath.isEmpty()) {
-            return null;
-        } else {
-            return source.resolve(modelProcessor::locateExistingPom, parentPath);
-        }
-    }
-
     private Model getSuperModel(String modelVersion) {
         return superPomProvider.getSuperPom(modelVersion);
     }
diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelValidator.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelValidator.java
index 94eb22f1dc..8867a82573 100644
--- a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelValidator.java
+++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelValidator.java
@@ -326,6 +326,24 @@ public class DefaultModelValidator implements ModelValidator {
                         "is either LATEST or RELEASE (both of them are being deprecated)",
                         parent);
             }
+
+            if (parent.getRelativePath() != null
+                    && !parent.getRelativePath().isEmpty()
+                    && (parent.getGroupId() != null && !parent.getGroupId().isEmpty()
+                            || parent.getArtifactId() != null
+                                    && !parent.getArtifactId().isEmpty())
+                    && validationLevel >= ModelValidator.VALIDATION_LEVEL_MAVEN_4_0
+                    && VALID_MODEL_VERSIONS.contains(m.getModelVersion())
+                    && !Objects.equals(m.getModelVersion(), ModelBuilder.MODEL_VERSION_4_0_0)) {
+                addViolation(
+                        problems,
+                        Severity.WARNING,
+                        Version.BASE,
+                        "parent.relativePath",
+                        null,
+                        "only specify relativePath or groupId/artifactId in modelVersion 4.1.0",
+                        parent);
+            }
         }
 
         if (validationLevel == ModelValidator.VALIDATION_LEVEL_MINIMAL) {
diff --git a/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java b/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java
index d2953b2916..265dabdcd7 100644
--- a/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java
+++ b/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java
@@ -21,10 +21,14 @@ package org.apache.maven.internal.transformation.impl;
 import javax.inject.Inject;
 import javax.inject.Named;
 
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.List;
+import java.util.Objects;
 import java.util.stream.Collectors;
 
 import org.apache.maven.api.SessionData;
@@ -35,42 +39,17 @@ import org.apache.maven.api.model.Model;
 import org.apache.maven.api.model.ModelBase;
 import org.apache.maven.api.model.Profile;
 import org.apache.maven.api.model.Repository;
-import org.apache.maven.api.services.Interpolator;
 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.ModelProblemCollector;
 import org.apache.maven.api.services.ModelSource;
-import org.apache.maven.api.services.SuperPomProvider;
-import org.apache.maven.api.services.model.DependencyManagementImporter;
-import org.apache.maven.api.services.model.DependencyManagementInjector;
-import org.apache.maven.api.services.model.InheritanceAssembler;
+import org.apache.maven.api.services.Source;
 import org.apache.maven.api.services.model.LifecycleBindingsInjector;
-import org.apache.maven.api.services.model.ModelCacheFactory;
-import org.apache.maven.api.services.model.ModelInterpolator;
-import org.apache.maven.api.services.model.ModelNormalizer;
-import org.apache.maven.api.services.model.ModelPathTranslator;
-import org.apache.maven.api.services.model.ModelProcessor;
-import org.apache.maven.api.services.model.ModelResolver;
-import org.apache.maven.api.services.model.ModelUrlNormalizer;
-import org.apache.maven.api.services.model.ModelValidator;
-import org.apache.maven.api.services.model.ModelVersionParser;
-import org.apache.maven.api.services.model.PluginConfigurationExpander;
-import org.apache.maven.api.services.model.PluginManagementInjector;
-import org.apache.maven.api.services.model.ProfileActivationContext;
-import org.apache.maven.api.services.model.ProfileInjector;
-import org.apache.maven.api.services.model.ProfileSelector;
-import org.apache.maven.api.spi.ModelTransformer;
 import org.apache.maven.internal.impl.InternalSession;
-import org.apache.maven.internal.impl.model.DefaultModelBuilder;
-import org.apache.maven.internal.impl.model.DefaultProfileSelector;
-import org.apache.maven.internal.impl.model.ProfileActivationFilePathInterpolator;
 import org.apache.maven.model.v4.MavenModelVersion;
 import org.apache.maven.project.MavenProject;
 import org.eclipse.aether.RepositorySystemSession;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Named
 class DefaultConsumerPomBuilder implements ConsumerPomBuilder {
@@ -78,74 +57,14 @@ class DefaultConsumerPomBuilder implements ConsumerPomBuilder {
 
     public static final String POM_PACKAGING = "pom";
 
-    private final ProfileInjector profileInjector;
-    private final InheritanceAssembler inheritanceAssembler;
-    private final DependencyManagementImporter dependencyManagementImporter;
-    private final DependencyManagementInjector dependencyManagementInjector;
     private final LifecycleBindingsInjector lifecycleBindingsInjector;
-    private final ModelInterpolator modelInterpolator;
-    private final ModelNormalizer modelNormalizer;
-    private final ModelPathTranslator modelPathTranslator;
-    private final ModelProcessor modelProcessor;
-    private final ModelUrlNormalizer modelUrlNormalizer;
-    private final ModelValidator modelValidator;
-    private final PluginConfigurationExpander pluginConfigurationExpander;
-    private final PluginManagementInjector pluginManagementInjector;
-    private final SuperPomProvider superPomProvider;
-    private final ModelVersionParser versionParser;
-    private final ProfileActivationFilePathInterpolator profileActivationFilePathInterpolator;
-    private final List<ModelTransformer> transformers;
-    private final ModelCacheFactory modelCacheFactory;
-    private final ModelResolver modelResolver;
-    private final Interpolator interpolator;
 
     @Inject
     @SuppressWarnings("checkstyle:ParameterNumber")
-    DefaultConsumerPomBuilder(
-            ProfileInjector profileInjector,
-            InheritanceAssembler inheritanceAssembler,
-            DependencyManagementImporter dependencyManagementImporter,
-            DependencyManagementInjector dependencyManagementInjector,
-            LifecycleBindingsInjector lifecycleBindingsInjector,
-            ModelInterpolator modelInterpolator,
-            ModelNormalizer modelNormalizer,
-            ModelPathTranslator modelPathTranslator,
-            ModelProcessor modelProcessor,
-            ModelUrlNormalizer modelUrlNormalizer,
-            ModelValidator modelValidator,
-            PluginConfigurationExpander pluginConfigurationExpander,
-            PluginManagementInjector pluginManagementInjector,
-            SuperPomProvider superPomProvider,
-            ModelVersionParser versionParser,
-            ProfileActivationFilePathInterpolator profileActivationFilePathInterpolator,
-            List<ModelTransformer> transformers,
-            ModelCacheFactory modelCacheFactory,
-            ModelResolver modelResolver,
-            Interpolator interpolator) {
-        this.profileInjector = profileInjector;
-        this.inheritanceAssembler = inheritanceAssembler;
-        this.dependencyManagementImporter = dependencyManagementImporter;
-        this.dependencyManagementInjector = dependencyManagementInjector;
+    DefaultConsumerPomBuilder(LifecycleBindingsInjector lifecycleBindingsInjector) {
         this.lifecycleBindingsInjector = lifecycleBindingsInjector;
-        this.modelInterpolator = modelInterpolator;
-        this.modelNormalizer = modelNormalizer;
-        this.modelPathTranslator = modelPathTranslator;
-        this.modelProcessor = modelProcessor;
-        this.modelUrlNormalizer = modelUrlNormalizer;
-        this.modelValidator = modelValidator;
-        this.pluginConfigurationExpander = pluginConfigurationExpander;
-        this.pluginManagementInjector = pluginManagementInjector;
-        this.superPomProvider = superPomProvider;
-        this.versionParser = versionParser;
-        this.profileActivationFilePathInterpolator = profileActivationFilePathInterpolator;
-        this.transformers = transformers;
-        this.modelCacheFactory = modelCacheFactory;
-        this.modelResolver = modelResolver;
-        this.interpolator = interpolator;
     }
 
-    private final Logger logger = LoggerFactory.getLogger(getClass());
-
     @Override
     public Model build(RepositorySystemSession session, MavenProject project, Path src) throws ModelBuilderException {
         Model model = project.getModel().getDelegate();
@@ -174,49 +93,18 @@ class DefaultConsumerPomBuilder implements ConsumerPomBuilder {
 
     private ModelBuilderResult buildModel(RepositorySystemSession session, MavenProject project, Path src)
             throws ModelBuilderException {
-        ProfileSelector customSelector = new DefaultProfileSelector() {
-            @Override
-            public List<Profile> getActiveProfiles(
-                    Collection<Profile> profiles, ProfileActivationContext context, ModelProblemCollector problems) {
-                return new ArrayList<>();
-            }
-        };
-        // TODO: the custom selector should be used as a flag on the request
-        DefaultModelBuilder modelBuilder = new DefaultModelBuilder(
-                modelProcessor,
-                modelValidator,
-                modelNormalizer,
-                modelInterpolator,
-                modelPathTranslator,
-                modelUrlNormalizer,
-                superPomProvider,
-                inheritanceAssembler,
-                customSelector,
-                profileInjector,
-                pluginManagementInjector,
-                dependencyManagementInjector,
-                dependencyManagementImporter,
-                pluginConfigurationExpander,
-                profileActivationFilePathInterpolator,
-                versionParser,
-                transformers,
-                modelCacheFactory,
-                modelResolver,
-                interpolator);
         InternalSession iSession = InternalSession.from(session);
         ModelBuilderRequest.ModelBuilderRequestBuilder request = ModelBuilderRequest.builder();
-        request.requestType(ModelBuilderRequest.RequestType.BUILD_POM);
+        request.requestType(ModelBuilderRequest.RequestType.CONSUMER_POM);
         request.session(iSession);
-        request.source(ModelSource.fromPath(src));
+        // in order to resolve parents, we need to fake being at the correct location
+        request.source(new PomConsumerModelSource(project.getModel().getPomPath(), src));
         request.locationTracking(false);
         request.systemProperties(session.getSystemProperties());
         request.userProperties(session.getUserProperties());
         request.lifecycleBindingsInjector(lifecycleBindingsInjector::injectLifecycleBindings);
         ModelBuilder.ModelBuilderSession mbSession =
                 iSession.getData().get(SessionData.key(ModelBuilder.ModelBuilderSession.class));
-        if (mbSession == null) {
-            mbSession = modelBuilder.newSession();
-        }
         return mbSession.build(request.build());
     }
 
@@ -320,4 +208,63 @@ class DefaultConsumerPomBuilder implements ConsumerPomBuilder {
                 .filter(r -> !org.apache.maven.api.Repository.CENTRAL_ID.equals(r.getId()))
                 .collect(Collectors.toList());
     }
+
+    static class PomConsumerModelSource implements ModelSource {
+        final Path path;
+        final Path src;
+
+        PomConsumerModelSource(Path path, Path src) {
+            this.path = path;
+            this.src = src;
+        }
+
+        @Override
+        public Path getPath() {
+            return path;
+        }
+
+        @Override
+        public InputStream openStream() throws IOException {
+            return Files.newInputStream(src);
+        }
+
+        @Override
+        public String getLocation() {
+            return src.toString();
+        }
+
+        @Override
+        public Source resolve(String relative) {
+            return ModelSource.fromPath(path.resolve(relative));
+        }
+
+        @Override
+        public ModelSource resolve(ModelLocator locator, String relative) {
+            String norm = relative.replace('\\', File.separatorChar).replace('/', File.separatorChar);
+            Path path = getPath().getParent().resolve(norm);
+            Path relatedPom = locator.locateExistingPom(path);
+            if (relatedPom != null) {
+                return ModelSource.fromPath(relatedPom);
+            }
+            return null;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            return this == o
+                    || o.getClass() == getClass()
+                            && Objects.equals(path, ((PomConsumerModelSource) o).path)
+                            && Objects.equals(src, ((PomConsumerModelSource) o).src);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(path, src);
+        }
+
+        @Override
+        public String toString() {
+            return "PomConsumerModelSource[" + "path=" + path + ']';
+        }
+    }
 }
diff --git a/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java b/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java
index 236ffe0055..954d3faf6d 100644
--- a/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java
+++ b/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java
@@ -74,6 +74,7 @@ import org.apache.maven.artifact.repository.ArtifactRepository;
 import org.apache.maven.bridge.MavenRepositorySystem;
 import org.apache.maven.internal.impl.InternalSession;
 import org.apache.maven.internal.impl.resolver.ArtifactDescriptorUtils;
+import org.apache.maven.model.building.DefaultModelProblem;
 import org.apache.maven.model.building.FileModelSource;
 import org.apache.maven.model.building.ModelBuildingRequest;
 import org.apache.maven.model.building.ModelSource2;
@@ -499,6 +500,11 @@ public class DefaultProjectBuilder implements ProjectBuilder {
             List<ProjectBuildingResult> results = new ArrayList<>();
             List<ModelBuilderResult> allModels = results(result).toList();
             for (ModelBuilderResult r : allModels) {
+                List<ModelProblem> problems = new ArrayList<>(r.getProblems());
+                results(r)
+                        .filter(c -> c != r)
+                        .flatMap(c -> c.getProblems().stream())
+                        .forEach(problems::remove);
                 if (r.getEffectiveModel() != null) {
                     File pom = r.getSource().getPath().toFile();
                     MavenProject project =
@@ -510,7 +516,7 @@ public class DefaultProjectBuilder implements ProjectBuilder {
                     project.setExecutionRoot(pom.equals(pomFile));
                     initProject(project, r);
                     project.setCollectedProjects(results(r)
-                            .filter(cr -> cr != r)
+                            .filter(cr -> cr != r && cr.getEffectiveModel() != null)
                             .map(cr -> projectIndex.get(cr.getEffectiveModel().getId()))
                             .collect(Collectors.toList()));
 
@@ -518,10 +524,9 @@ public class DefaultProjectBuilder implements ProjectBuilder {
                     if (request.isResolveDependencies()) {
                         resolutionResult = resolveDependencies(project);
                     }
-                    results.add(
-                            new DefaultProjectBuildingResult(project, convert(result.getProblems()), resolutionResult));
+                    results.add(new DefaultProjectBuildingResult(project, convert(problems), resolutionResult));
                 } else {
-                    results.add(new DefaultProjectBuildingResult(null, convert(result.getProblems()), null));
+                    results.add(new DefaultProjectBuildingResult(null, convert(problems), null));
                 }
             }
             return results;
@@ -535,20 +540,21 @@ public class DefaultProjectBuilder implements ProjectBuilder {
             if (problems == null) {
                 return null;
             }
-            return problems.stream()
-                    .map(p -> (org.apache.maven.model.building.ModelProblem)
-                            new org.apache.maven.model.building.DefaultModelProblem(
-                                    p.getMessage(),
-                                    org.apache.maven.model.building.ModelProblem.Severity.valueOf(
-                                            p.getSeverity().name()),
-                                    org.apache.maven.model.building.ModelProblem.Version.valueOf(
-                                            p.getVersion().name()),
-                                    p.getSource(),
-                                    p.getLineNumber(),
-                                    p.getColumnNumber(),
-                                    p.getModelId(),
-                                    p.getException()))
-                    .toList();
+            return problems.stream().map(p -> convert(p)).toList();
+        }
+
+        private static org.apache.maven.model.building.ModelProblem convert(ModelProblem p) {
+            return new DefaultModelProblem(
+                    p.getMessage(),
+                    org.apache.maven.model.building.ModelProblem.Severity.valueOf(
+                            p.getSeverity().name()),
+                    org.apache.maven.model.building.ModelProblem.Version.valueOf(
+                            p.getVersion().name()),
+                    p.getSource(),
+                    p.getLineNumber(),
+                    p.getColumnNumber(),
+                    p.getModelId(),
+                    p.getException());
         }
 
         @SuppressWarnings({"checkstyle:methodlength", "deprecation"})
diff --git a/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java b/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java
index 0cf6c5c92c..2b58df1dc9 100644
--- a/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java
+++ b/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java
@@ -20,9 +20,6 @@ package org.apache.maven.internal.transformation.impl;
 
 import javax.inject.Inject;
 
-import java.io.InputStream;
-import java.lang.reflect.Field;
-import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Arrays;
@@ -35,17 +32,17 @@ import org.apache.maven.api.SessionData;
 import org.apache.maven.api.model.Model;
 import org.apache.maven.api.model.Parent;
 import org.apache.maven.api.services.ModelBuilder;
+import org.apache.maven.api.services.ModelBuilderRequest;
 import org.apache.maven.api.services.ModelSource;
 import org.apache.maven.api.services.model.ModelResolver;
 import org.apache.maven.api.services.model.ModelResolverException;
 import org.apache.maven.api.spi.ModelTransformer;
 import org.apache.maven.api.spi.ModelTransformerException;
 import org.apache.maven.di.Injector;
+import org.apache.maven.execution.MavenExecutionRequest;
 import org.apache.maven.internal.impl.InternalMavenSession;
 import org.apache.maven.internal.impl.InternalSession;
-import org.apache.maven.internal.impl.model.DefaultModelBuilder;
 import org.apache.maven.internal.transformation.AbstractRepositoryTestCase;
-import org.apache.maven.model.v4.MavenStaxReader;
 import org.apache.maven.project.MavenProject;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -58,6 +55,9 @@ public class ConsumerPomBuilderTest extends AbstractRepositoryTestCase {
     @Inject
     ConsumerPomBuilder builder;
 
+    @Inject
+    ModelBuilder modelBuilder;
+
     @BeforeEach
     void setupTransformerContext() throws Exception {
         // We need to hack things a bit here to get the transformer context to work
@@ -71,33 +71,30 @@ public class ConsumerPomBuilderTest extends AbstractRepositoryTestCase {
         //    to maven central
         getContainer().lookup(Injector.class).bindImplicit(MyModelResolver.class);
         InternalSession iSession = InternalSession.from(session);
-        // set up the transformers
-        List<ModelTransformer> transformers = List.of(new CIFriendlyVersionModelTransformer(iSession));
-        Field transformersField = DefaultModelBuilder.class.getDeclaredField("transformers");
-        transformersField.setAccessible(true);
-        DefaultModelBuilder modelBuilder = (DefaultModelBuilder) getContainer().lookup(ModelBuilder.class);
-        transformersField.set(modelBuilder, transformers);
-        transformersField = DefaultConsumerPomBuilder.class.getDeclaredField("transformers");
-        transformersField.setAccessible(true);
-        transformersField.set(builder, transformers);
         // set up the model resolver
         iSession.getData().set(SessionData.key(ModelResolver.class), new MyModelResolver());
     }
 
     @Test
     void testTrivialConsumer() throws Exception {
-        MavenProject project;
-        Path file = Paths.get("src/test/resources/consumer/trivial/child/pom.xml");
-        try (InputStream inputStream = Files.newInputStream(file)) {
-            org.apache.maven.model.Model model =
-                    new org.apache.maven.model.Model(new MavenStaxReader().read(inputStream));
-            project = new MavenProject(model);
-            project.setOriginalModel(model);
-        }
         InternalMavenSession.from(InternalSession.from(session))
                 .getMavenSession()
                 .getRequest()
                 .setRootDirectory(Paths.get("src/test/resources/consumer/trivial"));
+
+        Path file = Paths.get("src/test/resources/consumer/trivial/child/pom.xml");
+
+        ModelBuilder.ModelBuilderSession mbs = modelBuilder.newSession();
+        InternalSession.from(session).getData().set(SessionData.key(ModelBuilder.ModelBuilderSession.class), mbs);
+        Model orgModel = mbs.build(ModelBuilderRequest.builder()
+                        .session(InternalSession.from(session))
+                        .source(ModelSource.fromPath(file))
+                        .requestType(ModelBuilderRequest.RequestType.BUILD_POM)
+                        .build())
+                .getEffectiveModel();
+
+        MavenProject project = new MavenProject(orgModel);
+        project.setOriginalModel(new org.apache.maven.model.Model(orgModel));
         Model model = builder.build(session, project, file);
 
         assertNotNull(model);
@@ -105,24 +102,26 @@ public class ConsumerPomBuilderTest extends AbstractRepositoryTestCase {
 
     @Test
     void testSimpleConsumer() throws Exception {
-        MavenProject project;
+        MavenExecutionRequest request = InternalMavenSession.from(InternalSession.from(session))
+                .getMavenSession()
+                .getRequest();
+        request.setRootDirectory(Paths.get("src/test/resources/consumer/simple"));
+        request.getUserProperties().setProperty("changelist", "MNG6957");
+
         Path file = Paths.get("src/test/resources/consumer/simple/simple-parent/simple-weather/pom.xml");
 
-        InternalMavenSession.from(InternalSession.from(session))
-                .getMavenSession()
-                .getRequest()
-                .getUserProperties()
-                .setProperty("changelist", "MNG6957");
-        try (InputStream inputStream = Files.newInputStream(file)) {
-            org.apache.maven.model.Model model =
-                    new org.apache.maven.model.Model(new MavenStaxReader().read(inputStream));
-            project = new MavenProject(model);
-            project.setOriginalModel(model);
-        }
-        InternalMavenSession.from(InternalSession.from(session))
-                .getMavenSession()
-                .getRequest()
-                .setRootDirectory(Paths.get("src/test/resources/consumer/simple"));
+        ModelBuilder.ModelBuilderSession mbs = modelBuilder.newSession();
+        InternalSession.from(session).getData().set(SessionData.key(ModelBuilder.ModelBuilderSession.class), mbs);
+        Model orgModel = mbs.build(ModelBuilderRequest.builder()
+                        .session(InternalSession.from(session))
+                        .source(ModelSource.fromPath(file))
+                        .requestType(ModelBuilderRequest.RequestType.BUILD_POM)
+                        .build())
+                .getEffectiveModel();
+
+        MavenProject project = new MavenProject(orgModel);
+        project.setOriginalModel(new org.apache.maven.model.Model(orgModel));
+        request.setRootDirectory(Paths.get("src/test/resources/consumer/simple"));
         Model model = builder.build(session, project, file);
 
         assertNotNull(model);
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java
index 990c2cfb80..8dd4de675c 100644
--- a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java
@@ -1616,8 +1616,10 @@ public class DefaultModelBuilder implements ModelBuilder {
         }
 
         String parentPath = childModel.getParent().getRelativePath();
-
-        if (parentPath == null || parentPath.isEmpty()) {
+        if (parentPath == null) {
+            parentPath = "..";
+            childModel.getParent().setRelativePath(parentPath);
+        } else if (parentPath.isEmpty()) {
             return null;
         }