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