You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by mi...@apache.org on 2020/12/22 11:19:44 UTC

[maven] 01/01: [MNG-6957] Versionless reactor dependencies/parent should work even if modules are aggregated in reverse order

This is an automated email from the ASF dual-hosted git repository.

michaelo pushed a commit to branch MNG-6957_squashed
in repository https://gitbox.apache.org/repos/asf/maven.git

commit 37afe816a24358f4461e508d2ed22a370476316c
Author: rfscholte <rf...@apache.org>
AuthorDate: Mon Dec 21 22:23:43 2020 +0100

    [MNG-6957] Versionless reactor dependencies/parent should work even if modules are aggregated in reverse order
    
    This closes #391
---
 .../aether/ConsumerModelSourceTransformer.java     |   4 +-
 .../maven/project/DefaultProjectBuilder.java       | 122 ++--
 .../apache/maven/project/ReactorModelCache.java    |   9 +-
 .../DefaultConsumerPomXMLFilterFactory.java        |  29 +-
 .../building/BuildModelSourceTransformer.java      |   2 +-
 .../building/DefaultBuildPomXMLFilterFactory.java  |  45 +-
 .../maven/model/building/DefaultModelBuilder.java  | 640 ++++++++++++++-------
 .../building/DefaultModelBuildingRequest.java      |  21 +-
 .../model/building/DefaultModelBuildingResult.java |  30 +
 .../model/building/DefaultTransformerContext.java  |  95 +++
 .../model/building/FilterModelBuildingRequest.java |  12 +-
 .../apache/maven/model/building/ModelBuilder.java  |   2 +
 .../maven/model/building/ModelBuildingRequest.java |  32 +-
 .../maven/model/building/ModelBuildingResult.java  |   7 +
 .../apache/maven/model/building/ModelCacheTag.java |  10 +-
 .../org/apache/maven/model/building/ModelData.java | 113 +---
 .../model/building/ModelSourceTransformer.java     |  14 +-
 .../maven/model/building/TransformerContext.java   |   6 +-
 ...sformer.java => TransformerContextBuilder.java} |  27 +-
 .../apache/maven/model/io/DefaultModelReader.java  |  14 +-
 .../org/apache/maven/model/io/ModelReader.java     |   6 +
 maven-model-builder/src/site/apt/index.apt         |  27 +-
 .../validation/DefaultModelValidatorTest.java      |   4 -
 .../xml/sax/filter/BuildPomXMLFilterFactory.java   |  45 +-
 .../maven/xml/sax/filter/CiFriendlyXMLFilter.java  |  31 +-
 .../sax/filter/ConsumerPomXMLFilterFactory.java    |  33 +-
 .../maven/xml/sax/filter/ParentXMLFilter.java      |  71 ++-
 .../xml/sax/filter/CiFriendlyXMLFilterTest.java    |   2 +-
 .../xml/sax/filter/ConsumerPomXMLFilterTest.java   |  17 +-
 .../maven/xml/sax/filter/ParentXMLFilterTest.java  |  37 +-
 30 files changed, 907 insertions(+), 600 deletions(-)

diff --git a/maven-core/src/main/java/org/apache/maven/internal/aether/ConsumerModelSourceTransformer.java b/maven-core/src/main/java/org/apache/maven/internal/aether/ConsumerModelSourceTransformer.java
index d81571d..4d5d263 100644
--- a/maven-core/src/main/java/org/apache/maven/internal/aether/ConsumerModelSourceTransformer.java
+++ b/maven-core/src/main/java/org/apache/maven/internal/aether/ConsumerModelSourceTransformer.java
@@ -46,13 +46,13 @@ import org.xml.sax.ext.LexicalHandler;
 class ConsumerModelSourceTransformer extends AbstractModelSourceTransformer
 {
     @Override
-    protected AbstractSAXFilter getSAXFilter( Path pomFile, 
+    protected AbstractSAXFilter getSAXFilter( Path pomFile,
                                               TransformerContext context,
                                               Consumer<LexicalHandler> lexicalHandlerConsumer )
         throws TransformerConfigurationException, SAXException, ParserConfigurationException
     {
         return new DefaultConsumerPomXMLFilterFactory( new DefaultBuildPomXMLFilterFactory( context,
-                                                                        lexicalHandlerConsumer ) ).get( pomFile );
+                                                                        lexicalHandlerConsumer, true ) ).get( pomFile );
     }
     
     /**
diff --git a/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java b/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java
index 624f6ad..308097d 100644
--- a/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java
+++ b/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java
@@ -21,7 +21,6 @@ package org.apache.maven.project;
 
 import java.io.File;
 import java.io.IOException;
-import java.nio.file.Path;
 import java.util.AbstractMap;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -33,6 +32,7 @@ import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 import javax.inject.Inject;
 import javax.inject.Named;
@@ -56,6 +56,7 @@ import org.apache.maven.model.Plugin;
 import org.apache.maven.model.Profile;
 import org.apache.maven.model.ReportPlugin;
 import org.apache.maven.model.building.ArtifactModelSource;
+import org.apache.maven.model.building.TransformerContextBuilder;
 import org.apache.maven.model.building.DefaultModelBuildingRequest;
 import org.apache.maven.model.building.DefaultModelProblem;
 import org.apache.maven.model.building.FileModelSource;
@@ -129,7 +130,7 @@ public class DefaultProjectBuilder
         throws ProjectBuildingException
     {
         return build( pomFile, new FileModelSource( pomFile ),
-                new InternalConfig( request, null, useGlobalModelCache() ? getModelCache() : null ) );
+                new InternalConfig( request, null, useGlobalModelCache() ? getModelCache() : null, null ) );
     }
 
     private boolean useGlobalModelCache()
@@ -142,7 +143,7 @@ public class DefaultProjectBuilder
         throws ProjectBuildingException
     {
         return build( null, modelSource,
-                 new InternalConfig( request, null, useGlobalModelCache() ? getModelCache() : null ) );
+                 new InternalConfig( request, null, useGlobalModelCache() ? getModelCache() : null, null ) );
     }
 
     private ProjectBuildingResult build( File pomFile, ModelSource modelSource, InternalConfig config )
@@ -263,14 +264,7 @@ public class DefaultProjectBuilder
 
     private List<String> getProfileIds( List<Profile> profiles )
     {
-        List<String> ids = new ArrayList<>( profiles.size() );
-
-        for ( Profile profile : profiles )
-        {
-            ids.add( profile.getId() );
-        }
-
-        return ids;
+        return profiles.stream().map( Profile::getId ).collect( Collectors.toList() );
     }
 
     private ModelBuildingRequest getModelBuildingRequest( InternalConfig config )
@@ -295,7 +289,7 @@ public class DefaultProjectBuilder
         request.setBuildStartTime( configuration.getBuildStartTime() );
         request.setModelResolver( resolver );
         request.setModelCache( config.modelCache );
-        request.setTransformerContext( (TransformerContext) config.session.getData().get( TransformerContext.KEY ) );
+        request.setTransformerContextBuilder( config.transformerContextBuilder );
 
         return request;
     }
@@ -314,7 +308,8 @@ public class DefaultProjectBuilder
         org.eclipse.aether.artifact.Artifact pomArtifact = RepositoryUtils.toArtifact( artifact );
         pomArtifact = ArtifactDescriptorUtils.toPomArtifact( pomArtifact );
 
-        InternalConfig config = new InternalConfig( request, null, useGlobalModelCache() ? getModelCache() : null );
+        InternalConfig config =
+            new InternalConfig( request, null, useGlobalModelCache() ? getModelCache() : null, null );
 
         boolean localProject;
 
@@ -385,37 +380,14 @@ public class DefaultProjectBuilder
 
         ReactorModelPool.Builder poolBuilder = new ReactorModelPool.Builder();
         final ReactorModelPool modelPool = poolBuilder.build();
-        
-        if ( Features.buildConsumer().isActive() )
-        {
-            final TransformerContext context = new TransformerContext()
-            {
-                @Override
-                public String getUserProperty( String key )
-                {
-                    return request.getUserProperties().getProperty( key );
-                }
-    
-                @Override
-                public Model getRawModel( Path p )
-                {
-                    return modelPool.get( p );
-                }
-    
-                @Override
-                public Model getRawModel( String groupId, String artifactId )
-                {
-                    return modelPool.get( groupId, artifactId, null );
-                }
-            };
-            request.getRepositorySession().getData().set( TransformerContext.KEY, context );
-        }
 
-        InternalConfig config = new InternalConfig( request, modelPool,
-                useGlobalModelCache() ? getModelCache() : new ReactorModelCache() );
+        InternalConfig config =
+            new InternalConfig( request, modelPool, useGlobalModelCache() ? getModelCache() : new ReactorModelCache(),
+                        modelBuilder.newTransformerContextBuilder() );
 
-        Map<String, MavenProject> projectIndex = new HashMap<>( 256 );
+        Map<File, MavenProject> projectIndex = new HashMap<>( 256 );
 
+        // phase 1: get file Models from the reactor.
         boolean noErrors =
             build( results, interimResults, projectIndex, pomFiles, new LinkedHashSet<>(), true, recursive,
                    config, poolBuilder );
@@ -424,6 +396,7 @@ public class DefaultProjectBuilder
 
         try
         {
+            // Phase 2: get effective models from the reactor
             noErrors =
                 build( results, new ArrayList<>(), projectIndex, interimResults, request,
                         new HashMap<>(), config.session ) && noErrors;
@@ -433,6 +406,12 @@ public class DefaultProjectBuilder
             Thread.currentThread().setContextClassLoader( oldContextClassLoader );
         }
 
+        if ( Features.buildConsumer().isActive() )
+        {
+            request.getRepositorySession().getData().set( TransformerContext.KEY,
+                                                          config.transformerContextBuilder.build() );
+        }
+
         if ( !noErrors )
         {
             throw new ProjectBuildingException( results );
@@ -443,7 +422,7 @@ public class DefaultProjectBuilder
 
     @SuppressWarnings( "checkstyle:parameternumber" )
     private boolean build( List<ProjectBuildingResult> results, List<InterimResult> interimResults,
-                           Map<String, MavenProject> projectIndex, List<File> pomFiles, Set<File> aggregatorFiles,
+                           Map<File, MavenProject> projectIndex, List<File> pomFiles, Set<File> aggregatorFiles,
                            boolean root, boolean recursive, InternalConfig config,
                            ReactorModelPool.Builder poolBuilder )
     {
@@ -467,20 +446,19 @@ public class DefaultProjectBuilder
 
     @SuppressWarnings( "checkstyle:parameternumber" )
     private boolean build( List<ProjectBuildingResult> results, List<InterimResult> interimResults,
-                           Map<String, MavenProject> projectIndex, File pomFile, Set<File> aggregatorFiles,
+                           Map<File, MavenProject> projectIndex, File pomFile, Set<File> aggregatorFiles,
                            boolean isRoot, boolean recursive, InternalConfig config,
                            ReactorModelPool.Builder poolBuilder )
     {
         boolean noErrors = true;
 
-        ModelBuildingRequest request = getModelBuildingRequest( config );
-
         MavenProject project = new MavenProject();
         project.setFile( pomFile );
 
-        request.setPomFile( pomFile );
-        request.setTwoPhaseBuilding( true );
-        request.setLocationTracking( true );
+        ModelBuildingRequest request = getModelBuildingRequest( config )
+                        .setPomFile( pomFile )
+                        .setTwoPhaseBuilding( true )
+                        .setLocationTracking( true );
 
         DefaultModelBuildingListener listener =
             new DefaultModelBuildingListener( project, projectBuildingHelper, config.request );
@@ -494,7 +472,7 @@ public class DefaultProjectBuilder
         catch ( ModelBuildingException e )
         {
             result = e.getResult();
-            if ( result == null || result.getEffectiveModel() == null )
+            if ( result == null || result.getFileModel() == null )
             {
                  results.add( new DefaultProjectBuildingResult( e.getModelId(), pomFile, e.getProblems() ) );
 
@@ -505,32 +483,17 @@ public class DefaultProjectBuilder
             noErrors = false;
         }
 
-        Model model = result.getEffectiveModel();
-        
-        poolBuilder.put( model.getPomFile().toPath(),  result.getRawModel() );
-        
-        try
-        {
-            // first pass: build without building parent.
-            initProject( project, projectIndex, false, result, new HashMap<>( 0 ), config.request );
-        }
-        catch ( InvalidArtifactRTException iarte )
-        {
-            result.getProblems().add( new DefaultModelProblem( null, ModelProblem.Severity.ERROR, null, model, -1, -1,
-                  iarte ) );
-        }
+        Model model = result.getFileModel().clone();
 
-        projectIndex.put( result.getModelIds().get( 0 ), project );
+        poolBuilder.put( model.getPomFile().toPath(),  model );
 
         InterimResult interimResult = new InterimResult( pomFile, request, result, listener, isRoot );
         interimResults.add( interimResult );
 
-        if ( recursive && !model.getModules().isEmpty() )
+        if ( recursive )
         {
             File basedir = pomFile.getParentFile();
-
             List<File> moduleFiles = new ArrayList<>();
-
             for ( String module : model.getModules() )
             {
                 if ( StringUtils.isEmpty( module ) )
@@ -609,6 +572,8 @@ public class DefaultProjectBuilder
             }
         }
 
+        projectIndex.put( pomFile, project );
+
         return noErrors;
     }
 
@@ -640,7 +605,7 @@ public class DefaultProjectBuilder
     }
 
     private boolean build( List<ProjectBuildingResult> results, List<MavenProject> projects,
-                           Map<String, MavenProject> projectIndex, List<InterimResult> interimResults,
+                           Map<File, MavenProject> projectIndex, List<InterimResult> interimResults,
                            ProjectBuildingRequest request, Map<File, Boolean> profilesXmls,
                            RepositorySystemSession session )
     {
@@ -685,12 +650,14 @@ public class DefaultProjectBuilder
             catch ( ModelBuildingException e )
             {
                 DefaultProjectBuildingResult result = null;
-                if ( project == null )
+                if ( project == null || interimResult.result.getEffectiveModel() == null )
                 {
                     result = new DefaultProjectBuildingResult( e.getModelId(), interimResult.pomFile, e.getProblems() );
                 }
                 else
                 {
+                    project.setModel( interimResult.result.getEffectiveModel() );
+
                     result = new DefaultProjectBuildingResult( project, e.getProblems(), null );
                 }
                 results.add( result );
@@ -703,15 +670,14 @@ public class DefaultProjectBuilder
     }
 
     @SuppressWarnings( "checkstyle:methodlength" )
-    private void initProject( MavenProject project, Map<String, MavenProject> projects,
+    private void initProject( MavenProject project, Map<File, MavenProject> projects,
                               boolean buildParentIfNotExisting, ModelBuildingResult result,
                               Map<File, Boolean> profilesXmls, ProjectBuildingRequest projectBuildingRequest )
     {
         Model model = result.getEffectiveModel();
 
         project.setModel( model );
-        project.setOriginalModel( result.getRawModel() );
-        project.setFile( model.getPomFile() );
+        project.setOriginalModel( result.getFileModel() );
 
         initParent( project, projects, buildParentIfNotExisting, result, projectBuildingRequest );
 
@@ -938,7 +904,7 @@ public class DefaultProjectBuilder
         }
     }
 
-    private void initParent( MavenProject project, Map<String, MavenProject> projects, boolean buildParentIfNotExisting,
+    private void initParent( MavenProject project, Map<File, MavenProject> projects, boolean buildParentIfNotExisting,
                              ModelBuildingResult result, ProjectBuildingRequest projectBuildingRequest )
     {
         Model parentModel = result.getModelIds().size() > 1 && !result.getModelIds().get( 1 ).isEmpty()
@@ -957,7 +923,7 @@ public class DefaultProjectBuilder
             // org.apache.maven.its.mng4834:parent:0.1
             String parentModelId = result.getModelIds().get( 1 );
             File parentPomFile = result.getRawModel( parentModelId ).getPomFile();
-            MavenProject parent = projects.get( parentModelId );
+            MavenProject parent = projects.get( parentPomFile );
             if ( parent == null && buildParentIfNotExisting )
             {
                 //
@@ -1096,15 +1062,21 @@ public class DefaultProjectBuilder
 
         private final ReactorModelCache modelCache;
 
-        InternalConfig( ProjectBuildingRequest request, ReactorModelPool modelPool, ReactorModelCache modelCache )
+        private final TransformerContextBuilder transformerContextBuilder;
+
+        InternalConfig( ProjectBuildingRequest request, ReactorModelPool modelPool, ReactorModelCache modelCache,
+                        TransformerContextBuilder transformerContextBuilder )
         {
             this.request = request;
             this.modelPool = modelPool;
             this.modelCache = modelCache;
+            this.transformerContextBuilder = transformerContextBuilder;
+
             session =
                 LegacyLocalRepositoryManager.overlay( request.getLocalRepository(), request.getRepositorySession(),
                                                       repoSystem );
             repositories = RepositoryUtils.toRepos( request.getRemoteRepositories() );
+
         }
 
     }
diff --git a/maven-core/src/main/java/org/apache/maven/project/ReactorModelCache.java b/maven-core/src/main/java/org/apache/maven/project/ReactorModelCache.java
index 72c25c7..20e57b9 100644
--- a/maven-core/src/main/java/org/apache/maven/project/ReactorModelCache.java
+++ b/maven-core/src/main/java/org/apache/maven/project/ReactorModelCache.java
@@ -80,13 +80,7 @@ class ReactorModelCache
             this.artifactId = ( artifactId != null ) ? artifactId : "";
             this.version = ( version != null ) ? version : "";
             this.tag = ( tag != null ) ? tag : "";
-
-            int hash = 17;
-            hash = hash * 31 + this.groupId.hashCode();
-            hash = hash * 31 + this.artifactId.hashCode();
-            hash = hash * 31 + this.version.hashCode();
-            hash = hash * 31 + this.tag.hashCode();
-            hashCode = hash;
+            this.hashCode = Objects.hash( groupId, artifactId, version, tag );
         }
 
         @Override
@@ -113,7 +107,6 @@ class ReactorModelCache
         {
             return hashCode;
         }
-
     }
     
     private static final class SourceCacheKey
diff --git a/maven-core/src/main/java/org/apache/maven/xml/internal/DefaultConsumerPomXMLFilterFactory.java b/maven-core/src/main/java/org/apache/maven/xml/internal/DefaultConsumerPomXMLFilterFactory.java
index aa68680..b27a87b 100644
--- a/maven-core/src/main/java/org/apache/maven/xml/internal/DefaultConsumerPomXMLFilterFactory.java
+++ b/maven-core/src/main/java/org/apache/maven/xml/internal/DefaultConsumerPomXMLFilterFactory.java
@@ -9,7 +9,7 @@ package org.apache.maven.xml.internal;
  * "License"); you may not use this file except in compliance
  * with the License.  You may obtain a copy of the License at
  *
- *   http://www.apache.org/licenses/LICENSE-2.0
+ *  http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
@@ -19,10 +19,7 @@ package org.apache.maven.xml.internal;
  * under the License.
  */
 
