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   * <b>Terms:</b>
42   * <ul>
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   * Implementation details:<br>
50   * The separators "_" and "-" between components are also optional (though they are usually recommended).<br>
51   * Example:<br>
52   * <code>log4j-1.2.9-beta-9-SNAPSHOT == log4j1.2.9beta9SNAPSHOT == log4j_1.2.9_beta_9_SNAPSHOT</code>
53   * Leading zeros are significant when performing comparisons.
54   * TODO: this parser is better than DefaultArtifactVersion - replace it with this (but align naming) and then remove
55   * this from here.
56   */
57  public class DefaultVersionInfo
58      implements VersionInfo
59  {
60      private final String strVersion;
61  
62      private final List<String> digits;
63  
64      private String annotation;
65  
66      private String annotationRevision;
67  
68      private final String buildSpecifier;
69  
70      private String annotationSeparator;
71  
72      private String annotationRevSeparator;
73  
74      private final String buildSeparator;
75  
76      private static final int DIGITS_INDEX = 1;
77  
78      private static final int ANNOTATION_SEPARATOR_INDEX = 2;
79  
80      private static final int ANNOTATION_INDEX = 3;
81  
82      private static final int ANNOTATION_REV_SEPARATOR_INDEX = 4;
83  
84      private static final int ANNOTATION_REVISION_INDEX = 5;
85  
86      private static final int BUILD_SEPARATOR_INDEX = 6;
87  
88      private static final int BUILD_SPECIFIER_INDEX = 7;
89  
90      private static final String SNAPSHOT_IDENTIFIER = "SNAPSHOT";
91  
92      private static final String DIGIT_SEPARATOR_STRING = ".";
93  
94      /** Constant <code>STANDARD_PATTERN</code> */
95      public static final Pattern STANDARD_PATTERN = Pattern.compile(
96          "^((?:\\d+\\.)*\\d+)"      // digit(s) and '.' repeated - followed by digit (version digits 1.22.0, etc)
97          + "([-_])?"                // optional - or _  (annotation separator)
98          + "([a-zA-Z]*)"            // alpha characters (looking for annotation - alpha, beta, RC, etc.)
99          + "([-_])?"                // optional - or _  (annotation revision separator)
100         + "(\\d*)"                 // digits  (any digits after rc or beta is an annotation revision)
101         + "(?:([-_])?(.*?))?$" );  // - or _ followed everything else (build specifier)
102 
103     /* *
104      * cmaki 02242009
105      * FIX for non-digit release numbers, e.g. trunk-SNAPSHOT or just SNAPSHOT
106      * This alternate pattern supports version numbers like:
107      * trunk-SNAPSHOT
108      * branchName-SNAPSHOT
109      * SNAPSHOT
110      */
111     // for SNAPSHOT releases only (possible versions include: trunk-SNAPSHOT or SNAPSHOT)
112     /** Constant <code>ALTERNATE_PATTERN</code> */
113     public static final Pattern ALTERNATE_PATTERN = Pattern.compile( "^(SNAPSHOT|[a-zA-Z]+[_-]SNAPSHOT)" );
114 
115     /**
116      * Constructs this object and parses the supplied version string.
117      *
118      * @param version the version string
119      * @throws org.apache.maven.shared.release.versions.VersionParseException if an exception during parsing the input
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     /**
177      * <p>Constructor for DefaultVersionInfo.</p>
178      *
179      * @param digits a {@link java.util.List} object
180      * @param annotation a {@link java.lang.String} object
181      * @param annotationRevision a {@link java.lang.String} object
182      * @param buildSpecifier a {@link java.lang.String} object
183      * @param annotationSeparator a {@link java.lang.String} object
184      * @param annotationRevSeparator a {@link java.lang.String} object
185      * @param buildSeparator a {@link java.lang.String} object
186      */
187     public DefaultVersionInfo( List<String> digits, String annotation, String annotationRevision, String buildSpecifier,
188                                String annotationSeparator, String annotationRevSeparator, String buildSeparator )
189     {
190         this.digits = digits;
191         this.annotation = annotation;
192         this.annotationRevision = annotationRevision;
193         this.buildSpecifier = buildSpecifier;
194         this.annotationSeparator = annotationSeparator;
195         this.annotationRevSeparator = annotationRevSeparator;
196         this.buildSeparator = buildSeparator;
197         this.strVersion = getVersionString( this, buildSpecifier, buildSeparator );
198     }
199 
200     @Override
201     public boolean isSnapshot()
202     {
203         return ArtifactUtils.isSnapshot( strVersion );
204     }
205 
206     @Override
207     public VersionInfo getNextVersion()
208     {
209         DefaultVersionInfo version = null;
210         if ( digits != null )
211         {
212             List<String> digits = new ArrayList<>( this.digits );
213             String annotationRevision = this.annotationRevision;
214             if ( StringUtils.isNumeric( annotationRevision ) )
215             {
216                 annotationRevision = incrementVersionString( annotationRevision );
217             }
218             else
219             {
220                 digits.set( digits.size() - 1, incrementVersionString( digits.get( digits.size() - 1 ) ) );
221             }
222 
223             version = new DefaultVersionInfo( digits, annotation, annotationRevision, buildSpecifier,
224                                               annotationSeparator, annotationRevSeparator, buildSeparator );
225         }
226         return version;
227     }
228 
229     /**
230      * {@inheritDoc}
231      *
232      * Compares this {@link DefaultVersionInfo} to the supplied {@link DefaultVersionInfo} to determine which version is
233      * greater.
234      */
235     @Override
236     public int compareTo( VersionInfo obj )
237     {
238         DefaultVersionInfo that = (DefaultVersionInfo) obj;
239 
240         int result;
241         // TODO: this is a workaround for a bug in DefaultArtifactVersion - fix there - 1.01 < 1.01.01
242         if ( strVersion.startsWith( that.strVersion ) && !strVersion.equals( that.strVersion )
243             && strVersion.charAt( that.strVersion.length() ) != '-' )
244         {
245             result = 1;
246         }
247         else if ( that.strVersion.startsWith( strVersion ) && !strVersion.equals( that.strVersion )
248             && that.strVersion.charAt( strVersion.length() ) != '-' )
249         {
250             result = -1;
251         }
252         else
253         {
254             // TODO: this is a workaround for a bug in DefaultArtifactVersion - fix there - it should not consider case in comparing the qualifier
255             // NOTE: The combination of upper-casing and lower-casing is an approximation of String.equalsIgnoreCase()
256             String thisVersion = strVersion.toUpperCase( Locale.ENGLISH ).toLowerCase( Locale.ENGLISH );
257             String thatVersion = that.strVersion.toUpperCase( Locale.ENGLISH ).toLowerCase( Locale.ENGLISH );
258 
259             result = new DefaultArtifactVersion( thisVersion ).compareTo( new DefaultArtifactVersion( thatVersion ) );
260         }
261         return result;
262     }
263 
264     @Override
265     public boolean equals( Object obj )
266     {
267         if ( !( obj instanceof DefaultVersionInfo ) )
268         {
269             return false;
270         }
271 
272         return compareTo( (VersionInfo) obj ) == 0;
273     }
274 
275     @Override
276     public int hashCode()
277     {
278         return strVersion.toLowerCase( Locale.ENGLISH ).hashCode();
279     }
280 
281     /**
282      * Takes a string and increments it as an integer.
283      * Preserves any lpad of "0" zeros.
284      *
285      * @param s the version number
286      * @return {@code String} increments the input {@code String} as an integer
287      * and Preserves any lpad of "0" zeros.
288      */
289     protected String incrementVersionString( String s )
290     {
291         int n = Integer.valueOf( s ).intValue() + 1;
292         String value = String.valueOf( n );
293         if ( value.length() < s.length() )
294         {
295             // String was left-padded with zeros
296             value = StringUtils.leftPad( value, s.length(), "0" );
297         }
298         return value;
299     }
300 
301     @Override
302     public String getSnapshotVersionString()
303     {
304         if ( strVersion.equals( Artifact.SNAPSHOT_VERSION ) )
305         {
306             return strVersion;
307         }
308 
309         String baseVersion = getReleaseVersionString();
310 
311         if ( baseVersion.length() > 0 )
312         {
313             baseVersion += "-";
314         }
315 
316         return baseVersion + Artifact.SNAPSHOT_VERSION;
317     }
318 
319     @Override
320     public String getReleaseVersionString()
321     {
322         String baseVersion = strVersion;
323 
324         Matcher m = Artifact.VERSION_FILE_PATTERN.matcher( baseVersion );
325         if ( m.matches() )
326         {
327             baseVersion = m.group( 1 );
328         }
329         // MRELEASE-623 SNAPSHOT is case-insensitive
330         else if ( StringUtils.right( baseVersion, 9 ).equalsIgnoreCase( "-" + Artifact.SNAPSHOT_VERSION ) )
331         {
332             baseVersion = baseVersion.substring( 0, baseVersion.length() - Artifact.SNAPSHOT_VERSION.length() - 1 );
333         }
334         else if ( baseVersion.equals( Artifact.SNAPSHOT_VERSION ) )
335         {
336             baseVersion = "1.0";
337         }
338         return baseVersion;
339     }
340 
341     @Override
342     public String toString()
343     {
344         return strVersion;
345     }
346 
347     /**
348      * <p>getVersionString.</p>
349      *
350      * @param info a {@link org.apache.maven.shared.release.versions.DefaultVersionInfo} object
351      * @param buildSpecifier a {@link java.lang.String} object
352      * @param buildSeparator a {@link java.lang.String} object
353      * @return a {@link java.lang.String} object
354      */
355     protected static String getVersionString( DefaultVersionInfo info, String buildSpecifier, String buildSeparator )
356     {
357         StringBuilder sb = new StringBuilder();
358 
359         if ( info.digits != null )
360         {
361             sb.append( joinDigitString( info.digits ) );
362         }
363 
364         if ( StringUtils.isNotEmpty( info.annotation ) )
365         {
366             sb.append( StringUtils.defaultString( info.annotationSeparator ) );
367             sb.append( info.annotation );
368         }
369 
370         if ( StringUtils.isNotEmpty( info.annotationRevision ) )
371         {
372             if ( StringUtils.isEmpty( info.annotation ) )
373             {
374                 sb.append( StringUtils.defaultString( info.annotationSeparator ) );
375             }
376             else
377             {
378                 sb.append( StringUtils.defaultString( info.annotationRevSeparator ) );
379             }
380             sb.append( info.annotationRevision );
381         }
382 
383         if ( StringUtils.isNotEmpty( buildSpecifier ) )
384         {
385             sb.append( StringUtils.defaultString( buildSeparator ) );
386             sb.append( buildSpecifier );
387         }
388 
389         return sb.toString();
390     }
391 
392     /**
393      * Simply joins the items in the list with "." period
394      *
395      * @return a single {@code String} of the items in the passed list, joined with a "."
396      * @param digits {@code List<String>} of digits
397      */
398     protected static String joinDigitString( List<String> digits )
399     {
400         return digits != null ? StringUtils.join( digits.iterator(), DIGIT_SEPARATOR_STRING ) : null;
401     }
402 
403     /**
404      * Splits the string on "." and returns a list
405      * containing each digit.
406      *
407      * @param strDigits
408      */
409     private List<String> parseDigits( String strDigits )
410     {
411         return Arrays.asList( StringUtils.split( strDigits, DIGIT_SEPARATOR_STRING ) );
412     }
413 
414     //--------------------------------------------------
415     // Getters & Setters
416     //--------------------------------------------------
417 
418     private static String nullIfEmpty( String s )
419     {
420         return StringUtils.isEmpty( s ) ? null : s;
421     }
422 
423     /**
424      * <p>Getter for the field <code>digits</code>.</p>
425      *
426      * @return a {@link java.util.List} object
427      */
428     public List<String> getDigits()
429     {
430         return digits;
431     }
432 
433     /**
434      * <p>Getter for the field <code>annotation</code>.</p>
435      *
436      * @return a {@link java.lang.String} object
437      */
438     public String getAnnotation()
439     {
440         return annotation;
441     }
442 
443     /**
444      * <p>Getter for the field <code>annotationRevision</code>.</p>
445      *
446      * @return a {@link java.lang.String} object
447      */
448     public String getAnnotationRevision()
449     {
450         return annotationRevision;
451     }
452 
453     /**
454      * <p>Getter for the field <code>buildSpecifier</code>.</p>
455      *
456      * @return a {@link java.lang.String} object
457      */
458     public String getBuildSpecifier()
459     {
460         return buildSpecifier;
461     }
462 
463 }