diff --git a/versions-common/src/main/java/org/codehaus/mojo/versions/api/AbstractVersionDetails.java b/versions-common/src/main/java/org/codehaus/mojo/versions/api/AbstractVersionDetails.java index 1b1a5a080c..a2a0e757ee 100644 --- a/versions-common/src/main/java/org/codehaus/mojo/versions/api/AbstractVersionDetails.java +++ b/versions-common/src/main/java/org/codehaus/mojo/versions/api/AbstractVersionDetails.java @@ -20,11 +20,19 @@ */ import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.Objects; import java.util.Optional; +import java.util.TreeSet; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.maven.artifact.ArtifactUtils; import org.apache.maven.artifact.versioning.ArtifactVersion; import org.apache.maven.artifact.versioning.DefaultArtifactVersion; +import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException; import org.apache.maven.artifact.versioning.Restriction; import org.apache.maven.artifact.versioning.VersionRange; import org.codehaus.mojo.versions.ordering.BoundArtifactVersion; @@ -46,6 +54,10 @@ public abstract class AbstractVersionDetails implements VersionDetails { + private static final Pattern PREVIEW_PATTERN = + Pattern.compile( "(?i)(?:.*[-.](alpha|a|beta|b|milestone|m|preview|rc)" + + "[-.]?(\\d{0,2}[a-z]?|\\d{6}\\.\\d{4})|\\d{8}(?:\\.?\\d{6})?)$" ); + /** * The current version. Guarded by {@link #currentVersionLock}. * @@ -60,6 +72,8 @@ public abstract class AbstractVersionDetails */ private boolean includeSnapshots = false; + protected boolean verboseDetail = true; + /** * Not sure if we need to be thread safe, but there's no harm being careful, after all we could be invoked from an * IDE. @@ -441,7 +455,7 @@ protected Optional getLowerBound( ArtifactVersion version, Optionalnull if no such version exists. + * @param updateScope the scope of updates to include. + * @return the newest version after currentVersion within the specified update scope, + * or null if no version is available. + */ + public final ArtifactVersion getReportNewestUpdate( Optional updateScope ) + { + return getArtifactVersionStream( updateScope ) + .min( Collections.reverseOrder( getVersionComparator() ) ).orElse( null ); + } + + /** + * Returns all versions newer than the specified current version, and within the specified update scope. + * @param updateScope the scope of updates to include. + * @return all versions after currentVersion within the specified update scope. + */ + public final ArtifactVersion[] getReportUpdates( Optional updateScope ) + { + TreeSet versions = getArtifactVersionStream( updateScope ) + .collect( Collectors.toCollection( () -> new TreeSet<>( getVersionComparator() ) ) ); + // filter out intermediate minor versions. + if ( !verboseDetail ) + { + int major = 0; + int minor = 0; + boolean needOneMore = false; + for ( Iterator it = versions.descendingIterator(); it.hasNext(); ) + { + ArtifactVersion version = it.next(); + boolean isPreview = PREVIEW_PATTERN.matcher( version.toString() ).matches(); + + // encountered a version in same Major.Minor version, remove it. + if ( version.getMajorVersion() == major && version.getMinorVersion() == minor ) + { + if ( needOneMore && !isPreview ) + { + needOneMore = false; + continue; + } + it.remove(); + continue; + } + + // encountered a new Major.Minor version, keep it. + major = version.getMajorVersion(); + minor = version.getMinorVersion(); + + // if version is a pre-release, also search for the last release. + needOneMore = isPreview; + + } + } + return versions.toArray( new ArtifactVersion[0] ); + } + + /** + * Returns all versions newer than the specified current version, and within the specified update scope. + * @param updateScope the scope of updates to include. + * @return all versions after currentVersion within the specified update scope. + */ + private Stream getArtifactVersionStream( Optional updateScope ) + { + if ( isCurrentVersionDefined() ) + { + try + { + // one range spec can have multiple restrictions, and multiple 'lower bound', we want the highest one. + // [1.0,2.0),[3.0,4.0) -> 3.0 + ArtifactVersion highestLowerBound = VersionRange.createFromVersionSpec( getCurrentVersion().toString() ) + .getRestrictions().stream().map( Restriction::getLowerBound ).filter( Objects::nonNull ) + .max( getVersionComparator() ).orElse( getCurrentVersion() ); + Restriction restriction = getVersionComparator().restrictionFor( highestLowerBound, updateScope ); + + return Arrays.stream( getVersions() ).filter( + candidate -> ( isIncludeSnapshots() || !ArtifactUtils.isSnapshot( candidate.toString() ) ) + && isVersionInRestriction( restriction, candidate ) ); + } + catch ( InvalidVersionSpecificationException | InvalidSegmentException ignored ) + { + ignored.printStackTrace( System.err ); + } + } + return Stream.empty(); + } + } diff --git a/versions-maven-plugin/src/it/it-823-ranges-update-report-001/invoker.properties b/versions-maven-plugin/src/it/it-823-ranges-update-report-001/invoker.properties new file mode 100644 index 0000000000..53663b3aa4 --- /dev/null +++ b/versions-maven-plugin/src/it/it-823-ranges-update-report-001/invoker.properties @@ -0,0 +1,2 @@ +invoker.goals.1=${project.groupId}:${project.artifactId}:${project.version}:property-updates-report +invoker.goals.2=${project.groupId}:${project.artifactId}:${project.version}:dependency-updates-report diff --git a/versions-maven-plugin/src/it/it-823-ranges-update-report-001/pom.xml b/versions-maven-plugin/src/it/it-823-ranges-update-report-001/pom.xml new file mode 100644 index 0000000000..1d60d77461 --- /dev/null +++ b/versions-maven-plugin/src/it/it-823-ranges-update-report-001/pom.xml @@ -0,0 +1,22 @@ + + 4.0.0 + localhost + it-823-ranges-update-report + 1.0 + pom + ranges-update-report + http://localhost/ + + + [1.1.2,3.0] + + + + localhost + dummy-api + ${verion.dummy-api} + + + + diff --git a/versions-maven-plugin/src/it/it-823-ranges-update-report-001/verify.groovy b/versions-maven-plugin/src/it/it-823-ranges-update-report-001/verify.groovy new file mode 100644 index 0000000000..9306dda5df --- /dev/null +++ b/versions-maven-plugin/src/it/it-823-ranges-update-report-001/verify.groovy @@ -0,0 +1,26 @@ + +propertyUpdatesReport = new File( basedir, "target/site/property-updates-report.html" ).text + .replaceAll( '<[^>]+>', ' ' ) + .replaceAll( '&[^;]+;', ' ' ) + .replaceAll( '\\s+', ' ' ) + +assert ! ( propertyUpdatesReport =~ /\b1\.1\.0-2\b/ ) +assert ! ( propertyUpdatesReport =~ /\b1\.1\.1-2\b/ ) +// Summary +assert propertyUpdatesReport =~ / \[1\.1\.2,3\.4\) 1\.1\.3 1\.3 3/ +// Detail +assert propertyUpdatesReport =~ /Newer versions 1\.1\.3 Latest Incremental/ +assert propertyUpdatesReport =~ /\b1\.2\.2 1\.3 Latest Minor 2\.0 2\.1 3\.0\b/ + +dependencyUpdatesReport = new File( basedir, "target/site/dependency-updates-report.html" ).text + .replaceAll( '<[^>]+>', ' ' ) + .replaceAll( '&[^;]+;', ' ' ) + .replaceAll( '\\s+', ' ' ) + +assert ! ( dependencyUpdatesReport =~ /\b1\.1\.0-2\b/ ) +assert ! ( dependencyUpdatesReport =~ /\b1\.1\.1-2\b/ ) +// Summary +assert propertyUpdatesReport =~ / \[1\.1\.2,3\.4\) 1\.1\.3 1\.3 3/ +// Detail +assert propertyUpdatesReport =~ /Newer versions 1\.1\.3 Latest Incremental/ +assert propertyUpdatesReport =~ /\b1\.2\.2 1\.3 Latest Minor 2\.0 2\.1 3\.0\b/ diff --git a/versions-maven-plugin/src/it/it-823-ranges-update-report-002/invoker.properties b/versions-maven-plugin/src/it/it-823-ranges-update-report-002/invoker.properties new file mode 100644 index 0000000000..53663b3aa4 --- /dev/null +++ b/versions-maven-plugin/src/it/it-823-ranges-update-report-002/invoker.properties @@ -0,0 +1,2 @@ +invoker.goals.1=${project.groupId}:${project.artifactId}:${project.version}:property-updates-report +invoker.goals.2=${project.groupId}:${project.artifactId}:${project.version}:dependency-updates-report diff --git a/versions-maven-plugin/src/it/it-823-ranges-update-report-002/pom.xml b/versions-maven-plugin/src/it/it-823-ranges-update-report-002/pom.xml new file mode 100644 index 0000000000..7078a1dbcc --- /dev/null +++ b/versions-maven-plugin/src/it/it-823-ranges-update-report-002/pom.xml @@ -0,0 +1,22 @@ + + 4.0.0 + localhost + it-823-ranges-update-report + 1.0 + pom + ranges-update-report + http://localhost/ + + + [1.1.2,3.0) + + + + localhost + dummy-api + ${verion.dummy-api} + + + + diff --git a/versions-maven-plugin/src/it/it-823-ranges-update-report-002/verify.groovy b/versions-maven-plugin/src/it/it-823-ranges-update-report-002/verify.groovy new file mode 100644 index 0000000000..46425f4fd2 --- /dev/null +++ b/versions-maven-plugin/src/it/it-823-ranges-update-report-002/verify.groovy @@ -0,0 +1,26 @@ + +propertyUpdatesReport = new File( basedir, "target/site/property-updates-report.html" ).text + .replaceAll( '<[^>]+>', ' ' ) + .replaceAll( '&[^;]+;', ' ' ) + .replaceAll( '\\s+', ' ' ) + +assert ! ( propertyUpdatesReport =~ /\b1\.1\.0-2\b/ ) +assert ! ( propertyUpdatesReport =~ /\b1\.1\.1-2\b/ ) +// Summary +assert propertyUpdatesReport =~ / \[1\.1\.2,3\.4\) 1\.1\.3 1\.3 3/ +// Detail +assert propertyUpdatesReport =~ /Newer versions 1\.1\.3 Latest Incremental/ +assert propertyUpdatesReport =~ /\b1\.2\.2 1\.3 Latest Minor 2\.0 2\.1 \* 3\.0\b/ + +dependencyUpdatesReport = new File( basedir, "target/site/dependency-updates-report.html" ).text + .replaceAll( '<[^>]+>', ' ' ) + .replaceAll( '&[^;]+;', ' ' ) + .replaceAll( '\\s+', ' ' ) + +assert ! ( dependencyUpdatesReport =~ /\b1\.1\.0-2\b/ ) +assert ! ( dependencyUpdatesReport =~ /\b1\.1\.1-2\b/ ) +// Summary +assert propertyUpdatesReport =~ / \[1\.1\.2,3\.4\) 1\.1\.3 1\.3 3/ +// Detail +assert propertyUpdatesReport =~ /Newer versions 1\.1\.3 Latest Incremental/ +assert propertyUpdatesReport =~ /\b1\.2\.2 1\.3 Latest Minor 2\.0 2\.1 \* 3\.0\b/ diff --git a/versions-maven-plugin/src/main/java/org/codehaus/mojo/versions/reporting/AbstractVersionsReportRenderer.java b/versions-maven-plugin/src/main/java/org/codehaus/mojo/versions/reporting/AbstractVersionsReportRenderer.java index a43033af52..4c0ed1538b 100644 --- a/versions-maven-plugin/src/main/java/org/codehaus/mojo/versions/reporting/AbstractVersionsReportRenderer.java +++ b/versions-maven-plugin/src/main/java/org/codehaus/mojo/versions/reporting/AbstractVersionsReportRenderer.java @@ -20,10 +20,15 @@ */ import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Locale; import java.util.Map; import org.apache.maven.artifact.versioning.ArtifactVersion; +import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException; +import org.apache.maven.artifact.versioning.Restriction; +import org.apache.maven.artifact.versioning.VersionRange; import org.apache.maven.doxia.sink.Sink; import org.apache.maven.doxia.sink.SinkEventAttributes; import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet; @@ -58,11 +63,11 @@ public abstract class AbstractVersionsReportRenderer extends VersionsReportRe */ protected T model; - protected ArtifactVersionsCache newestUpdateCache - = new ArtifactVersionsCache( AbstractVersionDetails::getNewestUpdate ); + protected final ArtifactVersionsCache newestUpdateCache + = new ArtifactVersionsCache( AbstractVersionDetails::getReportNewestUpdate ); - protected ArtifactVersionsCache allUpdatesCache - = new ArtifactVersionsCache( AbstractVersionDetails::getAllUpdates ); + protected final ArtifactVersionsCache allUpdatesCache + = new ArtifactVersionsCache( AbstractVersionDetails::getReportUpdates ); protected AbstractVersionsReportRenderer( I18N i18n, Sink sink, Locale locale, String bundleName, T model ) { @@ -139,6 +144,12 @@ protected void renderOverviewTableRow( Q stats ) renderStatRow( "report.overview.numNewerMajorAvailable", stats.getMajor(), false ); } + /** + * Renders one table row for the given statistics. + * @param textKey the key of the text to be rendered. + * @param statCount the number of artifacts with the given stat. + * @param forceSuccessIcon if true, the success icon will be rendered regardless. + */ protected void renderStatRow( String textKey, int statCount, boolean forceSuccessIcon ) { sink.tableRow(); @@ -207,9 +218,9 @@ protected void renderSummaryTableHeader( boolean hasScope, boolean hasType ) "report.latestIncremental", "report.latestMinor", "report.latestMajor" ); } - protected void renderSummaryTableRow( Dependency artifact, ArtifactVersions details, - boolean includeScope ) + protected void renderSummaryTableRow( Dependency artifact, ArtifactVersions details, boolean includeScope ) { + details.setCurrentVersion( artifact.getVersion() ); ArtifactVersion[] allUpdates = allUpdatesCache.get( details, empty() ); boolean upToDate = allUpdates == null || allUpdates.length == 0; @@ -392,27 +403,7 @@ else if ( newestUpdateCache.get( details, of( MAJOR ) ) != null ) sink.text( getText( "report.updateVersions" ) ); sink.tableHeaderCell_(); sink.tableCell( cellAttributes ); - for ( int i = 0; i < allUpdates.length; i++ ) - { - if ( i > 0 ) - { - sink.lineBreak(); - } - String label = getLabel( allUpdates[i], details ); - if ( label != null ) - { - safeBold(); - } - sink.text( allUpdates[i].toString() ); - if ( label != null ) - { - safeBold_(); - sink.nonBreakingSpace(); - safeItalic(); - sink.text( label ); - safeItalic_(); - } - } + renderVersions( allUpdates, details ); sink.tableCell_(); sink.tableRow_(); } @@ -437,6 +428,65 @@ protected void renderTableHeaderCells( String... keys ) } ); } + /** + * Renders the list of versions that are available for the given artifact or property. + * @param allUpdates the list of all updates available. + * @param details the versions details for the given artifact or property. + */ + protected void renderVersions( ArtifactVersion[] allUpdates, AbstractVersionDetails details ) + { + List versionRange = getArtifactVersionRange( details ); + boolean someNotAllowed = false; + for ( int i = 0; i < allUpdates.length; i++ ) + { + if ( i > 0 ) + { + sink.lineBreak(); + } + // if candidate version in range, display no star. + ArtifactVersion candidate = allUpdates[i]; + boolean allowed = versionRange.stream().anyMatch( restriction -> + details.isVersionInRestriction( restriction, candidate ) ); + String label = getLabel( allUpdates[i], details ); + if ( !allowed ) + { + sink.text( "* " ); + someNotAllowed = true; + } + if ( allowed && label != null ) + { + safeBold(); + } + sink.text( allUpdates[i].toString() ); + if ( label != null ) + { + if ( allowed ) + { + safeBold_(); + } + sink.nonBreakingSpace(); + safeItalic(); + sink.text( label ); + safeItalic_(); + } + } + if ( someNotAllowed ) + { + sink.lineBreak(); + sink.lineBreak(); + sink.text( "* " ); + safeItalic(); + sink.text( getText( "report.excludedVersion" ) ); + safeItalic_(); + } + } + + /** + * Returns a text label to describe if the given version is a major, minor, incremental or subincremental update. + * @param version the version to describe. + * @param details the artifact for which to render the versions. + * @return a text label to describe if the given version is a major, minor, incremental or subincremental update. + */ protected String getLabel( ArtifactVersion version, AbstractVersionDetails details ) { @@ -462,4 +512,26 @@ protected String getLabel( ArtifactVersion version, AbstractVersionDetails detai return null; } + + /** + * Builds the list of restrictions for the given artifact or property, based on its version range. + * used to determine if a candidate version is outside the range, and if it should be displayed with a star. + * @param details the artifact or property for which to render the versions. + * @return the list of restrictions for the spec versions range. + */ + private List getArtifactVersionRange( AbstractVersionDetails details ) + { + try + { + String spec = details.getCurrentVersion().toString(); + VersionRange range = VersionRange.createFromVersionSpec( spec ); + return range.getRestrictions(); + } + catch ( InvalidVersionSpecificationException ignored ) + { + ignored.printStackTrace( System.err ); + } + return Collections.EMPTY_LIST; + } + } diff --git a/versions-maven-plugin/src/main/java/org/codehaus/mojo/versions/reporting/PropertyUpdatesReportRenderer.java b/versions-maven-plugin/src/main/java/org/codehaus/mojo/versions/reporting/PropertyUpdatesReportRenderer.java index c68656e6c6..a0c8bc17b7 100644 --- a/versions-maven-plugin/src/main/java/org/codehaus/mojo/versions/reporting/PropertyUpdatesReportRenderer.java +++ b/versions-maven-plugin/src/main/java/org/codehaus/mojo/versions/reporting/PropertyUpdatesReportRenderer.java @@ -19,15 +19,11 @@ * under the License. */ -import java.util.HashSet; import java.util.Locale; import java.util.Map; -import java.util.Set; import org.apache.maven.artifact.ArtifactUtils; import org.apache.maven.artifact.versioning.ArtifactVersion; -import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException; -import org.apache.maven.artifact.versioning.VersionRange; import org.apache.maven.doxia.sink.Sink; import org.apache.maven.doxia.sink.SinkEventAttributes; import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet; @@ -261,52 +257,12 @@ else if ( newestUpdateCache.get( details, of( MAJOR ) ) != null ) sink.tableRow_(); if ( !upToDate ) { - Set rangeVersions = getVersionsInRange( property, details, allUpdates ); sink.tableRow(); sink.tableHeaderCell( headerAttributes ); sink.text( getText( "report.updateVersions" ) ); sink.tableHeaderCell_(); sink.tableCell( cellAttributes ); - boolean someNotAllowed = false; - for ( int i = 0; i < allUpdates.length; i++ ) - { - if ( i > 0 ) - { - sink.lineBreak(); - } - boolean allowed = ( rangeVersions.contains( allUpdates[i].toString() ) ); - String label = getLabel( allUpdates[i], details ); - if ( !allowed ) - { - sink.text( "* " ); - someNotAllowed = true; - } - if ( allowed && label != null ) - { - safeBold(); - } - sink.text( allUpdates[i].toString() ); - if ( label != null ) - { - if ( allowed ) - { - safeBold_(); - } - sink.nonBreakingSpace(); - safeItalic(); - sink.text( label ); - safeItalic_(); - } - } - if ( someNotAllowed ) - { - sink.lineBreak(); - sink.lineBreak(); - sink.text( "* " ); - safeItalic(); - sink.text( getText( "report.excludedVersion" ) ); - safeItalic_(); - } + renderVersions( allUpdates, details ); sink.tableCell_(); sink.tableRow_(); } @@ -315,7 +271,8 @@ else if ( newestUpdateCache.get( details, of( MAJOR ) ) != null ) sink.text( getText( "report.versionRange" ) ); sink.tableHeaderCell_(); sink.tableCell( cellAttributes ); - sink.text( StringUtils.isEmpty( property.getVersion() ) ? "[,)" : property.getVersion() ); + sink.text( !StringUtils.isEmpty( details.getCurrentVersion().toString() ) + ? details.getCurrentVersion().toString() : "[,)" ); sink.tableCell_(); sink.tableRow_(); sink.tableRow(); @@ -355,36 +312,6 @@ else if ( newestUpdateCache.get( details, of( MAJOR ) ) != null ) sink.table_(); } - @SuppressWarnings( "checkstyle:MethodLength" ) - protected Set getVersionsInRange( Property property, PropertyVersions versions, - ArtifactVersion[] artifactVersions ) - { - VersionRange range; - Set rangeVersions = new HashSet<>(); - ArtifactVersion[] tmp; - if ( property.getVersion() != null ) - { - try - { - range = VersionRange.createFromVersionSpec( property.getVersion() ); - tmp = versions.getAllUpdates( range ); - } - catch ( InvalidVersionSpecificationException e ) - { - tmp = artifactVersions; - } - } - else - { - tmp = artifactVersions; - } - for ( ArtifactVersion artifactVersion : tmp ) - { - rangeVersions.add( artifactVersion.toString() ); - } - return rangeVersions; - } - @Override protected void renderSummaryTableHeader( boolean hasScope, boolean hasType ) { diff --git a/versions-maven-plugin/src/main/resources/dependency-updates-report.properties b/versions-maven-plugin/src/main/resources/dependency-updates-report.properties index 9b8626d4a1..fb4a6e44b0 100644 --- a/versions-maven-plugin/src/main/resources/dependency-updates-report.properties +++ b/versions-maven-plugin/src/main/resources/dependency-updates-report.properties @@ -53,3 +53,4 @@ report.minorUpdatesAvailable=There is at least one newer minor version available Minor updates are sometimes passive. report.majorUpdatesAvailable=There is at least one newer major version available. \ Major updates are rarely passive. +report.excludedVersion=Outside allowed version range