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.Locale;
28 import java.util.Properties;
29 import java.util.Stack;
30
31 /**
32 * <p>
33 * Generic implementation of version comparison.
34 * </p>
35 *
36 * Features:
37 * <ul>
38 * <li>mixing of '<code>-</code>' (hyphen) and '<code>.</code>' (dot) separators,</li>
39 * <li>transition between characters and digits also constitutes a separator:
40 * <code>1.0alpha1 => [1, 0, alpha, 1]</code></li>
41 * <li>unlimited number of version components,</li>
42 * <li>version components in the text can be digits or strings,</li>
43 * <li>strings are checked for well-known qualifiers and the qualifier ordering is used for version ordering.
44 * Well-known qualifiers (case insensitive) are:<ul>
45 * <li><code>alpha</code> or <code>a</code></li>
46 * <li><code>beta</code> or <code>b</code></li>
47 * <li><code>milestone</code> or <code>m</code></li>
48 * <li><code>rc</code> or <code>cr</code></li>
49 * <li><code>snapshot</code></li>
50 * <li><code>(the empty string)</code> or <code>ga</code> or <code>final</code></li>
51 * <li><code>sp</code></li>
52 * </ul>
53 * Unknown qualifiers are considered after known qualifiers, with lexical order (always case insensitive),
54 * </li>
55 * <li>a hyphen usually precedes a qualifier, and is always less important than something preceded with a dot.</li>
56 * </ul>
57 *
58 * @see <a href="https://cwiki.apache.org/confluence/display/MAVENOLD/Versioning">"Versioning" on Maven Wiki</a>
59 * @author <a href="mailto:kenney@apache.org">Kenney Westerhof</a>
60 * @author <a href="mailto:hboutemy@apache.org">Hervé Boutemy</a>
61 */
62 public class ComparableVersion
63 implements Comparable<ComparableVersion>
64 {
65 private String value;
66
67 private String canonical;
68
69 private ListItem items;
70
71 private interface Item
72 {
73 int INTEGER_ITEM = 0;
74 int STRING_ITEM = 1;
75 int LIST_ITEM = 2;
76
77 int compareTo( Item item );
78
79 int getType();
80
81 boolean isNull();
82 }
83
84 /**
85 * Represents a numeric item in the version item list.
86 */
87 private static class IntegerItem
88 implements Item
89 {
90 private static final BigInteger BIG_INTEGER_ZERO = new BigInteger( "0" );
91
92 private final BigInteger value;
93
94 public static final IntegerItem ZERO = new IntegerItem();
95
96 private IntegerItem()
97 {
98 this.value = BIG_INTEGER_ZERO;
99 }
100
101 IntegerItem( String str )
102 {
103 this.value = new BigInteger( str );
104 }
105
106 public int getType()
107 {
108 return INTEGER_ITEM;
109 }
110
111 public boolean isNull()
112 {
113 return BIG_INTEGER_ZERO.equals( value );
114 }
115
116 public int compareTo( Item item )
117 {
118 if ( item == null )
119 {
120 return BIG_INTEGER_ZERO.equals( value ) ? 0 : 1; // 1.0 == 1, 1.1 > 1
121 }
122
123 switch ( item.getType() )
124 {
125 case INTEGER_ITEM:
126 return value.compareTo( ( (IntegerItem) item ).value );
127
128 case STRING_ITEM:
129 return 1; // 1.1 > 1-sp
130
131 case LIST_ITEM:
132 return 1; // 1.1 > 1-1
133
134 default:
135 throw new RuntimeException( "invalid item: " + item.getClass() );
136 }
137 }
138
139 public String toString()
140 {
141 return value.toString();
142 }
143 }
144
145 /**
146 * Represents a string in the version item list, usually a qualifier.
147 */
148 private static class StringItem
149 implements Item
150 {
151 private static final List<String> QUALIFIERS =
152 Arrays.asList( "alpha", "beta", "milestone", "rc", "snapshot", "", "sp" );
153
154 private static final Properties ALIASES = new Properties();
155 static
156 {
157 ALIASES.put( "ga", "" );
158 ALIASES.put( "final", "" );
159 ALIASES.put( "cr", "rc" );
160 }
161
162 /**
163 * A comparable value for the empty-string qualifier. This one is used to determine if a given qualifier makes
164 * the version older than one without a qualifier, or more recent.
165 */
166 private static final String RELEASE_VERSION_INDEX = String.valueOf( QUALIFIERS.indexOf( "" ) );
167
168 private String value;
169
170 StringItem( String value, boolean followedByDigit )
171 {
172 if ( followedByDigit && value.length() == 1 )
173 {
174 // a1 = alpha-1, b1 = beta-1, m1 = milestone-1
175 switch ( value.charAt( 0 ) )
176 {
177 case 'a':
178 value = "alpha";
179 break;
180 case 'b':
181 value = "beta";
182 break;
183 case 'm':
184 value = "milestone";
185 break;
186 default:
187 }
188 }
189 this.value = ALIASES.getProperty( value , value );
190 }
191
192 public int getType()
193 {
194 return STRING_ITEM;
195 }
196
197 public boolean isNull()
198 {
199 return ( comparableQualifier( value ).compareTo( RELEASE_VERSION_INDEX ) == 0 );
200 }
201
202 /**
203 * Returns a comparable value for a qualifier.
204 *
205 * This method takes into account the ordering of known qualifiers then unknown qualifiers with lexical
206 * ordering.
207 *
208 * just returning an Integer with the index here is faster, but requires a lot of if/then/else to check for -1
209 * or QUALIFIERS.size and then resort to lexical ordering. Most comparisons are decided by the first character,
210 * so this is still fast. If more characters are needed then it requires a lexical sort anyway.
211 *
212 * @param qualifier
213 * @return an equivalent value that can be used with lexical comparison
214 */
215 public static String comparableQualifier( String qualifier )
216 {
217 int i = QUALIFIERS.indexOf( qualifier );
218
219 return i == -1 ? ( QUALIFIERS.size() + "-" + qualifier ) : String.valueOf( i );
220 }
221
222 public int compareTo( Item item )
223 {
224 if ( item == null )
225 {
226 // 1-rc < 1, 1-ga > 1
227 return comparableQualifier( value ).compareTo( RELEASE_VERSION_INDEX );
228 }
229 switch ( item.getType() )
230 {
231 case INTEGER_ITEM:
232 return -1; // 1.any < 1.1 ?
233
234 case STRING_ITEM:
235 return comparableQualifier( value ).compareTo( comparableQualifier( ( (StringItem) item ).value ) );
236
237 case LIST_ITEM:
238 return -1; // 1.any < 1-1
239
240 default:
241 throw new RuntimeException( "invalid item: " + item.getClass() );
242 }
243 }
244
245 public String toString()
246 {
247 return value;
248 }
249 }
250
251 /**
252 * Represents a version list item. This class is used both for the global item list and for sub-lists (which start
253 * with '-(number)' in the version specification).
254 */
255 private static class ListItem
256 extends ArrayList<Item>
257 implements Item
258 {
259 public int getType()
260 {
261 return LIST_ITEM;
262 }
263
264 public boolean isNull()
265 {
266 return ( size() == 0 );
267 }
268
269 void normalize()
270 {
271 for ( int i = size() - 1; i >= 0; i-- )
272 {
273 Item lastItem = get( i );
274
275 if ( lastItem.isNull() )
276 {
277 // remove null trailing items: 0, "", empty list
278 remove( i );
279 }
280 else if ( !( lastItem instanceof ListItem ) )
281 {
282 break;
283 }
284 }
285 }
286
287 public int compareTo( Item item )
288 {
289 if ( item == null )
290 {
291 if ( size() == 0 )
292 {
293 return 0; // 1-0 = 1- (normalize) = 1
294 }
295 Item first = get( 0 );
296 return first.compareTo( null );
297 }
298 switch ( item.getType() )
299 {
300 case INTEGER_ITEM:
301 return -1; // 1-1 < 1.0.x
302
303 case STRING_ITEM:
304 return 1; // 1-1 > 1-sp
305
306 case LIST_ITEM:
307 Iterator<Item> left = iterator();
308 Iterator<Item> right = ( (ListItem) item ).iterator();
309
310 while ( left.hasNext() || right.hasNext() )
311 {
312 Item l = left.hasNext() ? left.next() : null;
313 Item r = right.hasNext() ? right.next() : null;
314
315 // if this is shorter, then invert the compare and mul with -1
316 int result = l == null ? ( r == null ? 0 : -1 * r.compareTo( l ) ) : l.compareTo( r );
317
318 if ( result != 0 )
319 {
320 return result;
321 }
322 }
323
324 return 0;
325
326 default:
327 throw new RuntimeException( "invalid item: " + item.getClass() );
328 }
329 }
330
331 public String toString()
332 {
333 StringBuilder buffer = new StringBuilder();
334 for ( Item item : this )
335 {
336 if ( buffer.length() > 0 )
337 {
338 buffer.append( ( item instanceof ListItem ) ? '-' : '.' );
339 }
340 buffer.append( item );
341 }
342 return buffer.toString();
343 }
344 }
345
346 public ComparableVersion( String version )
347 {
348 parseVersion( version );
349 }
350
351 @SuppressWarnings( "checkstyle:innerassignment" )
352 public final void parseVersion( String version )
353 {
354 this.value = version;
355
356 items = new ListItem();
357
358 version = version.toLowerCase( Locale.ENGLISH );
359
360 ListItem list = items;
361
362 Stack<Item> stack = new Stack<>();
363 stack.push( list );
364
365 boolean isDigit = false;
366
367 int startIndex = 0;
368
369 for ( int i = 0; i < version.length(); i++ )
370 {
371 char c = version.charAt( i );
372
373 if ( c == '.' )
374 {
375 if ( i == startIndex )
376 {
377 list.add( IntegerItem.ZERO );
378 }
379 else
380 {
381 list.add( parseItem( isDigit, version.substring( startIndex, i ) ) );
382 }
383 startIndex = i + 1;
384 }
385 else if ( c == '-' )
386 {
387 if ( i == startIndex )
388 {
389 list.add( IntegerItem.ZERO );
390 }
391 else
392 {
393 list.add( parseItem( isDigit, version.substring( startIndex, i ) ) );
394 }
395 startIndex = i + 1;
396
397 list.add( list = new ListItem() );
398 stack.push( list );
399 }
400 else if ( Character.isDigit( c ) )
401 {
402 if ( !isDigit && i > startIndex )
403 {
404 list.add( new StringItem( version.substring( startIndex, i ), true ) );
405 startIndex = i;
406
407 list.add( list = new ListItem() );
408 stack.push( list );
409 }
410
411 isDigit = true;
412 }
413 else
414 {
415 if ( isDigit && i > startIndex )
416 {
417 list.add( parseItem( true, version.substring( startIndex, i ) ) );
418 startIndex = i;
419
420 list.add( list = new ListItem() );
421 stack.push( list );
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 String getCanonical()
458 {
459 return canonical;
460 }
461
462 public boolean equals( Object o )
463 {
464 return ( o instanceof ComparableVersion ) && canonical.equals( ( (ComparableVersion) o ).canonical );
465 }
466
467 public int hashCode()
468 {
469 return canonical.hashCode();
470 }
471
472 // CHECKSTYLE_OFF: LineLength
473 /**
474 * Main to test version parsing and comparison.
475 * <p>
476 * To check how "1.2.7" compares to "1.2-SNAPSHOT", for example, you can issue
477 * <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>
478 * command to command line. Result of given command will be something like this:
479 * <pre>
480 * Display parameters as parsed by Maven (in canonical form) and comparison result:
481 * 1. 1.2.7 == 1.2.7
482 * 1.2.7 > 1.2-SNAPSHOT
483 * 2. 1.2-SNAPSHOT == 1.2-snapshot
484 * </pre>
485 *
486 * @param args the version strings to parse and compare. You can pass arbitrary number of version strings and always
487 * two adjacent will be compared
488 */
489 // CHECKSTYLE_ON: LineLength
490 public static void main( String... args )
491 {
492 System.out.println( "Display parameters as parsed by Maven (in canonical form) and comparison result:" );
493 if ( args.length == 0 )
494 {
495 return;
496 }
497
498 ComparableVersion prev = null;
499 int i = 1;
500 for ( String version : args )
501 {
502 ComparableVersion c = new ComparableVersion( version );
503
504 if ( prev != null )
505 {
506 int compare = prev.compareTo( c );
507 System.out.println( " " + prev.toString() + ' '
508 + ( ( compare == 0 ) ? "==" : ( ( compare < 0 ) ? "<" : ">" ) ) + ' ' + version );
509 }
510
511 System.out.println( String.valueOf( i++ ) + ". " + version + " == " + c.getCanonical() );
512
513 prev = c;
514 }
515 }
516 }