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