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

[maven] branch master updated: [MNG-6656] Introduce base for build/consumer pom

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

rfscholte pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven.git


The following commit(s) were added to refs/heads/master by this push:
     new bdec668  [MNG-6656] Introduce base for build/consumer pom
bdec668 is described below

commit bdec668de9c600165bb69c95b6ea0625d9f74fb0
Author: rfscholte <rf...@apache.org>
AuthorDate: Mon Jun 22 21:24:49 2020 +0200

    [MNG-6656] Introduce base for build/consumer pom
---
 maven-core/pom.xml                                 |  22 ++
 .../aether/ConsumerModelSourceTransformer.java     | 110 ++++++++
 .../DefaultRepositorySystemSessionFactory.java     |  56 +++-
 .../maven/project/DefaultProjectBuilder.java       |  77 ++++--
 .../apache/maven/project/ProjectModelResolver.java |  46 ++--
 .../org/apache/maven/project/ReactorModelPool.java | 120 +++++++--
 .../DefaultConsumerPomXMLFilterFactory.java        |  65 +++++
 .../apache/maven/project/ProjectBuilderTest.java   |   2 +-
 .../maven/repository/TestRepositorySystem.java     |   4 +-
 maven-model-builder/pom.xml                        |   6 +-
 .../java/org/apache/maven/feature/Features.java    |  63 +++++
 .../building/AbstractModelSourceTransformer.java   | 199 ++++++++++++++
 .../building/BuildModelSourceTransformer.java      |  84 ++++++
 .../building/DefaultBuildPomXMLFilterFactory.java  |  92 +++++++
 .../maven/model/building/DefaultModelBuilder.java  | 275 +++++++++++++++++---
 .../model/building/DefaultModelBuilderFactory.java |   9 +-
 .../building/DefaultModelBuildingRequest.java      |  30 ++-
 .../building/DefaultModelSourceTransformer.java    |  43 +++
 .../model/building/FilterModelBuildingRequest.java |  27 +-
 .../maven/model/building/ModelBuildingRequest.java |  20 ++
 .../apache/maven/model/building/ModelProblem.java  |   3 +-
 .../model/building/ModelSourceTransformer.java     |  35 +++
 .../maven/model/building/TransformerContext.java   |  64 +++++
 .../maven/model/building/TransformerException.java |  40 +++
 .../apache/maven/model/io/DefaultModelReader.java  |  43 ++-
 .../model/validation/DefaultModelValidator.java    |  39 +--
 .../maven/model/validation/ModelValidator.java     |  16 +-
 .../model/building/FileToRawModelMergerTest.java   |  82 ++++++
 .../DefaultInheritanceAssemblerTest.java           |  21 +-
 .../validation/DefaultModelValidatorTest.java      |  21 +-
 maven-xml/pom.xml                                  |  47 ++++
 .../main/java/org/apache/maven/xml/Factories.java  | 118 +++++++++
 .../java/org/apache/maven/xml/sax/SAXEvent.java    |  34 +++
 .../org/apache/maven/xml/sax/SAXEventFactory.java  | 144 ++++++++++
 .../org/apache/maven/xml/sax/SAXEventUtils.java    |  49 ++++
 .../maven/xml/sax/ext/CommentRenormalizer.java     | 108 ++++++++
 .../xml/sax/filter/AbstractEventXMLFilter.java     | 289 +++++++++++++++++++++
 .../maven/xml/sax/filter/AbstractSAXFilter.java    | 130 +++++++++
 .../maven/xml/sax/filter/BuildPomXMLFilter.java    |  58 +++++
 .../xml/sax/filter/BuildPomXMLFilterFactory.java   | 112 ++++++++
 .../xml/sax/filter/BuildPomXMLFilterListener.java  |  42 +++
 .../maven/xml/sax/filter/CiFriendlyXMLFilter.java  |  83 ++++++
 .../maven/xml/sax/filter/ConsumerPomXMLFilter.java |  54 ++++
 .../sax/filter/ConsumerPomXMLFilterFactory.java    |  89 +++++++
 .../apache/maven/xml/sax/filter/DependencyKey.java |  92 +++++++
 .../maven/xml/sax/filter/FastForwardFilter.java    | 128 +++++++++
 .../maven/xml/sax/filter/ModulesXMLFilter.java     | 111 ++++++++
 .../maven/xml/sax/filter/ParentXMLFilter.java      | 210 +++++++++++++++
 .../xml/sax/filter/ReactorDependencyXMLFilter.java | 165 ++++++++++++
 .../xml/sax/filter/RelativePathXMLFilter.java      | 108 ++++++++
 .../maven/xml/sax/filter/RelativeProject.java      |  56 ++++
 .../apache/maven/xml/sax/SAXEventUtilsTest.java    |  43 +++
 .../maven/xml/sax/ext/CommentRenormalizerTest.java |  84 ++++++
 .../xml/sax/filter/AbstractXMLFilterTests.java     | 119 +++++++++
 .../xml/sax/filter/ConsumerPomXMLFilterTest.java   | 235 +++++++++++++++++
 .../maven/xml/sax/filter/ModulesXMLFilterTest.java |  95 +++++++
 .../maven/xml/sax/filter/ParentXMLFilterTest.java  | 215 +++++++++++++++
 .../sax/filter/ReactorDependencyXMLFilterTest.java | 145 +++++++++++
 .../xml/sax/filter/RelativePathXMLFilterTest.java  | 115 ++++++++
 pom.xml                                            |  12 +
 60 files changed, 4849 insertions(+), 155 deletions(-)

diff --git a/maven-core/pom.xml b/maven-core/pom.xml
index 43a6bee..7128c35 100644
--- a/maven-core/pom.xml
+++ b/maven-core/pom.xml
@@ -62,6 +62,10 @@ under the License.
     </dependency>
     <dependency>
       <groupId>org.apache.maven</groupId>
+      <artifactId>maven-xml</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven</groupId>
       <artifactId>maven-plugin-api</artifactId>
     </dependency>
     <dependency>
@@ -142,6 +146,11 @@ under the License.
       <artifactId>hamcrest-library</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.xmlunit</groupId>
+      <artifactId>xmlunit-assertj</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <build>
@@ -218,6 +227,19 @@ under the License.
           </execution>
         </executions>
       </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-failsafe-plugin</artifactId>
+        <executions>
+          <execution>
+            <!--  <phase></phase> -->
+            <goals>
+              <goal>integration-test</goal>
+              <goal>verify</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
     </plugins>
   </build>
 </project>
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
new file mode 100644
index 0000000..728c78e
--- /dev/null
+++ b/maven-core/src/main/java/org/apache/maven/internal/aether/ConsumerModelSourceTransformer.java
@@ -0,0 +1,110 @@
+package org.apache.maven.internal.aether;
+
+/*
+ * 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.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.sax.SAXTransformerFactory;
+import javax.xml.transform.sax.TransformerHandler;
+
+import org.apache.maven.model.building.AbstractModelSourceTransformer;
+import org.apache.maven.model.building.DefaultBuildPomXMLFilterFactory;
+import org.apache.maven.model.building.TransformerContext;
+import org.apache.maven.xml.Factories;
+import org.apache.maven.xml.internal.DefaultConsumerPomXMLFilterFactory;
+import org.apache.maven.xml.sax.filter.AbstractSAXFilter;
+import org.xml.sax.SAXException;
+
+class ConsumerModelSourceTransformer extends AbstractModelSourceTransformer
+{
+    @Override
+    protected AbstractSAXFilter getSAXFilter( Path pomFile, TransformerContext context )
+        throws TransformerConfigurationException, SAXException, ParserConfigurationException
+    {
+        return new DefaultConsumerPomXMLFilterFactory( new DefaultBuildPomXMLFilterFactory( context ) ).get( pomFile );
+    }
+    
+    /**
+     * This transformer will ensure that encoding and version are kept.
+     * However, it cannot prevent:
+     * <ul>
+     *   <li>attributes will be on one line</li>
+     *   <li>Unnecessary whitespace before the rootelement will be removed</li> 
+     * </ul>
+     */
+    @Override
+    protected TransformerHandler getTransformerHandler( Path pomFile )
+        throws IOException, org.apache.maven.model.building.TransformerException
+    {
+        final TransformerHandler transformerHandler;
+        
+        final SAXTransformerFactory transformerFactory =
+                        (SAXTransformerFactory) Factories.newTransformerFactory();
+        
+        // Keep same encoding+version
+        try ( InputStream input = Files.newInputStream( pomFile ) )
+        {
+            XMLStreamReader streamReader =
+                XMLInputFactory.newFactory().createXMLStreamReader( input );
+
+            transformerHandler = transformerFactory.newTransformerHandler();
+
+            final String encoding = streamReader.getCharacterEncodingScheme();
+            final String version = streamReader.getVersion();
+            
+            Transformer transformer = transformerHandler.getTransformer();
+            transformer.setOutputProperty( OutputKeys.METHOD, "xml" );
+            if ( encoding == null && version == null )
+            {
+                transformer.setOutputProperty( OutputKeys.OMIT_XML_DECLARATION, "yes" );
+            }
+            else
+            {
+                transformer.setOutputProperty( OutputKeys.OMIT_XML_DECLARATION, "no" );
+
+                if ( encoding != null )
+                {
+                    transformer.setOutputProperty( OutputKeys.ENCODING, encoding );
+                }
+                if ( version != null )
+                {
+                    transformer.setOutputProperty( OutputKeys.VERSION, version );
+                }
+            }
+        }
+        catch ( XMLStreamException | TransformerConfigurationException e )
+        {
+            throw new org.apache.maven.model.building.TransformerException( 
+                               "Failed to detect XML encoding and version", e );
+        }
+        return transformerHandler;
+    }
+
+}
diff --git a/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java b/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java
index 248a3b6..28f75cd 100644
--- a/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java
+++ b/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java
@@ -24,6 +24,9 @@ import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
 import org.apache.maven.bridge.MavenRepositorySystem;
 import org.apache.maven.eventspy.internal.EventSpyDispatcher;
 import org.apache.maven.execution.MavenExecutionRequest;
+import org.apache.maven.feature.Features;
+import org.apache.maven.model.building.TransformerContext;
+import org.apache.maven.model.building.TransformerException;
 import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
 import org.apache.maven.settings.Mirror;
 import org.apache.maven.settings.Proxy;
@@ -38,12 +41,16 @@ import org.codehaus.plexus.util.xml.Xpp3Dom;
 import org.eclipse.aether.ConfigurationProperties;
 import org.eclipse.aether.DefaultRepositorySystemSession;
 import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.SessionData;
+import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.repository.LocalRepository;
 import org.eclipse.aether.repository.NoLocalRepositoryManagerException;
 import org.eclipse.aether.repository.RepositoryPolicy;
 import org.eclipse.aether.repository.WorkspaceReader;
 import org.eclipse.aether.resolution.ResolutionErrorPolicy;
 import org.eclipse.aether.spi.localrepo.LocalRepositoryManagerFactory;
+import org.eclipse.aether.transform.FileTransformer;
+import org.eclipse.aether.transform.TransformException;
 import org.eclipse.aether.util.repository.AuthenticationBuilder;
 import org.eclipse.aether.util.repository.DefaultAuthenticationSelector;
 import org.eclipse.aether.util.repository.DefaultMirrorSelector;
@@ -53,8 +60,13 @@ import org.eclipse.sisu.Nullable;
 
 import javax.inject.Inject;
 import javax.inject.Named;
+
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Properties;
@@ -96,7 +108,6 @@ public class DefaultRepositorySystemSessionFactory
     public DefaultRepositorySystemSession newRepositorySession( MavenExecutionRequest request )
     {
         DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession();
-
         session.setCache( request.getRepositoryCache() );
 
         Map<Object, Object> configProps = new LinkedHashMap<>();
@@ -139,7 +150,6 @@ public class DefaultRepositorySystemSessionFactory
                 session.setLocalRepositoryManager( simpleLocalRepoMgrFactory.newInstance( session, localRepo ) );
                 logger.info( "Disabling enhanced local repository: using legacy is strongly discouraged to ensure"
                                  + " build reproducibility." );
-
             }
             catch ( NoLocalRepositoryManagerException e )
             {
@@ -238,6 +248,11 @@ public class DefaultRepositorySystemSessionFactory
         mavenRepositorySystem.injectProxy( session, request.getPluginArtifactRepositories() );
         mavenRepositorySystem.injectAuthentication( session, request.getPluginArtifactRepositories() );
 
+        if ( Features.buildConsumer().isActive() )
+        {
+            session.setFileTransformerManager( a -> getTransformersForArtifact( a, session.getData() ) );
+        }
+
         return session;
     }
 
@@ -266,5 +281,40 @@ public class DefaultRepositorySystemSessionFactory
 
         return props.getProperty( "version", "unknown-version" );
     }
+    
+    private Collection<FileTransformer> getTransformersForArtifact( final Artifact artifact,
+                                                                    final SessionData sessionData )
+    {
+        TransformerContext context = (TransformerContext) sessionData.get( TransformerContext.KEY );
+        Collection<FileTransformer> transformers = new ArrayList<>();
+        
+        // In case of install:install-file there's no transformer context, as the goal is unrelated to the lifecycle. 
+        if ( "pom".equals( artifact.getExtension() ) && context != null )
+        {
+            transformers.add( new FileTransformer()
+            {
+                @Override
+                public InputStream transformData( File pomFile )
+                    throws IOException, TransformException
+                {
+                    try
+                    {
+                        return new ConsumerModelSourceTransformer().transform( pomFile.toPath(), context );
+                    }
+                    catch ( TransformerException e )
+                    {
+                        throw new TransformException( e );
+                    }
+                }
+                
+                @Override
+                public Artifact transformArtifact( Artifact artifact )
+                {
+                    return artifact;
+                }
+            } );
+        }
+        return Collections.unmodifiableCollection( transformers );
+    }
 
-}
+}
\ No newline at end of file
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 1ebc1fc..624f6ad 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,6 +21,7 @@ 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;
@@ -44,6 +45,7 @@ import org.apache.maven.artifact.InvalidRepositoryException;
 import org.apache.maven.artifact.repository.ArtifactRepository;
 import org.apache.maven.artifact.repository.LegacyLocalRepositoryManager;
 import org.apache.maven.bridge.MavenRepositorySystem;
+import org.apache.maven.feature.Features;
 import org.apache.maven.model.Build;
 import org.apache.maven.model.Dependency;
 import org.apache.maven.model.DependencyManagement;
@@ -53,6 +55,7 @@ import org.apache.maven.model.Model;
 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.DefaultModelBuildingRequest;
 import org.apache.maven.model.building.DefaultModelProblem;
 import org.apache.maven.model.building.FileModelSource;
@@ -64,6 +67,7 @@ import org.apache.maven.model.building.ModelProblem;
 import org.apache.maven.model.building.ModelProcessor;
 import org.apache.maven.model.building.ModelSource;
 import org.apache.maven.model.building.StringModelSource;
+import org.apache.maven.model.building.TransformerContext;
 import org.apache.maven.model.resolution.ModelResolver;
 import org.apache.maven.repository.internal.ArtifactDescriptorUtils;
 import org.codehaus.plexus.logging.Logger;
@@ -291,6 +295,7 @@ public class DefaultProjectBuilder
         request.setBuildStartTime( configuration.getBuildStartTime() );
         request.setModelResolver( resolver );
         request.setModelCache( config.modelCache );
+        request.setTransformerContext( (TransformerContext) config.session.getData().get( TransformerContext.KEY ) );
 
         return request;
     }
@@ -342,7 +347,16 @@ public class DefaultProjectBuilder
             artifact.setResolved( true );
         }
 
-        return build( localProject ? pomFile : null, new FileModelSource( pomFile ), config );
+        if ( localProject )
+        {
+            return build( pomFile, new FileModelSource( pomFile ), config );
+        }
+        else
+        {
+            return build( null, new ArtifactModelSource( pomFile, artifact.getGroupId(), artifact.getArtifactId(),
+                                                         artifact.getVersion() ),
+                          config );
+        }
     }
 
     private ModelSource createStubModelSource( Artifact artifact )
@@ -369,7 +383,33 @@ public class DefaultProjectBuilder
 
         List<InterimResult> interimResults = new ArrayList<>();
 
-        ReactorModelPool modelPool = new ReactorModelPool();
+        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() );
@@ -378,9 +418,7 @@ public class DefaultProjectBuilder
 
         boolean noErrors =
             build( results, interimResults, projectIndex, pomFiles, new LinkedHashSet<>(), true, recursive,
-                   config );
-
-        populateReactorModelPool( modelPool, interimResults );
+                   config, poolBuilder );
 
         ClassLoader oldContextClassLoader = Thread.currentThread().getContextClassLoader();
 
@@ -406,7 +444,8 @@ 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,
-                           boolean isRoot, boolean recursive, InternalConfig config )
+                           boolean root, boolean recursive, InternalConfig config,
+                           ReactorModelPool.Builder poolBuilder )
     {
         boolean noErrors = true;
 
@@ -414,7 +453,8 @@ public class DefaultProjectBuilder
         {
             aggregatorFiles.add( pomFile );
 
-            if ( !build( results, interimResults, projectIndex, pomFile, aggregatorFiles, isRoot, recursive, config ) )
+            if ( !build( results, interimResults, projectIndex, pomFile, aggregatorFiles, root, recursive, config,
+                         poolBuilder ) )
             {
                 noErrors = false;
             }
@@ -428,7 +468,8 @@ public class DefaultProjectBuilder
     @SuppressWarnings( "checkstyle:parameternumber" )
     private boolean build( List<ProjectBuildingResult> results, List<InterimResult> interimResults,
                            Map<String, MavenProject> projectIndex, File pomFile, Set<File> aggregatorFiles,
-                           boolean isRoot, boolean recursive, InternalConfig config )
+                           boolean isRoot, boolean recursive, InternalConfig config,
+                           ReactorModelPool.Builder poolBuilder )
     {
         boolean noErrors = true;
 
@@ -465,6 +506,9 @@ public class DefaultProjectBuilder
         }
 
         Model model = result.getEffectiveModel();
+        
+        poolBuilder.put( model.getPomFile().toPath(),  result.getRawModel() );
+        
         try
         {
             // first pass: build without building parent.
@@ -559,7 +603,7 @@ public class DefaultProjectBuilder
             interimResult.modules = new ArrayList<>();
 
             if ( !build( results, interimResult.modules, projectIndex, moduleFiles, aggregatorFiles, false,
-                         recursive, config ) )
+                         recursive, config, poolBuilder ) )
             {
                 noErrors = false;
             }
@@ -595,17 +639,6 @@ public class DefaultProjectBuilder
 
     }
 
