001package org.apache.maven.artifact.versioning; 002 003/* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022import java.math.BigInteger; 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.Iterator; 026import java.util.List; 027import java.util.Locale; 028import java.util.Properties; 029import java.util.Stack; 030 031/** 032 * Generic implementation of version comparison. 033 * 034 * <p>Features: 035 * <ul> 036 * <li>mixing of '<code>-</code>' (dash) and '<code>.</code>' (dot) separators,</li> 037 * <li>transition between characters and digits also constitutes a separator: 038 * <code>1.0alpha1 => [1, 0, alpha, 1]</code></li> 039 * <li>unlimited number of version components,</li> 040 * <li>version components in the text can be digits or strings,</li> 041 * <li>strings are checked for well-known qualifiers and the qualifier ordering is used for version ordering. 042 * Well-known qualifiers (case insensitive) are:<ul> 043 * <li><code>alpha</code> or <code>a</code></li> 044 * <li><code>beta</code> or <code>b</code></li> 045 * <li><code>milestone</code> or <code>m</code></li> 046 * <li><code>rc</code> or <code>cr</code></li> 047 * <li><code>snapshot</code></li> 048 * <li><code>(the empty string)</code> or <code>ga</code> or <code>final</code></li> 049 * <li><code>sp</code></li> 050 * </ul> 051 * Unknown qualifiers are considered after known qualifiers, with lexical order (always case insensitive), 052 * </li> 053 * <li>a dash usually precedes a qualifier, and is always less important than something preceded with a dot.</li> 054 * </ul></p> 055 * 056 * @see <a href="https://cwiki.apache.org/confluence/display/MAVENOLD/Versioning">"Versioning" on Maven Wiki</a> 057 * @author <a href="mailto:kenney@apache.org">Kenney Westerhof</a> 058 * @author <a href="mailto:hboutemy@apache.org">Hervé Boutemy</a> 059 */ 060public class ComparableVersion 061 implements Comparable<ComparableVersion> 062{ 063 private String value; 064 065 private String canonical; 066 067 private ListItem items; 068 069 private interface Item 070 { 071 int INTEGER_ITEM = 0; 072 int STRING_ITEM = 1; 073 int LIST_ITEM = 2; 074 075 int compareTo( Item item ); 076 077 int getType(); 078 079 boolean isNull(); 080 } 081 082 /** 083 * Represents a numeric item in the version item list. 084 */ 085 private static class IntegerItem 086 implements Item 087 { 088 private static final BigInteger BIG_INTEGER_ZERO = new BigInteger( "0" ); 089 090 private final BigInteger value; 091 092 public static final IntegerItem ZERO = new IntegerItem(); 093 094 private IntegerItem() 095 { 096 this.value = BIG_INTEGER_ZERO; 097 } 098 099 public IntegerItem( String str ) 100 { 101 this.value = new BigInteger( str ); 102 } 103 104 public int getType() 105 { 106 return INTEGER_ITEM; 107 } 108 109 public boolean isNull() 110 { 111 return BIG_INTEGER_ZERO.equals( value ); 112 } 113 114 public int compareTo( Item item ) 115 { 116 if ( item == null ) 117 { 118 return BIG_INTEGER_ZERO.equals( value ) ? 0 : 1; // 1.0 == 1, 1.1 > 1 119 } 120 121 switch ( item.getType() ) 122 { 123 case INTEGER_ITEM: 124 return value.compareTo( ( (IntegerItem) item ).value ); 125 126 case STRING_ITEM: 127 return 1; // 1.1 > 1-sp 128 129 case LIST_ITEM: 130 return 1; // 1.1 > 1-1 131 132 default: 133 throw new RuntimeException( "invalid item: " + item.getClass() ); 134 } 135 } 136 137 public String toString() 138 { 139 return value.toString(); 140 } 141 } 142 143 /** 144 * Represents a string in the version item list, usually a qualifier. 145 */ 146 private static class StringItem 147 implements Item 148 { 149 private static final String[] QUALIFIERS = { "alpha", "beta", "milestone", "rc", "snapshot", "", "sp" }; 150 151 @SuppressWarnings( "checkstyle:constantname" ) 152 private static final List<String> _QUALIFIERS = Arrays.asList( QUALIFIERS ); 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 public 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 ( Iterator<Item> iter = iterator(); iter.hasNext(); ) 335 { 336 Item item = iter.next(); 337 if ( buffer.length() > 0 ) 338 { 339 buffer.append( ( item instanceof ListItem ) ? '-' : '.' ); 340 } 341 buffer.append( item ); 342 } 343 return buffer.toString(); 344 } 345 } 346 347 public ComparableVersion( String version ) 348 { 349 parseVersion( version ); 350 } 351 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<Item>(); 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 /** 473 * Main to test version parsing and comparison. 474 * 475 * @param args the version strings to parse and compare 476 */ 477 public static void main( String... args ) 478 { 479 System.out.println( "Display parameters as parsed by Maven (in canonical form) and comparison result:" ); 480 if ( args.length == 0 ) 481 { 482 return; 483 } 484 485 ComparableVersion prev = null; 486 int i = 1; 487 for ( String version : args ) 488 { 489 ComparableVersion c = new ComparableVersion( version ); 490 491 if ( prev != null ) 492 { 493 int compare = prev.compareTo( c ); 494 System.out.println( " " + prev.toString() + ' ' 495 + ( ( compare == 0 ) ? "==" : ( ( compare < 0 ) ? "<" : ">" ) ) + ' ' + version ); 496 } 497 498 System.out.println( String.valueOf( i++ ) + ". " + version + " == " + c.getCanonical() ); 499 500 prev = c; 501 } 502 } 503}