-import java.util.Optional;
-
 import org.apache.maven.model.building.DefaultBuildPomXMLFilterFactory;
-import org.apache.maven.model.building.TransformerContext;
 import org.apache.maven.xml.sax.filter.ConsumerPomXMLFilterFactory;
 
 /**
@@ -34,32 +31,8 @@ import org.apache.maven.xml.sax.filter.ConsumerPomXMLFilterFactory;
  */
 public class DefaultConsumerPomXMLFilterFactory extends ConsumerPomXMLFilterFactory
 {
-    private final TransformerContext context;
-    
     public DefaultConsumerPomXMLFilterFactory( DefaultBuildPomXMLFilterFactory buildPomXMLFilterFactory )
     {
         super( buildPomXMLFilterFactory );
-        this.context = buildPomXMLFilterFactory.getContext();
-    }
-    
-    @Override
-    protected Optional<String> getChangelist()
-    {
-        return Optional.ofNullable( context.getUserProperty( "changelist" ) );
-    }
-
-    @Override
-    protected Optional<String> getRevision()
-    {
-        return Optional.ofNullable( context.getUserProperty( "revision" ) );
     }
-
-    @Override
-    protected Optional<String> getSha1()
-    {
-        return Optional.ofNullable( context.getUserProperty( "sha1" ) );
-    }
-
-
-
 }
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/BuildModelSourceTransformer.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/BuildModelSourceTransformer.java
index 9766b8d..1119623 100644
--- a/maven-model-builder/src/main/java/org/apache/maven/model/building/BuildModelSourceTransformer.java
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/BuildModelSourceTransformer.java
@@ -58,7 +58,7 @@ class BuildModelSourceTransformer extends AbstractModelSourceTransformer
         throws TransformerConfigurationException, SAXException, ParserConfigurationException
     {
         BuildPomXMLFilterFactory buildPomXMLFilterFactory =
-            new DefaultBuildPomXMLFilterFactory( context, lexicalHandlerConsumer );
+            new DefaultBuildPomXMLFilterFactory( context, lexicalHandlerConsumer, false );
 
         return buildPomXMLFilterFactory.get( pomFile );
     }
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultBuildPomXMLFilterFactory.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultBuildPomXMLFilterFactory.java
index cf095e0..2424874 100644
--- a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultBuildPomXMLFilterFactory.java
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultBuildPomXMLFilterFactory.java
@@ -32,32 +32,35 @@ import org.apache.maven.xml.sax.filter.RelativeProject;
 import org.xml.sax.ext.LexicalHandler;
 
 /**
- * 
+ * A BuildPomXMLFilterFactory which is context aware
+ *
  * @author Robert Scholte
  * @since 4.0.0
  */
 public class DefaultBuildPomXMLFilterFactory extends BuildPomXMLFilterFactory
 {
     private final TransformerContext context;
-    
+
+    /**
+     *
+     * @param context a set of data to extract values from as required for the build pom
+     * @param lexicalHandlerConsumer the lexical handler consumer
+     * @param consume {@code true} if this factory is being used for creating the consumer pom, otherwise {@code false}
+     */
     public DefaultBuildPomXMLFilterFactory( TransformerContext context,
-                                            Consumer<LexicalHandler> lexicalHandlerConsumer )
+                                            Consumer<LexicalHandler> lexicalHandlerConsumer, 
+                                            boolean consume )
     {
-        super( lexicalHandlerConsumer );
+        super( lexicalHandlerConsumer, consume );
         this.context = context;
     }
-    
-    public final TransformerContext getContext()
-    {
-        return context;
-    }
-    
+
     @Override
     protected Function<Path, Optional<RelativeProject>> getRelativePathMapper()
     {
         return p -> Optional.ofNullable( context.getRawModel( p ) ).map( m -> toRelativeProject( m ) );
     }
-    
+
     @Override
     protected BiFunction<String, String, String> getDependencyKeyToVersionMapper()
     {
@@ -66,6 +69,24 @@ public class DefaultBuildPomXMLFilterFactory extends BuildPomXMLFilterFactory
                             .orElse( null );
     }
 
+    @Override
+    protected Optional<String> getChangelist()
+    {
+        return Optional.ofNullable( context.getUserProperty( "changelist" ) );
+    }
+
+    @Override
+    protected Optional<String> getRevision()
+    {
+        return Optional.ofNullable( context.getUserProperty( "revision" ) );
+    }
+
+    @Override
+    protected Optional<String> getSha1()
+    {
+        return Optional.ofNullable( context.getUserProperty( "sha1" ) );
+    }
+
     private static RelativeProject toRelativeProject( final Model m )
     {
         String groupId = m.getGroupId();
@@ -82,7 +103,7 @@ public class DefaultBuildPomXMLFilterFactory extends BuildPomXMLFilterFactory
 
         return new RelativeProject( groupId, m.getArtifactId(), version );
     }
-    
+
     private static String toVersion( final Model m )
     {
         String version = m.getVersion();
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java
index 8b31908..e12e566 100644
--- a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java
@@ -25,16 +25,21 @@ import static org.apache.maven.model.building.Result.newResult;
 import org.apache.maven.artifact.versioning.ArtifactVersion;
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 
 import javax.inject.Inject;
 import javax.inject.Named;
@@ -69,6 +74,7 @@ import org.apache.maven.model.composition.DependencyManagementImporter;
 import org.apache.maven.model.inheritance.InheritanceAssembler;
 import org.apache.maven.model.interpolation.ModelInterpolator;
 import org.apache.maven.model.io.ModelParseException;
+import org.apache.maven.model.io.ModelReader;
 import org.apache.maven.model.management.DependencyManagementInjector;
 import org.apache.maven.model.management.PluginManagementInjector;
 import org.apache.maven.model.merge.ModelMerger;
@@ -117,7 +123,7 @@ public class DefaultModelBuilder
 
     @Inject
     private ModelUrlNormalizer modelUrlNormalizer;
-    
+
     @Inject
     private SuperPomProvider superPomProvider;
 
@@ -151,7 +157,7 @@ public class DefaultModelBuilder
 
     @Inject
     private ReportingConverter reportingConverter;
-    
+
     private ModelMerger modelMerger = new FileToRawModelMerger();
 
     public DefaultModelBuilder setModelProcessor( ModelProcessor modelProcessor )
@@ -255,7 +261,13 @@ public class DefaultModelBuilder
         this.reportingConverter = reportingConverter;
         return this;
     }
-    
+
+    @Override
+    public DefaultTransformerContextBuilder newTransformerContextBuilder()
+    {
+        return new DefaultTransformerContextBuilder();
+    }
+
     @Override
     public ModelBuildingResult build( ModelBuildingRequest request )
         throws ModelBuildingException
@@ -272,6 +284,33 @@ public class DefaultModelBuilder
 
         DefaultModelProblemCollector problems = new DefaultModelProblemCollector( result );
 
+        // read and validate raw model
+        Model fileModel = readFileModel( request, problems );
+
+        request.setFileModel( fileModel );
+        result.setFileModel( fileModel );
+
+        activateFileModel( request, result, problems );
+
+        if ( !request.isTwoPhaseBuilding() )
+        {
+            return build( request, result, importIds );
+        }
+        else if ( hasModelErrors( problems ) )
+        {
+            throw problems.newModelBuildingException();
+        }
+
+        return result;
+    }
+
+    private void activateFileModel( final ModelBuildingRequest request, final DefaultModelBuildingResult result,
+                          DefaultModelProblemCollector problems )
+        throws ModelBuildingException
+    {
+        Model inputModel = request.getFileModel();
+        problems.setRootModel( inputModel );
+
         // profile activation
         DefaultProfileActivationContext profileActivationContext = getProfileActivationContext( request );
 
@@ -292,47 +331,89 @@ public class DefaultModelBuilder
             profileActivationContext.setUserProperties( profileProps );
         }
 
