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