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 => [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 }