-        // read and validate raw model
-        Model inputModel = request.getRawModel();
-        if ( inputModel == null )
+        profileActivationContext.setProjectProperties( inputModel.getProperties() );
+        problems.setSource( inputModel );
+        List<Profile> activePomProfiles = profileSelector.getActiveProfiles( inputModel.getProfiles(),
+                                                                             profileActivationContext, problems );
+
+        // model normalization
+        problems.setSource( inputModel );
+        modelNormalizer.mergeDuplicates( inputModel, request, problems );
+
+        Map<String, Activation> interpolatedActivations = getProfileActivations( inputModel, false );
+        injectProfileActivations( inputModel, interpolatedActivations );
+
+        // profile injection
+        for ( Profile activeProfile : activePomProfiles )
+        {
+            profileInjector.injectProfile( inputModel, activeProfile, request, problems );
+        }
+
+        for ( Profile activeProfile : activeExternalProfiles )
         {
-            inputModel = readModel( request.getModelSource(), request.getPomFile(), request, problems );
+            profileInjector.injectProfile( inputModel, activeProfile, request, problems );
         }
+    }
+
+    @SuppressWarnings( "checkstyle:methodlength" )
+    private Model readEffectiveModel( final ModelBuildingRequest request, final DefaultModelBuildingResult result,
+                          DefaultModelProblemCollector problems )
+        throws ModelBuildingException
+    {
+        Model inputModel =
+            readRawModel( request, problems );
 
         problems.setRootModel( inputModel );
 
         ModelData resultData = new ModelData( request.getModelSource(), inputModel );
         ModelData superData = new ModelData( null, getSuperModel() );
 
+        // profile activation
+        DefaultProfileActivationContext profileActivationContext = getProfileActivationContext( request );
+
+        List<Profile> activeExternalProfiles = result.getActiveExternalProfiles();
+
+        if ( !activeExternalProfiles.isEmpty() )
+        {
+            Properties profileProps = new Properties();
+            for ( Profile profile : activeExternalProfiles )
+            {
+                profileProps.putAll( profile.getProperties() );
+            }
+            profileProps.putAll( profileActivationContext.getUserProperties() );
+            profileActivationContext.setUserProperties( profileProps );
+        }
+
         Collection<String> parentIds = new LinkedHashSet<>();
-        List<ModelData> lineage = new ArrayList<>();
+
+        List<Model> lineage = new ArrayList<>();
 
         for ( ModelData currentData = resultData; currentData != null; )
         {
-            lineage.add( currentData );
+            String modelId = currentData.getId();
+            result.addModelId( modelId );
 
             Model rawModel = currentData.getModel();
-            currentData.setRawModel( rawModel );
+            result.setRawModel( modelId, rawModel );
+
+            profileActivationContext.setProjectProperties( rawModel.getProperties() );
+            problems.setSource( rawModel );
+            List<Profile> activePomProfiles = profileSelector.getActiveProfiles( rawModel.getProfiles(),
+                                                                                 profileActivationContext, problems );
+            result.setActivePomProfiles( modelId, activePomProfiles );
 
             Model tmpModel = rawModel.clone();
-            currentData.setModel( tmpModel );
 
             problems.setSource( tmpModel );
 
             // model normalization
             modelNormalizer.mergeDuplicates( tmpModel, request, problems );
 
-            profileActivationContext.setProjectProperties( tmpModel.getProperties() );
-
-            List<Profile> activePomProfiles = profileSelector.getActiveProfiles( rawModel.getProfiles(),
-                                                                                 profileActivationContext, problems );
-            currentData.setActiveProfiles( activePomProfiles );
-
-            Map<String, Activation> interpolatedActivations = getProfileActivations( rawModel, false );
+            Map<String, Activation> interpolatedActivations = getProfileActivations( tmpModel, false );
             injectProfileActivations( tmpModel, interpolatedActivations );
 
             // profile injection
-            for ( Profile activeProfile : activePomProfiles )
+            for ( Profile activeProfile : result.getActivePomProfiles( modelId ) )
             {
                 profileInjector.injectProfile( tmpModel, activeProfile, request, problems );
             }
@@ -343,8 +424,11 @@ public class DefaultModelBuilder
                 {
                     profileInjector.injectProfile( tmpModel, activeProfile, request, problems );
                 }
+                result.setEffectiveModel( tmpModel );
             }
 
+            lineage.add( tmpModel );
+
             if ( currentData == superData )
             {
                 break;
@@ -352,36 +436,19 @@ public class DefaultModelBuilder
 
             configureResolver( request.getModelResolver(), tmpModel, problems );
 
-            ModelData parentData = readParent( tmpModel, currentData.getSource(), request, problems );
+            ModelData parentData =
+                readParent( currentData.getModel(), currentData.getSource(), request, result, problems );
 
             if ( parentData == null )
             {
                 currentData = superData;
             }
-            else if ( currentData == resultData )
-            { // First iteration - add initial id after version resolution.
-                currentData.setGroupId( currentData.getRawModel().getGroupId() == null ? parentData.getGroupId()
-                                                                                      : currentData.getRawModel()
-                                                                                          .getGroupId() );
-
-                currentData.setVersion( currentData.getRawModel().getVersion() == null ? parentData.getVersion()
-                                                                                      : currentData.getRawModel()
-                                                                                          .getVersion() );
-
-                currentData.setArtifactId( currentData.getRawModel().getArtifactId() );
-                parentIds.add( currentData.getId() );
-                // Reset - only needed for 'getId'.
-                currentData.setGroupId( null );
-                currentData.setArtifactId( null );
-                currentData.setVersion( null );
-                currentData = parentData;
-            }
             else if ( !parentIds.add( parentData.getId() ) )
             {
                 StringBuilder message = new StringBuilder( "The parents form a cycle: " );
-                for ( String modelId : parentIds )
+                for ( String parentId : parentIds )
                 {
-                    message.append( modelId ).append( " -> " );
+                    message.append( parentId ).append( " -> " );
                 }
                 message.append( parentData.getId() );
 
@@ -396,86 +463,50 @@ public class DefaultModelBuilder
             }
         }
 
-        problems.setSource( inputModel );
+        problems.setSource( result.getRawModel() );
         checkPluginVersions( lineage, request, problems );
 
         // inheritance assembly
         assembleInheritance( lineage, request, problems );
 
-        Model resultModel = resultData.getModel();
+        Model resultModel = lineage.get( 0 );
+
+        // consider caching inherited model
 
         problems.setSource( resultModel );
         problems.setRootModel( resultModel );
 
         // model interpolation
         resultModel = interpolateModel( resultModel, request, problems );
-        resultData.setModel( resultModel );
-
-        if ( resultModel.getParent() != null )
-        {
-            final ModelData parentData = lineage.get( 1 );
-            if ( parentData.getVersion() == null || parentData.getVersion().contains( "${" ) )
-            {
-                final Model interpolatedParent = interpolateModel( parentData.getModel(), request, problems );
-                // parentData.setModel( interpolatedParent );
-                parentData.setVersion( interpolatedParent.getVersion() );
-            }
-        }
 
         // url normalization
         modelUrlNormalizer.normalize( resultModel, request );
 
-        // Now the fully interpolated model is available: reconfigure the resolver
-        configureResolver( request.getModelResolver(), resultModel, problems, true );
-
-        resultData.setGroupId( resultModel.getGroupId() );
-        resultData.setArtifactId( resultModel.getArtifactId() );
-        resultData.setVersion( resultModel.getVersion() );
-
-        if ( request.getPomFile() != null )
-        {
-            intoCache( request.getModelCache(), new FileModelSource( request.getPomFile() ), ModelCacheTag.RAW,
-                      resultData );
-        }
-        else
-        {
-            intoCache( request.getModelCache(), request.getModelSource(), ModelCacheTag.RAW, resultData );
-        }
-
         result.setEffectiveModel( resultModel );
-        
-        for ( ModelData currentData : lineage )
-        {
-            String modelId = ( currentData != superData ) ? currentData.getId() : "";
-
-            result.addModelId( modelId );
-            result.setActivePomProfiles( modelId, currentData.getActiveProfiles() );
-            result.setRawModel( modelId, currentData.getRawModel() );
-        }
 
-        if ( !request.isTwoPhaseBuilding() )
-        {
-            build( request, result, importIds );
-        }
+        // Now the fully interpolated model is available: reconfigure the resolver
+        configureResolver( request.getModelResolver(), resultModel, problems, true );
 
-        return result;
+        return resultModel;
     }
 
     @Override
-    public ModelBuildingResult build( ModelBuildingRequest request, ModelBuildingResult result )
+    public ModelBuildingResult build( final ModelBuildingRequest request, final ModelBuildingResult result )
         throws ModelBuildingException
     {
         return build( request, result, new LinkedHashSet<>() );
     }
 
