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 ( 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 public final void parseVersion( String version ) 352 { 353 this.value = version; 354 355 items = new ListItem(); 356 357 version = version.toLowerCase( Locale.ENGLISH ); 358 359 ListItem list = items; 360 361 Stack<Item> stack = new Stack<>(); 362 stack.push( list ); 363 364 boolean isDigit = false; 365 366 int startIndex = 0; 367 368 for ( int i = 0; i < version.length(); i++ ) 369 { 370 char c = version.charAt( i ); 371 372 if ( c == '.' ) 373 { 374 if ( i == startIndex ) 375 { 376 list.add( IntegerItem.ZERO ); 377 } 378 else 379 { 380 list.add( parseItem( isDigit, version.substring( startIndex, i ) ) ); 381 } 382 startIndex = i + 1; 383 } 384 else if ( c == '-' ) 385 { 386 if ( i == startIndex ) 387 { 388 list.add( IntegerItem.ZERO ); 389 } 390 else 391 { 392 list.add( parseItem( isDigit, version.substring( startIndex, i ) ) ); 393 } 394 startIndex = i + 1; 395 396 list.add( list = new ListItem() ); 397 stack.push( list ); 398 } 399 else if ( Character.isDigit( c ) ) 400 { 401 if ( !isDigit && i > startIndex ) 402 { 403 list.add( new StringItem( version.substring( startIndex, i ), true ) ); 404 startIndex = i; 405 406 list.add( list = new ListItem() ); 407 stack.push( list ); 408 } 409 410 isDigit = true; 411 } 412 else 413 { 414 if ( isDigit && i > startIndex ) 415 { 416 list.add( parseItem( true, version.substring( startIndex, i ) ) ); 417 startIndex = i; 418 419 list.add( list = new ListItem() ); 420 stack.push( list ); 421 } 422 423 isDigit = false; 424 } 425 } 426 427 if ( version.length() > startIndex ) 428 { 429 list.add( parseItem( isDigit, version.substring( startIndex ) ) ); 430 } 431 432 while ( !stack.isEmpty() ) 433 { 434 list = (ListItem) stack.pop(); 435 list.normalize(); 436 } 437 438 canonical = items.toString(); 439 } 440 441 private static Item parseItem( boolean isDigit, String buf ) 442 { 443 return isDigit ? new IntegerItem( buf ) : new StringItem( buf, false ); 444 } 445 446 public int compareTo( ComparableVersion o ) 447 { 448 return items.compareTo( o.items ); 449 } 450 451 public String toString() 452 { 453 return value; 454 } 455 456 public String getCanonical() 457 { 458 return canonical; 459 } 460 461 public boolean equals( Object o ) 462 { 463 return ( o instanceof ComparableVersion ) && canonical.equals( ( (ComparableVersion) o ).canonical ); 464 } 465 466 public int hashCode() 467 { 468 return canonical.hashCode(); 469 } 470 471 /** 472 * Main to test version parsing and comparison. 473 * 474 * @param args the version strings to parse and compare 475 */ 476 public static void main( String... args ) 477 { 478 System.out.println( "Display parameters as parsed by Maven (in canonical form) and comparison result:" ); 479 if ( args.length == 0 ) 480 { 481 return; 482 } 483 484 ComparableVersion prev = null; 485 int i = 1; 486 for ( String version : args ) 487 { 488 ComparableVersion c = new ComparableVersion( version ); 489 490 if ( prev != null ) 491 { 492 int compare = prev.compareTo( c ); 493 System.out.println( " " + prev.toString() + ' ' 494 + ( ( compare == 0 ) ? "==" : ( ( compare < 0 ) ? "<" : ">" ) ) + ' ' + version ); 495 } 496 497 System.out.println( String.valueOf( i++ ) + ". " + version + " == " + c.getCanonical() ); 498 499 prev = c; 500 } 501 } 502}