diff --git a/pom.xml b/pom.xml index 620ed09ab..5afea400a 100644 --- a/pom.xml +++ b/pom.xml @@ -256,7 +256,7 @@ org.mockito - mockito-core + mockito-inline 4.7.0 test diff --git a/src/main/java/org/codehaus/mojo/versions/UseLatestVersionsMojo.java b/src/main/java/org/codehaus/mojo/versions/UseLatestVersionsMojo.java index 60301607a..cbfb7adf9 100644 --- a/src/main/java/org/codehaus/mojo/versions/UseLatestVersionsMojo.java +++ b/src/main/java/org/codehaus/mojo/versions/UseLatestVersionsMojo.java @@ -22,9 +22,8 @@ import javax.xml.stream.XMLStreamException; import java.io.IOException; -import java.util.ArrayList; import java.util.Collection; -import java.util.List; +import java.util.Collections; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.metadata.ArtifactMetadataRetrievalException; @@ -75,8 +74,33 @@ public class UseLatestVersionsMojo @Parameter( property = "allowIncrementalUpdates", defaultValue = "true" ) private boolean allowIncrementalUpdates; + /** + *

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

+ *

Only valid if allowSnapshots is false.

+ * + * @since 2.12.0 + */ + @Parameter( property = "allowDowngrade", + defaultValue = "false" ) + private boolean allowDowngrade; + // ------------------------------ METHODS -------------------------- + + /** + * {@inheritDoc} + */ + @Override + public void execute() throws MojoExecutionException, MojoFailureException + { + if ( allowDowngrade && allowSnapshots ) + { + throw new MojoExecutionException( "allowDowngrade is only valid with allowSnapshots equal to false" ); + } + super.execute(); + } + /** * @param pom the pom to update. * @throws org.apache.maven.plugin.MojoExecutionException when things go wrong @@ -109,9 +133,7 @@ protected void update( ModifiedPomXMLEventReader pom ) dependency.setGroupId( getProject().getParent().getGroupId() ); dependency.setVersion( getProject().getParent().getVersion() ); dependency.setType( "pom" ); - List list = new ArrayList(); - list.add( dependency ); - useLatestVersions( pom, list ); + useLatestVersions( pom, Collections.singletonList( dependency ) ); } } catch ( ArtifactMetadataRetrievalException | IOException e ) @@ -154,7 +176,8 @@ private void useLatestVersions( ModifiedPomXMLEventReader pom, Collection 0 ) 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 a06ba2c4f..ee2a954df 100644 --- a/src/main/java/org/codehaus/mojo/versions/api/AbstractVersionDetails.java +++ b/src/main/java/org/codehaus/mojo/versions/api/AbstractVersionDetails.java @@ -26,6 +26,7 @@ import org.apache.maven.artifact.versioning.ArtifactVersion; import org.apache.maven.artifact.versioning.DefaultArtifactVersion; import org.apache.maven.artifact.versioning.VersionRange; +import org.codehaus.mojo.versions.ordering.InvalidSegmentException; import org.codehaus.mojo.versions.ordering.VersionComparator; /** @@ -135,30 +136,6 @@ public final ArtifactVersion[] getVersions( ArtifactVersion currentVersion, Arti return getVersions( currentVersion, upperBound, includeSnapshots, false, false ); } - /** - * Gets newer versions of the specified artifact version. - * - * @param version The current version of the artifact. - * @param upperBoundFixedSegment Indicates the segment in the version number that cannot be changed. For example, a - * value of 0 indicates that the major version number cannot be changed. A value of -1 indicates any - * segment value can be changed. - * @param includeSnapshots Whether to include snapshot versions. - * @return Returns the newer artifact versions. - */ - private ArtifactVersion[] getNewerVersions( ArtifactVersion version, int upperBoundFixedSegment, - boolean includeSnapshots ) - { - ArtifactVersion lowerBound = version; - ArtifactVersion upperBound = null; - - if ( upperBoundFixedSegment != -1 ) - { - upperBound = getVersionComparator().incrementSegment( lowerBound, upperBoundFixedSegment ); - } - - return getVersions( version, upperBound, includeSnapshots, false, false ); - } - private ArtifactVersion[] getNewerVersions( ArtifactVersion version, boolean includeSnapshots ) { return getVersions( version, null, includeSnapshots, false, true ); @@ -243,9 +220,43 @@ public final ArtifactVersion[] getNewerVersions( String version, boolean include return getNewerVersions( new DefaultArtifactVersion( version ), includeSnapshots ); } + /** + * Returns an array of newer versions than the given version, given the upper bound segment and whether snapshots + * should be included. + * + * @param version current version + * @param upperBoundSegment the upper bound segment + * @param includeSnapshots whether snapshot versions should be included + * @deprecated please use {@link AbstractVersionDetails#getNewerVersions(String, int, boolean, boolean)} instead + * @return array of newer versions fulfilling the criteria + */ + @Deprecated public final ArtifactVersion[] getNewerVersions( String version, int upperBoundSegment, boolean includeSnapshots ) { - return getNewerVersions( new DefaultArtifactVersion( version ), upperBoundSegment, includeSnapshots ); + return getNewerVersions( version, upperBoundSegment, includeSnapshots, false ); + } + + /** + * Returns an array of newer versions than the given version, given the upper bound segment and whether snapshots + * should be included. + * + * @param versionString current version + * @param upperBoundSegment the upper bound segment + * @param includeSnapshots whether snapshot versions should be included + * @param allowDowngrade whether to allow downgrading if the current version is a snapshots and snapshots + * are disallowed + * @return array of newer versions fulfilling the criteria + */ + public final ArtifactVersion[] getNewerVersions( String versionString, int upperBoundSegment, + boolean includeSnapshots, boolean allowDowngrade ) + { + ArtifactVersion currentVersion = new DefaultArtifactVersion( versionString ); + ArtifactVersion lowerBound = + allowDowngrade ? getLowerBoundArtifactVersion( currentVersion, upperBoundSegment ) : currentVersion; + ArtifactVersion upperBound = upperBoundSegment == -1 ? null + : getVersionComparator().incrementSegment( lowerBound, upperBoundSegment ); + + return getVersions( lowerBound, upperBound, includeSnapshots, allowDowngrade, allowDowngrade ); } public final ArtifactVersion getOldestVersion( ArtifactVersion lowerBound, ArtifactVersion upperBound ) @@ -483,4 +494,52 @@ public ArtifactVersion[] getAllUpdates( VersionRange versionRange, boolean inclu { return getVersions( versionRange, getCurrentVersion(), null, includeSnapshots, false, true ); } + + protected ArtifactVersion getLowerBoundArtifactVersion( ArtifactVersion version, int segment ) + { + String lowerBound = getLowerBound( version, segment ); + return lowerBound != null ? new DefaultArtifactVersion( lowerBound ) : null; + } + + protected String getLowerBound( ArtifactVersion version, int segment ) + { + if ( segment < 0 ) + { + return null; + } + + int segmentCount = getVersionComparator().getSegmentCount( version ); + if ( segment > segmentCount ) + { + throw new InvalidSegmentException( segment, segmentCount, + version.toString() ); + } + + StringBuilder newVersion = new StringBuilder(); + newVersion.append( version.getMajorVersion() ); + if ( segmentCount > 0 ) + { + newVersion.append( "." ) + .append( segment >= 1 ? version.getMinorVersion() : 0 ); + } + if ( segmentCount > 1 ) + { + newVersion.append( "." ) + .append( segment >= 2 ? version.getIncrementalVersion() : 0 ); + } + if ( segmentCount > 2 ) + { + if ( version.getQualifier() != null ) + { + newVersion.append( "-" ) + .append( segment >= 3 ? version.getQualifier() : "0" ); + } + else + { + newVersion.append( "-" ) + .append( segment >= 3 ? version.getBuildNumber() : "0" ); + } + } + return newVersion.toString(); + } } diff --git a/src/main/java/org/codehaus/mojo/versions/api/PropertyVersions.java b/src/main/java/org/codehaus/mojo/versions/api/PropertyVersions.java index eb363589a..4ab81204b 100644 --- a/src/main/java/org/codehaus/mojo/versions/api/PropertyVersions.java +++ b/src/main/java/org/codehaus/mojo/versions/api/PropertyVersions.java @@ -23,7 +23,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; @@ -39,7 +38,6 @@ import org.apache.maven.artifact.versioning.VersionRange; import org.apache.maven.plugin.MojoExecutionException; import org.codehaus.mojo.versions.Property; -import org.codehaus.mojo.versions.ordering.InvalidSegmentException; import org.codehaus.mojo.versions.ordering.VersionComparator; /** @@ -145,12 +143,8 @@ public ArtifactAssociation[] getAssociations() private VersionComparator[] lookupComparators() { - Set result = new HashSet(); - for ( ArtifactAssociation association : associations ) - { - result.add( helper.getVersionComparator( association.getArtifact() ) ); - } - return result.toArray( new VersionComparator[0] ); + return associations.stream().map( association -> helper.getVersionComparator( association.getArtifact() ) ) + .distinct().toArray( VersionComparator[]::new ); } /** @@ -346,19 +340,15 @@ public ArtifactVersion getNewestVersion( String currentVersion, Property propert throw new MojoExecutionException( e.getMessage(), e ); } - ArtifactVersion lowerBoundArtifactVersion = null; + ArtifactVersion lowerBoundArtifactVersion = helper.createArtifactVersion( currentVersion ); if ( allowDowngrade ) { - if ( segment != -1 ) - { - lowerBoundArtifactVersion = getLowerBound( helper, currentVersion, segment ); - } - helper.getLog().debug( "lowerBoundArtifactVersion is null based on allowDowngrade:" + allowDowngrade ); + String updatedVersion = getLowerBound( lowerBoundArtifactVersion, segment ); + lowerBoundArtifactVersion = updatedVersion != null ? helper.createArtifactVersion( updatedVersion ) : null; } - else + if ( helper.getLog().isDebugEnabled() ) { - lowerBoundArtifactVersion = helper.createArtifactVersion( currentVersion ); - helper.getLog().debug( "lowerBoundArtifactVersion: " + lowerBoundArtifactVersion.toString() ); + helper.getLog().debug( "lowerBoundArtifactVersion: " + lowerBoundArtifactVersion ); } ArtifactVersion upperBound = null; @@ -375,7 +365,7 @@ public ArtifactVersion getNewestVersion( String currentVersion, Property propert if ( property.isSearchReactor() ) { helper.getLog().debug( "Property ${" + property.getName() + "}: Searching reactor for a valid version..." ); - Collection reactorArtifacts = helper.extractArtifacts( reactorProjects ); + Set reactorArtifacts = helper.extractArtifacts( reactorProjects ); ArtifactVersion[] reactorVersions = getVersions( reactorArtifacts ); helper.getLog().debug( "Property ${" + property.getName() + "}: Set of valid available versions from the reactor is " + Arrays.asList( @@ -527,45 +517,4 @@ public ArtifactVersion incrementSegment( ArtifactVersion v, int segment ) } } - - - private ArtifactVersion getLowerBound( VersionsHelper helper, - String currentVersion, int segment ) - { - ArtifactVersion version = helper.createArtifactVersion( currentVersion ); - int segmentCount = getVersionComparator().getSegmentCount( version ); - if ( segment < 0 || segment > segmentCount ) - { - throw new InvalidSegmentException( segment, segmentCount, - currentVersion ); - } - - StringBuilder newVersion = new StringBuilder(); - newVersion.append( segment >= 0 ? version.getMajorVersion() : 0 ); - if ( segmentCount > 0 ) - { - newVersion.append( "." ) - .append( segment >= 1 ? version.getMinorVersion() : 0 ); - } - if ( segmentCount > 1 ) - { - newVersion.append( "." ) - .append( segment >= 2 ? version.getIncrementalVersion() : 0 ); - } - if ( segmentCount > 2 ) - { - if ( version.getQualifier() != null ) - { - newVersion.append( "-" ) - .append( segment >= 3 ? version.getQualifier() : "0" ); - } - else - { - newVersion.append( "-" ) - .append( segment >= 3 ? version.getBuildNumber() : "0" ); - } - } - return helper.createArtifactVersion( newVersion.toString() ); - } - } diff --git a/src/test/java/org/codehaus/mojo/versions/UseLatestVersionsMojoTest.java b/src/test/java/org/codehaus/mojo/versions/UseLatestVersionsMojoTest.java new file mode 100644 index 000000000..4d64826b3 --- /dev/null +++ b/src/test/java/org/codehaus/mojo/versions/UseLatestVersionsMojoTest.java @@ -0,0 +1,244 @@ +package org.codehaus.mojo.versions; + +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; +import org.apache.maven.artifact.metadata.ArtifactMetadataSource; +import org.apache.maven.artifact.versioning.DefaultArtifactVersion; +import org.apache.maven.model.Dependency; +import org.apache.maven.model.DependencyManagement; +import org.apache.maven.model.Model; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugin.testing.AbstractMojoTestCase; +import org.apache.maven.project.MavenProject; +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.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; + +import static org.apache.maven.artifact.Artifact.SCOPE_COMPILE; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +@SuppressWarnings( "deprecation" ) +public class UseLatestVersionsMojoTest extends AbstractMojoTestCase +{ + private UseLatestVersionsMojo mojo; + private TestChangeRecorder changeRecorder; + + @Before + public void setUp() throws Exception + { + super.setUp(); + RepositorySystem repositorySystemMock = mock( RepositorySystem.class ); + when( repositorySystemMock.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 ); + } ); + + ArtifactMetadataSource artifactMetadataSourceMock = mock( ArtifactMetadataSource.class ); + when( artifactMetadataSourceMock.retrieveAvailableVersions( any( Artifact.class ), any(), any() ) ).then( + invocation -> + { + Artifact artifact = invocation.getArgument( 0 ); + if ( "dependency-artifact".equals( artifact.getArtifactId() ) ) + { + return Arrays.asList( new DefaultArtifactVersion( "1.1.1-SNAPSHOT" ), + new DefaultArtifactVersion( "1.1.0" ), new DefaultArtifactVersion( "1.1.0-SNAPSHOT" ), + new DefaultArtifactVersion( "1.0.0" ), new DefaultArtifactVersion( "1.0.0-SNAPSHOT" ), + new DefaultArtifactVersion( "0.9.0" ) ); + } + fail(); + return null; + } ); + + changeRecorder = new TestChangeRecorder(); + + mojo = new UseLatestVersionsMojo() + {{ + MavenProject project = new MavenProject() + {{ + setModel( new Model() + {{ + setGroupId( "default-group" ); + setArtifactId( "project-artifact" ); + setVersion( "1.0.0-SNAPSHOT" ); + + setDependencies( Collections.singletonList( + DependencyBuilder.dependencyWith( "default-group", "dependency-artifact", "1.1.1-SNAPSHOT", + "default", "pom", SCOPE_COMPILE ) ) ); + + setDependencyManagement( new DependencyManagement() ); + getDependencyManagement().setDependencies( Collections.singletonList( + DependencyBuilder.dependencyWith( "default-group", "dependency-artifact", "1.1.1-SNAPSHOT", + "default", "pom", SCOPE_COMPILE ) ) ); + }} ); + }}; + setProject( project ); + repositorySystem = repositorySystemMock; + artifactMetadataSource = artifactMetadataSourceMock; + 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 + { + setVariableValueToObject( mojo, "processDependencies", true ); + setVariableValueToObject( mojo, "allowSnapshots", false ); + setVariableValueToObject( mojo, "allowMajorUpdates", false ); + setVariableValueToObject( mojo, "allowMinorUpdates", true ); + setVariableValueToObject( mojo, "allowIncrementalUpdates", true ); + setVariableValueToObject( mojo, "allowDowngrade", true ); + + try ( MockedStatic pomHelper = mockStatic( PomHelper.class ) ) + { + pomHelper.when( () -> PomHelper.setDependencyVersion( any(), any(), any(), any(), any(), any() ) ) + .thenReturn( true ); + mojo.update( null ); + } + assertThat( changeRecorder.getChanges(), + hasItem( new VersionChange( "default-group", "dependency-artifact", "1.1.1-SNAPSHOT", + "1.1.0" ) ) ); + } + + @Test + public void testDependenciesDowngradeMinor() + throws MojoExecutionException, XMLStreamException, MojoFailureException, IllegalAccessException + { + setVariableValueToObject( mojo, "processDependencies", true ); + setVariableValueToObject( mojo, "allowSnapshots", false ); + setVariableValueToObject( mojo, "allowMajorUpdates", false ); + setVariableValueToObject( mojo, "allowMinorUpdates", true ); + setVariableValueToObject( mojo, "allowIncrementalUpdates", false ); + setVariableValueToObject( mojo, "allowDowngrade", true ); + + try ( MockedStatic pomHelper = mockStatic( PomHelper.class ) ) + { + pomHelper.when( () -> PomHelper.setDependencyVersion( any(), any(), any(), any(), any(), any() ) ) + .thenReturn( true ); + mojo.update( null ); + } + assertThat( changeRecorder.getChanges(), + hasItem( new VersionChange( "default-group", "dependency-artifact", "1.1.1-SNAPSHOT", + "1.0.0" ) ) ); + } + + @Test + public void testDependenciesDowngradeMajor() + throws MojoExecutionException, XMLStreamException, MojoFailureException, IllegalAccessException + { + setVariableValueToObject( mojo, "processDependencies", true ); + setVariableValueToObject( mojo, "allowSnapshots", false ); + setVariableValueToObject( mojo, "allowMajorUpdates", true ); + setVariableValueToObject( mojo, "allowMinorUpdates", false ); + setVariableValueToObject( mojo, "allowIncrementalUpdates", false ); + setVariableValueToObject( mojo, "allowDowngrade", true ); + + try ( MockedStatic pomHelper = mockStatic( PomHelper.class ) ) + { + pomHelper.when( () -> PomHelper.setDependencyVersion( any(), any(), any(), any(), any(), any() ) ) + .thenReturn( true ); + mojo.update( null ); + } + assertThat( changeRecorder.getChanges(), + hasItem( new VersionChange( "default-group", "dependency-artifact", "1.1.1-SNAPSHOT", + "0.9.0" ) ) ); + } + + @Test + public void testDependencyManagementDowngrade() + throws MojoExecutionException, XMLStreamException, MojoFailureException, IllegalAccessException + { + setVariableValueToObject( mojo, "processDependencyManagement", true ); + setVariableValueToObject( mojo, "allowSnapshots", false ); + setVariableValueToObject( mojo, "allowMajorUpdates", false ); + setVariableValueToObject( mojo, "allowMinorUpdates", true ); + setVariableValueToObject( mojo, "allowIncrementalUpdates", true ); + setVariableValueToObject( mojo, "allowDowngrade", true ); + + try ( MockedStatic pomHelper = mockStatic( PomHelper.class ) ) + { + pomHelper.when( () -> PomHelper.getRawModel( any( MavenProject.class ) ) ) + .thenReturn( mojo.getProject().getModel() ); + pomHelper.when( () -> PomHelper.setDependencyVersion( any(), any(), any(), any(), any(), any() ) ) + .thenReturn( true ); + mojo.update( null ); + } + assertThat( changeRecorder.getChanges(), + hasItem( new VersionChange( "default-group", "dependency-artifact", "1.1.1-SNAPSHOT", + "1.1.0" ) ) ); + } + + @Test + public void testParentDowngrade() + throws MojoExecutionException, XMLStreamException, MojoFailureException, IllegalAccessException + { + setVariableValueToObject( mojo, "processParent", true ); + setVariableValueToObject( mojo, "allowSnapshots", false ); + setVariableValueToObject( mojo, "allowMajorUpdates", false ); + setVariableValueToObject( mojo, "allowMinorUpdates", true ); + setVariableValueToObject( mojo, "allowIncrementalUpdates", true ); + setVariableValueToObject( mojo, "allowDowngrade", true ); + + mojo.getProject().setParentArtifact( new DefaultArtifact( "default-group", "dependency-artifact", + "1.1.1-SNAPSHOT", "compile", "pom", "default", null ) ); + mojo.getProject().setParent( new MavenProject() + {{ + setGroupId( mojo.getProject().getParentArtifact().getGroupId() ); + setArtifactId( mojo.getProject().getParentArtifact().getArtifactId() ); + setVersion( mojo.getProject().getParentArtifact().getVersion() ); + }} ); + + + try ( MockedStatic pomHelper = mockStatic( PomHelper.class ) ) + { + pomHelper.when( () -> PomHelper.setDependencyVersion( any(), any(), any(), any(), any(), any() ) ) + .thenReturn( true ); + mojo.update( null ); + } + assertThat( changeRecorder.getChanges(), + hasItem( new VersionChange( "default-group", "dependency-artifact", "1.1.1-SNAPSHOT", + "1.1.0" ) ) ); + } +} \ No newline at end of file