View Javadoc
1   package org.apache.maven.shared.release.versions;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import org.apache.maven.artifact.Artifact;
23  import org.apache.maven.artifact.ArtifactUtils;
24  import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
25  import org.codehaus.plexus.util.StringUtils;
26  
27  import java.util.ArrayList;
28  import java.util.Arrays;
29  import java.util.List;
30  import java.util.Locale;
31  import java.util.regex.Matcher;
32  import java.util.regex.Pattern;
33  
34  /**
35   * This compares and increments versions for a common java versioning scheme.
36   * <p/>
37   * The supported version scheme has the following parts.<br>
38   * <code><i>component-digits-annotation-annotationRevision-buildSpecifier</i></code><br>
39   * Example:<br>
40   * <code>my-component-1.0.1-alpha-2-SNAPSHOT</code>
41   * <p/>
42   * <ul>Terms:
43   * <li><i>component</i> - name of the versioned component (log4j, commons-lang, etc)
44   * <li><i>digits</i> - Numeric digits with at least one "." period. (1.0, 1.1, 1.01, 1.2.3, etc)
45   * <li><i>annotationRevision</i> - Integer qualifier for the annotation. (4 as in RC-4)
46   * <li><i>buildSpecifier</i> - Additional specifier for build. (SNAPSHOT, or build number like "20041114.081234-2")
47   * </ul>
48   * <b>Digits is the only required piece of the version string, and must contain at lease one "." period.</b>
49   * <p/>
50   * Implementation details:<br>
51   * The separators "_" and "-" between components are also optional (though they are usually recommended).<br>
52   * Example:<br>
53   * <code>log4j-1.2.9-beta-9-SNAPSHOT == log4j1.2.9beta9SNAPSHOT == log4j_1.2.9_beta_9_SNAPSHOT</code>
54   * <p/>
55   * Leading zeros are significant when performing comparisons.
56   * <p/>
57   * TODO: this parser is better than DefaultArtifactVersion - replace it with this (but align naming) and then remove
58   * this from here.
59   */
60  public class DefaultVersionInfo
61      implements VersionInfo
62  {
63      private final String strVersion;
64  
65      private final List<String> digits;
66  
67      private String annotation;
68  
69      private String annotationRevision;
70  
71      private final String buildSpecifier;
72  
73      private String annotationSeparator;
74  
75      private String annotationRevSeparator;
76  
77      private final String buildSeparator;
78  
79      private static final int DIGITS_INDEX = 1;
80  
81      private static final int ANNOTATION_SEPARATOR_INDEX = 2;
82  
83      private static final int ANNOTATION_INDEX = 3;
84  
85      private static final int ANNOTATION_REV_SEPARATOR_INDEX = 4;
86  
87      private static final int ANNOTATION_REVISION_INDEX = 5;
88  
89      private static final int BUILD_SEPARATOR_INDEX = 6;
90  
91      private static final int BUILD_SPECIFIER_INDEX = 7;
92  
93      private static final String SNAPSHOT_IDENTIFIER = "SNAPSHOT";
94  
95      private static final String DIGIT_SEPARATOR_STRING = ".";
96  
97      public static final Pattern STANDARD_PATTERN = Pattern.compile(
98          "^((?:\\d+\\.)*\\d+)"      // digit(s) and '.' repeated - followed by digit (version digits 1.22.0, etc)
99          + "([-_])?"                // optional - or _  (annotation separator)
100         + "([a-zA-Z]*)"            // alpha characters (looking for annotation - alpha, beta, RC, etc.)
101         + "([-_])?"                // optional - or _  (annotation revision separator)
102         + "(\\d*)"                 // digits  (any digits after rc or beta is an annotation revision)
103         + "(?:([-_])?(.*?))?$" );  // - or _ followed everything else (build specifier)
104 
105     /* *
106      * cmaki 02242009
107      * FIX for non-digit release numbers, e.g. trunk-SNAPSHOT or just SNAPSHOT
108      * This alternate pattern supports version numbers like:
109      * trunk-SNAPSHOT
110      * branchName-SNAPSHOT
111      * SNAPSHOT
112      */
113     // for SNAPSHOT releases only (possible versions include: trunk-SNAPSHOT or SNAPSHOT)
114     public static final Pattern ALTERNATE_PATTERN = Pattern.compile( "^(SNAPSHOT|[a-zA-Z]+[_-]SNAPSHOT)" );
115 
116     /**
117      * Constructs this object and parses the supplied version string.
118      *
119      * @param version
120      */
121     public DefaultVersionInfo( String version )
122         throws VersionParseException
123     {
124         strVersion = version;
125 
126         // FIX for non-digit release numbers, e.g. trunk-SNAPSHOT or just SNAPSHOT
127         Matcher matcher = ALTERNATE_PATTERN.matcher( strVersion );
128         // TODO: hack because it didn't support "SNAPSHOT"
129         if ( matcher.matches() )
130         {
131             annotation = null;
132             digits = null;
133             buildSpecifier = version;
134             buildSeparator = null;
135             return;
136         }
137 
138         Matcher m = STANDARD_PATTERN.matcher( strVersion );
139         if ( m.matches() )
140         {
141             digits = parseDigits( m.group( DIGITS_INDEX ) );
142             if ( !SNAPSHOT_IDENTIFIER.equals( m.group( ANNOTATION_INDEX ) ) )
143             {
144                 annotationSeparator = m.group( ANNOTATION_SEPARATOR_INDEX );
145                 annotation = nullIfEmpty( m.group( ANNOTATION_INDEX ) );
146 
147                 if ( StringUtils.isNotEmpty( m.group( ANNOTATION_REV_SEPARATOR_INDEX ) )
148                     && StringUtils.isEmpty( m.group( ANNOTATION_REVISION_INDEX ) ) )
149                 {
150                     // The build separator was picked up as the annotation revision separator
151                     buildSeparator = m.group( ANNOTATION_REV_SEPARATOR_INDEX );
152                     buildSpecifier = nullIfEmpty( m.group( BUILD_SPECIFIER_INDEX ) );
153                 }
154                 else
155                 {
156                     annotationRevSeparator = m.group( ANNOTATION_REV_SEPARATOR_INDEX );
157                     annotationRevision = nullIfEmpty( m.group( ANNOTATION_REVISION_INDEX ) );
158 
159                     buildSeparator = m.group( BUILD_SEPARATOR_INDEX );
160                     buildSpecifier = nullIfEmpty( m.group( BUILD_SPECIFIER_INDEX ) );
161                 }
162             }
163             else
164             {
165                 // Annotation was "SNAPSHOT" so populate the build specifier with that data
166                 buildSeparator = m.group( ANNOTATION_SEPARATOR_INDEX );
167                 buildSpecifier = nullIfEmpty( m.group( ANNOTATION_INDEX ) );
168             }
169         }
170         else
171         {
172             throw new VersionParseException( "Unable to parse the version string: \"" + version + "\"" );
173         }
174     }
175 
176     public DefaultVersionInfo( List<String> digits, String annotation, String annotationRevision, String buildSpecifier,
177                                String annotationSeparator, String annotationRevSeparator, String buildSeparator )
178     {
179         this.digits = digits;
180         this.annotation = annotation;
181         this.annotationRevision = annotationRevision;
182         this.buildSpecifier = buildSpecifier;
183         this.annotationSeparator = annotationSeparator;
184         this.annotationRevSeparator = annotationRevSeparator;
185         this.buildSeparator = buildSeparator;
186         this.strVersion = getVersionString( this, buildSpecifier, buildSeparator );
187     }
188 
189     @Override
190     public boolean isSnapshot()
191     {
192         return ArtifactUtils.isSnapshot( strVersion );
193     }
194 
195     @Override
196     public VersionInfo getNextVersion()
197     {
198         DefaultVersionInfo version = null;
199         if ( digits != null )
200         {
201             List<String> digits = new ArrayList<>( this.digits );
202             String annotationRevision = this.annotationRevision;
203             if ( StringUtils.isNumeric( annotationRevision ) )
204             {
205                 annotationRevision = incrementVersionString( annotationRevision );
206             }
207             else
208             {
209                 digits.set( digits.size() - 1, incrementVersionString( digits.get( digits.size() - 1 ) ) );
210             }
211 
212             version = new DefaultVersionInfo( digits, annotation, annotationRevision, buildSpecifier,
213                                               annotationSeparator, annotationRevSeparator, buildSeparator );
214         }
215         return version;
216     }
217 
218     /**
219      * Compares this {@link DefaultVersionInfo} to the supplied {@link DefaultVersionInfo} to determine which version is
220      * greater.
221      *
222      * @param obj the comparison version
223      * @return the comparison value
224      * @throws IllegalArgumentException if the components differ between the objects or if either of the annotations can
225      *             not be determined.
226      */
227     @Override
228     public int compareTo( VersionInfo obj )
229     {
230         DefaultVersionInfo that = (DefaultVersionInfo) obj;
231 
232         int result;
233         // TODO: this is a workaround for a bug in DefaultArtifactVersion - fix there - 1.01 < 1.01.01
234         if ( strVersion.startsWith( that.strVersion ) && !strVersion.equals( that.strVersion )
235             && strVersion.charAt( that.strVersion.length() ) != '-' )
236         {
237             result = 1;
238         }
239         else if ( that.strVersion.startsWith( strVersion ) && !strVersion.equals( that.strVersion )
240             && that.strVersion.charAt( strVersion.length() ) != '-' )
241         {
242             result = -1;
243         }
244         else
245         {
246             // TODO: this is a workaround for a bug in DefaultArtifactVersion - fix there - it should not consider case in comparing the qualifier
247             // NOTE: The combination of upper-casing and lower-casing is an approximation of String.equalsIgnoreCase()
248             String thisVersion = strVersion.toUpperCase( Locale.ENGLISH ).toLowerCase( Locale.ENGLISH );
249             String thatVersion = that.strVersion.toUpperCase( Locale.ENGLISH ).toLowerCase( Locale.ENGLISH );
250 
251             result = new DefaultArtifactVersion( thisVersion ).compareTo( new DefaultArtifactVersion( thatVersion ) );
252         }
253         return result;
254     }
255 
256     @Override
257     public boolean equals( Object obj )
258     {
259         if ( !( obj instanceof DefaultVersionInfo ) )
260         {
261             return false;
262         }
263 
264         return compareTo( (VersionInfo) obj ) == 0;
265     }
266 
267     @Override
268     public int hashCode()
269     {
270         return strVersion.toLowerCase( Locale.ENGLISH ).hashCode();
271     }
272 
273     /**
274      * Takes a string and increments it as an integer.
275      * Preserves any lpad of "0" zeros.
276      *
277      * @param s
278      */
279     protected String incrementVersionString( String s )
280     {
281         int n = Integer.valueOf( s ).intValue() + 1;
282         String value = String.valueOf( n );
283         if ( value.length() < s.length() )
284         {
285             // String was left-padded with zeros
286             value = StringUtils.leftPad( value, s.length(), "0" );
287         }
288         return value;
289     }
290 
291     @Override
292     public String getSnapshotVersionString()
293     {
294         if ( strVersion.equals( Artifact.SNAPSHOT_VERSION ) )
295         {
296             return strVersion;
297         }
298 
299         String baseVersion = getReleaseVersionString();
300 
301         if ( baseVersion.length() > 0 )
302         {
303             baseVersion += "-";
304         }
305 
306         return baseVersion + Artifact.SNAPSHOT_VERSION;
307     }
308 
309     @Override
310     public String getReleaseVersionString()
311     {
312         String baseVersion = strVersion;
313 
314         Matcher m = Artifact.VERSION_FILE_PATTERN.matcher( baseVersion );
315         if ( m.matches() )
316         {
317             baseVersion = m.group( 1 );
318         }
319         // MRELEASE-623 SNAPSHOT is case-insensitive
320         else if ( StringUtils.right( baseVersion, 9 ).equalsIgnoreCase( "-" + Artifact.SNAPSHOT_VERSION ) )
321         {
322             baseVersion = baseVersion.substring( 0, baseVersion.length() - Artifact.SNAPSHOT_VERSION.length() - 1 );
323         }
324         else if ( baseVersion.equals( Artifact.SNAPSHOT_VERSION ) )
325         {
326             baseVersion = "1.0";
327         }
328         return baseVersion;
329     }
330 
331     @Override
332     public String toString()
333     {
334         return strVersion;
335     }
336 
337     protected static String getVersionString( DefaultVersionInfo info, String buildSpecifier, String buildSeparator )
338     {
339         StringBuilder sb = new StringBuilder();
340 
341         if ( info.digits != null )
342         {
343             sb.append( joinDigitString( info.digits ) );
344         }
345 
346         if ( StringUtils.isNotEmpty( info.annotation ) )
347         {
348             sb.append( StringUtils.defaultString( info.annotationSeparator ) );
349             sb.append( info.annotation );
350         }
351 
352         if ( StringUtils.isNotEmpty( info.annotationRevision ) )
353         {
354             if ( StringUtils.isEmpty( info.annotation ) )
355             {
356                 sb.append( StringUtils.defaultString( info.annotationSeparator ) );
357             }
358             else
359             {
360                 sb.append( StringUtils.defaultString( info.annotationRevSeparator ) );
361             }
362             sb.append( info.annotationRevision );
363         }
364 
365         if ( StringUtils.isNotEmpty( buildSpecifier ) )
366         {
367             sb.append( StringUtils.defaultString( buildSeparator ) );
368             sb.append( buildSpecifier );
369         }
370 
371         return sb.toString();
372     }
373 
374     /**
375      * Simply joins the items in the list with "." period
376      *
377      * @param digits
378      */
379     protected static String joinDigitString( List<String> digits )
380     {
381         return digits != null ? StringUtils.join( digits.iterator(), DIGIT_SEPARATOR_STRING ) : null;
382     }
383 
384     /**
385      * Splits the string on "." and returns a list
386      * containing each digit.
387      *
388      * @param strDigits
389      */
390     private List<String> parseDigits( String strDigits )
391     {
392         return Arrays.asList( StringUtils.split( strDigits, DIGIT_SEPARATOR_STRING ) );
393     }
394 
395     //--------------------------------------------------
396     // Getters & Setters
397     //--------------------------------------------------
398 
399     private static String nullIfEmpty( String s )
400     {
401         return StringUtils.isEmpty( s ) ? null : s;
402     }
403 
404     public List<String> getDigits()
405     {
406         return digits;
407     }
408 
409     public String getAnnotation()
410     {
411         return annotation;
412     }
413 
414     public String getAnnotationRevision()
415     {
416         return annotationRevision;
417     }
418 
419     public String getBuildSpecifier()
420     {
421         return buildSpecifier;
422     }
423 
424 }