com.seeq.utilities.VersionNumber Maven / Gradle / Ivy
The newest version!
package com.seeq.utilities;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.apache.commons.lang.StringUtils;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.seeq.utilities.process.RuntimeVersion;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.With;
/**
* A utility class that understands semantic versioning for comparisons. While this is
* based on a SemanticVersion class that came from cassandra 2.0, an external library could be used if needed.
*
* Feel free to add other methods to expose the actual major.minor.patch if you need them.
*/
@EqualsAndHashCode
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class VersionNumber implements Comparable {
@Getter
@With
private final int major;
@Getter
@With
private final int minor;
private final int patch;
@Getter
@With
private final String[] preRelease; // Example: v202006051321
private final String[] build;
// Matches a semantic version number
private static final String VERSION_REGEXP = "(\\d+)\\.(\\d+)\\.(\\d+)(-[.\\w]+)?(\\-[.\\w]+)?";
private static final Pattern pattern = Pattern.compile(VERSION_REGEXP);
// Matches a semantic version number that contains a (non-semantic) suffix
private static final Pattern versionRegexForSnapshot = Pattern.compile("(\\d+\\.\\d+\\.\\d+(-v\\d+)?)(-[^v].*?)$");
public VersionNumber(String version) {
// the build number generated by our build isn't semantic due to the suffix (e.g. "-SNAPSHOT")
Matcher matcher = versionRegexForSnapshot.matcher(version);
if (matcher.find()) {
version = matcher.group(1);
}
matcher = pattern.matcher(version);
if (!matcher.matches()) {
throw new IllegalArgumentException("Invalid version value: " + version
+ " (see http://semver.org/ for details)");
}
try {
this.major = Integer.parseInt(matcher.group(1));
this.minor = Integer.parseInt(matcher.group(2));
this.patch = Integer.parseInt(matcher.group(3));
String pr = matcher.group(4);
String bld = matcher.group(5);
this.preRelease = pr == null || pr.isEmpty() ? null : parseIdentifiers(version, pr);
this.build = bld == null || bld.isEmpty() ? null : parseIdentifiers(version, bld);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid version value: " + version
+ " (see http://semver.org/ for details)");
}
}
/**
* Strip the RXX. prefix. So for a version like R19.0.36.03, pull out "R19." and leave just 0.36.03.
* For a version like R50.0.1, pull out "R" and leave just 50.0.1.
*
* @param versionString
* The version string to strip the prefix from.
* @return The version string with the prefix removed, if necessary.
*/
public static String removeMarketingPrefix(String versionString) {
Matcher matcher = Pattern.compile("^R(\\d+\\.)?(" + VERSION_REGEXP + ")").matcher(versionString);
if (matcher.matches()) {
return matcher.group(2);
} else {
return versionString;
}
}
/**
* Default method to test whether an upgrade should apply. The function returns true if 'upgradeAtVersion' is
* greater than 'fromVersion' and less than or equal to 'toVersion'.
*
* @param upgradeAtVersion
* the first version of Seeq where the upgrade should apply
* @param fromVersion
* the previously-installed version of Seeq
* @param toVersion
* the current version of Seeq
* @return true if the upgrade should be applied
*/
public static boolean shouldUpgrade(VersionNumber upgradeAtVersion, VersionNumber fromVersion,
VersionNumber toVersion) {
return fromVersion.compareTo(upgradeAtVersion) < 0 &&
toVersion.compareTo(upgradeAtVersion) >= 0;
}
private static String[] parseIdentifiers(String version, String str) {
// Drop initial - or +
str = str.substring(1);
String[] parts = str.split("\\.");
for (String part : parts) {
if (!part.matches("\\w+")) {
throw new IllegalArgumentException("Invalid version value: " + version
+ " (see http://semver.org/ for details)");
}
}
return parts;
}
@Override
public int compareTo(VersionNumber other) {
// A defaultResultPreRelease argument of 1 means that if
// the preRelease version of 'this' is null and the preRelease version
// of 'other' is not null, we will return 1. By convention/definition,
// 'this' is considered greater than 'other'. The defaultReleaseBuild
// argument of -1 means that the opposite is true for the build version.
return this.compareTo(other, 1, -1);
}
/**
* This method implements compareTo with parameters to specify whether null preRelease
* and build versions should be considered less than, greater than or equal to non-null
* preRelease and build versions.
*
* @param other
* VersionNumber to compare with.
* @param defaultResultPreRelease
* default result of comparing the preRelease versions. Used if one of the preRelease
* versions is null.
* @param defaultResultBuild
* default result of comparing the build versions. Used if one of the build
* versions is null.
* @return a -ve integer, zero, or a +ve integer as this object is less than, equal to,
* or greater than other.
*/
private int compareTo(VersionNumber other, int defaultResultPreRelease, int defaultResultBuild) {
if (this.major < other.major) {
return -1;
}
if (this.major > other.major) {
return 1;
}
if (this.minor < other.minor) {
return -1;
}
if (this.minor > other.minor) {
return 1;
}
if (this.patch < other.patch) {
return -1;
}
if (this.patch > other.patch) {
return 1;
}
int c = compareIdentifiers(this.preRelease, other.preRelease, defaultResultPreRelease);
if (c != 0) {
return c;
}
return compareIdentifiers(this.build, other.build, defaultResultBuild);
}
private static int compareIdentifiers(String[] ids1, String[] ids2, int defaultPred) {
if (ids1 == null) {
return ids2 == null ? 0 : defaultPred;
} else if (ids2 == null) {
return -defaultPred;
}
int min = Math.min(ids1.length, ids2.length);
for (int i = 0; i < min; i++) {
Integer i1 = tryParseInt(ids1[i]);
Integer i2 = tryParseInt(ids2[i]);
if (i1 != null) {
// integer have precedence
if (i2 == null || i1 < i2) {
return -1;
} else if (i1 > i2) {
return 1;
}
} else {
// integer have precedence
if (i2 != null) {
return 1;
}
int c = ids1[i].compareTo(ids2[i]);
if (c != 0) {
return c;
}
}
}
return Integer.compare(ids1.length, ids2.length);
}
private static Integer tryParseInt(String str) {
try {
return Integer.valueOf(str);
} catch (NumberFormatException e) {
return null;
}
}
private String toString(boolean withGenericPatch) {
String paddingFormatter = this.major == 0 ? "%02d" : "%d";
StringBuilder sb = new StringBuilder()
.append(this.major)
.append('.')
.append(String.format(paddingFormatter, this.minor))
.append('.')
.append(withGenericPatch ? "XX" : String.format(paddingFormatter, this.patch));
if (this.preRelease != null) {
sb.append('-').append(StringUtils.join(this.preRelease, "."));
}
if (this.build != null) {
sb.append('+').append(StringUtils.join(this.build, "."));
}
return sb.toString();
}
@Override
public String toString() {
return this.toString(false);
}
public static Optional readVersionNumberFromFile(Path path) throws IOException {
return Files.readAllLines(path, StandardCharsets.UTF_8).stream()
.findFirst()
.map(String::trim)
.map(VersionNumber::removeMarketingPrefix)
.map(VersionNumber::new);
}
/**
* Asserts that upgrades from the current version (as determined by the version.txt file at {@code versionPath}) are
* supported.
*
* @param buildVersion
* - The current build version
* @param versionPath
* - The path to version.txt
* @throws IOException
* - If the version.txt can't be read
*/
public static void assertUpgradeAllowed(RuntimeVersion buildVersion, Path versionPath) throws IOException {
VersionNumber buildVersionNumber =
new VersionNumber(VersionNumber.removeMarketingPrefix(buildVersion.getVersion()));
// Existing version is in $DATA_DIR/version.txt.
// Check it against the version we're running and see if we allow the upgrade.
// If it doesn't exist we're either a brand new install or R18-or-older. In that case we'll allow it
// and let the graph DB do its check.
if (versionPath.toFile().exists()) {
readVersionNumberFromFile(versionPath)
.ifPresent(version -> VersionNumber.assertUpgradeAllowed(Optional.of(version), buildVersionNumber));
}
}
/**
* Asserts that upgrades from the specified 'from' version to the specified 'to' version are supported.
* The logic here must be kept in sync with the logic in _check_upgrade_compatibility from
* common/migrations/_impl.py.
*
* @param maybeFrom
* The 'from' version. If empty, this signifies that the current version is unknown (i.e.,
* there is no version.txt file) and calculates an upgrade path starting at 0.37.XX.
* @param to
* The 'to' version.
*/
public static void assertUpgradeAllowed(Optional maybeFrom, VersionNumber to) {
VersionNumber from = maybeFrom.orElse(new VersionNumber("0.0.0"));
if (from.compareTo(to, 0, -1) > 0) {
throw new IllegalStateException(
String.format("Downgrades are not supported (%s to %s).",
maybeFrom.map(VersionNumber::toString).orElse("your current version"),
to)
);
}
List upgradePath = deriveUpgradePath(from, to);
if (upgradePath.size() > 0) {
String upgradeSnippet = upgradePathToSnippet(maybeFrom, upgradePath);
throw new IllegalStateException(
String.format(
"Upgrades from %s to %s are not allowed. Please run the following upgrades, in order " +
"(where XX signifies the latest patch version):\n%s",
maybeFrom.map(VersionNumber::toString).orElse("your current version"),
to,
upgradeSnippet
)
);
}
}
private static String upgradePathToSnippet(Optional from, List upgradePath) {
List fullUpgradePath = ImmutableList.builder()
.add(from.map(VersionNumber::toString).map(current -> current + " (current)").orElse("current"))
.addAll(upgradePath.stream().map(v -> v.toString(true)).iterator())
.build();
return IntStream.range(0, fullUpgradePath.size() - 1)
.mapToObj(i -> String.format(" %s -> %s", fullUpgradePath.get(i), fullUpgradePath.get(i + 1)))
.collect(Collectors.joining("\n"));
}
private static List deriveUpgradePath(VersionNumber from, VersionNumber to) {
VersionNumber genericTo = to.withPreRelease(null);
Optional maybeLastRequiredUpgrade = Optional.empty();
List upgradePath = new ArrayList<>();
Optional maybeNextVersion = Optional.of(from);
do {
maybeNextVersion = getFurthestCompatibleUpgradeStopVersion(maybeNextVersion.get());
if (maybeNextVersion.isPresent() && maybeNextVersion.get().compareTo(genericTo) < 0) {
upgradePath.add(maybeNextVersion.get());
maybeLastRequiredUpgrade = maybeNextVersion;
} else if (maybeLastRequiredUpgrade.isPresent()) {
int lastVersion = maybeLastRequiredUpgrade.get().getMajor() == 0 ?
maybeLastRequiredUpgrade.get().getMinor() : maybeLastRequiredUpgrade.get().getMajor();
int toVersion = genericTo.getMajor() == 0 ? genericTo.getMinor() : genericTo.getMajor();
if (lastVersion < toVersion) {
upgradePath.add(genericTo);
}
}
} while (maybeNextVersion.isPresent() && maybeNextVersion.get().compareTo(to) < 0);
return upgradePath;
}
/*
The current list of historical required upgrade stops is: R20.0.39, R50.3.0, R58.0.0
Until a new required upgrade stop is determined, we allow upgrades from R58.0.0 or later to any future version
For R58.0.0, we allow upgrades from versions in the interval [R50.3.0, R58.0.0)
For R50.3.0, we allow upgrades from versions in the interval [R20.0.39, R50.3.0)
For R20.0.38, we allow upgrades from versions R19.0.36 or R19.0.37
For R19.0.37, we allow upgrades from any previous version
*/
@VisibleForTesting
static Optional getFurthestCompatibleUpgradeStopVersion(VersionNumber from) {
if (from.getMajor() == 0) {
// Prior to R50.0.0, the number that is now Major was encoded in Minor, and Major was 0.
if (from.getMinor() < 36) {
return Optional.of(new VersionNumber("0.36.0"));
} else if (from.getMinor() < 38) {
return Optional.of(new VersionNumber("0.39.0"));
} else {
return Optional.of(new VersionNumber("50.3.0"));
}
} else if (from.getMajor() == 50) {
if (from.getMinor() < 3) {
return Optional.of(new VersionNumber("50.3.0"));
} else {
return Optional.of(new VersionNumber("58.0.0"));
}
} else if (from.getMajor() < 58) {
return Optional.of(new VersionNumber("58.0.0"));
}
return Optional.empty();
}
}