diff --git a/maven-core/pom.xml b/maven-core/pom.xml index 43a6bee05fd8..7128c3555d75 100644 --- a/maven-core/pom.xml +++ b/maven-core/pom.xml @@ -60,6 +60,10 @@ under the License. org.apache.maven maven-artifact + + org.apache.maven + maven-xml + org.apache.maven maven-plugin-api @@ -142,6 +146,11 @@ under the License. hamcrest-library test + + org.xmlunit + xmlunit-assertj + test + @@ -218,6 +227,19 @@ under the License. + + org.apache.maven.plugins + maven-failsafe-plugin + + + + + integration-test + verify + + + + 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 000000000000..728c78e7eb0a --- /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: + * + */ + @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 248a3b6dd120..28f75cdce81e 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.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.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 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 configProps = new LinkedHashMap<>(); @@ -139,7 +150,6 @@ else if ( request.isUpdateSnapshots() ) 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 @@ else if ( request.isUpdateSnapshots() ) 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 @@ private String getMavenVersion() return props.getProperty( "version", "unknown-version" ); } + + private Collection getTransformersForArtifact( final Artifact artifact, + final SessionData sessionData ) + { + TransformerContext context = (TransformerContext) sessionData.get( TransformerContext.KEY ); + Collection 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 1ebc1fc0945f..624f6ade9d90 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 @@ 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.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.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.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 @@ private ModelBuildingRequest getModelBuildingRequest( InternalConfig config ) 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 ProjectBuildingResult build( Artifact artifact, boolean allowStubModel, P 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 List build( List pomFiles, boolean recursive List 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 List build( List pomFiles, boolean recursive 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 List build( List pomFiles, boolean recursive @SuppressWarnings( "checkstyle:parameternumber" ) private boolean build( List results, List interimResults, Map projectIndex, List pomFiles, Set aggregatorFiles, - boolean isRoot, boolean recursive, InternalConfig config ) + boolean root, boolean recursive, InternalConfig config, + ReactorModelPool.Builder poolBuilder ) { boolean noErrors = true; @@ -414,7 +453,8 @@ private boolean build( List results, List { 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 @@ private boolean build( List results, List @SuppressWarnings( "checkstyle:parameternumber" ) private boolean build( List results, List interimResults, Map projectIndex, File pomFile, Set aggregatorFiles, - boolean isRoot, boolean recursive, InternalConfig config ) + boolean isRoot, boolean recursive, InternalConfig config, + ReactorModelPool.Builder poolBuilder ) { boolean noErrors = true; @@ -465,6 +506,9 @@ private boolean build( List results, List } Model model = result.getEffectiveModel(); + + poolBuilder.put( model.getPomFile().toPath(), result.getRawModel() ); + try { // first pass: build without building parent. @@ -559,7 +603,7 @@ private boolean build( List results, List interimResult.modules = new ArrayList<>(); if ( !build( results, interimResult.modules, projectIndex, moduleFiles, aggregatorFiles, false, - recursive, config ) ) + recursive, config, poolBuilder ) ) { noErrors = false; } @@ -595,17 +639,6 @@ static class InterimResult } - private void populateReactorModelPool( ReactorModelPool reactorModelPool, List 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 results, List projects, Map projectIndex, List interimResults, ProjectBuildingRequest request, Map profilesXmls, @@ -865,7 +898,7 @@ HashMap compute() 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 @@ HashMap compute() 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 24b36dda310d..bcc3730e72bb 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 @@ * 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.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 void addRepository( final Repository repository, boolean replace ) private static void removeMatchingRepository( Iterable repositories, final String id ) { - Iterator iterator = repositories.iterator( ); + Iterator 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 ModelResolver newCopy() 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 ModelSource resolveModel( final Dependency dependency ) } 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 64b30dd90581..b96b14b7fed5 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 @@ * 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> modelsByGa = new HashMap<>(); + + private final Map 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 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() ).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 @@ public boolean equals( Object obj ) 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 @@ public int hashCode() 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 000000000000..f49e62d32b0b --- /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 getChangelist() + { + return Optional.ofNullable( context.getUserProperty( "changelist" ) ); + } + + @Override + protected Optional getRevision() + { + return Optional.ofNullable( context.getUserProperty( "revision" ) ); + } + + @Override + protected Optional 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 da4308878a17..538f887b02d2 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 void testReadModifiedPoms() throws Exception { File parent = new File( tempDir.toFile(), "pom.xml" ); String parentContent = FileUtils.fileRead( parent ); parentContent = parentContent.replaceAll( "pom", - "pomaddedValue" ); + "pomaddedValue" ); 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 a3c1c0d61cd8..c95f42843f5e 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 Artifact createArtifactWithClassifier( String groupId, String artifactId, 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 81d25870ccd2..583d6cbd5f65 100644 --- a/maven-model-builder/pom.xml +++ b/maven-model-builder/pom.xml @@ -58,6 +58,11 @@ under the License. org.apache.maven maven-builder-support + + org.apache.maven + maven-xml + + org.eclipse.sisu org.eclipse.sisu.inject @@ -67,7 +72,6 @@ under the License. org.eclipse.sisu.plexus test - com.google.inject guice 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 000000000000..48848c9abbe3 --- /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 000000000000..31b88bfa7d7a --- /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 000000000000..dbf9211e9ee6 --- /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 000000000000..3de90decbba9 --- /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> getRelativePathMapper() + { + return p -> Optional.ofNullable( context.getRawModel( p ) ).map( m -> toRelativeProject( m ) ); + } + + @Override + protected BiFunction 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 b72550b13a4b..77a117da7acc 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 @@ * 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.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.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 DefaultModelBuilder setReportingConverter( ReportingConverter reportingCo this.reportingConverter = reportingConverter; return this; } - + @SuppressWarnings( "checkstyle:methodlength" ) @Override public ModelBuildingResult build( ModelBuildingRequest request ) @@ -426,7 +437,7 @@ else if ( !parentIds.add( parentData.getId() ) ) } result.setEffectiveModel( resultModel ); - + for ( ModelData currentData : lineage ) { String modelId = ( currentData != superData ) ? currentData.getId() : ""; @@ -529,6 +540,7 @@ public Result buildRawModel( File pomFile, int validationLevel, } } + @SuppressWarnings( "checkstyle:methodlength" ) private Model readModel( ModelSource modelSource, File pomFile, ModelBuildingRequest request, DefaultModelProblemCollector problems ) throws ModelBuildingException @@ -637,7 +649,6 @@ private Model readModel( ModelSource modelSource, File pomFile, ModelBuildingReq .setMessage( "Non-readable POM " + modelSource.getLocation() + ": " + msg ).setException( e ) ); throw problems.newModelBuildingException(); } - if ( pomFile != null ) { model.setPomFile( pomFile ); @@ -646,8 +657,33 @@ else if ( modelSource instanceof FileModelSource ) { model.setPomFile( ( (FileModelSource) modelSource ).getFile() ); } - problems.setSource( model ); + + modelValidator.validateFileModel( model, request, problems ); + request.setFileModel( model ); + + if ( Features.buildConsumer().isActive() && pomFile != null ) + { + 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 @@ private ModelData readParent( Model childModel, ModelSource childSource, ModelBu 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 @@ private ModelData readParent( Model childModel, ModelSource childSource, ModelBu 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 @@ private void importDependencyManagement( Model model, ModelBuildingRequest reque final ModelSource importSource; try { - importSource = modelResolver.resolveModel( groupId, artifactId, version ); + importSource = modelResolver.resolveModel( dependency ); } catch ( UnresolvableModelException e ) { @@ -1516,4 +1556,155 @@ protected boolean hasFatalErrors( ModelProblemCollectorExt problems ) } } + /** + * 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 context ) + { + // don't merge + } + + + @Override + protected void mergeBuildBase_Resources( BuildBase target, BuildBase source, boolean sourceDominant, + Map context ) + { + // don't merge + } + + @Override + protected void mergeBuildBase_TestResources( BuildBase target, BuildBase source, boolean sourceDominant, + Map context ) + { + // don't merge + } + + @Override + protected void mergeCiManagement_Notifiers( CiManagement target, CiManagement source, boolean sourceDominant, + Map context ) + { + // don't merge + } + + @Override + protected void mergeDependencyManagement_Dependencies( DependencyManagement target, DependencyManagement source, + boolean sourceDominant, Map context ) + { + Iterator 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 context ) + { + // don't merge + } + + @Override + protected void mergeModel_Contributors( Model target, Model source, boolean sourceDominant, + Map context ) + { + // don't merge + } + + @Override + protected void mergeModel_Developers( Model target, Model source, boolean sourceDominant, + Map context ) + { + // don't merge + } + + @Override + protected void mergeModel_Licenses( Model target, Model source, boolean sourceDominant, + Map context ) + { + // don't merge + } + + @Override + protected void mergeModel_MailingLists( Model target, Model source, boolean sourceDominant, + Map context ) + { + // don't merge + } + + @Override + protected void mergeModel_Profiles( Model target, Model source, boolean sourceDominant, + Map context ) + { + Iterator 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 context ) + { + Iterator 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 context ) + { + target.setPluginRepositories( source.getPluginRepositories() ); + } + + @Override + protected void mergeModelBase_Repositories( ModelBase target, ModelBase source, boolean sourceDominant, + Map context ) + { + // don't merge + } + + @Override + protected void mergePlugin_Dependencies( Plugin target, Plugin source, boolean sourceDominant, + Map context ) + { + Iterator 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 context ) + { + // don't merge + } + + @Override + protected void mergeReporting_Plugins( Reporting target, Reporting source, boolean sourceDominant, + Map context ) + { + // don't merge + } + + @Override + protected void mergeReportPlugin_ReportSets( ReportPlugin target, ReportPlugin source, boolean sourceDominant, + Map context ) + { + // don't merge + } + + @Override + protected void mergePluginContainer_Plugins( PluginContainer target, PluginContainer source, + boolean sourceDominant, Map 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 4240574ff214..daf56ca79e83 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 @@ protected ModelLocator newModelLocator() protected ModelReader newModelReader() { - return new DefaultModelReader(); + DefaultModelReader reader = new DefaultModelReader(); + reader.setTransformer( newModelSourceTransformer() ); + return reader; } protected ProfileSelector newProfileSelector() @@ -199,6 +201,11 @@ protected ReportingConverter newReportingConverter() 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 84a68f74c8fb..2012bb1a578f 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 @@ 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. @@ -382,6 +385,19 @@ public DefaultModelBuildingRequest setModelCache( ModelCache modelCache ) return this; } + @Override + public Model getFileModel() + { + return fileModel; + } + + @Override + public ModelBuildingRequest setFileModel( Model fileModel ) + { + this.fileModel = fileModel; + return this; + } + @Override public Model getRawModel() { @@ -407,5 +423,17 @@ public ModelBuildingRequest setWorkspaceModelResolver( WorkspaceModelResolver wo 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 000000000000..50ad04b572e5 --- /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 a51126f280ae..1dd2643e1dac 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 @@ -256,6 +256,19 @@ public FilterModelBuildingRequest setModelCache( ModelCache modelCache ) return this; } + @Override + public Model getFileModel() + { + return request.getFileModel(); + } + + @Override + public ModelBuildingRequest setFileModel( Model fileModel ) + { + request.setFileModel( fileModel ); + return this; + } + @Override public Model getRawModel() { @@ -281,5 +294,17 @@ public ModelBuildingRequest setWorkspaceModelResolver( WorkspaceModelResolver wo 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 dce0c321d0c3..9523f4c9d0bc 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 2c7a72e70a10..30b67249eaf2 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 @@ enum Version 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 000000000000..2aa3ddb48dcf --- /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 000000000000..2f763a2a9001 --- /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 000000000000..6c89d67b2c57 --- /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 6e78fd06700d..1bae7475e8f2 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.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 @@ public class DefaultModelReader implements ModelReader { + @Inject + private ModelSourceTransformer transformer; + public void setTransformer( ModelSourceTransformer transformer ) + { + this.transformer = transformer; + } + @Override public Model read( File input, Map 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 ad7e3c7e5a9f..f789333fe7c6 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 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 void validateRawModel( Model m, ModelBuildingRequest request, ModelProble 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 void validateRawModel( Model m, ModelBuildingRequest request, ModelProble if ( request.getValidationLevel() >= ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0 ) { + Set 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 void validateRawModel( Model m, ModelBuildingRequest request, ModelProble } } } + + @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 void validateEffectiveModel( Model m, ModelBuildingRequest request, Model if ( request.getValidationLevel() >= ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0 ) { - Set 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 84e3faddc5cd..198ba5ac5238 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 @@ */ 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 000000000000..485dc4c83da7 --- /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 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 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 09f930cb28e4..992447180c3b 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 @@ */ 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 @@ public class DefaultInheritanceAssemblerTest extends TestCase { - private ModelReader reader; + private DefaultModelReader reader; private ModelWriter writer; @@ -54,6 +60,15 @@ protected void setUp() 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 3e07c57754a8..d2a9e600db4d 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 @@ private SimpleProblemCollector validateEffective( String pom, int level ) 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 @@ private SimpleProblemCollector validateRaw( String pom, int level ) { 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 void testBadDependencyVersion() 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 void testIncompleteParent() 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 000000000000..6b98f1230c21 --- /dev/null +++ b/maven-xml/pom.xml @@ -0,0 +1,47 @@ + + + + + + 4.0.0 + + org.apache.maven + maven + 3.7.0-SNAPSHOT + + maven-xml + Maven XML + + + + org.xmlunit + xmlunit-assertj + test + + + org.mockito + mockito-core + test + + + + \ 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 000000000000..eb2166a686e1 --- /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 000000000000..81269570ad60 --- /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 000000000000..84e13e5fad58 --- /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 000000000000..237ec44cb3a6 --- /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 000000000000..3ae19a46a0d1 --- /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 000000000000..d23bdec0085f --- /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 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(); + } + + 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 000000000000..89de519c5963 --- /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 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 000000000000..14bcf704e384 --- /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(); + } + + 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 000000000000..6f3f3198c9a1 --- /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> getRelativePathMapper() + { + return null; + } + + protected BiFunction 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 000000000000..b97c757330dd --- /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 000000000000..a13e4d432fc7 --- /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 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 000000000000..1c227a733a28 --- /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 +{ + 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 000000000000..f7751d2b817b --- /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 getChangelist() + { + return Optional.empty(); + } + + protected Optional getRevision() + { + return Optional.empty(); + } + + protected Optional 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 000000000000..1168351744fa --- /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 000000000000..0100e6bffd00 --- /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 + * + *
    + *
  • execution.configuration
  • + *
  • plugin.configuration
  • + *
  • plugin.goals
  • + *
  • profile.reports
  • + *
  • project.reports
  • + *
  • reportSet.configuration
  • + *
      + */ + private final Deque state = new ArrayDeque<>(); + + private int domDepth = 0; + + private ContentHandler originalHandler; + + FastForwardFilter() + { + super(); + } + + 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 000000000000..261c853f9c41 --- /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(); + } + + 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 000000000000..df43ec1ef7d6 --- /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; + +/** + *

      + * Transforms relativePath to version. + * We could decide to simply allow {@code }, 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 + *

      + * + * @author Robert Scholte + * @since 3.7.0 + */ +class ParentXMLFilter + extends AbstractEventXMLFilter +{ + private boolean parsingParent; + + // states + private String state; + + // whiteSpace after , to be used to position + private String parentWhitespace = ""; + + private String groupId; + + private String artifactId; + + private String relativePath; + + private boolean hasVersion; + + private Optional resolvedParent; + + private final Function> relativePathMapper; + + private Path projectPath; + + /** + * + * + * @param relativePathMapper + */ + ParentXMLFilter( Function> 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 resolveRelativePath( Path relativePath ) + { + Optional 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 000000000000..38f8fb8d2192 --- /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 , to be used to position + private String dependencyWhitespace = ""; + + private boolean hasVersion; + + private String groupId; + + private String artifactId; + + private final BiFunction reactorVersionMapper; + + public ReactorDependencyXMLFilter( BiFunction 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 000000000000..25f2137be521 --- /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(); + } + + 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 000000000000..067e170b5a05 --- /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 000000000000..02e55dc5991d --- /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 000000000000..b6bc381c7b87 --- /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 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 000000000000..4fa3b0d63919 --- /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 000000000000..16d458b79ada --- /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> getRelativePathMapper() + { + return null; + } + + @Override + protected BiFunction getDependencyKeyToVersionMapper() + { + return null; + } + }; + + ConsumerPomXMLFilter filter = new ConsumerPomXMLFilterFactory( buildPomXMLFilterFactory ) + { + @Override + protected Optional getSha1() + { + return Optional.empty(); + } + + @Override + protected Optional getRevision() + { + return Optional.empty(); + } + + @Override + protected Optional 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 = "\n" + + " \n" + + " GROUPID\n" + + " PARENT\n" + + " VERSION\n" + + " ../pom.xml\n" + + " \n" + + " PROJECT\n" + + " \n" + + " ab\n" + + " ../cd\n" + + " \n" + + ""; + String expected = "\n" + + " \n" + + " GROUPID\n" + + " PARENT\n" + + " VERSION\n" + + " \n" + + " PROJECT\n" + + ""; + String actual = transform( input ); + assertThat( actual ).and( expected ).ignoreWhitespace().areIdentical(); + } + + @Test + public void aggregatorWithCliFriendlyVersion() throws Exception { + String input = "\n" + + "\n" + + " 4.0.0\n" + + " org.sonatype.mavenbook.multispring\n" + + " parent\n" + + " 0.9-${changelist}-SNAPSHOT\n" + + " pom\n" + + " Multi-Spring Chapter Parent Project\n" + + " \n" + + " simple-parent\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " apache.snapshots\n" + + " http://repository.apache.org/snapshots/\n" + + " \n" + + " \n" + + ""; + String expected = "\n" + + "\n" + + " 4.0.0\n" + + " org.sonatype.mavenbook.multispring\n" + + " parent\n" + + " 0.9-CL-SNAPSHOT\n" + + " pom\n" + + " Multi-Spring Chapter Parent Project\n" + + " \n" + + " \n" + + " \n" + + " apache.snapshots\n" + + " http://repository.apache.org/snapshots/\n" + + " \n" + + " \n" + + ""; + String actual = transform( input ); + assertThat( actual ).and( expected ).ignoreWhitespace().areIdentical(); + } + + @Test + public void licenseHeader() throws Exception { + String input = "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + " 4.0.0\n" + + " \n" + + " org.apache.maven\n" + + " maven\n" + + " 3.7.0-SNAPSHOT\n" + + " \n" + + " maven-xml\n" + + " Maven XML\n" + + " \n" + + " \n" + + " 1.8\n" + + " 1.8\n" + + " \n" + + "\n" + + " \n" + + " \n" + + " \n" + + " org.codehaus.mojo\n" + + " animal-sniffer-maven-plugin\n" + + " \n" + + " \n" + + " org.codehaus.mojo.signature\n" + + " java18\n" + + " 1.0\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " javax.inject\n" + + " javax.inject\n" + + " true\n" + + " \n" + + " \n" + + " org.xmlunit\n" + + " xmlunit-assertj\n" + + " test\n" + + " \n" + + " \n" + + ""; + 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 000000000000..552b72140717 --- /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 = ""; + String expected = ""; + String actual = transform( input ); + assertThat( actual ).and( expected ).areIdentical(); + } + + @Test + public void setOfModules() throws Exception { + String input = "" + + "ab" + + "../cd" + + ""; + String expected = ""; + String actual = transform( input ); + assertThat( actual ).and( expected ).areIdentical(); + } + + @Test + public void noModules() throws Exception { + String input = "NAME"; + String expected = input; + String actual = transform( input ); + assertThat( actual ).and( expected ).areIdentical(); + } + + @Test + public void comment() throws Exception { + + String input = "" + + "" + + "ab" + + "../cd" + + "" + + "" + + ""; + String expected = ""; + String actual = transform( input ); + assertThat( actual ).and( expected ).areIdentical(); + } + + @Test + public void setOfModulesLF() throws Exception { + String input = "\n" + + "\n" + + " \n" + + " ab\n" + + " ../cd\n" + + " \n" + + "\n" + + "\n"; + String expected = "\n" + + "\n" + + " \n" + + "\n" + + "\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 000000000000..bb0022249394 --- /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 = ""; + String expected = input; + String actual = transform( input ); + assertEquals( expected, actual ); + } + + @Test + public void testNoRelativePath() throws Exception + { + String input = "" + + "GROUPID" + + "ARTIFACTID" + + "VERSION" + + ""; + String expected = input; + + String actual = transform( input ); + + assertEquals( expected, actual ); + } + + @Test + public void testDefaultRelativePath() throws Exception + { + String input = "" + + "GROUPID" + + "ARTIFACTID" + + ""; + String expected = "" + + "GROUPID" + + "ARTIFACTID" + + "1.0.0" + + ""; + + String actual = transform( input ); + + assertEquals( expected, actual ); + } + + @Test + public void testNoVersion() throws Exception + { + String input = "" + + "GROUPID" + + "ARTIFACTID" + + "RELATIVEPATH" + + ""; + String expected = "" + + "GROUPID" + + "ARTIFACTID" + + "RELATIVEPATH" + + "1.0.0" + + ""; + + 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 = "" + + "GROUPID" + + "ARTIFACTID" + + "RELATIVEPATH" + + ""; + String expected = input; + + String actual = transform( input, filter ); + + assertEquals( expected, actual ); + } + + @Test + public void testRelativePathAndVersion() throws Exception + { + String input = "" + + "GROUPID" + + "ARTIFACTID" + + "RELATIVEPATH" + + "1.0.0" + + ""; + String expected = "" + + "GROUPID" + + "ARTIFACTID" + + "RELATIVEPATH" + + "1.0.0" + + ""; + + String actual = transform( input ); + + assertEquals( expected, actual ); + } + + @Test + public void testWithWeirdNamespace() throws Exception + { + String input = "" + + "GROUPID" + + "ARTIFACTID" + + "RELATIVEPATH" + + ""; + String expected = "" + + "GROUPID" + + "ARTIFACTID" + + "RELATIVEPATH" + + "1.0.0" + + ""; + + String actual = transform( input ); + + assertEquals( expected, actual ); + } + + @Test + public void comment() throws Exception + { + String input = "" + + "GROUPID" + + "ARTIFACTID" + + "" + + "" + + ""; + String expected = "" + + "GROUPID" + + "ARTIFACTID" + + "" + + "1.0.0" + + "" + + ""; + + String actual = transform( input ); + + assertEquals( expected, actual ); + } + + @Test + public void testIndent() throws Exception + { + String input = "\n" + + " \n" + + " GROUPID\n" + + " ARTIFACTID\n" + + " \n" + + " \n" + + ""; + String expected = "" + System.lineSeparator() + + " " + System.lineSeparator() + + " GROUPID" + System.lineSeparator() + + " ARTIFACTID" + System.lineSeparator() + + " " + System.lineSeparator() + + " 1.0.0" + System.lineSeparator() + + " " + System.lineSeparator() + + ""; + + 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 000000000000..db6606cb6a68 --- /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 = "" + + "GROUPID" + + "ARTIFACTID" + + "VERSION" + + ""; + 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 = "" + + "GROUPID" + + "ARTIFACTID" + + ""; + String expected = input; + + String actual = transform( input, filter ); + + assertThat( actual ).isEqualTo( expected ); + } + + @Test + public void testReactorDependency() throws Exception + { + String input = "" + + "GROUPID" + + "ARTIFACTID" + + ""; + String expected = "" + + "GROUPID" + + "ARTIFACTID" + + "1.0.0" + + ""; + + String actual = transform( input ); + + assertThat( actual ).isEqualTo( expected ); + } + + @Test + public void testReactorDependencyLF() throws Exception + { + String input = "\n" + + " GROUPID\n" + + " ARTIFACTID\n" + + " " + + ""; + String expected = "\n" + + " GROUPID\n" + + " ARTIFACTID\n" + + " \n" + + " 1.0.0\n" + + ""; + + String actual = transform( input ); + + assertThat( actual ).and( expected ).ignoreWhitespace().areIdentical(); + } + + @Test + public void multipleDependencies() throws Exception { + String input = "\n" + + " 4.0.0\n" + + " tests.project\n" + + " duplicate-plugin-defs-merged\n" + + " 1\n" + + " \n" + + " \n" + + " \n" + + " maven-compiler-plugin\n" + + " \n" + + " \n" + + " group\n" + + " first\n" + + " 1\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " maven-compiler-plugin\n" + + " \n" + + " \n" + + " group\n" + + " second\n" + + " 1\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + 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 000000000000..00655b38f721 --- /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 = "\n" + + " \n" + + " GROUPID\n" + + " PARENT\n" + + " VERSION\n" + + " ../pom.xml\n" + + " \n" + + " PROJECT\n" + + ""; + String expected = "\n" + + " \n" + + " GROUPID\n" + + " PARENT\n" + + " VERSION\n" + + " \n" + + " PROJECT\n" + + ""; + String actual = transform( input ); + assertThat( actual ).and( expected ).areIdentical(); + } + + @Test + public void testRelativePathNS() throws Exception + { + String input = "\n" + + " \n" + + " GROUPID\n" + + " PARENT\n" + + " VERSION\n" + + " ../pom.xml\n" + + " \n" + + " PROJECT\n" + + ""; + String expected = "\n" + + " \n" + + " GROUPID\n" + + " PARENT\n" + + " VERSION\n" + + " \n" + + " PROJECT\n" + + ""; + String actual = transform( input ); + assertThat( actual ).and( expected ).areIdentical(); + } + + @Test + public void testRelativePathPasNS() throws Exception + { + String input = "\n" + + " \n" + + " GROUPID\n" + + " PARENT\n" + + " VERSION\n" + + " ../pom.xml\n" + + " \n" + + " PROJECT\n" + + ""; + String expected = "\n" + + " \n" + + " GROUPID\n" + + " PARENT\n" + + " VERSION\n" + + " \n" + + " PROJECT\n" + + ""; + String actual = transform( input ); + assertThat( actual ).and( expected ).areIdentical(); + } + +} diff --git a/pom.xml b/pom.xml index 29626fe89232..13b3e549fe1e 100644 --- a/pom.xml +++ b/pom.xml @@ -97,6 +97,7 @@ under the License. apache-maven maven-wrapper apache-maven/maven-wrapper.pom + maven-xml @@ -250,6 +251,11 @@ under the License. maven-slf4j-wrapper ${project.version} + + org.apache.maven + maven-xml + ${project.version} + @@ -419,6 +425,12 @@ under the License. ${mockitoVersion} test + + org.xmlunit + xmlunit-assertj + ${xmlunitVersion} + test + org.xmlunit xmlunit-core