-    private ModelBuildingResult build( ModelBuildingRequest request, ModelBuildingResult result,
+    private ModelBuildingResult build( final ModelBuildingRequest request, final ModelBuildingResult phaseOneResult,
                                        Collection<String> imports )
         throws ModelBuildingException
     {
-        // phase 2
-        Model resultModel = result.getEffectiveModel();
+        DefaultModelBuildingResult result = asDefaultModelBuildingResult( phaseOneResult );
 
         DefaultModelProblemCollector problems = new DefaultModelProblemCollector( result );
+
+        // phase 2
+        Model resultModel = readEffectiveModel( request, result, problems );
         problems.setSource( resultModel );
         problems.setRootModel( resultModel );
 
@@ -529,6 +560,18 @@ public class DefaultModelBuilder
         return result;
     }
 
+    private DefaultModelBuildingResult asDefaultModelBuildingResult( ModelBuildingResult phaseOneResult )
+    {
+        if ( phaseOneResult instanceof DefaultModelBuildingResult )
+        {
+            return (DefaultModelBuildingResult) phaseOneResult;
+        }
+        else
+        {
+            return new DefaultModelBuildingResult( phaseOneResult );
+        }
+    }
+
     @Override
     public Result<? extends Model> buildRawModel( File pomFile, int validationLevel, boolean locationTracking )
     {
@@ -538,7 +581,7 @@ public class DefaultModelBuilder
             new DefaultModelProblemCollector( new DefaultModelBuildingResult() );
         try
         {
-            return newResult( readModel( null, pomFile, request, collector ), collector.getProblems() );
+            return newResult( readFileModel( request, collector ), collector.getProblems() );
         }
         catch ( ModelBuildingException e )
         {
@@ -547,24 +590,15 @@ public class DefaultModelBuilder
     }
 
     @SuppressWarnings( "checkstyle:methodlength" )
-    private Model readModel( ModelSource modelSource, File pomFile, ModelBuildingRequest request,
-                             DefaultModelProblemCollector problems )
+    private Model readFileModel( ModelBuildingRequest request,
+                                 DefaultModelProblemCollector problems )
         throws ModelBuildingException
     {
-        if ( modelSource == null )
+        ModelSource modelSource = request.getModelSource();
+        Model model = getModelFromCache( modelSource, request.getModelCache() );
+        if ( model != null )
         {
-            modelSource =
-                new FileModelSource( Objects.requireNonNull( pomFile, "neither pomFile nor modelSource can be null" ) );
-        }
-
-        Model model;
-        if ( pomFile == null )
-        {
-            model = getModelFromCache( modelSource, request.getModelCache() );
-            if ( model != null )
-            {
-                return model;
-            }
+            return model;
         }
 
         problems.setSource( modelSource.getLocation() );
@@ -577,7 +611,7 @@ public class DefaultModelBuilder
             options.put( ModelProcessor.SOURCE, modelSource );
 
             InputSource source;
-            if ( request.isLocationTracking() ) 
+            if ( request.isLocationTracking() )
             {
                 source = (InputSource) options.computeIfAbsent( ModelProcessor.INPUT_SOURCE, k -> new InputSource() );
             }
@@ -609,7 +643,7 @@ public class DefaultModelBuilder
                     throw e;
                 }
 
-                if ( pomFile != null )
+                if ( modelSource instanceof FileModelSource )
                 {
                     problems.add( new ModelProblemCollectorRequest( Severity.ERROR, Version.V20 )
                         .setMessage( "Malformed POM " + modelSource.getLocation() + ": " + e.getMessage() )
@@ -655,69 +689,106 @@ public class DefaultModelBuilder
                 .setMessage( "Non-readable POM " + modelSource.getLocation() + ": " + msg ).setException( e ) );
             throw problems.newModelBuildingException();
         }
-        if ( pomFile != null )
-        {
-            model.setPomFile( pomFile );
-        }
-        else if ( modelSource instanceof FileModelSource )
+
+        if ( modelSource instanceof FileModelSource )
         {
             model.setPomFile( ( (FileModelSource) modelSource ).getFile() );
         }
         problems.setSource( model );
 
         modelValidator.validateFileModel( model, request, problems );
-        request.setFileModel( model );
-        
-        if ( Features.buildConsumer().isActive() && pomFile != null )
+
+        if ( hasFatalErrors( problems ) )
+        {
+            throw problems.newModelBuildingException();
+        }
+
+        intoCache( request.getModelCache(), modelSource, ModelCacheTag.FILE, model );
+        if ( modelSource instanceof FileModelSource )
         {
+            if ( request.getTransformerContextBuilder() instanceof DefaultTransformerContextBuilder )
+            {
+                DefaultTransformerContextBuilder contextBuilder =
+                        (DefaultTransformerContextBuilder) request.getTransformerContextBuilder();
+                contextBuilder.putSource( getGroupId( model ), model.getArtifactId(), modelSource );
+            }
+        }
+
+        return model;
+    }
+
+    private Model readRawModel( ModelBuildingRequest request, DefaultModelProblemCollector problems )
+        throws ModelBuildingException
+    {
+        ModelSource modelSource = request.getModelSource();
+
+        ModelData cachedData = fromCache( request.getModelCache(), modelSource, ModelCacheTag.RAW );
+        if ( cachedData != null )
+        {
+          return cachedData.getModel();
+        }
+
+        Model rawModel;
+        if ( Features.buildConsumer().isActive() && modelSource instanceof FileModelSource )
+        {
+            rawModel = readFileModel( request, problems );
+            File pomFile = ( (FileModelSource) modelSource ).getFile();
+
+            TransformerContext context = null;
+            if ( request.getTransformerContextBuilder() != null )
+            {
+                context = request.getTransformerContextBuilder().initialize( request, problems );
+            }
+
             try
             {
-                Model rawModel =
-                    modelProcessor.read( pomFile,
-                               Collections.singletonMap( "transformerContext", request.getTransformerContext() ) );
-
-                model.setPomFile( pomFile );
-                
-                // model with locationTrackers, required for proper feedback during validations
-                model = request.getFileModel().clone();
-                
+                // must implement TransformContext, but should use request to access system properties/modelcache
+                Model transformedFileModel = modelProcessor.read( pomFile,
+                   Collections.singletonMap( ModelReader.TRANSFORMER_CONTEXT, context ) );
+
+                // rawModel with locationTrackers, required for proper feedback during validations
+
                 // Apply enriched data
-                modelMerger.merge( model, rawModel, false, null );
+                modelMerger.merge( rawModel, transformedFileModel, false, null );
             }
             catch ( IOException e )
             {
                 problems.add( new ModelProblemCollectorRequest( Severity.FATAL, Version.V37 ).setException( e ) );
             }
         }
+        else if ( request.getFileModel() == null )
+        {
+            rawModel = readFileModel( request, problems );
+        }
+        else
+        {
+            rawModel = request.getFileModel().clone();
+        }
 
-        modelValidator.validateRawModel( model, request, problems );
+        modelValidator.validateRawModel( rawModel, request, problems );
 
         if ( hasFatalErrors( problems ) )
         {
             throw problems.newModelBuildingException();
         }
 
-        if ( pomFile != null )
-        {
-            intoCache( request.getModelCache(), modelSource, ModelCacheTag.FILEMODEL, model );
-        }
+        String groupId = getGroupId( rawModel );
+        String artifactId = rawModel.getArtifactId();
+        String version = getVersion( rawModel );
 
-        String groupId = getGroupId( model );
-        String artifactId = model.getArtifactId();
-        String version = getVersion( model );
-
-        ModelData modelData = new ModelData( modelSource, model, groupId, artifactId, version );
+        ModelData modelData = new ModelData( modelSource, rawModel, groupId, artifactId, version );
         intoCache( request.getModelCache(), groupId, artifactId, version, ModelCacheTag.RAW, modelData );
+        intoCache( request.getModelCache(), modelSource, ModelCacheTag.RAW, modelData );
 
-        return model;
+        return rawModel;
     }
 
-    private Model getModelFromCache( ModelSource modelSource, ModelCache cache )
+    private Model getModelFromCache( Source source, ModelCache cache )
     {
         Model model;
-        if ( modelSource instanceof ArtifactModelSource )
+        if ( source instanceof ArtifactModelSource )
         {
-            ArtifactModelSource artifactModelSource = ( ArtifactModelSource ) modelSource;
+            ArtifactModelSource artifactModelSource = ( ArtifactModelSource ) source;
             ModelData modelData = fromCache( cache, artifactModelSource.getGroupId(),
                                             artifactModelSource.getArtifactId(),
                                             artifactModelSource.getVersion(), ModelCacheTag.RAW );
@@ -725,19 +796,14 @@ public class DefaultModelBuilder
             {
                 model = modelData.getModel();
             }
-            else 
+            else
             {
                 model = null;
             }
         }
         else
         {
-            model = fromCache( cache, modelSource, ModelCacheTag.FILEMODEL );
-            
-            if ( model != null )
-            {
-                model = model.clone();
-            }
+            model = fromCache( cache, source, ModelCacheTag.FILE );
         }
         return model;
     }
@@ -752,13 +818,13 @@ public class DefaultModelBuilder
         return groupId;
     }
 
-    private String getVersion( Model model ) 
+    private String getVersion( Model model )
     {
         String version = model.getVersion();
         if ( version == null && model.getParent() != null )
         {
             version = model.getParent().getVersion();
-        } 
+        }
         return version;
     }
 
@@ -807,7 +873,7 @@ public class DefaultModelBuilder
         }
     }
 
-    private void checkPluginVersions( List<ModelData> lineage, ModelBuildingRequest request,
+    private void checkPluginVersions( List<Model> lineage, ModelBuildingRequest request,
                                       ModelProblemCollector problems )
     {
         if ( request.getValidationLevel() < ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0 )
@@ -821,7 +887,7 @@ public class DefaultModelBuilder
 
         for ( int i = lineage.size() - 1; i >= 0; i-- )
         {
-            Model model = lineage.get( i ).getModel();
+            Model model = lineage.get( i );
             Build build = model.getBuild();
             if ( build != null )
             {
@@ -859,13 +925,13 @@ public class DefaultModelBuilder
         }
     }
 
-    private void assembleInheritance( List<ModelData> lineage, ModelBuildingRequest request,
+    private void assembleInheritance( List<Model> lineage, ModelBuildingRequest request,
                                       ModelProblemCollector problems )
     {
         for ( int i = lineage.size() - 2; i >= 0; i-- )
         {
-            Model parent = lineage.get( i + 1 ).getModel();
-            Model child = lineage.get( i ).getModel();
+            Model parent = lineage.get( i + 1 );
+            Model child = lineage.get( i );
             inheritanceAssembler.assembleModelInheritance( child, parent, request, problems );
         }
     }
@@ -940,7 +1006,7 @@ public class DefaultModelBuilder
                 problems.add( mpcr );
             }
 
-            
+
         }
         interpolatedModel.setPomFile( model.getPomFile() );
 
@@ -950,8 +1016,8 @@ public class DefaultModelBuilder
         return interpolatedModel;
     }
 
-    private ModelData readParent( Model childModel, ModelSource childSource, ModelBuildingRequest request,
-                                  DefaultModelProblemCollector problems )
+    private ModelData readParent( Model childModel, Source childSource, ModelBuildingRequest request,
+                                  ModelBuildingResult result, DefaultModelProblemCollector problems )
         throws ModelBuildingException
     {
         ModelData parentData = null;
@@ -960,18 +1026,18 @@ public class DefaultModelBuilder
 
         if ( parent != null )
         {
-            ModelSource expectedParentSource = getParentPomFile( childModel, childSource );
+            Source expectedParentSource = getParentPomFile( childModel, childSource );
 
             if ( expectedParentSource != null )
             {
-                ModelData candidateData = readParentLocally( childModel, childSource, request, problems );
+                ModelData candidateData = readParentLocally( childModel, childSource, request, result, problems );
 
                 if ( candidateData != null )
                 {
                     /*
-                     * NOTE: This is a sanity check of the cache hit. If the cached parent POM was locally resolved, 
+                     * NOTE: This is a sanity check of the cache hit. If the cached parent POM was locally resolved,
                      * the child's GAV should match with that parent, too. If it doesn't, we ignore the cache and
-                     * resolve externally, to mimic the behavior if the cache didn't exist in the first place. 
+                     * resolve externally, to mimic the behavior if the cache didn't exist in the first place.
                      * Otherwise, the cache would obscure a bad POM.
                      */
                     try
@@ -995,11 +1061,11 @@ public class DefaultModelBuilder
 
             if ( parentData == null )
             {
-                ModelData candidateData = fromCache( request.getModelCache(), 
+                ModelData candidateData = fromCache( request.getModelCache(),
                                                      parent.getGroupId(), parent.getArtifactId(),
                                                      parent.getVersion(), ModelCacheTag.RAW );
 
-                
+
                 if ( candidateData != null && candidateData.getSource() instanceof ArtifactModelSource )
                 {
                     // ArtifactModelSource means repositorySource
@@ -1007,15 +1073,15 @@ public class DefaultModelBuilder
                 }
                 else
                 {
-                    parentData = readParentExternally( childModel, request, problems );
-                    
-                    intoCache( request.getModelCache(), 
+                    parentData = readParentExternally( childModel, request, result, problems );
+
+                    intoCache( request.getModelCache(),
                               parentData.getGroupId(), parentData.getArtifactId(),
                               parentData.getVersion(), ModelCacheTag.RAW, parentData );
                 }
             }
-            
-            if ( parentData != null ) 
+
+            if ( parentData != null )
             {
                 Model parentModel = parentData.getModel();
 
@@ -1032,8 +1098,8 @@ public class DefaultModelBuilder
         return parentData;
     }
 
-    private ModelData readParentLocally( Model childModel, ModelSource childSource, ModelBuildingRequest request,
-                                         DefaultModelProblemCollector problems )
+    private ModelData readParentLocally( Model childModel, Source childSource, ModelBuildingRequest request,
+                                         ModelBuildingResult result, DefaultModelProblemCollector problems )
         throws ModelBuildingException
     {
         final Parent parent = childModel.getParent();
@@ -1049,7 +1115,16 @@ public class DefaultModelBuilder
                 return null;
             }
 
-            candidateModel = readModel( candidateSource, null, request, problems );
+            ModelBuildingRequest candidateBuildRequest = new FilterModelBuildingRequest( request )
+            {
+                  @Override
+                public ModelSource getModelSource()
+                {
+                    return candidateSource;
+                }
+            };
+
+            candidateModel = readRawModel( candidateBuildRequest, problems );
         }
         else
         {
@@ -1083,11 +1158,6 @@ public class DefaultModelBuilder
             groupId = candidateModel.getParent().getGroupId();
         }
         String artifactId = candidateModel.getArtifactId();
-        String version = candidateModel.getVersion();
-        if ( version == null && candidateModel.getParent() != null )
-        {
-            version = candidateModel.getParent().getVersion();
-        }
 
         if ( groupId == null || !groupId.equals( parent.getGroupId() ) || artifactId == null
             || !artifactId.equals( parent.getArtifactId() ) )
@@ -1107,6 +1177,12 @@ public class DefaultModelBuilder
                 .setMessage( buffer.toString() ).setLocation( parent.getLocation( "" ) ) );
             return null;
         }
+
+        String version = candidateModel.getVersion();
+        if ( version == null && candidateModel.getParent() != null )
+        {
+            version = candidateModel.getParent().getVersion();
+        }
         if ( version != null && parent.getVersion() != null && !version.equals( parent.getVersion() ) )
         {
             try
@@ -1161,12 +1237,10 @@ public class DefaultModelBuilder
          * if ( version == null || !version.equals( parent.getVersion() ) ) { return null; }
          */
 
-        ModelData parentData = new ModelData( candidateSource, candidateModel, groupId, artifactId, version );
-
-        return parentData;
+        return new ModelData( candidateSource, candidateModel, groupId, artifactId, version );
     }
 
-    private ModelSource getParentPomFile( Model childModel, ModelSource source )
+    private ModelSource getParentPomFile( Model childModel, Source source )
     {
         if ( !( source instanceof ModelSource2 ) )
         {
@@ -1184,12 +1258,12 @@ public class DefaultModelBuilder
     }
 
     private ModelData readParentExternally( Model childModel, ModelBuildingRequest request,
-                                            DefaultModelProblemCollector problems )
+                                            ModelBuildingResult result, DefaultModelProblemCollector problems )
         throws ModelBuildingException
     {
         problems.setSource( childModel );
 
-        Parent parent = childModel.getParent().clone();
+        Parent parent = childModel.getParent();
 
         String groupId = parent.getGroupId();
         String artifactId = parent.getArtifactId();
@@ -1237,20 +1311,28 @@ public class DefaultModelBuilder
             throw problems.newModelBuildingException();
         }
 
-        ModelBuildingRequest lenientRequest = request;
-        if ( request.getValidationLevel() > ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0 )
+        int validationLevel = Math.min( request.getValidationLevel(), ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0 );
+        ModelBuildingRequest lenientRequest = new FilterModelBuildingRequest( request )
         {
-            lenientRequest = new FilterModelBuildingRequest( request )
+            @Override
+            public int getValidationLevel()
             {
-                @Override
-                public int getValidationLevel()
-                {
-                    return ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0;
-                }
-            };
-        }
+                return validationLevel;
+            }
+
+            @Override
+            public ModelSource getModelSource()
+            {
+                return modelSource;
+            }
+            @Override
+            public Model getFileModel()
+            {
+                return null;
+            }
+        };
 
-        Model parentModel = readModel( modelSource, null, lenientRequest, problems );
+        Model parentModel = readRawModel( lenientRequest, problems );
 
         if ( !parent.getVersion().equals( version ) )
         {
@@ -1276,10 +1358,8 @@ public class DefaultModelBuilder
             // MNG-2199: What else to check here ?
         }
 
-        ModelData parentData = new ModelData( modelSource, parentModel, parent.getGroupId(), parent.getArtifactId(),
-                                              parent.getVersion() );
-
-        return parentData;
+        return new ModelData( modelSource, parentModel, parent.getGroupId(), parent.getArtifactId(),
+                              parent.getVersion() );
     }
 
     private Model getSuperModel()
