View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.impl.model;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.lang.reflect.Field;
25  import java.nio.file.Files;
26  import java.nio.file.Path;
27  import java.nio.file.Paths;
28  import java.util.ArrayList;
29  import java.util.Arrays;
30  import java.util.Collection;
31  import java.util.HashMap;
32  import java.util.HashSet;
33  import java.util.Iterator;
34  import java.util.LinkedHashMap;
35  import java.util.LinkedHashSet;
36  import java.util.List;
37  import java.util.Map;
38  import java.util.Objects;
39  import java.util.Set;
40  import java.util.concurrent.ConcurrentHashMap;
41  import java.util.concurrent.CopyOnWriteArrayList;
42  import java.util.concurrent.Executor;
43  import java.util.concurrent.Executors;
44  import java.util.concurrent.atomic.AtomicReference;
45  import java.util.function.BiFunction;
46  import java.util.function.Supplier;
47  import java.util.function.UnaryOperator;
48  import java.util.stream.Collectors;
49  import java.util.stream.Stream;
50  
51  import org.apache.maven.api.Constants;
52  import org.apache.maven.api.RemoteRepository;
53  import org.apache.maven.api.Session;
54  import org.apache.maven.api.Type;
55  import org.apache.maven.api.VersionRange;
56  import org.apache.maven.api.annotations.Nonnull;
57  import org.apache.maven.api.annotations.Nullable;
58  import org.apache.maven.api.cache.CacheMetadata;
59  import org.apache.maven.api.cache.CacheRetention;
60  import org.apache.maven.api.di.Inject;
61  import org.apache.maven.api.di.Named;
62  import org.apache.maven.api.di.Singleton;
63  import org.apache.maven.api.feature.Features;
64  import org.apache.maven.api.model.Activation;
65  import org.apache.maven.api.model.Dependency;
66  import org.apache.maven.api.model.DependencyManagement;
67  import org.apache.maven.api.model.DeploymentRepository;
68  import org.apache.maven.api.model.DistributionManagement;
69  import org.apache.maven.api.model.Exclusion;
70  import org.apache.maven.api.model.InputLocation;
71  import org.apache.maven.api.model.InputSource;
72  import org.apache.maven.api.model.Model;
73  import org.apache.maven.api.model.Parent;
74  import org.apache.maven.api.model.Profile;
75  import org.apache.maven.api.model.Repository;
76  import org.apache.maven.api.services.BuilderProblem;
77  import org.apache.maven.api.services.BuilderProblem.Severity;
78  import org.apache.maven.api.services.Interpolator;
79  import org.apache.maven.api.services.MavenException;
80  import org.apache.maven.api.services.ModelBuilder;
81  import org.apache.maven.api.services.ModelBuilderException;
82  import org.apache.maven.api.services.ModelBuilderRequest;
83  import org.apache.maven.api.services.ModelBuilderResult;
84  import org.apache.maven.api.services.ModelProblem;
85  import org.apache.maven.api.services.ModelProblem.Version;
86  import org.apache.maven.api.services.ModelProblemCollector;
87  import org.apache.maven.api.services.ModelSource;
88  import org.apache.maven.api.services.ProblemCollector;
89  import org.apache.maven.api.services.RepositoryFactory;
90  import org.apache.maven.api.services.Request;
91  import org.apache.maven.api.services.RequestTrace;
92  import org.apache.maven.api.services.Result;
93  import org.apache.maven.api.services.Source;
94  import org.apache.maven.api.services.Sources;
95  import org.apache.maven.api.services.SuperPomProvider;
96  import org.apache.maven.api.services.VersionParserException;
97  import org.apache.maven.api.services.model.DependencyManagementImporter;
98  import org.apache.maven.api.services.model.DependencyManagementInjector;
99  import org.apache.maven.api.services.model.InheritanceAssembler;
100 import org.apache.maven.api.services.model.ModelInterpolator;
101 import org.apache.maven.api.services.model.ModelNormalizer;
102 import org.apache.maven.api.services.model.ModelPathTranslator;
103 import org.apache.maven.api.services.model.ModelProcessor;
104 import org.apache.maven.api.services.model.ModelResolver;
105 import org.apache.maven.api.services.model.ModelResolverException;
106 import org.apache.maven.api.services.model.ModelUrlNormalizer;
107 import org.apache.maven.api.services.model.ModelValidator;
108 import org.apache.maven.api.services.model.ModelVersionParser;
109 import org.apache.maven.api.services.model.PathTranslator;
110 import org.apache.maven.api.services.model.PluginConfigurationExpander;
111 import org.apache.maven.api.services.model.PluginManagementInjector;
112 import org.apache.maven.api.services.model.ProfileInjector;
113 import org.apache.maven.api.services.model.ProfileSelector;
114 import org.apache.maven.api.services.model.RootLocator;
115 import org.apache.maven.api.services.xml.XmlReaderException;
116 import org.apache.maven.api.services.xml.XmlReaderRequest;
117 import org.apache.maven.api.spi.ModelParserException;
118 import org.apache.maven.api.spi.ModelTransformer;
119 import org.apache.maven.impl.InternalSession;
120 import org.apache.maven.impl.RequestTraceHelper;
121 import org.apache.maven.impl.util.PhasingExecutor;
122 import org.slf4j.Logger;
123 import org.slf4j.LoggerFactory;
124 
125 /**
126  * The model builder is responsible for building the {@link Model} from the POM file.
127  * There are two ways to main use cases: the first one is to build the model from a POM file
128  * on the file system in order to actually build the project. The second one is to build the
129  * model for a dependency  or an external parent.
130  */
131 @Named
132 @Singleton
133 public class DefaultModelBuilder implements ModelBuilder {
134 
135     public static final String NAMESPACE_PREFIX = "http://maven.apache.org/POM/";
136     private static final String RAW = "raw";
137     private static final String FILE = "file";
138     private static final String IMPORT = "import";
139     private static final String PARENT = "parent";
140     private static final String MODEL = "model";
141 
142     private final Logger logger = LoggerFactory.getLogger(getClass());
143 
144     private final ModelProcessor modelProcessor;
145     private final ModelValidator modelValidator;
146     private final ModelNormalizer modelNormalizer;
147     private final ModelInterpolator modelInterpolator;
148     private final ModelPathTranslator modelPathTranslator;
149     private final ModelUrlNormalizer modelUrlNormalizer;
150     private final SuperPomProvider superPomProvider;
151     private final InheritanceAssembler inheritanceAssembler;
152     private final ProfileSelector profileSelector;
153     private final ProfileInjector profileInjector;
154     private final PluginManagementInjector pluginManagementInjector;
155     private final DependencyManagementInjector dependencyManagementInjector;
156     private final DependencyManagementImporter dependencyManagementImporter;
157     private final PluginConfigurationExpander pluginConfigurationExpander;
158     private final ModelVersionParser versionParser;
159     private final List<ModelTransformer> transformers;
160     private final ModelResolver modelResolver;
161     private final Interpolator interpolator;
162     private final PathTranslator pathTranslator;
163     private final RootLocator rootLocator;
164 
165     @SuppressWarnings("checkstyle:ParameterNumber")
166     @Inject
167     public DefaultModelBuilder(
168             ModelProcessor modelProcessor,
169             ModelValidator modelValidator,
170             ModelNormalizer modelNormalizer,
171             ModelInterpolator modelInterpolator,
172             ModelPathTranslator modelPathTranslator,
173             ModelUrlNormalizer modelUrlNormalizer,
174             SuperPomProvider superPomProvider,
175             InheritanceAssembler inheritanceAssembler,
176             ProfileSelector profileSelector,
177             ProfileInjector profileInjector,
178             PluginManagementInjector pluginManagementInjector,
179             DependencyManagementInjector dependencyManagementInjector,
180             DependencyManagementImporter dependencyManagementImporter,
181             PluginConfigurationExpander pluginConfigurationExpander,
182             ModelVersionParser versionParser,
183             @Nullable List<ModelTransformer> transformers,
184             ModelResolver modelResolver,
185             Interpolator interpolator,
186             PathTranslator pathTranslator,
187             RootLocator rootLocator) {
188         this.modelProcessor = modelProcessor;
189         this.modelValidator = modelValidator;
190         this.modelNormalizer = modelNormalizer;
191         this.modelInterpolator = modelInterpolator;
192         this.modelPathTranslator = modelPathTranslator;
193         this.modelUrlNormalizer = modelUrlNormalizer;
194         this.superPomProvider = superPomProvider;
195         this.inheritanceAssembler = inheritanceAssembler;
196         this.profileSelector = profileSelector;
197         this.profileInjector = profileInjector;
198         this.pluginManagementInjector = pluginManagementInjector;
199         this.dependencyManagementInjector = dependencyManagementInjector;
200         this.dependencyManagementImporter = dependencyManagementImporter;
201         this.pluginConfigurationExpander = pluginConfigurationExpander;
202         this.versionParser = versionParser;
203         this.transformers = transformers;
204         this.modelResolver = modelResolver;
205         this.interpolator = interpolator;
206         this.pathTranslator = pathTranslator;
207         this.rootLocator = rootLocator;
208     }
209 
210     @Override
211     public ModelBuilderSession newSession() {
212         return new ModelBuilderSessionImpl();
213     }
214 
215     protected class ModelBuilderSessionImpl implements ModelBuilderSession {
216         ModelBuilderSessionState mainSession;
217 
218         /**
219          * Builds a model based on the provided ModelBuilderRequest.
220          *
221          * @param request The request containing the parameters for building the model.
222          * @return The result of the model building process.
223          * @throws ModelBuilderException If an error occurs during model building.
224          */
225         @Override
226         public ModelBuilderResult build(ModelBuilderRequest request) throws ModelBuilderException {
227             RequestTraceHelper.ResolverTrace trace = RequestTraceHelper.enter(request.getSession(), request);
228             try {
229                 // Create or derive a session based on the request
230                 ModelBuilderSessionState session;
231                 if (mainSession == null) {
232                     mainSession = new ModelBuilderSessionState(request);
233                     session = mainSession;
234                 } else {
235                     session = mainSession.derive(
236                             request,
237                             new DefaultModelBuilderResult(request, ProblemCollector.create(mainSession.session)));
238                 }
239                 // Build the request
240                 if (request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_PROJECT) {
241                     // build the build poms
242                     session.buildBuildPom();
243                 } else {
244                     // simply build the effective model
245                     session.buildEffectiveModel(new LinkedHashSet<>());
246                 }
247                 return session.result;
248             } finally {
249                 RequestTraceHelper.exit(trace);
250             }
251         }
252     }
253 
254     protected class ModelBuilderSessionState implements ModelProblemCollector {
255         final Session session;
256         final ModelBuilderRequest request;
257         final DefaultModelBuilderResult result;
258         final Graph dag;
259         final Map<GAKey, Set<ModelSource>> mappedSources;
260 
261         String source;
262         Model sourceModel;
263         Model rootModel;
264 
265         List<RemoteRepository> pomRepositories;
266         List<RemoteRepository> externalRepositories;
267         List<RemoteRepository> repositories;
268 
269         // Cycle detection chain shared across all derived sessions
270         // Contains both GAV coordinates (groupId:artifactId:version) and file paths
271         final Set<String> parentChain;
272 
273         ModelBuilderSessionState(ModelBuilderRequest request) {
274             this(
275                     request.getSession(),
276                     request,
277                     new DefaultModelBuilderResult(request, ProblemCollector.create(request.getSession())),
278                     new Graph(),
279                     new ConcurrentHashMap<>(64),
280                     List.of(),
281                     repos(request),
282                     repos(request),
283                     new LinkedHashSet<>());
284         }
285 
286         static List<RemoteRepository> repos(ModelBuilderRequest request) {
287             return List.copyOf(
288                     request.getRepositories() != null
289                             ? request.getRepositories()
290                             : request.getSession().getRemoteRepositories());
291         }
292 
293         @SuppressWarnings("checkstyle:ParameterNumber")
294         private ModelBuilderSessionState(
295                 Session session,
296                 ModelBuilderRequest request,
297                 DefaultModelBuilderResult result,
298                 Graph dag,
299                 Map<GAKey, Set<ModelSource>> mappedSources,
300                 List<RemoteRepository> pomRepositories,
301                 List<RemoteRepository> externalRepositories,
302                 List<RemoteRepository> repositories,
303                 Set<String> parentChain) {
304             this.session = session;
305             this.request = request;
306             this.result = result;
307             this.dag = dag;
308             this.mappedSources = mappedSources;
309             this.pomRepositories = pomRepositories;
310             this.externalRepositories = externalRepositories;
311             this.repositories = repositories;
312             this.parentChain = parentChain;
313             this.result.setSource(this.request.getSource());
314         }
315 
316         ModelBuilderSessionState derive(ModelSource source) {
317             return derive(source, new DefaultModelBuilderResult(request, ProblemCollector.create(session)));
318         }
319 
320         ModelBuilderSessionState derive(ModelSource source, DefaultModelBuilderResult result) {
321             return derive(ModelBuilderRequest.build(request, source), result);
322         }
323 
324         /**
325          * Creates a new session, sharing cached datas and propagating errors.
326          */
327         ModelBuilderSessionState derive(ModelBuilderRequest request) {
328             return derive(request, new DefaultModelBuilderResult(request, ProblemCollector.create(session)));
329         }
330 
331         ModelBuilderSessionState derive(ModelBuilderRequest request, DefaultModelBuilderResult result) {
332             if (session != request.getSession()) {
333                 throw new IllegalArgumentException("Session mismatch");
334             }
335             // Create a new parentChain for each derived session to prevent cycle detection issues
336             // The parentChain now contains both GAV coordinates and file paths
337             return new ModelBuilderSessionState(
338                     session,
339                     request,
340                     result,
341                     dag,
342                     mappedSources,
343                     pomRepositories,
344                     externalRepositories,
345                     repositories,
346                     new LinkedHashSet<>());
347         }
348 
349         @Override
350         public String toString() {
351             return "ModelBuilderSession[" + "session="
352                     + session + ", " + "request="
353                     + request + ", " + "result="
354                     + result + ']';
355         }
356 
357         PhasingExecutor createExecutor() {
358             return new PhasingExecutor(Executors.newFixedThreadPool(getParallelism()));
359         }
360 
361         private int getParallelism() {
362             int parallelism = Runtime.getRuntime().availableProcessors() / 2 + 1;
363             try {
364                 String str = request.getUserProperties().get(Constants.MAVEN_MODEL_BUILDER_PARALLELISM);
365                 if (str != null) {
366                     parallelism = Integer.parseInt(str);
367                 }
368             } catch (Exception e) {
369                 // ignore
370             }
371             return Math.max(1, Math.min(parallelism, Runtime.getRuntime().availableProcessors()));
372         }
373 
374         public Model getRawModel(Path from, String groupId, String artifactId) {
375             ModelSource source = getSource(groupId, artifactId);
376             if (source != null) {
377                 if (addEdge(from, source.getPath())) {
378                     return null;
379                 }
380                 try {
381                     return derive(source).readRawModel();
382                 } catch (ModelBuilderException e) {
383                     // gathered with problem collector
384                 }
385             }
386             return null;
387         }
388 
389         public Model getRawModel(Path from, Path path) {
390             if (!Files.isRegularFile(path)) {
391                 throw new IllegalArgumentException("Not a regular file: " + path);
392             }
393             if (addEdge(from, path)) {
394                 return null;
395             }
396             try {
397                 return derive(Sources.buildSource(path)).readRawModel();
398             } catch (ModelBuilderException e) {
399                 // gathered with problem collector
400             }
401             return null;
402         }
403 
404         /**
405          * Returns false if the edge was added, true if it caused a cycle.
406          */
407         private boolean addEdge(Path from, Path p) {
408             try {
409                 dag.addEdge(from.toString(), p.toString());
410                 return false;
411             } catch (Graph.CycleDetectedException e) {
412                 add(Severity.FATAL, Version.BASE, "Cycle detected between models at " + from + " and " + p, null, e);
413                 return true;
414             }
415         }
416 
417         public ModelSource getSource(String groupId, String artifactId) {
418             Set<ModelSource> sources = mappedSources.get(new GAKey(groupId, artifactId));
419             if (sources != null) {
420                 return sources.stream()
421                         .reduce((a, b) -> {
422                             throw new IllegalStateException(String.format(
423                                     "No unique Source for %s:%s: %s and %s",
424                                     groupId, artifactId, a.getLocation(), b.getLocation()));
425                         })
426                         .orElse(null);
427             }
428             return null;
429         }
430 
431         public void putSource(String groupId, String artifactId, ModelSource source) {
432             mappedSources
433                     .computeIfAbsent(new GAKey(groupId, artifactId), k -> new HashSet<>())
434                     .add(source);
435             // Also  register the source under the null groupId
436             if (groupId != null) {
437                 putSource(null, artifactId, source);
438             }
439         }
440 
441         @Override
442         public ProblemCollector<ModelProblem> getProblemCollector() {
443             return result.getProblemCollector();
444         }
445 
446         @Override
447         public void setSource(String source) {
448             this.source = source;
449             this.sourceModel = null;
450         }
451 
452         @Override
453         public void setSource(Model source) {
454             this.sourceModel = source;
455             this.source = null;
456 
457             if (rootModel == null) {
458                 rootModel = source;
459             }
460         }
461 
462         @Override
463         public String getSource() {
464             if (source == null && sourceModel != null) {
465                 source = ModelProblemUtils.toPath(sourceModel);
466             }
467             return source;
468         }
469 
470         private String getModelId() {
471             return ModelProblemUtils.toId(sourceModel);
472         }
473 
474         @Override
475         public void setRootModel(Model rootModel) {
476             this.rootModel = rootModel;
477         }
478 
479         @Override
480         public Model getRootModel() {
481             return rootModel;
482         }
483 
484         @Override
485         public void add(
486                 BuilderProblem.Severity severity,
487                 ModelProblem.Version version,
488                 String message,
489                 InputLocation location,
490                 Exception exception) {
491             int line = -1;
492             int column = -1;
493             String source = null;
494             String modelId = null;
495 
496             if (location != null) {
497                 line = location.getLineNumber();
498                 column = location.getColumnNumber();
499                 if (location.getSource() != null) {
500                     modelId = location.getSource().getModelId();
501                     source = location.getSource().getLocation();
502                 }
503             }
504 
505             if (modelId == null) {
506                 modelId = getModelId();
507                 source = getSource();
508             }
509 
510             if (line <= 0 && column <= 0 && exception instanceof ModelParserException e) {
511                 line = e.getLineNumber();
512                 column = e.getColumnNumber();
513             }
514 
515             ModelProblem problem =
516                     new DefaultModelProblem(message, severity, version, source, line, column, modelId, exception);
517 
518             add(problem);
519         }
520 
521         @Override
522         public ModelBuilderException newModelBuilderException() {
523             return new ModelBuilderException(result);
524         }
525 
526         public void mergeRepositories(Model model, boolean replace) {
527             if (model.getRepositories().isEmpty()
528                     || InternalSession.from(session).getSession().isIgnoreArtifactDescriptorRepositories()) {
529                 return;
530             }
531             // We need to interpolate the repositories before we can use them
532             Model interpolatedModel = interpolateModel(
533                     Model.newBuilder()
534                             .pomFile(model.getPomFile())
535                             .properties(model.getProperties())
536                             .repositories(model.getRepositories())
537                             .build(),
538                     request,
539                     this);
540             List<RemoteRepository> repos = interpolatedModel.getRepositories().stream()
541                     // filter out transitive invalid repositories
542                     // this should be safe because invalid repo coming from build POMs
543                     // have been rejected earlier during validation
544                     .filter(repo -> repo.getUrl() != null && !repo.getUrl().contains("${"))
545                     .map(session::createRemoteRepository)
546                     .toList();
547             if (replace) {
548                 Set<String> ids = repos.stream().map(RemoteRepository::getId).collect(Collectors.toSet());
549                 repositories = repositories.stream()
550                         .filter(r -> !ids.contains(r.getId()))
551                         .toList();
552                 pomRepositories = pomRepositories.stream()
553                         .filter(r -> !ids.contains(r.getId()))
554                         .toList();
555             } else {
556                 Set<String> ids =
557                         pomRepositories.stream().map(RemoteRepository::getId).collect(Collectors.toSet());
558                 repos = repos.stream().filter(r -> !ids.contains(r.getId())).toList();
559             }
560 
561             RepositoryFactory repositoryFactory = session.getService(RepositoryFactory.class);
562             if (request.getRepositoryMerging() == ModelBuilderRequest.RepositoryMerging.REQUEST_DOMINANT) {
563                 repositories = repositoryFactory.aggregate(session, repositories, repos, true);
564                 pomRepositories = repositories;
565             } else {
566                 pomRepositories = repositoryFactory.aggregate(session, pomRepositories, repos, true);
567                 repositories = repositoryFactory.aggregate(session, pomRepositories, externalRepositories, false);
568             }
569         }
570 
571         //
572         // Transform raw model to build pom.
573         // Infer inner reactor dependencies version
574         //
575         Model transformFileToRaw(Model model) {
576             if (model.getDependencies().isEmpty()) {
577                 return model;
578             }
579             List<Dependency> newDeps = new ArrayList<>(model.getDependencies().size());
580             boolean changed = false;
581             for (Dependency dep : model.getDependencies()) {
582                 Dependency newDep = null;
583                 if (dep.getVersion() == null) {
584                     newDep = inferDependencyVersion(model, dep);
585                     if (newDep != null) {
586                         changed = true;
587                     }
588                 } else if (dep.getGroupId() == null) {
589                     // Handle missing groupId when version is present
590                     newDep = inferDependencyGroupId(model, dep);
591                     if (newDep != null) {
592                         changed = true;
593                     }
594                 }
595                 newDeps.add(newDep == null ? dep : newDep);
596             }
597             return changed ? model.withDependencies(newDeps) : model;
598         }
599 
600         private Dependency inferDependencyVersion(Model model, Dependency dep) {
601             Model depModel = getRawModel(model.getPomFile(), dep.getGroupId(), dep.getArtifactId());
602             if (depModel == null) {
603                 return null;
604             }
605             Dependency.Builder depBuilder = Dependency.newBuilder(dep);
606             String version = depModel.getVersion();
607             InputLocation versionLocation = depModel.getLocation("version");
608             if (version == null && depModel.getParent() != null) {
609                 version = depModel.getParent().getVersion();
610                 versionLocation = depModel.getParent().getLocation("version");
611             }
612             depBuilder.version(version).location("version", versionLocation);
613             if (dep.getGroupId() == null) {
614                 String depGroupId = depModel.getGroupId();
615                 InputLocation groupIdLocation = depModel.getLocation("groupId");
616                 if (depGroupId == null && depModel.getParent() != null) {
617                     depGroupId = depModel.getParent().getGroupId();
618                     groupIdLocation = depModel.getParent().getLocation("groupId");
619                 }
620                 depBuilder.groupId(depGroupId).location("groupId", groupIdLocation);
621             }
622             return depBuilder.build();
623         }
624 
625         private Dependency inferDependencyGroupId(Model model, Dependency dep) {
626             Model depModel = getRawModel(model.getPomFile(), dep.getGroupId(), dep.getArtifactId());
627             if (depModel == null) {
628                 return null;
629             }
630             Dependency.Builder depBuilder = Dependency.newBuilder(dep);
631             String depGroupId = depModel.getGroupId();
632             InputLocation groupIdLocation = depModel.getLocation("groupId");
633             if (depGroupId == null && depModel.getParent() != null) {
634                 depGroupId = depModel.getParent().getGroupId();
635                 groupIdLocation = depModel.getParent().getLocation("groupId");
636             }
637             depBuilder.groupId(depGroupId).location("groupId", groupIdLocation);
638             return depBuilder.build();
639         }
640 
641         String replaceCiFriendlyVersion(Map<String, String> properties, String version) {
642             return version != null ? interpolator.interpolate(version, properties::get) : null;
643         }
644 
645         /**
646          * Get enhanced properties that include profile-aware property resolution.
647          * This method activates profiles to ensure that properties defined in profiles
648          * are available for CI-friendly version processing and repository URL interpolation.
649          * It also includes directory-related properties that may be needed during profile activation.
650          */
651         private Map<String, String> getEnhancedProperties(Model model, Path rootDirectory) {
652             Map<String, String> properties = new HashMap<>();
653 
654             // Add directory-specific properties first, as they may be needed for profile activation
655             if (model.getProjectDirectory() != null) {
656                 String basedir = model.getProjectDirectory().toString();
657                 String basedirUri = model.getProjectDirectory().toUri().toString();
658                 properties.put("basedir", basedir);
659                 properties.put("project.basedir", basedir);
660                 properties.put("project.basedir.uri", basedirUri);
661             }
662             try {
663                 String root = rootDirectory.toString();
664                 String rootUri = rootDirectory.toUri().toString();
665                 properties.put("project.rootDirectory", root);
666                 properties.put("project.rootDirectory.uri", rootUri);
667             } catch (IllegalStateException e) {
668                 // Root directory not available, continue without it
669             }
670 
671             // Handle root vs non-root project properties with profile activation
672             if (!Objects.equals(rootDirectory, model.getProjectDirectory())) {
673                 Path rootModelPath = modelProcessor.locateExistingPom(rootDirectory);
674                 if (rootModelPath != null) {
675                     // Check if the root model path is within the root directory to prevent infinite loops
676                     // This can happen when a .mvn directory exists in a subdirectory and parent inference
677                     // tries to read models above the discovered root directory
678                     if (isParentWithinRootDirectory(rootModelPath, rootDirectory)) {
679                         Model rootModel =
680                                 derive(Sources.buildSource(rootModelPath)).readFileModel();
681                         properties.putAll(getPropertiesWithProfiles(rootModel, properties));
682                     }
683                 }
684             } else {
685                 properties.putAll(getPropertiesWithProfiles(model, properties));
686             }
687 
688             return properties;
689         }
690 
691         /**
692          * Get properties from a model including properties from activated profiles.
693          * This performs lightweight profile activation to merge profile properties.
694          *
695          * @param model the model to get properties from
696          * @param baseProperties base properties (including directory properties) to include in profile activation context
697          */
698         private Map<String, String> getPropertiesWithProfiles(Model model, Map<String, String> baseProperties) {
699             Map<String, String> properties = new HashMap<>();
700 
701             // Start with base properties (including directory properties)
702             properties.putAll(baseProperties);
703 
704             // Add model properties
705             properties.putAll(model.getProperties());
706 
707             try {
708                 // Create a profile activation context for this model with base properties available
709                 DefaultProfileActivationContext profileContext = getProfileActivationContext(request, model);
710 
711                 // Activate profiles and merge their properties
712                 List<Profile> activeProfiles = getActiveProfiles(model.getProfiles(), profileContext);
713 
714                 for (Profile profile : activeProfiles) {
715                     properties.putAll(profile.getProperties());
716                 }
717             } catch (Exception e) {
718                 // If profile activation fails, log a warning but continue with base properties
719                 // This ensures that CI-friendly versions still work even if profile activation has issues
720                 logger.warn("Failed to activate profiles for CI-friendly version processing: {}", e.getMessage());
721                 logger.debug("Profile activation failure details", e);
722             }
723 
724             // User properties override everything
725             properties.putAll(session.getEffectiveProperties());
726 
727             return properties;
728         }
729 
730         /**
731          * Convenience method for getting properties with profiles without additional base properties.
732          * This is a backward compatibility method that provides an empty base properties map.
733          */
734         private Map<String, String> getPropertiesWithProfiles(Model model) {
735             return getPropertiesWithProfiles(model, new HashMap<>());
736         }
737 
738         private void buildBuildPom() throws ModelBuilderException {
739             // Retrieve and normalize the source path, ensuring it's non-null and in absolute form
740             Path top = request.getSource().getPath();
741             if (top == null) {
742                 throw new IllegalStateException("Recursive build requested but source has no path");
743             }
744             top = top.toAbsolutePath().normalize();
745 
746             // Obtain the root directory, resolving it if necessary
747             Path rootDirectory;
748             try {
749                 rootDirectory = session.getRootDirectory();
750             } catch (IllegalStateException e) {
751                 rootDirectory = session.getService(RootLocator.class).findMandatoryRoot(top);
752             }
753 
754             // Locate and normalize the root POM if it exists, fallback to top otherwise
755             Path root = modelProcessor.locateExistingPom(rootDirectory);
756             if (root != null) {
757                 root = root.toAbsolutePath().normalize();
758             } else {
759                 root = top;
760             }
761 
762             // Load all models starting from the root
763             loadFromRoot(root, top);
764 
765             // Check for errors after loading models
766             if (hasErrors()) {
767                 throw newModelBuilderException();
768             }
769 
770             // For the top model and all its children, build the effective model.
771             // This is done through the phased executor
772             var allResults = results(result).toList();
773             List<RuntimeException> exceptions = new CopyOnWriteArrayList<>();
774             InternalSession session = InternalSession.from(this.session);
775             RequestTrace trace = session.getCurrentTrace();
776             try (PhasingExecutor executor = createExecutor()) {
777                 for (DefaultModelBuilderResult r : allResults) {
778                     executor.execute(() -> {
779                         ModelBuilderSessionState mbs = derive(r.getSource(), r);
780                         session.setCurrentTrace(trace);
781                         try {
782                             mbs.buildEffectiveModel(new LinkedHashSet<>());
783                         } catch (ModelBuilderException e) {
784                             // gathered with problem collector
785                             // Propagate problems from child session to parent session
786                             for (var problem : e.getResult()
787                                     .getProblemCollector()
788                                     .problems()
789                                     .toList()) {
790                                 getProblemCollector().reportProblem(problem);
791                             }
792                         } catch (RuntimeException t) {
793                             exceptions.add(t);
794                         } finally {
795                             session.setCurrentTrace(null);
796                         }
797                     });
798                 }
799             }
800 
801             // Check for errors again after execution
802             if (exceptions.size() == 1) {
803                 throw exceptions.get(0);
804             } else if (exceptions.size() > 1) {
805                 MavenException fatalException = new MavenException("Multiple fatal exceptions occurred");
806                 exceptions.forEach(fatalException::addSuppressed);
807                 throw fatalException;
808             } else if (hasErrors()) {
809                 throw newModelBuilderException();
810             }
811         }
812 
813         /**
814          * Generates a stream of DefaultModelBuilderResult objects, starting with the provided
815          * result and recursively including all its child results.
816          *
817          * @param r The initial DefaultModelBuilderResult object from which to generate the stream.
818          * @return A Stream of DefaultModelBuilderResult objects, starting with the provided result
819          *         and including all its child results.
820          */
821         Stream<DefaultModelBuilderResult> results(DefaultModelBuilderResult r) {
822             return Stream.concat(Stream.of(r), r.getChildren().stream().flatMap(this::results));
823         }
824 
825         private void loadFromRoot(Path root, Path top) {
826             try (PhasingExecutor executor = createExecutor()) {
827                 DefaultModelBuilderResult r = Objects.equals(top, root)
828                         ? result
829                         : new DefaultModelBuilderResult(request, ProblemCollector.create(session));
830                 loadFilePom(executor, top, root, Set.of(), r);
831             }
832             if (result.getFileModel() == null && !Objects.equals(top, root)) {
833                 logger.warn(
834                         "The top project ({}) cannot be found in the reactor from root project ({}). "
835                                 + "Make sure the root directory is correct (a missing '.mvn' directory in the root "
836                                 + "project is the most common cause) and the project is correctly included "
837                                 + "in the reactor (missing activated profiles, command line options, etc.). For this "
838                                 + "build, the top project will be used as the root project.",
839                         top,
840                         root);
841                 mappedSources.clear();
842                 loadFromRoot(top, top);
843             }
844         }
845 
846         private void loadFilePom(
847                 Executor executor, Path top, Path pom, Set<Path> parents, DefaultModelBuilderResult r) {
848             try {
849                 Path pomDirectory = Files.isDirectory(pom) ? pom : pom.getParent();
850                 ModelSource src = Sources.buildSource(pom);
851                 Model model = derive(src, r).readFileModel();
852                 // keep all loaded file models in memory, those will be needed
853                 // during the raw to build transformation
854                 putSource(getGroupId(model), model.getArtifactId(), src);
855                 Model activated = activateFileModel(model);
856                 for (String subproject : getSubprojects(activated)) {
857                     if (subproject == null || subproject.isEmpty()) {
858                         continue;
859                     }
860 
861                     subproject = subproject.replace('\\', File.separatorChar).replace('/', File.separatorChar);
862 
863                     Path rawSubprojectFile = modelProcessor.locateExistingPom(pomDirectory.resolve(subproject));
864 
865                     if (rawSubprojectFile == null) {
866                         ModelProblem problem = new DefaultModelProblem(
867                                 "Child subproject " + subproject + " of " + pomDirectory + " does not exist",
868                                 Severity.ERROR,
869                                 Version.BASE,
870                                 model,
871                                 -1,
872                                 -1,
873                                 null);
874                         r.getProblemCollector().reportProblem(problem);
875                         continue;
876                     }
877 
878                     Path subprojectFile = rawSubprojectFile.toAbsolutePath().normalize();
879 
880                     if (parents.contains(subprojectFile)) {
881                         StringBuilder buffer = new StringBuilder(256);
882                         for (Path aggregatorFile : parents) {
883                             buffer.append(aggregatorFile).append(" -> ");
884                         }
885                         buffer.append(subprojectFile);
886 
887                         ModelProblem problem = new DefaultModelProblem(
888                                 "Child subproject " + subprojectFile + " of " + pom + " forms aggregation cycle "
889                                         + buffer,
890                                 Severity.ERROR,
891                                 Version.BASE,
892                                 model,
893                                 -1,
894                                 -1,
895                                 null);
896                         r.getProblemCollector().reportProblem(problem);
897                         continue;
898                     }
899 
900                     DefaultModelBuilderResult cr = Objects.equals(top, subprojectFile)
901                             ? result
902                             : new DefaultModelBuilderResult(request, ProblemCollector.create(session));
903                     if (request.isRecursive()) {
904                         r.getChildren().add(cr);
905                     }
906 
907                     InternalSession session = InternalSession.from(this.session);
908                     RequestTrace trace = session.getCurrentTrace();
909                     executor.execute(() -> {
910                         session.setCurrentTrace(trace);
911                         try {
912                             loadFilePom(executor, top, subprojectFile, concat(parents, pom), cr);
913                         } finally {
914                             session.setCurrentTrace(null);
915                         }
916                     });
917                 }
918             } catch (ModelBuilderException e) {
919                 // gathered with problem collector
920                 add(Severity.ERROR, Version.V40, "Failed to load project " + pom, e);
921             }
922         }
923 
924         static <T> Set<T> concat(Set<T> a, T b) {
925             Set<T> result = new HashSet<>(a);
926             result.add(b);
927             return Set.copyOf(result);
928         }
929 
930         void buildEffectiveModel(Collection<String> importIds) throws ModelBuilderException {
931             Model resultModel = readEffectiveModel();
932             setSource(resultModel);
933             setRootModel(resultModel);
934 
935             // model path translation
936             resultModel =
937                     modelPathTranslator.alignToBaseDirectory(resultModel, resultModel.getProjectDirectory(), request);
938 
939             // plugin management injection
940             resultModel = pluginManagementInjector.injectManagement(resultModel, request, this);
941 
942             // lifecycle bindings injection
943             if (request.getRequestType() != ModelBuilderRequest.RequestType.CONSUMER_DEPENDENCY) {
944                 org.apache.maven.api.services.ModelTransformer lifecycleBindingsInjector =
945                         request.getLifecycleBindingsInjector();
946                 if (lifecycleBindingsInjector != null) {
947                     resultModel = lifecycleBindingsInjector.transform(resultModel, request, this);
948                 }
949             }
950 
951             // dependency management import
952             resultModel = importDependencyManagement(resultModel, importIds);
953 
954             // dependency management injection
955             resultModel = dependencyManagementInjector.injectManagement(resultModel, request, this);
956 
957             resultModel = modelNormalizer.injectDefaultValues(resultModel, request, this);
958 
959             if (request.getRequestType() != ModelBuilderRequest.RequestType.CONSUMER_DEPENDENCY) {
960                 // plugins configuration
961                 resultModel = pluginConfigurationExpander.expandPluginConfiguration(resultModel, request, this);
962             }
963 
964             for (var transformer : transformers) {
965                 resultModel = transformer.transformEffectiveModel(resultModel);
966             }
967 
968             result.setEffectiveModel(resultModel);
969             // Set the default relative path for the parent in the file model
970             if (result.getFileModel().getParent() != null
971                     && result.getFileModel().getParent().getRelativePath() == null) {
972                 result.setFileModel(result.getFileModel()
973                         .withParent(result.getFileModel()
974                                 .getParent()
975                                 .withRelativePath(resultModel.getParent().getRelativePath())));
976             }
977 
978             // effective model validation
979             modelValidator.validateEffectiveModel(
980                     session,
981                     resultModel,
982                     isBuildRequest() ? ModelValidator.VALIDATION_LEVEL_STRICT : ModelValidator.VALIDATION_LEVEL_MINIMAL,
983                     this);
984 
985             if (hasErrors()) {
986                 throw newModelBuilderException();
987             }
988         }
989 
990         Model readParent(
991                 Model childModel,
992                 Parent parent,
993                 DefaultProfileActivationContext profileActivationContext,
994                 Set<String> parentChain) {
995             Model parentModel;
996 
997             if (parent != null) {
998                 // Check for circular parent resolution using model IDs
999                 String parentId = parent.getGroupId() + ":" + parent.getArtifactId() + ":" + parent.getVersion();
1000                 if (!parentChain.add(parentId)) {
1001                     StringBuilder message = new StringBuilder("The parents form a cycle: ");
1002                     for (String id : parentChain) {
1003                         message.append(id).append(" -> ");
1004                     }
1005                     message.append(parentId);
1006 
1007                     add(Severity.FATAL, Version.BASE, message.toString());
1008                     throw newModelBuilderException();
1009                 }
1010 
1011                 try {
1012                     parentModel = resolveParent(childModel, parent, profileActivationContext, parentChain);
1013 
1014                     if (!"pom".equals(parentModel.getPackaging())) {
1015                         add(
1016                                 Severity.ERROR,
1017                                 Version.BASE,
1018                                 "Invalid packaging for parent POM " + ModelProblemUtils.toSourceHint(parentModel)
1019                                         + ", must be \"pom\" but is \"" + parentModel.getPackaging() + "\"",
1020                                 parentModel.getLocation("packaging"));
1021                     }
1022                     result.setParentModel(parentModel);
1023 
1024                     // Recursively read the parent's parent
1025                     if (parentModel.getParent() != null) {
1026                         readParent(parentModel, parentModel.getParent(), profileActivationContext, parentChain);
1027                     }
1028                 } finally {
1029                     // Remove from chain when done processing this parent
1030                     parentChain.remove(parentId);
1031                 }
1032             } else {
1033                 String superModelVersion = childModel.getModelVersion();
1034                 if (superModelVersion == null || !KNOWN_MODEL_VERSIONS.contains(superModelVersion)) {
1035                     // Maven 3.x is always using 4.0.0 version to load the supermodel, so
1036                     // do the same when loading a dependency.  The model validator will also
1037                     // check that field later.
1038                     superModelVersion = MODEL_VERSION_4_0_0;
1039                 }
1040                 parentModel = getSuperModel(superModelVersion);
1041             }
1042 
1043             return parentModel;
1044         }
1045 
1046         private Model resolveParent(
1047                 Model childModel,
1048                 Parent parent,
1049                 DefaultProfileActivationContext profileActivationContext,
1050                 Set<String> parentChain)
1051                 throws ModelBuilderException {
1052             Model parentModel = null;
1053             if (isBuildRequest()) {
1054                 parentModel = readParentLocally(childModel, parent, profileActivationContext, parentChain);
1055             }
1056             if (parentModel == null) {
1057                 parentModel = resolveAndReadParentExternally(childModel, parent, profileActivationContext, parentChain);
1058             }
1059             return parentModel;
1060         }
1061 
1062         private Model readParentLocally(
1063                 Model childModel,
1064                 Parent parent,
1065                 DefaultProfileActivationContext profileActivationContext,
1066                 Set<String> parentChain)
1067                 throws ModelBuilderException {
1068             ModelSource candidateSource;
1069             String parentPath = parent.getRelativePath();
1070             if (request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_PROJECT) {
1071                 if (parentPath != null && !parentPath.isEmpty()) {
1072                     candidateSource = request.getSource().resolve(modelProcessor::locateExistingPom, parentPath);
1073                     if (candidateSource == null) {
1074                         wrongParentRelativePath(childModel);
1075                         return null;
1076                     }
1077                 } else {
1078                     candidateSource =
1079                             resolveReactorModel(parent.getGroupId(), parent.getArtifactId(), parent.getVersion());
1080                     if (candidateSource == null && parentPath == null) {
1081                         candidateSource = request.getSource().resolve(modelProcessor::locateExistingPom, "..");
1082                     }
1083                 }
1084             } else {
1085                 candidateSource = resolveReactorModel(parent.getGroupId(), parent.getArtifactId(), parent.getVersion());
1086                 if (candidateSource == null) {
1087                     if (parentPath == null) {
1088                         parentPath = "..";
1089                     }
1090                     if (!parentPath.isEmpty()) {
1091                         candidateSource = request.getSource().resolve(modelProcessor::locateExistingPom, parentPath);
1092                     }
1093                 }
1094             }
1095 
1096             if (candidateSource == null) {
1097                 return null;
1098             }
1099 
1100             // Check for circular parent resolution using source locations (file paths)
1101             // This must be done BEFORE calling derive() to prevent StackOverflowError
1102             String sourceLocation = candidateSource.getLocation();
1103 
1104             if (!parentChain.add(sourceLocation)) {
1105                 StringBuilder message = new StringBuilder("The parents form a cycle: ");
1106                 for (String location : parentChain) {
1107                     message.append(location).append(" -> ");
1108                 }
1109                 message.append(sourceLocation);
1110 
1111                 add(Severity.FATAL, Version.BASE, message.toString());
1112                 throw newModelBuilderException();
1113             }
1114 
1115             try {
1116                 ModelBuilderSessionState derived = derive(candidateSource);
1117                 Model candidateModel = derived.readAsParentModel(profileActivationContext, parentChain);
1118                 addActivePomProfiles(derived.result.getActivePomProfiles());
1119 
1120                 String groupId = getGroupId(candidateModel);
1121                 String artifactId = candidateModel.getArtifactId();
1122                 String version = getVersion(candidateModel);
1123 
1124                 // Ensure that relative path and GA match, if both are provided
1125                 if (groupId == null
1126                         || !groupId.equals(parent.getGroupId())
1127                         || artifactId == null
1128                         || !artifactId.equals(parent.getArtifactId())) {
1129                     mismatchRelativePathAndGA(childModel, groupId, artifactId);
1130                     return null;
1131                 }
1132 
1133                 if (version != null && parent.getVersion() != null && !version.equals(parent.getVersion())) {
1134                     try {
1135                         VersionRange parentRange = versionParser.parseVersionRange(parent.getVersion());
1136                         if (!parentRange.contains(versionParser.parseVersion(version))) {
1137                             // version skew drop back to resolution from the repository
1138                             return null;
1139                         }
1140 
1141                         // Validate versions aren't inherited when using parent ranges the same way as when read
1142                         // externally.
1143                         String rawChildModelVersion = childModel.getVersion();
1144 
1145                         if (rawChildModelVersion == null) {
1146                             // Message below is checked for in the MNG-2199 core IT.
1147                             add(Severity.FATAL, Version.V31, "Version must be a constant", childModel.getLocation(""));
1148 
1149                         } else {
1150                             if (rawChildVersionReferencesParent(rawChildModelVersion)) {
1151                                 // Message below is checked for in the MNG-2199 core IT.
1152                                 add(
1153                                         Severity.FATAL,
1154                                         Version.V31,
1155                                         "Version must be a constant",
1156                                         childModel.getLocation("version"));
1157                             }
1158                         }
1159 
1160                         // MNG-2199: What else to check here ?
1161                     } catch (VersionParserException e) {
1162                         // invalid version range, so drop back to resolution from the repository
1163                         return null;
1164                     }
1165                 }
1166                 return candidateModel;
1167             } finally {
1168                 // Remove the source location from the chain when we're done processing this parent
1169                 parentChain.remove(sourceLocation);
1170             }
1171         }
1172 
1173         private void mismatchRelativePathAndGA(Model childModel, String groupId, String artifactId) {
1174             Parent parent = childModel.getParent();
1175             StringBuilder buffer = new StringBuilder(256);
1176             buffer.append("'parent.relativePath'");
1177             if (childModel != getRootModel()) {
1178                 buffer.append(" of POM ").append(ModelProblemUtils.toSourceHint(childModel));
1179             }
1180             buffer.append(" points at ").append(groupId).append(':').append(artifactId);
1181             buffer.append(" instead of ").append(parent.getGroupId()).append(':');
1182             buffer.append(parent.getArtifactId()).append(", please verify your project structure");
1183 
1184             setSource(childModel);
1185             boolean warn = MODEL_VERSION_4_0_0.equals(childModel.getModelVersion())
1186                     || childModel.getParent().getRelativePath() == null;
1187             add(warn ? Severity.WARNING : Severity.FATAL, Version.BASE, buffer.toString(), parent.getLocation(""));
1188         }
1189 
1190         private void wrongParentRelativePath(Model childModel) {
1191             Parent parent = childModel.getParent();
1192             String parentPath = parent.getRelativePath();
1193             StringBuilder buffer = new StringBuilder(256);
1194             buffer.append("'parent.relativePath'");
1195             if (childModel != getRootModel()) {
1196                 buffer.append(" of POM ").append(ModelProblemUtils.toSourceHint(childModel));
1197             }
1198             buffer.append(" points at '").append(parentPath);
1199             buffer.append("' but no POM could be found, please verify your project structure");
1200 
1201             setSource(childModel);
1202             add(Severity.FATAL, Version.BASE, buffer.toString(), parent.getLocation(""));
1203         }
1204 
1205         Model resolveAndReadParentExternally(
1206                 Model childModel,
1207                 Parent parent,
1208                 DefaultProfileActivationContext profileActivationContext,
1209                 Set<String> parentChain)
1210                 throws ModelBuilderException {
1211             ModelBuilderRequest request = this.request;
1212             setSource(childModel);
1213 
1214             String groupId = parent.getGroupId();
1215             String artifactId = parent.getArtifactId();
1216             String version = parent.getVersion();
1217 
1218             // add repositories specified by the current model so that we can resolve the parent
1219             if (!childModel.getRepositories().isEmpty()) {
1220                 var previousRepositories = repositories;
1221                 mergeRepositories(childModel, false);
1222                 if (!Objects.equals(previousRepositories, repositories)) {
1223                     if (logger.isDebugEnabled()) {
1224                         logger.debug("Merging repositories from " + childModel.getId() + "\n"
1225                                 + repositories.stream()
1226                                         .map(Object::toString)
1227                                         .collect(Collectors.joining("\n", "    ", "")));
1228                     }
1229                 }
1230             }
1231 
1232             ModelSource modelSource;
1233             try {
1234                 modelSource = resolveReactorModel(parent.getGroupId(), parent.getArtifactId(), parent.getVersion());
1235                 if (modelSource == null) {
1236                     AtomicReference<Parent> modified = new AtomicReference<>();
1237                     modelSource = modelResolver.resolveModel(request.getSession(), repositories, parent, modified);
1238                     if (modified.get() != null) {
1239                         parent = modified.get();
1240                     }
1241                 }
1242             } catch (ModelResolverException e) {
1243                 // Message below is checked for in the MNG-2199 core IT.
1244                 StringBuilder buffer = new StringBuilder(256);
1245                 buffer.append("Non-resolvable parent POM");
1246                 if (!containsCoordinates(e.getMessage(), groupId, artifactId, version)) {
1247                     buffer.append(' ').append(ModelProblemUtils.toId(groupId, artifactId, version));
1248                 }
1249                 if (childModel != getRootModel()) {
1250                     buffer.append(" for ").append(ModelProblemUtils.toId(childModel));
1251                 }
1252                 buffer.append(": ").append(e.getMessage());
1253                 if (request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_PROJECT) {
1254                     buffer.append(" and parent could not be found in reactor");
1255                 }
1256 
1257                 add(Severity.FATAL, Version.BASE, buffer.toString(), parent.getLocation(""), e);
1258                 throw newModelBuilderException();
1259             }
1260 
1261             ModelBuilderRequest lenientRequest = ModelBuilderRequest.builder(request)
1262                     .requestType(ModelBuilderRequest.RequestType.CONSUMER_PARENT)
1263                     .source(modelSource)
1264                     .build();
1265 
1266             Model parentModel = derive(lenientRequest).readAsParentModel(profileActivationContext, parentChain);
1267 
1268             if (!parent.getVersion().equals(version)) {
1269                 String rawChildModelVersion = childModel.getVersion();
1270 
1271                 if (rawChildModelVersion == null) {
1272                     // Message below is checked for in the MNG-2199 core IT.
1273                     add(Severity.FATAL, Version.V31, "Version must be a constant", childModel.getLocation(""));
1274                 } else {
1275                     if (rawChildVersionReferencesParent(rawChildModelVersion)) {
1276                         // Message below is checked for in the MNG-2199 core IT.
1277                         add(
1278                                 Severity.FATAL,
1279                                 Version.V31,
1280                                 "Version must be a constant",
1281                                 childModel.getLocation("version"));
1282                     }
1283                 }
1284 
1285                 // MNG-2199: What else to check here ?
1286             }
1287 
1288             return parentModel;
1289         }
1290 
1291         Model activateFileModel(Model inputModel) throws ModelBuilderException {
1292             setRootModel(inputModel);
1293 
1294             // profile activation
1295             DefaultProfileActivationContext profileActivationContext = getProfileActivationContext(request, inputModel);
1296 
1297             setSource("(external profiles)");
1298             List<Profile> activeExternalProfiles = getActiveProfiles(request.getProfiles(), profileActivationContext);
1299 
1300             result.setActiveExternalProfiles(activeExternalProfiles);
1301 
1302             if (!activeExternalProfiles.isEmpty()) {
1303                 Map<String, String> profileProps = new HashMap<>();
1304                 for (Profile profile : activeExternalProfiles) {
1305                     profileProps.putAll(profile.getProperties());
1306                 }
1307                 profileProps.putAll(request.getUserProperties());
1308                 profileActivationContext.setUserProperties(profileProps);
1309             }
1310 
1311             profileActivationContext.setModel(inputModel);
1312             setSource(inputModel);
1313             List<Profile> activePomProfiles = getActiveProfiles(inputModel.getProfiles(), profileActivationContext);
1314 
1315             // model normalization
1316             setSource(inputModel);
1317             inputModel = modelNormalizer.mergeDuplicates(inputModel, request, this);
1318 
1319             Map<String, Activation> interpolatedActivations = getProfileActivations(inputModel);
1320             inputModel = injectProfileActivations(inputModel, interpolatedActivations);
1321 
1322             // profile injection
1323             inputModel = profileInjector.injectProfiles(inputModel, activePomProfiles, request, this);
1324             inputModel = profileInjector.injectProfiles(inputModel, activeExternalProfiles, request, this);
1325 
1326             return inputModel;
1327         }
1328 
1329         @SuppressWarnings("checkstyle:methodlength")
1330         private Model readEffectiveModel() throws ModelBuilderException {
1331             Model inputModel = readRawModel();
1332             if (hasFatalErrors()) {
1333                 throw newModelBuilderException();
1334             }
1335 
1336             setRootModel(inputModel);
1337 
1338             Model activatedFileModel = activateFileModel(inputModel);
1339 
1340             // profile activation
1341             DefaultProfileActivationContext profileActivationContext =
1342                     getProfileActivationContext(request, activatedFileModel);
1343 
1344             List<Profile> activeExternalProfiles = result.getActiveExternalProfiles();
1345 
1346             if (!activeExternalProfiles.isEmpty()) {
1347                 Map<String, String> profileProps = new HashMap<>();
1348                 for (Profile profile : activeExternalProfiles) {
1349                     profileProps.putAll(profile.getProperties());
1350                 }
1351                 profileProps.putAll(request.getUserProperties());
1352                 profileActivationContext.setUserProperties(profileProps);
1353             }
1354 
1355             Model parentModel = readParent(
1356                     activatedFileModel, activatedFileModel.getParent(), profileActivationContext, parentChain);
1357 
1358             // Now that we have read the parent, we can set the relative
1359             // path correctly if it was not set in the input model
1360             if (inputModel.getParent() != null && inputModel.getParent().getRelativePath() == null) {
1361                 String relPath;
1362                 if (parentModel.getPomFile() != null && isBuildRequest()) {
1363                     relPath = inputModel
1364                             .getPomFile()
1365                             .getParent()
1366                             .toAbsolutePath()
1367                             .relativize(
1368                                     parentModel.getPomFile().toAbsolutePath().getParent())
1369                             .toString();
1370                 } else {
1371                     relPath = "..";
1372                 }
1373                 inputModel = inputModel.withParent(inputModel.getParent().withRelativePath(relPath));
1374             }
1375 
1376             Model model = inheritanceAssembler.assembleModelInheritance(inputModel, parentModel, request, this);
1377 
1378             // profile activation
1379             profileActivationContext.setModel(model);
1380 
1381             // profile injection
1382             List<Profile> activePomProfiles = getActiveProfiles(model.getProfiles(), profileActivationContext);
1383             model = profileInjector.injectProfiles(model, activePomProfiles, request, this);
1384             model = profileInjector.injectProfiles(model, activeExternalProfiles, request, this);
1385 
1386             addActivePomProfiles(activePomProfiles);
1387 
1388             // model interpolation
1389             Model resultModel = model;
1390             resultModel = interpolateModel(resultModel, request, this);
1391 
1392             // model normalization
1393             resultModel = modelNormalizer.mergeDuplicates(resultModel, request, this);
1394 
1395             // url normalization
1396             resultModel = modelUrlNormalizer.normalize(resultModel, request);
1397 
1398             // Now the fully interpolated model is available: reconfigure the resolver
1399             if (!resultModel.getRepositories().isEmpty()) {
1400                 List<String> oldRepos =
1401                         repositories.stream().map(Object::toString).toList();
1402                 mergeRepositories(resultModel, true);
1403                 List<String> newRepos =
1404                         repositories.stream().map(Object::toString).toList();
1405                 if (!Objects.equals(oldRepos, newRepos)) {
1406                     logger.debug("Replacing repositories from " + resultModel.getId() + "\n"
1407                             + newRepos.stream().map(s -> "    " + s).collect(Collectors.joining("\n")));
1408                 }
1409             }
1410 
1411             return resultModel;
1412         }
1413 
1414         private void addActivePomProfiles(List<Profile> activePomProfiles) {
1415             if (activePomProfiles != null) {
1416                 if (result.getActivePomProfiles() == null) {
1417                     result.setActivePomProfiles(new ArrayList<>());
1418                 }
1419                 result.getActivePomProfiles().addAll(activePomProfiles);
1420             }
1421         }
1422 
1423         private List<Profile> getActiveProfiles(
1424                 Collection<Profile> interpolatedProfiles, DefaultProfileActivationContext profileActivationContext) {
1425             if (isBuildRequestWithActivation()) {
1426                 return profileSelector.getActiveProfiles(interpolatedProfiles, profileActivationContext, this);
1427             } else {
1428                 return List.of();
1429             }
1430         }
1431 
1432         Model readFileModel() throws ModelBuilderException {
1433             Model model = cache(request.getSource(), FILE, this::doReadFileModel);
1434             // set the file model in the result outside the cache
1435             result.setFileModel(model);
1436             return model;
1437         }
1438 
1439         @SuppressWarnings("checkstyle:methodlength")
1440         Model doReadFileModel() throws ModelBuilderException {
1441             ModelSource modelSource = request.getSource();
1442             Model model;
1443             Path rootDirectory;
1444             setSource(modelSource.getLocation());
1445             logger.debug("Reading file model from " + modelSource.getLocation());
1446             try {
1447                 boolean strict = isBuildRequest();
1448                 try {
1449                     rootDirectory = request.getSession().getRootDirectory();
1450                 } catch (IllegalStateException ignore) {
1451                     rootDirectory = modelSource.getPath();
1452                     while (rootDirectory != null && !Files.isDirectory(rootDirectory)) {
1453                         rootDirectory = rootDirectory.getParent();
1454                     }
1455                 }
1456                 try (InputStream is = modelSource.openStream()) {
1457                     model = modelProcessor.read(XmlReaderRequest.builder()
1458                             .strict(strict)
1459                             .location(modelSource.getLocation())
1460                             .path(modelSource.getPath())
1461                             .rootDirectory(rootDirectory)
1462                             .inputStream(is)
1463                             .transformer(new InterningTransformer(session))
1464                             .build());
1465                 } catch (XmlReaderException e) {
1466                     if (!strict) {
1467                         throw e;
1468                     }
1469                     try (InputStream is = modelSource.openStream()) {
1470                         model = modelProcessor.read(XmlReaderRequest.builder()
1471                                 .strict(false)
1472                                 .location(modelSource.getLocation())
1473                                 .path(modelSource.getPath())
1474                                 .rootDirectory(rootDirectory)
1475                                 .inputStream(is)
1476                                 .transformer(new InterningTransformer(session))
1477                                 .build());
1478                     } catch (XmlReaderException ne) {
1479                         // still unreadable even in non-strict mode, rethrow original error
1480                         throw e;
1481                     }
1482 
1483                     add(
1484                             Severity.ERROR,
1485                             Version.V20,
1486                             "Malformed POM " + modelSource.getLocation() + ": " + e.getMessage(),
1487                             e);
1488                 }
1489 
1490                 InputLocation loc = model.getLocation("");
1491                 InputSource v4src = loc != null ? loc.getSource() : null;
1492                 if (v4src != null) {
1493                     try {
1494                         Field field = InputSource.class.getDeclaredField("modelId");
1495                         field.setAccessible(true);
1496                         field.set(v4src, ModelProblemUtils.toId(model));
1497                     } catch (Throwable t) {
1498                         // TODO: use a lazy source ?
1499                         throw new IllegalStateException("Unable to set modelId on InputSource", t);
1500                     }
1501                 }
1502             } catch (XmlReaderException e) {
1503                 add(
1504                         Severity.FATAL,
1505                         Version.BASE,
1506                         "Non-parseable POM " + modelSource.getLocation() + ": " + e.getMessage(),
1507                         e);
1508                 throw newModelBuilderException();
1509             } catch (IOException e) {
1510                 String msg = e.getMessage();
1511                 if (msg == null || msg.isEmpty()) {
1512                     // NOTE: There's java.nio.charset.MalformedInputException and sun.io.MalformedInputException
1513                     if (e.getClass().getName().endsWith("MalformedInputException")) {
1514                         msg = "Some input bytes do not match the file encoding.";
1515                     } else {
1516                         msg = e.getClass().getSimpleName();
1517                     }
1518                 }
1519                 add(Severity.FATAL, Version.BASE, "Non-readable POM " + modelSource.getLocation() + ": " + msg, e);
1520                 throw newModelBuilderException();
1521             }
1522 
1523             if (model.getModelVersion() == null) {
1524                 String namespace = model.getNamespaceUri();
1525                 if (namespace != null && namespace.startsWith(NAMESPACE_PREFIX)) {
1526                     model = model.withModelVersion(namespace.substring(NAMESPACE_PREFIX.length()));
1527                 }
1528             }
1529 
1530             if (isBuildRequest()) {
1531                 model = model.withPomFile(modelSource.getPath());
1532 
1533                 Parent parent = model.getParent();
1534                 if (parent != null) {
1535                     String groupId = parent.getGroupId();
1536                     String artifactId = parent.getArtifactId();
1537                     String version = parent.getVersion();
1538                     String path = parent.getRelativePath();
1539                     if ((groupId == null || artifactId == null || version == null)
1540                             && (path == null || !path.isEmpty())) {
1541                         Path pomFile = model.getPomFile();
1542                         Path relativePath = Paths.get(path != null ? path : "..");
1543                         Path pomPath = pomFile.resolveSibling(relativePath).normalize();
1544                         if (Files.isDirectory(pomPath)) {
1545                             pomPath = modelProcessor.locateExistingPom(pomPath);
1546                         }
1547                         if (pomPath != null && Files.isRegularFile(pomPath)) {
1548                             // Check if parent POM is above the root directory
1549                             if (!isParentWithinRootDirectory(pomPath, rootDirectory)) {
1550                                 add(
1551                                         Severity.FATAL,
1552                                         Version.BASE,
1553                                         "Parent POM " + pomPath + " is located above the root directory "
1554                                                 + rootDirectory
1555                                                 + ". This setup is invalid when a .mvn directory exists in a subdirectory.",
1556                                         parent.getLocation("relativePath"));
1557                                 throw newModelBuilderException();
1558                             }
1559 
1560                             Model parentModel =
1561                                     derive(Sources.buildSource(pomPath)).readFileModel();
1562                             String parentGroupId = getGroupId(parentModel);
1563                             String parentArtifactId = parentModel.getArtifactId();
1564                             String parentVersion = getVersion(parentModel);
1565                             if ((groupId == null || groupId.equals(parentGroupId))
1566                                     && (artifactId == null || artifactId.equals(parentArtifactId))
1567                                     && (version == null || version.equals(parentVersion))) {
1568                                 model = model.withParent(parent.with()
1569                                         .groupId(parentGroupId)
1570                                         .artifactId(parentArtifactId)
1571                                         .version(parentVersion)
1572                                         .build());
1573                             } else {
1574                                 mismatchRelativePathAndGA(model, parentGroupId, parentArtifactId);
1575                             }
1576                         } else {
1577                             if (!MODEL_VERSION_4_0_0.equals(model.getModelVersion()) && path != null) {
1578                                 wrongParentRelativePath(model);
1579                             }
1580                         }
1581                     }
1582                 }
1583 
1584                 // subprojects discovery
1585                 if (!hasSubprojectsDefined(model)
1586                         // only discover subprojects if POM > 4.0.0
1587                         && !MODEL_VERSION_4_0_0.equals(model.getModelVersion())
1588                         // and if packaging is POM (we check type, but the session is not yet available,
1589                         // we would require the project realm if we want to support extensions
1590                         && Type.POM.equals(model.getPackaging())) {
1591                     List<String> subprojects = new ArrayList<>();
1592                     try (Stream<Path> files = Files.list(model.getProjectDirectory())) {
1593                         for (Path f : files.toList()) {
1594                             if (Files.isDirectory(f)) {
1595                                 Path subproject = modelProcessor.locateExistingPom(f);
1596                                 if (subproject != null) {
1597                                     subprojects.add(f.getFileName().toString());
1598                                 }
1599                             }
1600                         }
1601                         if (!subprojects.isEmpty()) {
1602                             model = model.withSubprojects(subprojects);
1603                         }
1604                     } catch (IOException e) {
1605                         add(Severity.FATAL, Version.V41, "Error discovering subprojects", e);
1606                     }
1607                 }
1608 
1609                 // Enhanced property resolution with profile activation for CI-friendly versions and repository URLs
1610                 // This includes directory properties, profile properties, and user properties
1611                 Map<String, String> properties = getEnhancedProperties(model, rootDirectory);
1612 
1613                 // CI friendly version processing with profile-aware properties
1614                 model = model.with()
1615                         .version(replaceCiFriendlyVersion(properties, model.getVersion()))
1616                         .parent(
1617                                 model.getParent() != null
1618                                         ? model.getParent()
1619                                                 .withVersion(replaceCiFriendlyVersion(
1620                                                         properties,
1621                                                         model.getParent().getVersion()))
1622                                         : null)
1623                         .build();
1624 
1625                 // Repository URL interpolation with the same profile-aware properties
1626                 UnaryOperator<String> callback = properties::get;
1627                 model = model.with()
1628                         .repositories(interpolateRepository(model.getRepositories(), callback))
1629                         .pluginRepositories(interpolateRepository(model.getPluginRepositories(), callback))
1630                         .profiles(map(model.getProfiles(), this::interpolateRepository, callback))
1631                         .distributionManagement(interpolateRepository(model.getDistributionManagement(), callback))
1632                         .build();
1633                 // Override model properties with user properties
1634                 Map<String, String> newProps = merge(model.getProperties(), session.getUserProperties());
1635                 if (newProps != null) {
1636                     model = model.withProperties(newProps);
1637                 }
1638                 model = model.withProfiles(merge(model.getProfiles(), session.getUserProperties()));
1639             }
1640 
1641             for (var transformer : transformers) {
1642                 model = transformer.transformFileModel(model);
1643             }
1644 
1645             setSource(model);
1646             modelValidator.validateFileModel(
1647                     session,
1648                     model,
1649                     isBuildRequest() ? ModelValidator.VALIDATION_LEVEL_STRICT : ModelValidator.VALIDATION_LEVEL_MINIMAL,
1650                     this);
1651             InternalSession internalSession = InternalSession.from(session);
1652             if (Features.mavenMaven3Personality(internalSession.getSession().getConfigProperties())
1653                     && Objects.equals(ModelBuilder.MODEL_VERSION_4_1_0, model.getModelVersion())) {
1654                 add(Severity.FATAL, Version.BASE, "Maven3 mode: no higher model version than 4.0.0 allowed");
1655             }
1656             if (hasFatalErrors()) {
1657                 throw newModelBuilderException();
1658             }
1659 
1660             return model;
1661         }
1662 
1663         private DistributionManagement interpolateRepository(
1664                 DistributionManagement distributionManagement, UnaryOperator<String> callback) {
1665             return distributionManagement == null
1666                     ? null
1667                     : distributionManagement
1668                             .with()
1669                             .repository((DeploymentRepository)
1670                                     interpolateRepository(distributionManagement.getRepository(), callback))
1671                             .snapshotRepository((DeploymentRepository)
1672                                     interpolateRepository(distributionManagement.getSnapshotRepository(), callback))
1673                             .build();
1674         }
1675 
1676         private Profile interpolateRepository(Profile profile, UnaryOperator<String> callback) {
1677             return profile == null
1678                     ? null
1679                     : profile.with()
1680                             .repositories(interpolateRepository(profile.getRepositories(), callback))
1681                             .pluginRepositories(interpolateRepository(profile.getPluginRepositories(), callback))
1682                             .build();
1683         }
1684 
1685         private List<Repository> interpolateRepository(List<Repository> repositories, UnaryOperator<String> callback) {
1686             return map(repositories, this::interpolateRepository, callback);
1687         }
1688 
1689         private Repository interpolateRepository(Repository repository, UnaryOperator<String> callback) {
1690             return repository == null
1691                     ? null
1692                     : repository
1693                             .with()
1694                             .id(interpolator.interpolate(repository.getId(), callback))
1695                             .url(interpolator.interpolate(repository.getUrl(), callback))
1696                             .build();
1697         }
1698 
1699         /**
1700          * Merges a list of model profiles with user-defined properties.
1701          * For each property defined in both the model and user properties, the user property value
1702          * takes precedence and overrides the model value.
1703          *
1704          * @param profiles list of profiles from the model
1705          * @param userProperties map of user-defined properties that override model properties
1706          * @return a new list containing profiles with overridden properties if changes were made,
1707          *         or the original list if no overrides were needed
1708          */
1709         List<Profile> merge(List<Profile> profiles, Map<String, String> userProperties) {
1710             List<Profile> result = null;
1711             for (int i = 0; i < profiles.size(); i++) {
1712                 Profile profile = profiles.get(i);
1713                 Map<String, String> props = merge(profile.getProperties(), userProperties);
1714                 if (props != null) {
1715                     Profile merged = profile.withProperties(props);
1716                     if (result == null) {
1717                         result = new ArrayList<>(profiles);
1718                     }
1719                     result.set(i, merged);
1720                 }
1721             }
1722             return result != null ? result : profiles;
1723         }
1724 
1725         /**
1726          * Merges model properties with user properties, giving precedence to user properties.
1727          * For any property key present in both maps, the user property value will override
1728          * the model property value when they differ.
1729          *
1730          * @param properties properties defined in the model
1731          * @param userProperties user-defined properties that override model properties
1732          * @return a new map with model properties overridden by user properties if changes were needed,
1733          *         or null if no overrides were needed
1734          */
1735         Map<String, String> merge(Map<String, String> properties, Map<String, String> userProperties) {
1736             Map<String, String> result = null;
1737             for (Map.Entry<String, String> entry : properties.entrySet()) {
1738                 String key = entry.getKey();
1739                 String value = userProperties.get(key);
1740                 if (value != null && !Objects.equals(value, entry.getValue())) {
1741                     if (result == null) {
1742                         result = new LinkedHashMap<>(properties);
1743                     }
1744                     result.put(entry.getKey(), value);
1745                 }
1746             }
1747             return result;
1748         }
1749 
1750         Model readRawModel() throws ModelBuilderException {
1751             // ensure file model is available
1752             readFileModel();
1753             Model model = cache(request.getSource(), RAW, this::doReadRawModel);
1754             // set the raw model in the result outside the cache
1755             result.setRawModel(model);
1756             return model;
1757         }
1758 
1759         private Model doReadRawModel() throws ModelBuilderException {
1760             Model rawModel = readFileModel();
1761 
1762             if (!MODEL_VERSION_4_0_0.equals(rawModel.getModelVersion()) && isBuildRequest()) {
1763                 rawModel = transformFileToRaw(rawModel);
1764             }
1765 
1766             for (var transformer : transformers) {
1767                 rawModel = transformer.transformRawModel(rawModel);
1768             }
1769 
1770             modelValidator.validateRawModel(
1771                     session,
1772                     rawModel,
1773                     isBuildRequest() ? ModelValidator.VALIDATION_LEVEL_STRICT : ModelValidator.VALIDATION_LEVEL_MINIMAL,
1774                     this);
1775 
1776             if (hasFatalErrors()) {
1777                 throw newModelBuilderException();
1778             }
1779 
1780             return rawModel;
1781         }
1782 
1783         /**
1784          * Record to store both the parent model and its activated profiles for caching.
1785          */
1786         private record ParentModelWithProfiles(Model model, List<Profile> activatedProfiles) {}
1787 
1788         /**
1789          * Reads the request source's parent with cycle detection.
1790          */
1791         Model readAsParentModel(DefaultProfileActivationContext profileActivationContext, Set<String> parentChain)
1792                 throws ModelBuilderException {
1793             Map<DefaultProfileActivationContext.Record, ParentModelWithProfiles> parentsPerContext =
1794                     cache(request.getSource(), PARENT, ConcurrentHashMap::new);
1795 
1796             for (Map.Entry<DefaultProfileActivationContext.Record, ParentModelWithProfiles> e :
1797                     parentsPerContext.entrySet()) {
1798                 if (e.getKey().matches(profileActivationContext)) {
1799                     ParentModelWithProfiles cached = e.getValue();
1800                     // CRITICAL: On cache hit, we need to replay the cached record's keys into the
1801                     // current recording context. The matches() method already re-evaluated the
1802                     // conditions and recorded some keys in ctx, but we also need to ensure all
1803                     // the keys from the cached record are recorded in the current context.
1804                     if (profileActivationContext.record != null) {
1805                         replayRecordIntoContext(e.getKey(), profileActivationContext);
1806                     }
1807                     // Add the activated profiles from cache to the result
1808                     addActivePomProfiles(cached.activatedProfiles());
1809                     return cached.model();
1810                 }
1811             }
1812 
1813             // Cache miss: process the parent model
1814             // CRITICAL: Use a separate recording context to avoid recording intermediate keys
1815             // that aren't essential to the final result. Only replay the final essential keys
1816             // into the parent recording context to maintain clean cache keys and avoid
1817             // over-recording during parent model processing.
1818             DefaultProfileActivationContext ctx = profileActivationContext.start();
1819             ParentModelWithProfiles modelWithProfiles = doReadAsParentModel(ctx, parentChain);
1820             DefaultProfileActivationContext.Record record = ctx.stop();
1821             replayRecordIntoContext(record, profileActivationContext);
1822 
1823             parentsPerContext.put(record, modelWithProfiles);
1824             addActivePomProfiles(modelWithProfiles.activatedProfiles());
1825             return modelWithProfiles.model();
1826         }
1827 
1828         private ParentModelWithProfiles doReadAsParentModel(
1829                 DefaultProfileActivationContext childProfileActivationContext, Set<String> parentChain)
1830                 throws ModelBuilderException {
1831             Model raw = readRawModel();
1832             Model parentData = readParent(raw, raw.getParent(), childProfileActivationContext, parentChain);
1833             Model parent = new DefaultInheritanceAssembler(new DefaultInheritanceAssembler.InheritanceModelMerger() {
1834                         @Override
1835                         protected void mergeModel_Modules(
1836                                 Model.Builder builder,
1837                                 Model target,
1838                                 Model source,
1839                                 boolean sourceDominant,
1840                                 Map<Object, Object> context) {}
1841 
1842                         @Override
1843                         protected void mergeModel_Subprojects(
1844                                 Model.Builder builder,
1845                                 Model target,
1846                                 Model source,
1847                                 boolean sourceDominant,
1848                                 Map<Object, Object> context) {}
1849                     })
1850                     .assembleModelInheritance(raw, parentData, request, this);
1851 
1852             // Profile injection SHOULD be performed on parent models to ensure
1853             // that profile content becomes part of the parent model before inheritance.
1854             // This ensures proper precedence: child elements override parent elements,
1855             // including elements that came from parent profiles.
1856             //
1857             // Use the child's activation context (passed as parameter) to determine
1858             // which parent profiles should be active, ensuring consistency.
1859             List<Profile> parentActivePomProfiles =
1860                     getActiveProfiles(parent.getProfiles(), childProfileActivationContext);
1861 
1862             // Inject profiles into parent model
1863             Model injectedParentModel = profileInjector
1864                     .injectProfiles(parent, parentActivePomProfiles, request, this)
1865                     .withProfiles(List.of()); // Remove profiles after injection to avoid double-processing
1866 
1867             // Note: addActivePomProfiles() will be called by the caller for cache miss case
1868             return new ParentModelWithProfiles(injectedParentModel.withParent(null), parentActivePomProfiles);
1869         }
1870 
1871         private Model importDependencyManagement(Model model, Collection<String> importIds) {
1872             DependencyManagement depMgmt = model.getDependencyManagement();
1873 
1874             if (depMgmt == null) {
1875                 return model;
1876             }
1877 
1878             String importing = model.getGroupId() + ':' + model.getArtifactId() + ':' + model.getVersion();
1879 
1880             importIds.add(importing);
1881 
1882             List<DependencyManagement> importMgmts = null;
1883 
1884             List<Dependency> deps = new ArrayList<>(depMgmt.getDependencies());
1885             for (Iterator<Dependency> it = deps.iterator(); it.hasNext(); ) {
1886                 Dependency dependency = it.next();
1887 
1888                 if (!("pom".equals(dependency.getType()) && "import".equals(dependency.getScope()))
1889                         || "bom".equals(dependency.getType())) {
1890                     continue;
1891                 }
1892 
1893                 it.remove();
1894 
1895                 DependencyManagement importMgmt = loadDependencyManagement(dependency, importIds);
1896 
1897                 if (importMgmt != null) {
1898                     if (importMgmts == null) {
1899                         importMgmts = new ArrayList<>();
1900                     }
1901 
1902                     importMgmts.add(importMgmt);
1903                 }
1904             }
1905 
1906             importIds.remove(importing);
1907 
1908             model = model.withDependencyManagement(
1909                     model.getDependencyManagement().withDependencies(deps));
1910 
1911             return dependencyManagementImporter.importManagement(model, importMgmts, request, this);
1912         }
1913 
1914         private DependencyManagement loadDependencyManagement(Dependency dependency, Collection<String> importIds) {
1915             String groupId = dependency.getGroupId();
1916             String artifactId = dependency.getArtifactId();
1917             String version = dependency.getVersion();
1918 
1919             if (groupId == null || groupId.isEmpty()) {
1920                 add(
1921                         Severity.ERROR,
1922                         Version.BASE,
1923                         "'dependencyManagement.dependencies.dependency.groupId' for " + dependency.getManagementKey()
1924                                 + " is missing.",
1925                         dependency.getLocation(""));
1926                 return null;
1927             }
1928             if (artifactId == null || artifactId.isEmpty()) {
1929                 add(
1930                         Severity.ERROR,
1931                         Version.BASE,
1932                         "'dependencyManagement.dependencies.dependency.artifactId' for " + dependency.getManagementKey()
1933                                 + " is missing.",
1934                         dependency.getLocation(""));
1935                 return null;
1936             }
1937             if (version == null || version.isEmpty()) {
1938                 add(
1939                         Severity.ERROR,
1940                         Version.BASE,
1941                         "'dependencyManagement.dependencies.dependency.version' for " + dependency.getManagementKey()
1942                                 + " is missing.",
1943                         dependency.getLocation(""));
1944                 return null;
1945             }
1946 
1947             String imported = groupId + ':' + artifactId + ':' + version;
1948 
1949             if (importIds.contains(imported)) {
1950                 StringBuilder message =
1951                         new StringBuilder("The dependencies of type=pom and with scope=import form a cycle: ");
1952                 for (String modelId : importIds) {
1953                     message.append(modelId).append(" -> ");
1954                 }
1955                 message.append(imported);
1956                 add(Severity.ERROR, Version.BASE, message.toString());
1957                 return null;
1958             }
1959 
1960             Model importModel = cache(
1961                     repositories,
1962                     groupId,
1963                     artifactId,
1964                     version,
1965                     null,
1966                     IMPORT,
1967                     () -> doLoadDependencyManagement(dependency, groupId, artifactId, version, importIds));
1968             DependencyManagement importMgmt = importModel != null ? importModel.getDependencyManagement() : null;
1969             if (importMgmt == null) {
1970                 importMgmt = DependencyManagement.newInstance();
1971             }
1972 
1973             // [MNG-5600] Dependency management import should support exclusions.
1974             List<Exclusion> exclusions = dependency.getExclusions();
1975             if (importMgmt != null && !exclusions.isEmpty()) {
1976                 // Dependency excluded from import.
1977                 List<Dependency> dependencies = importMgmt.getDependencies().stream()
1978                         .filter(candidate -> exclusions.stream().noneMatch(exclusion -> match(exclusion, candidate)))
1979                         .map(candidate -> addExclusions(candidate, exclusions))
1980                         .collect(Collectors.toList());
1981                 importMgmt = importMgmt.withDependencies(dependencies);
1982             }
1983 
1984             return importMgmt;
1985         }
1986 
1987         @SuppressWarnings("checkstyle:parameternumber")
1988         private Model doLoadDependencyManagement(
1989                 Dependency dependency,
1990                 String groupId,
1991                 String artifactId,
1992                 String version,
1993                 Collection<String> importIds) {
1994             Model importModel;
1995             ModelSource importSource;
1996             try {
1997                 importSource = resolveReactorModel(groupId, artifactId, version);
1998                 if (importSource == null) {
1999                     importSource = modelResolver.resolveModel(
2000                             request.getSession(), repositories, dependency, new AtomicReference<>());
2001                 }
2002             } catch (ModelBuilderException | ModelResolverException e) {
2003                 StringBuilder buffer = new StringBuilder(256);
2004                 buffer.append("Non-resolvable import POM");
2005                 if (!containsCoordinates(e.getMessage(), groupId, artifactId, version)) {
2006                     buffer.append(' ').append(ModelProblemUtils.toId(groupId, artifactId, version));
2007                 }
2008                 buffer.append(": ").append(e.getMessage());
2009 
2010                 add(Severity.ERROR, Version.BASE, buffer.toString(), dependency.getLocation(""), e);
2011                 return null;
2012             }
2013 
2014             Path rootDirectory;
2015             try {
2016                 rootDirectory = request.getSession().getRootDirectory();
2017             } catch (IllegalStateException e) {
2018                 rootDirectory = null;
2019             }
2020             if (request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_PROJECT && rootDirectory != null) {
2021                 Path sourcePath = importSource.getPath();
2022                 if (sourcePath != null && sourcePath.startsWith(rootDirectory)) {
2023                     add(
2024                             Severity.WARNING,
2025                             Version.BASE,
2026                             "BOM imports from within reactor should be avoided",
2027                             dependency.getLocation(""));
2028                 }
2029             }
2030 
2031             final ModelBuilderResult importResult;
2032             try {
2033                 ModelBuilderRequest importRequest = ModelBuilderRequest.builder()
2034                         .session(request.getSession())
2035                         .requestType(ModelBuilderRequest.RequestType.CONSUMER_DEPENDENCY)
2036                         .systemProperties(request.getSystemProperties())
2037                         .userProperties(request.getUserProperties())
2038                         .source(importSource)
2039                         .repositories(repositories)
2040                         .build();
2041                 ModelBuilderSessionState modelBuilderSession = derive(importRequest);
2042                 // build the effective model
2043                 modelBuilderSession.buildEffectiveModel(importIds);
2044                 importResult = modelBuilderSession.result;
2045             } catch (ModelBuilderException e) {
2046                 return null;
2047             }
2048 
2049             importModel = importResult.getEffectiveModel();
2050 
2051             return importModel;
2052         }
2053 
2054         ModelSource resolveReactorModel(String groupId, String artifactId, String version)
2055                 throws ModelBuilderException {
2056             Set<ModelSource> sources = mappedSources.get(new GAKey(groupId, artifactId));
2057             if (sources != null) {
2058                 for (ModelSource source : sources) {
2059                     Model model = derive(source).readRawModel();
2060                     if (Objects.equals(getVersion(model), version)) {
2061                         return source;
2062                     }
2063                 }
2064                 // TODO: log a warning ?
2065             }
2066             return null;
2067         }
2068 
2069         private <T> T cache(
2070                 List<RemoteRepository> repositories,
2071                 String groupId,
2072                 String artifactId,
2073                 String version,
2074                 String classifier,
2075                 String tag,
2076                 Supplier<T> supplier) {
2077             RequestTraceHelper.ResolverTrace trace = RequestTraceHelper.enter(session, request);
2078             try {
2079                 RgavCacheKey r = new RgavCacheKey(
2080                         session, trace.mvnTrace(), repositories, groupId, artifactId, version, classifier, tag);
2081                 SourceResponse<RgavCacheKey, T> response =
2082                         InternalSession.from(session).request(r, tr -> new SourceResponse<>(tr, supplier.get()));
2083                 return response.response;
2084             } finally {
2085                 RequestTraceHelper.exit(trace);
2086             }
2087         }
2088 
2089         private <T> T cache(Source source, String tag, Supplier<T> supplier) throws ModelBuilderException {
2090             RequestTraceHelper.ResolverTrace trace = RequestTraceHelper.enter(session, request);
2091             try {
2092                 SourceCacheKey r = new SourceCacheKey(session, trace.mvnTrace(), source, tag);
2093                 SourceResponse<SourceCacheKey, T> response =
2094                         InternalSession.from(session).request(r, tr -> new SourceResponse<>(tr, supplier.get()));
2095                 return response.response;
2096             } finally {
2097                 RequestTraceHelper.exit(trace);
2098             }
2099         }
2100 
2101         boolean isBuildRequest() {
2102             return request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_PROJECT
2103                     || request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_EFFECTIVE
2104                     || request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_CONSUMER;
2105         }
2106 
2107         boolean isBuildRequestWithActivation() {
2108             return request.getRequestType() != ModelBuilderRequest.RequestType.BUILD_CONSUMER;
2109         }
2110 
2111         /**
2112          * Replays the keys from a cached record into the current recording context.
2113          * This ensures that when there's a cache hit, all the keys that were originally
2114          * accessed during the cached computation are recorded in the current context.
2115          */
2116         private void replayRecordIntoContext(
2117                 DefaultProfileActivationContext.Record cachedRecord, DefaultProfileActivationContext targetContext) {
2118             if (targetContext.record == null) {
2119                 return; // Target context is not recording
2120             }
2121 
2122             // Replay all the recorded keys from the cached record into the target context's record
2123             // We need to access the mutable maps in the target context's record
2124             DefaultProfileActivationContext.Record targetRecord = targetContext.record;
2125 
2126             // Replay active profiles
2127             cachedRecord.usedActiveProfiles.forEach(targetRecord.usedActiveProfiles::putIfAbsent);
2128 
2129             // Replay inactive profiles
2130             cachedRecord.usedInactiveProfiles.forEach(targetRecord.usedInactiveProfiles::putIfAbsent);
2131 
2132             // Replay system properties
2133             cachedRecord.usedSystemProperties.forEach(targetRecord.usedSystemProperties::putIfAbsent);
2134 
2135             // Replay user properties
2136             cachedRecord.usedUserProperties.forEach(targetRecord.usedUserProperties::putIfAbsent);
2137 
2138             // Replay model properties
2139             cachedRecord.usedModelProperties.forEach(targetRecord.usedModelProperties::putIfAbsent);
2140 
2141             // Replay model infos
2142             cachedRecord.usedModelInfos.forEach(targetRecord.usedModelInfos::putIfAbsent);
2143 
2144             // Replay exists checks
2145             cachedRecord.usedExists.forEach(targetRecord.usedExists::putIfAbsent);
2146         }
2147     }
2148 
2149     @SuppressWarnings("deprecation")
2150     private static List<String> getSubprojects(Model activated) {
2151         List<String> subprojects = activated.getSubprojects();
2152         if (subprojects.isEmpty()) {
2153             subprojects = activated.getModules();
2154         }
2155         return subprojects;
2156     }
2157 
2158     /**
2159      * Checks if subprojects are explicitly defined in the main model.
2160      * This method distinguishes between:
2161      * 1. No subprojects/modules element present - returns false (should auto-discover)
2162      * 2. Empty subprojects/modules element present - returns true (should NOT auto-discover)
2163      * 3. Non-empty subprojects/modules - returns true (should NOT auto-discover)
2164      */
2165     @SuppressWarnings("deprecation")
2166     private static boolean hasSubprojectsDefined(Model model) {
2167         // Only consider the main model: profiles do not influence auto-discovery
2168         // Inline the check for explicit elements using location tracking
2169         return model.getLocation("subprojects") != null || model.getLocation("modules") != null;
2170     }
2171 
2172     @Override
2173     public Model buildRawModel(ModelBuilderRequest request) throws ModelBuilderException {
2174         RequestTraceHelper.ResolverTrace trace = RequestTraceHelper.enter(request.getSession(), request);
2175         try {
2176             ModelBuilderSessionState build = new ModelBuilderSessionState(request);
2177             Model model = build.readRawModel();
2178             if (build.hasErrors()) {
2179                 throw build.newModelBuilderException();
2180             }
2181             return model;
2182         } finally {
2183             RequestTraceHelper.exit(trace);
2184         }
2185     }
2186 
2187     static String getGroupId(Model model) {
2188         String groupId = model.getGroupId();
2189         if (groupId == null && model.getParent() != null) {
2190             groupId = model.getParent().getGroupId();
2191         }
2192         return groupId;
2193     }
2194 
2195     static String getVersion(Model model) {
2196         String version = model.getVersion();
2197         if (version == null && model.getParent() != null) {
2198             version = model.getParent().getVersion();
2199         }
2200         return version;
2201     }
2202 
2203     private DefaultProfileActivationContext getProfileActivationContext(ModelBuilderRequest request, Model model) {
2204         return new DefaultProfileActivationContext(
2205                 pathTranslator,
2206                 rootLocator,
2207                 interpolator,
2208                 request.getActiveProfileIds(),
2209                 request.getInactiveProfileIds(),
2210                 request.getSystemProperties(),
2211                 request.getUserProperties(),
2212                 model);
2213     }
2214 
2215     private Map<String, Activation> getProfileActivations(Model model) {
2216         return model.getProfiles().stream()
2217                 .filter(p -> p.getActivation() != null)
2218                 .collect(Collectors.toMap(Profile::getId, Profile::getActivation));
2219     }
2220 
2221     private Model injectProfileActivations(Model model, Map<String, Activation> activations) {
2222         List<Profile> profiles = new ArrayList<>();
2223         boolean modified = false;
2224         for (Profile profile : model.getProfiles()) {
2225             Activation activation = profile.getActivation();
2226             if (activation != null) {
2227                 // restore activation
2228                 profile = profile.withActivation(activations.get(profile.getId()));
2229                 modified = true;
2230             }
2231             profiles.add(profile);
2232         }
2233         return modified ? model.withProfiles(profiles) : model;
2234     }
2235 
2236     private Model interpolateModel(Model model, ModelBuilderRequest request, ModelProblemCollector problems) {
2237         Model interpolatedModel =
2238                 modelInterpolator.interpolateModel(model, model.getProjectDirectory(), request, problems);
2239         if (interpolatedModel.getParent() != null) {
2240             Map<String, String> map1 = request.getSession().getUserProperties();
2241             Map<String, String> map2 = model.getProperties();
2242             Map<String, String> map3 = request.getSession().getSystemProperties();
2243             UnaryOperator<String> cb = Interpolator.chain(map1::get, map2::get, map3::get);
2244             try {
2245                 String interpolated =
2246                         interpolator.interpolate(interpolatedModel.getParent().getVersion(), cb);
2247                 interpolatedModel = interpolatedModel.withParent(
2248                         interpolatedModel.getParent().withVersion(interpolated));
2249             } catch (Exception e) {
2250                 problems.add(
2251                         Severity.ERROR,
2252                         Version.BASE,
2253                         "Failed to interpolate field: "
2254                                 + interpolatedModel.getParent().getVersion()
2255                                 + " on class: ",
2256                         e);
2257             }
2258         }
2259         interpolatedModel = interpolatedModel.withPomFile(model.getPomFile());
2260         return interpolatedModel;
2261     }
2262 
2263     private boolean rawChildVersionReferencesParent(String rawChildModelVersion) {
2264         return rawChildModelVersion.equals("${pom.version}")
2265                 || rawChildModelVersion.equals("${project.version}")
2266                 || rawChildModelVersion.equals("${pom.parent.version}")
2267                 || rawChildModelVersion.equals("${project.parent.version}");
2268     }
2269 
2270     private Model getSuperModel(String modelVersion) {
2271         return superPomProvider.getSuperPom(modelVersion);
2272     }
2273 
2274     private static org.apache.maven.api.model.Dependency addExclusions(
2275             org.apache.maven.api.model.Dependency candidate, List<Exclusion> exclusions) {
2276         return candidate.withExclusions(Stream.concat(candidate.getExclusions().stream(), exclusions.stream())
2277                 .toList());
2278     }
2279 
2280     private boolean match(Exclusion exclusion, Dependency candidate) {
2281         return match(exclusion.getGroupId(), candidate.getGroupId())
2282                 && match(exclusion.getArtifactId(), candidate.getArtifactId());
2283     }
2284 
2285     private boolean match(String match, String text) {
2286         return match.equals("*") || match.equals(text);
2287     }
2288 
2289     private boolean containsCoordinates(String message, String groupId, String artifactId, String version) {
2290         return message != null
2291                 && (groupId == null || message.contains(groupId))
2292                 && (artifactId == null || message.contains(artifactId))
2293                 && (version == null || message.contains(version));
2294     }
2295 
2296     record GAKey(String groupId, String artifactId) {}
2297 
2298     public record RgavCacheKey(
2299             Session session,
2300             RequestTrace trace,
2301             List<RemoteRepository> repositories,
2302             String groupId,
2303             String artifactId,
2304             String version,
2305             String classifier,
2306             String tag)
2307             implements Request<Session> {
2308         @Nonnull
2309         @Override
2310         public Session getSession() {
2311             return session;
2312         }
2313 
2314         @Nullable
2315         @Override
2316         public RequestTrace getTrace() {
2317             return trace;
2318         }
2319 
2320         @Override
2321         public boolean equals(Object o) {
2322             return o instanceof RgavCacheKey that
2323                     && Objects.equals(tag, that.tag)
2324                     && Objects.equals(groupId, that.groupId)
2325                     && Objects.equals(version, that.version)
2326                     && Objects.equals(artifactId, that.artifactId)
2327                     && Objects.equals(classifier, that.classifier)
2328                     && Objects.equals(repositories, that.repositories);
2329         }
2330 
2331         @Override
2332         public int hashCode() {
2333             return Objects.hash(repositories, groupId, artifactId, version, classifier, tag);
2334         }
2335 
2336         @Override
2337         public String toString() {
2338             StringBuilder sb = new StringBuilder();
2339             sb.append(getClass().getSimpleName()).append("[").append("gav='");
2340             if (groupId != null) {
2341                 sb.append(groupId);
2342             }
2343             sb.append(":");
2344             if (artifactId != null) {
2345                 sb.append(artifactId);
2346             }
2347             sb.append(":");
2348             if (version != null) {
2349                 sb.append(version);
2350             }
2351             sb.append(":");
2352             if (classifier != null) {
2353                 sb.append(classifier);
2354             }
2355             sb.append("', tag='");
2356             sb.append(tag);
2357             sb.append("']");
2358             return sb.toString();
2359         }
2360     }
2361 
2362     public record SourceCacheKey(Session session, RequestTrace trace, Source source, String tag)
2363             implements Request<Session>, CacheMetadata {
2364         @Nonnull
2365         @Override
2366         public Session getSession() {
2367             return session;
2368         }
2369 
2370         @Nullable
2371         @Override
2372         public RequestTrace getTrace() {
2373             return trace;
2374         }
2375 
2376         @Override
2377         public CacheRetention getCacheRetention() {
2378             return source instanceof CacheMetadata cacheMetadata ? cacheMetadata.getCacheRetention() : null;
2379         }
2380 
2381         @Override
2382         public boolean equals(Object o) {
2383             return o instanceof SourceCacheKey that
2384                     && Objects.equals(tag, that.tag)
2385                     && Objects.equals(source, that.source);
2386         }
2387 
2388         @Override
2389         public int hashCode() {
2390             return Objects.hash(source, tag);
2391         }
2392 
2393         @Override
2394         public String toString() {
2395             return getClass().getSimpleName() + "[" + "location=" + source.getLocation() + ", tag=" + tag + ", path="
2396                     + source.getPath() + ']';
2397         }
2398     }
2399 
2400     public static class SourceResponse<R extends Request<?>, T> implements Result<R> {
2401         private final R request;
2402         private final T response;
2403 
2404         SourceResponse(R request, T response) {
2405             this.request = request;
2406             this.response = response;
2407         }
2408 
2409         @Nonnull
2410         @Override
2411         public R getRequest() {
2412             return request;
2413         }
2414     }
2415 
2416     static class InterningTransformer implements XmlReaderRequest.Transformer {
2417         static final Set<String> DEFAULT_CONTEXTS = Set.of(
2418                 // Core Maven coordinates
2419                 "groupId",
2420                 "artifactId",
2421                 "version",
2422                 "namespaceUri",
2423                 "packaging",
2424 
2425                 // Dependency-related fields
2426                 "scope",
2427                 "type",
2428                 "classifier",
2429 
2430                 // Build and plugin-related fields
2431                 "phase",
2432                 "goal",
2433                 "execution",
2434 
2435                 // Repository-related fields
2436                 "layout",
2437                 "policy",
2438                 "checksumPolicy",
2439                 "updatePolicy",
2440 
2441                 // Common metadata fields
2442                 "modelVersion",
2443                 "name",
2444                 "url",
2445                 "system",
2446                 "distribution",
2447                 "status",
2448 
2449                 // SCM fields
2450                 "connection",
2451                 "developerConnection",
2452                 "tag",
2453 
2454                 // Common enum-like values that appear frequently
2455                 "id",
2456                 "inherited",
2457                 "optional");
2458 
2459         private final Set<String> contexts;
2460 
2461         /**
2462          * Creates an InterningTransformer with default contexts.
2463          */
2464         InterningTransformer() {
2465             this.contexts = DEFAULT_CONTEXTS;
2466         }
2467 
2468         /**
2469          * Creates an InterningTransformer with contexts from session properties.
2470          *
2471          * @param session the Maven session to read properties from
2472          */
2473         InterningTransformer(Session session) {
2474             this.contexts = parseContextsFromSession(session);
2475         }
2476 
2477         private Set<String> parseContextsFromSession(Session session) {
2478             String contextsProperty = session.getUserProperties().get(Constants.MAVEN_MODEL_BUILDER_INTERNS);
2479             if (contextsProperty == null) {
2480                 contextsProperty = session.getSystemProperties().get(Constants.MAVEN_MODEL_BUILDER_INTERNS);
2481             }
2482 
2483             if (contextsProperty == null || contextsProperty.trim().isEmpty()) {
2484                 return DEFAULT_CONTEXTS;
2485             }
2486 
2487             return Arrays.stream(contextsProperty.split(","))
2488                     .map(String::trim)
2489                     .filter(s -> !s.isEmpty())
2490                     .collect(Collectors.toSet());
2491         }
2492 
2493         @Override
2494         public String transform(String input, String context) {
2495             return input != null && contexts.contains(context) ? input.intern() : input;
2496         }
2497 
2498         /**
2499          * Get the contexts that will be interned by this transformer.
2500          * Used for testing purposes.
2501          */
2502         Set<String> getContexts() {
2503             return contexts;
2504         }
2505     }
2506 
2507     private static <T, A> List<T> map(List<T> resources, BiFunction<T, A, T> mapper, A argument) {
2508         List<T> newResources = null;
2509         if (resources != null) {
2510             for (int i = 0; i < resources.size(); i++) {
2511                 T resource = resources.get(i);
2512                 T newResource = mapper.apply(resource, argument);
2513                 if (newResource != resource) {
2514                     if (newResources == null) {
2515                         newResources = new ArrayList<>(resources);
2516                     }
2517                     newResources.set(i, newResource);
2518                 }
2519             }
2520         }
2521         return newResources;
2522     }
2523 
2524     /**
2525      * Checks if the parent POM path is within the root directory.
2526      * This prevents invalid setups where a parent POM is located above the root directory.
2527      *
2528      * @param parentPath the path to the parent POM
2529      * @param rootDirectory the root directory
2530      * @return true if the parent is within the root directory, false otherwise
2531      */
2532     private static boolean isParentWithinRootDirectory(Path parentPath, Path rootDirectory) {
2533         if (parentPath == null || rootDirectory == null) {
2534             return true; // Allow if either is null (fallback behavior)
2535         }
2536 
2537         try {
2538             Path normalizedParent = parentPath.toAbsolutePath().normalize();
2539             Path normalizedRoot = rootDirectory.toAbsolutePath().normalize();
2540 
2541             // Check if the parent path starts with the root directory path
2542             return normalizedParent.startsWith(normalizedRoot);
2543         } catch (Exception e) {
2544             // If there's any issue with path resolution, allow it (fallback behavior)
2545             return true;
2546         }
2547     }
2548 }