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                 Item first = get( 0 );
534                 return first.compareTo( null );
535             }
536             switch ( item.getType() )
537             {
538                 case INT_ITEM:
539                 case LONG_ITEM:
540                 case BIGINTEGER_ITEM:
541                     return -1; // 1-1 < 1.0.x
542 
543                 case STRING_ITEM:
544                     return 1; // 1-1 > 1-sp
545 
546                 case LIST_ITEM:
547                     Iterator<Item> left = iterator();
548                     Iterator<Item> right = ( (ListItem) item ).iterator();
549 
550                     while ( left.hasNext() || right.hasNext() )
551                     {
552                         Item l = left.hasNext() ? left.next() : null;
553                         Item r = right.hasNext() ? right.next() : null;
554 
555                         // if this is shorter, then invert the compare and mul with -1
556                         int result = l == null ? ( r == null ? 0 : -1 * r.compareTo( l ) ) : l.compareTo( r );
557 
558                         if ( result != 0 )
559                         {
560                             return result;
561                         }
562                     }
563 
564                     return 0;
565 
566                 default:
567                     throw new IllegalStateException( "invalid item: " + item.getClass() );
568             }
569         }
570 
571         @Override
572         public String toString()
573         {
574             StringBuilder buffer = new StringBuilder();
575             for ( Item item : this )
576             {
577                 if ( buffer.length() > 0 )
578                 {
579                     buffer.append( ( item instanceof ListItem ) ? '-' : '.' );
580                 }
581                 buffer.append( item );
582             }
583             return buffer.toString();
584         }
585     }
586 
587     public ComparableVersion( String version )
588     {
589         parseVersion( version );
590     }
591 
592     @SuppressWarnings( "checkstyle:innerassignment" )
593     public final void parseVersion( String version )
594     {
595         this.value = version;
596 
597         items = new ListItem();
598 
599         version = version.toLowerCase( Locale.ENGLISH );
600 
601         ListItem list = items;
602 
603         Deque<Item> stack = new ArrayDeque<>();
604         stack.push( list );
605 
606         boolean isDigit = false;
607 
608         int startIndex = 0;
609 
610         for ( int i = 0; i < version.length(); i++ )
611         {
612             char c = version.charAt( i );
613 
614             if ( c == '.' )
615             {
616                 if ( i == startIndex )
617                 {
618                     list.add( IntItem.ZERO );
619                 }
620                 else
621                 {
622                     list.add( parseItem( isDigit, version.substring( startIndex, i ) ) );
623                 }
624                 startIndex = i + 1;
625             }
626             else if ( c == '-' )
627             {
628                 if ( i == startIndex )
629                 {
630                     list.add( IntItem.ZERO );
631                 }
632                 else
633                 {
634                     list.add( parseItem( isDigit, version.substring( startIndex, i ) ) );
635                 }
636                 startIndex = i + 1;
637 
638                 list.add( list = new ListItem() );
639                 stack.push( list );
640             }
641             else if ( Character.isDigit( c ) )
642             {
643                 if ( !isDigit && i > startIndex )
644                 {
645                     list.add( new StringItem( version.substring( startIndex, i ), true ) );
646                     startIndex = i;
647 
648                     list.add( list = new ListItem() );
649                     stack.push( list );
650                 }
651 
652                 isDigit = true;
653             }
654             else
655             {
656                 if ( isDigit && i > startIndex )
657                 {
658                     list.add( parseItem( true, version.substring( startIndex, i ) ) );
659                     startIndex = i;
660 
661                     list.add( list = new ListItem() );
662                     stack.push( list );
663                 }
664 
665                 isDigit = false;
666             }
667         }
668 
669         if ( version.length() > startIndex )
670         {
671             list.add( parseItem( isDigit, version.substring( startIndex ) ) );
672         }
673 
674         while ( !stack.isEmpty() )
675         {
676             list = (ListItem) stack.pop();
677             list.normalize();
678         }
679     }
680 
681     private static Item parseItem( boolean isDigit, String buf )
682     {
683         if ( isDigit )
684         {
685             buf = stripLeadingZeroes( buf );
686             if ( buf.length() <= MAX_INTITEM_LENGTH )
687             {
688                 // lower than 2^31
689                 return new IntItem( buf );
690             }
691             else if ( buf.length() <= MAX_LONGITEM_LENGTH )
692             {
693                 // lower than 2^63
694                 return new LongItem( buf );
695             }
696             return new BigIntegerItem( buf );
697         }
698         return new StringItem( buf, false );
699     }
700 
701     private static String stripLeadingZeroes( String buf )
702     {
703         if ( buf == null || buf.isEmpty() )
704         {
705             return "0";
706         }
707         for ( int i = 0; i < buf.length(); ++i )
708         {
709             char c = buf.charAt( i );
710             if ( c != '0' )
711             {
712                 return buf.substring( i );
713             }
714         }
715         return buf;
716     }
717 
718     @Override
719     public int compareTo( ComparableVersion o )
720     {
721         return items.compareTo( o.items );
722     }
723 
724     @Override
725     public String toString()
726     {
727         return value;
728     }
729 
730     public String getCanonical()
731     {
732         if ( canonical == null )
733         {
734             canonical = items.toString();
735         }
736         return canonical;
737     }
738 
739     @Override
740     public boolean equals( Object o )
741     {
742         return ( o instanceof ComparableVersion ) && items.equals( ( (ComparableVersion) o ).items );
743     }
744 
745     @Override
746     public int hashCode()
747     {
748         return items.hashCode();
749     }
750 
751     // CHECKSTYLE_OFF: LineLength
752     /**
753      * Main to test version parsing and comparison.
754      * <p>
755      * To check how "1.2.7" compares to "1.2-SNAPSHOT", for example, you can issue
756      * <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>
757      * command to command line. Result of given command will be something like this:
758      * <pre>
759      * Display parameters as parsed by Maven (in canonical form) and comparison result:
760      * 1. 1.2.7 == 1.2.7
761      *    1.2.7 &gt; 1.2-SNAPSHOT
762      * 2. 1.2-SNAPSHOT == 1.2-snapshot
763      * </pre>
764      *
765      * @param args the version strings to parse and compare. You can pass arbitrary number of version strings and always
766      * two adjacent will be compared
767      */
768     // CHECKSTYLE_ON: LineLength
769     public static void main( String... args )
770     {
771         System.out.println( "Display parameters as parsed by Maven (in canonical form) and comparison result:" );
772         if ( args.length == 0 )
773         {
774             return;
775         }
776 
777         ComparableVersion prev = null;
778         int i = 1;
779         for ( String version : args )
780         {
781             ComparableVersion c = new ComparableVersion( version );
782 
783             if ( prev != null )
784             {
785                 int compare = prev.compareTo( c );
786                 System.out.println( "   " + prev.toString() + ' '
787                     + ( ( compare == 0 ) ? "==" : ( ( compare < 0 ) ? "<" : ">" ) ) + ' ' + version );
788             }
789 
790             System.out.println( String.valueOf( i++ ) + ". " + version + " == " + c.getCanonical() );
791 
792             prev = c;
793         }
794     }
795 }