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.ArrayDeque;
24  import java.util.ArrayList;
25  import java.util.Arrays;
26  import java.util.Deque;
27  import java.util.Iterator;
28  import java.util.List;
29  import java.util.Locale;
30  import java.util.Properties;
31  
32  import org.apache.commons.lang3.StringUtils;
33  
34  /**
35   * <p>
36   * Generic implementation of version comparison.
37   * </p>
38   * 
39   * Features:
40   * <ul>
41   * <li>mixing of '<code>-</code>' (hyphen) and '<code>.</code>' (dot) separators,</li>
42   * <li>transition between characters and digits also constitutes a separator:
43   *     <code>1.0alpha1 =&gt; [1, 0, alpha, 1]</code></li>
44   * <li>unlimited number of version components,</li>
45   * <li>version components in the text can be digits or strings,</li>
46   * <li>strings are checked for well-known qualifiers and the qualifier ordering is used for version ordering.
47   *     Well-known qualifiers (case insensitive) are:<ul>
48   *     <li><code>alpha</code> or <code>a</code></li>
49   *     <li><code>beta</code> or <code>b</code></li>
50   *     <li><code>milestone</code> or <code>m</code></li>
51   *     <li><code>rc</code> or <code>cr</code></li>
52   *     <li><code>snapshot</code></li>
53   *     <li><code>(the empty string)</code> or <code>ga</code> or <code>final</code></li>
54   *     <li><code>sp</code></li>
55   *     </ul>
56   *     Unknown qualifiers are considered after known qualifiers, with lexical order (always case insensitive),
57   *   </li>
58   * <li>a hyphen usually precedes a qualifier, and is always less important than something preceded with a dot.</li>
59   * </ul>
60   *
61   * @see <a href="https://cwiki.apache.org/confluence/display/MAVENOLD/Versioning">"Versioning" on Maven Wiki</a>
62   * @author <a href="mailto:kenney@apache.org">Kenney Westerhof</a>
63   * @author <a href="mailto:hboutemy@apache.org">Hervé Boutemy</a>
64   */
65  public class ComparableVersion
66      implements Comparable<ComparableVersion>
67  {
68      private static final int MAX_INTITEM_LENGTH = 9;
69  
70      private static final int MAX_LONGITEM_LENGTH = 18;
71  
72      private String value;
73  
74      private String canonical;
75  
76      private ListItem items;
77  
78      private interface Item
79      {
80          int INT_ITEM = 3;
81          int LONG_ITEM = 4;
82          int BIGINTEGER_ITEM = 0;
83          int STRING_ITEM = 1;
84          int LIST_ITEM = 2;
85  
86          int compareTo( Item item );
87  
88          int getType();
89  
90          boolean isNull();
91      }
92  
93      /**
94       * Represents a numeric item in the version item list that can be represented with an int.
95       */
96      private static class IntItem
97          implements Item
98      {
99          private final int value;
100 
101         public static final IntItem ZERO = new IntItem();
102 
103         private IntItem()
104         {
105             this.value = 0;
106         }
107 
108         IntItem( String str )
109         {
110             this.value = Integer.parseInt( str );
111         }
112 
113         @Override
114         public int getType()
115         {
116             return INT_ITEM;
117         }
118 
119         @Override
120         public boolean isNull()
121         {
122             return value == 0;
123         }
124 
125         @Override
126         public int compareTo( Item item )
127         {
128             if ( item == null )
129             {
130                 return ( value == 0 ) ? 0 : 1; // 1.0 == 1, 1.1 > 1
131             }
132 
133             switch ( item.getType() )
134             {
135                 case INT_ITEM:
136                     int itemValue = ( (IntItem) item ).value;
137                     return ( value < itemValue ) ? -1 : ( ( value == itemValue ) ? 0 : 1 );
138                 case LONG_ITEM:
139                 case BIGINTEGER_ITEM:
140                     return -1;
141 
142                 case STRING_ITEM:
143                     return 1; // 1.1 > 1-sp
144 
145                 case LIST_ITEM:
146                     return 1; // 1.1 > 1-1
147 
148                 default:
149                     throw new IllegalStateException( "invalid item: " + item.getClass() );
150             }
151         }
152 
153         @Override
154         public String toString()
155         {
156             return Integer.toString( value );
157         }
158     }
159 
160     /**
161      * Represents a numeric item in the version item list that can be represented with a long.
162      */
163     private static class LongItem
164         implements Item
165     {
166         private final long value;
167 
168         LongItem( String str )
169         {
170             this.value = Long.parseLong( str );
171         }
172 
173         @Override
174         public int getType()
175         {
176             return LONG_ITEM;
177         }
178 
179         @Override
180         public boolean isNull()
181         {
182             return value == 0;
183         }
184 
185         @Override
186         public int compareTo( Item item )
187         {
188             if ( item == null )
189             {
190                 return ( value == 0 ) ? 0 : 1; // 1.0 == 1, 1.1 > 1
191             }
192 
193             switch ( item.getType() )
194             {
195                 case INT_ITEM:
196                     return 1;
197                 case LONG_ITEM:
198                     long itemValue = ( (LongItem) item ).value;
199                     return ( value < itemValue ) ? -1 : ( ( value == itemValue ) ? 0 : 1 );
200                 case BIGINTEGER_ITEM:
201                     return -1;
202 
203                 case STRING_ITEM:
204                     return 1; // 1.1 > 1-sp
205 
206                 case LIST_ITEM:
207                     return 1; // 1.1 > 1-1
208 
209                 default:
210                     throw new IllegalStateException( "invalid item: " + item.getClass() );
211             }
212         }
213 
214         @Override
215         public String toString()
216         {
217             return Long.toString( value );
218         }
219     }
220 
221     /**
222      * Represents a numeric item in the version item list.
223      */
224     private static class BigIntegerItem
225         implements Item
226     {
227         private final BigInteger value;
228 
229         BigIntegerItem( String str )
230         {
231             this.value = new BigInteger( str );
232         }
233 
234         @Override
235         public int getType()
236         {
237             return BIGINTEGER_ITEM;
238         }
239 
240         @Override
241         public boolean isNull()
242         {
243             return BigInteger.ZERO.equals( value );
244         }
245 
246         @Override
247         public int compareTo( Item item )
248         {
249             if ( item == null )
250             {
251                 return BigInteger.ZERO.equals( value ) ? 0 : 1; // 1.0 == 1, 1.1 > 1
252             }
253 
254             switch ( item.getType() )
255             {
256                 case INT_ITEM:
257                 case LONG_ITEM:
258                     return 1;
259 
260                 case BIGINTEGER_ITEM:
261                     return value.compareTo( ( (BigIntegerItem) item ).value );
262 
263                 case STRING_ITEM:
264                     return 1; // 1.1 > 1-sp
265 
266                 case LIST_ITEM:
267                     return 1; // 1.1 > 1-1
268 
269                 default:
270                     throw new IllegalStateException( "invalid item: " + item.getClass() );
271             }
272         }
273 
274         @Override
275         public String toString()
276         {
277             return value.toString();
278         }
279     }
280 
281     /**
282      * Represents a string in the version item list, usually a qualifier.
283      */
284     private static class StringItem
285         implements Item
286     {
287         private static final List<String> QUALIFIERS =
288                 Arrays.asList( "alpha", "beta", "milestone", "rc", "snapshot", "", "sp"  );
289 
290         private static final Properties ALIASES = new Properties();
291         static
292         {
293             ALIASES.put( "ga", "" );
294             ALIASES.put( "final", "" );
295             ALIASES.put( "cr", "rc" );
296         }
297 
298         /**
299          * A comparable value for the empty-string qualifier. This one is used to determine if a given qualifier makes
300          * the version older than one without a qualifier, or more recent.
301          */
302         private static final String RELEASE_VERSION_INDEX = String.valueOf( QUALIFIERS.indexOf( "" ) );
303 
304         private final String value;
305 
306         StringItem( String value, boolean followedByDigit )
307         {
308             if ( followedByDigit && value.length() == 1 )
309             {
310                 // a1 = alpha-1, b1 = beta-1, m1 = milestone-1
311                 switch ( value.charAt( 0 ) )
312                 {
313                     case 'a':
314                         value = "alpha";
315                         break;
316                     case 'b':
317                         value = "beta";
318                         break;
319                     case 'm':
320                         value = "milestone";
321                         break;
322                     default:
323                 }
324             }
325             this.value = ALIASES.getProperty( value , value );
326         }
327 
328         @Override
329         public int getType()
330         {
331             return STRING_ITEM;
332         }
333 
334         @Override
335         public boolean isNull()
336         {
337             return ( comparableQualifier( value ).compareTo( RELEASE_VERSION_INDEX ) == 0 );
338         }
339 
340         /**
341          * Returns a comparable value for a qualifier.
342          *
343          * This method takes into account the ordering of known qualifiers then unknown qualifiers with lexical
344          * ordering.
345          *
346          * just returning an Integer with the index here is faster, but requires a lot of if/then/else to check for -1
347          * or QUALIFIERS.size and then resort to lexical ordering. Most comparisons are decided by the first character,
348          * so this is still fast. If more characters are needed then it requires a lexical sort anyway.
349          *
350          * @param qualifier
351          * @return an equivalent value that can be used with lexical comparison
352          */
353         public static String comparableQualifier( String qualifier )
354         {
355             int i = QUALIFIERS.indexOf( qualifier );
356 
357             return i == -1 ? ( QUALIFIERS.size() + "-" + qualifier ) : String.valueOf( i );
358         }
359 
360         @Override
361         public int compareTo( Item item )
362         {
363             if ( item == null )
364             {
365                 // 1-rc < 1, 1-ga > 1
366                 return comparableQualifier( value ).compareTo( RELEASE_VERSION_INDEX );
367             }
368             switch ( item.getType() )
369             {
370                 case INT_ITEM:
371                 case LONG_ITEM:
372                 case BIGINTEGER_ITEM:
373                     return -1; // 1.any < 1.1 ?
374 
375                 case STRING_ITEM:
376                     return comparableQualifier( value ).compareTo( comparableQualifier( ( (StringItem) item ).value ) );
377 
378                 case LIST_ITEM:
379                     return -1; // 1.any < 1-1
380 
381                 default:
382                     throw new IllegalStateException( "invalid item: " + item.getClass() );
383             }
384         }
385 
386         @Override
387         public String toString()
388         {
389             return value;
390         }
391     }
392 
393     /**
394      * Represents a version list item. This class is used both for the global item list and for sub-lists (which start
395      * with '-(number)' in the version specification).
396      */
397     private static class ListItem
398         extends ArrayList<Item>
399         implements Item
400     {
401         @Override
402         public int getType()
403         {
404             return LIST_ITEM;
405         }
406 
407         @Override
408         public boolean isNull()
409         {
410             return ( size() == 0 );
411         }
412 
413         void normalize()
414         {
415             for ( int i = size() - 1; i >= 0; i-- )
416             {
417                 Item lastItem = get( i );
418 
419                 if ( lastItem.isNull() )
420                 {
421                     // remove null trailing items: 0, "", empty list
422                     remove( i );
423                 }
424                 else if ( !( lastItem instanceof ListItem ) )
425                 {
426                     break;
427                 }
428             }
429         }
430 
431         @Override
432         public int compareTo( Item item )
433         {
434             if ( item == null )
435             {
436                 if ( size() == 0 )
437                 {
438                     return 0; // 1-0 = 1- (normalize) = 1
439                 }
440                 Item first = get( 0 );
441                 return first.compareTo( null );
442             }
443             switch ( item.getType() )
444             {
445                 case INT_ITEM:
446                 case LONG_ITEM:
447                 case BIGINTEGER_ITEM:
448                     return -1; // 1-1 < 1.0.x
449 
450                 case STRING_ITEM:
451                     return 1; // 1-1 > 1-sp
452 
453                 case LIST_ITEM:
454                     Iterator<Item> left = iterator();
455                     Iterator<Item> right = ( (ListItem) item ).iterator();
456 
457                     while ( left.hasNext() || right.hasNext() )
458                     {
459                         Item l = left.hasNext() ? left.next() : null;
460                         Item r = right.hasNext() ? right.next() : null;
461 
462                         // if this is shorter, then invert the compare and mul with -1
463                         int result = l == null ? ( r == null ? 0 : -1 * r.compareTo( l ) ) : l.compareTo( r );
464 
465                         if ( result != 0 )
466                         {
467                             return result;
468                         }
469                     }
470 
471                     return 0;
472 
473                 default:
474                     throw new IllegalStateException( "invalid item: " + item.getClass() );
475             }
476         }
477 
478         @Override
479         public String toString()
480         {
481             StringBuilder buffer = new StringBuilder();
482             for ( Item item : this )
483             {
484                 if ( buffer.length() > 0 )
485                 {
486                     buffer.append( ( item instanceof ListItem ) ? '-' : '.' );
487                 }
488                 buffer.append( item );
489             }
490             return buffer.toString();
491         }
492     }
493 
494     public ComparableVersion( String version )
495     {
496         parseVersion( version );
497     }
498 
499     @SuppressWarnings( "checkstyle:innerassignment" )
500     public final void parseVersion( String version )
501     {
502         this.value = version;
503 
504         items = new ListItem();
505 
506         version = version.toLowerCase( Locale.ENGLISH );
507 
508         ListItem list = items;
509 
510         Deque<Item> stack = new ArrayDeque<>();
511         stack.push( list );
512 
513         boolean isDigit = false;
514 
515         int startIndex = 0;
516 
517         for ( int i = 0; i < version.length(); i++ )
518         {
519             char c = version.charAt( i );
520 
521             if ( c == '.' )
522             {
523                 if ( i == startIndex )
524                 {
525                     list.add( IntItem.ZERO );
526                 }
527                 else
528                 {
529                     list.add( parseItem( isDigit, version.substring( startIndex, i ) ) );
530                 }
531                 startIndex = i + 1;
532             }
533             else if ( c == '-' )
534             {
535                 if ( i == startIndex )
536                 {
537                     list.add( IntItem.ZERO );
538                 }
539                 else
540                 {
541                     list.add( parseItem( isDigit, version.substring( startIndex, i ) ) );
542                 }
543                 startIndex = i + 1;
544 
545                 list.add( list = new ListItem() );
546                 stack.push( list );
547             }
548             else if ( Character.isDigit( c ) )
549             {
550                 if ( !isDigit && i > startIndex )
551                 {
552                     list.add( new StringItem( version.substring( startIndex, i ), true ) );
553                     startIndex = i;
554 
555                     list.add( list = new ListItem() );
556                     stack.push( list );
557                 }
558 
559                 isDigit = true;
560             }
561             else
562             {
563                 if ( isDigit && i > startIndex )
564                 {
565                     list.add( parseItem( true, version.substring( startIndex, i ) ) );
566                     startIndex = i;
567 
568                     list.add( list = new ListItem() );
569                     stack.push( list );
570                 }
571 
572                 isDigit = false;
573             }
574         }
575 
576         if ( version.length() > startIndex )
577         {
578             list.add( parseItem( isDigit, version.substring( startIndex ) ) );
579         }
580 
581         while ( !stack.isEmpty() )
582         {
583             list = (ListItem) stack.pop();
584             list.normalize();
585         }
586 
587         canonical = items.toString();
588     }
589 
590     private static Item parseItem( boolean isDigit, String buf )
591     {
592         if ( isDigit )
593         {
594             buf = stripLeadingZeroes( buf );
595             if ( buf.length() <= MAX_INTITEM_LENGTH )
596             {
597                 // lower than 2^31
598                 return new IntItem( buf );
599             }
600             else if ( buf.length() <= MAX_LONGITEM_LENGTH )
601             {
602                 // lower than 2^63
603                 return new LongItem( buf );
604             }
605             return new BigIntegerItem( buf );
606         }
607         return new StringItem( buf, false );
608     }
609 
610     private static String stripLeadingZeroes( String buf )
611     {
612         String strippedBuf = StringUtils.stripStart( buf, "0" );
613         if ( strippedBuf.isEmpty() )
614         {
615             return "0";
616         }
617         return strippedBuf;
618     }
619 
620     @Override
621     public int compareTo( ComparableVersion o )
622     {
623         return items.compareTo( o.items );
624     }
625 
626     @Override
627     public String toString()
628     {
629         return value;
630     }
631 
632     public String getCanonical()
633     {
634         return canonical;
635     }
636 
637     @Override
638     public boolean equals( Object o )
639     {
640         return ( o instanceof ComparableVersion ) && canonical.equals( ( (ComparableVersion) o ).canonical );
641     }
642 
643     @Override
644     public int hashCode()
645     {
646         return canonical.hashCode();
647     }
648 
649     // CHECKSTYLE_OFF: LineLength
650     /**
651      * Main to test version parsing and comparison.
652      * <p>
653      * To check how "1.2.7" compares to "1.2-SNAPSHOT", for example, you can issue
654      * <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>
655      * command to command line. Result of given command will be something like this:
656      * <pre>
657      * Display parameters as parsed by Maven (in canonical form) and comparison result:
658      * 1. 1.2.7 == 1.2.7
659      *    1.2.7 &gt; 1.2-SNAPSHOT
660      * 2. 1.2-SNAPSHOT == 1.2-snapshot
661      * </pre>
662      *
663      * @param args the version strings to parse and compare. You can pass arbitrary number of version strings and always
664      * two adjacent will be compared
665      */
666     // CHECKSTYLE_ON: LineLength
667     public static void main( String... args )
668     {
669         System.out.println( "Display parameters as parsed by Maven (in canonical form) and comparison result:" );
670         if ( args.length == 0 )
671         {
672             return;
673         }
674 
675         ComparableVersion prev = null;
676         int i = 1;
677         for ( String version : args )
678         {
679             ComparableVersion c = new ComparableVersion( version );
680 
681             if ( prev != null )
682             {
683                 int compare = prev.compareTo( c );
684                 System.out.println( "   " + prev.toString() + ' '
685                     + ( ( compare == 0 ) ? "==" : ( ( compare < 0 ) ? "<" : ">" ) ) + ' ' + version );
686             }
687 
688             System.out.println( String.valueOf( i++ ) + ". " + version + " == " + c.getCanonical() );
689 
690             prev = c;
691         }
692     }
693 }