@@ -1486,7 +1566,7 @@ public class DefaultModelBuilder
         }
     }
 
-    private <T> T fromCache( ModelCache modelCache, String groupId, String artifactId, String version,
+    private static <T> T fromCache( ModelCache modelCache, String groupId, String artifactId, String version,
                             ModelCacheTag<T> tag )
     {
         if ( modelCache != null )
@@ -1500,7 +1580,7 @@ public class DefaultModelBuilder
         return null;
     }
 
-    private <T> T fromCache( ModelCache modelCache, Source source, ModelCacheTag<T> tag )
+    private static <T> T fromCache( ModelCache modelCache, Source source, ModelCacheTag<T> tag )
     {
         if ( modelCache != null )
         {
@@ -1564,8 +1644,8 @@ public class DefaultModelBuilder
 
     /**
      * As long as Maven controls the BuildPomXMLFilter, the entities that need merging are known.
-     * All others can simply be copied from source to target to restore the locationTracker 
-     * 
+     * All others can simply be copied from source to target to restore the locationTracker
+     *
      * @author Robert Scholte
      * @since 4.0.0
      */
@@ -1577,7 +1657,7 @@ public class DefaultModelBuilder
         {
             // don't merge
         }
-        
+
 
         @Override
         protected void mergeBuildBase_Resources( BuildBase target, BuildBase source, boolean sourceDominant,
@@ -1585,21 +1665,21 @@ public class DefaultModelBuilder
         {
             // don't merge
         }
-        
+
         @Override
         protected void mergeBuildBase_TestResources( BuildBase target, BuildBase source, boolean sourceDominant,
                                                      Map<Object, Object> context )
         {
             // don't merge
         }
-        
+
         @Override
         protected void mergeCiManagement_Notifiers( CiManagement target, CiManagement source, boolean sourceDominant,
                                                     Map<Object, Object> context )
         {
             // don't merge
         }
-        
+
         @Override
         protected void mergeDependencyManagement_Dependencies( DependencyManagement target, DependencyManagement source,
                                                                boolean sourceDominant, Map<Object, Object> context )
@@ -1608,14 +1688,14 @@ public class DefaultModelBuilder
             target.getDependencies().stream().forEach( t -> mergeDependency( t, sourceIterator.next(), sourceDominant,
                                                                              context ) );
         }
-        
+
         @Override
         protected void mergeDependency_Exclusions( Dependency target, Dependency source, boolean sourceDominant,
                                                    Map<Object, Object> context )
         {
             // don't merge
         }
-        
+
         @Override
         protected void mergeModel_Contributors( Model target, Model source, boolean sourceDominant,
                                                 Map<Object, Object> context )
@@ -1629,21 +1709,21 @@ public class DefaultModelBuilder
         {
             // don't merge
         }
-        
+
         @Override
         protected void mergeModel_Licenses( Model target, Model source, boolean sourceDominant,
                                             Map<Object, Object> context )
         {
             // don't merge
         }
-        
+
         @Override
         protected void mergeModel_MailingLists( Model target, Model source, boolean sourceDominant,
                                                 Map<Object, Object> context )
         {
             // don't merge
         }
-        
+
         @Override
         protected void mergeModel_Profiles( Model target, Model source, boolean sourceDominant,
                                             Map<Object, Object> context )
@@ -1652,7 +1732,7 @@ public class DefaultModelBuilder
             target.getProfiles().stream().forEach( t -> mergeProfile( t, sourceIterator.next(), sourceDominant,
                                                                       context ) );
         }
-        
+
         @Override
         protected void mergeModelBase_Dependencies( ModelBase target, ModelBase source, boolean sourceDominant,
                                                     Map<Object, Object> context )
@@ -1661,21 +1741,21 @@ public class DefaultModelBuilder
             target.getDependencies().stream().forEach( t -> mergeDependency( t, sourceIterator.next(), sourceDominant,
                                                                              context ) );
         }
-        
+
         @Override
         protected void mergeModelBase_PluginRepositories( ModelBase target, ModelBase source, boolean sourceDominant,
                                                           Map<Object, Object> context )
         {
             target.setPluginRepositories( source.getPluginRepositories() );
         }
-        
+
         @Override
         protected void mergeModelBase_Repositories( ModelBase target, ModelBase source, boolean sourceDominant,
                                                     Map<Object, Object> context )
         {
             // don't merge
         }
-        
+
         @Override
         protected void mergePlugin_Dependencies( Plugin target, Plugin source, boolean sourceDominant,
                                                  Map<Object, Object> context )
@@ -1684,14 +1764,14 @@ public class DefaultModelBuilder
             target.getDependencies().stream().forEach( t -> mergeDependency( t, sourceIterator.next(), sourceDominant,
                                                                              context ) );
         }
-        
+
         @Override
         protected void mergePlugin_Executions( Plugin target, Plugin source, boolean sourceDominant,
                                                Map<Object, Object> context )
         {
             // don't merge
         }
-        
+
         @Override
         protected void mergeReporting_Plugins( Reporting target, Reporting source, boolean sourceDominant,
                                                Map<Object, Object> context )
@@ -1705,7 +1785,7 @@ public class DefaultModelBuilder
         {
             // don't merge
         }
-        
+
         @Override
         protected void mergePluginContainer_Plugins( PluginContainer target, PluginContainer source,
                                                      boolean sourceDominant, Map<Object, Object> context )
@@ -1713,4 +1793,132 @@ public class DefaultModelBuilder
             // don't merge
         }
     }
+
+    /**
+     * Builds up the transformer context.
+     * After the buildplan is ready, the build()-method returns the immutable context useful during distribution.
+     * This is an inner class, as it must be able to call readRawModel()
+     *
+     * @author Robert Scholte
+     * @since 4.0.0
+     */
+    private class DefaultTransformerContextBuilder implements TransformerContextBuilder
+    {
+        private final DefaultTransformerContext context = new DefaultTransformerContext();
+
+        private final Map<DefaultTransformerContext.GAKey, Set<Source>> mappedSources
+                = new ConcurrentHashMap<>( 64 );
+
+        /**
+         * If an interface could be extracted, DefaultModelProblemCollector should be ModelProblemCollectorExt
+         *
+         * @param request
+         * @param collector
+         * @return
+         */
+        @Override
+        public TransformerContext initialize( ModelBuildingRequest request, ModelProblemCollector collector )
+        {
+            // We must assume the TransformerContext was created using this.newTransformerContextBuilder()
+            DefaultModelProblemCollector problems = (DefaultModelProblemCollector) collector;
+            return new TransformerContext()
+            {
+                @Override
+                public String getUserProperty( String key )
+                {
+                    return context.userProperties.computeIfAbsent( key,
+                                                           k -> request.getUserProperties().getProperty( key ) );
+                }
+
+                @Override
+                public Model getRawModel( String gId, String aId )
+                {
+                    return context.modelByGA.computeIfAbsent( new DefaultTransformerContext.GAKey( gId, aId ),
+                                                              k -> findRawModel( gId, aId ) );
+                }
+
+                @Override
+                public Model getRawModel( Path path )
+                {
+                    return context.modelByPath.computeIfAbsent( path, k -> findRawModel( path ) );
+                }
+
+                private Model findRawModel( String groupId, String artifactId )
+                {
+                    Source source = getSource( groupId, artifactId );
+                    if ( source != null )
+                    {
+                        try
+                        {
+                            ModelBuildingRequest gaBuildingRequest = new FilterModelBuildingRequest( request )
+                            {
+                                @Override
+                                public ModelSource getModelSource()
+                                {
+                                    return (ModelSource) source;
+                                }
+
+                            };
+                            return readRawModel( gaBuildingRequest, problems );
+                        }
+                        catch ( ModelBuildingException e )
+                        {
+                            // gathered with problem collector
+                        }
+                    }
+                    return null;
+                }
+
+                private Model findRawModel( Path p )
+                {
+                    if ( !Files.isRegularFile( p ) )
+                    {
+                        throw new IllegalArgumentException( "Not a regular file: " + p );
+                    }
+
+                    DefaultModelBuildingRequest req = new DefaultModelBuildingRequest( request )
+                                    .setPomFile( p.toFile() )
+                                    .setModelSource( new FileModelSource( p.toFile() ) );
+
+                    try
+                    {
+                        return readRawModel( req, problems );
+                    }
+                    catch ( ModelBuildingException e )
+                    {
+                        // gathered with problem collector
+                    }
+                    return null;
+                }
+            };
+        }
+
+        @Override
+        public TransformerContext build()
+        {
+            return context;
+        }
+
+        public Source getSource( String groupId, String artifactId )
+        {
+            Set<Source> sources = mappedSources.get( new DefaultTransformerContext.GAKey( groupId, artifactId ) );
+            if ( sources == null )
+            {
+                return null;
+            }
+            return sources.stream().reduce( ( a, b ) ->
+            {
+                throw new IllegalStateException( String.format( "No unique Source for %s:%s: %s and %s",
+                                                                groupId, artifactId,
+                                                                a.getLocation(), b.getLocation() ) );
+            } ).orElse( null );
+        }
+
+        public void putSource( String groupId, String artifactId, Source source )
+        {
+            mappedSources.computeIfAbsent( new DefaultTransformerContext.GAKey( groupId, artifactId ),
+                    k -> new HashSet<>() ).add( source );
+        }
+
+    }
 }
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuildingRequest.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuildingRequest.java
index 2012bb1..44ad425 100644
--- a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuildingRequest.java
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuildingRequest.java
@@ -40,8 +40,6 @@ public class DefaultModelBuildingRequest
 {
     private Model fileModel;
 
-    private Model rawModel;
-
     private File pomFile;
 
     private ModelSource modelSource;
@@ -74,7 +72,7 @@ public class DefaultModelBuildingRequest
 
     private WorkspaceModelResolver workspaceResolver;
     
-    private TransformerContext context;
+    private TransformerContextBuilder contextBuilder;
 
     /**
      * Creates an empty request.
@@ -104,6 +102,7 @@ public class DefaultModelBuildingRequest
         setModelResolver( request.getModelResolver() );
         setModelBuildingListener( request.getModelBuildingListener() );
         setModelCache( request.getModelCache() );
+        setTransformerContextBuilder( request.getTransformerContextBuilder() );
     }
 
     @Override
@@ -397,17 +396,16 @@ public class DefaultModelBuildingRequest
         this.fileModel = fileModel;
         return this;
     }
-    
+
     @Override
     public Model getRawModel()
     {
-        return rawModel;
+        return null;
     }
 
     @Override
     public ModelBuildingRequest setRawModel( Model rawModel )
     {
-        this.rawModel = rawModel;
         return this;
     }
 
@@ -423,17 +421,18 @@ public class DefaultModelBuildingRequest
         this.workspaceResolver = workspaceResolver;
         return this;
     }
-    
+
     @Override
-    public TransformerContext getTransformerContext()
+    public TransformerContextBuilder getTransformerContextBuilder()
     {
-        return context;
+        return contextBuilder;
     }
     
     @Override
-    public ModelBuildingRequest setTransformerContext( TransformerContext context )
+    public ModelBuildingRequest setTransformerContextBuilder( TransformerContextBuilder contextBuilder )
     {
-        this.context = context;
+        this.contextBuilder = contextBuilder;
         return this;
     }
+
 }
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuildingResult.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuildingResult.java
index 13b7714..b2fcfa0 100644
--- a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuildingResult.java
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuildingResult.java
@@ -36,6 +36,7 @@ import org.apache.maven.model.Profile;
 class DefaultModelBuildingResult
     implements ModelBuildingResult
 {
+    private Model fileModel;
 
     private Model effectiveModel;
 
@@ -58,6 +59,35 @@ class DefaultModelBuildingResult
         problems = new ArrayList<>();
     }
 
+    DefaultModelBuildingResult( ModelBuildingResult result )
+    {
+        this();
+        this.activeExternalProfiles.addAll( result.getActiveExternalProfiles() );
+        this.effectiveModel = result.getEffectiveModel();
+        this.fileModel = result.getFileModel();
+        this.problems.addAll( result.getProblems() );
+
+        for ( String modelId : result.getModelIds() )
+        {
+            this.modelIds.add( modelId );
+            this.rawModels.put( modelId, result.getRawModel( modelId ) );
+            this.activePomProfiles.put( modelId, result.getActivePomProfiles( modelId ) );
+        }
+    }
+
+    @Override
+    public Model getFileModel()
+    {
+        return fileModel;
+    }
+
+    public DefaultModelBuildingResult setFileModel( Model fileModel )
+    {
+        this.fileModel = fileModel;
+
+        return this;
+    }
+
     @Override
     public Model getEffectiveModel()
     {
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultTransformerContext.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultTransformerContext.java
new file mode 100644
index 0000000..080c62b
--- /dev/null
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultTransformerContext.java
@@ -0,0 +1,95 @@
+package org.apache.maven.model.building;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+import org.apache.maven.model.Model;
+
+/**
+ *
+ * @author Robert Scholte
+ * @since 4.0.0
+ */
+class DefaultTransformerContext implements TransformerContext
+{
+    final Map<String, String> userProperties = new HashMap<>();
+
+    final Map<Path, Model> modelByPath = new HashMap<>();
+
+    final Map<GAKey, Model> modelByGA = new HashMap<>();
+
+    @Override
+    public String getUserProperty( String key )
+    {
+        return userProperties.get( key );
+    }
+
+    @Override
+    public Model getRawModel( Path p )
+    {
+        return modelByPath.get( p );
+    }
+
+    @Override
+    public Model getRawModel( String groupId, String artifactId )
+    {
+        return modelByGA.get( new GAKey( groupId, artifactId ) );
+    }
+
+    static class GAKey
+    {
+        private final String groupId;
+        private final String artifactId;
+        private final int hashCode;
+
+        GAKey( String groupId, String artifactId )
+        {
+            this.groupId = groupId;
+            this.artifactId = artifactId;
+            this.hashCode = Objects.hash( groupId, artifactId );
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return hashCode;
+        }
+
+        @Override
+        public boolean equals( Object obj )
+        {
+            if ( this == obj )
+            {
+                return true;
+            }
+            if ( !( obj instanceof GAKey ) )
+            {
+                return false;
+            }
+
+            GAKey other = (GAKey) obj;
+            return Objects.equals( artifactId, other.artifactId ) && Objects.equals( groupId, other.groupId );
+        }
+    }
+}
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/FilterModelBuildingRequest.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/FilterModelBuildingRequest.java
index 1dd2643..1374cbb 100644
--- a/maven-model-builder/src/main/java/org/apache/maven/model/building/FilterModelBuildingRequest.java
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/FilterModelBuildingRequest.java
@@ -268,7 +268,7 @@ class FilterModelBuildingRequest
         request.setFileModel( fileModel );
         return this;
     }
