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                 // Add profiles from parent, preserving model ID tracking
1137                 for (Map.Entry<String, List<Profile>> entry :
1138                         derived.result.getActivePomProfilesByModel().entrySet()) {
1139                     addActivePomProfiles(entry.getKey(), entry.getValue());
1140                 }
1141 
1142                 String groupId = getGroupId(candidateModel);
1143                 String artifactId = candidateModel.getArtifactId();
1144                 String version = getVersion(candidateModel);
1145 
1146                 // Ensure that relative path and GA match, if both are provided
1147                 if (parent.getGroupId() != null && (groupId == null || !groupId.equals(parent.getGroupId()))
1148                         || parent.getArtifactId() != null
1149                                 && (artifactId == null || !artifactId.equals(parent.getArtifactId()))) {
1150                     mismatchRelativePathAndGA(childModel, parent, groupId, artifactId);
1151                     return null;
1152                 }
1153 
1154                 if (version != null && parent.getVersion() != null && !version.equals(parent.getVersion())) {
1155                     try {
1156                         VersionRange parentRange = versionParser.parseVersionRange(parent.getVersion());
1157                         if (!parentRange.contains(versionParser.parseVersion(version))) {
1158                             // version skew drop back to resolution from the repository
1159                             return null;
1160                         }
1161 
1162                         // Validate versions aren't inherited when using parent ranges the same way as when read
1163                         // externally.
1164                         String rawChildModelVersion = childModel.getVersion();
1165 
1166                         if (rawChildModelVersion == null) {
1167                             // Message below is checked for in the MNG-2199 core IT.
1168                             add(Severity.FATAL, Version.V31, "Version must be a constant", childModel.getLocation(""));
1169 
1170                         } else {
1171                             if (rawChildVersionReferencesParent(rawChildModelVersion)) {
1172                                 // Message below is checked for in the MNG-2199 core IT.
1173                                 add(
1174                                         Severity.FATAL,
1175                                         Version.V31,
1176                                         "Version must be a constant",
1177                                         childModel.getLocation("version"));
1178                             }
1179                         }
1180 
1181                         // MNG-2199: What else to check here ?
1182                     } catch (VersionParserException e) {
1183                         // invalid version range, so drop back to resolution from the repository
1184                         return null;
1185                     }
1186                 }
1187                 return candidateModel;
1188             } finally {
1189                 // Remove the source location from the chain when we're done processing this parent
1190                 parentChain.remove(sourceLocation);
1191             }
1192         }
1193 
1194         private void mismatchRelativePathAndGA(Model childModel, Parent parent, String groupId, String artifactId) {
1195             StringBuilder buffer = new StringBuilder(256);
1196             buffer.append("'parent.relativePath'");
1197             if (childModel != getRootModel()) {
1198                 buffer.append(" of POM ").append(ModelProblemUtils.toSourceHint(childModel));
1199             }
1200             buffer.append(" points at ").append(groupId).append(':').append(artifactId);
1201             buffer.append(" instead of ").append(parent.getGroupId()).append(':');
1202             buffer.append(parent.getArtifactId()).append(", please verify your project structure");
1203 
1204             setSource(childModel);
1205             boolean warn = MODEL_VERSION_4_0_0.equals(childModel.getModelVersion())
1206                     || childModel.getParent().getRelativePath() == null;
1207             add(warn ? Severity.WARNING : Severity.FATAL, Version.BASE, buffer.toString(), parent.getLocation(""));
1208         }
1209 
1210         private void wrongParentRelativePath(Model childModel) {
1211             Parent parent = childModel.getParent();
1212             String parentPath = parent.getRelativePath();
1213             StringBuilder buffer = new StringBuilder(256);
1214             buffer.append("'parent.relativePath'");
1215             if (childModel != getRootModel()) {
1216                 buffer.append(" of POM ").append(ModelProblemUtils.toSourceHint(childModel));
1217             }
1218             buffer.append(" points at '").append(parentPath);
1219             buffer.append("' but no POM could be found, please verify your project structure");
1220 
1221             setSource(childModel);
1222             add(Severity.FATAL, Version.BASE, buffer.toString(), parent.getLocation(""));
1223         }
1224 
1225         Model resolveAndReadParentExternally(
1226                 Model childModel,
1227                 Parent parent,
1228                 DefaultProfileActivationContext profileActivationContext,
1229                 Set<String> parentChain)
1230                 throws ModelBuilderException {
1231             ModelBuilderRequest request = this.request;
1232             setSource(childModel);
1233 
1234             String groupId = parent.getGroupId();
1235             String artifactId = parent.getArtifactId();
1236             String version = parent.getVersion();
1237             String classifier = parent instanceof Mixin ? ((Mixin) parent).getClassifier() : null;
1238             String extension = parent instanceof Mixin ? ((Mixin) parent).getExtension() : null;
1239 
1240             // add repositories specified by the current model so that we can resolve the parent
1241             if (!childModel.getRepositories().isEmpty()) {
1242                 var previousRepositories = repositories;
1243                 mergeRepositories(childModel, false);
1244                 if (!Objects.equals(previousRepositories, repositories)) {
1245                     if (logger.isDebugEnabled()) {
1246                         logger.debug("Merging repositories from " + childModel.getId() + "\n"
1247                                 + repositories.stream()
1248                                         .map(Object::toString)
1249                                         .collect(Collectors.joining("\n", "    ", "")));
1250                     }
1251                 }
1252             }
1253 
1254             ModelSource modelSource;
1255             try {
1256                 modelSource = classifier == null && extension == null
1257                         ? resolveReactorModel(groupId, artifactId, version)
1258                         : null;
1259                 if (modelSource == null) {
1260                     ModelResolver.ModelResolverRequest req = new ModelResolver.ModelResolverRequest(
1261                             request.getSession(),
1262                             null,
1263                             repositories,
1264                             groupId,
1265                             artifactId,
1266                             version,
1267                             classifier,
1268                             extension != null ? extension : "pom");
1269                     ModelResolver.ModelResolverResult result = modelResolver.resolveModel(req);
1270                     modelSource = result.source();
1271                     if (result.version() != null) {
1272                         parent = parent.withVersion(result.version());
1273                     }
1274                 }
1275             } catch (ModelResolverException e) {
1276                 // Message below is checked for in the MNG-2199 core IT.
1277                 StringBuilder buffer = new StringBuilder(256);
1278                 buffer.append("Non-resolvable parent POM");
1279                 if (!containsCoordinates(e.getMessage(), groupId, artifactId, version)) {
1280                     buffer.append(' ').append(ModelProblemUtils.toId(groupId, artifactId, version));
1281                 }
1282                 if (childModel != getRootModel()) {
1283                     buffer.append(" for ").append(ModelProblemUtils.toId(childModel));
1284                 }
1285                 buffer.append(": ").append(e.getMessage());
1286                 if (request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_PROJECT) {
1287                     buffer.append(" and parent could not be found in reactor");
1288                 }
1289 
1290                 add(Severity.FATAL, Version.BASE, buffer.toString(), parent.getLocation(""), e);
1291                 throw newModelBuilderException();
1292             }
1293 
1294             ModelBuilderRequest lenientRequest = ModelBuilderRequest.builder(request)
1295                     .requestType(ModelBuilderRequest.RequestType.CONSUMER_PARENT)
1296                     .source(modelSource)
1297                     .build();
1298 
1299             ModelBuilderSessionState derived = derive(lenientRequest);
1300             Model parentModel = derived.readAsParentModel(profileActivationContext, parentChain);
1301             // Add profiles from parent, preserving model ID tracking
1302             for (Map.Entry<String, List<Profile>> entry :
1303                     derived.result.getActivePomProfilesByModel().entrySet()) {
1304                 addActivePomProfiles(entry.getKey(), entry.getValue());
1305             }
1306 
1307             if (!parent.getVersion().equals(version)) {
1308                 String rawChildModelVersion = childModel.getVersion();
1309 
1310                 if (rawChildModelVersion == null) {
1311                     // Message below is checked for in the MNG-2199 core IT.
1312                     add(Severity.FATAL, Version.V31, "Version must be a constant", childModel.getLocation(""));
1313                 } else {
1314                     if (rawChildVersionReferencesParent(rawChildModelVersion)) {
1315                         // Message below is checked for in the MNG-2199 core IT.
1316                         add(
1317                                 Severity.FATAL,
1318                                 Version.V31,
1319                                 "Version must be a constant",
1320                                 childModel.getLocation("version"));
1321                     }
1322                 }
1323 
1324                 // MNG-2199: What else to check here ?
1325             }
1326 
1327             return parentModel;
1328         }
1329 
1330         Model activateFileModel(Model inputModel) throws ModelBuilderException {
1331             setRootModel(inputModel);
1332 
1333             // profile activation
1334             DefaultProfileActivationContext profileActivationContext = getProfileActivationContext(request, inputModel);
1335 
1336             setSource("(external profiles)");
1337             List<Profile> activeExternalProfiles = getActiveProfiles(request.getProfiles(), profileActivationContext);
1338 
1339             result.setActiveExternalProfiles(activeExternalProfiles);
1340 
1341             if (!activeExternalProfiles.isEmpty()) {
1342                 Map<String, String> profileProps = new HashMap<>();
1343                 for (Profile profile : activeExternalProfiles) {
1344                     profileProps.putAll(profile.getProperties());
1345                 }
1346                 profileProps.putAll(request.getUserProperties());
1347                 profileActivationContext.setUserProperties(profileProps);
1348             }
1349 
1350             profileActivationContext.setModel(inputModel);
1351             setSource(inputModel);
1352             List<Profile> activePomProfiles = getActiveProfiles(inputModel.getProfiles(), profileActivationContext);
1353 
1354             // model normalization
1355             setSource(inputModel);
1356             inputModel = modelNormalizer.mergeDuplicates(inputModel, request, this);
1357 
1358             Map<String, Activation> interpolatedActivations = getProfileActivations(inputModel);
1359             inputModel = injectProfileActivations(inputModel, interpolatedActivations);
1360 
1361             // profile injection
1362             inputModel = profileInjector.injectProfiles(inputModel, activePomProfiles, request, this);
1363             inputModel = profileInjector.injectProfiles(inputModel, activeExternalProfiles, request, this);
1364 
1365             return inputModel;
1366         }
1367 
1368         @SuppressWarnings("checkstyle:methodlength")
1369         private Model readEffectiveModel() throws ModelBuilderException {
1370             Model inputModel = readRawModel();
1371             if (hasFatalErrors()) {
1372                 throw newModelBuilderException();
1373             }
1374 
1375             setRootModel(inputModel);
1376 
1377             Model activatedFileModel = activateFileModel(inputModel);
1378 
1379             // profile activation
1380             DefaultProfileActivationContext profileActivationContext =
1381                     getProfileActivationContext(request, activatedFileModel);
1382 
1383             List<Profile> activeExternalProfiles = result.getActiveExternalProfiles();
1384 
1385             if (!activeExternalProfiles.isEmpty()) {
1386                 Map<String, String> profileProps = new HashMap<>();
1387                 for (Profile profile : activeExternalProfiles) {
1388                     profileProps.putAll(profile.getProperties());
1389                 }
1390                 profileProps.putAll(request.getUserProperties());
1391                 profileActivationContext.setUserProperties(profileProps);
1392             }
1393 
1394             Model parentModel = readParent(
1395                     activatedFileModel, activatedFileModel.getParent(), profileActivationContext, parentChain);
1396 
1397             // Now that we have read the parent, we can set the relative
1398             // path correctly if it was not set in the input model
1399             if (inputModel.getParent() != null && inputModel.getParent().getRelativePath() == null) {
1400                 String relPath;
1401                 if (parentModel.getPomFile() != null && isBuildRequest()) {
1402                     relPath = inputModel
1403                             .getPomFile()
1404                             .getParent()
1405                             .toAbsolutePath()
1406                             .relativize(
1407                                     parentModel.getPomFile().toAbsolutePath().getParent())
1408                             .toString();
1409                 } else {
1410                     relPath = "..";
1411                 }
1412                 inputModel = inputModel.withParent(inputModel.getParent().withRelativePath(relPath));
1413             }
1414 
1415             Model model = inheritanceAssembler.assembleModelInheritance(inputModel, parentModel, request, this);
1416 
1417             // Mixins
1418             for (Mixin mixin : model.getMixins()) {
1419                 Model parent = resolveParent(model, mixin, profileActivationContext, parentChain);
1420                 model = inheritanceAssembler.assembleModelInheritance(model, parent, request, this);
1421             }
1422 
1423             // model normalization
1424             model = modelNormalizer.mergeDuplicates(model, request, this);
1425 
1426             // profile activation
1427             profileActivationContext.setModel(model);
1428 
1429             // Activate profiles from the input model (before inheritance) to get only local profiles
1430             // Parent profiles are already added when the parent model is read
1431             List<Profile> localActivePomProfiles =
1432                     getActiveProfiles(inputModel.getProfiles(), profileActivationContext);
1433 
1434             // profile injection - inject all profiles (local + inherited) into the model
1435             List<Profile> activePomProfiles = getActiveProfiles(model.getProfiles(), profileActivationContext);
1436             model = profileInjector.injectProfiles(model, activePomProfiles, request, this);
1437             model = profileInjector.injectProfiles(model, activeExternalProfiles, request, this);
1438 
1439             // Track only the local profiles for this model
1440             // Use ModelProblemUtils.toId() to get groupId:artifactId:version format (without packaging)
1441             addActivePomProfiles(ModelProblemUtils.toId(model), localActivePomProfiles);
1442 
1443             // model interpolation
1444             Model resultModel = model;
1445             resultModel = interpolateModel(resultModel, request, this);
1446 
1447             // model normalization
1448             resultModel = modelNormalizer.mergeDuplicates(resultModel, request, this);
1449 
1450             // url normalization
1451             resultModel = modelUrlNormalizer.normalize(resultModel, request);
1452 
1453             // Now the fully interpolated model is available: reconfigure the resolver
1454             if (!resultModel.getRepositories().isEmpty()) {
1455                 List<String> oldRepos =
1456                         repositories.stream().map(Object::toString).toList();
1457                 mergeRepositories(resultModel, true);
1458                 List<String> newRepos =
1459                         repositories.stream().map(Object::toString).toList();
1460                 if (!Objects.equals(oldRepos, newRepos)) {
1461                     logger.debug("Replacing repositories from " + resultModel.getId() + "\n"
1462                             + newRepos.stream().map(s -> "    " + s).collect(Collectors.joining("\n")));
1463                 }
1464             }
1465 
1466             return resultModel;
1467         }
1468 
1469         private void addActivePomProfiles(String modelId, List<Profile> activePomProfiles) {
1470             if (activePomProfiles != null && !activePomProfiles.isEmpty()) {
1471                 // Track profiles by model ID
1472                 result.setActivePomProfiles(modelId, activePomProfiles);
1473             }
1474         }
1475 
1476         private List<Profile> getActiveProfiles(
1477                 Collection<Profile> interpolatedProfiles, DefaultProfileActivationContext profileActivationContext) {
1478             if (isBuildRequestWithActivation()) {
1479                 return profileSelector.getActiveProfiles(interpolatedProfiles, profileActivationContext, this);
1480             } else {
1481                 return List.of();
1482             }
1483         }
1484 
1485         Model readFileModel() throws ModelBuilderException {
1486             Model model = cache(request.getSource(), FILE, this::doReadFileModel);
1487             // set the file model in the result outside the cache
1488             result.setFileModel(model);
1489             return model;
1490         }
1491 
1492         @SuppressWarnings("checkstyle:methodlength")
1493         Model doReadFileModel() throws ModelBuilderException {
1494             ModelSource modelSource = request.getSource();
1495             Model model;
1496             Path rootDirectory;
1497             setSource(modelSource.getLocation());
1498             logger.debug("Reading file model from " + modelSource.getLocation());
1499             try {
1500                 boolean strict = isBuildRequest();
1501                 try {
1502                     rootDirectory = request.getSession().getRootDirectory();
1503                 } catch (IllegalStateException ignore) {
1504                     rootDirectory = modelSource.getPath();
1505                     while (rootDirectory != null && !Files.isDirectory(rootDirectory)) {
1506                         rootDirectory = rootDirectory.getParent();
1507                     }
1508                 }
1509                 try (InputStream is = modelSource.openStream()) {
1510                     model = modelProcessor.read(XmlReaderRequest.builder()
1511                             .strict(strict)
1512                             .location(modelSource.getLocation())
1513                             .modelId(modelSource.getModelId())
1514                             .path(modelSource.getPath())
1515                             .rootDirectory(rootDirectory)
1516                             .inputStream(is)
1517                             .transformer(new InterningTransformer(session))
1518                             .build());
1519                 } catch (XmlReaderException e) {
1520                     if (!strict) {
1521                         throw e;
1522                     }
1523                     try (InputStream is = modelSource.openStream()) {
1524                         model = modelProcessor.read(XmlReaderRequest.builder()
1525                                 .strict(false)
1526                                 .location(modelSource.getLocation())
1527                                 .modelId(modelSource.getModelId())
1528                                 .path(modelSource.getPath())
1529                                 .rootDirectory(rootDirectory)
1530                                 .inputStream(is)
1531                                 .transformer(new InterningTransformer(session))
1532                                 .build());
1533                     } catch (XmlReaderException ne) {
1534                         // still unreadable even in non-strict mode, rethrow original error
1535                         throw e;
1536                     }
1537 
1538                     add(
1539                             Severity.ERROR,
1540                             Version.V20,
1541                             "Malformed POM " + modelSource.getLocation() + ": " + e.getMessage(),
1542                             e);
1543                 }
1544             } catch (XmlReaderException e) {
1545                 add(
1546                         Severity.FATAL,
1547                         Version.BASE,
1548                         "Non-parseable POM " + modelSource.getLocation() + ": " + e.getMessage(),
1549                         e);
1550                 throw newModelBuilderException();
1551             } catch (IOException e) {
1552                 String msg = e.getMessage();
1553                 if (msg == null || msg.isEmpty()) {
1554                     // NOTE: There's java.nio.charset.MalformedInputException and sun.io.MalformedInputException
1555                     if (e.getClass().getName().endsWith("MalformedInputException")) {
1556                         msg = "Some input bytes do not match the file encoding.";
1557                     } else {
1558                         msg = e.getClass().getSimpleName();
1559                     }
1560                 }
1561                 add(Severity.FATAL, Version.BASE, "Non-readable POM " + modelSource.getLocation() + ": " + msg, e);
1562                 throw newModelBuilderException();
1563             }
1564 
1565             if (model.getModelVersion() == null) {
1566                 String namespace = model.getNamespaceUri();
1567                 if (namespace != null && namespace.startsWith(NAMESPACE_PREFIX)) {
1568                     model = model.withModelVersion(namespace.substring(NAMESPACE_PREFIX.length()));
1569                 }
1570             }
1571 
1572             if (isBuildRequest()) {
1573                 model = model.withPomFile(modelSource.getPath());
1574 
1575                 Parent parent = model.getParent();
1576                 if (parent != null) {
1577                     String groupId = parent.getGroupId();
1578                     String artifactId = parent.getArtifactId();
1579                     String version = parent.getVersion();
1580                     String path = parent.getRelativePath();
1581                     if ((groupId == null || artifactId == null || version == null)
1582                             && (path == null || !path.isEmpty())) {
1583                         Path pomFile = model.getPomFile();
1584                         Path relativePath = Paths.get(path != null ? path : "..");
1585                         Path pomPath = pomFile.resolveSibling(relativePath).normalize();
1586                         if (Files.isDirectory(pomPath)) {
1587                             pomPath = modelProcessor.locateExistingPom(pomPath);
1588                         }
1589                         if (pomPath != null && Files.isRegularFile(pomPath)) {
1590                             // Check if parent POM is above the root directory
1591                             if (!isParentWithinRootDirectory(pomPath, rootDirectory)) {
1592                                 add(
1593                                         Severity.FATAL,
1594                                         Version.BASE,
1595                                         "Parent POM " + pomPath + " is located above the root directory "
1596                                                 + rootDirectory
1597                                                 + ". This setup is invalid when a .mvn directory exists in a subdirectory.",
1598                                         parent.getLocation("relativePath"));
1599                                 throw newModelBuilderException();
1600                             }
1601 
1602                             Model parentModel =
1603                                     derive(Sources.buildSource(pomPath)).readFileModel();
1604                             String parentGroupId = getGroupId(parentModel);
1605                             String parentArtifactId = parentModel.getArtifactId();
1606                             String parentVersion = getVersion(parentModel);
1607                             if ((groupId == null || groupId.equals(parentGroupId))
1608                                     && (artifactId == null || artifactId.equals(parentArtifactId))
1609                                     && (version == null || version.equals(parentVersion))) {
1610                                 model = model.withParent(parent.with()
1611                                         .groupId(parentGroupId)
1612                                         .artifactId(parentArtifactId)
1613                                         .version(parentVersion)
1614                                         .build());
1615                             } else {
1616                                 mismatchRelativePathAndGA(model, parent, parentGroupId, parentArtifactId);
1617                             }
1618                         } else {
1619                             if (!MODEL_VERSION_4_0_0.equals(model.getModelVersion()) && path != null) {
1620                                 wrongParentRelativePath(model);
1621                             }
1622                         }
1623                     }
1624                 }
1625 
1626                 // subprojects discovery
1627                 if (!hasSubprojectsDefined(model)
1628                         // only discover subprojects if POM > 4.0.0
1629                         && !MODEL_VERSION_4_0_0.equals(model.getModelVersion())
1630                         // and if packaging is POM (we check type, but the session is not yet available,
1631                         // we would require the project realm if we want to support extensions
1632                         && Type.POM.equals(model.getPackaging())) {
1633                     List<String> subprojects = new ArrayList<>();
1634                     try (Stream<Path> files = Files.list(model.getProjectDirectory())) {
1635                         for (Path f : files.toList()) {
1636                             if (Files.isDirectory(f)) {
1637                                 Path subproject = modelProcessor.locateExistingPom(f);
1638                                 if (subproject != null) {
1639                                     subprojects.add(f.getFileName().toString());
1640                                 }
1641                             }
1642                         }
1643                         if (!subprojects.isEmpty()) {
1644                             model = model.withSubprojects(subprojects);
1645                         }
1646                     } catch (IOException e) {
1647                         add(Severity.FATAL, Version.V41, "Error discovering subprojects", e);
1648                     }
1649                 }
1650 
1651                 // Enhanced property resolution with profile activation for CI-friendly versions and repository URLs
1652                 // This includes directory properties, profile properties, and user properties
1653                 Map<String, String> properties = getEnhancedProperties(model, rootDirectory);
1654 
1655                 // CI friendly version processing with profile-aware properties
1656                 model = model.with()
1657                         .version(replaceCiFriendlyVersion(properties, model.getVersion()))
1658                         .parent(
1659                                 model.getParent() != null
1660                                         ? model.getParent()
1661                                                 .withVersion(replaceCiFriendlyVersion(
1662                                                         properties,
1663                                                         model.getParent().getVersion()))
1664                                         : null)
1665                         .build();
1666 
1667                 // Repository URL interpolation with the same profile-aware properties
1668                 UnaryOperator<String> callback = properties::get;
1669                 model = model.with()
1670                         .repositories(interpolateRepository(model.getRepositories(), callback))
1671                         .pluginRepositories(interpolateRepository(model.getPluginRepositories(), callback))
1672                         .profiles(map(model.getProfiles(), this::interpolateRepository, callback))
1673                         .distributionManagement(interpolateRepository(model.getDistributionManagement(), callback))
1674                         .build();
1675                 // Override model properties with user properties
1676                 Map<String, String> newProps = merge(model.getProperties(), session.getUserProperties());
1677                 if (newProps != null) {
1678                     model = model.withProperties(newProps);
1679                 }
1680                 model = model.withProfiles(merge(model.getProfiles(), session.getUserProperties()));
1681             }
1682 
1683             for (var transformer : transformers) {
1684                 model = transformer.transformFileModel(model);
1685             }
1686 
1687             setSource(model);
1688             modelValidator.validateFileModel(
1689                     session,
1690                     model,
1691                     isBuildRequest() ? ModelValidator.VALIDATION_LEVEL_STRICT : ModelValidator.VALIDATION_LEVEL_MINIMAL,
1692                     this);
1693             InternalSession internalSession = InternalSession.from(session);
1694             if (Features.mavenMaven3Personality(internalSession.getSession().getConfigProperties())
1695                     && Objects.equals(ModelBuilder.MODEL_VERSION_4_1_0, model.getModelVersion())) {
1696                 add(Severity.FATAL, Version.BASE, "Maven3 mode: no higher model version than 4.0.0 allowed");
1697             }
1698             if (hasFatalErrors()) {
1699                 throw newModelBuilderException();
1700             }
1701 
1702             return model;
1703         }
1704 
1705         private DistributionManagement interpolateRepository(
1706                 DistributionManagement distributionManagement, UnaryOperator<String> callback) {
1707             return distributionManagement == null
1708                     ? null
1709                     : distributionManagement
1710                             .with()
1711                             .repository((DeploymentRepository)
1712                                     interpolateRepository(distributionManagement.getRepository(), callback))
1713                             .snapshotRepository((DeploymentRepository)
1714                                     interpolateRepository(distributionManagement.getSnapshotRepository(), callback))
1715                             .build();
1716         }
1717 
1718         private Profile interpolateRepository(Profile profile, UnaryOperator<String> callback) {
1719             return profile == null
1720                     ? null
1721                     : profile.with()
1722                             .repositories(interpolateRepository(profile.getRepositories(), callback))
1723                             .pluginRepositories(interpolateRepository(profile.getPluginRepositories(), callback))
1724                             .build();
1725         }
1726 
1727         private List<Repository> interpolateRepository(List<Repository> repositories, UnaryOperator<String> callback) {
1728             return map(repositories, this::interpolateRepository, callback);
1729         }
1730 
1731         private Repository interpolateRepository(Repository repository, UnaryOperator<String> callback) {
1732             return repository == null
1733                     ? null
1734                     : repository
1735                             .with()
1736                             .id(interpolator.interpolate(repository.getId(), callback))
1737                             .url(interpolator.interpolate(repository.getUrl(), callback))
1738                             .build();
1739         }
1740 
1741         /**
1742          * Merges a list of model profiles with user-defined properties.
1743          * For each property defined in both the model and user properties, the user property value
1744          * takes precedence and overrides the model value.
1745          *
1746          * @param profiles list of profiles from the model
1747          * @param userProperties map of user-defined properties that override model properties
1748          * @return a new list containing profiles with overridden properties if changes were made,
1749          *         or the original list if no overrides were needed
1750          */
1751         List<Profile> merge(List<Profile> profiles, Map<String, String> userProperties) {
1752             List<Profile> result = null;
1753             for (int i = 0; i < profiles.size(); i++) {
1754                 Profile profile = profiles.get(i);
1755                 Map<String, String> props = merge(profile.getProperties(), userProperties);
1756                 if (props != null) {
1757                     Profile merged = profile.withProperties(props);
1758                     if (result == null) {
1759                         result = new ArrayList<>(profiles);
1760                     }
1761                     result.set(i, merged);
1762                 }
1763             }
1764             return result != null ? result : profiles;
1765         }
1766 
1767         /**
1768          * Merges model properties with user properties, giving precedence to user properties.
1769          * For any property key present in both maps, the user property value will override
1770          * the model property value when they differ.
1771          *
1772          * @param properties properties defined in the model
1773          * @param userProperties user-defined properties that override model properties
1774          * @return a new map with model properties overridden by user properties if changes were needed,
1775          *         or null if no overrides were needed
1776          */
1777         Map<String, String> merge(Map<String, String> properties, Map<String, String> userProperties) {
1778             Map<String, String> result = null;
1779             for (Map.Entry<String, String> entry : properties.entrySet()) {
1780                 String key = entry.getKey();
1781                 String value = userProperties.get(key);
1782                 if (value != null && !Objects.equals(value, entry.getValue())) {
1783                     if (result == null) {
1784                         result = new LinkedHashMap<>(properties);
1785                     }
1786                     result.put(entry.getKey(), value);
1787                 }
1788             }
1789             return result;
1790         }
1791 
1792         Model readRawModel() throws ModelBuilderException {
1793             // ensure file model is available
1794             readFileModel();
1795             Model model = cache(request.getSource(), RAW, this::doReadRawModel);
1796             // set the raw model in the result outside the cache
1797             result.setRawModel(model);
1798             return model;
1799         }
1800 
1801         private Model doReadRawModel() throws ModelBuilderException {
1802             Model rawModel = readFileModel();
1803 
1804             if (!MODEL_VERSION_4_0_0.equals(rawModel.getModelVersion()) && isBuildRequest()) {
1805                 rawModel = transformFileToRaw(rawModel);
1806             }
1807 
1808             for (var transformer : transformers) {
1809                 rawModel = transformer.transformRawModel(rawModel);
1810             }
1811 
1812             modelValidator.validateRawModel(
1813                     session,
1814                     rawModel,
1815                     isBuildRequest() ? ModelValidator.VALIDATION_LEVEL_STRICT : ModelValidator.VALIDATION_LEVEL_MINIMAL,
1816                     this);
1817 
1818             if (hasFatalErrors()) {
1819                 throw newModelBuilderException();
1820             }
1821 
1822             return rawModel;
1823         }
1824 
1825         /**
1826          * Record to store both the parent model and its activated profiles for caching.
1827          */
1828         private record ParentModelWithProfiles(Model model, List<Profile> activatedProfiles) {}
1829 
1830         /**
1831          * Reads the request source's parent with cycle detection.
1832          */
1833         Model readAsParentModel(DefaultProfileActivationContext profileActivationContext, Set<String> parentChain)
1834                 throws ModelBuilderException {
1835             Map<DefaultProfileActivationContext.Record, ParentModelWithProfiles> parentsPerContext =
1836                     cache(request.getSource(), PARENT, ConcurrentHashMap::new);
1837 
1838             for (Map.Entry<DefaultProfileActivationContext.Record, ParentModelWithProfiles> e :
1839                     parentsPerContext.entrySet()) {
1840                 if (e.getKey().matches(profileActivationContext)) {
1841                     ParentModelWithProfiles cached = e.getValue();
1842                     // CRITICAL: On cache hit, we need to replay the cached record's keys into the
1843                     // current recording context. The matches() method already re-evaluated the
1844                     // conditions and recorded some keys in ctx, but we also need to ensure all
1845                     // the keys from the cached record are recorded in the current context.
1846                     if (profileActivationContext.record != null) {
1847                         replayRecordIntoContext(e.getKey(), profileActivationContext);
1848                     }
1849                     // Add the activated profiles from cache to the result
1850                     addActivePomProfiles(cached.model().getId(), cached.activatedProfiles());
1851                     return cached.model();
1852                 }
1853             }
1854 
1855             // Cache miss: process the parent model
1856             // CRITICAL: Use a separate recording context to avoid recording intermediate keys
1857             // that aren't essential to the final result. Only replay the final essential keys
1858             // into the parent recording context to maintain clean cache keys and avoid
1859             // over-recording during parent model processing.
1860             DefaultProfileActivationContext ctx = profileActivationContext.start();
1861             ParentModelWithProfiles modelWithProfiles = doReadAsParentModel(ctx, parentChain);
1862             DefaultProfileActivationContext.Record record = ctx.stop();
1863             replayRecordIntoContext(record, profileActivationContext);
1864 
1865             parentsPerContext.put(record, modelWithProfiles);
1866             // Use ModelProblemUtils.toId() to get groupId:artifactId:version format (without packaging)
1867             addActivePomProfiles(
1868                     ModelProblemUtils.toId(modelWithProfiles.model()), modelWithProfiles.activatedProfiles());
1869             return modelWithProfiles.model();
1870         }
1871 
1872         private ParentModelWithProfiles doReadAsParentModel(
1873                 DefaultProfileActivationContext childProfileActivationContext, Set<String> parentChain)
1874                 throws ModelBuilderException {
1875             Model raw = readRawModel();
1876             Model parentData = readParent(raw, raw.getParent(), childProfileActivationContext, parentChain);
1877             DefaultInheritanceAssembler defaultInheritanceAssembler =
1878                     new DefaultInheritanceAssembler(new DefaultInheritanceAssembler.InheritanceModelMerger() {
1879                         @Override
1880                         protected void mergeModel_Modules(
1881                                 Model.Builder builder,
1882                                 Model target,
1883                                 Model source,
1884                                 boolean sourceDominant,
1885                                 Map<Object, Object> context) {}
1886 
1887                         @Override
1888                         protected void mergeModel_Subprojects(
1889                                 Model.Builder builder,
1890                                 Model target,
1891                                 Model source,
1892                                 boolean sourceDominant,
1893                                 Map<Object, Object> context) {}
1894                     });
1895             Model parent = defaultInheritanceAssembler.assembleModelInheritance(raw, parentData, request, this);
1896             for (Mixin mixin : parent.getMixins()) {
1897                 Model parentModel = resolveParent(parent, mixin, childProfileActivationContext, parentChain);
1898                 parent = defaultInheritanceAssembler.assembleModelInheritance(parent, parentModel, request, this);
1899             }
1900 
1901             // Profile injection SHOULD be performed on parent models to ensure
1902             // that profile content becomes part of the parent model before inheritance.
1903             // This ensures proper precedence: child elements override parent elements,
1904             // including elements that came from parent profiles.
1905             //
1906             // Use the child's activation context (passed as parameter) to determine
1907             // which parent profiles should be active, ensuring consistency.
1908             List<Profile> parentActivePomProfiles =
1909                     getActiveProfiles(parent.getProfiles(), childProfileActivationContext);
1910 
1911             // Inject profiles into parent model
1912             Model injectedParentModel = profileInjector
1913                     .injectProfiles(parent, parentActivePomProfiles, request, this)
1914                     .withProfiles(List.of()); // Remove profiles after injection to avoid double-processing
1915 
1916             // Note: addActivePomProfiles() will be called by the caller for cache miss case
1917             return new ParentModelWithProfiles(injectedParentModel.withParent(null), parentActivePomProfiles);
1918         }
1919 
1920         private Model importDependencyManagement(Model model, Collection<String> importIds) {
1921             DependencyManagement depMgmt = model.getDependencyManagement();
1922 
1923             if (depMgmt == null) {
1924                 return model;
1925             }
1926 
1927             String importing = model.getGroupId() + ':' + model.getArtifactId() + ':' + model.getVersion();
1928 
1929             importIds.add(importing);
1930 
1931             List<DependencyManagement> importMgmts = null;
1932 
1933             List<Dependency> deps = new ArrayList<>(depMgmt.getDependencies());
1934             for (Iterator<Dependency> it = deps.iterator(); it.hasNext(); ) {
1935                 Dependency dependency = it.next();
1936 
1937                 if (!("pom".equals(dependency.getType()) && "import".equals(dependency.getScope()))
1938                         || "bom".equals(dependency.getType())) {
1939                     continue;
1940                 }
1941 
1942                 it.remove();
1943 
1944                 DependencyManagement importMgmt = loadDependencyManagement(dependency, importIds);
1945 
1946                 if (importMgmt != null) {
1947                     if (importMgmts == null) {
1948                         importMgmts = new ArrayList<>();
1949                     }
1950 
1951                     importMgmts.add(importMgmt);
1952                 }
1953             }
1954 
1955             importIds.remove(importing);
1956 
1957             model = model.withDependencyManagement(
1958                     model.getDependencyManagement().withDependencies(deps));
1959 
1960             return dependencyManagementImporter.importManagement(model, importMgmts, request, this);
1961         }
1962 
1963         private DependencyManagement loadDependencyManagement(Dependency dependency, Collection<String> importIds) {
1964             String groupId = dependency.getGroupId();
1965             String artifactId = dependency.getArtifactId();
1966             String version = dependency.getVersion();
1967 
1968             if (groupId == null || groupId.isEmpty()) {
1969                 add(
1970                         Severity.ERROR,
1971                         Version.BASE,
1972                         "'dependencyManagement.dependencies.dependency.groupId' for " + dependency.getManagementKey()
1973                                 + " is missing.",
1974                         dependency.getLocation(""));
1975                 return null;
1976             }
1977             if (artifactId == null || artifactId.isEmpty()) {
1978                 add(
1979                         Severity.ERROR,
1980                         Version.BASE,
1981                         "'dependencyManagement.dependencies.dependency.artifactId' for " + dependency.getManagementKey()
1982                                 + " is missing.",
1983                         dependency.getLocation(""));
1984                 return null;
1985             }
1986             if (version == null || version.isEmpty()) {
1987                 add(
1988                         Severity.ERROR,
1989                         Version.BASE,
1990                         "'dependencyManagement.dependencies.dependency.version' for " + dependency.getManagementKey()
1991                                 + " is missing.",
1992                         dependency.getLocation(""));
1993                 return null;
1994             }
1995 
1996             String imported = groupId + ':' + artifactId + ':' + version;
1997 
1998             if (importIds.contains(imported)) {
1999                 StringBuilder message =
2000                         new StringBuilder("The dependencies of type=pom and with scope=import form a cycle: ");
2001                 for (String modelId : importIds) {
2002                     message.append(modelId).append(" -> ");
2003                 }
2004                 message.append(imported);
2005                 add(Severity.ERROR, Version.BASE, message.toString());
2006                 return null;
2007             }
2008 
2009             Model importModel = cache(
2010                     repositories,
2011                     groupId,
2012                     artifactId,
2013                     version,
2014                     null,
2015                     IMPORT,
2016                     () -> doLoadDependencyManagement(dependency, groupId, artifactId, version, importIds));
2017             DependencyManagement importMgmt = importModel != null ? importModel.getDependencyManagement() : null;
2018             if (importMgmt == null) {
2019                 importMgmt = DependencyManagement.newInstance();
2020             }
2021 
2022             // [MNG-5600] Dependency management import should support exclusions.
2023             List<Exclusion> exclusions = dependency.getExclusions();
2024             if (importMgmt != null && !exclusions.isEmpty()) {
2025                 // Dependency excluded from import.
2026                 List<Dependency> dependencies = importMgmt.getDependencies().stream()
2027                         .filter(candidate -> exclusions.stream().noneMatch(exclusion -> match(exclusion, candidate)))
2028                         .map(candidate -> addExclusions(candidate, exclusions))
2029                         .collect(Collectors.toList());
2030                 importMgmt = importMgmt.withDependencies(dependencies);
2031             }
2032 
2033             return importMgmt;
2034         }
2035 
2036         @SuppressWarnings("checkstyle:parameternumber")
2037         private Model doLoadDependencyManagement(
2038                 Dependency dependency,
2039                 String groupId,
2040                 String artifactId,
2041                 String version,
2042                 Collection<String> importIds) {
2043             Model importModel;
2044             ModelSource importSource;
2045             try {
2046                 importSource = resolveReactorModel(groupId, artifactId, version);
2047                 if (importSource == null) {
2048                     importSource = modelResolver.resolveModel(
2049                             request.getSession(), repositories, dependency, new AtomicReference<>());
2050                 }
2051             } catch (ModelBuilderException | ModelResolverException e) {
2052                 StringBuilder buffer = new StringBuilder(256);
2053                 buffer.append("Non-resolvable import POM");
2054                 if (!containsCoordinates(e.getMessage(), groupId, artifactId, version)) {
2055                     buffer.append(' ').append(ModelProblemUtils.toId(groupId, artifactId, version));
2056                 }
2057                 buffer.append(": ").append(e.getMessage());
2058 
2059                 add(Severity.ERROR, Version.BASE, buffer.toString(), dependency.getLocation(""), e);
2060                 return null;
2061             }
2062 
2063             Path rootDirectory;
2064             try {
2065                 rootDirectory = request.getSession().getRootDirectory();
2066             } catch (IllegalStateException e) {
2067                 rootDirectory = null;
2068             }
2069             if (request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_PROJECT && rootDirectory != null) {
2070                 Path sourcePath = importSource.getPath();
2071                 if (sourcePath != null && sourcePath.startsWith(rootDirectory)) {
2072                     add(
2073                             Severity.WARNING,
2074                             Version.BASE,
2075                             "BOM imports from within reactor should be avoided",
2076                             dependency.getLocation(""));
2077                 }
2078             }
2079 
2080             final ModelBuilderResult importResult;
2081             try {
2082                 ModelBuilderRequest importRequest = ModelBuilderRequest.builder()
2083                         .session(request.getSession())
2084                         .requestType(ModelBuilderRequest.RequestType.CONSUMER_DEPENDENCY)
2085                         .systemProperties(request.getSystemProperties())
2086                         .userProperties(request.getUserProperties())
2087                         .source(importSource)
2088                         .repositories(repositories)
2089                         .build();
2090                 ModelBuilderSessionState modelBuilderSession = derive(importRequest);
2091                 // build the effective model
2092                 modelBuilderSession.buildEffectiveModel(importIds);
2093                 importResult = modelBuilderSession.result;
2094             } catch (ModelBuilderException e) {
2095                 return null;
2096             }
2097 
2098             importModel = importResult.getEffectiveModel();
2099 
2100             return importModel;
2101         }
2102 
2103         ModelSource resolveReactorModel(String groupId, String artifactId, String version)
2104                 throws ModelBuilderException {
2105             Set<ModelSource> sources = mappedSources.get(new GAKey(groupId, artifactId));
2106             if (sources != null) {
2107                 for (ModelSource source : sources) {
2108                     Model model = derive(source).readRawModel();
2109                     if (Objects.equals(getVersion(model), version)) {
2110                         return source;
2111                     }
2112                 }
2113                 // TODO: log a warning ?
2114             }
2115             return null;
2116         }
2117 
2118         private <T> T cache(
2119                 List<RemoteRepository> repositories,
2120                 String groupId,
2121                 String artifactId,
2122                 String version,
2123                 String classifier,
2124                 String tag,
2125                 Supplier<T> supplier) {
2126             RequestTraceHelper.ResolverTrace trace = RequestTraceHelper.enter(session, request);
2127             try {
2128                 RgavCacheKey r = new RgavCacheKey(
2129                         session, trace.mvnTrace(), repositories, groupId, artifactId, version, classifier, tag);
2130                 SourceResponse<RgavCacheKey, T> response =
2131                         InternalSession.from(session).request(r, tr -> new SourceResponse<>(tr, supplier.get()));
2132                 return response.response;
2133             } finally {
2134                 RequestTraceHelper.exit(trace);
2135             }
2136         }
2137 
2138         private <T> T cache(Source source, String tag, Supplier<T> supplier) throws ModelBuilderException {
2139             RequestTraceHelper.ResolverTrace trace = RequestTraceHelper.enter(session, request);
2140             try {
2141                 SourceCacheKey r = new SourceCacheKey(session, trace.mvnTrace(), source, tag);
2142                 SourceResponse<SourceCacheKey, T> response =
2143                         InternalSession.from(session).request(r, tr -> new SourceResponse<>(tr, supplier.get()));
2144                 return response.response;
2145             } finally {
2146                 RequestTraceHelper.exit(trace);
2147             }
2148         }
2149 
2150         boolean isBuildRequest() {
2151             return request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_PROJECT
2152                     || request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_EFFECTIVE
2153                     || request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_CONSUMER;
2154         }
2155 
2156         boolean isBuildRequestWithActivation() {
2157             return request.getRequestType() != ModelBuilderRequest.RequestType.BUILD_CONSUMER;
2158         }
2159 
2160         /**
2161          * Replays the keys from a cached record into the current recording context.
2162          * This ensures that when there's a cache hit, all the keys that were originally
2163          * accessed during the cached computation are recorded in the current context.
2164          */
2165         private void replayRecordIntoContext(
2166                 DefaultProfileActivationContext.Record cachedRecord, DefaultProfileActivationContext targetContext) {
2167             if (targetContext.record == null) {
2168                 return; // Target context is not recording
2169             }
2170 
2171             // Replay all the recorded keys from the cached record into the target context's record
2172             // We need to access the mutable maps in the target context's record
2173             DefaultProfileActivationContext.Record targetRecord = targetContext.record;
2174 
2175             // Replay active profiles
2176             cachedRecord.usedActiveProfiles.forEach(targetRecord.usedActiveProfiles::putIfAbsent);
2177 
2178             // Replay inactive profiles
2179             cachedRecord.usedInactiveProfiles.forEach(targetRecord.usedInactiveProfiles::putIfAbsent);
2180 
2181             // Replay system properties
2182             cachedRecord.usedSystemProperties.forEach(targetRecord.usedSystemProperties::putIfAbsent);
2183 
2184             // Replay user properties
2185             cachedRecord.usedUserProperties.forEach(targetRecord.usedUserProperties::putIfAbsent);
2186 
2187             // Replay model properties
2188             cachedRecord.usedModelProperties.forEach(targetRecord.usedModelProperties::putIfAbsent);
2189 
2190             // Replay model infos
2191             cachedRecord.usedModelInfos.forEach(targetRecord.usedModelInfos::putIfAbsent);
2192 
2193             // Replay exists checks
2194             cachedRecord.usedExists.forEach(targetRecord.usedExists::putIfAbsent);
2195         }
2196     }
2197 
2198     @SuppressWarnings("deprecation")
2199     private static List<String> getSubprojects(Model activated) {
2200         List<String> subprojects = activated.getSubprojects();
2201         if (subprojects.isEmpty()) {
2202             subprojects = activated.getModules();
2203         }
2204         return subprojects;
2205     }
2206 
2207     /**
2208      * Checks if subprojects are explicitly defined in the main model.
2209      * This method distinguishes between:
2210      * 1. No subprojects/modules element present - returns false (should auto-discover)
2211      * 2. Empty subprojects/modules element present - returns true (should NOT auto-discover)
2212      * 3. Non-empty subprojects/modules - returns true (should NOT auto-discover)
2213      */
2214     @SuppressWarnings("deprecation")
2215     private static boolean hasSubprojectsDefined(Model model) {
2216         // Only consider the main model: profiles do not influence auto-discovery
2217         // Inline the check for explicit elements using location tracking
2218         return model.getLocation("subprojects") != null || model.getLocation("modules") != null;
2219     }
2220 
2221     @Override
2222     public Model buildRawModel(ModelBuilderRequest request) throws ModelBuilderException {
2223         RequestTraceHelper.ResolverTrace trace = RequestTraceHelper.enter(request.getSession(), request);
2224         try {
2225             ModelBuilderSessionState build = new ModelBuilderSessionState(request);
2226             Model model = build.readRawModel();
2227             if (build.hasErrors()) {
2228                 throw build.newModelBuilderException();
2229             }
2230             return model;
2231         } finally {
2232             // Clean up REQUEST_SCOPED cache entries for raw model building as well
2233             try {
2234                 clearRequestScopedCache(request);
2235             } catch (Exception e) {
2236                 // Log but don't fail the build due to cache cleanup issues
2237                 logger.debug("Failed to clear REQUEST_SCOPED cache for raw model request: {}", request, e);
2238             }
2239             RequestTraceHelper.exit(trace);
2240         }
2241     }
2242 
2243     static String getGroupId(Model model) {
2244         String groupId = model.getGroupId();
2245         if (groupId == null && model.getParent() != null) {
2246             groupId = model.getParent().getGroupId();
2247         }
2248         return groupId;
2249     }
2250 
2251     static String getVersion(Model model) {
2252         String version = model.getVersion();
2253         if (version == null && model.getParent() != null) {
2254             version = model.getParent().getVersion();
2255         }
2256         return version;
2257     }
2258 
2259     private DefaultProfileActivationContext getProfileActivationContext(ModelBuilderRequest request, Model model) {
2260         return new DefaultProfileActivationContext(
2261                 pathTranslator,
2262                 rootLocator,
2263                 interpolator,
2264                 request.getActiveProfileIds(),
2265                 request.getInactiveProfileIds(),
2266                 request.getSystemProperties(),
2267                 request.getUserProperties(),
2268                 model);
2269     }
2270 
2271     private Map<String, Activation> getProfileActivations(Model model) {
2272         return model.getProfiles().stream()
2273                 .filter(p -> p.getActivation() != null)
2274                 .collect(Collectors.toMap(Profile::getId, Profile::getActivation));
2275     }
2276 
2277     private Model injectProfileActivations(Model model, Map<String, Activation> activations) {
2278         List<Profile> profiles = new ArrayList<>();
2279         boolean modified = false;
2280         for (Profile profile : model.getProfiles()) {
2281             Activation activation = profile.getActivation();
2282             if (activation != null) {
2283                 // restore activation
2284                 profile = profile.withActivation(activations.get(profile.getId()));
2285                 modified = true;
2286             }
2287             profiles.add(profile);
2288         }
2289         return modified ? model.withProfiles(profiles) : model;
2290     }
2291 
2292     private Model interpolateModel(Model model, ModelBuilderRequest request, ModelProblemCollector problems) {
2293         Model interpolatedModel =
2294                 modelInterpolator.interpolateModel(model, model.getProjectDirectory(), request, problems);
2295         if (interpolatedModel.getParent() != null) {
2296             Map<String, String> map1 = request.getSession().getUserProperties();
2297             Map<String, String> map2 = model.getProperties();
2298             Map<String, String> map3 = request.getSession().getSystemProperties();
2299             UnaryOperator<String> cb = Interpolator.chain(map1::get, map2::get, map3::get);
2300             try {
2301                 String interpolated =
2302                         interpolator.interpolate(interpolatedModel.getParent().getVersion(), cb);
2303                 interpolatedModel = interpolatedModel.withParent(
2304                         interpolatedModel.getParent().withVersion(interpolated));
2305             } catch (Exception e) {
2306                 problems.add(
2307                         Severity.ERROR,
2308                         Version.BASE,
2309                         "Failed to interpolate field: "
2310                                 + interpolatedModel.getParent().getVersion()
2311                                 + " on class: ",
2312                         e);
2313             }
2314         }
2315         interpolatedModel = interpolatedModel.withPomFile(model.getPomFile());
2316         return interpolatedModel;
2317     }
2318 
2319     private boolean rawChildVersionReferencesParent(String rawChildModelVersion) {
2320         return rawChildModelVersion.equals("${pom.version}")
2321                 || rawChildModelVersion.equals("${project.version}")
2322                 || rawChildModelVersion.equals("${pom.parent.version}")
2323                 || rawChildModelVersion.equals("${project.parent.version}");
2324     }
2325 
2326     private Model getSuperModel(String modelVersion) {
2327         return superPomProvider.getSuperPom(modelVersion);
2328     }
2329 
2330     private static org.apache.maven.api.model.Dependency addExclusions(
2331             org.apache.maven.api.model.Dependency candidate, List<Exclusion> exclusions) {
2332         return candidate.withExclusions(Stream.concat(candidate.getExclusions().stream(), exclusions.stream())
2333                 .toList());
2334     }
2335 
2336     private boolean match(Exclusion exclusion, Dependency candidate) {
2337         return match(exclusion.getGroupId(), candidate.getGroupId())
2338                 && match(exclusion.getArtifactId(), candidate.getArtifactId());
2339     }
2340 
2341     private boolean match(String match, String text) {
2342         return match.equals("*") || match.equals(text);
2343     }
2344 
2345     private boolean containsCoordinates(String message, String groupId, String artifactId, String version) {
2346         return message != null
2347                 && (groupId == null || message.contains(groupId))
2348                 && (artifactId == null || message.contains(artifactId))
2349                 && (version == null || message.contains(version));
2350     }
2351 
2352     record GAKey(String groupId, String artifactId) {}
2353 
2354     public record RgavCacheKey(
2355             Session session,
2356             RequestTrace trace,
2357             List<RemoteRepository> repositories,
2358             String groupId,
2359             String artifactId,
2360             String version,
2361             String classifier,
2362             String tag)
2363             implements Request<Session> {
2364         @Nonnull
2365         @Override
2366         public Session getSession() {
2367             return session;
2368         }
2369 
2370         @Nullable
2371         @Override
2372         public RequestTrace getTrace() {
2373             return trace;
2374         }
2375 
2376         @Override
2377         public boolean equals(Object o) {
2378             return o instanceof RgavCacheKey that
2379                     && Objects.equals(tag, that.tag)
2380                     && Objects.equals(groupId, that.groupId)
2381                     && Objects.equals(version, that.version)
2382                     && Objects.equals(artifactId, that.artifactId)
2383                     && Objects.equals(classifier, that.classifier)
2384                     && Objects.equals(repositories, that.repositories);
2385         }
2386 
2387         @Override
2388         public int hashCode() {
2389             return Objects.hash(repositories, groupId, artifactId, version, classifier, tag);
2390         }
2391 
2392         @Override
2393         public String toString() {
2394             StringBuilder sb = new StringBuilder();
2395             sb.append(getClass().getSimpleName()).append("[").append("gav='");
2396             if (groupId != null) {
2397                 sb.append(groupId);
2398             }
2399             sb.append(":");
2400             if (artifactId != null) {
2401                 sb.append(artifactId);
2402             }
2403             sb.append(":");
2404             if (version != null) {
2405                 sb.append(version);
2406             }
2407             sb.append(":");
2408             if (classifier != null) {
2409                 sb.append(classifier);
2410             }
2411             sb.append("', tag='");
2412             sb.append(tag);
2413             sb.append("']");
2414             return sb.toString();
2415         }
2416     }
2417 
2418     public record SourceCacheKey(Session session, RequestTrace trace, Source source, String tag)
2419             implements Request<Session>, CacheMetadata {
2420         @Nonnull
2421         @Override
2422         public Session getSession() {
2423             return session;
2424         }
2425 
2426         @Nullable
2427         @Override
2428         public RequestTrace getTrace() {
2429             return trace;
2430         }
2431 
2432         @Override
2433         public CacheRetention getCacheRetention() {
2434             return source instanceof CacheMetadata cacheMetadata
2435                     ? cacheMetadata.getCacheRetention()
2436                     : CacheRetention.REQUEST_SCOPED;
2437         }
2438 
2439         @Override
2440         public boolean equals(Object o) {
2441             return o instanceof SourceCacheKey that
2442                     && Objects.equals(tag, that.tag)
2443                     && Objects.equals(source, that.source);
2444         }
2445 
2446         @Override
2447         public int hashCode() {
2448             return Objects.hash(source, tag);
2449         }
2450 
2451         @Override
2452         public String toString() {
2453             return getClass().getSimpleName() + "[" + "location=" + source.getLocation() + ", tag=" + tag + ", path="
2454                     + source.getPath() + ']';
2455         }
2456     }
2457 
2458     public static class SourceResponse<R extends Request<?>, T> implements Result<R> {
2459         private final R request;
2460         private final T response;
2461 
2462         SourceResponse(R request, T response) {
2463             this.request = request;
2464             this.response = response;
2465         }
2466 
2467         @Nonnull
2468         @Override
2469         public R getRequest() {
2470             return request;
2471         }
2472     }
2473 
2474     static class InterningTransformer implements XmlReaderRequest.Transformer {
2475         static final Set<String> DEFAULT_CONTEXTS = Set.of(
2476                 // Core Maven coordinates
2477                 "groupId",
2478                 "artifactId",
2479                 "version",
2480                 "namespaceUri",
2481                 "packaging",
2482 
2483                 // Dependency-related fields
2484                 "scope",
2485                 "type",
2486                 "classifier",
2487 
2488                 // Build and plugin-related fields
2489                 "phase",
2490                 "goal",
2491                 "execution",
2492 
2493                 // Repository-related fields
2494                 "layout",
2495                 "policy",
2496                 "checksumPolicy",
2497                 "updatePolicy",
2498 
2499                 // Common metadata fields
2500                 "modelVersion",
2501                 "name",
2502                 "url",
2503                 "system",
2504                 "distribution",
2505                 "status",
2506 
2507                 // SCM fields
2508                 "connection",
2509                 "developerConnection",
2510                 "tag",
2511 
2512                 // Common enum-like values that appear frequently
2513                 "id",
2514                 "inherited",
2515                 "optional");
2516 
2517         private final Set<String> contexts;
2518 
2519         /**
2520          * Creates an InterningTransformer with default contexts.
2521          */
2522         InterningTransformer() {
2523             this.contexts = DEFAULT_CONTEXTS;
2524         }
2525 
2526         /**
2527          * Creates an InterningTransformer with contexts from session properties.
2528          *
2529          * @param session the Maven session to read properties from
2530          */
2531         InterningTransformer(Session session) {
2532             this.contexts = parseContextsFromSession(session);
2533         }
2534 
2535         private Set<String> parseContextsFromSession(Session session) {
2536             String contextsProperty = session.getUserProperties().get(Constants.MAVEN_MODEL_BUILDER_INTERNS);
2537             if (contextsProperty == null) {
2538                 contextsProperty = session.getSystemProperties().get(Constants.MAVEN_MODEL_BUILDER_INTERNS);
2539             }
2540 
2541             if (contextsProperty == null || contextsProperty.trim().isEmpty()) {
2542                 return DEFAULT_CONTEXTS;
2543             }
2544 
2545             return Arrays.stream(contextsProperty.split(","))
2546                     .map(String::trim)
2547                     .filter(s -> !s.isEmpty())
2548                     .collect(Collectors.toSet());
2549         }
2550 
2551         @Override
2552         public String transform(String input, String context) {
2553             return input != null && contexts.contains(context) ? input.intern() : input;
2554         }
2555 
2556         /**
2557          * Get the contexts that will be interned by this transformer.
2558          * Used for testing purposes.
2559          */
2560         Set<String> getContexts() {
2561             return contexts;
2562         }
2563     }
2564 
2565     /**
2566      * Clears REQUEST_SCOPED cache entries for a specific request.
2567      * <p>
2568      * The method identifies the outer request and removes the corresponding cache entry from the session data.
2569      *
2570      * @param req the request whose REQUEST_SCOPED cache should be cleared
2571      * @param <REQ> the request type
2572      */
2573     private <REQ extends Request<?>> void clearRequestScopedCache(REQ req) {
2574         if (req.getSession() instanceof Session session) {
2575             // Use the same key as DefaultRequestCache
2576             SessionData.Key<Cache> key = SessionData.key(Cache.class, CacheMetadata.class);
2577 
2578             // Get the outer request key using the same logic as DefaultRequestCache
2579             Object outerRequestKey = getOuterRequest(req);
2580 
2581             Cache<Object, Object> caches = session.getData().get(key);
2582             if (caches != null) {
2583                 Object removedCache = caches.get(outerRequestKey);
2584                 if (removedCache instanceof Cache<?, ?> map) {
2585                     int beforeSize = map.size();
2586                     map.removeIf((k, v) -> !(k instanceof RgavCacheKey) && !(k instanceof SourceCacheKey));
2587                     int afterSize = map.size();
2588                     if (logger.isDebugEnabled()) {
2589                         logger.debug(
2590                                 "Cleared REQUEST_SCOPED cache for request: {}, removed {} entries, remaining entries: {}",
2591                                 outerRequestKey.getClass().getSimpleName(),
2592                                 afterSize - beforeSize,
2593                                 afterSize);
2594                     }
2595                 }
2596             }
2597         }
2598     }
2599 
2600     /**
2601      * Gets the outer request for cache key purposes.
2602      * This replicates the logic from DefaultRequestCache.doGetOuterRequest().
2603      */
2604     private Object getOuterRequest(Request<?> req) {
2605         RequestTrace trace = req.getTrace();
2606         if (trace != null) {
2607             RequestTrace parent = trace.parent();
2608             if (parent != null && parent.data() instanceof Request<?> parentRequest) {
2609                 return getOuterRequest(parentRequest);
2610             }
2611         }
2612         return req;
2613     }
2614 
2615     private static <T, A> List<T> map(List<T> resources, BiFunction<T, A, T> mapper, A argument) {
2616         List<T> newResources = null;
2617         if (resources != null) {
2618             for (int i = 0; i < resources.size(); i++) {
2619                 T resource = resources.get(i);
2620                 T newResource = mapper.apply(resource, argument);
2621                 if (newResource != resource) {
2622                     if (newResources == null) {
2623                         newResources = new ArrayList<>(resources);
2624                     }
2625                     newResources.set(i, newResource);
2626                 }
2627             }
2628         }
2629         return newResources;
2630     }
2631 
2632     /**
2633      * Checks if the parent POM path is within the root directory.
2634      * This prevents invalid setups where a parent POM is located above the root directory.
2635      *
2636      * @param parentPath the path to the parent POM
2637      * @param rootDirectory the root directory
2638      * @return true if the parent is within the root directory, false otherwise
2639      */
2640     private static boolean isParentWithinRootDirectory(Path parentPath, Path rootDirectory) {
2641         if (parentPath == null || rootDirectory == null) {
2642             return true; // Allow if either is null (fallback behavior)
2643         }
2644 
2645         try {
2646             Path normalizedParent = parentPath.toAbsolutePath().normalize();
2647             Path normalizedRoot = rootDirectory.toAbsolutePath().normalize();
2648 
2649             // Check if the parent path starts with the root directory path
2650             return normalizedParent.startsWith(normalizedRoot);
2651         } catch (Exception e) {
2652             // If there's any issue with path resolution, allow it (fallback behavior)
2653             return true;
2654         }
2655     }
2656 }