
org.xwiki.extension.version.internal.DefaultVersion Maven / Gradle / Ivy
Show all versions of xwiki-commons-extension-api Show documentation
/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This 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; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xwiki.extension.version.internal;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xwiki.extension.version.Version;
/**
* Default implementation of {@link Version}. Note each repositories generally provide their own implementation based on
* their own version standard.
*
* Based on AETHER rules which is itself based on Maven specifications.
*
* org.sonatype.aether.util.version.GenericVersion has been rewritten because it's impossible to extends it or even
* access its details to properly implements {@link #getType()} for example.
*
* @version $Id: 12b2b95a14737eba6ac6e70d9466b1b9bf0dfb7f $
* @since 4.0M1
*/
public class DefaultVersion implements Version
{
/**
* Serialization identifier.
*/
private static final long serialVersionUID = 1L;
private static final String MAX_INTEGER_STRING = String.valueOf(Integer.MAX_VALUE);
private static final int MAX_INTEGER_LENGTH = MAX_INTEGER_STRING.length();
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultVersion.class);
/**
* The original version string representation.
*/
private String rawVersion;
/**
* The version cut in peaces for easier comparison.
*/
private List elements;
/**
* @see #getType()
*/
private Type type = Type.STABLE;
/**
* Used to parse the string representation of the version.
*
* @version $Id: 12b2b95a14737eba6ac6e70d9466b1b9bf0dfb7f $
*/
static final class Tokenizer
{
/**
* The string representation of the version.
*/
private final String rawVersion;
/**
* The current index in the parsed version.
*/
private int index;
/**
* @see #isNumber()
*/
private boolean number;
/**
* @see #getToken()
*/
private String token;
/**
* @param rawVersion the string representation of the version
*/
public Tokenizer(String rawVersion)
{
this.rawVersion = (rawVersion.length() > 0) ? rawVersion : "0";
}
/**
* @return the token
*/
public String getToken()
{
return this.token;
}
/**
* @return indicate if the token is a number
*/
public boolean isNumber()
{
return this.number;
}
/**
* @return move to the next token
*/
public boolean next()
{
final int n = this.rawVersion.length();
if (this.index >= n) {
return false;
}
int state = -2;
int start = this.index;
int end = n;
for (; this.index < n; this.index++) {
char c = this.rawVersion.charAt(this.index);
if (c == '.' || c == '-') {
end = this.index;
this.index++;
break;
} else {
int digit = Character.digit(c, 10);
if (digit >= 0) {
if (state == -1) {
end = this.index;
break;
}
if (state == 0) {
// normalize numbers and strip leading zeros
start++;
}
state = (state > 0 || digit > 0) ? 1 : 0;
} else {
if (state >= 0) {
end = this.index;
break;
}
state = -1;
}
}
}
if (start < end) {
this.token = this.rawVersion.substring(start, end);
this.number = state >= 0;
} else {
this.token = "0";
this.number = true;
}
return true;
}
@Override
public String toString()
{
return this.token;
}
}
/**
* A peace of the version.
*
* @version $Id: 12b2b95a14737eba6ac6e70d9466b1b9bf0dfb7f $
*/
static final class Element implements Comparable
{
/**
* Message used in the exception produced when one of the {@link ElementType} is unknown.
*/
private static final String ERROR_UNKNOWNKIND = "Unknown version element kind ";
/**
* The kind of element.
*
* @version $Id: 12b2b95a14737eba6ac6e70d9466b1b9bf0dfb7f $
*/
enum ElementType
{
/**
* A known qualifier id.
*/
QUALIFIER,
/**
* An integer.
*/
INT,
/**
* An unknown literal string.
*/
STRING
}
/**
* The list of known qualifiers.
*/
private static final Map QUALIFIERS;
static {
QUALIFIERS = new HashMap();
QUALIFIERS.put("alpha", Integer.valueOf(-5));
QUALIFIERS.put("a", Integer.valueOf(-5));
QUALIFIERS.put("beta", Integer.valueOf(-4));
QUALIFIERS.put("b", Integer.valueOf(-4));
QUALIFIERS.put("milestone", Integer.valueOf(-3));
QUALIFIERS.put("cr", Integer.valueOf(-2));
QUALIFIERS.put("rc", Integer.valueOf(-2));
QUALIFIERS.put("snapshot", Integer.valueOf(-1));
QUALIFIERS.put("ga", Integer.valueOf(0));
QUALIFIERS.put("final", Integer.valueOf(0));
QUALIFIERS.put("", Integer.valueOf(0));
QUALIFIERS.put("sp", Integer.valueOf(1));
}
/**
* The kind of element.
*/
private final ElementType elementType;
/**
* The value of the element.
*/
private final Object value;
/**
* @see #getVersionType()
*/
private Type versionType = Type.STABLE;
private boolean isInteger(String number)
{
return number.length() < MAX_INTEGER_LENGTH
|| (number.length() == MAX_INTEGER_LENGTH && MAX_INTEGER_STRING.compareTo(number) >= 0);
}
public Element(String token)
{
this.elementType = ElementType.STRING;
this.value = token;
}
/**
* @param tokenizer the token from which to create the version element
*/
public Element(Tokenizer tokenizer)
{
String token = tokenizer.getToken();
if (tokenizer.isNumber()) {
if (isInteger(token)) {
try {
this.elementType = ElementType.INT;
this.value = Integer.valueOf(token);
} catch (NumberFormatException e) {
throw new IllegalStateException(e);
}
} else {
this.elementType = ElementType.STRING;
this.value = token;
}
} else {
String lowerCaseToken = token.toLowerCase(Locale.ENGLISH);
Integer qualifier = QUALIFIERS.get(lowerCaseToken);
if (qualifier != null) {
this.elementType = ElementType.QUALIFIER;
this.value = qualifier;
if (qualifier.intValue() == -1) {
this.versionType = Type.SNAPSHOT;
} else if (qualifier < 0) {
this.versionType = Type.BETA;
}
} else {
this.elementType = ElementType.STRING;
this.value = lowerCaseToken;
}
}
}
/**
* @return indicate of the element is a number
*/
public boolean isNumber()
{
return this.elementType == ElementType.INT || this.elementType == ElementType.QUALIFIER;
}
/**
* @return the type of the version element
*/
public Type getVersionType()
{
return this.versionType;
}
@Override
public int compareTo(Element that)
{
int rel;
if (that == null) {
// null in this context denotes the pad element (0 or "ga")
switch (this.elementType) {
case STRING:
rel = 1;
break;
case INT:
case QUALIFIER:
rel = (Integer) this.value;
break;
default:
throw new IllegalStateException(ERROR_UNKNOWNKIND + this.elementType);
}
} else {
rel = this.elementType.compareTo(that.elementType);
if (rel == 0) {
switch (this.elementType) {
case INT:
case QUALIFIER:
rel = (Integer) this.value - (Integer) that.value;
break;
case STRING:
rel = ((String) this.value).compareToIgnoreCase((String) that.value);
break;
default:
throw new IllegalStateException(ERROR_UNKNOWNKIND + this.elementType);
}
}
}
return rel;
}
@Override
public boolean equals(Object obj)
{
return (obj instanceof Element) && compareTo((Element) obj) == 0;
}
@Override
public int hashCode()
{
HashCodeBuilder builder = new HashCodeBuilder();
builder.append(this.value);
builder.append(this.elementType);
return builder.toHashCode();
}
@Override
public String toString()
{
return this.value.toString();
}
}
/**
* @param rawVersion the original string representation of the version
*/
public DefaultVersion(String rawVersion)
{
setVersion(rawVersion);
}
/**
* Create a new {@link DefaultVersion} by cloning the provided version.
*
* @param version the version to copy
*/
public DefaultVersion(Version version)
{
this(version.getValue());
}
/**
* Make sure the version has been parsed.
*/
private void initElements()
{
if (this.elements == null) {
parse();
}
}
/**
* @param rawVersion the string representation to parse
*/
private void setVersion(String rawVersion)
{
this.rawVersion = rawVersion;
}
/**
* Parse the string representation of the version into separated elements.
*/
private void parse()
{
this.elements = new ArrayList();
try {
for (Tokenizer tokenizer = new Tokenizer(this.rawVersion); tokenizer.next();) {
Element element = new Element(tokenizer);
this.elements.add(element);
if (element.getVersionType() != Type.STABLE) {
this.type = element.getVersionType();
}
}
trimPadding(this.elements);
} catch (Exception e) {
// Make sure to never fail no matter what
LOGGER.error("Failed to parse version [" + this.rawVersion + "]", e);
this.elements.add(new Element(this.rawVersion));
}
}
/**
* Remove empty elements.
*
* @param elements the list of clean
*/
private static void trimPadding(List elements)
{
for (ListIterator it = elements.listIterator(elements.size()); it.hasPrevious();) {
Element element = it.previous();
if (element.compareTo(null) == 0) {
it.remove();
} else {
break;
}
}
}
@Override
public Type getType()
{
initElements();
return this.type;
}
// Version
@Override
public String getValue()
{
return this.rawVersion;
}
// Object
@Override
public String toString()
{
return getValue();
}
@Override
public int hashCode()
{
initElements();
return this.elements.hashCode();
}
@Override
public boolean equals(Object obj)
{
if (obj == this) {
return true;
}
boolean equals;
if (obj instanceof DefaultVersion) {
equals = equals((DefaultVersion) obj);
} else if (obj instanceof Version) {
equals = equals(new DefaultVersion(((Version) obj).getValue()));
} else {
equals = false;
}
return equals;
}
/**
* @param version the version
* @return true if the provided version is equals to this version
*/
public boolean equals(DefaultVersion version)
{
return compareTo(version) == 0;
}
@Override
public int compareTo(Version version)
{
if (version == this) {
return 0;
}
if (version instanceof DefaultVersion) {
return compareTo((DefaultVersion) version);
} else {
return compareTo(new DefaultVersion(version.getValue()));
}
}
/**
* @param version the version to compare as a String
* @return a negative integer, zero, or a positive integer as this version is less than, equal to, or greater than
* the specified version
* @since 7.4.2, 8.0M2
*/
public int compareTo(String version)
{
return compareTo(new DefaultVersion(version));
}
/**
* @param version the version to compare
* @return a negative integer, zero, or a positive integer as this version is less than, equal to, or greater than
* the specified version
*/
public int compareTo(DefaultVersion version)
{
initElements();
version.initElements();
final List otherElements = version.elements;
boolean number = true;
int rel;
for (int index = 0;; index++) {
if (index >= this.elements.size() && index >= otherElements.size()) {
return 0;
} else if (index >= this.elements.size()) {
return -comparePadding(otherElements, index, null);
} else if (index >= otherElements.size()) {
return comparePadding(this.elements, index, null);
}
Element thisElement = this.elements.get(index);
Element thatElement = otherElements.get(index);
if (thisElement.isNumber() != thatElement.isNumber()) {
if (number == thisElement.isNumber()) {
rel = comparePadding(this.elements, index, Boolean.valueOf(number));
} else {
rel = -comparePadding(otherElements, index, Boolean.valueOf(number));
}
break;
} else {
rel = thisElement.compareTo(thatElement);
if (rel != 0) {
break;
}
number = thisElement.isNumber();
}
}
return rel;
}
/**
* Compare the end of the version with 0.
*
* @param elements the elements to compare to 0
* @param index the index where to start comparing with 0
* @param number indicate of the previous element is a number
* @return the comparison result
*/
private static int comparePadding(List elements, int index, Boolean number)
{
int rel = 0;
for (Iterator it = elements.listIterator(index); it.hasNext();) {
Element element = it.next();
if (number != null && number.booleanValue() != element.isNumber()) {
break;
}
rel = element.compareTo(null);
if (rel != 0) {
break;
}
}
return rel;
}
// Serializable
/**
* @param out the stream
* @throws IOException error when serializing the version
*/
private void writeObject(ObjectOutputStream out) throws IOException
{
out.writeObject(getValue());
}
/**
* @param in the stream
* @throws IOException error when unserializing the version
* @throws ClassNotFoundException error when unserializing the version
*/
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
{
setVersion((String) in.readObject());
}
}