-    
+
     @Override
     public Model getRawModel()
     {
@@ -294,17 +294,17 @@ class FilterModelBuildingRequest
         request.setWorkspaceModelResolver( workspaceResolver );
         return this;
     }
-    
+
     @Override
-    public TransformerContext getTransformerContext()
+    public TransformerContextBuilder getTransformerContextBuilder()
     {
-        return request.getTransformerContext();
+        return request.getTransformerContextBuilder();
     }
     
     @Override
-    public ModelBuildingRequest setTransformerContext( TransformerContext context )
+    public ModelBuildingRequest setTransformerContextBuilder( TransformerContextBuilder contextBuilder )
     {
-        request.setTransformerContext( context );
+        request.setTransformerContextBuilder( contextBuilder );
         return this;
     }
 }
\ No newline at end of file
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelBuilder.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelBuilder.java
index e42469a..74c1fa6 100644
--- a/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelBuilder.java
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelBuilder.java
@@ -60,4 +60,6 @@ public interface ModelBuilder
      */
     Result<? extends Model> buildRawModel( File pomFile, int validationLevel, boolean locationTracking );
 
+    TransformerContextBuilder newTransformerContextBuilder();
+
 }
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelBuildingRequest.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelBuildingRequest.java
index c9451ef..bbd9e8b 100644
--- a/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelBuildingRequest.java
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelBuildingRequest.java
@@ -63,16 +63,19 @@ public interface ModelBuildingRequest
      * Denotes strict validation as recommended by the current Maven version.
      */
     int VALIDATION_LEVEL_STRICT = VALIDATION_LEVEL_MAVEN_3_0;
-    
+
     /**
-     * 
-     * @return the file model
+     * Gets the file model to build (with profile activation).
+     * If not set, model source will be used to load file model.
+     *
+     * @return The file model to build or {@code null} if not set.
      * @since 4.0.0
      */
     Model getFileModel();
-    
+
     /**
-     * 
+     * Set the file model with profile activation
+     *
      * @param fileModel
      * @return This request, never {@code null}.
      * @since 4.0.0
@@ -80,17 +83,15 @@ public interface ModelBuildingRequest
     ModelBuildingRequest setFileModel( Model fileModel );
 
     /**
-     * Gets the raw model to build. If not set, model source will be used to load raw model.
-     *
-     * @return The raw model to build or {@code null} if not set.
+     * @deprecated rawModel is never set, instead the fileModel is set
      */
+    @Deprecated
     Model getRawModel();
 
     /**
-     * Set raw model.
-     *
-     * @param rawModel
+     * @deprecated setting the rawModel has no effect, instead the fileModel of phase one will be set
      */
+    @Deprecated
     ModelBuildingRequest setRawModel( Model rawModel );
 
     /**
@@ -349,10 +350,9 @@ public interface ModelBuildingRequest
     WorkspaceModelResolver getWorkspaceModelResolver();
 
     ModelBuildingRequest setWorkspaceModelResolver( WorkspaceModelResolver workspaceResolver );
-    
-    TransformerContext getTransformerContext();
 
-    ModelBuildingRequest setTransformerContext( TransformerContext context );
-    
-    
+    TransformerContextBuilder getTransformerContextBuilder();
+
+    ModelBuildingRequest setTransformerContextBuilder( TransformerContextBuilder contextBuilder );
+
 }
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelBuildingResult.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelBuildingResult.java
index 44b1295..603d214 100644
--- a/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelBuildingResult.java
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelBuildingResult.java
@@ -43,6 +43,13 @@ public interface ModelBuildingResult
     List<String> getModelIds();
 
     /**
+     *
+     * @return the file model
+     * @since 4.0.0
+     */
+    Model getFileModel();
+
+    /**
      * Gets the assembled model.
      *
      * @return The assembled model, never {@code null}.
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelCacheTag.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelCacheTag.java
index f38bb62..7e0cb0d 100644
--- a/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelCacheTag.java
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelCacheTag.java
@@ -65,7 +65,7 @@ interface ModelCacheTag<T>
     T fromCache( T data );
 
     /**
-     * The tag used to denote raw model data.
+     * The tag used for the raw model without profile activation
      */
     ModelCacheTag<ModelData> RAW = new ModelCacheTag<ModelData>()
     {
@@ -129,12 +129,16 @@ interface ModelCacheTag<T>
 
     };
 