-    private void populateReactorModelPool( ReactorModelPool reactorModelPool, List<InterimResult> interimResults )
-    {
-        for ( InterimResult interimResult : interimResults )
-        {
-            Model model = interimResult.result.getEffectiveModel();
-            reactorModelPool.put( model.getGroupId(), model.getArtifactId(), model.getVersion(), model.getPomFile() );
-
-            populateReactorModelPool( reactorModelPool, interimResult.modules );
-        }
-    }
-
     private boolean build( List<ProjectBuildingResult> results, List<MavenProject> projects,
                            Map<String, MavenProject> projectIndex, List<InterimResult> interimResults,
                            ProjectBuildingRequest request, Map<File, Boolean> profilesXmls,
@@ -865,7 +898,7 @@ public class DefaultProjectBuilder
                 DeploymentRepository r = project.getDistributionManagement().getRepository();
                 if ( !StringUtils.isEmpty( r.getId() ) && !StringUtils.isEmpty( r.getUrl() ) )
                 {
-                    ArtifactRepository repo = repositorySystem.buildArtifactRepository( r );
+                    ArtifactRepository repo = MavenRepositorySystem.buildArtifactRepository( r );
                     repositorySystem.injectProxy( projectBuildingRequest.getRepositorySession(),
                                                   Arrays.asList( repo ) );
                     repositorySystem.injectAuthentication( projectBuildingRequest.getRepositorySession(),
@@ -889,7 +922,7 @@ public class DefaultProjectBuilder
                 DeploymentRepository r = project.getDistributionManagement().getSnapshotRepository();
                 if ( !StringUtils.isEmpty( r.getId() ) && !StringUtils.isEmpty( r.getUrl() ) )
                 {
-                    ArtifactRepository repo = repositorySystem.buildArtifactRepository( r );
+                    ArtifactRepository repo = MavenRepositorySystem.buildArtifactRepository( r );
                     repositorySystem.injectProxy( projectBuildingRequest.getRepositorySession(),
                                                   Arrays.asList( repo ) );
                     repositorySystem.injectAuthentication( projectBuildingRequest.getRepositorySession(),
diff --git a/maven-core/src/main/java/org/apache/maven/project/ProjectModelResolver.java b/maven-core/src/main/java/org/apache/maven/project/ProjectModelResolver.java
index 24b36dd..bcc3730 100644
--- a/maven-core/src/main/java/org/apache/maven/project/ProjectModelResolver.java
+++ b/maven-core/src/main/java/org/apache/maven/project/ProjectModelResolver.java
@@ -19,7 +19,6 @@ package org.apache.maven.project;
  * under the License.
  */
 
-import java.io.File;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
@@ -28,8 +27,10 @@ import java.util.List;
 import java.util.Set;
 
 import org.apache.maven.model.Dependency;
+import org.apache.maven.model.Model;
 import org.apache.maven.model.Parent;
 import org.apache.maven.model.Repository;
+import org.apache.maven.model.building.ArtifactModelSource;
 import org.apache.maven.model.building.FileModelSource;
 import org.apache.maven.model.building.ModelSource;
 import org.apache.maven.model.resolution.InvalidRepositoryException;
@@ -155,10 +156,10 @@ public class ProjectModelResolver
 
     private static void removeMatchingRepository( Iterable<RemoteRepository> repositories, final String id )
     {
-        Iterator iterator = repositories.iterator( );
+        Iterator<RemoteRepository> iterator = repositories.iterator( );
         while ( iterator.hasNext() )
         {
-            RemoteRepository next =  ( RemoteRepository ) iterator.next();
+            RemoteRepository next = iterator.next();
             if ( next.getId().equals( id ) )
             {
                 iterator.remove();
@@ -174,32 +175,20 @@ public class ProjectModelResolver
     public ModelSource resolveModel( String groupId, String artifactId, String version )
         throws UnresolvableModelException
     {
-        File pomFile = null;
+        Artifact pomArtifact = new DefaultArtifact( groupId, artifactId, "", "pom", version );
 
-        if ( modelPool != null )
+        try
         {
-            pomFile = modelPool.get( groupId, artifactId, version );
+            ArtifactRequest request = new ArtifactRequest( pomArtifact, repositories, context );
+            request.setTrace( trace );
+            pomArtifact = resolver.resolveArtifact( session, request ).getArtifact();
         }
-
-        if ( pomFile == null )
+        catch ( ArtifactResolutionException e )
         {
-            Artifact pomArtifact = new DefaultArtifact( groupId, artifactId, "", "pom", version );
-
-            try
-            {
-                ArtifactRequest request = new ArtifactRequest( pomArtifact, repositories, context );
-                request.setTrace( trace );
-                pomArtifact = resolver.resolveArtifact( session, request ).getArtifact();
-            }
-            catch ( ArtifactResolutionException e )
-            {
-                throw new UnresolvableModelException( e.getMessage(), groupId, artifactId, version, e );
-            }
-
-            pomFile = pomArtifact.getFile();
+            throw new UnresolvableModelException( e.getMessage(), groupId, artifactId, version, e );
         }
 
-        return new FileModelSource( pomFile );
+        return new ArtifactModelSource( pomArtifact.getFile(), groupId, artifactId, version );
     }
 
     @Override
@@ -285,6 +274,17 @@ public class ProjectModelResolver
             }
 
             dependency.setVersion( versionRangeResult.getHighestVersion().toString() );
+            
+            if ( modelPool != null )
+            {
+                Model model =
+                    modelPool.get( dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion() );
+
+                if ( model != null )
+                {
+                    return new FileModelSource( model.getPomFile() );
+                }
+            }
 
             return resolveModel( dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion() );
         }
diff --git a/maven-core/src/main/java/org/apache/maven/project/ReactorModelPool.java b/maven-core/src/main/java/org/apache/maven/project/ReactorModelPool.java
index 64b30dd..b96b14b 100644
--- a/maven-core/src/main/java/org/apache/maven/project/ReactorModelPool.java
+++ b/maven-core/src/main/java/org/apache/maven/project/ReactorModelPool.java
@@ -19,53 +19,124 @@ package org.apache.maven.project;
  * under the License.
  */
 
-import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import org.apache.maven.model.Model;
 
 /**
- * Holds all POM files that are known to the reactor. This allows the project builder to resolve imported POMs from the
+ * Holds all Models that are known to the reactor. This allows the project builder to resolve imported Models from the
  * reactor when building another project's effective model.
  *
  * @author Benjamin Bentmann
+ * @author Robert Scholte
  */
 class ReactorModelPool
 {
+    private final Map<GAKey, Set<Model>> modelsByGa = new HashMap<>();
+
+    private final Map<Path, Model> modelsByPath = new HashMap<>();
+
+    /**
+     * Get the model by its GAV or (since 3.7.0) by its GA if there is only one.
+     *  
+     * @param groupId, never {@code null}
+     * @param artifactId, never {@code null}
+     * @param version, can be {@code null}
+     * @return the matching model or {@code null}
+     * @throws IllegalStateException if version was null and multiple modules share the same groupId + artifactId
+     */
+    public Model get( String groupId, String artifactId, String version )
+    {
+        return modelsByGa.getOrDefault( new GAKey( groupId, artifactId ), Collections.emptySet() ).stream()
+                        .filter( m -> version == null || version.equals( getVersion( m ) ) )
+                        .reduce( ( a, b ) -> 
+                        {
+                            throw new IllegalStateException( "Multiple modules with key "
+                                + a.getGroupId() + ':' + a.getArtifactId() );
+                        } ).orElse( null );
+    }
 
-    private final Map<CacheKey, File> pomFiles = new HashMap<>();
-
-    public File get( String groupId, String artifactId, String version )
+    /**
+     * Find model by path, useful when location the parent by relativePath
+     * 
+     * @param path
+     * @return the matching model or {@code null}
+     * @since 3.7.0
+     */
+    public Model get( Path path )
     {
-        return pomFiles.get( new CacheKey( groupId, artifactId, version ) );
+        final Path pomFile;
+        if ( Files.isDirectory( path ) )
+        {
+            pomFile = path.resolve( "pom.xml" );
+        }
+        else
+        {
+            pomFile = path;
+        }
+        return modelsByPath.get( pomFile );
+    }
+    
+    private String getVersion( Model model )
+    {
+        String version = model.getVersion();
+        if ( version == null && model.getParent() != null )
+        {
+            version = model.getParent().getVersion();
+        }
+        return version;
     }
 
-    public void put( String groupId, String artifactId, String version, File pomFile )
+    static class Builder
     {
-        pomFiles.put( new CacheKey( groupId, artifactId, version ), pomFile );
+        private ReactorModelPool pool = new ReactorModelPool();
+        
+        Builder put( Path pomFile, Model model )
+        {
+            pool.modelsByPath.put( pomFile, model );
+            pool.modelsByGa.computeIfAbsent( new GAKey( getGroupId( model ), model.getArtifactId() ),
+                                             k -> new HashSet<Model>() ).add( model );
+            return this;
+        }
+        
+        ReactorModelPool build() 
+        {
+            return pool;
+        }
+
+        private static String getGroupId( Model model )
+        {
+            String groupId = model.getGroupId();
+            if ( groupId == null && model.getParent() != null )
+            {
+                groupId = model.getParent().getGroupId();
+            }
+            return groupId;
+        }
     }
 
-    private static final class CacheKey
+    private static final class GAKey
     {
 
         private final String groupId;
 
         private final String artifactId;
 
-        private final String version;
-
         private final int hashCode;
 
-        CacheKey( String groupId, String artifactId, String version )
+        GAKey( String groupId, String artifactId )
         {
             this.groupId = ( groupId != null ) ? groupId : "";
             this.artifactId = ( artifactId != null ) ? artifactId : "";
-            this.version = ( version != null ) ? version : "";
 
-            int hash = 17;
-            hash = hash * 31 + this.groupId.hashCode();
-            hash = hash * 31 + this.artifactId.hashCode();
-            hash = hash * 31 + this.version.hashCode();
-            hashCode = hash;
+            hashCode = Objects.hash( this.groupId, this.artifactId );
         }
 
         @Override
@@ -76,15 +147,9 @@ class ReactorModelPool
                 return true;
             }
 
-            if ( !( obj instanceof CacheKey ) )
-            {
-                return false;
-            }
-
-            CacheKey that = (CacheKey) obj;
+            GAKey that = (GAKey) obj;
 
-            return artifactId.equals( that.artifactId ) && groupId.equals( that.groupId )
-                && version.equals( that.version );
+            return artifactId.equals( that.artifactId ) && groupId.equals( that.groupId );
         }
 
         @Override
@@ -97,10 +162,9 @@ class ReactorModelPool
         public String toString()
         {
             StringBuilder buffer = new StringBuilder( 128 );
-            buffer.append( groupId ).append( ':' ).append( artifactId ).append( ':' ).append( version );
+            buffer.append( groupId ).append( ':' ).append( artifactId );
             return buffer.toString();
         }
-
     }
 
 }
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
new file mode 100644
index 0000000..f49e62d
--- /dev/null
+++ b/maven-core/src/main/java/org/apache/maven/xml/internal/DefaultConsumerPomXMLFilterFactory.java
@@ -0,0 +1,65 @@
+package org.apache.maven.xml.internal;
+
+/*
+ * 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.util.Optional;
+
+import org.apache.maven.model.building.DefaultBuildPomXMLFilterFactory;
+import org.apache.maven.model.building.TransformerContext;
+import org.apache.maven.xml.sax.filter.ConsumerPomXMLFilterFactory;
+
+/**
+ * The default implementation of the {@link ConsumerPomXMLFilterFactory}
+ * It will provide several values for the consumer pom based on its context.
+ * 
+ * @author Robert Scholte
+ * @since 3.7.0
+ */
+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-core/src/test/java/org/apache/maven/project/ProjectBuilderTest.java b/maven-core/src/test/java/org/apache/maven/project/ProjectBuilderTest.java
index da43088..538f887 100644
--- a/maven-core/src/test/java/org/apache/maven/project/ProjectBuilderTest.java
+++ b/maven-core/src/test/java/org/apache/maven/project/ProjectBuilderTest.java
@@ -159,7 +159,7 @@ public class ProjectBuilderTest
             File parent = new File( tempDir.toFile(), "pom.xml" );
             String parentContent = FileUtils.fileRead( parent );
             parentContent = parentContent.replaceAll( "<packaging>pom</packaging>",
-            		"<packaging>pom</packaging><properties><addedProperty>addedValue</addedProperty></properties>" );
+                     "<packaging>pom</packaging><properties><addedProperty>addedValue</addedProperty></properties>" );
             FileUtils.fileWrite( parent, "UTF-8", parentContent );
             // re-build pom with modified parent
             ProjectBuildingResult result = projectBuilder.build( child, configuration );
diff --git a/maven-core/src/test/java/org/apache/maven/repository/TestRepositorySystem.java b/maven-core/src/test/java/org/apache/maven/repository/TestRepositorySystem.java
index a3c1c0d..c95f428 100644
--- a/maven-core/src/test/java/org/apache/maven/repository/TestRepositorySystem.java
+++ b/maven-core/src/test/java/org/apache/maven/repository/TestRepositorySystem.java
@@ -111,14 +111,14 @@ public class TestRepositorySystem
     public ArtifactRepository createDefaultLocalRepository()
         throws InvalidRepositoryException
     {
-        return createLocalRepository( new File( System.getProperty( "basedir", "" ), "target/local-repo" ).getAbsoluteFile() );
+        return createLocalRepository( new File( System.getProperty( "basedir", "." ), "target/local-repo" ).getAbsoluteFile() );
     }
 
     public ArtifactRepository createDefaultRemoteRepository()
         throws InvalidRepositoryException
     {
         return new MavenArtifactRepository( DEFAULT_REMOTE_REPO_ID, "file://"
-            + new File( System.getProperty( "basedir", "" ), "src/test/remote-repo" ).toURI().getPath(),
+            + new File( System.getProperty( "basedir", "." ), "src/test/remote-repo" ).getAbsoluteFile().toURI().getPath(),
                                             new DefaultRepositoryLayout(), new ArtifactRepositoryPolicy(),
                                             new ArtifactRepositoryPolicy() );
     }
diff --git a/maven-model-builder/pom.xml b/maven-model-builder/pom.xml
index 81d2587..583d6cb 100644
--- a/maven-model-builder/pom.xml
+++ b/maven-model-builder/pom.xml
@@ -59,6 +59,11 @@ under the License.
       <artifactId>maven-builder-support</artifactId>
     </dependency>
     <dependency>
+      <groupId>org.apache.maven</groupId>
+      <artifactId>maven-xml</artifactId>
+    </dependency>
+    <!-- Testing -->
+    <dependency>
       <groupId>org.eclipse.sisu</groupId>
       <artifactId>org.eclipse.sisu.inject</artifactId>
     </dependency>
@@ -67,7 +72,6 @@ under the License.
       <artifactId>org.eclipse.sisu.plexus</artifactId>
       <scope>test</scope>
     </dependency>
-    <!-- Testing -->
     <dependency>
       <groupId>com.google.inject</groupId>
       <artifactId>guice</artifactId>
diff --git a/maven-model-builder/src/main/java/org/apache/maven/feature/Features.java b/maven-model-builder/src/main/java/org/apache/maven/feature/Features.java
new file mode 100644
index 0000000..48848c9
--- /dev/null
+++ b/maven-model-builder/src/main/java/org/apache/maven/feature/Features.java
@@ -0,0 +1,63 @@
+package org.apache.maven.feature;
+
+/*
+ * 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.
+ */
+
+/**
+ * Centralized class for feature information
+ * 
+ * @author Robert Scholte
+ * @since 3.7.0
+ */
+public final class Features
+{
+    private Features() 
+    {
+    }
+    
+    private static final Feature BUILDCONSUMER = new Feature( "maven.experimental.buildconsumer", "true" );
+    
+    public static Feature buildConsumer()
+    {
+        return BUILDCONSUMER;
+    }
+    
+    /**
+     * Represents some feature
+     * 
+     * @author Robert Scholte
+     * @since 3.7.0
+     */
+    public static class Feature
+    {
+        private final boolean active;
+
+        Feature( String name, String defaultValue )
+        {
+            active = "true".equals( System.getProperty( name, defaultValue ) );
+        }
+        
+        public boolean isActive()
+        {
+           return active; 
+        }
+        
+    }
+    
+}
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/AbstractModelSourceTransformer.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/AbstractModelSourceTransformer.java
new file mode 100644
index 0000000..31b88bf
--- /dev/null
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/AbstractModelSourceTransformer.java
@@ -0,0 +1,199 @@
+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.io.FileInputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.nio.file.Path;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.sax.SAXResult;
+import javax.xml.transform.sax.SAXSource;
+import javax.xml.transform.sax.TransformerHandler;
+import javax.xml.transform.stream.StreamResult;
+
+import org.apache.maven.xml.Factories;
+import org.apache.maven.xml.sax.ext.CommentRenormalizer;
+import org.apache.maven.xml.sax.filter.AbstractSAXFilter;
+import org.xml.sax.SAXException;
+
+/**
+ * Offers a transformation implementation based on PipelineStreams.
+ * Subclasses are responsible for providing the right SAXFilter.
+ * 
+ * @author Robert Scholte
+ * @since 3.7.0
+ */
+public abstract class AbstractModelSourceTransformer
+    implements ModelSourceTransformer
+{
+    private static final AtomicInteger TRANSFORM_THREAD_COUNTER = new AtomicInteger();
+    
+    private final TransformerFactory transformerFactory = Factories.newTransformerFactory();
+                    
+    protected abstract AbstractSAXFilter getSAXFilter( Path pomFile, TransformerContext context )
+        throws TransformerConfigurationException, SAXException, ParserConfigurationException;
+
+    protected OutputStream filterOutputStream( OutputStream outputStream, Path pomFile )
+    {
+        return outputStream;
+    }
+    
+    protected TransformerHandler getTransformerHandler( Path pomFile )
+        throws IOException, org.apache.maven.model.building.TransformerException
+    {
+        return null;
+    }
+
+    @Override
+    public final InputStream transform( Path pomFile, TransformerContext context )
+        throws IOException, org.apache.maven.model.building.TransformerException
+    {
+        final TransformerHandler transformerHandler = getTransformerHandler( pomFile );
+
+        final AbstractSAXFilter filter;
+        try
+        {
+            filter = getSAXFilter( pomFile, context );
+            filter.setLexicalHandler( transformerHandler );
+        }
+        catch ( TransformerConfigurationException | SAXException | ParserConfigurationException e )
+        {
+            throw new org.apache.maven.model.building.TransformerException( e );
+        }
+        
+        final SAXSource transformSource =
+            new SAXSource( filter,
+                           new org.xml.sax.InputSource( new FileInputStream( pomFile.toFile() ) ) );
+
+        final PipedOutputStream pout = new PipedOutputStream();
+        final PipedInputStream pipedInputStream = new PipedInputStream( pout );
+        
+        OutputStream out = filterOutputStream( pout, pomFile );
+
+        final javax.xml.transform.Result result; 
+        if ( transformerHandler == null )
+        {
+            result = new StreamResult( out );
+        }
+        else
+        {
+            result = new SAXResult( transformerHandler );
+            ( (SAXResult) result ).setLexicalHandler( new CommentRenormalizer( transformerHandler ) );
+            transformerHandler.setResult( new StreamResult( out ) );
+        }
+
+        IOExceptionHandler eh = new IOExceptionHandler();
+
+        Thread transformThread = new Thread( () -> 
+        {
+            try ( PipedOutputStream pos = pout )
+            {
+                transformerFactory.newTransformer().transform( transformSource, result );
+            }
+            catch ( TransformerException | IOException e )
+            {
+                eh.uncaughtException( Thread.currentThread(), e );
+            }
+        }, "TransformThread-" + TRANSFORM_THREAD_COUNTER.incrementAndGet() );
+        transformThread.setUncaughtExceptionHandler( eh );
+        transformThread.setDaemon( true );
+        transformThread.start();
+
+        return new ThreadAwareInputStream( pipedInputStream, eh );
+    }
+
+    private static class IOExceptionHandler
+        implements Thread.UncaughtExceptionHandler, AutoCloseable
+    {
+        private volatile Throwable cause;
+
+        @Override
+        public void uncaughtException( Thread t, Throwable e )
+        {
+            try
+            {
+                throw e;
+            }
+            catch ( TransformerException | IOException | RuntimeException | Error allGood )
+            {
+                // all good
+                this.cause = e;
+            }
+            catch ( Throwable notGood )
+            {
+                throw new AssertionError( "Unexpected Exception", e );
+            }
+        }
+
+        @Override
+        public void close()
+            throws IOException
+        {
+            if ( cause != null )
+            {
+                try
+                {
+                    throw cause;
+                }
+                catch ( IOException | RuntimeException | Error e )
+                {
+                    throw e;
+                }
+                catch ( Throwable t )
+                {
+                    // Any checked exception
+                    throw new RuntimeException( "Failed to transform pom", t );
+                }
+            }
+        }
+    }
+
+    private class ThreadAwareInputStream
+        extends FilterInputStream
+    {
+        final IOExceptionHandler h;
+
+        protected ThreadAwareInputStream( InputStream in, IOExceptionHandler h )
+        {
+            super( in );
+            this.h = h;
+        }
+
+        @Override
+        public void close()
+            throws IOException
+        {
+            try ( IOExceptionHandler eh = h )
+            {
+                super.close();
+            }
+        }
+    }
+}
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
new file mode 100644
index 0000000..dbf9211
--- /dev/null
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/BuildModelSourceTransformer.java
@@ -0,0 +1,84 @@
+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.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Path;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.TransformerConfigurationException;
+
+import org.apache.maven.xml.sax.filter.AbstractSAXFilter;
+import org.apache.maven.xml.sax.filter.BuildPomXMLFilterFactory;
+import org.apache.maven.xml.sax.filter.BuildPomXMLFilterListener;
+import org.eclipse.sisu.Nullable;
+import org.xml.sax.SAXException;
+
+/**
+ * ModelSourceTransformer for the build pom
+ * 
+ * @author Robert Scholte
+ * @since 3.7.0
+ */
+@Named
+@Singleton
+class BuildModelSourceTransformer extends AbstractModelSourceTransformer
+{
+    @Inject
+    @Nullable
+    private BuildPomXMLFilterListener xmlFilterListener;
+    
+    protected AbstractSAXFilter getSAXFilter( Path pomFile, TransformerContext context )
+        throws TransformerConfigurationException, SAXException, ParserConfigurationException
+    {
+        BuildPomXMLFilterFactory buildPomXMLFilterFactory = new DefaultBuildPomXMLFilterFactory( context );
+        
+        return buildPomXMLFilterFactory.get( pomFile );
+    }
+    
+    @Override
+    protected OutputStream filterOutputStream( OutputStream outputStream, Path pomFile )
+    {
+        OutputStream out;
+        if ( xmlFilterListener != null )
+        {
+            out = new FilterOutputStream( outputStream )
+            {
+                @Override
+                public void write( byte[] b, int off, int len )
+                    throws IOException
+                {
+                    super.write( b, off, len );
+                    xmlFilterListener.write( pomFile, b, off, len );
+                }  
+            };
+        }
+        else
+        {
+            out = outputStream;
+        }
+        return out;
+    }
+}
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
new file mode 100644
index 0000000..3de90de
--- /dev/null
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultBuildPomXMLFilterFactory.java
@@ -0,0 +1,92 @@
+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.Optional;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import org.apache.maven.model.Model;
+import org.apache.maven.xml.sax.filter.BuildPomXMLFilterFactory;
+import org.apache.maven.xml.sax.filter.RelativeProject;
+
+/**
+ * 
+ * @author Robert Scholte
+ * @since 3.7.0
+ */
+public class DefaultBuildPomXMLFilterFactory extends BuildPomXMLFilterFactory
+{
+    private final TransformerContext context;
+    
+    public DefaultBuildPomXMLFilterFactory( TransformerContext context )
+    {
+        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()
+    {
+        return (g, a) -> Optional.ofNullable( context.getRawModel( g, a ) )
+                            .map( m -> toVersion( m ) )
+                            .orElse( null );
+    }
+
+    private static RelativeProject toRelativeProject( final Model m )
+    {
+        String groupId = m.getGroupId();
+        if ( groupId == null && m.getParent() != null )
+        {
+            groupId = m.getParent().getGroupId();
+        }
+
+        String version = m.getVersion();
+        if ( version == null && m.getParent() != null )
+        {
+            version = m.getParent().getVersion();
+        }
+
+        return new RelativeProject( groupId, m.getArtifactId(), version );
+    }
+    
+    private static String toVersion( final Model m )
+    {
+        String version = m.getVersion();
+        if ( version == null && m.getParent() != null )
+        {
+            version = m.getParent().getVersion();
+        }
+
+        return version;
+    }
+}
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 b72550b..77a117d 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
@@ -19,22 +19,49 @@ package org.apache.maven.model.building;
  * under the License.
  */
 
+import static org.apache.maven.model.building.Result.error;
+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.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+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 javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
 import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
 import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
 import org.apache.maven.artifact.versioning.VersionRange;
 import org.apache.maven.building.Source;
+import org.apache.maven.feature.Features;
 import org.apache.maven.model.Activation;
 import org.apache.maven.model.Build;
+import org.apache.maven.model.BuildBase;
+import org.apache.maven.model.CiManagement;
 import org.apache.maven.model.Dependency;
 import org.apache.maven.model.DependencyManagement;
 import org.apache.maven.model.InputLocation;
 import org.apache.maven.model.InputSource;
 import org.apache.maven.model.Model;
+import org.apache.maven.model.ModelBase;
 import org.apache.maven.model.Parent;
 import org.apache.maven.model.Plugin;
+import org.apache.maven.model.PluginContainer;
 import org.apache.maven.model.PluginManagement;
 import org.apache.maven.model.Profile;
+import org.apache.maven.model.ReportPlugin;
+import org.apache.maven.model.Reporting;
 import org.apache.maven.model.Repository;
 import org.apache.maven.model.building.ModelProblem.Severity;
 import org.apache.maven.model.building.ModelProblem.Version;
@@ -44,6 +71,7 @@ import org.apache.maven.model.interpolation.ModelInterpolator;
 import org.apache.maven.model.io.ModelParseException;
 import org.apache.maven.model.management.DependencyManagementInjector;
 import org.apache.maven.model.management.PluginManagementInjector;
+import org.apache.maven.model.merge.ModelMerger;
 import org.apache.maven.model.normalization.ModelNormalizer;
 import org.apache.maven.model.path.ModelPathTranslator;
 import org.apache.maven.model.path.ModelUrlNormalizer;
@@ -64,25 +92,6 @@ import org.codehaus.plexus.interpolation.MapBasedValueSource;
 import org.codehaus.plexus.interpolation.StringSearchInterpolator;
 import org.eclipse.sisu.Nullable;
 
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-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 javax.inject.Inject;
-import javax.inject.Named;
-import javax.inject.Singleton;
-
-import static org.apache.maven.model.building.Result.error;
-import static org.apache.maven.model.building.Result.newResult;
-
 /**
  * @author Benjamin Bentmann
  */
@@ -108,7 +117,7 @@ public class DefaultModelBuilder
 
     @Inject
     private ModelUrlNormalizer modelUrlNormalizer;
-
+    
     @Inject
     private SuperPomProvider superPomProvider;
 
@@ -142,6 +151,8 @@ public class DefaultModelBuilder
 
     @Inject
     private ReportingConverter reportingConverter;
+    
+    private ModelMerger modelMerger = new FileToRawModelMerger();
 
     public DefaultModelBuilder setModelProcessor( ModelProcessor modelProcessor )
     {
@@ -244,7 +255,7 @@ public class DefaultModelBuilder
         this.reportingConverter = reportingConverter;
         return this;
     }
-
+    
     @SuppressWarnings( "checkstyle:methodlength" )
     @Override
     public ModelBuildingResult build( ModelBuildingRequest request )
@@ -426,7 +437,7 @@ public class DefaultModelBuilder
         }
 
         result.setEffectiveModel( resultModel );
-
+        
         for ( ModelData currentData : lineage )
         {
             String modelId = ( currentData != superData ) ? currentData.getId() : "";
@@ -529,6 +540,7 @@ public class DefaultModelBuilder
         }
     }
 
+    @SuppressWarnings( "checkstyle:methodlength" )
     private Model readModel( ModelSource modelSource, File pomFile, ModelBuildingRequest request,
                              DefaultModelProblemCollector problems )
         throws ModelBuildingException
@@ -637,7 +649,6 @@ public class DefaultModelBuilder
                 .setMessage( "Non-readable POM " + modelSource.getLocation() + ": " + msg ).setException( e ) );
             throw problems.newModelBuildingException();
         }
