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 2019/08/26 14:35:16 UTC

[maven] branch MNG-6656 updated: [MNG-6656] Support versionless parent

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

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


The following commit(s) were added to refs/heads/MNG-6656 by this push:
     new a684370  [MNG-6656] Support versionless parent
a684370 is described below

commit a6843705d184bcf7267df1e6ae421cb257bd5093
Author: rfscholte <rf...@apache.org>
AuthorDate: Mon Aug 26 16:35:08 2019 +0200

    [MNG-6656] Support versionless parent
---
 .../DefaultRepositorySystemSessionFactory.java     |   8 +-
 .../internal/DefaultBuildPomXMLFilterFactory.java  |  34 ++-
 .../maven/model/building/DefaultModelBuilder.java  |  23 +-
 .../model/building/DefaultModelCacheManager.java   |  38 +--
 .../maven/model/building/ModelCacheManager.java    |  38 +++
 .../org/apache/maven/model/building/ModelData.java |  23 +-
 .../model/validation/DefaultModelValidator.java    |   8 +-
 .../maven/xml/filter/BuildPomXMLFilterFactory.java |  35 ++-
 .../xml/filter/ConsumerPomXMLFilterFactory.java    |   6 +-
 .../apache/maven/xml/filter/ParentXMLFilter.java   | 313 +++++++++++++++++++++
 .../apache/maven/xml/filter/RelativeProject.java   |  56 ++++
 .../maven/xml/filter/AbstractXMLFilterTests.java   |  30 +-
 .../maven/xml/filter/ConsumerPomXMLFilterTest.java |  11 +-
 .../maven/xml/filter/ParentXMLFilterTest.java      | 185 ++++++++++++
 pom.xml                                            |  27 +-
 15 files changed, 752 insertions(+), 83 deletions(-)

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 20bd449..e4f8439 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
@@ -34,6 +34,7 @@ import java.util.Properties;
 
 import javax.inject.Inject;
 import javax.inject.Named;
+import javax.inject.Provider;
 import javax.xml.parsers.ParserConfigurationException;
 import javax.xml.transform.TransformerException;
 import javax.xml.transform.TransformerFactory;
@@ -114,7 +115,8 @@ public class DefaultRepositorySystemSessionFactory
     MavenRepositorySystem mavenRepositorySystem;
     
     @Inject
