From b551d90f0a9bfa284d613173bc7785305260b95e Mon Sep 17 00:00:00 2001 From: Andrzej Jarmoniuk Date: Wed, 31 Aug 2022 08:10:40 +0200 Subject: [PATCH] Fixing #231 bug/feature: added "allowDowngrade", default "false", fixed range handling wrt downgrading --- .../versions/AbstractVersionsUpdaterMojo.java | 24 +- .../mojo/versions/UpdateParentMojo.java | 34 ++- .../versions/api/AbstractVersionDetails.java | 36 ++- .../mojo/versions/api/ArtifactVersions.java | 26 +- .../mojo/versions/UpdateParentMojoTest.java | 235 ++++++++++++++++-- .../versions/UseLatestVersionsMojoTest.java | 30 +-- .../versions/utils/TestChangeRecorder.java | 48 ++++ 7 files changed, 342 insertions(+), 91 deletions(-) create mode 100644 src/test/java/org/codehaus/mojo/versions/utils/TestChangeRecorder.java diff --git a/src/main/java/org/codehaus/mojo/versions/AbstractVersionsUpdaterMojo.java b/src/main/java/org/codehaus/mojo/versions/AbstractVersionsUpdaterMojo.java index 83121bda2..113f73344 100644 --- a/src/main/java/org/codehaus/mojo/versions/AbstractVersionsUpdaterMojo.java +++ b/src/main/java/org/codehaus/mojo/versions/AbstractVersionsUpdaterMojo.java @@ -289,6 +289,27 @@ public void execute() */ protected ArtifactVersion findLatestVersion( Artifact artifact, VersionRange versionRange, Boolean allowingSnapshots, boolean usePluginRepositories ) + throws ArtifactMetadataRetrievalException, MojoExecutionException + { + return findLatestVersion( artifact, versionRange, allowingSnapshots, usePluginRepositories, false ); + } + + /** + * Finds the latest version of the specified artifact that matches the version range. + * + * @param artifact The artifact. + * @param versionRange The version range. + * @param allowingSnapshots null for no override, otherwise the local override to apply. + * @param usePluginRepositories Use plugin repositories + * @return The latest version of the specified artifact that matches the specified version range or + * null if no matching version could be found. + * @throws ArtifactMetadataRetrievalException If the artifact metadata could not be found. + * @throws MojoExecutionException if something goes wrong. + * @since 1.0-alpha-1 + */ + protected ArtifactVersion findLatestVersion( Artifact artifact, VersionRange versionRange, + Boolean allowingSnapshots, boolean usePluginRepositories, + boolean allowDowngrade ) throws ArtifactMetadataRetrievalException, MojoExecutionException { boolean includeSnapshots = this.allowSnapshots; @@ -301,7 +322,8 @@ protected ArtifactVersion findLatestVersion( Artifact artifact, VersionRange ver includeSnapshots = false; } final ArtifactVersions artifactVersions = getHelper().lookupArtifactVersions( artifact, usePluginRepositories ); - return artifactVersions.getNewestVersion( versionRange, includeSnapshots ); + return artifactVersions.getNewestVersion( versionRange, null, null, includeSnapshots, + true, true, allowDowngrade ); } /** diff --git a/src/main/java/org/codehaus/mojo/versions/UpdateParentMojo.java b/src/main/java/org/codehaus/mojo/versions/UpdateParentMojo.java index bfa125301..68893603f 100644 --- a/src/main/java/org/codehaus/mojo/versions/UpdateParentMojo.java +++ b/src/main/java/org/codehaus/mojo/versions/UpdateParentMojo.java @@ -65,6 +65,17 @@ public class UpdateParentMojo extends AbstractVersionsUpdaterMojo @Parameter( property = "forceUpdate", defaultValue = "false" ) protected boolean forceUpdate = false; + /** + *

Whether to downgrade a snapshot dependency if allowSnapshots is false + * and there exists a version within the range fulfilling the criteria.

+ *

Default false