-
         if ( pomFile != null )
         {
             model.setPomFile( pomFile );
@@ -646,8 +657,33 @@ public class DefaultModelBuilder
         {
             model.setPomFile( ( (FileModelSource) modelSource ).getFile() );
         }
-
         problems.setSource( model );
+
+        modelValidator.validateFileModel( model, request, problems );
+        request.setFileModel( model );
+        
+        if ( Features.buildConsumer().isActive() && pomFile != null )
+        {
+            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();
+                
+                // Apply enriched data
+                modelMerger.merge( model, rawModel, false, null );
+            }
+            catch ( IOException e )
+            {
+                problems.add( new ModelProblemCollectorRequest( Severity.WARNING, Version.V37 ).setException( e ) );
+            }
+        }
+
         modelValidator.validateRawModel( model, request, problems );
 
         if ( hasFatalErrors( problems ) )
@@ -953,12 +989,17 @@ public class DefaultModelBuilder
 
             if ( parentData == null )
             {
-                parentData = fromCache( request.getModelCache(), 
-                                       parent.getGroupId(), parent.getArtifactId(),
-                                       parent.getVersion(), ModelCacheTag.RAW );
+                ModelData candidateData = fromCache( request.getModelCache(), 
+                                                     parent.getGroupId(), parent.getArtifactId(),
+                                                     parent.getVersion(), ModelCacheTag.RAW );
+
                 
-                // ArtifactModelSource means repositorySource
-                if ( parentData == null || !( parentData.getSource() instanceof ArtifactModelSource ) )
+                if ( candidateData != null && candidateData.getSource() instanceof ArtifactModelSource )
+                {
+                    // ArtifactModelSource means repositorySource
+                    parentData = candidateData;
+                }
+                else
                 {
                     parentData = readParentExternally( childModel, request, problems );
                     
@@ -967,21 +1008,20 @@ public class DefaultModelBuilder
                               parentData.getVersion(), ModelCacheTag.RAW, parentData );
                 }
             }
-
-            Model parentModel = parentData.getModel();
-
-            if ( !"pom".equals( parentModel.getPackaging() ) )
+            
+            if ( parentData != null ) 
             {
-                problems.add( new ModelProblemCollectorRequest( Severity.ERROR, Version.BASE )
-                    .setMessage( "Invalid packaging for parent POM " + ModelProblemUtils.toSourceHint( parentModel )
-                                     + ", must be \"pom\" but is \"" + parentModel.getPackaging() + "\"" )
-                    .setLocation( parentModel.getLocation( "packaging" ) ) );
+                Model parentModel = parentData.getModel();
+
+                if ( !"pom".equals( parentModel.getPackaging() ) )
+                {
+                    problems.add( new ModelProblemCollectorRequest( Severity.ERROR, Version.BASE )
+                        .setMessage( "Invalid packaging for parent POM " + ModelProblemUtils.toSourceHint( parentModel )
+                                         + ", must be \"pom\" but is \"" + parentModel.getPackaging() + "\"" )
+                        .setLocation( parentModel.getLocation( "packaging" ) ) );
+                }
             }
         }
-        else
-        {
-            parentData = null;
-        }
 
         return parentData;
     }
@@ -1353,7 +1393,7 @@ public class DefaultModelBuilder
                     final ModelSource importSource;
                     try
                     {
-                        importSource = modelResolver.resolveModel( groupId, artifactId, version );
+                        importSource = modelResolver.resolveModel( dependency );
                     }
                     catch ( UnresolvableModelException e )
                     {
@@ -1516,4 +1556,155 @@ 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 
+     * 
+     * @author Robert Scholte
+     * @since 3.7.0
+     */
+    class FileToRawModelMerger extends ModelMerger
+    {
+        @Override
+        protected void mergeBuild_Extensions( Build target, Build source, boolean sourceDominant,
+                                              Map<Object, Object> context )
+        {
+            // don't merge
+        }
+        
+
+        @Override
+        protected void mergeBuildBase_Resources( BuildBase target, BuildBase source, boolean sourceDominant,
+                                                 Map<Object, Object> context )
+        {
+            // 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 )
+        {
+            Iterator<Dependency> sourceIterator = source.getDependencies().iterator();
+            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 )
+        {
+            // don't merge
+        }
+
+        @Override
+        protected void mergeModel_Developers( Model target, Model source, boolean sourceDominant,
+                                              Map<Object, Object> context )
+        {
+            // 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 )
+        {
+            Iterator<Profile> sourceIterator = source.getProfiles().iterator();
+            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 )
+        {
+            Iterator<Dependency> sourceIterator = source.getDependencies().iterator();
+            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 )
+        {
+            Iterator<Dependency> sourceIterator = source.getDependencies().iterator();
+            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 )
+        {
+            // don't merge
+        }
+
+        @Override
+        protected void mergeReportPlugin_ReportSets( ReportPlugin target, ReportPlugin source, boolean sourceDominant,
+                                                     Map<Object, Object> context )
+        {
+            // don't merge
+        }
+        
+        @Override
+        protected void mergePluginContainer_Plugins( PluginContainer target, PluginContainer source,
+                                                     boolean sourceDominant, Map<Object, Object> context )
+        {
+            // don't merge
+        }
+    }
 }
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilderFactory.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilderFactory.java
index 4240574..daf56ca 100644
--- a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilderFactory.java
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilderFactory.java
@@ -91,7 +91,9 @@ public class DefaultModelBuilderFactory
 
     protected ModelReader newModelReader()
     {
-        return new DefaultModelReader();
+        DefaultModelReader reader = new DefaultModelReader();
+        reader.setTransformer( newModelSourceTransformer() );
+        return reader;
     }
 
     protected ProfileSelector newProfileSelector()
@@ -199,6 +201,11 @@ public class DefaultModelBuilderFactory
         return new DefaultReportingConverter();
     }
 
+    private ModelSourceTransformer newModelSourceTransformer()
+    {
+        return new DefaultModelSourceTransformer();
+    }
+
     /**
      * Creates a new model builder instance.
      *
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 84a68f7..2012bb1 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
@@ -38,6 +38,7 @@ import org.apache.maven.model.resolution.WorkspaceModelResolver;
 public class DefaultModelBuildingRequest
     implements ModelBuildingRequest
 {
+    private Model fileModel;
 
     private Model rawModel;
 
@@ -72,6 +73,8 @@ public class DefaultModelBuildingRequest
     private ModelCache modelCache;
 
     private WorkspaceModelResolver workspaceResolver;
+    
+    private TransformerContext context;
 
     /**
      * Creates an empty request.
@@ -383,6 +386,19 @@ public class DefaultModelBuildingRequest
     }
 
     @Override
+    public Model getFileModel()
+    {
+        return fileModel;
+    }
+    
+    @Override
+    public ModelBuildingRequest setFileModel( Model fileModel )
+    {
+        this.fileModel = fileModel;
+        return this;
+    }
+    
+    @Override
     public Model getRawModel()
     {
         return rawModel;
@@ -407,5 +423,17 @@ public class DefaultModelBuildingRequest
         this.workspaceResolver = workspaceResolver;
         return this;
     }
-
+    
+    @Override
+    public TransformerContext getTransformerContext()
+    {
+        return context;
+    }
+    
+    @Override
+    public ModelBuildingRequest setTransformerContext( TransformerContext context )
+    {
+        this.context = context;
+        return this;
+    }
 }
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelSourceTransformer.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelSourceTransformer.java
new file mode 100644
index 0000000..50ad04b
--- /dev/null
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelSourceTransformer.java
@@ -0,0 +1,43 @@
+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.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/**
+ * Default ModelSourceTransformer, provides pomFile as inputStream and ignores the context 
+ * 
+ * @author Robert Scholte
+ * @since 3.7.0
+ */
+public class DefaultModelSourceTransformer implements ModelSourceTransformer
+{
+
+    @Override
+    public InputStream transform( Path pomFile, TransformerContext context )
+        throws IOException, TransformerException
+    {
+        return Files.newInputStream( pomFile );
+    }
+
+}
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 a51126f..1dd2643 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
@@ -257,6 +257,19 @@ class FilterModelBuildingRequest
     }
 
     @Override
+    public Model getFileModel()
+    {
+        return request.getFileModel();
+    }
+    
+    @Override
+    public ModelBuildingRequest setFileModel( Model fileModel )
+    {
+        request.setFileModel( fileModel );
+        return this;
+    }
+    
+    @Override
     public Model getRawModel()
     {
         return request.getRawModel();
@@ -281,5 +294,17 @@ class FilterModelBuildingRequest
         request.setWorkspaceModelResolver( workspaceResolver );
         return this;
     }
-
+    
+    @Override
+    public TransformerContext getTransformerContext()
+    {
+        return request.getTransformerContext();
+    }
+    
+    @Override
+    public ModelBuildingRequest setTransformerContext( TransformerContext context )
+    {
+        request.setTransformerContext( context );
+        return this;
+    }
 }
