package org.codehaus.mojo.pomtools.versioning; /* * Copyright 2005-2006 The Apache Software Foundation. * * Licensed 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 <a href="mailto:dhawkins@codehaus.org">David Hawkins</a> * @version $Id$ */ import java.util.Arrays; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.maven.artifact.versioning.ArtifactVersion; import org.apache.maven.artifact.versioning.DefaultArtifactVersion; import org.codehaus.plexus.util.StringUtils; /** This compares and increments versions for a common java versioning scheme. * <p> * The supported version scheme has the following parts.<br> * <code><i>component-digits-annotation-annotationRevision-buildSpecifier</i></code><br> * Example:<br> * <code>my-component-1.0.1-alpha-2-SNAPSHOT</code> * * <ul>Terms: * <li><i>component</i> - name of the versioned component (log4j, commons-lang, etc) * <li><i>digits</i> - Numeric digits with at least one "." period. (1.0, 1.1, 1.01, 1.2.3, etc) * <li><i>annotation</i> - Version annotation - Valid Values are (alpha, beta, RC). * Use {@link DefaultVersionInfo#setAnnotationOrder(List)} to change the valid values. * <li><i>annotationRevision</i> - Integer qualifier for the annotation. (4 as in RC-4) * <li><i>buildSpecifier</i> - Additional specifier for build. (SNAPSHOT, or build number like "20041114.081234-2") * </ul> * <b>Digits is the only required piece of the version string, and must contain at lease one "." period.</b> * <p> * Implementation details:<br> * The separators "_" and "-" between components are also optional (though they are usually reccommended).<br> * Example:<br> * <code>log4j-1.2.9-beta-9-SNAPSHOT == log4j1.2.9beta9SNAPSHOT == log4j_1.2.9_beta_9_SNAPSHOT</code> * <p> * All numbers in the "digits" part of the version are considered Integers. Therefore 1.01.01 is the same as 1.1.1 * Leading zeros are ignored when performing comparisons. * * @author <a href="mailto:dhawkins@codehaus.org">David Hawkins</a> * @version $Id$ */ public class DefaultVersionInfo implements VersionInfo, Cloneable { private String strVersion; private String component; private List digits; private String annotation; private String annotationRevision; private String buildSpecifier; private String digitSeparator; private String annotationSeparator; private String annotationRevSeparator; private String buildSeparator; private List annotationOrder; private boolean parsed = false; private static final int COMPONENT_INDEX = 1; private static final int DIGIT_SEPARATOR_INDEX = 2; private static final int DIGITS_INDEX = 3; private static final int ANNOTATION_SEPARATOR_INDEX = 4; private static final int ANNOTATION_INDEX = 5; private static final int ANNOTATION_REV_SEPARATOR_INDEX = 6; private static final int ANNOTATION_REVISION_INDEX = 7; private static final int BUILD_SEPARATOR_INDEX = 8; private static final int BUILD_SPECIFIER_INDEX = 9; public static final String SNAPSHOT_IDENTIFIER = "SNAPSHOT"; protected static final String DIGIT_SEPARATOR_STRING = "."; /** Default order of annotations to consider in {@link #compareTo(Object)}. * <code>null</code> denotes a version without an annotation. Therefore, a "SP" * or Service Pack build is considered to be greater than a version without * an annotation. */ public static final String[] DEFAULT_ANNOTATION_ORDER = new String[] { "DEV", "ALPHA", "B", "BETA", "RC", null, "SP" }; protected static final Pattern DATESTAMP_PATTERN = Pattern.compile( "^(SNAPSHOT.)?((\\d{8})(?:\\.(\\d+))?)$" ); protected static final Pattern STANDARD_PATTERN = Pattern.compile( "^(?:([a-zA-Z].*?)([-_])(?=\\d))?" + // non greedy .* to grab the component. dash must precede a number "((?:\\d+[.])*\\d+)" + // digit(s) and '.' repeated - followed by digits "([-_])?" + // optional - or _ (annotation separator) "([a-zA-Z]*)" + // alpha characters (looking for annotation - alpha, beta, RC, etc.) "([-_])?" + // optional - or _ (annotation revision separator) "(\\d*)" + // digits (any digits after rc or beta is an annotation revision) "(?:([-_])?(.*?))?$" ); // - or _ followed everything else (build specifier) protected static final Pattern OPTIONAL_DIGIT_SEPARATOR_PATTERN = Pattern.compile( "^(.*?)" + // non greedy .* to grab the component. "([-_])?" + // optional - or _ (digits separator) "((?:\\d+[.])+\\d+)" + // digit(s) and '.' repeated - followed by digit (version digits 1.22.0, etc) "([-_])?" + // optional - or _ (annotation separator) "([a-zA-Z]*)" + // alpha characters (looking for annotation - alpha, beta, RC, etc.) "([-_])?" + // optional - or _ (annotation revision separator) "(\\d*)" + // digits (any digits after rc or beta is an annotation revision) "(?:([-_])?(.*?))?$" ); // - or _ followed everything else (build specifier) protected static final Pattern DIGIT_SEPARATOR_PATTERN = Pattern.compile( "(\\d+)\\.?" ); /** Constructs this object and parses the supplied version string. * * @param version */ public DefaultVersionInfo( String version ) { annotationOrder = Arrays.asList( DEFAULT_ANNOTATION_ORDER ); if ( version == null ) { throw new VersionParseRTException( "Version cannot be null" ); } parseVersion( version ); } /** Internal routine for parsing the supplied version string into its parts. * * @param version */ protected void parseVersion( String version ) { this.strVersion = version; if ( StringUtils.isEmpty( strVersion ) ) { // Don't try to parse null strings return; } Matcher m = DATESTAMP_PATTERN.matcher( strVersion ); if ( m.matches() ) { // Just grab the digits part of the string. this.buildSpecifier = m.group( 2 ); this.parsed = true; } else { m = OPTIONAL_DIGIT_SEPARATOR_PATTERN.matcher( strVersion ); if ( !m.matches() ) { m = STANDARD_PATTERN.matcher( strVersion ); } if ( m.matches() ) { setComponent( m.group( COMPONENT_INDEX ) ); this.digitSeparator = m.group( DIGIT_SEPARATOR_INDEX ); setDigits( parseDigits( m.group( DIGITS_INDEX ) ) ); if ( !SNAPSHOT_IDENTIFIER.equals( m.group( ANNOTATION_INDEX ) ) ) { this.annotationSeparator = m.group( ANNOTATION_SEPARATOR_INDEX ); setAnnotation( m.group( ANNOTATION_INDEX ) ); if ( StringUtils.isNotEmpty( m.group( ANNOTATION_REV_SEPARATOR_INDEX ) ) && StringUtils.isEmpty( m.group( ANNOTATION_REVISION_INDEX ) ) ) { // The build separator was picked up as the annotation revision separator this.buildSeparator = m.group( ANNOTATION_REV_SEPARATOR_INDEX ); setBuildSpecifier( m.group( BUILD_SPECIFIER_INDEX ) ); } else { this.annotationRevSeparator = m.group( ANNOTATION_REV_SEPARATOR_INDEX ); setAnnotationRevision( m.group( ANNOTATION_REVISION_INDEX ) ); this.buildSeparator = m.group( BUILD_SEPARATOR_INDEX ); setBuildSpecifier( m.group( BUILD_SPECIFIER_INDEX ) ); } } else { // Annotation was "SNAPSHOT" so populate the build specifier with that data this.buildSeparator = m.group( ANNOTATION_SEPARATOR_INDEX ); setBuildSpecifier( m.group( ANNOTATION_INDEX ) ); } this.parsed = true; } } } public boolean equals( Object obj ) { if ( !( obj instanceof DefaultVersionInfo ) ) { throw new ClassCastException( "DefaultVersionInfo object expected" ); } DefaultVersionInfo that = (DefaultVersionInfo) obj; return StringUtils.equals( this.strVersion, that.strVersion ); } public int hashCode() { return strVersion.hashCode(); } public boolean isParsed() { return this.parsed; } public boolean isSnapshot() { return SNAPSHOT_IDENTIFIER.equalsIgnoreCase( this.buildSpecifier ); } public VersionInfo getNextVersion() { if ( !isParsed() ) { return null; } DefaultVersionInfo result; try { result = (DefaultVersionInfo) this.clone(); } catch ( CloneNotSupportedException e ) { return null; } if ( StringUtils.isNumeric( result.annotationRevision ) ) { result.annotationRevision = incrementVersionString( result.annotationRevision ); } else if ( result.digits != null && !result.digits.isEmpty() ) { try { List tmpDigits = result.digits; tmpDigits.set( tmpDigits.size() - 1, incrementVersionString( (String) tmpDigits.get( tmpDigits.size() - 1 ) ) ); } catch ( NumberFormatException e ) { return null; } } else { return null; } return result; } /** Compares this {@link DefaultVersionInfo} to the supplied {@link DefaultVersionInfo} * to determine which version is greater. * <p> * Decision order is: digits, annotation, annotationRev, buildSpecifier. * <p> * Presence of an annotation is considered to be less than an equivalent version without an annotation.<br> * Example: 1.0 is greater than 1.0-alpha.<br> * <p> * The {@link DefaultVersionInfo#getAnnotationOrder()} is used in determining the rank order of annotations.<br> * For example: alpha < beta < RC < release * * @param that * @return * @throws IllegalArgumentException if the components differ between the objects or if * either of the annotations can not be determined. */ public int compareTo( Object obj ) { if ( !( obj instanceof DefaultVersionInfo ) ) { throw new ClassCastException( "DefaultVersionInfo object expected" ); } DefaultVersionInfo that = (DefaultVersionInfo) obj; if ( !isParsed() ) { throw new IllegalArgumentException( "Cannot perform comparison on a component that wasn't " + "able to be parsed" ); } else if ( !StringUtils.equals( this.component, that.component ) ) { throw new IllegalArgumentException( "Cannot perform comparison on different components: \"" + this.component + "\" compared to \"" + that.component + "\"" ); } if ( this.digits == null && that.digits != null ) { return -1; } else if ( this.digits != null && that.digits == null ) { return 1; } else if ( this.digits == null && that.digits == null ) { // nothing, keep looking at rest of verison this.digits = null; //dummy statment to silence checkstyle } else if ( !this.digits.equals( that.digits ) ) { for ( int i = 0; i < this.digits.size(); i++ ) { if ( i >= that.digits.size() ) { // We've gone past the end of the digit list of that. We are greater return 1; } if ( !StringUtils.equals( (String) this.digits.get( i ), (String) that.digits.get( i ) ) ) { return compareToAsIntegers( (String) this.digits.get( i ), (String) that.digits.get( i ) ); } } if ( this.digits.size() < that.digits.size() ) { // The lists were equal up to the end of this list. The other has more digits so it is greater. return -1; } } if ( !StringUtils.equalsIgnoreCase( this.annotation, that.annotation ) ) { int nThis = annotationOrder.indexOf( StringUtils.lowerCase( this.annotation ) ); int nThat = annotationOrder.indexOf( StringUtils.upperCase( that.annotation ) ); if ( nThis == -1 || nThat == -1 ) { // Here we have a situation where one of the annotations is unknown // If both are non-null, just compare them lexically. // else consider the version with the null annotation as being greater // a 1.0-unknown is less than 1.0 if ( this.annotation != null && that.annotation == null ) { return -1; } else if ( this.annotation == null && that.annotation != null ) { return 1; } else { return this.annotation.toUpperCase().compareTo( that.annotation.toUpperCase() ); } } return nThis - nThat; } if ( !StringUtils.equals( this.annotationRevision, that.annotationRevision ) ) { return compareToAsIntegers( this.annotationRevision, that.annotationRevision ); } if ( !StringUtils.equals( this.buildSpecifier, that.buildSpecifier ) ) { if ( this.buildSpecifier == null && that.buildSpecifier != null ) { return 1; } else if ( this.buildSpecifier != null && that.buildSpecifier == null ) { return -1; } else { // Just do a simple string comparison? return this.buildSpecifier.compareTo( that.buildSpecifier ); } } return 0; } private int compareToAsIntegers( String s1, String s2 ) { int n1 = StringUtils.isEmpty( s1 ) ? -1 : Integer.parseInt( s1 ); int n2 = StringUtils.isEmpty( s2 ) ? -1 : Integer.parseInt( s2 ); return n1 - n2; } /** Takes a string and increments it as an integer. * Preserves any lpad of "0" zeros. * * @param s * @return */ protected String incrementVersionString( String s ) { if ( StringUtils.isEmpty( s ) ) { return null; } try { Integer n = new Integer( Integer.parseInt( s ) + 1 ); if ( n.toString().length() < s.length() ) { // String was left-padded with zeros return StringUtils.leftPad( n.toString(), s.length(), "0" ); } return n.toString(); } catch ( NumberFormatException e ) { return null; } } public String getSnapshotVersionString() { return getVersionString( this, SNAPSHOT_IDENTIFIER, StringUtils.defaultString( this.buildSeparator, "-" ) ); } public String getReleaseVersionString() { return getVersionString( this, null, null ); } public ArtifactVersion getArtifactVersion() { return new DefaultArtifactVersion( getVersionString() ); } public String toString() { return getVersionString(); } public String getVersionString() { return getVersionString( this, this.buildSpecifier, this.buildSeparator ); } protected static String getVersionString( DefaultVersionInfo info, String buildSpecifier, String buildSeparator ) { if ( !info.isParsed() ) { return info.strVersion; } StringBuffer sb = new StringBuffer(); if ( StringUtils.isNotEmpty( info.component ) ) { sb.append( info.component ); } if ( info.digits != null ) { sb.append( StringUtils.defaultString( info.digitSeparator ) ); sb.append( joinDigitString( info.digits ) ); } if ( StringUtils.isNotEmpty( info.annotation ) ) { sb.append( StringUtils.defaultString( info.annotationSeparator ) ); sb.append( info.annotation ); } if ( StringUtils.isNotEmpty( info.annotationRevision ) ) { if ( StringUtils.isEmpty( info.annotation ) ) { sb.append( StringUtils.defaultString( info.annotationSeparator ) ); } else { sb.append( StringUtils.defaultString( info.annotationRevSeparator ) ); } sb.append( info.annotationRevision ); } if ( StringUtils.isNotEmpty( buildSpecifier ) ) { sb.append( StringUtils.defaultString( buildSeparator ) ); sb.append( buildSpecifier ); } return sb.toString(); } /** Simply joins the items in the list with "." period * * @param digits * @return */ protected static String joinDigitString( List digits ) { if ( digits == null ) { return null; } return StringUtils.join( digits.iterator(), DIGIT_SEPARATOR_STRING ); } /** Splits the string on "." and returns a list * containing each digit. * * @param strDigits * @return */ protected List parseDigits( String strDigits ) { if ( StringUtils.isEmpty( strDigits ) ) { return null; } String[] strings = StringUtils.split( strDigits, DIGIT_SEPARATOR_STRING ); return Arrays.asList( strings ); } //-------------------------------------------------- // Getters & Setters //-------------------------------------------------- private String nullIfEmpty( String s ) { return ( StringUtils.isEmpty( s ) ) ? null : s; } public String getAnnotation() { return annotation; } protected void setAnnotation( String annotation ) { this.annotation = nullIfEmpty( annotation ); } public String getAnnotationRevision() { return annotationRevision; } protected void setAnnotationRevision( String annotationRevision ) { this.annotationRevision = nullIfEmpty( annotationRevision ); } public String getComponent() { return component; } protected void setComponent( String component ) { this.component = nullIfEmpty( component ); } public List getDigits() { return digits; } protected void setDigits( List digits ) { this.digits = digits; } public String getBuildSpecifier() { return buildSpecifier; } protected void setBuildSpecifier( String buildSpecifier ) { this.buildSpecifier = nullIfEmpty( buildSpecifier ); } public List getAnnotationOrder() { return annotationOrder; } protected void setAnnotationOrder( List annotationOrder ) { this.annotationOrder = annotationOrder; } }