blob: d1d1fbf9ea0f993f22d371177d331afd96b5be51 [file] [log] [blame]
package org.eclipse.aether.util.version;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
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}.
*/
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( "release", 0 );
QUALIFIERS.put( "", 0 );
QUALIFIERS.put( "sp", 1 );
}
private final String version;
private int index;
private String token;
private boolean number;
private boolean terminatedByNumber;
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 );
default:
}
}
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;
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 );
}
}
}