View Javadoc

1   package org.apache.maven.artifact.versioning;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.math.BigInteger;
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.Iterator;
26  import java.util.List;
27  import java.util.ListIterator;
28  import java.util.Locale;
29  import java.util.Properties;
30  import java.util.Stack;
31  
32  /**
33   * Generic implementation of version comparison. Features:
34   * <ul>
35   * <li>mixing of '<code>-</code>' (dash) and '<code>.</code>' (dot) separators,</li>
36   * <li>transition between characters and digits also constitutes a separator:
37   *     <code>1.0alpha1 =&gt; [1, 0, alpha, 1]</code></li>
38   * <li>unlimited number of version components,</li>
39   * <li>version components in the text can be digits or strings</li>
40   * <li>strings are checked for well-known qualifiers and the qualifier ordering is used for version ordering.
41   *     Well-known qualifiers (case insensitive):<ul>
42   *     <li><code>snapshot</code></li>
43   *     <li><code>alpha</code> or <code>a</code></li>
44   *     <li><code>beta</code> or <code>b</code></li>
45   *     <li><code>milestone</code> or <code>m</code></li>
46   *     <li><code>rc</code> or <code>cr</code></li>
47   *     <li><code>(the empty string)</code> or <code>ga</code> or <code>final</code></li>
48   *     <li><code>sp</code></li>
49   *     </ul>
50   *   </li>
51   * <li>a dash usually precedes a qualifier, and is always less important than something preceded with a dot.</li>
52   * </ul>
53   *
54   * @see <a href="http://docs.codehaus.org/display/MAVEN/Versioning">"Versioning" on Maven Wiki</a>
55   * @author <a href="mailto:kenney@apache.org">Kenney Westerhof</a>
56   * @author <a href="mailto:hboutemy@apache.org">Hervé Boutemy</a>
57   * @version $Id: ComparableVersion.java 958295 2010-06-26 23:16:18Z hboutemy $
58   */
59  public class ComparableVersion
60      implements Comparable<ComparableVersion>
61  {
62      private String value;
63  
64      private String canonical;
65  
66      private ListItem items;
67  
68      private interface Item
69      {
70          final int INTEGER_ITEM = 0;
71          final int STRING_ITEM = 1;
72          final int LIST_ITEM = 2;
73  
74          int compareTo( Item item );
75  
76          int getType();
77  
78          boolean isNull();
79      }
80  
81      /**
82       * Represents a numeric item in the version item list.
83       */
84      private static class IntegerItem
85          implements Item
86      {
87      	private static final BigInteger BigInteger_ZERO = new BigInteger( "0" );
88  
89          private final BigInteger value;
90  
91          public static final IntegerItem ZERO = new IntegerItem();
92  
93          private IntegerItem()
94          {
95              this.value = BigInteger_ZERO;
96          }
97  
98          public IntegerItem( String str )
99          {
100             this.value = new BigInteger( str );
101         }
102 
103         public int getType()
104         {
105             return INTEGER_ITEM;
106         }
107 
108         public boolean isNull()
109         {
110             return BigInteger_ZERO.equals( value );
111         }
112 
113         public int compareTo( Item item )
114         {
115             if ( item == null )
116             {
117                 return BigInteger_ZERO.equals( value ) ? 0 : 1; // 1.0 == 1, 1.1 > 1
118             }
119 
120             switch ( item.getType() )
121             {
122                 case INTEGER_ITEM:
123                     return value.compareTo( ( (IntegerItem) item ).value );
124 
125                 case STRING_ITEM:
126                     return 1; // 1.1 > 1-sp
127 
128                 case LIST_ITEM:
129                     return 1; // 1.1 > 1-1
130 
131                 default:
132                     throw new RuntimeException( "invalid item: " + item.getClass() );
133             }
134         }
135 
136         public String toString()
137         {
138             return value.toString();
139         }
140     }
141 
142     /**
143      * Represents a string in the version item list, usually a qualifier.
144      */
145     private static class StringItem
146         implements Item
147     {
148         private static final String[] QUALIFIERS = { "alpha", "beta", "milestone", "rc", "snapshot", "", "sp" };
149 
150         private static final List<String> _QUALIFIERS = Arrays.asList( QUALIFIERS );
151 
152         private static final Properties ALIASES = new Properties();
153         static
154         {
155             ALIASES.put( "ga", "" );
156             ALIASES.put( "final", "" );
157             ALIASES.put( "cr", "rc" );
158         }
159 
160         /**
161          * A comparable value for the empty-string qualifier. This one is used to determine if a given qualifier makes
162          * the version older than one without a qualifier, or more recent.
163          */
164         private static final String RELEASE_VERSION_INDEX = String.valueOf( _QUALIFIERS.indexOf( "" ) );
165 
166         private String value;
167 
168         public StringItem( String value, boolean followedByDigit )
169         {
170             if ( followedByDigit && value.length() == 1 )
171             {
172                 // a1 = alpha-1, b1 = beta-1, m1 = milestone-1
173                 switch ( value.charAt( 0 ) )
174                 {
175                     case 'a':
176                         value = "alpha";
177                         break;
178                     case 'b':
179                         value = "beta";
180                         break;
181                     case 'm':
182                         value = "milestone";
183                         break;
184                 }
185             }
186             this.value = ALIASES.getProperty( value , value );
187         }
188 
189         public int getType()
190         {
191             return STRING_ITEM;
192         }
193 
194         public boolean isNull()
195         {
196             return ( comparableQualifier( value ).compareTo( RELEASE_VERSION_INDEX ) == 0 );
197         }
198 
199         /**
200          * Returns a comparable value for a qualifier.
201          *
202          * This method both takes into account the ordering of known qualifiers as well as lexical ordering for unknown
203          * qualifiers.
204          *
205          * just returning an Integer with the index here is faster, but requires a lot of if/then/else to check for -1
206          * or QUALIFIERS.size and then resort to lexical ordering. Most comparisons are decided by the first character,
207          * so this is still fast. If more characters are needed then it requires a lexical sort anyway.
208          *
209          * @param qualifier
210          * @return an equivalent value that can be used with lexical comparison
211          */
212         public static String comparableQualifier( String qualifier )
213         {
214             int i = _QUALIFIERS.indexOf( qualifier );
215 
216             return i == -1 ? _QUALIFIERS.size() + "-" + qualifier : String.valueOf( i );
217         }
218 
219         public int compareTo( Item item )
220         {
221             if ( item == null )
222             {
223                 // 1-rc < 1, 1-ga > 1
224                 return comparableQualifier( value ).compareTo( RELEASE_VERSION_INDEX );
225             }
226             switch ( item.getType() )
227             {
228                 case INTEGER_ITEM:
229                     return -1; // 1.any < 1.1 ?
230 
231                 case STRING_ITEM:
232                     return comparableQualifier( value ).compareTo( comparableQualifier( ( (StringItem) item ).value ) );
233 
234                 case LIST_ITEM:
235                     return -1; // 1.any < 1-1
236 
237                 default:
238                     throw new RuntimeException( "invalid item: " + item.getClass() );
239             }
240         }
241 
242         public String toString()
243         {
244             return value;
245         }
246     }
247 
248     /**
249      * Represents a version list item. This class is used both for the global item list and for sub-lists (which start
250      * with '-(number)' in the version specification).
251      */
252     private static class ListItem
253         extends ArrayList<Item>
254         implements Item
255     {
256         public int getType()
257         {
258             return LIST_ITEM;
259         }
260 
261         public boolean isNull()
262         {
263             return ( size() == 0 );
264         }
265 
266         void normalize()
267         {
268             for( ListIterator<Item> iterator = listIterator( size() ); iterator.hasPrevious(); )
269             {
270                 Item item = iterator.previous();
271                 if ( item.isNull() )
272                 {
273                     iterator.remove(); // remove null trailing items: 0, "", empty list
274                 }
275                 else
276                 {
277                     break;
278                 }
279             }
280         }
281 
282         public int compareTo( Item item )
283         {
284             if ( item == null )
285             {
286                 if ( size() == 0 )
287                 {
288                     return 0; // 1-0 = 1- (normalize) = 1
289                 }
290                 Item first = get( 0 );
291                 return first.compareTo( null );
292             }
293             switch ( item.getType() )
294             {
295                 case INTEGER_ITEM:
296                     return -1; // 1-1 < 1.0.x
297 
298                 case STRING_ITEM:
299                     return 1; // 1-1 > 1-sp
300 
301                 case LIST_ITEM:
302                     Iterator<Item> left = iterator();
303                     Iterator<Item> right = ( (ListItem) item ).iterator();
304 
305                     while ( left.hasNext() || right.hasNext() )
306                     {
307                         Item l = left.hasNext() ? left.next() : null;
308                         Item r = right.hasNext() ? right.next() : null;
309 
310                         // if this is shorter, then invert the compare and mul with -1
311                         int result = l == null ? -1 * r.compareTo( l ) : l.compareTo( r );
312 
313                         if ( result != 0 )
314                         {
315                             return result;
316                         }
317                     }
318 
319                     return 0;
320 
321                 default:
322                     throw new RuntimeException( "invalid item: " + item.getClass() );
323             }
324         }
325 
326         public String toString()
327         {
328             StringBuilder buffer = new StringBuilder( "(" );
329             for( Iterator<Item> iter = iterator(); iter.hasNext(); )
330             {
331                 buffer.append( iter.next() );
332                 if ( iter.hasNext() )
333                 {
334                     buffer.append( ',' );
335                 }
336             }
337             buffer.append( ')' );
338             return buffer.toString();
339         }
340     }
341 
342     public ComparableVersion( String version )
343     {
344         parseVersion( version );
345     }
346 
347     public final void parseVersion( String version )
348     {
349         this.value = version;
350 
351         items = new ListItem();
352 
353         version = version.toLowerCase( Locale.ENGLISH );
354 
355         ListItem list = items;
356 
357         Stack<Item> stack = new Stack<Item>();
358         stack.push( list );
359 
360         boolean isDigit = false;
361 
362         int startIndex = 0;
363 
364         for ( int i = 0; i < version.length(); i++ )
365         {
366             char c = version.charAt( i );
367 
368             if ( c == '.' )
369             {
370                 if ( i == startIndex )
371                 {
372                     list.add( IntegerItem.ZERO );
373                 }
374                 else
375                 {
376                     list.add( parseItem( isDigit, version.substring( startIndex, i ) ) );
377                 }
378                 startIndex = i + 1;
379             }
380             else if ( c == '-' )
381             {
382                 if ( i == startIndex )
383                 {
384                     list.add( IntegerItem.ZERO );
385                 }
386                 else
387                 {
388                     list.add( parseItem( isDigit, version.substring( startIndex, i ) ) );
389                 }
390                 startIndex = i + 1;
391 
392                 if ( isDigit )
393                 {
394                     list.normalize(); // 1.0-* = 1-*
395 
396                     if ( ( i + 1 < version.length() ) && Character.isDigit( version.charAt( i + 1 ) ) )
397                     {
398                         // new ListItem only if previous were digits and new char is a digit,
399                         // ie need to differentiate only 1.1 from 1-1
400                         list.add( list = new ListItem() );
401 
402                         stack.push( list );
403                     }
404                 }
405             }
406             else if ( Character.isDigit( c ) )
407             {
408                 if ( !isDigit && i > startIndex )
409                 {
410                     list.add( new StringItem( version.substring( startIndex, i ), true ) );
411                     startIndex = i;
412                 }
413 
414                 isDigit = true;
415             }
416             else
417             {
418                 if ( isDigit && i > startIndex )
419                 {
420                     list.add( parseItem( true, version.substring( startIndex, i ) ) );
421                     startIndex = i;
422                 }
423 
424                 isDigit = false;
425             }
426         }
427 
428         if ( version.length() > startIndex )
429         {
430             list.add( parseItem( isDigit, version.substring( startIndex ) ) );
431         }
432 
433         while ( !stack.isEmpty() )
434         {
435             list = (ListItem) stack.pop();
436             list.normalize();
437         }
438 
439         canonical = items.toString();
440     }
441 
442     private static Item parseItem( boolean isDigit, String buf )
443     {
444         return isDigit ? new IntegerItem( buf ) : new StringItem( buf, false );
445     }
446 
447     public int compareTo( ComparableVersion o )
448     {
449         return items.compareTo( o.items );
450     }
451 
452     public String toString()
453     {
454         return value;
455     }
456 
457     public boolean equals( Object o )
458     {
459         return ( o instanceof ComparableVersion ) && canonical.equals( ( (ComparableVersion) o ).canonical );
460     }
461 
462     public int hashCode()
463     {
464         return canonical.hashCode();
465     }
466 }