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   * <p>
33   * Generic implementation of version comparison.
34   * </p>
35   * 
36   * Features:
37   * <ul>
38   * <li>mixing of '<code>-</code>' (hyphen) and '<code>.</code>' (dot) separators,</li>
39   * <li>transition between characters and digits also constitutes a separator:
40   *     <code>1.0alpha1 =&gt; [1, 0, alpha, 1]</code></li>
41   * <li>unlimited number of version components,</li>
42   * <li>version components in the text can be digits or strings,</li>
43   * <li>strings are checked for well-known qualifiers and the qualifier ordering is used for version ordering.
44   *     Well-known qualifiers (case insensitive) are:<ul>
45   *     <li><code>alpha</code> or <code>a</code></li>
46   *     <li><code>beta</code> or <code>b</code></li>
47   *     <li><code>milestone</code> or <code>m</code></li>
48   *     <li><code>rc</code> or <code>cr</code></li>
49   *     <li><code>snapshot</code></li>
50   *     <li><code>(the empty string)</code> or <code>ga</code> or <code>final</code></li>
51   *     <li><code>sp</code></li>
52   *     </ul>
53   *     Unknown qualifiers are considered after known qualifiers, with lexical order (always case insensitive),
54   *   </li>
55   * <li>a hyphen usually precedes a qualifier, and is always less important than something preceded with a dot.</li>
56   * </ul>
57   *
58   * @see <a href="https://cwiki.apache.org/confluence/display/MAVENOLD/Versioning">"Versioning" on Maven Wiki</a>
59   * @author <a href="mailto:kenney@apache.org">Kenney Westerhof</a>
60   * @author <a href="mailto:hboutemy@apache.org">Hervé Boutemy</a>
61   */
62  public class ComparableVersion
63      implements Comparable<ComparableVersion>
64  {
65      private String value;
66  
67      private String canonical;
68  
69      private ListItem items;
70  
71      private interface Item
72      {
73          int INTEGER_ITEM = 0;
74          int STRING_ITEM = 1;
75          int LIST_ITEM = 2;
76  
77          int compareTo( Item item );
78  
79          int getType();
80  
81          boolean isNull();
82      }
83  
84      /**
85       * Represents a numeric item in the version item list.
86       */
87      private static class IntegerItem
88          implements Item
89      {
90          private static final BigInteger BIG_INTEGER_ZERO = new BigInteger( "0" );
91  
92          private final BigInteger value;
93  
94          public static final IntegerItem ZERO = new IntegerItem();
95  
96          private IntegerItem()
97          {
98              this.value = BIG_INTEGER_ZERO;
99          }
100 
101         IntegerItem( String str )
102         {
103             this.value = new BigInteger( str );
104         }
105 
106         public int getType()
107         {
108             return INTEGER_ITEM;
109         }
110 
111         public boolean isNull()
112         {
113             return BIG_INTEGER_ZERO.equals( value );
114         }
115 
116         public int compareTo( Item item )
117         {
118             if ( item == null )
119             {
120                 return BIG_INTEGER_ZERO.equals( value ) ? 0 : 1; // 1.0 == 1, 1.1 > 1
121             }
122 
123             switch ( item.getType() )
124             {
125                 case INTEGER_ITEM:
126                     return value.compareTo( ( (IntegerItem) item ).value );
127 
128                 case STRING_ITEM:
129                     return 1; // 1.1 > 1-sp
130 
131                 case LIST_ITEM:
132                     return 1; // 1.1 > 1-1
133 
134                 default:
135                     throw new RuntimeException( "invalid item: " + item.getClass() );
136             }
137         }
138 
139         public String toString()
140         {
141             return value.toString();
142         }
143     }
144 
145     /**
146      * Represents a string in the version item list, usually a qualifier.
147      */
148     private static class StringItem
149         implements Item
150     {
151         private static final List<String> QUALIFIERS =
152                 Arrays.asList( "alpha", "beta", "milestone", "rc", "snapshot", "", "sp"  );
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         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     @SuppressWarnings( "checkstyle:innerassignment" )
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<>();
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     // CHECKSTYLE_OFF: LineLength
473     /**
474      * Main to test version parsing and comparison.
475      * <p>
476      * To check how "1.2.7" compares to "1.2-SNAPSHOT", for example, you can issue
477      * <pre>java -jar ${maven.repo.local}/org/apache/maven/maven-artifact/${maven.version}/maven-artifact-${maven.version}.jar "1.2.7" "1.2-SNAPSHOT"</pre>
478      * command to command line. Result of given command will be something like this:
479      * <pre>
480      * Display parameters as parsed by Maven (in canonical form) and comparison result:
481      * 1. 1.2.7 == 1.2.7
482      *    1.2.7 &gt; 1.2-SNAPSHOT
483      * 2. 1.2-SNAPSHOT == 1.2-snapshot
484      * </pre>
485      *
486      * @param args the version strings to parse and compare. You can pass arbitrary number of version strings and always
487      * two adjacent will be compared
488      */
489     // CHECKSTYLE_ON: LineLength
490     public static void main( String... args )
491     {
492         System.out.println( "Display parameters as parsed by Maven (in canonical form) and comparison result:" );
493         if ( args.length == 0 )
494         {
495             return;
496         }
497 
498         ComparableVersion prev = null;
499         int i = 1;
500         for ( String version : args )
501         {
502             ComparableVersion c = new ComparableVersion( version );
503 
504             if ( prev != null )
505             {
506                 int compare = prev.compareTo( c );
507                 System.out.println( "   " + prev.toString() + ' '
508                     + ( ( compare == 0 ) ? "==" : ( ( compare < 0 ) ? "<" : ">" ) ) + ' ' + version );
509             }
510 
511             System.out.println( String.valueOf( i++ ) + ". " + version + " == " + c.getCanonical() );
512 
513             prev = c;
514         }
515     }
516 }