View Javadoc
1   package org.apache.maven.artifact.versioning;
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 java.math.BigInteger;
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.Iterator;
26  import java.util.List;
27  import java.util.Locale;
28  import java.util.Properties;
29  import java.util.Stack;
30  
31  /**
32   * Generic implementation of version comparison.
33   *
34   * <p>Features:
35   * <ul>
36   * <li>mixing of '<code>-</code>' (dash) and '<code>.</code>' (dot) separators,</li>
37   * <li>transition between characters and digits also constitutes a separator:
38   *     <code>1.0alpha1 =&gt; [1, 0, alpha, 1]</code></li>
39   * <li>unlimited number of version components,</li>
40   * <li>version components in the text can be digits or strings,</li>
41   * <li>strings are checked for well-known qualifiers and the qualifier ordering is used for version ordering.
42   *     Well-known qualifiers (case insensitive) are:<ul>
43   *     <li><code>alpha</code> or <code>a</code></li>
44   *     <li><code>beta</code> or <code>b</code></li>
45   *     <li><code>milestone</code> or <code>m</code></li>
46   *     <li><code>rc</code> or <code>cr</code></li>
47   *     <li><code>snapshot</code></li>
48   *     <li><code>(the empty string)</code> or <code>ga</code> or <code>final</code></li>
49   *     <li><code>sp</code></li>
50   *     </ul>
51   *     Unknown qualifiers are considered after known qualifiers, with lexical order (always case insensitive),
52   *   </li>
53   * <li>a dash usually precedes a qualifier, and is always less important than something preceded with a dot.</li>
54   * </ul></p>
55   *
56   * @see <a href="https://cwiki.apache.org/confluence/display/MAVENOLD/Versioning">"Versioning" on Maven Wiki</a>
57   * @author <a href="mailto:kenney@apache.org">Kenney Westerhof</a>
58   * @author <a href="mailto:hboutemy@apache.org">Hervé Boutemy</a>
59   */
60  public class ComparableVersion
61      implements Comparable<ComparableVersion>
62  {
63      private String value;
64  
65      private String canonical;
66  
67      private ListItem items;
68  
69      private interface Item
70      {
71          int INTEGER_ITEM = 0;
72          int STRING_ITEM = 1;
73          int LIST_ITEM = 2;
74  
75          int compareTo( Item item );
76  
77          int getType();
78  
79          boolean isNull();
80      }
81  
82      /**
83       * Represents a numeric item in the version item list.
84       */
85      private static class IntegerItem
86          implements Item
87      {
88          private static final BigInteger BIG_INTEGER_ZERO = new BigInteger( "0" );
89  
90          private final BigInteger value;
91  
92          public static final IntegerItem ZERO = new IntegerItem();
93  
94          private IntegerItem()
95          {
96              this.value = BIG_INTEGER_ZERO;
97          }
98  
99          public IntegerItem( String str )
100         {
101             this.value = new BigInteger( str );
102         }
103 
104         public int getType()
105         {
106             return INTEGER_ITEM;
107         }
108 
109         public boolean isNull()
110         {
111             return BIG_INTEGER_ZERO.equals( value );
112         }
113 
114         public int compareTo( Item item )
115         {
116             if ( item == null )
117             {
118                 return BIG_INTEGER_ZERO.equals( value ) ? 0 : 1; // 1.0 == 1, 1.1 > 1
119             }
120 
121             switch ( item.getType() )
122             {
123                 case INTEGER_ITEM:
124                     return value.compareTo( ( (IntegerItem) item ).value );
125 
126                 case STRING_ITEM:
127                     return 1; // 1.1 > 1-sp
128 
129                 case LIST_ITEM:
130                     return 1; // 1.1 > 1-1
131 
132                 default:
133                     throw new RuntimeException( "invalid item: " + item.getClass() );
134             }
135         }
136 
137         public String toString()
138         {
139             return value.toString();
140         }
141     }
142 
143     /**
144      * Represents a string in the version item list, usually a qualifier.
145      */
146     private static class StringItem
147         implements Item
148     {
149         private static final String[] QUALIFIERS = { "alpha", "beta", "milestone", "rc", "snapshot", "", "sp" };
150 
151         @SuppressWarnings( "checkstyle:constantname" )
152         private static final List<String> _QUALIFIERS = Arrays.asList( QUALIFIERS );
153 
154         private static final Properties ALIASES = new Properties();
155         static
156         {
157             ALIASES.put( "ga", "" );
158             ALIASES.put( "final", "" );
159             ALIASES.put( "cr", "rc" );
160         }
161 
162         /**
163          * A comparable value for the empty-string qualifier. This one is used to determine if a given qualifier makes
164          * the version older than one without a qualifier, or more recent.
165          */
166         private static final String RELEASE_VERSION_INDEX = String.valueOf( _QUALIFIERS.indexOf( "" ) );
167 
168         private String value;
169 
170         public StringItem( String value, boolean followedByDigit )
171         {
172             if ( followedByDigit && value.length() == 1 )
173             {
174                 // a1 = alpha-1, b1 = beta-1, m1 = milestone-1
175                 switch ( value.charAt( 0 ) )
176                 {
177                     case 'a':
178                         value = "alpha";
179                         break;
180                     case 'b':
181                         value = "beta";
182                         break;
183                     case 'm':
184                         value = "milestone";
185                         break;
186                     default:
187                 }
188             }
189             this.value = ALIASES.getProperty( value , value );
190         }
191 
192         public int getType()
193         {
194             return STRING_ITEM;
195         }
196 
197         public boolean isNull()
198         {
199             return ( comparableQualifier( value ).compareTo( RELEASE_VERSION_INDEX ) == 0 );
200         }
201 
202         /**
203          * Returns a comparable value for a qualifier.
204          *
205          * This method takes into account the ordering of known qualifiers then unknown qualifiers with lexical
206          * ordering.
207          *
208          * just returning an Integer with the index here is faster, but requires a lot of if/then/else to check for -1
209          * or QUALIFIERS.size and then resort to lexical ordering. Most comparisons are decided by the first character,
210          * so this is still fast. If more characters are needed then it requires a lexical sort anyway.
211          *
212          * @param qualifier
213          * @return an equivalent value that can be used with lexical comparison
214          */
215         public static String comparableQualifier( String qualifier )
216         {
217             int i = _QUALIFIERS.indexOf( qualifier );
218 
219             return i == -1 ? ( _QUALIFIERS.size() + "-" + qualifier ) : String.valueOf( i );
220         }
221 
222         public int compareTo( Item item )
223         {
224             if ( item == null )
225             {
226                 // 1-rc < 1, 1-ga > 1
227                 return comparableQualifier( value ).compareTo( RELEASE_VERSION_INDEX );
228             }
229             switch ( item.getType() )
230             {
231                 case INTEGER_ITEM:
232                     return -1; // 1.any < 1.1 ?
233 
234                 case STRING_ITEM:
235                     return comparableQualifier( value ).compareTo( comparableQualifier( ( (StringItem) item ).value ) );
236 
237                 case LIST_ITEM:
238                     return -1; // 1.any < 1-1
239 
240                 default:
241                     throw new RuntimeException( "invalid item: " + item.getClass() );
242             }
243         }
244 
245         public String toString()
246         {
247             return value;
248         }
249     }
250 
251     /**
252      * Represents a version list item. This class is used both for the global item list and for sub-lists (which start
253      * with '-(number)' in the version specification).
254      */
255     private static class ListItem
256         extends ArrayList<Item>
257         implements Item
258     {
259         public int getType()
260         {
261             return LIST_ITEM;
262         }
263 
264         public boolean isNull()
265         {
266             return ( size() == 0 );
267         }
268 
269         void normalize()
270         {
271             for ( int i = size() - 1; i >= 0; i-- )
272             {
273                 Item lastItem = get( i );
274 
275                 if ( lastItem.isNull() )
276                 {
277                     // remove null trailing items: 0, "", empty list
278                     remove( i );
279                 }
280                 else if ( !( lastItem instanceof ListItem ) )
281                 {
282                     break;
283                 }
284             }
285         }
286 
287         public int compareTo( Item item )
288         {
289             if ( item == null )
290             {
291                 if ( size() == 0 )
292                 {
293                     return 0; // 1-0 = 1- (normalize) = 1
294                 }
295                 Item first = get( 0 );
296                 return first.compareTo( null );
297             }
298             switch ( item.getType() )
299             {
300                 case INTEGER_ITEM:
301                     return -1; // 1-1 < 1.0.x
302 
303                 case STRING_ITEM:
304                     return 1; // 1-1 > 1-sp
305 
306                 case LIST_ITEM:
307                     Iterator<Item> left = iterator();
308                     Iterator<Item> right = ( (ListItem) item ).iterator();
309 
310                     while ( left.hasNext() || right.hasNext() )
311                     {
312                         Item l = left.hasNext() ? left.next() : null;
313                         Item r = right.hasNext() ? right.next() : null;
314 
315                         // if this is shorter, then invert the compare and mul with -1
316                         int result = l == null ? ( r == null ? 0 : -1 * r.compareTo( l ) ) : l.compareTo( r );
317 
318                         if ( result != 0 )
319                         {
320                             return result;
321                         }
322                     }
323 
324                     return 0;
325 
326                 default:
327                     throw new RuntimeException( "invalid item: " + item.getClass() );
328             }
329         }
330 
331         public String toString()
332         {
333             StringBuilder buffer = new StringBuilder();
334             for ( Iterator<Item> iter = iterator(); iter.hasNext(); )
335             {
336                 Item item = iter.next();
337                 if ( buffer.length() > 0 )
338                 {
339                     buffer.append( ( item instanceof ListItem ) ? '-' : '.' );
340                 }
341                 buffer.append( item );
342             }
343             return buffer.toString();
344         }
345     }
346 
347     public ComparableVersion( String version )
348     {
349         parseVersion( version );
350     }
351 
352     public final void parseVersion( String version )
353     {
354         this.value = version;
355 
356         items = new ListItem();
357 
358         version = version.toLowerCase( Locale.ENGLISH );
359 
360         ListItem list = items;
361 
362         Stack<Item> stack = new Stack<Item>();
363         stack.push( list );
364 
365         boolean isDigit = false;
366 
367         int startIndex = 0;
368 
369         for ( int i = 0; i < version.length(); i++ )
370         {
371             char c = version.charAt( i );
372 
373             if ( c == '.' )
374             {
375                 if ( i == startIndex )
376                 {
377                     list.add( IntegerItem.ZERO );
378                 }
379                 else
380                 {
381                     list.add( parseItem( isDigit, version.substring( startIndex, i ) ) );
382                 }
383                 startIndex = i + 1;
384             }
385             else if ( c == '-' )
386             {
387                 if ( i == startIndex )
388                 {
389                     list.add( IntegerItem.ZERO );
390                 }
391                 else
392                 {
393                     list.add( parseItem( isDigit, version.substring( startIndex, i ) ) );
394                 }
395                 startIndex = i + 1;
396 
397                 list.add( list = new ListItem() );
398                 stack.push( list );
399             }
400             else if ( Character.isDigit( c ) )
401             {
402                 if ( !isDigit && i > startIndex )
403                 {
404                     list.add( new StringItem( version.substring( startIndex, i ), true ) );
405                     startIndex = i;
406 
407                     list.add( list = new ListItem() );
408                     stack.push( list );
409                 }
410 
411                 isDigit = true;
412             }
413             else
414             {
415                 if ( isDigit && i > startIndex )
416                 {
417                     list.add( parseItem( true, version.substring( startIndex, i ) ) );
418                     startIndex = i;
419 
420                     list.add( list = new ListItem() );
421                     stack.push( list );
422                 }
423 
424                 isDigit = false;
425             }
426         }
427 
428         if ( version.length() > startIndex )
429         {
430             list.add( parseItem( isDigit, version.substring( startIndex ) ) );
431         }
432 
433         while ( !stack.isEmpty() )
434         {
435             list = (ListItem) stack.pop();
436             list.normalize();
437         }
438 
439         canonical = items.toString();
440     }
441 
442     private static Item parseItem( boolean isDigit, String buf )
443     {
444         return isDigit ? new IntegerItem( buf ) : new StringItem( buf, false );
445     }
446 
447     public int compareTo( ComparableVersion o )
448     {
449         return items.compareTo( o.items );
450     }
451 
452     public String toString()
453     {
454         return value;
455     }
456 
457     public String getCanonical()
458     {
459         return canonical;
460     }
461 
462     public boolean equals( Object o )
463     {
464         return ( o instanceof ComparableVersion ) && canonical.equals( ( (ComparableVersion) o ).canonical );
465     }
466 
467     public int hashCode()
468     {
469         return canonical.hashCode();
470     }
471 
472     /**
473      * Main to test version parsing and comparison.
474      *
475      * @param args the version strings to parse and compare
476      */
477     public static void main( String... args )
478     {
479         System.out.println( "Display parameters as parsed by Maven (in canonical form) and comparison result:" );
480         if ( args.length == 0 )
481         {
482             return;
483         }
484 
485         ComparableVersion prev = null;
486         int i = 1;
487         for ( String version : args )
488         {
489             ComparableVersion c = new ComparableVersion( version );
490 
491             if ( prev != null )
492             {
493                 int compare = prev.compareTo( c );
494                 System.out.println( "   " + prev.toString() + ' '
495                     + ( ( compare == 0 ) ? "==" : ( ( compare < 0 ) ? "<" : ">" ) ) + ' ' + version );
496             }
497 
498             System.out.println( String.valueOf( i++ ) + ". " + version + " == " + c.getCanonical() );
499 
500             prev = c;
501         }
502     }
503 }