/******************************************************************************* * Copyright (c) 2010, 2011 Sonatype, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Sonatype, Inc. - initial API and implementation *******************************************************************************/ package org.eclipse.aether.util.version; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.TreeMap; import org.eclipse.aether.version.Version; /** * A generic version, that is a version that accepts any input string and tries to apply common sense sorting. See * {@link GenericVersionScheme} for details. */ final class GenericVersion implements Version { private final String version; private final Item[] items; private final int hash; /** * Creates a generic version from the specified string. * * @param version The version string, must not be {@code null}. */ public GenericVersion( String version ) { this.version = version; items = parse( version ); hash = Arrays.hashCode( items ); } private static Item[] parse( String version ) { List<Item> items = new ArrayList<Item>(); for ( Tokenizer tokenizer = new Tokenizer( version ); tokenizer.next(); ) { Item item = tokenizer.toItem(); items.add( item ); } trimPadding( items ); return items.toArray( new Item[items.size()] ); } private static void trimPadding( List<Item> items ) { Boolean number = null; int end = items.size() - 1; for ( int i = end; i > 0; i-- ) { Item item = items.get( i ); if ( !Boolean.valueOf( item.isNumber() ).equals( number ) ) { end = i; number = item.isNumber(); } if ( end == i && ( i == items.size() - 1 || items.get( i - 1 ).isNumber() == item.isNumber() ) && item.compareTo( null ) == 0 ) { items.remove( i ); end--; } } } public int compareTo( Version obj ) { final Item[] these = items; final Item[] those = ( (GenericVersion) obj ).items; boolean number = true; for ( int index = 0;; index++ ) { if ( index >= these.length && index >= those.length ) { return 0; } else if ( index >= these.length ) { return -comparePadding( those, index, null ); } else if ( index >= those.length ) { return comparePadding( these, index, null ); } Item thisItem = these[index]; Item thatItem = those[index]; if ( thisItem.isNumber() != thatItem.isNumber() ) { if ( number == thisItem.isNumber() ) { return comparePadding( these, index, number ); } else { return -comparePadding( those, index, number ); } } else { int rel = thisItem.compareTo( thatItem ); if ( rel != 0 ) { return rel; } number = thisItem.isNumber(); } } } private static int comparePadding( Item[] items, int index, Boolean number ) { int rel = 0; for ( int i = index; i < items.length; i++ ) { Item item = items[i]; if ( number != null && number != item.isNumber() ) { break; } rel = item.compareTo( null ); if ( rel != 0 ) { break; } } return rel; } @Override public boolean equals( Object obj ) { return ( obj instanceof GenericVersion ) && compareTo( (GenericVersion) obj ) == 0; } @Override public int hashCode() { return hash; } @Override public String toString() { return version; } static final class Tokenizer { private static final Integer QUALIFIER_ALPHA = -5; private static final Integer QUALIFIER_BETA = -4; private static final Integer QUALIFIER_MILESTONE = -3; private static final Map<String, Integer> QUALIFIERS; static { QUALIFIERS = new TreeMap<String, Integer>( String.CASE_INSENSITIVE_ORDER ); QUALIFIERS.put( "alpha", QUALIFIER_ALPHA ); QUALIFIERS.put( "beta", QUALIFIER_BETA ); QUALIFIERS.put( "milestone", QUALIFIER_MILESTONE ); QUALIFIERS.put( "cr", -2 ); QUALIFIERS.put( "rc", -2 ); QUALIFIERS.put( "snapshot", -1 ); QUALIFIERS.put( "ga", 0 ); QUALIFIERS.put( "final", 0 ); QUALIFIERS.put( "", 0 ); QUALIFIERS.put( "sp", 1 ); } private final String version; private int index; private String token; private boolean number; private boolean terminatedByNumber; public Tokenizer( String version ) { this.version = ( version.length() > 0 ) ? version : "0"; } public boolean next() { final int n = version.length(); if ( index >= n ) { return false; } int state = -2; int start = index; int end = n; terminatedByNumber = false; for ( ; index < n; index++ ) { char c = version.charAt( index ); if ( c == '.' || c == '-' || c == '_' ) { end = index; index++; break; } else { int digit = Character.digit( c, 10 ); if ( digit >= 0 ) { if ( state == -1 ) { end = index; terminatedByNumber = true; break; } if ( state == 0 ) { // normalize numbers and strip leading zeros (prereq for Integer/BigInteger handling) start++; } state = ( state > 0 || digit > 0 ) ? 1 : 0; } else { if ( state >= 0 ) { end = index; break; } state = -1; } } } if ( end - start > 0 ) { token = version.substring( start, end ); number = state >= 0; } else { token = "0"; number = true; } return true; } @Override public String toString() { return String.valueOf( token ); } public Item toItem() { if ( number ) { try { if ( token.length() < 10 ) { return new Item( Item.KIND_INT, Integer.parseInt( token ) ); } else { return new Item( Item.KIND_BIGINT, new BigInteger( token ) ); } } catch ( NumberFormatException e ) { throw new IllegalStateException( e ); } } else { if ( index >= version.length() ) { if ( "min".equalsIgnoreCase( token ) ) { return Item.MIN; } else if ( "max".equalsIgnoreCase( token ) ) { return Item.MAX; } } if ( terminatedByNumber && token.length() == 1 ) { switch ( token.charAt( 0 ) ) { case 'a': case 'A': return new Item( Item.KIND_QUALIFIER, QUALIFIER_ALPHA ); case 'b': case 'B': return new Item( Item.KIND_QUALIFIER, QUALIFIER_BETA ); case 'm': case 'M': return new Item( Item.KIND_QUALIFIER, QUALIFIER_MILESTONE ); } } Integer qualifier = QUALIFIERS.get( token ); if ( qualifier != null ) { return new Item( Item.KIND_QUALIFIER, qualifier ); } else { return new Item( Item.KIND_STRING, token.toLowerCase( Locale.ENGLISH ) ); } } } } static final class Item { static final int KIND_MAX = 8; static final int KIND_BIGINT = 5; static final int KIND_INT = 4; static final int KIND_STRING = 3; static final int KIND_QUALIFIER = 2; static final int KIND_MIN = 0; static final Item MAX = new Item( KIND_MAX, "max" ); static final Item MIN = new Item( KIND_MIN, "min" ); private final int kind; private final Object value; public Item( int kind, Object value ) { this.kind = kind; this.value = value; } public boolean isNumber() { return ( kind & KIND_QUALIFIER ) == 0; // i.e. kind != string/qualifier } public int compareTo( Item that ) { int rel; if ( that == null ) { // null in this context denotes the pad item (0 or "ga") switch ( kind ) { case KIND_MIN: rel = -1; break; case KIND_MAX: case KIND_BIGINT: case KIND_STRING: rel = 1; break; case KIND_INT: case KIND_QUALIFIER: rel = (Integer) value; break; default: throw new IllegalStateException( "unknown version item kind " + kind ); } } else { rel = kind - that.kind; if ( rel == 0 ) { switch ( kind ) { case KIND_MAX: case KIND_MIN: break; case KIND_BIGINT: rel = ( (BigInteger) value ).compareTo( (BigInteger) that.value ); break; case KIND_INT: case KIND_QUALIFIER: rel = ( (Integer) value ).compareTo( (Integer) that.value ); break; case KIND_STRING: rel = ( (String) value ).compareToIgnoreCase( (String) that.value ); break; default: throw new IllegalStateException( "unknown version item kind " + kind ); } } } return rel; } @Override public boolean equals( Object obj ) { return ( obj instanceof Item ) && compareTo( (Item) obj ) == 0; } @Override public int hashCode() { return value.hashCode() + kind * 31; } @Override public String toString() { return String.valueOf( value ); } } }