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 GenericVersionScheme for details.
/** * 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.
Params:
  • version – The version string, must not be null.
/** * 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<>(); 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.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 ); } } }