+ * + * @since 2.12.0 + */ + @Parameter( property = "allowDowngrade", + defaultValue = "false" ) + protected boolean allowDowngrade; + // -------------------------- OTHER METHODS -------------------------- /** @@ -98,33 +109,40 @@ protected void update( ModifiedPomXMLEventReader pom ) version = parentVersion; } + Dependency dependency = new Dependency(); + dependency.setGroupId( getProject().getParent().getGroupId() ); + dependency.setArtifactId( getProject().getParent().getArtifactId() ); + dependency.setVersion( version ); + dependency.setType( "pom" ); + Artifact artifact = getHelper().createDependencyArtifact( dependency ); + VersionRange versionRange; try { versionRange = VersionRange.createFromVersionSpec( version ); + if ( versionRange.getRecommendedVersion() != null ) + { + versionRange = versionRange.restrict( + VersionRange.createFromVersionSpec( "[" + versionRange.getRecommendedVersion() + ",)" ) ); + } } catch ( InvalidVersionSpecificationException e ) { throw new MojoExecutionException( "Invalid version range specification: " + version, e ); } - Dependency dependency = new Dependency(); - dependency.setGroupId( getProject().getParent().getGroupId() ); - dependency.setArtifactId( getProject().getParent().getArtifactId() ); - dependency.setVersion( version ); - dependency.setType( "pom" ); - Artifact artifact = getHelper().createDependencyArtifact( dependency ); - ArtifactVersion artifactVersion; try { - artifactVersion = findLatestVersion( artifact, versionRange, null, false ); + artifactVersion = findLatestVersion( artifact, versionRange, false, true, + allowDowngrade ); } catch ( ArtifactMetadataRetrievalException e ) { throw new MojoExecutionException( e.getMessage(), e ); } + if ( !shouldApplyUpdate( artifact, currentVersion, artifactVersion, forceUpdate ) ) { return; diff --git a/src/main/java/org/codehaus/mojo/versions/api/AbstractVersionDetails.java b/src/main/java/org/codehaus/mojo/versions/api/AbstractVersionDetails.java index ee2a954df..1cd11640f 100644 --- a/src/main/java/org/codehaus/mojo/versions/api/AbstractVersionDetails.java +++ b/src/main/java/org/codehaus/mojo/versions/api/AbstractVersionDetails.java @@ -19,8 +19,11 @@ * under the License. */ +import java.util.Arrays; +import java.util.Collections; import java.util.Set; import java.util.TreeSet; +import java.util.stream.Collectors; import org.apache.maven.artifact.ArtifactUtils; import org.apache.maven.artifact.versioning.ArtifactVersion; @@ -156,11 +159,26 @@ public final ArtifactVersion getNewestVersion( VersionRange versionRange, Artifa ArtifactVersion upperBound, boolean includeSnapshots, boolean includeLower, boolean includeUpper ) { - ArtifactVersion latest = null; + return getNewestVersion( versionRange, lowerBound, upperBound, includeSnapshots, includeLower, + includeUpper, false ); + } + + private static Iterable reverse( T[] array ) + { + return Arrays.stream( array ).sorted( Collections.reverseOrder() ).collect( Collectors.toList() ); + } + + public final ArtifactVersion getNewestVersion( VersionRange versionRange, ArtifactVersion lowerBound, + ArtifactVersion upperBound, boolean includeSnapshots, + boolean includeLower, boolean includeUpper, boolean allowDowngrade ) + { final VersionComparator versionComparator = getVersionComparator(); - for ( ArtifactVersion candidate : getVersions( includeSnapshots ) ) + // reverse( getVersions( ... ) ) will contain versions sorted from latest to oldest, + // so we only need to find the first candidate fulfilling the criteria + for ( ArtifactVersion candidate : reverse( getVersions( includeSnapshots ) ) ) { - if ( versionRange != null && !ArtifactVersions.isVersionInRange( candidate, versionRange ) ) + if ( !allowDowngrade && versionRange != null + && !ArtifactVersions.isVersionInRange( candidate, versionRange ) ) { continue; } @@ -178,17 +196,9 @@ public final ArtifactVersion getNewestVersion( VersionRange versionRange, Artifa { continue; } - if ( latest == null ) - { - latest = candidate; - } - else if ( versionComparator.compare( latest, candidate ) < 0 ) - { - latest = candidate; - } - + return candidate; } - return latest; + return null; } public final ArtifactVersion getNewestVersion( ArtifactVersion lowerBound, ArtifactVersion upperBound, diff --git a/src/main/java/org/codehaus/mojo/versions/api/ArtifactVersions.java b/src/main/java/org/codehaus/mojo/versions/api/ArtifactVersions.java index bb15f2ec1..e5d9df192 100644 --- a/src/main/java/org/codehaus/mojo/versions/api/ArtifactVersions.java +++ b/src/main/java/org/codehaus/mojo/versions/api/ArtifactVersions.java @@ -20,7 +20,6 @@ */ import java.util.List; -import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; @@ -96,7 +95,8 @@ public static boolean isVersionInRange( ArtifactVersion version, VersionRange ra { return false; } - for ( Restriction r : ( (List) range.getRestrictions() ) ) + + for ( Restriction r : range.getRestrictions() ) { if ( r.containsVersion( version ) ) { @@ -157,24 +157,10 @@ public String getArtifactId() public ArtifactVersion[] getVersions( boolean includeSnapshots ) { - Set result; - if ( includeSnapshots ) - { - result = versions; - } - else - { - result = new TreeSet<>( versionComparator ); - for ( ArtifactVersion candidate : versions ) - { - if ( ArtifactUtils.isSnapshot( candidate.toString() ) ) - { - continue; - } - result.add( candidate ); - } - } - return result.toArray( new ArtifactVersion[0] ); + return includeSnapshots + ? versions.toArray( new ArtifactVersion[0] ) + : versions.stream().filter( v -> !ArtifactUtils.isSnapshot( v.toString() ) ) + .toArray( ArtifactVersion[]::new ); } public VersionComparator getVersionComparator() diff --git a/src/test/java/org/codehaus/mojo/versions/UpdateParentMojoTest.java b/src/test/java/org/codehaus/mojo/versions/UpdateParentMojoTest.java index e5a25b1d3..0faeec399 100644 --- a/src/test/java/org/codehaus/mojo/versions/UpdateParentMojoTest.java +++ b/src/test/java/org/codehaus/mojo/versions/UpdateParentMojoTest.java @@ -2,47 +2,238 @@ import javax.xml.stream.XMLStreamException; +import java.util.Arrays; import java.util.Collections; import org.apache.maven.artifact.Artifact; -import org.apache.maven.artifact.versioning.ArtifactVersion; +import org.apache.maven.artifact.DefaultArtifact; +import org.apache.maven.artifact.metadata.ArtifactMetadataRetrievalException; +import org.apache.maven.artifact.metadata.ArtifactMetadataSource; +import org.apache.maven.artifact.resolver.ArtifactResolver; +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.model.Dependency; +import org.apache.maven.model.Model; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.project.MavenProject; -import org.apache.maven.project.artifact.ProjectArtifact; import org.apache.maven.repository.RepositorySystem; +import org.codehaus.mojo.versions.api.PomHelper; +import org.codehaus.mojo.versions.change.VersionChange; +import org.codehaus.mojo.versions.utils.TestChangeRecorder; +import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; +import org.mockito.MockedStatic; +import static org.apache.maven.artifact.Artifact.SCOPE_COMPILE; +import static org.apache.maven.plugin.testing.ArtifactStubFactory.setVariableValueToObject; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; public class UpdateParentMojoTest { + private TestChangeRecorder changeRecorder; + + private UpdateParentMojo mojo; + + private ArtifactResolver artifactResolver; + + private static RepositorySystem repositorySystem; + + @SuppressWarnings( "deprecation" ) + private static ArtifactMetadataSource artifactMetadataSource; + + @BeforeClass + @SuppressWarnings( "deprecation" ) + public static void setUpStatic() throws ArtifactMetadataRetrievalException + { + repositorySystem = mockRepositorySystem(); + artifactMetadataSource = mockArtifactMetaDataSource(); + } + + @Before + public void setUp() throws IllegalAccessException + { + changeRecorder = new TestChangeRecorder(); + artifactResolver = mock( ArtifactResolver.class ); + + mojo = new UpdateParentMojo() + {{ + setProject( createProject() ); + reactorProjects = Collections.emptyList(); + repositorySystem = UpdateParentMojoTest.repositorySystem; + artifactMetadataSource = UpdateParentMojoTest.artifactMetadataSource; + resolver = UpdateParentMojoTest.this.artifactResolver; + + setVariableValueToObject( this, "changeRecorder", changeRecorder ); + }}; + } + + private MavenProject createProject() + { + return new MavenProject() + {{ + setModel( new Model() + {{ + setGroupId( "default-group" ); + setArtifactId( "project-artifact" ); + setVersion( "1.0.1-SNAPSHOT" ); + }} ); + + setParent( new MavenProject() + {{ + setGroupId( "default-group" ); + setArtifactId( "parent-artifact" ); + setVersion( "1.0.1-SNAPSHOT" ); + }} ); + }}; + } + + private static RepositorySystem mockRepositorySystem() + { + RepositorySystem repositorySystem = mock( RepositorySystem.class ); + when( repositorySystem.createDependencyArtifact( any( Dependency.class ) ) ).thenAnswer( invocation -> + { + Dependency dependency = invocation.getArgument( 0 ); + return new DefaultArtifact( dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion(), + dependency.getScope(), dependency.getType(), dependency.getClassifier() != null + ? dependency.getClassifier() : "default", null ); + } ); + return repositorySystem; + } + + @SuppressWarnings( "deprecation" ) + private static ArtifactMetadataSource mockArtifactMetaDataSource() throws ArtifactMetadataRetrievalException + { + ArtifactMetadataSource artifactMetadataSource = mock( ArtifactMetadataSource.class ); + when( artifactMetadataSource.retrieveAvailableVersions( any( Artifact.class ), any(), any() ) ).then( + invocation -> + { + Artifact artifact = invocation.getArgument( 0 ); + if ( "parent-artifact".equals( artifact.getArtifactId() ) ) + { + return Arrays.asList( new DefaultArtifactVersion( "1.0.1-SNAPSHOT" ), + new DefaultArtifactVersion( "1.0.0" ), + new DefaultArtifactVersion( "0.9.0" ) ); + } + else if ( "unknown-artifact".equals( artifact.getArtifactId() ) ) + { + return Collections.emptyList(); + } + fail(); + return null; + } ); + return artifactMetadataSource; + } @Test - public void testArtifactIdDoesNotExist() throws MojoExecutionException, XMLStreamException, MojoFailureException + @SuppressWarnings( "deprecation" ) + public void testArtifactIdDoesNotExist() + throws ArtifactMetadataRetrievalException, MojoExecutionException, + XMLStreamException, MojoFailureException, InvalidVersionSpecificationException { - UpdateParentMojo mojo = new UpdateParentMojo() + mojo.getProject().setParent( new MavenProject() + {{ + setGroupId( "default-group" ); + setArtifactId( "unknown-artifact" ); + setVersion( "1.0.1-SNAPSHOT" ); + }} ); + + Artifact artifact = + new DefaultArtifact( "default-group", "unknown-artifact", "1.0.1-SNAPSHOT", SCOPE_COMPILE, "pom", + "default", null ); + assertThat( + mojo.findLatestVersion( artifact, VersionRange.createFromVersionSpec( "1.0.1-SNAPSHOT" ), null, false ), + is( nullValue() ) ); + + try ( MockedStatic pomHelper = mockStatic( PomHelper.class ) ) + { + pomHelper.when( () -> PomHelper.setProjectParentVersion( any(), any() ) ).thenReturn( true ); + mojo.update( null ); + } + } + + @Test + public void testParentDowngradeAllowed() + throws MojoExecutionException, XMLStreamException, MojoFailureException + { + mojo.allowDowngrade = true; + try ( MockedStatic pomHelper = mockStatic( PomHelper.class ) ) + { + pomHelper.when( () -> PomHelper.setProjectParentVersion( any(), any() ) ) + .thenReturn( true ); + mojo.update( null ); + } + assertThat( changeRecorder.getChanges(), + hasItem( new VersionChange( "default-group", "parent-artifact", "1.0.1-SNAPSHOT", + "1.0.0" ) ) ); + } + + @Test + public void testParentDowngradeForbidden() + throws MojoExecutionException, XMLStreamException, MojoFailureException + { + mojo.allowDowngrade = false; + try ( MockedStatic pomHelper = mockStatic( PomHelper.class ) ) + { + pomHelper.when( () -> PomHelper.setProjectParentVersion( any(), any() ) ) + .thenReturn( true ); + mojo.update( null ); + } + assertThat( changeRecorder.getChanges(), is( empty() ) ); + } + + @Test + public void testParentDowngradeAllowedWithRange() + throws MojoExecutionException, XMLStreamException, MojoFailureException + { + mojo.allowDowngrade = true; + mojo.getProject().setParent( new MavenProject() + {{ + setGroupId( "default-group" ); + setArtifactId( "parent-artifact" ); + setVersion( "[1.0.1-SNAPSHOT,)" ); + }} ); + + try ( MockedStatic pomHelper = mockStatic( PomHelper.class ) ) + { + pomHelper.when( () -> PomHelper.setProjectParentVersion( any(), any() ) ) + .thenReturn( true ); + mojo.update( null ); + } + assertThat( changeRecorder.getChanges(), + hasItem( new VersionChange( "default-group", "parent-artifact", "[1.0.1-SNAPSHOT,)", + "1.0.0" ) ) ); + } + + @Test + public void testParentDowngradeForbiddenWithRange() + throws MojoExecutionException, XMLStreamException, MojoFailureException + { + mojo.allowDowngrade = false; + mojo.getProject().setParent( new MavenProject() + {{ + setGroupId( "default-group" ); + setArtifactId( "parent-artifact" ); + setVersion( "[1.0.1-SNAPSHOT,)" ); + }} ); + + try ( MockedStatic pomHelper = mockStatic( PomHelper.class ) ) { - { - project = new MavenProject(); - project.setParent( new MavenProject() ); - reactorProjects = Collections.emptyList(); - forceUpdate = true; - - repositorySystem = mock( RepositorySystem.class ); - when( repositorySystem.createDependencyArtifact( any() ) ) - .thenReturn( new ProjectArtifact( project ) ); - } - - protected ArtifactVersion findLatestVersion( Artifact artifact, VersionRange versionRange, - Boolean allowingSnapshots, boolean usePluginRepositories ) - { - return null; - } - }; - mojo.update( null ); + pomHelper.when( () -> PomHelper.setProjectParentVersion( any(), any() ) ) + .thenReturn( true ); + mojo.update( null ); + } + assertThat( changeRecorder.getChanges(), is( empty() ) ); } } diff --git a/src/test/java/org/codehaus/mojo/versions/UseLatestVersionsMojoTest.java b/src/test/java/org/codehaus/mojo/versions/UseLatestVersionsMojoTest.java index 4d64826b3..5ac43e263 100644 --- a/src/test/java/org/codehaus/mojo/versions/UseLatestVersionsMojoTest.java +++ b/src/test/java/org/codehaus/mojo/versions/UseLatestVersionsMojoTest.java @@ -2,11 +2,8 @@ import javax.xml.stream.XMLStreamException; -import java.io.OutputStream; import java.util.Arrays; import java.util.Collections; -import java.util.LinkedList; -import java.util.List; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.DefaultArtifact; @@ -22,7 +19,7 @@ import org.apache.maven.repository.RepositorySystem; import org.codehaus.mojo.versions.api.PomHelper; import org.codehaus.mojo.versions.change.VersionChange; -import org.codehaus.mojo.versions.recording.ChangeRecorder; +import org.codehaus.mojo.versions.utils.TestChangeRecorder; import org.junit.Before; import org.junit.Test; import org.mockito.MockedStatic; @@ -70,8 +67,6 @@ public void setUp() throws Exception return null; } ); - changeRecorder = new TestChangeRecorder(); - mojo = new UseLatestVersionsMojo() {{ MavenProject project = new MavenProject() @@ -95,31 +90,12 @@ public void setUp() throws Exception setProject( project ); repositorySystem = repositorySystemMock; artifactMetadataSource = artifactMetadataSourceMock; + + changeRecorder = new TestChangeRecorder(); setVariableValueToObject( this, "changeRecorder", changeRecorder ); }}; } - private static class TestChangeRecorder implements ChangeRecorder - { - private final List changes = new LinkedList<>(); - - @Override - public void recordUpdate( String kind, String groupId, String artifactId, String oldVersion, String newVersion ) - { - changes.add( new VersionChange( groupId, artifactId, oldVersion, newVersion ) ); - } - - @Override - public void serialize( OutputStream outputStream ) - { - } - - public List getChanges() - { - return changes; - } - } - @Test public void testDependenciesDowngradeIncremental() throws MojoExecutionException, XMLStreamException, MojoFailureException, IllegalAccessException diff --git a/src/test/java/org/codehaus/mojo/versions/utils/TestChangeRecorder.java b/src/test/java/org/codehaus/mojo/versions/utils/TestChangeRecorder.java new file mode 100644 index 000000000..2966cafe6 --- /dev/null +++ b/src/test/java/org/codehaus/mojo/versions/utils/TestChangeRecorder.java @@ -0,0 +1,48 @@ +package org.codehaus.mojo.versions.utils; + +/* + * 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.OutputStream; +import java.util.LinkedList; +import java.util.List; + +import org.codehaus.mojo.versions.change.VersionChange; +import org.codehaus.mojo.versions.recording.ChangeRecorder; + +public class TestChangeRecorder implements ChangeRecorder +{ + private final List changes = new LinkedList<>(); + + @Override + public void recordUpdate( String kind, String groupId, String artifactId, String oldVersion, String newVersion ) + { + changes.add( new VersionChange( groupId, artifactId, oldVersion, newVersion ) ); + } + + @Override + public void serialize( OutputStream outputStream ) + { + } + + public List getChanges() + { + return changes; + } +} \ No newline at end of file