-    private ConsumerPomXMLFilterFactory consumerPomXMLFilterFactory;
+    @Nullable
+    private Provider<ConsumerPomXMLFilterFactory> consumerPomXMLFilterFactory;
     
     public DefaultRepositorySystemSession newRepositorySession( MavenExecutionRequest request )
     {
@@ -286,7 +288,6 @@ public class DefaultRepositorySystemSessionFactory
                         public InputStream transformData( File file )
                             throws IOException, TransformException
                         {
-                            System.out.println( "transforming " + file.getAbsolutePath() );
                             final PipedOutputStream pipedOutputStream  = new PipedOutputStream();
                             final PipedInputStream pipedInputStream  = new PipedInputStream( pipedOutputStream );
                             
@@ -294,8 +295,7 @@ public class DefaultRepositorySystemSessionFactory
                             try
                             {
                                 transformSource =
-                                    new SAXSource( consumerPomXMLFilterFactory.get( artifact.getGroupId(),
-                                                                                    artifact.getArtifactId() ),
+                                    new SAXSource( consumerPomXMLFilterFactory.get().get( file.toPath() ),
                                                    new InputSource( new FileReader( file ) ) );
                             }
                             catch ( SAXException | ParserConfigurationException e )
diff --git a/maven-core/src/main/java/org/apache/maven/xml/internal/DefaultBuildPomXMLFilterFactory.java b/maven-core/src/main/java/org/apache/maven/xml/internal/DefaultBuildPomXMLFilterFactory.java
index 0b51da2..cdded2b 100644
--- a/maven-core/src/main/java/org/apache/maven/xml/internal/DefaultBuildPomXMLFilterFactory.java
+++ b/maven-core/src/main/java/org/apache/maven/xml/internal/DefaultBuildPomXMLFilterFactory.java
@@ -19,16 +19,20 @@ package org.apache.maven.xml.internal;
  * under the License.
  */
 
+
+import java.nio.file.Path;
 import java.util.Optional;
+import java.util.function.Function;
 
 import javax.inject.Inject;
 import javax.inject.Named;
-//import javax.inject.Provider;
 import javax.inject.Singleton;
 
 import org.apache.maven.execution.MavenSession;
+import org.apache.maven.model.Model;
+import org.apache.maven.model.building.ModelCacheManager;
 import org.apache.maven.xml.filter.BuildPomXMLFilterFactory;
-import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement;
+import org.apache.maven.xml.filter.RelativeProject;
 
 /**
  * 
@@ -37,12 +41,14 @@ import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement;
  */
 @Named
 @Singleton
-@IgnoreJRERequirement( )
 public class DefaultBuildPomXMLFilterFactory extends BuildPomXMLFilterFactory
 {
     private MavenSession session;
     
     @Inject
+    private ModelCacheManager rawModelCache; 
+    
+    @Inject
     public DefaultBuildPomXMLFilterFactory( MavenSession session )
     {
         this.session = session;
@@ -66,4 +72,26 @@ public class DefaultBuildPomXMLFilterFactory extends BuildPomXMLFilterFactory
         return Optional.ofNullable( session.getUserProperties().getProperty( "sha1" ) );
     }
 
+    @Override
+    protected Function<Path, Optional<RelativeProject>> getRelativePathMapper()
+    {
+        return p -> Optional.ofNullable( rawModelCache.get( p ) ).map( m -> toRelativeProject( m ) );
+    }
+
+    private 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 );
+    }
 }
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 1fea6dd..0b0a615 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
@@ -51,6 +51,7 @@ import javax.xml.transform.stream.StreamResult;
 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.FileSource;
 import org.apache.maven.model.Activation;
 import org.apache.maven.model.Build;
 import org.apache.maven.model.Dependency;
@@ -154,7 +155,12 @@ public class DefaultModelBuilder
     private ReportingConverter reportingConverter;
     
     @Inject
+    @Nullable
     private Provider<BuildPomXMLFilterFactory> buildPomXMLFilterFactory; 
+    
+    @Inject
+    @Nullable
+    private ModelCacheManager modelCacheManager;
 
     public DefaultModelBuilder setModelProcessor( ModelProcessor modelProcessor )
     {
@@ -294,7 +300,11 @@ public class DefaultModelBuilder
         {
             inputModel = readModel( request.getModelSource(), request.getPomFile(), request, problems );
         }
-
+        if ( modelCacheManager != null && request.getPomFile() != null )
+        {
+            modelCacheManager.put( request.getPomFile().toPath(), inputModel ); 
+        }
+        
         problems.setRootModel( inputModel );
 
         ModelData resultData = new ModelData( request.getModelSource(), inputModel );
@@ -755,7 +765,7 @@ public class DefaultModelBuilder
         }
         
         // re-read model from file
-        if ( Boolean.getBoolean( "maven.experimental.buildconsumer" ) )
+        if ( Boolean.getBoolean( "maven.experimental.buildconsumer" ) && !lineage.get( 0 ).isExternal() )
         {
             try
             {
@@ -791,9 +801,12 @@ public class DefaultModelBuilder
         
         final PipedOutputStream pipedOutputStream  = new PipedOutputStream();
         final PipedInputStream pipedInputStream  = new PipedInputStream( pipedOutputStream );
+
+        // Should always be FileSource for reactor poms
+        FileSource source = (FileSource) modelData.getSource();
         
         final SAXSource transformSource =
-            new SAXSource( buildPomXMLFilterFactory.get().get( modelData.getGroupId(), modelData.getArtifactId() ),
+            new SAXSource( buildPomXMLFilterFactory.get().get( source.getFile().toPath() ),
                            new org.xml.sax.InputSource( modelData.getSource().getInputStream() ) );
         
         final StreamResult result = new StreamResult( pipedOutputStream );
@@ -1103,6 +1116,8 @@ public class DefaultModelBuilder
          */
 
         ModelData parentData = new ModelData( candidateSource, candidateModel, groupId, artifactId, version );
+        
+        parentData.setExternal( false );
 
         return parentData;
     }
@@ -1220,6 +1235,8 @@ public class DefaultModelBuilder
         ModelData parentData = new ModelData( modelSource, parentModel, parent.getGroupId(), parent.getArtifactId(),
                                               parent.getVersion() );
 
+        parentData.setExternal( true );
+
         return parentData;
     }
 
diff --git a/maven-core/src/main/java/org/apache/maven/xml/internal/DefaultBuildPomXMLFilterFactory.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelCacheManager.java
similarity index 50%
copy from maven-core/src/main/java/org/apache/maven/xml/internal/DefaultBuildPomXMLFilterFactory.java
copy to maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelCacheManager.java
index 0b51da2..9a87805 100644
--- a/maven-core/src/main/java/org/apache/maven/xml/internal/DefaultBuildPomXMLFilterFactory.java
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelCacheManager.java
@@ -1,4 +1,4 @@
-package org.apache.maven.xml.internal;
+package org.apache.maven.model.building;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,16 +19,15 @@ package org.apache.maven.xml.internal;
  * under the License.
  */
 
-import java.util.Optional;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 
-import javax.inject.Inject;
 import javax.inject.Named;
-//import javax.inject.Provider;
 import javax.inject.Singleton;
 
-import org.apache.maven.execution.MavenSession;
-import org.apache.maven.xml.filter.BuildPomXMLFilterFactory;
-import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement;
+import org.apache.maven.model.Model;
 
 /**
  * 
@@ -37,33 +36,20 @@ import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement;
  */
 @Named
 @Singleton
-@IgnoreJRERequirement( )
-public class DefaultBuildPomXMLFilterFactory extends BuildPomXMLFilterFactory
+public class DefaultModelCacheManager implements ModelCacheManager
 {
-    private MavenSession session;
+    private static final Map<Path, Model> RAWMODELCACHE = Collections.synchronizedMap( new HashMap<Path, Model>() );
     
-    @Inject
-    public DefaultBuildPomXMLFilterFactory( MavenSession session )
-    {
-        this.session = session;
-    }
-
-    @Override
-    protected Optional<String> getChangelist()
-    {
-        return Optional.ofNullable( session.getUserProperties().getProperty( "changelist" ) );
-    }
-
     @Override
-    protected Optional<String> getRevision()
+    public void put( Path p, Model t )
     {
-        return Optional.ofNullable( session.getUserProperties().getProperty( "revision" ) );
+        RAWMODELCACHE.put( p, t );
     }
 
     @Override
-    protected Optional<String> getSha1()
+    public Model get( Path p )
     {
-        return Optional.ofNullable( session.getUserProperties().getProperty( "sha1" ) );
+        return RAWMODELCACHE.get( p );
     }
 
 }
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelCacheManager.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelCacheManager.java
new file mode 100644
index 0000000..6619607
--- /dev/null
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelCacheManager.java
@@ -0,0 +1,38 @@
+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;
+
+/**
+ * Registers models for usage later on
+ * 
+ * @author Robert Scholte
+ * @since 3.7.0
+ */
+public interface ModelCacheManager
+{
+    void put( Path p, Model t );
+    
+    Model get( Path p );
+
+}
diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelData.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelData.java
index 1f39ad4..2d093c6 100644
--- a/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelData.java
+++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/ModelData.java
@@ -45,6 +45,8 @@ class ModelData
     private String artifactId;
 
     private String version;
+    
+    private boolean external = true;
 
     /**
      * Creates a new container for the specified model.
@@ -200,6 +202,25 @@ class ModelData
     }
 
     /**
+     * 
+     * @return {@code false} if model is part of reactor, otherwise {@code true}
+     */
+    public boolean isExternal()
+    {
+        return external;
+    }
+    
+    /**
+     * Set to {@code false} if model is part of reactor, otherwise {@code true}
+     * 
+     * @param external
+     */
+    public void setExternal( boolean external )
+    {
+        this.external = external;
+    }
+    
+    /**
      * Gets the effective identifier of the model in the form {@code <groupId>:<artifactId>:<version>}.
      *
      * @return The effective identifier of the model, never {@code null}.
@@ -212,7 +233,7 @@ class ModelData
 
         return buffer.toString();
     }
-
+    
     @Override
     public String toString()
     {
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 cfa6e20..15a5587 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
@@ -98,8 +98,12 @@ 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 );
+            // resolvedModel will assign version based on relativePath
+            if ( !Boolean.getBoolean( "maven.experimental.buildconsumer" ) )
+            {
+                validateStringNotEmpty( "parent.version", problems, Severity.FATAL, Version.BASE, parent.getVersion(),
+                                        parent );
+            }
 
             if ( equals( parent.getGroupId(), m.getGroupId() ) && equals( parent.getArtifactId(), m.getArtifactId() ) )
             {
diff --git a/maven-xml/src/main/java/org/apache/maven/xml/filter/BuildPomXMLFilterFactory.java b/maven-xml/src/main/java/org/apache/maven/xml/filter/BuildPomXMLFilterFactory.java
index 03cad01..7a3f52a 100644
--- a/maven-xml/src/main/java/org/apache/maven/xml/filter/BuildPomXMLFilterFactory.java
+++ b/maven-xml/src/main/java/org/apache/maven/xml/filter/BuildPomXMLFilterFactory.java
@@ -19,7 +19,9 @@ package org.apache.maven.xml.filter;
  * under the License.
  */
 
+import java.nio.file.Path;
 import java.util.Optional;
+import java.util.function.Function;
 
 import javax.xml.parsers.ParserConfigurationException;
 import javax.xml.parsers.SAXParserFactory;
@@ -35,23 +37,31 @@ import org.xml.sax.XMLReader;
  */
 public abstract class BuildPomXMLFilterFactory
 {
-    public final BuildPomXMLFilter get( String groupId, String artifactId )
+    public final BuildPomXMLFilter get( Path projectFile )
         throws SAXException, ParserConfigurationException
     {
-        CiFriendlyXMLFilter filter = new CiFriendlyXMLFilter();
-        getChangelist().ifPresent( filter::setChangelist  );
-        getRevision().ifPresent( filter::setRevision );
-        getSha1().ifPresent( filter::setSha1 );
+        XMLReader parent = getParent();
         
-        if ( filter.isSet() )
+        if ( getRelativePathMapper() != null )
         {
-            filter.setParent( getParent() );
-            return new BuildPomXMLFilter( filter );
+            ParentXMLFilter parentFilter = new ParentXMLFilter( getRelativePathMapper() );
+            parentFilter.setProjectPath( projectFile.getParent() );
+            parentFilter.setParent( parent );
+            parent = parentFilter;
         }
-        else
+        
+        CiFriendlyXMLFilter ciFriendlyFilter = new CiFriendlyXMLFilter();
+        getChangelist().ifPresent( ciFriendlyFilter::setChangelist  );
+        getRevision().ifPresent( ciFriendlyFilter::setRevision );
+        getSha1().ifPresent( ciFriendlyFilter::setSha1 );
+        
+        if ( ciFriendlyFilter.isSet() )
         {
-            return new BuildPomXMLFilter( getParent() );
+            ciFriendlyFilter.setParent( parent );
+            parent = ciFriendlyFilter;
         }
+        
+        return new BuildPomXMLFilter( parent );
     }
     
     protected XMLReader getParent() throws SAXException, ParserConfigurationException 
@@ -67,4 +77,9 @@ public abstract class BuildPomXMLFilterFactory
     protected abstract Optional<String> getRevision();
     
     protected abstract Optional<String> getSha1();
+    
+    /**
+     * @return the mapper or {@code null} if relativePaths don't need to be mapped
+     */
+    protected abstract Function<Path, Optional<RelativeProject>> getRelativePathMapper();
 }
diff --git a/maven-xml/src/main/java/org/apache/maven/xml/filter/ConsumerPomXMLFilterFactory.java b/maven-xml/src/main/java/org/apache/maven/xml/filter/ConsumerPomXMLFilterFactory.java
index c185fdf..dfea9ba 100644
--- a/maven-xml/src/main/java/org/apache/maven/xml/filter/ConsumerPomXMLFilterFactory.java
+++ b/maven-xml/src/main/java/org/apache/maven/xml/filter/ConsumerPomXMLFilterFactory.java
@@ -19,6 +19,8 @@ package org.apache.maven.xml.filter;
  * under the License.
  */
 
+import java.nio.file.Path;
+
 import javax.inject.Inject;
 import javax.inject.Provider;
 import javax.xml.parsers.ParserConfigurationException;
@@ -47,10 +49,10 @@ public abstract class ConsumerPomXMLFilterFactory
         this.buildPomXMLFilterFactory = buildPomXMLFilterFactory;
     }
     
-    public final ConsumerPomXMLFilter get( String groupId, String artifactId )
+    public final ConsumerPomXMLFilter get( Path projectPath )
         throws SAXException, ParserConfigurationException
     {
-        XMLFilter parent = buildPomXMLFilterFactory.get().get( groupId, artifactId );
+        XMLFilter parent = buildPomXMLFilterFactory.get().get( projectPath );
         
         // Ensure that xs:any elements aren't touched by next filters
         XMLReader filter = new FastForwardFilter( parent );
diff --git a/maven-xml/src/main/java/org/apache/maven/xml/filter/ParentXMLFilter.java b/maven-xml/src/main/java/org/apache/maven/xml/filter/ParentXMLFilter.java
new file mode 100644
index 0000000..244ecc5
--- /dev/null
+++ b/maven-xml/src/main/java/org/apache/maven/xml/filter/ParentXMLFilter.java
@@ -0,0 +1,313 @@
+package org.apache.maven.xml.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.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+
+import org.apache.maven.xml.SAXEvent;
+import org.apache.maven.xml.SAXEventFactory;
+import org.apache.maven.xml.SAXEventUtils;
+import org.xml.sax.Attributes;
+import org.xml.sax.Locator;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.XMLFilterImpl;
+
+/**
+ * <p>
+ * Transforms relativePath to version.
+ * We could decide to simply allow {@code <parent/>}, but let's require the GA for now for checking
+ * </p>
+ * 
+ * @author Robert Scholte
+ */
+class ParentXMLFilter
+    extends XMLFilterImpl
+{
+    private boolean parsingParent = false;
+
+    // states
+    private String state;
+
+    private String groupId;
+
+    private String artifactId;
+    
+    private String relativePath;
+
+    private boolean hasVersion;
+    
+    private Optional<RelativeProject> resolvedParent;
+
+    private char[] linebreak;
+    
+    private List<SAXEvent> saxEvents = new ArrayList<>();
+
+    private SAXEventFactory eventFactory;
+    
+    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;
+    }
+    
+    private SAXEventFactory getEventFactory()
+    {
+        if ( eventFactory == null )
+        {
+            eventFactory = SAXEventFactory.newInstance( getContentHandler() );
+        }
+        return eventFactory;
+    }
+
+    private void processEvent( final SAXEvent event )
+        throws SAXException
+    {
+        if ( parsingParent )
+        {
+            final String eventState = state;
+
+            saxEvents.add( () -> 
+            {
+                if ( !( "relativePath".equals( eventState ) && resolvedParent.isPresent() ) )
+                {
+                    event.execute();
+                }
+            } );
+        }
+        else
+        {
+            event.execute();
+        }
+    }
+
+    @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 );
+        }
+        
+        processEvent( getEventFactory().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":
+                    int l;
+                    for ( l = length ; l >= 0; l-- )
+                    {
+                        int i = start + l - 1; 
+                        if ( ch[i] == '\n' || ch[i] == '\r' )
+                        {
+                            break;
+                        }
+                    }
+                    
+                    linebreak = new char[l];
+                    System.arraycopy( ch, start, linebreak, 0, l );
+                    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;
+            }
+        }
+        
+        processEvent( getEventFactory().characters( ch, start, length ) );
+    }
+
+    @Override
+    public void endDocument()
+        throws SAXException
+    {
+        processEvent( getEventFactory().endDocument() );
+    }
+
+    @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" ) ) );
+                    }
+                    
+                    // not with streams due to checked SAXException
+                    for ( SAXEvent saxEvent : saxEvents )
+                    {
+                        saxEvent.execute();
+                    }
+                    
+                    if ( !hasVersion && resolvedParent.isPresent() )
+                    {
+                        String versionQName = SAXEventUtils.renameQName( qName, "version" );
+                        
+                        getEventFactory().startElement( uri, "version", versionQName, null ).execute();
+                        
+                        String resolvedParentVersion = resolvedParent.get().getVersion();
+                        
+                        getEventFactory().characters( resolvedParentVersion.toCharArray(), 0,
+                                                      resolvedParentVersion.length() ).execute();
+                        
+                        getEventFactory().endElement( uri, "version", versionQName ).execute();
+                        
+                        if ( linebreak != null )
+                        {
+                            getEventFactory().characters( linebreak, 0, linebreak.length ).execute();
+                        }
+                    }
+                    
+                    parsingParent = false;
+                    break;
+                default:
+                    break;
+            }
+        }
+        
+        processEvent( getEventFactory().endElement( uri, localName, qName ) );
+
+        // for this simple structure resetting to parent it sufficient
+        state = "parent";
+    }
+
+    @Override
+    public void endPrefixMapping( String prefix )
+        throws SAXException
+    {
+        processEvent( getEventFactory().endPrefixMapping( prefix ) );
+    }
+
+    @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 setDocumentLocator( Locator locator )
+    {
+        try
+        {
+            processEvent( getEventFactory().setDocumentLocator( locator ) );
+        }
+        catch ( SAXException e )
+        {
+            // noop
+        }
+    }
+
+    @Override
+    public void skippedEntity( String name )
+        throws SAXException
+    {
+        processEvent( getEventFactory().skippedEntity( name ) );
+    }
+
+    @Override
+    public void startDocument()
+        throws SAXException
+    {
+        processEvent( getEventFactory().startDocument() );
+    }
+
+    @Override
+    public void startPrefixMapping( String prefix, String uri )
+        throws SAXException
+    {
+        processEvent( getEventFactory().startPrefixMapping( prefix, uri ) );
+    }
+
+    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/filter/RelativeProject.java b/maven-xml/src/main/java/org/apache/maven/xml/filter/RelativeProject.java
new file mode 100644
index 0000000..bbceff3
--- /dev/null
+++ b/maven-xml/src/main/java/org/apache/maven/xml/filter/RelativeProject.java
@@ -0,0 +1,56 @@
+package org.apache.maven.xml.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 String groupId;
+    
+    private String artifactId;
+    
+    private 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/filter/AbstractXMLFilterTests.java b/maven-xml/src/test/java/org/apache/maven/xml/filter/AbstractXMLFilterTests.java
index 4ee0e31..e406f6c 100644
--- a/maven-xml/src/test/java/org/apache/maven/xml/filter/AbstractXMLFilterTests.java
+++ b/maven-xml/src/test/java/org/apache/maven/xml/filter/AbstractXMLFilterTests.java
@@ -46,22 +46,38 @@ public abstract class AbstractXMLFilterTests
     
     protected abstract XMLFilter getFilter() throws TransformerException, SAXException, ParserConfigurationException;
     
