/*
* Copyright 2011 Tyler Blair. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* The views and conclusions contained in the software and documentation are those of the
* authors and contributors and should not be interpreted as representing official policies,
* either expressed or implied, of anybody else.
*/
package com.griefcraft.util;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* The version class is an immutable object representation of a semantic version (semver.org)
* <p/>
* Some (valid) test cases:
* 1.0.1 > 1.0.0
* 1.0.0 > 1.0.0-beta2
* 1.0.0-beta2 > 1.0.0-alpha3
*/
public final class Version implements Comparable {
/**
* Release levels that can be used
*/
public final static String[] releaseLevels = new String[]{
"release", "alpha", "beta", "rc"
};
/**
* The regex to match the build number
*/
private final static Pattern REGEX_BUILD_NUMBER = Pattern.compile(".*b(\\d+).*");
/**
* The raw version string
*/
private final String rawVersion;
/**
* Major level version
*/
private int major;
/**
* Minor level version
*/
private int minor;
/**
* Patch level version
*/
private int patch;
/**
* Release level weight. 0 is assumed to be greater than non-zero (e.g no -alpha/-beta designation)
* e.g alpha = 1, beta = 2, rc = 3
*/
private int releaseLevelWeight;
/**
* Release level version, for example where -alpha2, releaseLevelWeight = 1 and releaseLevel = 2
*/
private int releaseLevel;
/**
* If the build number is available, store it as well. Updating schemes such as bleeding will use the build
* number to know if it should update or not
*/
private int buildNumber;
public Version(String version) {
if (version.contains("-git-")) {
rawVersion = version.substring(0, version.indexOf("-git-"));
} else {
rawVersion = version;
}
calculateWeights();
}
/**
* Check if this version is newer than the given version
*
* @param version
* @return
*/
public boolean newerThan(Version version) {
return compareTo(version) > 0;
}
/**
* Check i this version is older than the given version
*
* @param version
* @return
*/
public boolean olderThan(Version version) {
return compareTo(version) < 0;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
// add the initial version - major.minor.patch
if (major > 0) {
builder.append(major);
builder.append(".");
builder.append(minor);
builder.append(".");
builder.append(patch);
}
// are we using a different release level than release?
if (releaseLevel > 0) {
String level = releaseLevels[releaseLevel];
builder.append("-");
builder.append(level);
builder.append(releaseLevelWeight);
}
// How about a build number ?
if (buildNumber > 0) {
if (major > 0) {
builder.append(" ");
builder.append("(b");
builder.append(buildNumber);
builder.append(")");
} else {
builder.append("b");
builder.append(buildNumber);
}
}
return builder.toString();
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Version)) {
return false;
}
Version version = (Version) o;
return version.compareTo(this) == 0;
}
public int compareTo(Object o) {
if (!(o instanceof Version)) {
return 0;
}
Version version = (Version) o;
return compareTo(version);
}
public int compareTo(Version o) {
// Easiest way to know immediately if they are different is if their build number differs!
if (buildNumber > 0 && o.getBuildNumber() > 0) {
if (buildNumber > o.getBuildNumber()) {
return 1;
} else if (buildNumber < o.getBuildNumber()) {
return -1;
}
}
// check for major version changes
if (major > o.getMajor()) {
return 1;
} else if (major < o.getMajor()) {
return -1;
}
// major versions are equal.. check minor level
if (minor > o.getMinor()) {
return 1;
} else if (minor < o.getMinor()) {
return -1;
}
// minor versions are equal, too! ..check patch level
if (patch > o.getPatch()) {
return 1;
} else if (patch < o.getPatch()) {
return -1;
}
// should we take into account release level?
if (releaseLevel != 0 || o.getReleaseLevel() != 0) {
// check for release status
if (releaseLevel == 0 || o.getReleaseLevel() == 0) {
return releaseLevel == 0 ? 1 : -1;
}
// they are both an alpha, beta, etc etc, so check for more mature state
if (releaseLevel > o.getReleaseLevel()) {
return 1;
} else if (releaseLevel < o.getReleaseLevel()) {
return -1;
}
// they are both the same release state, ...!
if (releaseLevelWeight > o.getReleaseLevelWeight()) {
return 1;
} else if (releaseLevelWeight < o.getReleaseLevelWeight()) {
return -1;
}
}
// the two versions are identical! wee
return 0;
}
/**
* @return
*/
public int getMajor() {
return major;
}
/**
* @return
*/
public int getMinor() {
return minor;
}
/**
* @return
*/
public int getPatch() {
return patch;
}
/**
* @return
*/
public int getReleaseLevelWeight() {
return releaseLevelWeight;
}
/**
* @return
*/
public int getReleaseLevel() {
return releaseLevel;
}
/**
* @return
*/
public int getBuildNumber() {
return buildNumber;
}
/**
* @return
*/
public String getRawVersion() {
return rawVersion;
}
/**
* Calculate the version weights
*/
private void calculateWeights() {
if (rawVersion.isEmpty()) {
return;
}
// parse the major version
this.major = parseNumber(rawVersion);
// parse the minor version
int minorIndex = rawVersion.indexOf(".") + 1;
this.minor = parseNumber(rawVersion.substring(minorIndex));
// parse the patch version, which is optional
int patchIndex = minorIndex + Integer.toString(this.minor).length() + 1;
if (patchIndex < rawVersion.length()) {
this.patch = parseNumber(rawVersion.substring(patchIndex));
} else {
this.patch = 0;
}
// search for release level
for (int index = 0; index < releaseLevels.length; index++) {
String level = releaseLevels[index];
if (!rawVersion.contains(level)) {
continue;
}
// literally translate the level to the corresponding index
this.releaseLevel = index;
// check for a weighting (e.g version) after it (e.g alpha2)
int levelIndex = rawVersion.indexOf(level) + level.length();
if (levelIndex < rawVersion.length()) {
this.releaseLevelWeight = parseNumber(rawVersion.substring(levelIndex));
} else {
this.releaseLevelWeight = 1;
}
break;
}
// search for build number
Matcher matcher = REGEX_BUILD_NUMBER.matcher(rawVersion);
if (matcher.matches()) {
this.buildNumber = Integer.parseInt(matcher.group(1));
}
}
/**
* Pull a number from the front of the given string
*
* @param str
* @return
*/
private int parseNumber(String str) {
if (str.isEmpty()) {
return 0;
}
// is it actually a number?
try {
return Integer.parseInt(str);
} catch (NumberFormatException e) {
}
int found = 0;
int index = 1;
while (index < str.length()) {
String peek = str.substring(0, index);
try {
found = Integer.parseInt(peek);
index++;
} catch (NumberFormatException e) {
return found;
}
}
return found;
}
}