\ No newline at end of file
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 dce0c32..9523f4c 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,6 +63,21 @@ 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
+     * @since 3.7.0
+     */
+    Model getFileModel();
+    
+    /**
+     * 
+     * @param fileModel
+     * @return This request, never {@code null}.
+     * @since 3.7.0
+     */
+    ModelBuildingRequest setFileModel( Model fileModel );
 
     /**
      * Gets the raw model to build. If not set, model source will be used to load raw model.
@@ -334,5 +349,10 @@ public interface ModelBuildingRequest
     WorkspaceModelResolver getWorkspaceModelResolver();
 
     ModelBuildingRequest setWorkspaceModelResolver( WorkspaceModelResolver workspaceResolver );
+    
+    TransformerContext getTransformerContext();
 
+    ModelBuildingRequest setTransformerContext( TransformerContext context );
+    
+    
 }
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelProblem.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelProblem.java
index 2c7a72e..30b6724 100644
--- a/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelProblem.java
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelProblem.java
@@ -50,7 +50,8 @@ public interface ModelProblem
         BASE,
         V20,
         V30,
-        V31
+        V31,
+        V37
     }
 
     /**
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
new file mode 100644
index 0000000..2aa3ddb
--- /dev/null
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelSourceTransformer.java
@@ -0,0 +1,35 @@
+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.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
+
+/**
+ * 
+ * @author Robert Scholte
+ * @since 3.7.0
+ */
+public interface ModelSourceTransformer
+{
+    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
new file mode 100644
index 0000000..2f763a2
--- /dev/null
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/TransformerContext.java
@@ -0,0 +1,64 @@
+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 org.apache.maven.model.Model;
+
+/**
+ * Context used to transform a pom file.
+ * 
+ * 
+ * @author Robert Scholte
+ * @since 3.7.0
+ */
+public interface TransformerContext
+{
+    /**
+     * Key to get the TransformerContext from the SessionData
+     */
+    Object KEY = TransformerContext.class;
+    
+    /**
+     * Get the value of the commandline argument {@code -Dkey=value}
+     * @param key
+     * @return
+     */
+    String getUserProperty( String key );
+    
+    /**
+     * Get the model based on the path, will be used to resolve the parent based on relativePath
+     * 
+     * @param p the path
+     * @return the model, otherwise {@code null}
+     */
+    Model getRawModel( Path p );
+    
+    /**
+     * Get the model from the reactor based on the groupId and artifactId, will be used for reactor dependencies
+     * 
+     * @param groupId the groupId
+     * @param artifactId the artifactId
+     * @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;
+}
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/TransformerException.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/TransformerException.java
new file mode 100644
index 0000000..6c89d67
--- /dev/null
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/TransformerException.java
@@ -0,0 +1,40 @@
+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.
+ */
+
+/**
+ * 
+ * @author Robert Scholte
+ * @since 3.7.0
+ */
+public class TransformerException extends Exception
+{
+
+    public TransformerException( Exception e )
+    {
+        super ( e );
+    }
+
+    public TransformerException( String message, Throwable exception )
+    {
+        super( message, exception );
+    }
+
+}
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 6e78fd0..1bae747 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
@@ -27,11 +27,15 @@ import java.io.Reader;
 import java.util.Map;
 import java.util.Objects;
 
+import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Singleton;
 
 import org.apache.maven.model.InputSource;
 import org.apache.maven.model.Model;
+import org.apache.maven.model.building.ModelSourceTransformer;
+import org.apache.maven.model.building.TransformerContext;
+import org.apache.maven.model.building.TransformerException;
 import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
 import org.apache.maven.model.io.xpp3.MavenXpp3ReaderEx;
 import org.codehaus.plexus.util.ReaderFactory;
@@ -48,18 +52,51 @@ import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
 public class DefaultModelReader
     implements ModelReader
 {
+    @Inject
+    private ModelSourceTransformer transformer;
 
+    public void setTransformer( ModelSourceTransformer transformer )
+    {
+        this.transformer = transformer;
+    }
+    
     @Override
     public Model read( File input, Map<String, ?> options )
         throws IOException
     {
         Objects.requireNonNull( input, "input cannot be null" );
 
-        Model model = read( new FileInputStream( input ), options );
+        TransformerContext context = null;
+        if ( options != null )
+        {
+            context = (TransformerContext) options.get( "transformerContext" );
+        }        
 
-        model.setPomFile( input );
+        final InputStream is;
+        if ( context == null )
+        {
+            is = new FileInputStream( input );
+        }
+        else
+        {
+            try
+            {
+                is = transformer.transform( input.toPath(), context );
+            }
+            catch ( TransformerException e )
+            {
+                throw new IOException( "Failed to transform " + input,  e );
+            }
+        }
 
-        return model;
+        try ( InputStream in = is )
+        {
+            Model model = read( is, options );
+
+            model.setPomFile( input );
+
+            return model;
+        }
     }
 
     @Override
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/validation/DefaultModelValidator.java b/maven-model-builder/src/main/java/org/apache/maven/model/validation/DefaultModelValidator.java
index ad7e3c7..f789333 100644
--- a/maven-model-builder/src/main/java/org/apache/maven/model/validation/DefaultModelValidator.java
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/validation/DefaultModelValidator.java
@@ -88,7 +88,7 @@ public class DefaultModelValidator
     private final Set<String> validIds = new HashSet<>();
 
     @Override
-    public void validateRawModel( Model m, ModelBuildingRequest request, ModelProblemCollector problems )
+    public void validateFileModel( Model m, ModelBuildingRequest request, ModelProblemCollector problems )
     {
         Parent parent = m.getParent();
         if ( parent != null )
@@ -99,9 +99,6 @@ public class DefaultModelValidator
             validateStringNotEmpty( "parent.artifactId", problems, Severity.FATAL, Version.BASE, parent.getArtifactId(),
                                     parent );
 
-            validateStringNotEmpty( "parent.version", problems, Severity.FATAL, Version.BASE, parent.getVersion(),
-                                    parent );
-
             if ( equals( parent.getGroupId(), m.getGroupId() ) && equals( parent.getArtifactId(), m.getArtifactId() ) )
             {
                 addViolation( problems, Severity.FATAL, Version.BASE, "parent.artifactId", null,
@@ -120,6 +117,17 @@ public class DefaultModelValidator
 
         if ( request.getValidationLevel() >= ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0 )
         {
+            Set<String> modules = new HashSet<>();
+            for ( int i = 0, n = m.getModules().size(); i < n; i++ )
+            {
+                String module = m.getModules().get( i );
+                if ( !modules.add( module ) )
+                {
+                    addViolation( problems, Severity.ERROR, Version.V20, "modules.module[" + i + "]", null,
+                                  "specifies duplicate child module " + module, m.getLocation( "modules" ) );
+                }
+            }
+
             Severity errOn30 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0 );
 
             // [MNG-6074] Maven should produce an error if no model version has been set in a POM file used to build an
@@ -221,6 +229,18 @@ public class DefaultModelValidator
             }
         }
     }
+    
+    @Override
+    public void validateRawModel( Model m, ModelBuildingRequest request, ModelProblemCollector problems )
+    {
+        Parent parent = m.getParent();
+        
+        if ( parent != null )
+        {
+            validateStringNotEmpty( "parent.version", problems, Severity.FATAL, Version.BASE, parent.getVersion(),
+                                    parent );
+        }
+    }
 
     private void validate30RawProfileActivation( ModelProblemCollector problems, Activation activation,
                                                  String sourceHint, String prefix, String fieldName,
@@ -376,17 +396,6 @@ public class DefaultModelValidator
 
         if ( request.getValidationLevel() >= ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0 )
         {
-            Set<String> modules = new HashSet<>();
-            for ( int i = 0, n = m.getModules().size(); i < n; i++ )
-            {
-                String module = m.getModules().get( i );
-                if ( !modules.add( module ) )
-                {
-                    addViolation( problems, Severity.ERROR, Version.V20, "modules.module[" + i + "]", null,
-                                  "specifies duplicate child module " + module, m.getLocation( "modules" ) );
-                }
-            }
-
             Severity errOn31 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_1 );
 
             validateBannedCharacters( EMPTY, "version", problems, errOn31, Version.V20, m.getVersion(), null, m,
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/validation/ModelValidator.java b/maven-model-builder/src/main/java/org/apache/maven/model/validation/ModelValidator.java
index 84e3fad..198ba5a 100644
--- a/maven-model-builder/src/main/java/org/apache/maven/model/validation/ModelValidator.java
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/validation/ModelValidator.java
@@ -30,15 +30,27 @@ import org.apache.maven.model.building.ModelProblemCollector;
  */
 public interface ModelValidator
 {
-
     /**
-     * Checks the specified (raw) model for missing or invalid values. The raw model is directly created from the POM
+     * Checks the specified file model for missing or invalid values. This model is directly created from the POM
      * file and has not been subjected to inheritance, interpolation or profile/default injection.
      *
      * @param model The model to validate, must not be {@code null}.
      * @param request The model building request that holds further settings, must not be {@code null}.
      * @param problems The container used to collect problems that were encountered, must not be {@code null}.
      */
+    default void validateFileModel( Model model, ModelBuildingRequest request, ModelProblemCollector problems )
+    {
+        // do nothing
+    }
+
+    /**
+     * Checks the specified (raw) model for missing or invalid values. The raw model is the file model + buildpom filter
+     * transformation and has not been subjected to inheritance, interpolation or profile/default injection.
+     *
+     * @param model The model to validate, must not be {@code null}.
+     * @param request The model building request that holds further settings, must not be {@code null}.
+     * @param problems The container used to collect problems that were encountered, must not be {@code null}.
+     */
     void validateRawModel( Model model, ModelBuildingRequest request, ModelProblemCollector problems );
 
     /**
diff --git a/maven-model-builder/src/test/java/org/apache/maven/model/building/FileToRawModelMergerTest.java b/maven-model-builder/src/test/java/org/apache/maven/model/building/FileToRawModelMergerTest.java
new file mode 100644
index 0000000..485dc4c
--- /dev/null
+++ b/maven-model-builder/src/test/java/org/apache/maven/model/building/FileToRawModelMergerTest.java
@@ -0,0 +1,82 @@
+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 static org.hamcrest.Matchers.hasItems;
+import static org.junit.Assert.assertThat;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.maven.model.building.DefaultModelBuilder.FileToRawModelMerger;
+import org.apache.maven.model.merge.ModelMerger;
+import org.junit.Test;
+
+public class FileToRawModelMergerTest
+{
+
+    /**
+     * Ensures that all list-merge methods are overridden
+     */
+    @Test
+    public void testOverriddenMergeMethods()
+    {
+        List<String> methodNames =
+            Stream.of( ModelMerger.class.getDeclaredMethods() )
+                .filter( m -> m.getName().startsWith( "merge" ) )
+                .filter( m -> 
+                    {
+                        String baseName = m.getName().substring( 5 /* merge */ );
+                        String entity = baseName.substring( baseName.indexOf( '_' ) + 1 );
+                        try
+                        {
+                            Type returnType = m.getParameterTypes()[0].getMethod( "get" + entity ).getGenericReturnType();
+                            if ( returnType instanceof ParameterizedType )
+                            {
+                                return !( (ParameterizedType) returnType ).getActualTypeArguments()[0].equals( String.class );
+                            }
+                            else
+                            {
+                                return false;
+                            }
+                        }
+                        catch ( ReflectiveOperationException | SecurityException e )
+                        {
+                            return false;
+                        }
+                    } )
+                .map( Method::getName )
+                .collect( Collectors.toList() );
+
+        List<String> overriddenMethods =
+            Stream.of( FileToRawModelMerger.class.getDeclaredMethods() )
+                .map( Method::getName )
+                .filter( m -> m.startsWith( "merge" ) )
+                .collect( Collectors.toList() );
+
+        assertThat( overriddenMethods, hasItems( methodNames.toArray( new String[0] ) ) );
+    }
+    
+
+}
diff --git a/maven-model-builder/src/test/java/org/apache/maven/model/inheritance/DefaultInheritanceAssemblerTest.java b/maven-model-builder/src/test/java/org/apache/maven/model/inheritance/DefaultInheritanceAssemblerTest.java
index 09f930c..9924471 100644
--- a/maven-model-builder/src/test/java/org/apache/maven/model/inheritance/DefaultInheritanceAssemblerTest.java
+++ b/maven-model-builder/src/test/java/org/apache/maven/model/inheritance/DefaultInheritanceAssemblerTest.java
@@ -20,18 +20,24 @@ package org.apache.maven.model.inheritance;
  */
 
 import org.apache.maven.model.Model;
+import org.apache.maven.model.building.AbstractModelSourceTransformer;
 import org.apache.maven.model.building.SimpleProblemCollector;
+import org.apache.maven.model.building.TransformerContext;
 import org.apache.maven.model.io.DefaultModelReader;
 import org.apache.maven.model.io.DefaultModelWriter;
-import org.apache.maven.model.io.ModelReader;
 import org.apache.maven.model.io.ModelWriter;
-
+import org.apache.maven.xml.sax.filter.AbstractSAXFilter;
+import org.xml.sax.SAXException;
 import org.xmlunit.matchers.CompareMatcher;
 
 import junit.framework.TestCase;
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.Path;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.TransformerConfigurationException;
 
 import static org.junit.Assert.assertThat;
 
@@ -41,7 +47,7 @@ import static org.junit.Assert.assertThat;
 public class DefaultInheritanceAssemblerTest
     extends TestCase
 {
-    private ModelReader reader;
+    private DefaultModelReader reader;
 
     private ModelWriter writer;
 
@@ -54,6 +60,15 @@ public class DefaultInheritanceAssemblerTest
         super.setUp();
 
         reader = new DefaultModelReader();
+        reader.setTransformer( new AbstractModelSourceTransformer()
+        {
+            @Override
+            protected AbstractSAXFilter getSAXFilter( Path pomFile, TransformerContext context )
+                throws TransformerConfigurationException, SAXException, ParserConfigurationException
+            {
+                return null;
+            }
+        } );
         writer = new DefaultModelWriter();
         assembler = new DefaultInheritanceAssembler();
     }
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 3e07c57..d2a9e60 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
@@ -64,10 +64,14 @@ public class DefaultModelValidatorTest
         throws Exception
     {
         ModelBuildingRequest request = new DefaultModelBuildingRequest().setValidationLevel( level );
+        
+        Model model =  read( pom );
 
-        SimpleProblemCollector problems = new SimpleProblemCollector( read( pom ) );
+        SimpleProblemCollector problems = new SimpleProblemCollector( model );
+        
+        request.setFileModel( model );
 
-        validator.validateEffectiveModel( problems.getModel(), request, problems );
+        validator.validateEffectiveModel( model, request, problems );
 
         return problems;
     }
@@ -77,9 +81,15 @@ public class DefaultModelValidatorTest
     {
         ModelBuildingRequest request = new DefaultModelBuildingRequest().setValidationLevel( level );
 
-        SimpleProblemCollector problems = new SimpleProblemCollector( read( pom ) );
+        Model model = read( pom );
+        
+        SimpleProblemCollector problems = new SimpleProblemCollector( model );
 
-        validator.validateRawModel( problems.getModel(), request, problems );
+        validator.validateFileModel( model, request, problems );
+        
+        request.setFileModel( model );
+        
+        validator.validateRawModel( model, request, problems );
 
         return problems;
     }
@@ -366,7 +376,7 @@ public class DefaultModelValidatorTest
     public void testDuplicateModule()
         throws Exception
     {
-        SimpleProblemCollector result = validate( "duplicate-module.xml" );
+        SimpleProblemCollector result = validateRaw( "duplicate-module.xml" );
 
         assertViolations( result, 0, 1, 0 );
 
@@ -416,7 +426,6 @@ public class DefaultModelValidatorTest
         SimpleProblemCollector result = validateRaw( "incomplete-parent.xml" );
 
         assertViolations( result, 3, 0, 0 );
-
         assertTrue( result.getFatals().get( 0 ).contains( "parent.groupId" ) );
         assertTrue( result.getFatals().get( 1 ).contains( "parent.artifactId" ) );
         assertTrue( result.getFatals().get( 2 ).contains( "parent.version" ) );
diff --git a/maven-xml/pom.xml b/maven-xml/pom.xml
new file mode 100644
index 0000000..6b98f12
--- /dev/null
+++ b/maven-xml/pom.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.apache.maven</groupId>
+    <artifactId>maven</artifactId>
+    <version>3.7.0-SNAPSHOT</version>
+  </parent>
+  <artifactId>maven-xml</artifactId>
+  <name>Maven XML</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.xmlunit</groupId>
+      <artifactId>xmlunit-assertj</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+  </dependencies>
+</project>
\ No newline at end of file
diff --git a/maven-xml/src/main/java/org/apache/maven/xml/Factories.java b/maven-xml/src/main/java/org/apache/maven/xml/Factories.java
new file mode 100644
index 0000000..eb2166a
--- /dev/null
+++ b/maven-xml/src/main/java/org/apache/maven/xml/Factories.java
@@ -0,0 +1,118 @@
+package org.apache.maven.xml;
+
+/*
+ * 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 javax.xml.XMLConstants;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+import javax.xml.transform.TransformerFactory;
+
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXNotRecognizedException;
+import org.xml.sax.SAXNotSupportedException;
+import org.xml.sax.XMLReader;
+
+/**
+ * Creates XML related factories with OWASP advices applied
+ * 
+ * @author Robert Scholte
+ * @since 3.7.0
+ */
+public final class Factories
+{
+    private Factories()
+    {
+    }
+    
+    /**
+     * 
+     * @return
+     * @see https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#transformerfactory
+     */
+    public static TransformerFactory newTransformerFactory() 
+    {
+        TransformerFactory tf = TransformerFactory.newInstance();
+        tf.setAttribute( XMLConstants.ACCESS_EXTERNAL_DTD, "" );
+        tf.setAttribute( XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "" );
+
+        return tf;
+    }
+    
+    public static SAXParserFactory newSAXParserFactory()
+    {
+        SAXParserFactory spf = SAXParserFactory.newInstance();
+
+        try
+        {
+            // Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-general-entities
+            // Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-general-entities
+   
+            // Using the SAXParserFactory's setFeature
+            spf.setFeature( "http://xml.org/sax/features/external-general-entities", false );
+            
+            // Xerces 2 only - http://xerces.apache.org/xerces-j/features.html#external-general-entities
+            spf.setFeature( "http://apache.org/xml/features/disallow-doctype-decl", true );
+        }
+        catch ( ParserConfigurationException e )
+        {
+            // Tried an unsupported feature.
+        }
+        catch ( SAXNotRecognizedException e )
+        {
+            // Tried an unknown feature.
+        }
+        catch ( SAXNotSupportedException e )
+        {
+            // Tried a feature known to the parser but unsupported.
+        }
+        return spf;
+    }
+    
+    public static SAXParser newSAXParser() throws ParserConfigurationException, SAXException
+    {
+        SAXParser saxParser = newSAXParserFactory().newSAXParser();
+        
+        return saxParser;
+    }
+    
+    public static XMLReader newXMLReader() throws SAXException, ParserConfigurationException
+    {
+        XMLReader reader = newSAXParser().getXMLReader();
+        
+        try
+        {
+            // Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-general-entities
+            // Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-general-entities
+       
+            // Using the XMLReader's setFeature
+            reader.setFeature( "http://xml.org/sax/features/external-general-entities", false );
+        }
+        catch ( SAXNotRecognizedException e )
+        {
+            // Tried an unknown feature.
+        }
+        catch ( SAXNotSupportedException e )
+        {
+            // Tried a feature known to the parser but unsupported.
+        }
+        return reader;
+    }
+}
diff --git a/maven-xml/src/main/java/org/apache/maven/xml/sax/SAXEvent.java b/maven-xml/src/main/java/org/apache/maven/xml/sax/SAXEvent.java
new file mode 100644
index 0000000..8126957
--- /dev/null
+++ b/maven-xml/src/main/java/org/apache/maven/xml/sax/SAXEvent.java
@@ -0,0 +1,34 @@
+package org.apache.maven.xml.sax;
+
+/*
+ * 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 org.xml.sax.SAXException;
+
+/**
+ * Command pattern to gather events which can be executed later on.
+ * 
+ * @author Robert Scholte
+ * @since 3.7.0
+ */
+@FunctionalInterface
+public interface SAXEvent
+{
+    void execute() throws SAXException;
+}
diff --git a/maven-xml/src/main/java/org/apache/maven/xml/sax/SAXEventFactory.java b/maven-xml/src/main/java/org/apache/maven/xml/sax/SAXEventFactory.java
new file mode 100644
index 0000000..84e13e5
--- /dev/null
+++ b/maven-xml/src/main/java/org/apache/maven/xml/sax/SAXEventFactory.java
@@ -0,0 +1,144 @@
+package org.apache.maven.xml.sax;
+
+/*
+ * 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 org.xml.sax.Attributes;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.Locator;
+import org.xml.sax.ext.LexicalHandler;
+
+/**
+ * Factory for SAXEvents
+ * 
+ * @author Robert Scholte
+ * @since 3.7.0
+ */
+public final class SAXEventFactory
+{
+    private final ContentHandler contentHandler;
+    
+    private final LexicalHandler lexicalHandler;
+
+    protected SAXEventFactory( ContentHandler contentHandler, LexicalHandler lexicalHandler )
+    {
+        this.contentHandler = contentHandler;
+        this.lexicalHandler = lexicalHandler;
+    }
+
+    public SAXEvent characters( final char[] ch, final int start, final int length )
+    {
+        final char[] txt = new char[length];
+        System.arraycopy( ch, start, txt, 0, length );
+        return () -> contentHandler.characters( txt, 0, length );
+    }
+
+    public SAXEvent endDocument()
+    {
+        return () -> contentHandler.endDocument();
+    }
+
+    public SAXEvent endElement( final String uri, final String localName, final String qName )
+    {
+        return () -> contentHandler.endElement( uri, localName, qName );
+    }
+
+    public SAXEvent endPrefixMapping( final String prefix )
+    {
+        return () ->  contentHandler.endPrefixMapping( prefix );
+    }
+
+    public SAXEvent ignorableWhitespace( final char[] ch, final int start, final int length )
+    {
+        return () ->  contentHandler.ignorableWhitespace( ch, start, length );
+    }
+
+    public SAXEvent processingInstruction( final String target, final String data )
+    {
+        return () -> contentHandler.processingInstruction( target, data );
+    }
+
+    public SAXEvent setDocumentLocator( final Locator locator )
+    {
+        return () -> contentHandler.setDocumentLocator( locator );
+    }
+
+    public SAXEvent skippedEntity( final String name )
+    {
+        return () -> contentHandler.skippedEntity( name );
+    }
+
+    public SAXEvent startDocument()
+    {
+        return () -> contentHandler.startDocument();
+    }
+
+    public SAXEvent startElement( final String uri, final String localName, final String qName, final Attributes atts )
+    {
+        return () -> contentHandler.startElement( uri, localName, qName, atts );
+    }
+
+    public SAXEvent startPrefixMapping( final String prefix, final String uri )
+    {
+        return () -> contentHandler.startPrefixMapping( prefix, uri );
+    }
+    
+    public static SAXEventFactory newInstance( ContentHandler contentHandler, LexicalHandler lexicalHandler )
+    {
+        return new SAXEventFactory( contentHandler, lexicalHandler );
+    }
+
+    public SAXEvent startDTD( String name, String publicId, String systemId )
+    {
+        return () -> lexicalHandler.startDTD( name, publicId, systemId );
+    }
+
+    public SAXEvent endDTD()
+    {
+        return () -> lexicalHandler.endDTD();
+    }
+
+    public SAXEvent startEntity( String name )
+    {
+        return () -> lexicalHandler.startEntity( name );
+    }
+
+    public SAXEvent endEntity( String name )
+    {
+        return () -> lexicalHandler.endEntity( name );
+        
+    }
+
+    public SAXEvent startCDATA()
+    {
+        return () -> lexicalHandler.startCDATA();
+    }
+
+    public SAXEvent endCDATA()
+    {
+        return () -> lexicalHandler.endCDATA();
+    }
+
+    public SAXEvent comment( char[] ch, int start, int length )
+    {
+        final char[] txt = new char[length];
+        System.arraycopy( ch, start, txt, 0, length );
+        return () -> lexicalHandler.comment(  txt, 0, length );
+    }
+}
diff --git a/maven-xml/src/main/java/org/apache/maven/xml/sax/SAXEventUtils.java b/maven-xml/src/main/java/org/apache/maven/xml/sax/SAXEventUtils.java
new file mode 100644
index 0000000..237ec44
--- /dev/null
+++ b/maven-xml/src/main/java/org/apache/maven/xml/sax/SAXEventUtils.java
@@ -0,0 +1,49 @@
+package org.apache.maven.xml.sax;
+
+/*
+ * 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.util.regex.Pattern;
+
+/**
+ * Utility class for SAXEvents
+ * 
+ * @author Robert Scholte
+ * @since 3.7.0
+ */
+public final class SAXEventUtils
+{
+    private static final Pattern PATTERN = Pattern.compile( "[^:]+$" );
+    
+    private SAXEventUtils()
+    {
+    }
+    
+    /**
+     * Returns the newLocalName prefixed with the namespace of the oldQName if present 
+     * 
+     * @param oldQName the QName, used for its namespace
+     * @param newLocalName the preferred localName
+     * @return the new QName
+     */
+    public static String renameQName( String oldQName, String newLocalName )
+    {
+        return PATTERN.matcher( oldQName ).replaceFirst( newLocalName );
+    }
+}
diff --git a/maven-xml/src/main/java/org/apache/maven/xml/sax/ext/CommentRenormalizer.java b/maven-xml/src/main/java/org/apache/maven/xml/sax/ext/CommentRenormalizer.java
new file mode 100644
index 0000000..3ae19a4
--- /dev/null
+++ b/maven-xml/src/main/java/org/apache/maven/xml/sax/ext/CommentRenormalizer.java
@@ -0,0 +1,108 @@
+package org.apache.maven.xml.sax.ext;
+
+/*
+ * 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 org.xml.sax.SAXException;
+import org.xml.sax.ext.LexicalHandler;
+
+/**
+ * During parsing the line separators are transformed to \n
+ * Unlike characters(), comments don't use the systems line separator for serialization.
+ * Hence use this class in the LexicalHandler chain to do so 
+ * 
+ * @author Robert Scholte
+ * @since 3.7.0
+ */
+public class CommentRenormalizer implements LexicalHandler
+{
+    private final LexicalHandler lexicalHandler;
+    
+    private final String lineSeparator;
+
+    public CommentRenormalizer( LexicalHandler lexicalHandler )
+    {
+        this( lexicalHandler,  System.lineSeparator() );
+    }
+
+    // for testing purpose
+    CommentRenormalizer( LexicalHandler lexicalHandler, String lineSeparator )
+    {
+        this.lexicalHandler = lexicalHandler;
+        this.lineSeparator = lineSeparator;
+    }
+
+    @Override
+    public void comment( char[] ch, int start, int length )
+        throws SAXException
+    {
+        if ( "\n".equals( lineSeparator ) )
+        {
+            lexicalHandler.comment( ch, start, length );
+        }
+        else
+        {
+            char[] ca = new String( ch, start, length ).replaceAll( "\n", lineSeparator ).toCharArray();
+            
+            lexicalHandler.comment( ca, 0, ca.length );
+        }
+    }
+
+    @Override
+    public void startDTD( String name, String publicId, String systemId )
+        throws SAXException
+    {
+        lexicalHandler.startDTD( name, publicId, systemId );
+    }
+
+    @Override
+    public void endDTD()
+        throws SAXException
+    {
+        lexicalHandler.endDTD();
+    }
+
+    @Override
+    public void startEntity( String name )
+        throws SAXException
+    {
+        lexicalHandler.startEntity( name );
+    }
+
+    @Override
+    public void endEntity( String name )
+        throws SAXException
+    {
+        lexicalHandler.endEntity( name );
+    }
+
+    @Override
+    public void startCDATA()
+        throws SAXException
+    {
+        lexicalHandler.startCDATA();
+    }
+
+    @Override
+    public void endCDATA()
+        throws SAXException
+    {
+        lexicalHandler.endCDATA();
+    }
+}
diff --git a/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/AbstractEventXMLFilter.java b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/AbstractEventXMLFilter.java
new file mode 100644
index 0000000..d23bdec
--- /dev/null
+++ b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/AbstractEventXMLFilter.java
@@ -0,0 +1,289 @@
+package org.apache.maven.xml.sax.filter;
+
+/*
+ * 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.util.ArrayDeque;
+import java.util.Queue;
+
+import org.apache.maven.xml.sax.SAXEvent;
+import org.apache.maven.xml.sax.SAXEventFactory;
+import org.xml.sax.Attributes;
+import org.xml.sax.Locator;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.ext.LexicalHandler;
+
+/**
+ * Builds up a list of SAXEvents, which will be executed with {@link #executeEvents()}
+ * 
+ * @author Robert Scholte
+ * @since 3.7.0
+ */
+abstract class AbstractEventXMLFilter extends AbstractSAXFilter
+{
+    private Queue<SAXEvent> saxEvents = new ArrayDeque<>();
+    
+    private SAXEventFactory eventFactory;
+    
+    // characters BEFORE startElement must get state of startingElement
+    // this way removing based on state keeps correct formatting
+    private SAXEvent characters;
+    
+    private boolean lockCharacters = false;
+    
+    protected abstract boolean isParsing();
+    
+    protected abstract String getState();
+    
+    protected boolean acceptEvent( String state )
+    {
+        return true;
+    }
+    
+    AbstractEventXMLFilter()
+    {
+        super();
+    }
+
+    <T extends XMLReader & LexicalHandler> AbstractEventXMLFilter( T parent )
+    {
+        setParent( parent );
+    }
+
+    private SAXEventFactory getEventFactory()
+    {
+        if ( eventFactory == null )
+        {
+            eventFactory = SAXEventFactory.newInstance( getContentHandler(), getLexicalHandler() );
+        }
+        return eventFactory;
+    }
+    
+    private void processEvent( final SAXEvent event )
+                    throws SAXException
+    {
+        if ( isParsing() )
+        {
+            final String eventState = getState();
+            final SAXEvent charactersEvent = characters;
+            
+            if ( !lockCharacters && charactersEvent != null )
+            {
+                saxEvents.add( () -> 
+                {
+                    if ( acceptEvent( eventState ) )
+                    {
+                        charactersEvent.execute();
+                    }
+                } );
+                characters = null;
+            }
+
+            saxEvents.add( () -> 
+            {
+                if ( acceptEvent( eventState ) )
+                {
+                    event.execute();
+                }
+            } );
+        }
+        else
+        {
+            event.execute();
+        }
+    }
+
+    /**
+     * Should be used to include extra events before a closing element.
+     * This is a lightweight solution to keep the correct indentation.
+     * 
+     * @return
+     */
+    protected Includer include() 
+    {
+        this.lockCharacters = true;
+        
+        return () -> lockCharacters = false;
+    }
+
+    protected final void executeEvents() throws SAXException
+    {
+        final String eventState = getState();
+        final SAXEvent charactersEvent = characters;
+        if ( charactersEvent != null )
+        {
+            saxEvents.add( () -> 
+            {
+                if ( acceptEvent( eventState ) )
+                {
+                    charactersEvent.execute();
+                }
+            } );
+            characters = null;
+        }
+        
+        // not with streams due to checked SAXException
+        while ( !saxEvents.isEmpty() )
+        {
+            saxEvents.poll().execute();
+        }
+    }
+    
+    @Override
+    public void setDocumentLocator( Locator locator )
+    {
+        try
+        {
+            processEvent( getEventFactory().setDocumentLocator( locator ) );
+        }
+        catch ( SAXException e )
+        {
+            // noop, setDocumentLocator can never throw a SAXException
+        }
+    }
+
+    @Override
+    public void startDocument() throws SAXException
+    {
+        processEvent( getEventFactory().startDocument() );
+    }
+
+    @Override
+    public void endDocument() throws SAXException
+    {
+        processEvent( getEventFactory().endDocument() );
+    }
+
+    @Override
+    public void startPrefixMapping( String prefix, String uri ) throws SAXException
+    {
+        processEvent( getEventFactory().startPrefixMapping( prefix, uri ) );
+    }
+
+    @Override
+    public void endPrefixMapping( String prefix ) throws SAXException
+    {
+        processEvent( getEventFactory().endPrefixMapping( prefix ) );
+    }
+
+    @Override
+    public void startElement( String uri, String localName, String qName, Attributes atts ) throws SAXException
+    {
+        processEvent( getEventFactory().startElement( uri, localName, qName, atts ) );
+    }
+
+    @Override
+    public void endElement( String uri, String localName, String qName ) throws SAXException
+    {
+        processEvent( getEventFactory().endElement( uri, localName, qName ) );
+    }
+
+    @Override
+    public void characters( char[] ch, int start, int length ) throws SAXException
+    {
+        if ( lockCharacters )
+        {
+            processEvent( getEventFactory().characters( ch, start, length ) );
+        }
+        else if ( isParsing() )
+        {
+            this.characters = getEventFactory().characters( ch, start, length );
+        }
+        else
+        {
+            super.characters( ch, start, length );
+        }
+    }
+
+    @Override
+    public void ignorableWhitespace( char[] ch, int start, int length ) throws SAXException
+    {
+        processEvent( getEventFactory().ignorableWhitespace( ch, start, length ) );
+    }
+
+    @Override
+    public void processingInstruction( String target, String data ) throws SAXException
+    {
+        processEvent( getEventFactory().processingInstruction( target, data ) );
+    }
+
+    @Override
+    public void skippedEntity( String name ) throws SAXException
+    {
+        processEvent( getEventFactory().skippedEntity( name ) );
+    }
+
+    @Override
+    public void startDTD( String name, String publicId, String systemId ) throws SAXException
+    {
+        processEvent( getEventFactory().startCDATA() );
+    }
+
+    @Override
+    public void endDTD() throws SAXException
+    {
+        processEvent( getEventFactory().endDTD() );
+    }
+
+    @Override
+    public void startEntity( String name ) throws SAXException
+    {
+        processEvent( getEventFactory().startEntity( name ) );
+    }
+
+    @Override
+    public void endEntity( String name ) throws SAXException
+    {
+        processEvent( getEventFactory().endEntity( name ) );
+    }
+
+    @Override
+    public void startCDATA()
+        throws SAXException
+    {
+        processEvent( getEventFactory().startCDATA() );
+    }
+
+    @Override
+    public void endCDATA()
+        throws SAXException
+    {
+        processEvent( getEventFactory().endCDATA() );        
+    }
+
+    @Override
+    public void comment( char[] ch, int start, int length )
+        throws SAXException
+    {
+        processEvent( getEventFactory().comment( ch, start, length ) );
+    }
+    
+    /**
+     * AutoCloseable with a close method that doesn't throw an exception
+     * 
+     * @author Robert Scholte
+     *
+     */
+    @FunctionalInterface
+    protected interface Includer extends AutoCloseable
+    {
+        void close();
+    }
+}
diff --git a/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/AbstractSAXFilter.java b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/AbstractSAXFilter.java
new file mode 100644
index 0000000..89de519
--- /dev/null
+++ b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/AbstractSAXFilter.java
@@ -0,0 +1,130 @@
+package org.apache.maven.xml.sax.filter;
+
+/*
+ * 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 org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.ext.LexicalHandler;
+import org.xml.sax.helpers.XMLFilterImpl;
+
+/**
+ * XMLFilter with LexicalHandler.
+ * Since some filters collect events before processing them, the LexicalHandler events must be collected too.
+ * Otherwise the LexicalHandler events might end up before all collected XMLReader events.
+ * 
+ * @author Robert Scholte
+ * @since 3.7.0 
+ */
+public class AbstractSAXFilter extends XMLFilterImpl implements LexicalHandler
+{
+    private LexicalHandler lexicalHandler;
+    
+    AbstractSAXFilter()
+    {
+        super();
+    }
+
+    public <T extends XMLReader & LexicalHandler> AbstractSAXFilter( T parent )
+    {
+        setParent( parent );
+        setLexicalHandler( parent );
+    }
+    
+    public LexicalHandler getLexicalHandler()
+    {
+        return lexicalHandler;
+    }
+    
+    public void setLexicalHandler( LexicalHandler lexicalHandler )
+    {
+        this.lexicalHandler = lexicalHandler;
+    }
+    
+    @Override
+    public void startDTD( String name, String publicId, String systemId )
+        throws SAXException
+    {
+        if ( lexicalHandler != null ) 
+        {
+            lexicalHandler.startDTD( name, publicId, systemId );
+        }
+    }
+
+    @Override
+    public void endDTD()
+        throws SAXException
+    {
+        if ( lexicalHandler != null ) 
+        {
+            lexicalHandler.endDTD();
+        }
+    }
+
+    @Override
+    public void startEntity( String name )
+        throws SAXException
+    {
+        if ( lexicalHandler != null ) 
+        {
+            lexicalHandler.startEntity( name );
+        }
+    }
+
+    @Override
+    public void endEntity( String name )
+        throws SAXException
+    {
+        if ( lexicalHandler != null ) 
+        {
+            lexicalHandler.endEntity( name );
+        }
+    }
+
+    @Override
+    public void startCDATA()
+        throws SAXException
+    {
+        if ( lexicalHandler != null ) 
+        {
+            lexicalHandler.startCDATA();
+        }
+    }
+
+    @Override
+    public void endCDATA()
+        throws SAXException
+    {
+        if ( lexicalHandler != null ) 
+        {
+            lexicalHandler.endCDATA();
+        }
+    }
+
+    @Override
+    public void comment( char[] ch, int start, int length )
+        throws SAXException
+    {
+        if ( lexicalHandler != null ) 
+        {
+            lexicalHandler.comment( ch, start, length );
+        }
+    }
+
+}
diff --git a/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/BuildPomXMLFilter.java b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/BuildPomXMLFilter.java
new file mode 100644
index 0000000..14bcf70
--- /dev/null
+++ b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/BuildPomXMLFilter.java
@@ -0,0 +1,58 @@
+package org.apache.maven.xml.sax.filter;
+
+/*
+ * 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 org.xml.sax.XMLReader;
+import org.xml.sax.ext.LexicalHandler;
+
+/**
+ * Filter to adjust pom on filesystem before being processed for effective pom.
+ * There should only be 1 BuildPomXMLFilter, so the same is being used by both
+ * org.apache.maven.model.building.DefaultModelBuilder.transformData(InputStream) and
+ * org.apache.maven.internal.aether.DefaultRepositorySystemSessionFactory.newFileTransformerManager()
+ * 
+ * 
+ * @author Robert Scholte
+ * @since 3.7.0
+ */
+public class BuildPomXMLFilter extends AbstractSAXFilter 
+{
+    BuildPomXMLFilter()
+    {
+        super();
+    }
+
+    <T extends XMLReader & LexicalHandler> BuildPomXMLFilter( T parent )
+    {
+        super( parent );
+    }
+    
+    /**
+     * Don't allow overwriting parent
+     */
+    @Override
+    public final void setParent( XMLReader parent )
+    {
+        if ( getParent() == null )
+        {
+            super.setParent( parent );
+        }
+    }
+}
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
new file mode 100644
index 0000000..6f3f319
--- /dev/null
+++ b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/BuildPomXMLFilterFactory.java
@@ -0,0 +1,112 @@
+package org.apache.maven.xml.sax.filter;
+
+/*
+ * 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.Optional;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.sax.SAXTransformerFactory;
+
+import org.apache.maven.xml.Factories;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.ext.LexicalHandler;
+
+/**
+ * Base implementation for providing the BuildPomXML.
+ * 
+ * @author Robert Scholte
+ * @since 3.7.0
+ */
+public class BuildPomXMLFilterFactory
+{
+    /**
+     * 
+     * @param projectFile will be used by ConsumerPomXMLFilter to get the right filter
+     * @return
+     * @throws SAXException
+     * @throws ParserConfigurationException
+     * @throws TransformerConfigurationException
+     */
+    public final BuildPomXMLFilter get( Path projectFile )
+        throws SAXException, ParserConfigurationException, TransformerConfigurationException
+    {
+        AbstractSAXFilter parent = new AbstractSAXFilter();
+        parent.setParent( getXMLReader() );
+        parent.setLexicalHandler( getLexicalHander() );
+
+        if ( getDependencyKeyToVersionMapper() != null )
+        {
+            ReactorDependencyXMLFilter reactorDependencyXMLFilter =
+                new ReactorDependencyXMLFilter( getDependencyKeyToVersionMapper() );
+            reactorDependencyXMLFilter.setParent( parent );
+            reactorDependencyXMLFilter.setLexicalHandler( parent );
+            parent = reactorDependencyXMLFilter;
+        }
+
+        if ( getRelativePathMapper() != null )
+        {
+            ParentXMLFilter parentFilter = new ParentXMLFilter( getRelativePathMapper() );
+            parentFilter.setProjectPath( projectFile.getParent() );
+            parentFilter.setParent( parent );
+            parentFilter.setLexicalHandler( parent );
+            parent = parentFilter;
+        }
+
+        return new BuildPomXMLFilter( parent );
+    }
+    
+    private XMLReader getXMLReader() throws SAXException, ParserConfigurationException 
+    {
+        XMLReader xmlReader = Factories.newXMLReader();
+        xmlReader.setFeature( "http://xml.org/sax/features/namespaces", true );
+        return xmlReader;
+    }
+    
+    private LexicalHandler getLexicalHander() throws TransformerConfigurationException 
+    {
+        TransformerFactory transformerFactory = Factories.newTransformerFactory();
+        if ( transformerFactory instanceof SAXTransformerFactory )
+        {
+            SAXTransformerFactory saxTransformerFactory = (SAXTransformerFactory) transformerFactory;
+            return saxTransformerFactory.newTransformerHandler();
+        }
+        throw new TransformerConfigurationException( "Failed to get LexicalHandler via TransformerFactory:"
+            + " it is not an instance of SAXTransformerFactory" );
+    }
+    
+    /**
+     * @return the mapper or {@code null} if relativePaths don't need to be mapped
+     */
+    protected Function<Path, Optional<RelativeProject>> getRelativePathMapper()
+    {
+        return null;
+    }
+    
+    protected BiFunction<String, String, String> getDependencyKeyToVersionMapper()
+    {
+        return null;
+    }
+}
diff --git a/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/BuildPomXMLFilterListener.java b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/BuildPomXMLFilterListener.java
new file mode 100644
index 0000000..b97c757
--- /dev/null
+++ b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/BuildPomXMLFilterListener.java
@@ -0,0 +1,42 @@
+package org.apache.maven.xml.sax.filter;
+
+/*
+ * 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;
+
+/**
+ * Listener can be used to capture the result of the build pom
+ * 
+ * @author Robert Scholte
+ * @since 3.7.0
+ */
+@FunctionalInterface
+public interface BuildPomXMLFilterListener
+{
+    /**
+     * Captures the result of the XML transformation
+     * 
+     * @param pomFile the original to being transformed
+     * @param b the byte array
+     * @param off the offset
+     * @param len the length
+     */
+    void write( Path pomFile, byte[] b, int off, int len );
+}
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
new file mode 100644
index 0000000..a13e4d4
--- /dev/null
+++ b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/CiFriendlyXMLFilter.java
@@ -0,0 +1,83 @@
+package org.apache.maven.xml.sax.filter;
+
+/*
+ * 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.util.function.Function;
+
+import org.xml.sax.SAXException;
+
+/**
+ * Resolves all ci-friendly properties occurrences
+ * 
+ * @author Robert Scholte
+ * @since 3.7.0
+ */
+class CiFriendlyXMLFilter
+    extends AbstractSAXFilter
+{
+    private Function<String, String> replaceChain = Function.identity();
+    
+    public CiFriendlyXMLFilter setChangelist( String changelist )
+    {
+        replaceChain = replaceChain.andThen( t -> t.replace( "${changelist}", changelist ) );
+        return this;
+    }
+    
+    public CiFriendlyXMLFilter setRevision( String revision )
+    {
+        replaceChain = replaceChain.andThen( t -> t.replace( "${revision}", revision ) );
+        return this;
+    }
+
+    public CiFriendlyXMLFilter setSha1( String sha1 )
+    {
+        replaceChain = replaceChain.andThen( t -> t.replace( "${sha1}", sha1 ) );
+        return this;
+    }
+    
+    /**
+     * @return {@code true} is any of the ci properties is set, otherwise {@code false}
+     */
+    public boolean isSet()
+    {
+        return !replaceChain.equals( Function.identity() );
+    }
+    
+    @Override
+    public void characters( char[] ch, int start, int length )
+        throws SAXException
+    {
+        String text = new String( ch, start, length );
+
+        // assuming this has the best performance
+        if ( text.contains( "${" ) )
+        {
+            String newText = replaceChain.apply( text );
+            
+            super.characters( newText.toCharArray(), 0, newText.length() );
+        }
+        else
+        {
+            super.characters( ch, start, length );
+        }
+    }
+    
+    
+}
\ No newline at end of file
diff --git a/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/ConsumerPomXMLFilter.java b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/ConsumerPomXMLFilter.java
new file mode 100644
index 0000000..1c227a7
--- /dev/null
+++ b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/ConsumerPomXMLFilter.java
@@ -0,0 +1,54 @@
+package org.apache.maven.xml.sax.filter;
+
+/*
+ * 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 org.xml.sax.XMLReader;
+import org.xml.sax.ext.LexicalHandler;
+
+/**
+ * XML Filter to transform pom.xml to consumer pom.
+ * This often means stripping of build-specific information.
+ * When extra information is required during filtering it is probably a member of the BuildPomXMLFilter
+ * 
+ * This filter is used at 1 locations:
+ * - {@link org.apache.maven.internal.aether.DefaultRepositorySystemSessionFactory} when publishing pom files.
+ * 
+ * @author Robert Scholte
+ * @since 3.7.0
+ */
+public class ConsumerPomXMLFilter extends AbstractSAXFilter
+{
+    <T extends XMLReader & LexicalHandler> ConsumerPomXMLFilter( T filter )
+    {
+        super( filter );
+    }
+    
+    /**
+     * Don't allow overwriting parent
+     */
+    @Override
+    public final void setParent( XMLReader parent )
+    {
+        if ( getParent() == null )
+        {
+            super.setParent( parent );
+        }
+    }
+}
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
new file mode 100644
index 0000000..f7751d2
--- /dev/null
+++ b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/ConsumerPomXMLFilterFactory.java
@@ -0,0 +1,89 @@
+package org.apache.maven.xml.sax.filter;
+
+/*
+ * 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.Optional;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.TransformerConfigurationException;
+
+import org.xml.sax.SAXException;
+
+/**
+ * 
+ * @author Robert Scholte
+ * @since 3.7.0
+ */
+public class ConsumerPomXMLFilterFactory
+{
+    private BuildPomXMLFilterFactory buildPomXMLFilterFactory;
+    
+    public ConsumerPomXMLFilterFactory( BuildPomXMLFilterFactory buildPomXMLFilterFactory )
+    {
+        this.buildPomXMLFilterFactory = buildPomXMLFilterFactory;
+    }
+    
+    public final ConsumerPomXMLFilter get( Path projectPath )
+        throws SAXException, ParserConfigurationException, TransformerConfigurationException
+    {
+        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();
+        getChangelist().ifPresent( ciFriendlyFilter::setChangelist  );
+        getRevision().ifPresent( ciFriendlyFilter::setRevision );
+        getSha1().ifPresent( ciFriendlyFilter::setSha1 );
+        
+        if ( ciFriendlyFilter.isSet() )
+        {
+            ciFriendlyFilter.setParent( parent );
+            ciFriendlyFilter.setLexicalHandler( parent );
+            filter = ciFriendlyFilter;
+        }
+        
+        // Strip modules
+        filter = new ModulesXMLFilter( filter );
+        // Adjust relativePath
+        filter = new RelativePathXMLFilter( filter );
+        
+        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/DependencyKey.java b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/DependencyKey.java
new file mode 100644
index 0000000..1168351
--- /dev/null
+++ b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/DependencyKey.java
@@ -0,0 +1,92 @@
+package org.apache.maven.xml.sax.filter;
+
+/*
+ * 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.util.Objects;
+
+/**
+ * 
+ * @author Robert Scholte
+ * @since 3.7.0
+ */
+public class DependencyKey
+{
+    private final String groupId;
+    
+    private final String artifactId;
+    
+    private final int hashCode;
+
+    public DependencyKey( String groupId, String artifactId )
+    {
+        this.groupId = groupId;
+        this.artifactId = artifactId;
+        
+        this.hashCode = Objects.hash( artifactId, groupId );
+    }
+ 
+    public String getGroupId()
+    {
+        return groupId;
+    }
+    
+    public String getArtifactId()
+    {
+        return artifactId;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return hashCode;
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj ) 
+        {
+            return true;
+        }
+        if ( obj == null )
+        {
+            return false;
+        }
+        if ( getClass() != obj.getClass() )
+        {
+            return false;
+        }
+        
+        DependencyKey other = (DependencyKey) obj;
+        
+        if ( !Objects.equals( artifactId, other.artifactId ) )
+        {
+            return false;
+        }
+        if ( !Objects.equals( groupId, other.groupId ) )
+        {
+            return false;
+        }
+        
+        return true;
+    }
+    
+    
+}
diff --git a/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/FastForwardFilter.java b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/FastForwardFilter.java
new file mode 100644
index 0000000..0100e6b
--- /dev/null
+++ b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/FastForwardFilter.java
@@ -0,0 +1,128 @@
+package org.apache.maven.xml.sax.filter;
+
+/*
+ * 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.util.ArrayDeque;
+import java.util.Deque;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLFilter;
+import org.xml.sax.XMLReader;
+import org.xml.sax.ext.LexicalHandler;
+
+/**
+ * This filter will skip all following filters and write directly to the output.
+ * Should be used in case of a DOM that should not be effected by other filters, even though the elements match 
+ * 
+ * @author Robert Scholte
+ * @since 3.7.0
+ */
+class FastForwardFilter extends AbstractSAXFilter
+{
+    /**
+     * DOM elements of pom
+     * 
+     * <ul>
+     *  <li>execution.configuration</li>
+     *  <li>plugin.configuration</li>
+     *  <li>plugin.goals</li>
+     *  <li>profile.reports</li>
+     *  <li>project.reports</li>
+     *  <li>reportSet.configuration</li>
+     * <ul>
+     */
+    private final Deque<String> state = new ArrayDeque<>();
+    
+    private int domDepth = 0;
+    
+    private ContentHandler originalHandler;
+    
+    FastForwardFilter()
+    {
+        super();
+    }
+
+    <T extends XMLReader & LexicalHandler> FastForwardFilter( T parent )
+    {
+        super( parent );
+    }
+
+    @Override
+    public void startElement( String uri, String localName, String qName, Attributes atts )
+        throws SAXException
+    {
+        super.startElement( uri, localName, qName, atts );
+        if ( domDepth > 0 )
+        {
+            domDepth++;
+        }
+        else
+        {
+            final String key = state.peek() + '.' + localName;
+            switch ( key )
+            {
+                case "execution.configuration":
+                case "plugin.configuration":
+                case "plugin.goals":
+                case "profile.reports":
+                case "project.reports":
+                case "reportSet.configuration":
+                    domDepth++;
+
+                    originalHandler = getContentHandler();
+
+                    ContentHandler outputContentHandler = getContentHandler();
+                    while ( outputContentHandler instanceof XMLFilter )
+                    {
+                        outputContentHandler = ( (XMLFilter) outputContentHandler ).getContentHandler();
+                    }
+                    setContentHandler( outputContentHandler );
+                    break;
+                default:
+                    break;
+            }
+            state.push( localName );
+        }
+    }
+    
+    @Override
+    public void endElement( String uri, String localName, String qName )
+        throws SAXException
+    {
+        if ( domDepth > 0 )
+        {
+            domDepth--;
+            
+            if ( domDepth == 0 )
+            {
+                setContentHandler( originalHandler );
+            }
+        }
+        else
+        {
+            state.pop();
+        }
+        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/ModulesXMLFilter.java b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/ModulesXMLFilter.java
new file mode 100644
index 0000000..261c853
--- /dev/null
+++ b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/ModulesXMLFilter.java
@@ -0,0 +1,111 @@
+package org.apache.maven.xml.sax.filter;
+
+/*
+ * 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 org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.ext.LexicalHandler;
+
+/**
+ * Remove all modules, this is just buildtime information
+ * 
+ * @author Robert Scholte
+ * @since 3.7.0
+ */
+class ModulesXMLFilter
+    extends AbstractEventXMLFilter
+{
+    private boolean parsingModules;
+    
+    private String state;
+    
+    ModulesXMLFilter()
+    {
+        super();
+    }
+
+    <T extends XMLReader & LexicalHandler> ModulesXMLFilter( T parent )
+    {
+        super( parent );
+    }
+
+    @Override
+    public void startElement( String uri, String localName, String qName, Attributes atts )
+        throws SAXException
+    {
+        if ( !parsingModules && "modules".equals( localName ) )
+        {
+            parsingModules = true;
+        }
+
+        if ( parsingModules )
+        {
+            state = localName;
+        }
+        
+        super.startElement( uri, localName, qName, atts );
+    }
+    
+    @Override
+    public void endElement( String uri, String localName, String qName )
+        throws SAXException
+    {
+        if ( parsingModules )
+        {
+            switch ( localName )
+            {
+                case "modules":
+                    executeEvents();
+                    
+                    parsingModules = false;
+                    break;
+                default:
+                    super.endElement( uri, localName, qName );
+                    break;
+            }
+        }
+        else
+        {
+            super.endElement( uri, localName, qName );
+        }
+        
+        // for this simple structure resetting to modules it sufficient
+        state = "modules"; 
+    }
+    
+    @Override
+    protected boolean isParsing()
+    {
+        return parsingModules;
+    }
+
+    @Override
+    protected String getState()
+    {
+        return state;
+    }
+
+    @Override
+    protected boolean acceptEvent( String state )
+    {
+        return false;
+    }
+}
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
new file mode 100644
index 0000000..df43ec1
--- /dev/null
+++ b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/ParentXMLFilter.java
@@ -0,0 +1,210 @@
+package org.apache.maven.xml.sax.filter;
+
+/*
+ * 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.nio.file.Paths;
+
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+
+import org.apache.maven.xml.sax.SAXEventUtils;
+import org.xml.sax.Attributes;
+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 
+ * </p>
+ * 
+ * @author Robert Scholte
+ * @since 3.7.0
+ */
+class ParentXMLFilter
+    extends AbstractEventXMLFilter
+{
+    private boolean parsingParent;
+
+    // states
+    private String state;
+
+    // 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 Optional<RelativeProject> resolvedParent;
+
+    private final Function<Path, Optional<RelativeProject>> relativePathMapper;
+    
+    private Path projectPath;
+
+    /**
+     * 
+     * 
+     * @param relativePathMapper
+     */
+    ParentXMLFilter( Function<Path, Optional<RelativeProject>> relativePathMapper )
+    {
+        this.relativePathMapper = relativePathMapper;
+    }
+
+    public void setProjectPath( Path projectPath )
+    {
+        this.projectPath = projectPath;
+    }
+    
+    @Override
+    protected boolean isParsing()
+    {
+        return parsingParent;
+    }
+
+    @Override
+    protected String getState()
+    {
+        return state;
+    }
+    
+    @Override
+    public void startElement( String uri, final String localName, String qName, Attributes atts )
+        throws SAXException
+    {
+        if ( !parsingParent && "parent".equals( localName ) )
+        {
+            parsingParent = true;
+        }
+
+        if ( parsingParent )
+        {
+            state = localName;
+            
+            hasVersion |= "version".equals( localName );
+        }
+        
+        super.startElement( uri, localName, qName, atts );
+    }
+
+    @Override
+    public void characters( char[] ch, int start, int length )
+        throws SAXException
+    {
+        if ( parsingParent )
+        {
+            final String eventState = state;
+            
+            switch ( eventState )
+            {
+                case "parent":
+                    parentWhitespace = new String( ch, start, length );
+                    break;
+                case "relativePath":
+                    relativePath = new String( ch, start, length );
+                    break;
+                case "groupId":
+                    groupId = new String( ch, start, length );
+                    break;
+                case "artifactId":
+                    artifactId = new String( ch, start, length );
+                    break;
+                default:
+                    break;
+            }
+        }
+        
+        super.characters( ch, start, length );
+    }
+
+    @Override
+    public void endElement( String uri, final String localName, String qName )
+        throws SAXException
+    {
+        if ( parsingParent )
+        {
+            switch ( localName )
+            {
+                case "parent":
+                    if ( !hasVersion || relativePath != null )
+                    {
+                        resolvedParent =
+                            resolveRelativePath( Paths.get( Objects.toString( relativePath, "../pom.xml" ) ) );
+                    }
+                    
+                    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:
+                    // marker?
+                    break;
+            }
+        }
+        
+        super.endElement( uri, localName, qName );
+        state = "";
+    }
+
+    protected Optional<RelativeProject> resolveRelativePath( Path relativePath )
+    {
+        Optional<RelativeProject> mappedProject =
+            relativePathMapper.apply( projectPath.resolve( relativePath ).normalize() );
+        
+        if ( mappedProject.isPresent() )
+        {
+            RelativeProject project = mappedProject.get();
+            
+            if ( Objects.equals( groupId, project.getGroupId() )
+                && Objects.equals( artifactId, project.getArtifactId() ) )
+            {
+                return mappedProject;
+            }
+        }
+        return Optional.empty();
+    }
+}
diff --git a/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/ReactorDependencyXMLFilter.java b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/ReactorDependencyXMLFilter.java
new file mode 100644
index 0000000..38f8fb8
--- /dev/null
+++ b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/ReactorDependencyXMLFilter.java
@@ -0,0 +1,165 @@
+package org.apache.maven.xml.sax.filter;
+
+/*
+ * 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.util.function.BiFunction;
+
+import org.apache.maven.xml.sax.SAXEventUtils;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+
+/**
+ * Will apply the version if the dependency is part of the reactor
+ * 
+ * @author Robert Scholte
+ * @since 3.7.0
+ */
+public class ReactorDependencyXMLFilter extends AbstractEventXMLFilter
+{
+    private boolean parsingDependency;
+
+    // states
+    private String state;
+    
+    // whiteSpace after <dependency>, to be used to position <version>
+    private String dependencyWhitespace = "";
+
+    private boolean hasVersion;
+
+    private String groupId;
+
+    private String artifactId;
+
+    private final BiFunction<String, String, String> reactorVersionMapper;
+
+    public ReactorDependencyXMLFilter( BiFunction<String, String, String> reactorVersionMapper )
+    {
+        this.reactorVersionMapper = reactorVersionMapper;
+    }
+
+    @Override
+    public void startElement( String uri, String localName, String qName, Attributes atts )
+        throws SAXException
+    {
+        if ( !parsingDependency && "dependency".equals( localName ) )
+        {
+            parsingDependency = true;
+        }
+        
+        if ( parsingDependency )
+        {
+            state = localName;
+
+            hasVersion |= "version".equals( localName );
+        }
+        super.startElement( uri, localName, qName, atts );
+    }
+
+    @Override
+    public void characters( char[] ch, int start, int length )
+        throws SAXException
+    {
+        if ( parsingDependency )
+        {
+            final String eventState = state;
+            switch ( eventState )
+            {
+                case "dependency":
+                    dependencyWhitespace = new String( ch, start, length );
+                    break;
+                case "groupId":
+                    groupId = new String( ch, start, length );
+                    break;
+                case "artifactId":
+                    artifactId = new String( ch, start, length );
+                    break;
+                default:
+                    break;
+            }
+        }
+        super.characters( ch, start, length );
+    }
+
+    @Override
+    public void endElement( String uri, final String localName, String qName )
+        throws SAXException
+    {
+        if ( parsingDependency )
+        {
+            switch ( localName )
+            {
+                case "dependency":
+                    if ( !hasVersion )
+                    {
+                        String version = getVersion();
+
+                        // dependency is not part of reactor, probably it is managed
+                        if ( version != null )
+                        {
+                            try ( Includer i = super.include() )
+                            {
+                                super.characters( dependencyWhitespace.toCharArray(), 0,
+                                                  dependencyWhitespace.length() );
+                                String versionQName = SAXEventUtils.renameQName( qName, "version" );
+                                
+                                super.startElement( uri, "version", versionQName, null );
+                                super.characters( version.toCharArray(), 0, version.length() );
+                                super.endElement( uri, "version", versionQName );
+                            }
+                        }
+                    }
+                    super.executeEvents();
+                    
+                    parsingDependency = false;
+                    
+                    // reset
+                    hasVersion = false;
+                    groupId = null;
+                    artifactId = null;
+                    
+                    break;
+                default: 
+                    break;
+            }
+        }
+
+        super.endElement( uri, localName, qName );
+        
+        state = "";
+    }
+
+    private String getVersion()
+    {
+        return reactorVersionMapper.apply( groupId, artifactId  );
+    }
+
+    @Override
+    protected boolean isParsing()
+    {
+        return parsingDependency;
+    }
+
+    @Override
+    protected String getState()
+    {
+        return state;
+    }
+    
+}
diff --git a/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/RelativePathXMLFilter.java b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/RelativePathXMLFilter.java
new file mode 100644
index 0000000..25f2137
--- /dev/null
+++ b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/RelativePathXMLFilter.java
@@ -0,0 +1,108 @@
+package org.apache.maven.xml.sax.filter;
+
+/*
+ * 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 org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.ext.LexicalHandler;
+
+/**
+ * Remove relativePath element, has no value for consumer pom
+ * 
+ * @author Robert Scholte
+ * @since 3.7.0
+ */
+class RelativePathXMLFilter
+    extends AbstractEventXMLFilter
+{
+    private boolean parsingParent;
+    
+    private String state;
+    
+    RelativePathXMLFilter()
+    {
+        super();
+    }
+
+    <T extends XMLReader & LexicalHandler> RelativePathXMLFilter( T parent )
+    {
+        super( parent );
+    }
+    
+    @Override
+    public void startElement( String uri, final String localName, String qName, Attributes atts )
+        throws SAXException
+    {
+        if ( !parsingParent && "parent".equals( localName ) )
+        {
+            parsingParent = true;
+        }
+
+        if ( parsingParent )
+        {
+            state = localName;
+        }
+        
+        super.startElement( uri, localName, qName, atts );
+    }
+    
+    @Override
+    public void endElement( String uri, String localName, String qName )
+        throws SAXException
+    {
+        if ( parsingParent )
+        {
+            switch ( localName )
+            {
+                case "parent":
+                    executeEvents();
+                    
+                    parsingParent = false;
+                    break;
+                default:
+                    break;
+            }
+        }
+        
+        super.endElement( uri, localName, qName );
+
+        // for this simple structure resetting to parent it sufficient
+        state = "parent";
+    }
+
+    @Override
+    protected boolean isParsing()
+    {
+        return parsingParent;
+    }
+
+    @Override
+    protected String getState()
+    {
+        return state;
+    }
+
+    @Override
+    protected boolean acceptEvent( String state )
+    {
+        return !"relativePath".equals( state );
+    }
+}
diff --git a/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/RelativeProject.java b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/RelativeProject.java
new file mode 100644
index 0000000..067e170
--- /dev/null
+++ b/maven-xml/src/main/java/org/apache/maven/xml/sax/filter/RelativeProject.java
@@ -0,0 +1,56 @@
+package org.apache.maven.xml.sax.filter;
+
+/*
+ * 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.
+ */
+
+/**
+ * 
+ * @author Robert Scholte
+ * @since 3.7.0
+ */
+public class RelativeProject
+{
+    private final String groupId;
+    
+    private final String artifactId;
+    
+    private final String version;
+    
+    public RelativeProject( String groupId, String artifactId, String version )
+    {
+        this.groupId = groupId;
+        this.artifactId = artifactId;
+        this.version = version;
+    }
+
+    public String getGroupId()
+    {
+        return groupId;
+    }
+    
+    public String getArtifactId()
+    {
+        return artifactId;
+    }
+    
+    public String getVersion()
+    {
+        return version;
+    }
+}
diff --git a/maven-xml/src/test/java/org/apache/maven/xml/sax/SAXEventUtilsTest.java b/maven-xml/src/test/java/org/apache/maven/xml/sax/SAXEventUtilsTest.java
new file mode 100644
index 0000000..02e55dc
--- /dev/null
+++ b/maven-xml/src/test/java/org/apache/maven/xml/sax/SAXEventUtilsTest.java
@@ -0,0 +1,43 @@
+package org.apache.maven.xml.sax;
+
+/*
+ * 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 static org.junit.Assert.assertThat;
+
+import org.apache.maven.xml.sax.SAXEventUtils;
+
+import static org.hamcrest.CoreMatchers.is;
+
+import org.junit.Test;
+
+public class SAXEventUtilsTest
+{
+    @Test
+    public void replaceWithNamespace()
+    {
+        assertThat( SAXEventUtils.renameQName( "org:bar", "com" ), is( "org:com" ) );
+    }
+
+    @Test
+    public void replaceWithoutNamespace()
+    {
+        assertThat( SAXEventUtils.renameQName( "bar", "com" ), is( "com" ) );
+    }
+}
diff --git a/maven-xml/src/test/java/org/apache/maven/xml/sax/ext/CommentRenormalizerTest.java b/maven-xml/src/test/java/org/apache/maven/xml/sax/ext/CommentRenormalizerTest.java
new file mode 100644
index 0000000..b6bc381
--- /dev/null
+++ b/maven-xml/src/test/java/org/apache/maven/xml/sax/ext/CommentRenormalizerTest.java
@@ -0,0 +1,84 @@
+package org.apache.maven.xml.sax.ext;
+
+/*
+ * 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 static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.xml.sax.ext.LexicalHandler;
+
+@RunWith( Parameterized.class )
+public class CommentRenormalizerTest
+{
+    private LexicalHandler lexicalHandler;
+    
+    private final String lineSeparator;
+    
+    @Parameters
+    public static Collection<Object[]> data() {
+        return Arrays.asList(new Object[][] {     
+                 { "\n" }, 
+                 { "\r\n" },  
+                 { "\r" }
+           });
+    }
+    
+    public CommentRenormalizerTest( String lineSeparator )
+    {
+        this.lineSeparator = lineSeparator;
+        this.lexicalHandler = mock( LexicalHandler.class );
+    }
+
+    @Test
+    public void singleLine()
+        throws Exception
+    {
+        CommentRenormalizer commentRenormalizer = new CommentRenormalizer( lexicalHandler, lineSeparator );
+
+        char[] ch = "single line".toCharArray();
+
+        commentRenormalizer.comment( ch, 0, ch.length );
+
+        verify( lexicalHandler ).comment( ch, 0, ch.length );
+    }
+    
+    @Test
+    public void multiLine()
+        throws Exception
+    {
+        CommentRenormalizer commentRenormalizer = new CommentRenormalizer( lexicalHandler, lineSeparator );
+
+        String text = "I%sam%sthe%sbest%s";
+
+        char[] chIn = String.format( text, "\n", "\n", "\n", "\n" ).toCharArray();
+        char[] chOut = String.format( text, lineSeparator, lineSeparator, lineSeparator, lineSeparator ).toCharArray();
+
+        commentRenormalizer.comment( chIn, 0, chIn.length );
+
+        verify( lexicalHandler ).comment( chOut, 0, chOut.length );
+    }
+}
diff --git a/maven-xml/src/test/java/org/apache/maven/xml/sax/filter/AbstractXMLFilterTests.java b/maven-xml/src/test/java/org/apache/maven/xml/sax/filter/AbstractXMLFilterTests.java
new file mode 100644
index 0000000..4fa3b0d
--- /dev/null
+++ b/maven-xml/src/test/java/org/apache/maven/xml/sax/filter/AbstractXMLFilterTests.java
@@ -0,0 +1,119 @@
+package org.apache.maven.xml.sax.filter;
+
+/*
+ * 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.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.sax.SAXResult;
+import javax.xml.transform.sax.SAXSource;
+import javax.xml.transform.sax.SAXTransformerFactory;
+import javax.xml.transform.sax.TransformerHandler;
+import javax.xml.transform.stream.StreamResult;
+
+import org.apache.maven.xml.Factories;
+import org.apache.maven.xml.sax.filter.AbstractSAXFilter;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+
+public abstract class AbstractXMLFilterTests
+{
+    public AbstractXMLFilterTests()
+    {
+        super();
+    }
+    
+    protected abstract AbstractSAXFilter getFilter() throws TransformerException, SAXException, ParserConfigurationException;
+    
+    private void setParent( AbstractSAXFilter filter ) throws SAXException, ParserConfigurationException
+    {
+        if( filter.getParent() == null )
+        {
+            XMLReader r = Factories.newXMLReader();
+            
+            filter.setParent( r );
+            filter.setFeature( "http://xml.org/sax/features/namespaces", true );
+        }
+    }
+    
+    protected String omitXmlDeclaration() {
+        return "yes";
+    }
+    
+    protected String indentAmount() {
+        return null;
+    }
+
+    protected String transform( String input )
+        throws TransformerException, SAXException, ParserConfigurationException
+    {
+        return transform( new StringReader( input ) );
+    }
+
+    protected String transform( Reader input ) throws TransformerException, SAXException, ParserConfigurationException
+    {
+        AbstractSAXFilter filter = getFilter();
+        setParent( filter );
+
+        return transform( input, filter );
+    }
+    
+    protected String transform( String input, AbstractSAXFilter filter ) 
+        throws TransformerException, SAXException, ParserConfigurationException
+    {
+        setParent( filter );
+        return transform( new StringReader( input ), filter );
+    }
+
+    protected String transform( Reader input, AbstractSAXFilter filter )
+        throws TransformerException, SAXException, ParserConfigurationException
+    {
+        Writer writer = new StringWriter();
+        StreamResult result = new StreamResult( writer );
+
+        SAXTransformerFactory transformerFactory = (SAXTransformerFactory) Factories.newTransformerFactory();
+        TransformerHandler transformerHandler = transformerFactory.newTransformerHandler();
+        filter.setLexicalHandler( transformerHandler );
+        transformerHandler.getTransformer().setOutputProperty( OutputKeys.OMIT_XML_DECLARATION, omitXmlDeclaration() );
+        if ( indentAmount() != null )
+        {
+            transformerHandler.getTransformer().setOutputProperty( OutputKeys.INDENT, "yes" );
+            transformerHandler.getTransformer().setOutputProperty( "{http://xml.apache.org/xslt}indent-amount",
+                                                                   indentAmount() );
+        }
+        transformerHandler.setResult( result );
+        Transformer transformer = transformerFactory.newTransformer();
+
+        SAXSource transformSource = new SAXSource( filter, new InputSource( input ) );
+
+        SAXResult transformResult = new SAXResult( transformerHandler );
+        transformResult.setLexicalHandler( filter );
+        transformer.transform( transformSource, transformResult );
+
+        return writer.toString();
+    }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..16d458b
--- /dev/null
+++ b/maven-xml/src/test/java/org/apache/maven/xml/sax/filter/ConsumerPomXMLFilterTest.java
@@ -0,0 +1,235 @@
+package org.apache.maven.xml.sax.filter;
+
+/*
+ * 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 static org.xmlunit.assertj.XmlAssert.assertThat;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Optional;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.TransformerConfigurationException;
+
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+public class ConsumerPomXMLFilterTest extends AbstractXMLFilterTests
+{
+    @Override
+    protected String omitXmlDeclaration()
+    {
+        return "no";
+    }
+    
+    @Override
+    protected AbstractSAXFilter getFilter() throws SAXException, ParserConfigurationException, TransformerConfigurationException
+    {
+        final BuildPomXMLFilterFactory buildPomXMLFilterFactory = new BuildPomXMLFilterFactory()
+        {
+            @Override
+            protected Function<Path, Optional<RelativeProject>> getRelativePathMapper()
+            {
+                return null;
+            }
+            
+            @Override
+            protected BiFunction<String, String, String> getDependencyKeyToVersionMapper()
+            {
+                return null;
+            }
+        };
+        
+        ConsumerPomXMLFilter filter = new ConsumerPomXMLFilterFactory( buildPomXMLFilterFactory )
+        {
+            @Override
+            protected Optional<String> getSha1()
+            {
+                return Optional.empty();
+            }
+            
+            @Override
+            protected Optional<String> getRevision()
+            {
+                return Optional.empty();
+            }
+            
+            @Override
+            protected Optional<String> getChangelist()
+            {
+                return Optional.of( "CL" );
+            }
+        }.get( Paths.get( "pom.xml" ) );
+        filter.setFeature( "http://xml.org/sax/features/namespaces", true );
+        return filter;
+    }
+    
+    @Test
+    public void aggregatorWithParent() throws Exception {
+        String input = "<project>\n"
+                     + "  <parent>\n"
+                     + "    <groupId>GROUPID</groupId>\n"
+                     + "    <artifactId>PARENT</artifactId>\n"
+                     + "    <version>VERSION</version>\n"
+                     + "    <relativePath>../pom.xml</relativePath>\n"
+                     + "  </parent>\n"
+                     + "  <artifactId>PROJECT</artifactId>\n"
+                     + "  <modules>\n"
+                     + "    <module>ab</module>\n"
+                     + "    <module>../cd</module>\n"
+                     + "  </modules>\n"
+                     + "</project>";
+        String expected = "<project>\n"
+                        + "  <parent>\n"
+                        + "    <groupId>GROUPID</groupId>\n"
+                        + "    <artifactId>PARENT</artifactId>\n"
+                        + "    <version>VERSION</version>\n"
+                        + "  </parent>\n"
+                        + "  <artifactId>PROJECT</artifactId>\n"
+                        + "</project>";
+        String actual = transform( input );
+        assertThat( actual ).and( expected ).ignoreWhitespace().areIdentical();
+    }
+    
+    @Test
+    public void aggregatorWithCliFriendlyVersion() throws Exception {
+        String input = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + 
+            "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n" +
+            "       xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" +
+            "       xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0\n" +
+            "                           http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n" + 
+            "  <modelVersion>4.0.0</modelVersion>\n" + 
+            "  <groupId>org.sonatype.mavenbook.multispring</groupId>\n" + 
+            "  <artifactId>parent</artifactId>\n" + 
+            "  <version>0.9-${changelist}-SNAPSHOT</version>\n" + 
+            "  <packaging>pom</packaging>\n" + 
+            "  <name>Multi-Spring Chapter Parent Project</name>\n" + 
+            "  <modules>\n" + 
+            "    <module>simple-parent</module>\n" + 
+            "  </modules>\n" + 
+            "  \n" + 
+            "  <pluginRepositories>\n" + 
+            "    <pluginRepository>\n" + 
+            "      <id>apache.snapshots</id>\n" + 
+            "      <url>http://repository.apache.org/snapshots/</url>\n" + 
+            "    </pluginRepository>\n" + 
+            "  </pluginRepositories>\n" + 
+            "</project>";
+        String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + 
+            "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n" +
+            "       xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" +
+            "       xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0\n" +
+            "                           http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n" + 
+            "  <modelVersion>4.0.0</modelVersion>\n" + 
+            "  <groupId>org.sonatype.mavenbook.multispring</groupId>\n" + 
+            "  <artifactId>parent</artifactId>\n" + 
+            "  <version>0.9-CL-SNAPSHOT</version>\n" + 
+            "  <packaging>pom</packaging>\n" + 
+            "  <name>Multi-Spring Chapter Parent Project</name>\n" + 
+            "  \n" + 
+            "  <pluginRepositories>\n" + 
+            "    <pluginRepository>\n" + 
+            "      <id>apache.snapshots</id>\n" + 
+            "      <url>http://repository.apache.org/snapshots/</url>\n" + 
+            "    </pluginRepository>\n" + 
+            "  </pluginRepositories>\n" + 
+            "</project>";
+        String actual = transform( input );
+        assertThat( actual ).and( expected ).ignoreWhitespace().areIdentical();
+    }
+    
+    @Test
+    public void licenseHeader() throws Exception {
+        String input = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + 
+            "\n" + 
+            "<!--\n" + 
+            "Licensed to the Apache Software Foundation (ASF) under one\n" + 
+            "or more contributor license agreements.  See the NOTICE file\n" + 
+            "distributed with this work for additional information\n" + 
+            "regarding copyright ownership.  The ASF licenses this file\n" + 
+            "to you under the Apache License, Version 2.0 (the\n" + 
+            "\"License\"); you may not use this file except in compliance\n" + 
+            "with the License.  You may obtain a copy of the License at\n" + 
+            "\n" + 
+            "    http://www.apache.org/licenses/LICENSE-2.0\n" + 
+            "\n" + 
+            "Unless required by applicable law or agreed to in writing,\n" + 
+            "software distributed under the License is distributed on an\n" + 
+            "\"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n" + 
+            "KIND, either express or implied.  See the License for the\n" + 
+            "specific language governing permissions and limitations\n" + 
+            "under the License.\n" + 
+            "-->\n" + 
+            "\n" + 
+            "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n" + 
+            "  xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" + 
+            "  xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n" + 
+            "  <modelVersion>4.0.0</modelVersion>\n" + 
+            "  <parent>\n" + 
+            "    <groupId>org.apache.maven</groupId>\n" + 
+            "    <artifactId>maven</artifactId>\n" + 
+            "    <version>3.7.0-SNAPSHOT</version>\n" + 
+            "  </parent>\n" + 
+            "  <artifactId>maven-xml</artifactId>\n" + 
+            "  <name>Maven XML</name>\n" + 
+            "  \n" + 
+            "  <properties>\n" + 
+            "    <maven.compiler.source>1.8</maven.compiler.source>\n" + 
+            "    <maven.compiler.target>1.8</maven.compiler.target>\n" + 
+            "  </properties>\n" + 
+            "\n" + 
+            "  <build>\n" + 
+            "    <plugins>\n" + 
+            "      <plugin>\n" + 
+            "        <groupId>org.codehaus.mojo</groupId>\n" + 
+            "        <artifactId>animal-sniffer-maven-plugin</artifactId>\n" + 
+            "        <configuration>\n" + 
+            "          <signature>\n" + 
+            "            <groupId>org.codehaus.mojo.signature</groupId>\n" + 
+            "            <artifactId>java18</artifactId>\n" + 
+            "            <version>1.0</version>\n" + 
+            "          </signature>\n" + 
+            "        </configuration>\n" + 
+            "      </plugin>\n" + 
+            "    </plugins>\n" + 
+            "  </build>\n" + 
+            "  \n" + 
+            "  <dependencies>\n" + 
+            "    <dependency>\n" + 
+            "      <groupId>javax.inject</groupId>\n" + 
+            "      <artifactId>javax.inject</artifactId>\n" + 
+            "      <optional>true</optional>\n" + 
+            "    </dependency>\n" + 
+            "    <dependency>\n" + 
+            "      <groupId>org.xmlunit</groupId>\n" + 
+            "      <artifactId>xmlunit-assertj</artifactId>\n" + 
+            "      <scope>test</scope>\n" + 
+            "    </dependency>\n" + 
+            "  </dependencies>\n" + 
+            "</project>";
+        String expected = input;
+        
+        String actual = transform( input );
+        assertThat( actual ).and( expected ).areIdentical();
+    }
+
+}
diff --git a/maven-xml/src/test/java/org/apache/maven/xml/sax/filter/ModulesXMLFilterTest.java b/maven-xml/src/test/java/org/apache/maven/xml/sax/filter/ModulesXMLFilterTest.java
new file mode 100644
index 0000000..552b721
--- /dev/null
+++ b/maven-xml/src/test/java/org/apache/maven/xml/sax/filter/ModulesXMLFilterTest.java
@@ -0,0 +1,95 @@
+package org.apache.maven.xml.sax.filter;
+
+/*
+ * 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 static org.xmlunit.assertj.XmlAssert.assertThat;
+
+import org.apache.maven.xml.sax.filter.ModulesXMLFilter;
+import org.junit.Test;
+
+public class ModulesXMLFilterTest extends AbstractXMLFilterTests {
+
+	@Override
+	protected ModulesXMLFilter getFilter()
+	{
+	    return new ModulesXMLFilter();
+	}
+	
+	@Test
+	public void emptyModules() throws Exception {
+		String input = "<project><modules/></project>";
+        String expected = "<project/>";
+        String actual = transform( input );
+        assertThat( actual ).and( expected ).areIdentical();
+	}
+
+	@Test
+	public void setOfModules() throws Exception {
+		String input = "<project><modules>"
+				+ "<module>ab</module>"
+				+ "<module>../cd</module>"
+				+ "</modules></project>";
+		String expected = "<project/>";
+		String actual = transform( input );
+		assertThat( actual ).and( expected ).areIdentical();
+	}
+	
+	@Test
+    public void noModules() throws Exception {
+        String input = "<project><name>NAME</name></project>";
+        String expected = input;
+        String actual = transform( input );
+        assertThat( actual ).and( expected ).areIdentical();
+    }
+	
+	@Test
+	public void comment() throws Exception {
+	    
+	    String input = "<project><!--before--><modules>"
+                        + "<!--pre-in-->"
+	                    + "<module><!--in-->ab</module>"
+	                    + "<module>../cd</module>"
+                        + "<!--post-in-->"
+	                    + "</modules>"
+	                    + "<!--after--></project>";
+	    String expected = "<project><!--before--><!--after--></project>";
+	    String actual = transform( input );
+	    assertThat( actual ).and( expected ).areIdentical();
+	}
+	
+    @Test
+    public void setOfModulesLF() throws Exception {
+        String input = "<project>\n"
+            + "\n"
+            + "  <modules>\n"
+            + "    <module>ab</module>\n"
+            + "    <module>../cd</module>\n"
+            + "  </modules>\n"
+            + "\n"
+            + "</project>\n";
+        String expected = "<project>\n"
+            + "\n"
+            + "  \n"
+            + "\n"
+            + "</project>\n";
+        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
new file mode 100644
index 0000000..bb00222
--- /dev/null
+++ b/maven-xml/src/test/java/org/apache/maven/xml/sax/filter/ParentXMLFilterTest.java
@@ -0,0 +1,215 @@
+package org.apache.maven.xml.sax.filter;
+
+/*
+ * 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 static org.junit.Assert.assertEquals;
+
+import java.nio.file.Paths;
+import java.util.Optional;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.TransformerException;
+
+import org.apache.maven.xml.sax.filter.ParentXMLFilter;
+import org.apache.maven.xml.sax.filter.RelativeProject;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+public class ParentXMLFilterTest extends AbstractXMLFilterTests
+{
+    @Override
+    protected ParentXMLFilter getFilter()
+        throws TransformerException, SAXException, ParserConfigurationException
+    {
+        ParentXMLFilter filter = new ParentXMLFilter( x -> Optional.of( new RelativeProject( "GROUPID", 
+                                                                                           "ARTIFACTID",
+                                                                                           "1.0.0" ) ) );
+        filter.setProjectPath( Paths.get( "pom.xml").toAbsolutePath() );
+        
+        return filter;
+    }
+    
+    @Test
+    public void testMinimum() throws Exception
+    {
+        String input = "<parent/>";
+        String expected = input;
+        String actual = transform( input );
+        assertEquals( expected, actual );
+    }
+
+    @Test
+    public void testNoRelativePath() throws Exception
+    {
+        String input = "<parent>"
+            + "<groupId>GROUPID</groupId>"
+            + "<artifactId>ARTIFACTID</artifactId>"
+            + "<version>VERSION</version>"
+            + "</parent>";
+        String expected = input;
+
+        String actual = transform( input );
+
+        assertEquals( expected, actual );
+    }
+
+    @Test
+    public void testDefaultRelativePath() throws Exception
+    {
+        String input = "<parent>"
+            + "<groupId>GROUPID</groupId>"
+            + "<artifactId>ARTIFACTID</artifactId>"
+            + "</parent>";
+        String expected = "<parent>"
+                        + "<groupId>GROUPID</groupId>"
+                        + "<artifactId>ARTIFACTID</artifactId>"
+                        + "<version>1.0.0</version>"
+                        + "</parent>";
+
+        String actual = transform( input );
+
+        assertEquals( expected, actual );
+    }
+
+    @Test
+    public void testNoVersion() throws Exception
+    {
+        String input = "<parent>"
+            + "<groupId>GROUPID</groupId>"
+            + "<artifactId>ARTIFACTID</artifactId>"
+            + "<relativePath>RELATIVEPATH</relativePath>"
+            + "</parent>";
+        String expected = "<parent>"
+                        + "<groupId>GROUPID</groupId>"
+                        + "<artifactId>ARTIFACTID</artifactId>"
+                        + "<relativePath>RELATIVEPATH</relativePath>"
+                        + "<version>1.0.0</version>"
+                        + "</parent>";
+
+        String actual = transform( input );
+
+        assertEquals( expected, actual );
+    }
+
+    @Test
+    public void testInvalidRelativePath() throws Exception
+    {
+        ParentXMLFilter filter = new ParentXMLFilter( x -> Optional.ofNullable( null ) );
+        filter.setProjectPath( Paths.get( "pom.xml").toAbsolutePath() );
+        
+        String input = "<parent>"
+            + "<groupId>GROUPID</groupId>"
+            + "<artifactId>ARTIFACTID</artifactId>"
+            + "<relativePath>RELATIVEPATH</relativePath>"
+            + "</parent>";
+        String expected = input;
+
+        String actual = transform( input, filter );
+
+        assertEquals( expected, actual );
+    }
+
+    @Test
+    public void testRelativePathAndVersion() throws Exception
+    {
+        String input = "<parent>"
+            + "<groupId>GROUPID</groupId>"
+            + "<artifactId>ARTIFACTID</artifactId>"
+            + "<relativePath>RELATIVEPATH</relativePath>"
+            + "<version>1.0.0</version>"
+            + "</parent>";
+        String expected = "<parent>"
+                        + "<groupId>GROUPID</groupId>"
+                        + "<artifactId>ARTIFACTID</artifactId>"
+                        + "<relativePath>RELATIVEPATH</relativePath>"
+                        + "<version>1.0.0</version>"
+                        + "</parent>";
+
+        String actual = transform( input );
+
+        assertEquals( expected, actual );
+    }
+
+    @Test
+    public void testWithWeirdNamespace() throws Exception
+    {
+        String input = "<relativePath:parent xmlns:relativePath=\"relativePath\">"
+            + "<relativePath:groupId>GROUPID</relativePath:groupId>"
+            + "<relativePath:artifactId>ARTIFACTID</relativePath:artifactId>"
+            + "<relativePath:relativePath>RELATIVEPATH</relativePath:relativePath>"
+            + "</relativePath:parent>";
+        String expected = "<relativePath:parent xmlns:relativePath=\"relativePath\">"
+                        + "<relativePath:groupId>GROUPID</relativePath:groupId>"
+                        + "<relativePath:artifactId>ARTIFACTID</relativePath:artifactId>"
+                        + "<relativePath:relativePath>RELATIVEPATH</relativePath:relativePath>"
+                        + "<relativePath:version>1.0.0</relativePath:version>"
+                        + "</relativePath:parent>";
+
+        String actual = transform( input );
+
+        assertEquals( expected, actual );
+    }
+    
+    @Test
+    public void comment() throws Exception
+    {
+        String input = "<project><!--before--><parent>"
+                    + "<groupId>GROUPID</groupId>"
+                    + "<artifactId>ARTIFACTID</artifactId>"
+                    + "<!--version here-->"
+                    + "</parent>"
+                    + "</project>";
+        String expected = "<project><!--before--><parent>"
+                        + "<groupId>GROUPID</groupId>"
+                        + "<artifactId>ARTIFACTID</artifactId>"
+                        + "<!--version here-->"
+                        + "<version>1.0.0</version>"
+                        + "</parent>"
+                        + "</project>";
+
+        String actual = transform( input );
+
+        assertEquals( expected, actual );
+    }
+    
+    @Test
+    public void testIndent() throws Exception
+    {
+        String input = "<project>\n"
+            + "  <parent>\n"
+            + "    <groupId>GROUPID</groupId>\n"
+            + "    <artifactId>ARTIFACTID</artifactId>\n"
+            + "    <!--version here-->\n"
+            + "  </parent>\n"
+            + "</project>";
+        String expected = "<project>" + System.lineSeparator()
+            + "  <parent>" + System.lineSeparator()
+            + "    <groupId>GROUPID</groupId>" + System.lineSeparator()
+            + "    <artifactId>ARTIFACTID</artifactId>" + System.lineSeparator()
+            + "    <!--version here-->" + System.lineSeparator()
+            + "    <version>1.0.0</version>" + System.lineSeparator()
+            + "  </parent>" + System.lineSeparator()
+            + "</project>";
+
+        String actual = transform( input );
+
+        assertEquals( expected, actual );
+    }
+}
diff --git a/maven-xml/src/test/java/org/apache/maven/xml/sax/filter/ReactorDependencyXMLFilterTest.java b/maven-xml/src/test/java/org/apache/maven/xml/sax/filter/ReactorDependencyXMLFilterTest.java
new file mode 100644
index 0000000..db6606c
--- /dev/null
+++ b/maven-xml/src/test/java/org/apache/maven/xml/sax/filter/ReactorDependencyXMLFilterTest.java
@@ -0,0 +1,145 @@
+package org.apache.maven.xml.sax.filter;
+
+/*
+ * 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 static org.xmlunit.assertj.XmlAssert.assertThat;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.TransformerException;
+
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+public class ReactorDependencyXMLFilterTest extends AbstractXMLFilterTests
+{
+    @Override
+    protected ReactorDependencyXMLFilter getFilter()
+        throws TransformerException, SAXException, ParserConfigurationException
+    {
+        return new ReactorDependencyXMLFilter( (g, a) -> "1.0.0" );
+    }
+
+    @Test
+    public void testDefaultDependency() throws Exception
+    {
+        String input = "<dependency>"
+            + "<groupId>GROUPID</groupId>"
+            + "<artifactId>ARTIFACTID</artifactId>"
+            + "<version>VERSION</version>"
+            + "</dependency>";
+        String expected = input;
+        
+        String actual = transform( input );
+        
+        assertThat( actual ).isEqualTo( expected );
+    }
+
+    @Test
+    public void testManagedDependency() throws Exception
+    {
+        ReactorDependencyXMLFilter filter = new ReactorDependencyXMLFilter( (g, a) -> null );
+        
+        String input = "<dependency>"
+            + "<groupId>GROUPID</groupId>"
+            + "<artifactId>ARTIFACTID</artifactId>"
+            + "</dependency>";
+        String expected = input;
+        
+        String actual = transform( input, filter );
+        
+        assertThat( actual ).isEqualTo( expected );
+    }
+
+    @Test
+    public void testReactorDependency() throws Exception
+    {
+        String input = "<dependency>"
+                        + "<groupId>GROUPID</groupId>"
+                        + "<artifactId>ARTIFACTID</artifactId>"
+                        + "</dependency>";
+        String expected = "<dependency>"
+                        + "<groupId>GROUPID</groupId>"
+                        + "<artifactId>ARTIFACTID</artifactId>"
+                        + "<version>1.0.0</version>"
+                        + "</dependency>";
+        
+        String actual = transform( input );
+        
+        assertThat( actual ).isEqualTo( expected );
+    }
+
+    @Test
+    public void testReactorDependencyLF() throws Exception
+    {
+        String input = "<dependency>\n"
+                        + "  <groupId>GROUPID</groupId>\n"
+                        + "  <artifactId>ARTIFACTID</artifactId>\n"
+                        + "  <!-- include version here --> " 
+                        + "</dependency>";
+        String expected = "<dependency>\n"
+                        + "  <groupId>GROUPID</groupId>\n"
+                        + "  <artifactId>ARTIFACTID</artifactId>\n"
+                        + "  <!-- include version here -->\n" 
+                        + "  <version>1.0.0</version>\n"
+                        + "</dependency>";
+        
+        String actual = transform( input );
+        
+        assertThat( actual ).and( expected ).ignoreWhitespace().areIdentical();
+    }
+
+    @Test
+    public void multipleDependencies() throws Exception {
+        String input = "<project>\n" + 
+            "  <modelVersion>4.0.0</modelVersion>\n" + 
+            "    <groupId>tests.project</groupId>\n" + 
+            "    <artifactId>duplicate-plugin-defs-merged</artifactId>\n" + 
+            "    <version>1</version>\n" + 
+            "    <build>\n" + 
+            "      <plugins>\n" + 
+            "        <plugin>\n" + 
+            "          <artifactId>maven-compiler-plugin</artifactId>\n" + 
+            "          <dependencies>\n" + 
+            "            <dependency>\n" + 
+            "              <groupId>group</groupId>\n" + 
+            "              <artifactId>first</artifactId>\n" + 
+            "              <version>1</version>\n" + 
+            "            </dependency>\n" + 
+            "          </dependencies>\n" + 
+            "        </plugin>\n" + 
+            "        <plugin>\n" + 
+            "          <artifactId>maven-compiler-plugin</artifactId>\n" + 
+            "          <dependencies>\n" + 
+            "            <dependency>\n" + 
+            "              <groupId>group</groupId>\n" + 
+            "              <artifactId>second</artifactId>\n" + 
+            "              <version>1</version>\n" + 
+            "            </dependency>\n" + 
+            "          </dependencies>\n" + 
+            "        </plugin>\n" + 
+            "      </plugins>\n" + 
+            "    </build>\n" + 
+            "</project>";
+        String expected = input;
+        
+        String actual = transform( input );
+        
+        assertThat( actual ).and( expected ).areIdentical();
+    }
+}
diff --git a/maven-xml/src/test/java/org/apache/maven/xml/sax/filter/RelativePathXMLFilterTest.java b/maven-xml/src/test/java/org/apache/maven/xml/sax/filter/RelativePathXMLFilterTest.java
new file mode 100644
index 0000000..00655b3
--- /dev/null
+++ b/maven-xml/src/test/java/org/apache/maven/xml/sax/filter/RelativePathXMLFilterTest.java
@@ -0,0 +1,115 @@
+package org.apache.maven.xml.sax.filter;
+
+/*
+ * 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 static org.xmlunit.assertj.XmlAssert.assertThat;
+
+import org.apache.maven.xml.sax.filter.RelativePathXMLFilter;
+import org.junit.Test;
+
+public class RelativePathXMLFilterTest extends AbstractXMLFilterTests
+{
+    @Override
+    protected RelativePathXMLFilter getFilter()
+    {
+        return new RelativePathXMLFilter();
+    }
+    
+    @Test
+    public void testRelativePath() throws Exception
+    {
+        String input = "<project>\n"
+                        + "  <parent>\n"
+                        + "    <groupId>GROUPID</groupId>\n"
+                        + "    <artifactId>PARENT</artifactId>\n"
+                        + "    <version>VERSION</version>\n"
+                        + "    <relativePath>../pom.xml</relativePath>\n"
+                        + "  </parent>\n"
+                        + "  <artifactId>PROJECT</artifactId>\n"
+                        + "</project>";
+           String expected = "<project>\n"
+                           + "  <parent>\n"
+                           + "    <groupId>GROUPID</groupId>\n"
+                           + "    <artifactId>PARENT</artifactId>\n"
+                           + "    <version>VERSION</version>\n"
+                           + "  </parent>\n"
+                           + "  <artifactId>PROJECT</artifactId>\n"
+                           + "</project>";
+           String actual = transform( input );
+           assertThat( actual ).and( expected ).areIdentical();
+    }
+    
+    @Test
+    public void testRelativePathNS() throws Exception
+    {
+        String input = "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n" + 
+            "  xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" + 
+            "  xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n"
+                        + "  <parent>\n"
+                        + "    <groupId>GROUPID</groupId>\n"
+                        + "    <artifactId>PARENT</artifactId>\n"
+                        + "    <version>VERSION</version>\n"
+                        + "    <relativePath>../pom.xml</relativePath>\n"
+                        + "  </parent>\n"
+                        + "  <artifactId>PROJECT</artifactId>\n"
+                        + "</project>";
+           String expected = "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n" + 
+               "  xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" + 
+               "  xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n"
+                           + "  <parent>\n"
+                           + "    <groupId>GROUPID</groupId>\n"
+                           + "    <artifactId>PARENT</artifactId>\n"
+                           + "    <version>VERSION</version>\n"
+                           + "  </parent>\n"
+                           + "  <artifactId>PROJECT</artifactId>\n"
+                           + "</project>";
+           String actual = transform( input );
+           assertThat( actual ).and( expected ).areIdentical();
+    }
+    
+    @Test
+    public void testRelativePathPasNS() throws Exception
+    {
+        String input = "<p:project xmlns:p=\"http://maven.apache.org/POM/4.0.0\"\n" + 
+            "  xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" + 
+            "  xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n"
+                        + "  <p:parent>\n"
+                        + "    <p:groupId>GROUPID</p:groupId>\n"
+                        + "    <p:artifactId>PARENT</p:artifactId>\n"
+                        + "    <p:version>VERSION</p:version>\n"
+                        + "    <p:relativePath>../pom.xml</p:relativePath>\n"
+                        + "  </p:parent>\n"
+                        + "  <p:artifactId>PROJECT</p:artifactId>\n"
+                        + "</p:project>";
+           String expected = "<p:project xmlns:p=\"http://maven.apache.org/POM/4.0.0\"\n" + 
+               "  xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" + 
+               "  xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n"
+                           + "  <p:parent>\n"
+                           + "    <p:groupId>GROUPID</p:groupId>\n"
+                           + "    <p:artifactId>PARENT</p:artifactId>\n"
+                           + "    <p:version>VERSION</p:version>\n"
+                           + "  </p:parent>\n"
+                           + "  <p:artifactId>PROJECT</p:artifactId>\n"
+                           + "</p:project>";
+           String actual = transform( input );
+           assertThat( actual ).and( expected ).areIdentical();
+    }
+
+}
diff --git a/pom.xml b/pom.xml
index e6dd009..a6c1bb5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -97,6 +97,7 @@ under the License.
     <module>apache-maven</module> <!-- rename to apache-maven/maven.pom after RAT-268 -->
     <module>maven-wrapper</module>
     <module>apache-maven/maven-wrapper.pom</module>
+    <module>maven-xml</module>
   </modules>
 
   <scm>
@@ -250,6 +251,11 @@ under the License.
         <artifactId>maven-slf4j-wrapper</artifactId>
         <version>${project.version}</version>
       </dependency>
+      <dependency>
+        <groupId>org.apache.maven</groupId>
+        <artifactId>maven-xml</artifactId>
+        <version>${project.version}</version>
+      </dependency>
       <!--bootstrap-end-comment-->
       <!--  Plexus -->
       <dependency>
@@ -421,6 +427,12 @@ under the License.
       </dependency>
       <dependency>
         <groupId>org.xmlunit</groupId>
+        <artifactId>xmlunit-assertj</artifactId>
+        <version>${xmlunitVersion}</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.xmlunit</groupId>
         <artifactId>xmlunit-core</artifactId>
         <version>${xmlunitVersion}</version>
         <scope>test</scope>