+    private void setParent( XMLFilter filter ) throws SAXException, ParserConfigurationException
+    {
+        if( filter.getParent() == null )
+        {
+            filter.setParent( SAXParserFactory.newInstance().newSAXParser().getXMLReader() );
+            filter.setFeature( "http://xml.org/sax/features/namespaces", true );
+        }
+    }
 
     protected String transform( String input )
         throws TransformerException, SAXException, ParserConfigurationException
     {
         return transform( new StringReader( input ) );
     }
+
+    protected String transform( Reader input ) throws TransformerException, SAXException, ParserConfigurationException
+    {
+        XMLFilter filter = getFilter();
+        setParent( filter );
+        return transform( input, filter );
+    }
     
-    protected String transform( Reader input )
+    protected String transform( String input, XMLFilter filter ) 
+        throws TransformerException, SAXException, ParserConfigurationException
+    {
+        setParent( filter );
+        return transform( new StringReader( input ), filter );
+    }
+
+    protected String transform( Reader input, XMLFilter filter )
         throws TransformerException, SAXException, ParserConfigurationException
     {
-        XMLFilter filter = getFilter();
-        if( filter.getParent() == null )
-        {
-            filter.setParent( SAXParserFactory.newInstance().newSAXParser().getXMLReader() );
-            filter.setFeature( "http://xml.org/sax/features/namespaces", true );
-        }
 
         Writer writer = new StringWriter();
         StreamResult result = new StreamResult( writer );
diff --git a/maven-xml/src/test/java/org/apache/maven/xml/filter/ConsumerPomXMLFilterTest.java b/maven-xml/src/test/java/org/apache/maven/xml/filter/ConsumerPomXMLFilterTest.java
index 8fe2d67..b02589e 100644
--- a/maven-xml/src/test/java/org/apache/maven/xml/filter/ConsumerPomXMLFilterTest.java
+++ b/maven-xml/src/test/java/org/apache/maven/xml/filter/ConsumerPomXMLFilterTest.java
@@ -21,7 +21,10 @@ package org.apache.maven.xml.filter;
 
 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.Function;
 
 import javax.inject.Provider;
 import javax.xml.parsers.ParserConfigurationException;
@@ -54,6 +57,12 @@ public class ConsumerPomXMLFilterTest extends AbstractXMLFilterTests
             {
                 return Optional.of( "CL" );
             }
+            
+            @Override
+            protected Function<Path, Optional<RelativeProject>> getRelativePathMapper()
+            {
+                return null;
+            }
         };
         
         Provider<BuildPomXMLFilterFactory> provider = new Provider<BuildPomXMLFilterFactory>()
