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 ( Item item : this )
335             {
336                 if ( buffer.length() > 0 )
337                 {
338                     buffer.append( ( item instanceof ListItem ) ? '-' : '.' );
339                 }
340                 buffer.append( item );
341             }
342             return buffer.toString();
343         }
344     }
345 
346     public ComparableVersion( String version )
347     {
348         parseVersion( version );
349     }
350 
351     public final void parseVersion( String version )
352     {
353         this.value = version;
354 
355         items = new ListItem();
356 
357         version = version.toLowerCase( Locale.ENGLISH );
358 
359         ListItem list = items;
360 
361         Stack<Item> stack = new Stack<>();
362         stack.push( list );
363 
364         boolean isDigit = false;
365 
366         int startIndex = 0;
367 
368         for ( int i = 0; i < version.length(); i++ )
369         {
370             char c = version.charAt( i );
371 
372             if ( c == '.' )
373             {
374                 if ( i == startIndex )
375                 {
376                     list.add( IntegerItem.ZERO );
377                 }
378                 else
379                 {
380                     list.add( parseItem( isDigit, version.substring( startIndex, i ) ) );
381                 }
382                 startIndex = i + 1;
383             }
384             else if ( c == '-' )
385             {
386                 if ( i == startIndex )
387                 {
388                     list.add( IntegerItem.ZERO );
389                 }
390                 else
391                 {
392                     list.add( parseItem( isDigit, version.substring( startIndex, i ) ) );
393                 }
394                 startIndex = i + 1;
395 
396                 list.add( list = new ListItem() );
397                 stack.push( list );
398             }
399             else if ( Character.isDigit( c ) )
400             {
401                 if ( !isDigit && i > startIndex )
402                 {
403                     list.add( new StringItem( version.substring( startIndex, i ), true ) );
404                     startIndex = i;
405 
406                     list.add( list = new ListItem() );
407                     stack.push( list );
408                 }
409 
410                 isDigit = true;
411             }
412             else
413             {
414                 if ( isDigit && i > startIndex )
415                 {
416                     list.add( parseItem( true, version.substring( startIndex, i ) ) );
417                     startIndex = i;
418 
419                     list.add( list = new ListItem() );
420                     stack.push( list );
421                 }
422 
423                 isDigit = false;
424             }
425         }
426 
427         if ( version.length() > startIndex )
428         {
429             list.add( parseItem( isDigit, version.substring( startIndex ) ) );
430         }
431 
432         while ( !stack.isEmpty() )
433         {
434             list = (ListItem) stack.pop();
435             list.normalize();
436         }
437 
438         canonical = items.toString();
439     }
440 
441     private static Item parseItem( boolean isDigit, String buf )
442     {
443         return isDigit ? new IntegerItem( buf ) : new StringItem( buf, false );
444     }
445 
446     public int compareTo( ComparableVersion o )
447     {
448         return items.compareTo( o.items );
449     }
450 
451     public String toString()
452     {
453         return value;
454     }
455 
456     public String getCanonical()
457     {
458         return canonical;
459     }
460 
461     public boolean equals( Object o )
462     {
463         return ( o instanceof ComparableVersion ) && canonical.equals( ( (ComparableVersion) o ).canonical );
464     }
465 
466     public int hashCode()
467     {
468         return canonical.hashCode();
469     }
470 
471     /**
472      * Main to test version parsing and comparison.
473      *
474      * @param args the version strings to parse and compare
475      */
476     public static void main( String... args )
477     {
478         System.out.println( "Display parameters as parsed by Maven (in canonical form) and comparison result:" );
479         if ( args.length == 0 )
480         {
481             return;
482         }
483 
484         ComparableVersion prev = null;
485         int i = 1;
486         for ( String version : args )
487         {
488             ComparableVersion c = new ComparableVersion( version );
489 
490             if ( prev != null )
491             {
492                 int compare = prev.compareTo( c );
493                 System.out.println( "   " + prev.toString() + ' '
494                     + ( ( compare == 0 ) ? "==" : ( ( compare < 0 ) ? "<" : ">" ) ) + ' ' + version );
495             }
496 
497             System.out.println( String.valueOf( i++ ) + ". " + version + " == " + c.getCanonical() );
498 
499             prev = c;
500         }
501     }
502 }