/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2006-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.util;
import java.io.Serializable;
import java.util.regex.Pattern;
/**
* Holds a version number. Versions are often of the form <code>{@linkplain #getMajor
* major}.{@linkplain #getMinor minor}.{@linkplain #getRevision revision}</code>, but
* are not required to. For example an EPSG database version is {@code "6.11.2"}. The
* separator character is the dot.
* <p>
* This class provides convenience methods for fetching the major, minor and reversion
* numbers, and for performing comparaisons.
*
* @since 2.4
*
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux
*
* @see org.geotools.factory.GeoTools#getVersion
*/
public class Version implements CharSequence, Comparable<Version>, Serializable {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = -6793384507333713770L;
/**
* The pattern to use for splitting version numbers.
*/
private static final Pattern PATTERN = Pattern.compile("(\\.|\\-)");
/**
* The version in string form, with leading and trailing spaces removed.
*/
private final String version;
/**
* The components of the version string. Will be created when first needed.
*/
private transient String[] components;
/**
* The parsed components of the version string. Will be created when first needed.
*/
private transient Comparable<?>[] parsed;
/**
* The hash code value. Will be computed when first needed.
*/
private transient int hashCode;
/**
* Creates a new version object from the supplied string.
*
* @param version The version as a string.
*/
public Version(final String version) {
this.version = version.trim();
}
/**
* Returns the major version number. This method returns an {@link Integer} if possible,
* or a {@link String} otherwise.
*
* @return The major version number.
*/
public Comparable<?> getMajor() {
return getComponent(0);
}
/**
* Returns the minor version number. This method returns an {@link Integer} if possible,
* or a {@link String} otherwise. If there is no minor version number, then this method
* returns {@code null}.
*
* @return The minor version number, or {@code null} if none.
*/
public Comparable<?> getMinor() {
return getComponent(1);
}
/**
* Returns the revision number. This method returns an {@link Integer} if possible,
* or a {@link String} otherwise. If there is no revision number, then this method
* returns {@code null}.
*
* @return The revision number, or {@code null} if none.
*/
public Comparable<?> getRevision() {
return getComponent(2);
}
/**
* Returns the specified components of this version string. For a version of the
* {@code major.minor.revision} form, index 0 stands for the major version number,
* 1 stands for the minor version number and 2 stands for the revision number.
* <p>
* The return value is an {@link Integer} if the component is parsable as an integer,
* or a {@link String} otherwise. If there is no component at the specified index,
* then this method returns {@code null}.
*
* @param index The index of the component to fetch.
* @return The value at the specified index, or {@code null} if none.
* @throws IndexOutOfBoundsException if {@code index} is negative.
*/
public synchronized Comparable<?> getComponent(final int index) {
if (parsed == null) {
if (components == null) {
components = PATTERN.split(version);
}
parsed = new Comparable[components.length];
}
if (index >= parsed.length) {
return null;
}
Comparable<?> candidate = parsed[index];
if (candidate == null) {
final String value = components[index].trim();
try {
candidate = Integer.valueOf(value);
} catch (NumberFormatException e) {
candidate = value;
}
parsed[index] = candidate;
}
return candidate;
}
/**
* Get the rank of the specified object according this type.
* This is for {@link #compareTo(Version, int)} internal only.
*/
private static int getTypeRank(final Object value) {
if (value instanceof CharSequence) {
return 0;
}
if (value instanceof Number) {
return 1;
}
throw new IllegalArgumentException(String.valueOf(value));
}
/**
* Compares this version with an other version object, up to the specified limit. A limit
* of 1 compares only the {@linkplain #getMajor major} version number. A limit of 2 compares
* the major and {@linkplain #getMinor minor} version numbers, <cite>etc</cite>. The
* comparaisons are performed as {@link Integer} object if possible, or as {@link String}
* otherwise.
*
* @param other The other version object to compare with.
* @param limit The maximum number of components to compare.
* @return A negative value if this version is lower than the supplied version, a positive
* value if it is higher, or 0 if they are equal.
*/
public int compareTo(final Version other, final int limit) {
for (int i=0; i<limit; i++) {
final Comparable<?> v1 = this.getComponent(i);
final Comparable<?> v2 = other.getComponent(i);
if (v1 == null) {
return (v2 == null) ? 0 : -1;
} else if (v2 == null) {
return +1;
}
final int dr = getTypeRank(v1) - getTypeRank(v2);
if (dr != 0) {
/*
* One value is a text while the other value is a number. We could be tempted to
* force a comparaison by converting the number to a String and then invoking the
* String.compareTo(String) method, but this strategy would violate the following
* contract from Comparable.compareTo(Object): "The implementor must also ensure
* that the relation is transitive". Use case:
*
* A is the integer 10
* B is the string "8Z"
* C is the integer 5.
*
* If mismatched types are converted to String before being compared, then we
* would have A < B < C. Transitivity implies that A < C, but if we compare A
* and C directly we get A > C because they are compared as numbers. An easy
* way to fix this inconsistency is to define all String as lexicographically
* preceding Integer, no matter their content. This is what we do here.
*/
return dr;
}
@SuppressWarnings("unchecked")
final int c = ((Comparable) v1).compareTo(v2);
if (c != 0) {
return c;
}
}
return 0;
}
/**
* Compares this version with an other version object. This method performs the same
* comparaison than {@link #compareTo(Version, int)} with no limit.
*
* @param other The other version object to compare with.
* @return A negative value if this version is lower than the supplied version, a positive
* value if it is higher, or 0 if they are equal.
*/
public int compareTo(final Version other) {
return compareTo(other, Integer.MAX_VALUE);
}
/**
* Compare this version string with the specified object for equality. Two version are
* considered equal if <code>{@linkplain #compareTo(Object) compareTo}(other) == 0</code>.
*
* @param other The object to compare with this version for equality.
*/
@Override
public boolean equals(final Object other) {
if (other != null && getClass().equals(other.getClass())) {
return compareTo((Version) other) == 0;
}
return false;
}
/**
* Returns the length of the version string.
*/
public int length() {
return version.length();
}
/**
* Returns the {@code char} value at the specified index.
*/
public char charAt(final int index) {
return version.charAt(index);
}
/**
* Returns a new version string that is a subsequence of this sequence.
*/
public CharSequence subSequence(final int start, final int end) {
return version.subSequence(start, end);
}
/**
* Returns the version string. This is the string specified at construction time.
*/
@Override
public String toString() {
return version;
}
/**
* Returns a hash code value for this version.
*/
@Override
public int hashCode() {
if (hashCode == 0) {
int code = (int) serialVersionUID;
int index = 0;
Comparable<?> component;
while ((component = getComponent(index)) != null) {
code = code * 37 + component.hashCode();
index++;
}
hashCode = code;
}
return hashCode;
}
}