@@ -68,7 +77,7 @@ public class ConsumerPomXMLFilterTest extends AbstractXMLFilterTests
         
         XMLFilter filter = new ConsumerPomXMLFilterFactory( provider )
         {
-        }.get( "G", "A" );
+        }.get( Paths.get( "pom.xml" ) );
         filter.setFeature( "http://xml.org/sax/features/namespaces", true );
         return filter;
     }
diff --git a/maven-xml/src/test/java/org/apache/maven/xml/filter/ParentXMLFilterTest.java b/maven-xml/src/test/java/org/apache/maven/xml/filter/ParentXMLFilterTest.java
new file mode 100644
index 0000000..aa70b0d
--- /dev/null
+++ b/maven-xml/src/test/java/org/apache/maven/xml/filter/ParentXMLFilterTest.java
@@ -0,0 +1,185 @@
+package org.apache.maven.xml.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.junit.Test;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLFilter;
+
+public class ParentXMLFilterTest extends AbstractXMLFilterTests
+{
+    @Override
+    protected XMLFilter 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>"
+                        + "<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>"
+                        + "<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:version>1.0.0</relativePath:version>"
+                        + "</relativePath:parent>";
+
+        String actual = transform( input );
+
+        assertEquals( expected, actual );
+    }
+
+    
+    @Test
+    public void testIndent() throws Exception
+    {
+        String input = "<parent>\n"
+                    + "  <groupId>GROUPID</groupId>\n"
+                    + "  <artifactId>ARTIFACTID</artifactId>\n"
+                    + "  </parent>";
+        // transformer is responsible for line separator and indents
+        String expected = "<parent>" + System.lineSeparator()
+                    + "  <groupId>GROUPID</groupId>" + System.lineSeparator()
+                    + "  <artifactId>ARTIFACTID</artifactId>" + System.lineSeparator()
+                    + "  <version>1.0.0</version>" + System.lineSeparator()
+                    + "</parent>";
+
+        String actual = transform( input );
+
+        assertEquals( expected, actual );
+    }
+}
diff --git a/pom.xml b/pom.xml
index bed2633..5c23389 100644
--- a/pom.xml
+++ b/pom.xml
@@ -47,8 +47,8 @@ under the License.
 
   <properties>
     <maven.version>3.0.5</maven.version>
