org.jboss.modules.Version Maven / Gradle / Ivy
/*
* JBoss, Home of Professional Open Source.
* Copyright 2016 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* Licensed 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.
*/
package org.jboss.modules;
import java.io.Serializable;
import java.math.BigInteger;
import java.text.Normalizer;
import java.util.NoSuchElementException;
/**
* A version for a module. Versions are series of letters and digits, optionally divided by separator characters, which
* include {@code .}, {@code -}, {@code +}, and {@code _}. The transition between letter and digit (or vice-versa) is
* also considered to be an invisible separator.
*
* Versions may be compared, sorted, used as hash keys, and iterated.
*
* @author David M. Lloyd
*/
public final class Version implements Comparable, Serializable {
private final String version;
private static final int TOK_INITIAL = 0;
private static final int TOK_PART_NUMBER = 1;
private static final int TOK_PART_ALPHA = 2;
private static final int TOK_SEP = 3;
private static final int TOK_SEP_EMPTY = 4;
boolean hasNext(long cookie) {
return cookieToEndIndex(cookie) < version.length();
}
/**
* Go to the next token.
*
* @param cookie the cookie ({@code 0L} indicates start of search)
* @return the next cookie value in the token sequence
* @throws IllegalArgumentException if the next token is not a valid token or the string is too long
*/
long next(long cookie) throws IllegalArgumentException {
int start, end, token;
// get old end value
end = cookieToEndIndex(cookie);
final String version = this.version;
final int length = version.length();
// hasNext() should have been called first on this cookie value
assert end < length;
start = end;
int cp = version.codePointAt(start);
// examine the previous token
token = cookieToToken(cookie);
if (token == TOK_PART_NUMBER && isValidAlphaPart(cp) || token == TOK_PART_ALPHA && isValidNumberPart(cp)) {
token = TOK_SEP_EMPTY;
end = start;
} else if ((token == TOK_INITIAL || token == TOK_SEP || token == TOK_SEP_EMPTY) && isValidAlphaPart(cp)) {
token = TOK_PART_ALPHA;
end = length;
// find end
for (int i = start; i < length; i = version.offsetByCodePoints(i, 1)) {
cp = version.codePointAt(i);
if (! isValidAlphaPart(cp)) {
end = i;
break;
}
}
} else if ((token == TOK_INITIAL || token == TOK_SEP || token == TOK_SEP_EMPTY) && isValidNumberPart(cp)) {
token = TOK_PART_NUMBER;
end = length;
// find end
for (int i = start; i < length; i = version.offsetByCodePoints(i, 1)) {
cp = version.codePointAt(i);
if (! isValidNumberPart(cp)) {
end = i;
break;
}
}
} else if ((token == TOK_PART_NUMBER || token == TOK_PART_ALPHA) && isValidSeparator(cp)) {
token = TOK_SEP;
end = version.offsetByCodePoints(start, 1);
} else {
throw new IllegalArgumentException("Invalid version code point \"" + new String(Character.toChars(cp)) + "\" at offset " + start + " of \"" + version + "\"");
}
if (end > (1 << 28) - 1) {
throw new IllegalArgumentException("Version string is too long");
}
assert end >= start;
return ((long) start) | ((long) end << 28) | ((long) token << 56);
}
/**
* Get the token start index from the cookie. The start index is stored in the lowest 28 bits (bits 0-27) of the cookie.
*
* @param cookie the cookie
* @return the start character index in the version string
*/
static int cookieToStartIndex(long cookie) {
return (int) (cookie & 0xfff_ffff);
}
/**
* Get the token end index from the cookie. The end index is stored in the second 28 bits (bits 28-55) of the cookie.
*
* @param cookie the cookie
* @return the end character index in the version string
*/
static int cookieToEndIndex(long cookie) {
return (int) ((cookie >> 28) & 0xfff_ffff);
}
/**
* Get the token type from the cookie. The token type is stored in bits 56 and up in the cookie.
* The value will be one of:
*
* - {@link #TOK_PART_NUMBER}
* - {@link #TOK_PART_ALPHA}
* - {@link #TOK_SEP}
* - {@link #TOK_SEP_EMPTY}
*
*
* @param cookie the cookie
* @return the token value
*/
static int cookieToToken(long cookie) {
return (int) (cookie >> 56);
}
static int sepMagnitude(int codePoint) {
switch (codePoint) {
case '.': return 1;
case '-': return 2;
case '+': return 3;
case '_': return 4;
default: throw new IllegalStateException();
}
}
static int compareSep(int sep1, int sep2) {
return Integer.signum(sepMagnitude(sep1) - sepMagnitude(sep2));
}
static boolean isValidAlphaPart(int codePoint) {
return Character.isLetter(codePoint);
}
static boolean isValidNumberPart(int codePoint) {
// Use this instead of isDigit(cp) in case a valid digit is not a valid decimal digit
return Character.digit(codePoint, 10) != -1;
}
static boolean isValidSeparator(int codePoint) {
return codePoint == '.' || codePoint == '-' || codePoint == '+' || codePoint == '_';
}
int comparePart(long cookie, Version other, long otherCookie) {
final int token = cookieToToken(cookie);
final int otherToken = cookieToToken(otherCookie);
final int start = cookieToStartIndex(cookie);
final int otherStart = cookieToStartIndex(cookie);
final int end = cookieToEndIndex(cookie);
final int otherEnd = cookieToEndIndex(otherCookie);
switch (token) {
case TOK_PART_NUMBER: {
if (otherToken == TOK_PART_ALPHA) {
return 1;
}
assert otherToken == TOK_PART_NUMBER;
// we need the length in digits, not in characters
final int digits = version.codePointCount(start, end);
final int otherDigits = other.version.codePointCount(otherStart, otherEnd);
// if one is shorter, we need to pad it out with invisible zeros for the value comparison
// otherwise, compare digits naively from left to right
int res;
// in this loop, i represents the place with a higher # being more significant
for (int i = Math.max(digits, otherDigits) - 1; i >= 0; i --) {
// if the value has no digit at location 'i', i.e. i > length, then the effective value is 0
int a = i >= digits ? 0 : Character.digit(version.codePointBefore(version.offsetByCodePoints(end, -i)), 10);
int b = i >= otherDigits ? 0 : Character.digit(other.version.codePointBefore(other.version.offsetByCodePoints(otherEnd, -i)), 10);
res = Integer.signum(a - b);
if (res != 0) {
return res;
}
}
// equal, so now the shortest wins
return Integer.signum(digits - otherDigits);
}
case TOK_PART_ALPHA: {
if (otherToken == TOK_PART_NUMBER) {
return -1;
}
assert otherToken == TOK_PART_ALPHA;
// compare string naively from left to right
for (int i = start; i < Math.min(end, otherEnd); i = version.offsetByCodePoints(i, 1)) {
int cp = version.codePointAt(i);
int ocp = other.version.codePointAt(i);
assert isValidAlphaPart(cp) && isValidAlphaPart(ocp);
int res = Integer.signum(cp - ocp);
if (res != 0) {
return res;
}
}
// identical prefix; fall back to length comparison
return Integer.signum(end - otherEnd);
}
case TOK_SEP: {
if (otherToken == TOK_SEP_EMPTY) {
return 1;
} else {
assert otherToken == TOK_SEP;
return compareSep(version.codePointAt(start), other.version.codePointAt(cookieToStartIndex(otherCookie)));
}
}
case TOK_SEP_EMPTY: {
return otherToken == TOK_SEP_EMPTY ? 0 : -1;
}
default: throw new IllegalStateException();
}
}
private Version(String v) {
if (v == null)
throw new IllegalArgumentException("Null version string");
this.version = Normalizer.normalize(v, Normalizer.Form.NFKC);
// validate
if (! hasNext(0L)) {
throw new IllegalArgumentException("Empty version string");
}
long cookie = next(0L);
while (hasNext(cookie)) {
cookie = next(cookie);
}
final int lastToken = cookieToToken(cookie);
if (lastToken == TOK_SEP || lastToken == TOK_SEP_EMPTY) {
throw new IllegalArgumentException("Version ends with a separator");
}
}
/**
* Parses the given string as a version string.
*
* @param v
* The string to parse
*
* @return The resulting {@code Version}
*
* @throws IllegalArgumentException
* If {@code v} is {@code null}, an empty string, or cannot be
* parsed as a version string
*/
public static Version parse(String v) {
return new Version(v);
}
/**
* Construct a new iterator over the parts of this version string.
*
* @return the iterator
*/
public Iterator iterator() {
return new Iterator();
}
/**
* Compares this module version to another module version. Module
* versions are compared as described in the class description.
*
* @param that
* The module version to compare
*
* @return A negative integer, zero, or a positive integer as this
* module version is less than, equal to, or greater than the
* given module version
*/
@Override
public int compareTo(Version that) {
long cookie = 0L;
long thatCookie = 0L;
int res;
while (hasNext(cookie)) {
cookie = next(cookie);
if (that.hasNext(thatCookie)) {
thatCookie = that.next(thatCookie);
res = comparePart(cookie, that, thatCookie);
if (res != 0) {
return res;
}
} else {
return 1;
}
}
return that.hasNext(thatCookie) ? - 1 : 0;
}
/**
* Tests this module version for equality with the given object.
*
* If the given object is not a {@code Version} then this method
* returns {@code false}. Two module version are equal if their
* corresponding components are equal.
*
* This method satisfies the general contract of the {@link
* java.lang.Object#equals(Object) Object.equals} method.
*
* @param ob
* the object to which this object is to be compared
*
* @return {@code true} if, and only if, the given object is a module
* reference that is equal to this module reference
*/
@Override
public boolean equals(Object ob) {
if (!(ob instanceof Version))
return false;
return compareTo((Version)ob) == 0;
}
/**
* Computes a hash code for this module version.
*
* The hash code is based upon the components of the version and
* satisfies the general contract of the {@link Object#hashCode
* Object.hashCode} method.
*
* @return The hash-code value for this module version
*/
@Override
public int hashCode() {
return version.hashCode();
}
/**
* Returns the normalized string representation of this version.
*
* @return The normalized string.
*/
@Override
public String toString() {
return version;
}
/**
* An iterator over the parts of a version.
*/
public class Iterator {
long cookie = 0L;
Iterator() {
}
/**
* Determine whether another token exists in this version.
*
* @return {@code true} if more tokens remain, {@code false} otherwise
*/
public boolean hasNext() {
return Version.this.hasNext(cookie);
}
/**
* Move to the next token.
*
* @throws NoSuchElementException if there are no more tokens to iterate
*/
public void next() {
final long cookie = this.cookie;
if (! Version.this.hasNext(cookie)) {
throw new NoSuchElementException();
}
this.cookie = Version.this.next(cookie);
}
/**
* Get the length of the current token. If there is no current token, zero is returned.
*
* @return the length of the current token
*/
public int length() {
final long cookie = this.cookie;
return cookieToEndIndex(cookie) - cookieToStartIndex(cookie);
}
/**
* Determine if the current token is some kind of separator (a character or a zero-length alphabetical-to-numeric
* or numeric-to-alphabetical transition).
*
* @return {@code true} if the token is a separator, {@code false} otherwise
*/
public boolean isSeparator() {
final int token = cookieToToken(cookie);
return token == TOK_SEP_EMPTY || token == TOK_SEP;
}
/**
* Determine if the current token is some kind of part (alphabetical or numeric).
*
* @return {@code true} if the token is a separator, {@code false} otherwise
*/
public boolean isPart() {
final int token = cookieToToken(cookie);
return token == TOK_PART_ALPHA || token == TOK_PART_NUMBER;
}
/**
* Determine if the current token is an empty (or zero-length alphabetical-to-numeric
* or numeric-to-alphabetical) separator.
*
* @return {@code true} if the token is an empty separator, {@code false} otherwise
*/
public boolean isEmptySeparator() {
return cookieToToken(cookie) == TOK_SEP_EMPTY;
}
/**
* Determine if the current token is a non-empty separator.
*
* @return {@code true} if the token is a non-empty separator, {@code false} otherwise
*/
public boolean isNonEmptySeparator() {
return cookieToToken(cookie) == TOK_SEP;
}
/**
* Get the code point of the current separator. If the iterator is not positioned on a non-empty separator
* (i.e. {@link #isNonEmptySeparator()} returns {@code false}), then an exception is thrown.
*
* @return the code point of the current separator
* @throws IllegalStateException if the current token is not a non-empty separator
*/
public int getSeparatorCodePoint() {
final long cookie = this.cookie;
if (cookieToToken(cookie) != TOK_SEP) {
throw new IllegalStateException();
}
return version.codePointAt(cookieToStartIndex(cookie));
}
/**
* Determine if the current token is an alphabetical part.
*
* @return {@code true} if the token is an alphabetical part, {@code false} otherwise
*/
public boolean isAlphaPart() {
return cookieToToken(cookie) == TOK_PART_ALPHA;
}
/**
* Determine if the current token is a numeric part.
*
* @return {@code true} if the token is a numeric part, {@code false} otherwise
*/
public boolean isNumberPart() {
return cookieToToken(cookie) == TOK_PART_NUMBER;
}
/**
* Get the current alphabetical part. If the iterator is not positioned on an alphabetical part (i.e.
* {@link #isAlphaPart()} returns {@code false}), then an exception is thrown.
*
* @return the current alphabetical part
* @throws IllegalStateException if the current token is not an alphabetical part
*/
public String getAlphaPart() {
final long cookie = this.cookie;
if (cookieToToken(cookie) != TOK_PART_ALPHA) {
throw new IllegalStateException();
}
return version.substring(cookieToStartIndex(cookie), cookieToEndIndex(cookie));
}
/**
* Get the current numeric part, as a {@code String}. If the iterator is not positioned on a numeric
* part (i.e. {@link #isNumberPart()} returns {@code false}), then an exception is thrown.
*
* @return the current numeric part as a {@code String}
* @throws IllegalStateException if the current token is not a numeric part
*/
public String getNumberPartAsString() {
final long cookie = this.cookie;
if (cookieToToken(cookie) != TOK_PART_NUMBER) {
throw new IllegalStateException();
}
return version.substring(cookieToStartIndex(cookie), cookieToEndIndex(cookie));
}
/**
* Get the current numeric part, as a {@code long}. If the iterator is not positioned on a numeric
* part (i.e. {@link #isNumberPart()} returns {@code false}), then an exception is thrown. If the value
* overflows the maximum value for a {@code long}, then only the low-order 64 bits of the version number
* value are returned.
*
* @return the current numeric part as a {@code long}
* @throws IllegalStateException if the current token is not a numeric part
*/
public long getNumberPartAsLong() {
final long cookie = this.cookie;
if (cookieToToken(cookie) != TOK_PART_NUMBER) {
throw new IllegalStateException();
}
long total = 0L;
final int start = cookieToStartIndex(cookie);
final int end = cookieToEndIndex(cookie);
for (int i = start; i < end; i = version.offsetByCodePoints(i, 1)) {
total = total * 10 + Character.digit(version.codePointAt(i), 10);
}
return total;
}
/**
* Get the current numeric part, as an {@code int}. If the iterator is not positioned on a numeric
* part (i.e. {@link #isNumberPart()} returns {@code false}), then an exception is thrown. If the value
* overflows the maximum value for an {@code int}, then only the low-order 32 bits of the version number
* value are returned.
*
* @return the current numeric part as an {@code int}
* @throws IllegalStateException if the current token is not a numeric part
*/
public int getNumberPartAsInt() {
final long cookie = this.cookie;
if (cookieToToken(cookie) != TOK_PART_NUMBER) {
throw new IllegalStateException();
}
int total = 0;
final int start = cookieToStartIndex(cookie);
final int end = cookieToEndIndex(cookie);
for (int i = start; i < end; i = version.offsetByCodePoints(i, 1)) {
total = total * 10 + Character.digit(version.codePointAt(i), 10);
}
return total;
}
/**
* Get the current numeric part, as a {@code BigInteger}. If the iterator is not positioned on a numeric
* part (i.e. {@link #isNumberPart()} returns {@code false}), then an exception is thrown.
*
* @return the current numeric part as a {@code BigInteger}
* @throws IllegalStateException if the current token is not a numeric part
*/
public BigInteger getNumberPartAsBigInteger() {
final long cookie = this.cookie;
if (cookieToToken(cookie) != TOK_PART_NUMBER) {
throw new IllegalStateException();
}
return new BigInteger(version.substring(cookieToStartIndex(cookie), cookieToEndIndex(cookie)));
}
}
Object writeReplace() {
return new Serialized(toString());
}
static final class Serialized implements Serializable {
private static final long serialVersionUID = - 8720461103628158977L;
private final String string;
Serialized(final String string) {
this.string = string;
}
Object readResolve() {
return new Version(string);
}
}
}