-    ModelCacheTag<Model> FILEMODEL = new ModelCacheTag<Model>() 
+    /**
+     * The tag used for the file model without profile activation
+     * @since 4.0.0
+     */
+    ModelCacheTag<Model> FILE = new ModelCacheTag<Model>()
     {
         @Override
         public String getName()
         {
-            return "file-model";
+            return "file";
         }
 
         @Override
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelData.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelData.java
index 1f39ad4..1ef4341 100644
--- a/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelData.java
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelData.java
@@ -19,10 +19,10 @@ package org.apache.maven.model.building;
  * under the License.
  */
 
-import java.util.List;
+import java.util.Objects;
 
+import org.apache.maven.building.Source;
 import org.apache.maven.model.Model;
-import org.apache.maven.model.Profile;
 
 /**
  * Holds a model along with some auxiliary information. This internal utility class assists the model builder during POM
@@ -32,13 +32,9 @@ import org.apache.maven.model.Profile;
  */
 class ModelData
 {
-    private final ModelSource source;
+    private final Source source;
 
-    private Model model;
-
-    private Model rawModel;
-
-    private List<Profile> activeProfiles;
+    private final Model model;
 
     private String groupId;
 
@@ -51,7 +47,7 @@ class ModelData
      *
      * @param model The model to wrap, may be {@code null}.
      */
-    ModelData( ModelSource source, Model model )
+    ModelData( Source source, Model model )
     {
         this.source = source;
         this.model = model;
@@ -65,16 +61,16 @@ class ModelData
      * @param artifactId The effective artifact identifier of the model, may be {@code null}.
      * @param version The effective version of the model, may be {@code null}.
      */
-    ModelData( ModelSource source, Model model, String groupId, String artifactId, String version )
+    ModelData( Source source, Model model, String groupId, String artifactId, String version )
     {
         this.source = source;
         this.model = model;
-        setGroupId( groupId );
-        setArtifactId( artifactId );
-        setVersion( version );
+        this.groupId = groupId;
+        this.artifactId = artifactId;
+        this.version = version;
     }
 
-    public ModelSource getSource()
+    public Source getSource()
     {
         return source;
     }
@@ -90,56 +86,6 @@ class ModelData
     }
 
     /**
-     * Sets the model being wrapped.
-     *
-     * @param model The model, may be {@code null}.
-     */
-    public void setModel( Model model )
-    {
-        this.model = model;
-    }
-
-    /**
-     * Gets the raw model being wrapped.
-     *
-     * @return The raw model or {@code null} if not set.
-     */
-    public Model getRawModel()
-    {
-        return rawModel;
-    }
-
-    /**
-     * Sets the raw model being wrapped.
-     *
-     * @param rawModel The raw model, may be {@code null}.
-     */
-    public void setRawModel( Model rawModel )
-    {
-        this.rawModel = rawModel;
-    }
-
-    /**
-     * Gets the active profiles from the model.
-     *
-     * @return The active profiles or {@code null} if not set.
-     */
-    public List<Profile> getActiveProfiles()
-    {
-        return activeProfiles;
-    }
-
-    /**
-     * Sets the active profiles from the model.
-     *
-     * @param activeProfiles The active profiles, may be {@code null}.
-     */
-    public void setActiveProfiles( List<Profile> activeProfiles )
-    {
-        this.activeProfiles = activeProfiles;
-    }
-
-    /**
      * Gets the effective group identifier of the model.
      *
      * @return The effective group identifier of the model or an empty string if unknown, never {@code null}.
@@ -150,16 +96,6 @@ class ModelData
     }
 
     /**
-     * Sets the effective group identifier of the model.
-     *
-     * @param groupId The effective group identifier of the model, may be {@code null}.
-     */
-    public void setGroupId( String groupId )
-    {
-        this.groupId = groupId;
-    }
-
-    /**
      * Gets the effective artifact identifier of the model.
      *
      * @return The effective artifact identifier of the model or an empty string if unknown, never {@code null}.
@@ -170,16 +106,6 @@ class ModelData
     }
 
     /**
-     * Sets the effective artifact identifier of the model.
-     *
-     * @param artifactId The effective artifact identifier of the model, may be {@code null}.
-     */
-    public void setArtifactId( String artifactId )
-    {
-        this.artifactId = artifactId;
-    }
-
-    /**
      * Gets the effective version of the model.
      *
      * @return The effective version of the model or an empty string if unknown, never {@code null}.
@@ -190,27 +116,14 @@ class ModelData
     }
 
     /**
-     * Sets the effective version of the model.
-     *
-     * @param version The effective version of the model, may be {@code null}.
-     */
-    public void setVersion( String version )
-    {
-        this.version = version;
-    }
-
-    /**
-     * Gets the effective identifier of the model in the form {@code <groupId>:<artifactId>:<version>}.
+     * Gets unique identifier of the model
      *
      * @return The effective identifier of the model, never {@code null}.
      */
     public String getId()
     {
-        StringBuilder buffer = new StringBuilder( 128 );
-
-        buffer.append( getGroupId() ).append( ':' ).append( getArtifactId() ).append( ':' ).append( getVersion() );
-
-        return buffer.toString();
+        // if source is null, it is the supermodel, which can be accessed via empty string
+        return Objects.toString( source, "" );
     }
 
     @Override
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelSourceTransformer.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelSourceTransformer.java
index b502e1e..a2556ce 100644
--- a/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelSourceTransformer.java
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelSourceTransformer.java
@@ -24,12 +24,24 @@ import java.io.InputStream;
 import java.nio.file.Path;
 
 /**
- * 
+ * The ModelSourceTransformer is a way to transform the local pom while streaming the input.
+ *
+ * The {@link #transform(Path, TransformerContext)} method uses a Path on purpose, to ensure the
+ * local pom is the the original source.
+ *
  * @author Robert Scholte
  * @since 4.0.0
  */
 public interface ModelSourceTransformer
 {
+    /**
+     *
+     * @param pomFile the pom file, cannot be null
+     * @param context the context, cannot be null
+     * @return the InputStream for the ModelReader
+     * @throws IOException if an I/O error occurs
+     * @throws TransformerException if the transformation fails
+     */
     InputStream transform( Path pomFile, TransformerContext context )
         throws IOException, TransformerException;
 }
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/TransformerContext.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/TransformerContext.java
index 3779a39..d7a43dc 100644
--- a/maven-model-builder/src/main/java/org/apache/maven/model/building/TransformerContext.java
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/TransformerContext.java
@@ -47,10 +47,10 @@ public interface TransformerContext
     /**
      * Get the model based on the path, will be used to resolve the parent based on relativePath
      * 
-     * @param p the path
+     * @param pomFile the path to the pomFile
      * @return the model, otherwise {@code null}
      */
-    Model getRawModel( Path p );
+    Model getRawModel( Path pomFile );
     
     /**
      * Get the model from the reactor based on the groupId and artifactId, will be used for reactor dependencies
@@ -60,5 +60,5 @@ public interface TransformerContext
      * @return the model, otherwise {@code null}
      * @throws IllegalStateException if multiple versions of the same GA are part of the reactor
      */
-    Model getRawModel( String groupId, String artifactId ) throws IllegalStateException;
+    Model getRawModel( String groupId, String artifactId );
 }
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelSourceTransformer.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/TransformerContextBuilder.java
similarity index 55%
copy from maven-model-builder/src/main/java/org/apache/maven/model/building/ModelSourceTransformer.java
copy to maven-model-builder/src/main/java/org/apache/maven/model/building/TransformerContextBuilder.java
index b502e1e..6117f2a 100644
--- a/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelSourceTransformer.java
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/TransformerContextBuilder.java
@@ -19,17 +19,28 @@ package org.apache.maven.model.building;
  * under the License.
  */
 
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.Path;
-
 /**
- * 
+ * The transformerContextBuilder is responsible for initializing the TransformerContext.
+ * In case rawModels are missing, it could do new buildingRequests on the ModelBuilder.
+ *
  * @author Robert Scholte
  * @since 4.0.0
  */
-public interface ModelSourceTransformer
+public interface TransformerContextBuilder
 {
-    InputStream transform( Path pomFile, TransformerContext context )
-        throws IOException, TransformerException;
+    /**
+     * This method is used to initialize the TransformerContext
+     *
+     * @param request the modelBuildingRequest
+     * @param problems the problemCollector
+     * @return the mutable transformerContext
+     */
+    TransformerContext initialize( ModelBuildingRequest request, ModelProblemCollector problems );
+
+    /**
+     * The immutable transformerContext, can be used after the buildplan is finished.
+     *
+     * @return the immutable transformerContext
+     */
+    TransformerContext build();
 }
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/io/DefaultModelReader.java b/maven-model-builder/src/main/java/org/apache/maven/model/io/DefaultModelReader.java
index 1bae747..1d8b264 100644
--- a/maven-model-builder/src/main/java/org/apache/maven/model/io/DefaultModelReader.java
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/io/DefaultModelReader.java
@@ -59,18 +59,14 @@ public class DefaultModelReader
     {
         this.transformer = transformer;
     }
-    
+
     @Override
     public Model read( File input, Map<String, ?> options )
         throws IOException
     {
         Objects.requireNonNull( input, "input cannot be null" );
 
-        TransformerContext context = null;
-        if ( options != null )
-        {
-            context = (TransformerContext) options.get( "transformerContext" );
-        }        
+        TransformerContext context = getTransformerContext( options );
 
         final InputStream is;
         if ( context == null )
@@ -135,6 +131,12 @@ public class DefaultModelReader
         return (InputSource) value;
     }
 
+    private TransformerContext getTransformerContext( Map<String, ?> options )
+    {
+        Object value = ( options != null ) ? options.get( TRANSFORMER_CONTEXT ) : null;
+        return (TransformerContext) value;
+    }
+
     private Model read( Reader reader, boolean strict, InputSource source )
         throws IOException
     {
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/io/ModelReader.java b/maven-model-builder/src/main/java/org/apache/maven/model/io/ModelReader.java
index 75a5ebe..04b1a30 100644
--- a/maven-model-builder/src/main/java/org/apache/maven/model/io/ModelReader.java
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/io/ModelReader.java
@@ -49,6 +49,12 @@ public interface ModelReader
     String INPUT_SOURCE = "org.apache.maven.model.io.inputSource";
 
     /**
+     * The key for the option to provide a transformer context, which can be used to transform the input while reading
+     * to get an advanced version of the model.
+     */
+    String TRANSFORMER_CONTEXT = "transformerContext";
+
+    /**
      * Reads the model from the specified file.
      *
      * @param input The file to deserialize the model from, must not be {@code null}.
diff --git a/maven-model-builder/src/site/apt/index.apt b/maven-model-builder/src/site/apt/index.apt
index e18ad9d..2f20852 100644
--- a/maven-model-builder/src/site/apt/index.apt
+++ b/maven-model-builder/src/site/apt/index.apt
@@ -43,6 +43,22 @@ Maven Model Builder
    Notice that model interpolation hasn't happened yet, then interpolation for file-based activation is limited to
    <<<$\{basedir}>>> (since Maven 3), System properties and request properties
 
+   ** file model validation: <<<ModelValidator>>> ({{{./apidocs/org/apache/maven/model/validation/ModelValidator.html}javadoc}}),
+   with its <<<DefaultModelValidator>>> implementation
+   ({{{./xref/org/apache/maven/model/validation/DefaultModelValidator.html}source}})
+
+   []
+
+ * phase 2, with optional plugin processing
+
+   ** Build up a raw model by re-reading the file and enrich it based on information available in the reactor. Some features:
+
+      *** Resolve version of versionless parents based on realtivePath (including ci-friendly versions)
+
+      *** Resolve version of versionless dependencies that are part of the reactor
+
+   []
+
    ** raw model validation: <<<ModelValidator>>> ({{{./apidocs/org/apache/maven/model/validation/ModelValidator.html}javadoc}}),
    with its <<<DefaultModelValidator>>> implementation
    ({{{./xref/org/apache/maven/model/validation/DefaultModelValidator.html}source}})
@@ -65,10 +81,6 @@ Maven Model Builder
    with its <<<DefaultUrlNormalizer>>> implementation
    ({{{./xref/org/apache/maven/model/path/DefaultUrlNormalizer.html}source}})
 
-   []
-
- * phase 2, with optional plugin processing
-
    ** model path translation: <<<ModelPathTranslator>>> ({{{./apidocs/org/apache/maven/model/path/ModelPathTranslator.html}javadoc}}),
    with its <<<DefaultModelPathTranslator>>> implementation
    ({{{./xref/org/apache/maven/model/path/DefaultModelPathTranslator.html}source}})
@@ -124,7 +136,7 @@ Maven Model Builder
 
   Notice that the 5 URLs from the model (<<<project.url>>>, <<<project.scm.connection>>>, <<<project.scm.developerConnection>>>,
   <<<project.scm.url>>> and <<<project.distributionManagement.site.url>>>) have a special inheritance handling:
-  
+
   ** if not configured in current model, the inherited value is the parent's one with current artifact id appended,
 
   ** since Maven 3.5.0, if <<<project.directory>>> POM property value is defined, it is used instead of artifact id:
@@ -194,6 +206,11 @@ Maven Model Builder
 *----+------+------+
 | <<<settings.*>>> | Local user settings (see {{{../maven-settings/settings.html}settings reference}}) | <<<$\{settings.localRepository\}>>> |
 *----+------+------+
+| <<<changelist>>> \
+<<<revision>>> \
+<<<sha1>>> | CI friendly placeholders for the project version (see {{{/maven-ci-friendly.html}Maven CI Friendly Versions}}) | <<<1.0.0-$\{changelist\}-SNAPSHOT>>> |
+*----+------+------+
+
 
 **  Notice
 
diff --git a/maven-model-builder/src/test/java/org/apache/maven/model/validation/DefaultModelValidatorTest.java b/maven-model-builder/src/test/java/org/apache/maven/model/validation/DefaultModelValidatorTest.java
index d2a9e60..252b1d9 100644
--- a/maven-model-builder/src/test/java/org/apache/maven/model/validation/DefaultModelValidatorTest.java
+++ b/maven-model-builder/src/test/java/org/apache/maven/model/validation/DefaultModelValidatorTest.java
@@ -69,8 +69,6 @@ public class DefaultModelValidatorTest
 
         SimpleProblemCollector problems = new SimpleProblemCollector( model );
         
-        request.setFileModel( model );
-
         validator.validateEffectiveModel( model, request, problems );
 
         return problems;
@@ -87,8 +85,6 @@ public class DefaultModelValidatorTest
 
         validator.validateFileModel( model, request, problems );
         
-        request.setFileModel( model );
-        
         validator.validateRawModel( model, request, problems );
 
         return problems;
diff --git a/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/BuildPomXMLFilterFactory.java b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/BuildPomXMLFilterFactory.java
index 066be35..2a194d1 100644
--- a/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/BuildPomXMLFilterFactory.java
+++ b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/BuildPomXMLFilterFactory.java
@@ -41,16 +41,19 @@ import org.xml.sax.ext.LexicalHandler;
  */
 public class BuildPomXMLFilterFactory
 {
+    private final boolean consume;
+
     private final Consumer<LexicalHandler> lexicalHandlerConsumer;
-    
-    public BuildPomXMLFilterFactory()
+
+    public BuildPomXMLFilterFactory( Consumer<LexicalHandler> lexicalHandlerConsumer )
     {
-        this( null ); 
+        this( lexicalHandlerConsumer, false );
     }
-    
-    public BuildPomXMLFilterFactory( Consumer<LexicalHandler> lexicalHandlerConsumer )
+
+    public BuildPomXMLFilterFactory( Consumer<LexicalHandler> lexicalHandlerConsumer, boolean consume )
     {
         this.lexicalHandlerConsumer = lexicalHandlerConsumer;
+        this.consume = consume;
     }
 
     /**
@@ -89,6 +92,18 @@ public class BuildPomXMLFilterFactory
             parent = parentFilter;
         }
 
+        CiFriendlyXMLFilter ciFriendlyFilter = new CiFriendlyXMLFilter( consume );
+        getChangelist().ifPresent( ciFriendlyFilter::setChangelist  );
+        getRevision().ifPresent( ciFriendlyFilter::setRevision );
+        getSha1().ifPresent( ciFriendlyFilter::setSha1 );
+
+        if ( ciFriendlyFilter.isSet() )
+        {
+            ciFriendlyFilter.setParent( parent );
+            parent.setLexicalHandler( ciFriendlyFilter );
+            parent = ciFriendlyFilter;
+        }
+
         return new BuildPomXMLFilter( parent );
     }
     
@@ -98,7 +113,7 @@ public class BuildPomXMLFilterFactory
         xmlReader.setFeature( "http://xml.org/sax/features/namespaces", true );
         return xmlReader;
     }
-    
+
     /**
      * @return the mapper or {@code null} if relativePaths don't need to be mapped
      */
@@ -111,4 +126,22 @@ public class BuildPomXMLFilterFactory
     {
         return null;
     }
+
+    // getters for the 3 magic properties of CIFriendly versions ( https://maven.apache.org/maven-ci-friendly.html )
+
+    protected Optional<String> getChangelist()
+    {
+        return Optional.empty();
+    }
+
+    protected Optional<String> getRevision()
+    {
+        return Optional.empty();
+    }
+
+    protected Optional<String> getSha1()
+    {
+        return Optional.empty();
+    }
+
 }
diff --git a/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/CiFriendlyXMLFilter.java b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/CiFriendlyXMLFilter.java
index 0bda110..2da8ddc 100644
--- a/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/CiFriendlyXMLFilter.java
+++ b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/CiFriendlyXMLFilter.java
@@ -26,27 +26,30 @@ import org.xml.sax.SAXException;
 
 /**
  * Resolves all ci-friendly properties occurrences between version-tags
- * 
+ *
  * @author Robert Scholte
  * @since 4.0.0
  */
 class CiFriendlyXMLFilter
     extends AbstractSAXFilter
 {
+    private final boolean replace;
+
     private Function<String, String> replaceChain = Function.identity();
-    
-    private String characters; 
-    
+
+    private String characters;
+
     private boolean parseVersion;
-    
-    CiFriendlyXMLFilter()
+
+    CiFriendlyXMLFilter( boolean replace )
     {
-        super();
+        this.replace = replace;
     }
 
-    CiFriendlyXMLFilter( AbstractSAXFilter parent )
+    CiFriendlyXMLFilter( AbstractSAXFilter parent, boolean replace )
     {
         super( parent );
+        this.replace = replace;
     }
 
     public CiFriendlyXMLFilter setChangelist( String changelist )
@@ -54,7 +57,7 @@ class CiFriendlyXMLFilter
         replaceChain = replaceChain.andThen( t -> t.replace( "${changelist}", changelist ) );
         return this;
     }
-    
+
     public CiFriendlyXMLFilter setRevision( String revision )
     {
         replaceChain = replaceChain.andThen( t -> t.replace( "${revision}", revision ) );
@@ -66,7 +69,7 @@ class CiFriendlyXMLFilter
         replaceChain = replaceChain.andThen( t -> t.replace( "${sha1}", sha1 ) );
         return this;
     }
-    
+
     /**
      * @return {@code true} is any of the ci properties is set, otherwise {@code false}
      */
@@ -74,7 +77,7 @@ class CiFriendlyXMLFilter
     {
         return !replaceChain.equals( Function.identity() );
     }
-    
+
     @Override
     public void characters( char[] ch, int start, int length )
         throws SAXException
@@ -100,7 +103,7 @@ class CiFriendlyXMLFilter
 
         super.startElement( uri, localName, qName, atts );
     }
-    
+
     @Override
     public void endElement( String uri, String localName, String qName )
         throws SAXException
@@ -108,7 +111,7 @@ class CiFriendlyXMLFilter
         if ( parseVersion )
         {
             // assuming this has the best performance
-            if ( characters != null && characters.contains( "${" ) )
+            if ( replace && characters != null && characters.contains( "${" ) )
             {
                 char[] ch = replaceChain.apply( characters ).toCharArray();
                 super.characters( ch, 0, ch.length );
@@ -124,4 +127,4 @@ class CiFriendlyXMLFilter
 
         super.endElement( uri, localName, qName );
     }
-}
\ No newline at end of file
+}
diff --git a/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/ConsumerPomXMLFilterFactory.java b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/ConsumerPomXMLFilterFactory.java
index 18e3208..0f2bd7e 100644
--- a/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/ConsumerPomXMLFilterFactory.java
+++ b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/ConsumerPomXMLFilterFactory.java
@@ -20,7 +20,6 @@ package org.apache.maven.xml.sax.filter;
  */
 
 import java.nio.file.Path;