-    <maven.compiler.source>1.7</maven.compiler.source>
-    <maven.compiler.target>1.7</maven.compiler.target>
+    <maven.compiler.source>1.8</maven.compiler.source>
+    <maven.compiler.target>1.8</maven.compiler.target>
     <classWorldsVersion>2.6.0</classWorldsVersion>
     <commonsCliVersion>1.4</commonsCliVersion>
     <commonsLangVersion>3.8.1</commonsLangVersion>
@@ -455,27 +455,6 @@ under the License.
     <pluginManagement>
       <plugins>
         <plugin>
-          <groupId>org.apache.maven.plugins</groupId>
-          <artifactId>maven-enforcer-plugin</artifactId>
-          <executions>
-            <execution>
-              <id>enforce-bytecode-version</id>
-              <goals>
-                <goal>enforce</goal>
-              </goals>
-              <configuration>
-                <rules>
-                  <enforceBytecodeVersion>
-                    <excludes>
-                      <exclude>org.apache.maven:maven-xml</exclude> <!-- Java 8 compatible -->
-                    </excludes>
-                  </enforceBytecodeVersion>
-                </rules>
-              </configuration>
-            </execution>
-          </executions>
-        </plugin>
-        <plugin>
           <groupId>org.codehaus.plexus</groupId>
           <artifactId>plexus-component-metadata</artifactId>
           <version>${plexusVersion}</version>
@@ -604,7 +583,7 @@ under the License.
         <configuration>
           <signature>
             <groupId>org.codehaus.mojo.signature</groupId>
-            <artifactId>java17</artifactId>
+            <artifactId>java18</artifactId>
             <version>1.0</version>
           </signature>
         </configuration>