-import java.util.Optional;
 
 import javax.xml.parsers.ParserConfigurationException;
 import javax.xml.transform.TransformerConfigurationException;
@@ -46,20 +45,10 @@ public class ConsumerPomXMLFilterFactory
     {
         BuildPomXMLFilter parent = buildPomXMLFilterFactory.get( projectPath );
         
-        
+
         // Ensure that xs:any elements aren't touched by next filters
         AbstractSAXFilter filter = new FastForwardFilter( parent );
-        
-        CiFriendlyXMLFilter ciFriendlyFilter = new CiFriendlyXMLFilter( filter );
-        getChangelist().ifPresent( ciFriendlyFilter::setChangelist  );
-        getRevision().ifPresent( ciFriendlyFilter::setRevision );
-        getSha1().ifPresent( ciFriendlyFilter::setSha1 );
-        
-        if ( ciFriendlyFilter.isSet() )
-        {
-            filter = ciFriendlyFilter;
-        }
-        
+
         // Strip modules
         filter = new ModulesXMLFilter( filter );
         // Adjust relativePath
@@ -67,22 +56,4 @@ public class ConsumerPomXMLFilterFactory
         
         return new ConsumerPomXMLFilter( filter );
     }
-    
-    // getters for the 3 magic properties of CIFriendly versions ( https://maven.apache.org/maven-ci-friendly.html )
-
-    protected Optional<String> getChangelist()
-    {
-        return Optional.empty();
-    }
-
-    protected Optional<String> getRevision()
-    {
-        return Optional.empty();
-    }
-
-    protected Optional<String> getSha1()
-    {
-        return Optional.empty();
-    }
-
 }
diff --git a/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/ParentXMLFilter.java b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/ParentXMLFilter.java
index ac48188..2d717f0 100644
--- a/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/ParentXMLFilter.java
+++ b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/ParentXMLFilter.java
@@ -19,6 +19,7 @@ package org.apache.maven.xml.sax.filter;
  * under the License.
  */
 
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 
@@ -34,10 +35,10 @@ import org.xml.sax.SAXException;
  * <p>
  * Transforms relativePath to version.
  * We could decide to simply allow {@code <parent/>}, but let's require the GA for now for checking
- * This filter does NOT remove the relativePath (which is done by {@link RelativePathXMLFilter}, it will only 
- * optionally include the version based on the path 
+ * This filter does NOT remove the relativePath (which is done by {@link RelativePathXMLFilter}, it will only
+ * optionally include the version based on the path
  * </p>
- * 
+ *
  * @author Robert Scholte
  * @since 4.0.0
  */
@@ -51,24 +52,24 @@ class ParentXMLFilter
 
     // whiteSpace after <parent>, to be used to position <version>
     private String parentWhitespace = "";
-    
+
     private String groupId;
 
     private String artifactId;
-    
+
     private String relativePath;
 
     private boolean hasVersion;
-    
+
+    private boolean hasRelativePath;
+
     private Optional<RelativeProject> resolvedParent;
 
     private final Function<Path, Optional<RelativeProject>> relativePathMapper;
-    
+
     private Path projectPath;
 
     /**
-     * 
-     * 
      * @param relativePathMapper
      */
     ParentXMLFilter( Function<Path, Optional<RelativeProject>> relativePathMapper )
@@ -80,7 +81,7 @@ class ParentXMLFilter
     {
         this.projectPath = projectPath;
     }
-    
+
     @Override
     protected boolean isParsing()
     {
@@ -92,7 +93,7 @@ class ParentXMLFilter
     {
         return state;
     }
-    
+
     @Override
     public void startElement( String uri, final String localName, String qName, Attributes atts )
         throws SAXException
@@ -105,10 +106,13 @@ class ParentXMLFilter
         if ( parsingParent )
         {
             state = localName;
-            
+
             hasVersion |= "version".equals( localName );
+
+            // can be set to empty on purpose to enforce repository download
+            hasRelativePath |= "relativePath".equals( localName );
         }
-        
+
         super.startElement( uri, localName, qName, atts );
     }
 
@@ -119,9 +123,9 @@ class ParentXMLFilter
         if ( parsingParent )
         {
             final String eventState = state;
-            
+
             final String charSegment =  new String( ch, start, length );
-            
+
             switch ( eventState )
             {
                 case "parent":
@@ -140,7 +144,7 @@ class ParentXMLFilter
                     break;
             }
         }
-        
+
         super.characters( ch, start, length );
     }
 
@@ -153,33 +157,37 @@ class ParentXMLFilter
             switch ( localName )
             {
                 case "parent":
-                    if ( !hasVersion || relativePath != null )
+                    if ( !hasVersion && ( !hasRelativePath || relativePath != null ) )
                     {
                         resolvedParent =
                             resolveRelativePath( Paths.get( Objects.toString( relativePath, "../pom.xml" ) ) );
                     }
-                    
+                    else
+                    {
+                        resolvedParent = Optional.empty();
+                    }
+
                     if ( !hasVersion && resolvedParent.isPresent() )
                     {
                         try ( Includer i = super.include() )
                         {
                             super.characters( parentWhitespace.toCharArray(), 0,
                                               parentWhitespace.length() );
-                            
+
                             String versionQName = SAXEventUtils.renameQName( qName, "version" );
-                            
+
                             super.startElement( uri, "version", versionQName, null );
-                            
+
                             String resolvedParentVersion = resolvedParent.get().getVersion();
-                            
+
                             super.characters( resolvedParentVersion.toCharArray(), 0,
                                                           resolvedParentVersion.length() );
-                            
+
                             super.endElement( uri, "version", versionQName );
                         }
                     }
                     super.executeEvents();
-                    
+
                     parsingParent = false;
                     break;
                 default:
@@ -187,20 +195,25 @@ class ParentXMLFilter
                     break;
             }
         }
-        
+
         super.endElement( uri, localName, qName );
         state = "";
     }
 
     protected Optional<RelativeProject> resolveRelativePath( Path relativePath )
     {
-        Optional<RelativeProject> mappedProject =
-            relativePathMapper.apply( projectPath.resolve( relativePath ).normalize() );
-        
+        Path pomPath = projectPath.resolve( relativePath );
+        if ( Files.isDirectory( pomPath ) )
+        {
+            pomPath = pomPath.resolve( "pom.xml" );
+        }
+
+        Optional<RelativeProject> mappedProject = relativePathMapper.apply( pomPath.normalize() );
+
         if ( mappedProject.isPresent() )
         {
             RelativeProject project = mappedProject.get();
-            
+
             if ( Objects.equals( groupId, project.getGroupId() )
                 && Objects.equals( artifactId, project.getArtifactId() ) )
             {
diff --git a/maven-xml/src/test/java/org/apache/maven/xml/sax/filter/CiFriendlyXMLFilterTest.java b/maven-xml/src/test/java/org/apache/maven/xml/sax/filter/CiFriendlyXMLFilterTest.java
index e93c720..7dcdc4f 100644
--- a/maven-xml/src/test/java/org/apache/maven/xml/sax/filter/CiFriendlyXMLFilterTest.java
+++ b/maven-xml/src/test/java/org/apache/maven/xml/sax/filter/CiFriendlyXMLFilterTest.java
@@ -35,7 +35,7 @@ public class CiFriendlyXMLFilterTest extends AbstractXMLFilterTests
     @Before
     public void setUp()
     {
-        filter = new CiFriendlyXMLFilter();
+        filter = new CiFriendlyXMLFilter( true );
         filter.setChangelist( "CHANGELIST" );
     }
     
diff --git a/maven-xml/src/test/java/org/apache/maven/xml/sax/filter/ConsumerPomXMLFilterTest.java b/maven-xml/src/test/java/org/apache/maven/xml/sax/filter/ConsumerPomXMLFilterTest.java
index 70dc5b4..5ea73a4 100644
--- a/maven-xml/src/test/java/org/apache/maven/xml/sax/filter/ConsumerPomXMLFilterTest.java
+++ b/maven-xml/src/test/java/org/apache/maven/xml/sax/filter/ConsumerPomXMLFilterTest.java
@@ -47,7 +47,7 @@ public class ConsumerPomXMLFilterTest extends AbstractXMLFilterTests
     protected AbstractSAXFilter getFilter( Consumer<LexicalHandler> lexicalHandlerConsumer )
         throws SAXException, ParserConfigurationException, TransformerConfigurationException
     {
-        final BuildPomXMLFilterFactory buildPomXMLFilterFactory = new BuildPomXMLFilterFactory( lexicalHandlerConsumer )
+        final BuildPomXMLFilterFactory buildPomXMLFilterFactory = new BuildPomXMLFilterFactory( lexicalHandlerConsumer, true )
         {
             @Override
             protected Function<Path, Optional<RelativeProject>> getRelativePathMapper()
@@ -60,10 +60,7 @@ public class ConsumerPomXMLFilterTest extends AbstractXMLFilterTests
             {
                 return null;
             }
-        };
-        
-        ConsumerPomXMLFilter filter = new ConsumerPomXMLFilterFactory( buildPomXMLFilterFactory )
-        {
+
             @Override
             protected Optional<String> getSha1()
             {
@@ -81,7 +78,11 @@ public class ConsumerPomXMLFilterTest extends AbstractXMLFilterTests
             {
                 return Optional.of( "CL" );
             }
-        }.get( Paths.get( "pom.xml" ) );
+
+        };
+
+        ConsumerPomXMLFilter filter =
+            new ConsumerPomXMLFilterFactory( buildPomXMLFilterFactory ).get( Paths.get( "pom.xml" ) );
         filter.setFeature( "http://xml.org/sax/features/namespaces", true );
         return filter;
     }
@@ -234,7 +235,7 @@ public class ConsumerPomXMLFilterTest extends AbstractXMLFilterTests
         String actual = transform( input );
         assertThat( actual ).and( expected ).areIdentical();
     }
-    
+
     @Test
     public void lexicalHandler() throws Exception
     {
@@ -245,7 +246,7 @@ public class ConsumerPomXMLFilterTest extends AbstractXMLFilterTests
                         + "<!--post-in-->"
                         + "</modules>"
                         + "<!--after--></project>";
-        String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + 
+        String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
                         "<project><!--before--><!--after--></project>";
         String actual = transform( input );
         assertThat( actual ).and( expected ).areIdentical();
diff --git a/maven-xml/src/test/java/org/apache/maven/xml/sax/filter/ParentXMLFilterTest.java b/maven-xml/src/test/java/org/apache/maven/xml/sax/filter/ParentXMLFilterTest.java
index eee0359..809aef5 100644
--- a/maven-xml/src/test/java/org/apache/maven/xml/sax/filter/ParentXMLFilterTest.java
+++ b/maven-xml/src/test/java/org/apache/maven/xml/sax/filter/ParentXMLFilterTest.java
@@ -38,15 +38,15 @@ public class ParentXMLFilterTest extends AbstractXMLFilterTests
     protected ParentXMLFilter getFilter( Consumer<LexicalHandler> lexicalHandlerConsumer )
         throws TransformerException, SAXException, ParserConfigurationException
     {
-        ParentXMLFilter filter = new ParentXMLFilter( x -> Optional.of( new RelativeProject( "GROUPID", 
+        ParentXMLFilter filter = new ParentXMLFilter( x -> Optional.of( new RelativeProject( "GROUPID",
                                                                                            "ARTIFACTID",
                                                                                            "1.0.0" ) ) );
         filter.setProjectPath( Paths.get( "pom.xml").toAbsolutePath() );
         lexicalHandlerConsumer.accept( filter );
-        
+
         return filter;
     }
-    
+
     @Test
     public void testMinimum() throws Exception
     {
@@ -89,6 +89,31 @@ public class ParentXMLFilterTest extends AbstractXMLFilterTests
         assertEquals( expected, actual );
     }
 
+    /**
+     * An empty relative path means it must downloaded from a repository.
+     * That implies that the version cannot be solved (if missing, Maven should complain)
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testEmptyRelativePathNoVersion() throws Exception
+    {
+        String input = "<parent>"
+            + "<groupId>GROUPID</groupId>"
+            + "<artifactId>ARTIFACTID</artifactId>"
+            + "<relativePath></relativePath>"
+            + "</parent>";
+        String expected = "<parent>"
+                        + "<groupId>GROUPID</groupId>"
+                        + "<artifactId>ARTIFACTID</artifactId>"
+                        + "<relativePath/>" // SAX optimization, however "" != null ...
+                        + "</parent>";
+
+        String actual = transform( input );
+
+        assertEquals( expected, actual );
+    }
+
     @Test
     public void testNoVersion() throws Exception
     {
@@ -114,7 +139,7 @@ public class ParentXMLFilterTest extends AbstractXMLFilterTests
     {
         ParentXMLFilter filter = new ParentXMLFilter( x -> Optional.ofNullable( null ) );
         filter.setProjectPath( Paths.get( "pom.xml").toAbsolutePath() );
-        
+
         String input = "<parent>"
             + "<groupId>GROUPID</groupId>"
             + "<artifactId>ARTIFACTID</artifactId>"
@@ -167,7 +192,7 @@ public class ParentXMLFilterTest extends AbstractXMLFilterTests
 
         assertEquals( expected, actual );
     }
-    
+
     @Test
     public void comment() throws Exception
     {
@@ -189,7 +214,7 @@ public class ParentXMLFilterTest extends AbstractXMLFilterTests
 
         assertEquals( expected, actual );
     }
-    
+
     @Test
     public void testIndent() throws Exception
     {