name.remal.version.Version Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of common Show documentation
Show all versions of common Show documentation
Java & Kotlin tools: common
package name.remal.version;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import com.google.common.base.Splitter;
import name.remal.gradle_plugins.api.RelocateClasses;
import net.jcip.annotations.Immutable;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.lang.Character.compare;
import static java.lang.Integer.parseInt;
import static java.lang.Long.parseUnsignedLong;
import static java.lang.Math.max;
import static java.lang.Math.min;
import static java.lang.String.format;
import static java.util.Arrays.*;
import static java.util.Collections.unmodifiableMap;
@Immutable
public final class Version implements Comparable, Serializable, Cloneable {
private static final long serialVersionUID = 1;
private static final char NUMBERS_DELIMITER = '.';
@NotNull
private static final char[] ALLOWED_SUFFIX_DELIMITERS;
static {
ALLOWED_SUFFIX_DELIMITERS = new char[]{'.', '_', '-', '+'};
sort(ALLOWED_SUFFIX_DELIMITERS);
}
@NotNull
private static final Pattern VERSION_PATTERN;
static {
StringBuilder sb = new StringBuilder();
sb.append("(\\d+(?:\\").append(NUMBERS_DELIMITER).append("\\d+)*)");
sb.append("([");
for (char ALLOWED_SUFFIX_DELIMITER : ALLOWED_SUFFIX_DELIMITERS) {
sb.append('\\').append(ALLOWED_SUFFIX_DELIMITER);
}
sb.append("][\\S]+)?");
VERSION_PATTERN = Pattern.compile(sb.toString());
}
private static final char DEFAULT_SUFFIX_DELIMITER = '-';
private static final Pattern SUFFIX_SPLITTER = Pattern.compile("(?:(\\W+)|(\\d+))");
private static final SuffixToken[] EMPTY_SUFFIX_TOKENS = new SuffixToken[0];
@NotNull
private static final Map SUFFIX_ORDERS;
static {
Map suffixOrders = new HashMap<>();
int order = 0;
++order;
suffixOrders.put("r", order);
suffixOrders.put("release", order);
suffixOrders.put("ga", order);
suffixOrders.put("final", order);
++order;
suffixOrders.put("sp", order);
order = 0;
--order;
suffixOrders.put("snapshot", order);
--order;
suffixOrders.put("nightly", order);
--order;
suffixOrders.put("rc", order);
suffixOrders.put("cr", order);
--order;
suffixOrders.put("milestone", order);
suffixOrders.put("m", order);
--order;
suffixOrders.put("beta", order);
suffixOrders.put("b", order);
--order;
suffixOrders.put("alpha", order);
suffixOrders.put("a", order);
--order;
suffixOrders.put("dev", order);
suffixOrders.put("pr", order);
SUFFIX_ORDERS = unmodifiableMap(suffixOrders);
}
@NotNull
private static final Version V0 = new Version(new int[]{0}, DEFAULT_SUFFIX_DELIMITER, null);
@NotNull
private static final Version V1 = new Version(new int[]{1}, DEFAULT_SUFFIX_DELIMITER, null);
@NotNull
private static final Version V0_0 = new Version(new int[]{0, 0}, DEFAULT_SUFFIX_DELIMITER, null);
@NotNull
private static final Version V0_1 = new Version(new int[]{0, 1}, DEFAULT_SUFFIX_DELIMITER, null);
@NotNull
private static final Version V1_0 = new Version(new int[]{1, 0}, DEFAULT_SUFFIX_DELIMITER, null);
@NotNull
private static final Version V0_0_0 = new Version(new int[]{0, 0, 0}, DEFAULT_SUFFIX_DELIMITER, null);
@NotNull
private static final Version V0_0_1 = new Version(new int[]{0, 0, 1}, DEFAULT_SUFFIX_DELIMITER, null);
@NotNull
private static final Version V0_1_0 = new Version(new int[]{0, 1, 0}, DEFAULT_SUFFIX_DELIMITER, null);
@NotNull
private static final Version V1_0_0 = new Version(new int[]{1, 0, 0}, DEFAULT_SUFFIX_DELIMITER, null);
@NotNull
private static final Version V0_0_0_0 = new Version(new int[]{0, 0, 0, 0}, DEFAULT_SUFFIX_DELIMITER, null);
@NotNull
private static final Version V0_0_0_1 = new Version(new int[]{0, 0, 0, 1}, DEFAULT_SUFFIX_DELIMITER, null);
@NotNull
private static final Version V0_0_1_0 = new Version(new int[]{0, 0, 1, 0}, DEFAULT_SUFFIX_DELIMITER, null);
@NotNull
private static final Version V0_1_0_0 = new Version(new int[]{0, 1, 0, 0}, DEFAULT_SUFFIX_DELIMITER, null);
@NotNull
private static final Version V1_0_0_0 = new Version(new int[]{1, 0, 0, 0}, DEFAULT_SUFFIX_DELIMITER, null);
@NotNull
private static Version createImpl(@NotNull int[] numbers, char suffixDelimiter, @Nullable String suffix) {
if (suffixDelimiter == DEFAULT_SUFFIX_DELIMITER && (suffix == null || suffix.isEmpty())) {
if (Arrays.equals(V0.numbers, numbers)) return V0;
if (Arrays.equals(V1.numbers, numbers)) return V1;
if (Arrays.equals(V0_0.numbers, numbers)) return V0_0;
if (Arrays.equals(V0_1.numbers, numbers)) return V0_1;
if (Arrays.equals(V1_0.numbers, numbers)) return V1_0;
if (Arrays.equals(V0_0_0.numbers, numbers)) return V0_0_0;
if (Arrays.equals(V0_0_1.numbers, numbers)) return V0_0_1;
if (Arrays.equals(V0_1_0.numbers, numbers)) return V0_1_0;
if (Arrays.equals(V1_0_0.numbers, numbers)) return V1_0_0;
if (Arrays.equals(V0_0_0_0.numbers, numbers)) return V0_0_0_0;
if (Arrays.equals(V0_0_0_1.numbers, numbers)) return V0_0_0_1;
if (Arrays.equals(V0_0_1_0.numbers, numbers)) return V0_0_1_0;
if (Arrays.equals(V0_1_0_0.numbers, numbers)) return V0_1_0_0;
if (Arrays.equals(V1_0_0_0.numbers, numbers)) return V1_0_0_0;
}
return new Version(numbers, suffixDelimiter, suffix);
}
@NotNull
public static Version create(@NotNull int[] numbers, char suffixDelimiter, @NotNull String suffix) {
return createImpl(numbers, suffixDelimiter, suffix);
}
@NotNull
public static Version create(@NotNull int[] numbers, @Nullable String suffix) {
return createImpl(numbers, DEFAULT_SUFFIX_DELIMITER, suffix);
}
@NotNull
public static Version create(@NotNull int... numbers) {
return create(numbers, null);
}
@NotNull
public static Version create(int major, int minor, int patch, int build, char suffixDelimiter, @NotNull String suffix) {
return create(new int[]{major, minor, patch, build}, suffixDelimiter, suffix);
}
@NotNull
public static Version create(int major, int minor, int patch, int build, @Nullable String suffix) {
return create(new int[]{major, minor, patch, build}, suffix);
}
@NotNull
public static Version create(int major, int minor, int patch, int build) {
return create(major, minor, patch, build, null);
}
@NotNull
public static Version create(int major, int minor, int patch, char suffixDelimiter, @NotNull String suffix) {
return create(new int[]{major, minor, patch}, suffixDelimiter, suffix);
}
@NotNull
public static Version create(int major, int minor, int patch, @Nullable String suffix) {
return create(new int[]{major, minor, patch}, suffix);
}
@NotNull
public static Version create(int major, int minor, int patch) {
return create(major, minor, patch, null);
}
@NotNull
public static Version create(int major, int minor, char suffixDelimiter, @NotNull String suffix) {
return create(new int[]{major, minor}, suffixDelimiter, suffix);
}
@NotNull
public static Version create(int major, int minor, @Nullable String suffix) {
return create(new int[]{major, minor}, suffix);
}
@NotNull
public static Version create(int major, int minor) {
return create(major, minor, null);
}
@NotNull
public static Version create(int major, char suffixDelimiter, @NotNull String suffix) {
return create(new int[]{major}, suffixDelimiter, suffix);
}
@NotNull
public static Version create(int major, @Nullable String suffix) {
return create(new int[]{major}, suffix);
}
@NotNull
public static Version create(int major) {
return create(major, null);
}
@NotNull
@RelocateClasses(Splitter.class)
@JsonCreator
public static Version parse(@NotNull String string) throws VersionParsingException {
//noinspection ConstantConditions
if (string == null) {
throw new VersionParsingException("NULL string");
}
string = string.trim();
if (string.isEmpty()) {
throw new VersionParsingException("Empty string");
}
Matcher matcher = VERSION_PATTERN.matcher(string);
if (!matcher.matches()) {
throw new VersionParsingException(format("\"%s\" doesn't match to /%s/", string, VERSION_PATTERN));
}
try {
String numbersString = matcher.group(1);
List numberStrings = Splitter.on(NUMBERS_DELIMITER).splitToList(numbersString);
int[] numbers = new int[numberStrings.size()];
for (int i = 0; i < numbers.length; ++i) {
numbers[i] = parseInt(numberStrings.get(i));
}
char suffixDelimiter = DEFAULT_SUFFIX_DELIMITER;
String suffix = matcher.group(2);
if (suffix != null && !suffix.isEmpty()) {
suffixDelimiter = suffix.charAt(0);
suffix = suffix.substring(1);
}
return createImpl(numbers, suffixDelimiter, suffix);
} catch (Exception e) {
throw e instanceof VersionParsingException ? (VersionParsingException) e : new VersionParsingException(e);
}
}
@Nullable
@Contract("null->null")
public static Version parseOrNull(@Nullable String string) {
if (string == null || string.isEmpty()) return null;
try {
return parse(string);
} catch (VersionParsingException ignored) {
return null;
}
}
@NotNull
private final int[] numbers;
private final char suffixDelimiter;
@NotNull
private final String suffix;
@NotNull
private final SuffixToken[] suffixTokens;
private final int suffixOrder;
private final long suffixSuborder;
private Version(@NotNull int[] numbers, char suffixDelimiter, @Nullable String suffix) {
if (0 == numbers.length) {
throw new IllegalArgumentException("numbers is an empty array");
}
if (binarySearch(ALLOWED_SUFFIX_DELIMITERS, suffixDelimiter) < 0) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < ALLOWED_SUFFIX_DELIMITERS.length; ++i) {
if (i == 0) {
sb.append("suffixDelimiter must be one of ");
} else {
sb.append(", ");
}
sb.append('\'').append(ALLOWED_SUFFIX_DELIMITERS[i]).append('\'');
}
throw new IllegalArgumentException(sb.toString());
}
for (int i = 0; i < numbers.length; ++i) {
int number = numbers[i];
if (number < 0) {
StringBuilder sb = new StringBuilder();
if (0 == i) {
sb.append("major");
} else if (1 == i) {
sb.append("minor");
} else if (2 == i) {
sb.append("patch");
} else if (3 == i) {
sb.append("build");
} else {
sb.append("numbers[").append(i).append(']');
}
sb.append(" < 0");
throw new IllegalArgumentException(sb.toString());
}
}
this.numbers = numbers.clone();
this.suffixDelimiter = suffixDelimiter;
this.suffix = suffix != null ? suffix : "";
if (!this.suffix.isEmpty()) {
{
List tokens = new ArrayList<>();
String suffixLower = this.suffix.toLowerCase();
Matcher matcher = SUFFIX_SPLITTER.matcher(suffixLower);
int substringStart = 0;
while (matcher.find()) {
if (substringStart < matcher.start()) tokens.add(new SuffixToken(suffixLower.substring(substringStart, matcher.start())));
String number = matcher.group(2);
if (number != null) tokens.add(new SuffixToken(number, parseUnsignedLong(number)));
substringStart = matcher.end();
}
if (substringStart < suffixLower.length()) tokens.add(new SuffixToken(suffixLower.substring(substringStart)));
this.suffixTokens = tokens.toArray(new SuffixToken[0]);
}
{
int suffixOrder = Integer.MIN_VALUE;
long suffixSuborder = 0;
for (int i = 0; i < suffixTokens.length; ++i) {
SuffixToken token = suffixTokens[i];
if (token.number == null) {
Integer order = SUFFIX_ORDERS.get(token.string.toLowerCase());
if (order != null && suffixOrder < order) {
suffixOrder = order;
suffixSuborder = 0;
if (i + 1 < suffixTokens.length) {
SuffixToken nextToken = suffixTokens[i + 1];
if (nextToken.number != null) {
suffixSuborder = nextToken.number;
++i;
}
}
}
}
}
this.suffixOrder = (Integer.MIN_VALUE == suffixOrder) ? (0) : (suffixOrder);
this.suffixSuborder = suffixSuborder;
}
} else {
this.suffixTokens = EMPTY_SUFFIX_TOKENS;
this.suffixOrder = 0;
this.suffixSuborder = 0;
}
}
private Version(@NotNull int[] numbers, char suffixDelimiter, @NotNull String suffix, @NotNull SuffixToken[] suffixTokens, int suffixOrder, long suffixSuborder) {
this.numbers = numbers.clone();
this.suffixDelimiter = suffixDelimiter;
this.suffix = suffix;
this.suffixTokens = suffixTokens.clone();
this.suffixOrder = suffixOrder;
this.suffixSuborder = suffixSuborder;
}
@Override
@NotNull
@JsonValue
public String toString() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < numbers.length; ++i) {
if (1 <= i) sb.append(NUMBERS_DELIMITER);
sb.append(numbers[i]);
}
if (!suffix.isEmpty()) {
sb.append(suffixDelimiter).append(suffix);
}
return sb.toString();
}
public int getNumbersCount() {
return numbers.length;
}
@Nullable
public Integer getNumberOrNull(int index) {
return index < numbers.length ? numbers[index] : null;
}
public int getNumberOr0(int index) {
return index < numbers.length ? numbers[index] : 0;
}
@NotNull
public Version withNumber(int index, int number) {
if (index < numbers.length && number == numbers[index]) return this;
int[] newNumbers = copyOf(numbers, max(numbers.length, index + 1));
newNumbers[index] = number;
return createImpl(newNumbers, suffixDelimiter, suffix);
}
@NotNull
public Version incrementNumber(int index, int delta) {
return withNumber(index, getNumberOr0(index) + delta);
}
@NotNull
public Version incrementNumber(int index) {
return incrementNumber(index, 1);
}
@NotNull
public Version withoutNumber(int index) {
if (numbers.length <= index) return this;
if (index <= 0) throw new IllegalArgumentException("Version must contain at least one number");
int[] newNumbers = copyOf(numbers, index);
return createImpl(newNumbers, suffixDelimiter, suffix);
}
public int getMajor() {
return numbers[0];
}
@NotNull
public Version withMajor(int major) {
return withNumber(0, major);
}
@NotNull
public Version incrementMajor(int delta) {
return incrementNumber(0, delta);
}
public boolean hasMinor() {
return 1 < numbers.length;
}
@Nullable
public Integer getMinor() {
return getNumberOrNull(1);
}
@NotNull
public Version withMinor(int minor) {
return withNumber(1, minor);
}
@NotNull
public Version incrementMinor(int delta) {
return incrementNumber(1, delta);
}
@NotNull
public Version incrementMinor() {
return incrementMinor(1);
}
@NotNull
public Version withoutMinor() {
return withoutNumber(1);
}
public boolean hasPatch() {
return 2 < numbers.length;
}
@Nullable
public Integer getPatch() {
return getNumberOrNull(2);
}
@NotNull
public Version withPatch(int patch) {
return withNumber(2, patch);
}
@NotNull
public Version incrementPatch(int delta) {
return incrementNumber(2, delta);
}
@NotNull
public Version incrementPatch() {
return incrementPatch(1);
}
@NotNull
public Version withoutPatch() {
return withoutNumber(2);
}
public boolean hasBuild() {
return 3 < numbers.length;
}
@Nullable
public Integer getBuild() {
return getNumberOrNull(3);
}
@NotNull
public Version withBuild(int build) {
return withNumber(3, build);
}
@NotNull
public Version incrementBuild(int delta) {
return incrementNumber(3, delta);
}
@NotNull
public Version incrementBuild() {
return incrementBuild(1);
}
@NotNull
public Version withoutBuild() {
return withoutNumber(3);
}
public char getSuffixDelimiter() {
return suffixDelimiter;
}
@NotNull
public Version withSuffixDelimiter(char suffixDelimiter) {
if (this.suffixDelimiter == suffixDelimiter) return this;
return createImpl(numbers, suffixDelimiter, suffix);
}
@NotNull
public Version withDefaultSuffixDelimiter() {
return withSuffixDelimiter(DEFAULT_SUFFIX_DELIMITER);
}
public boolean hasSuffix() {
return !suffix.isEmpty();
}
@NotNull
public String getSuffix() {
return suffix;
}
@NotNull
public Version withSuffix(@Nullable String suffix) {
if (suffix == null) suffix = "";
if (this.suffix.equals(suffix)) return this;
return createImpl(numbers, suffixDelimiter, suffix);
}
@NotNull
public Version appendSuffix(@NotNull String suffixPart, @NotNull String delimiter) {
if (suffix.isEmpty()) {
return withSuffix(suffixPart);
} else {
return withSuffix(suffix + delimiter + suffixPart);
}
}
@NotNull
public Version appendSuffix(@NotNull String suffixPart) {
return appendSuffix(suffixPart, "");
}
@NotNull
public Version prependSuffix(@NotNull String suffixPart, @NotNull String delimiter) {
if (suffix.isEmpty()) {
return withSuffix(suffixPart);
} else {
return withSuffix(suffixPart + delimiter + suffix);
}
}
@NotNull
public Version prependSuffix(@NotNull String suffixPart) {
return prependSuffix(suffixPart, "");
}
@NotNull
public Version withoutSuffix() {
return withSuffix("");
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) return true;
if (!(obj instanceof Version)) return false;
Version other = (Version) obj;
if (!Arrays.equals(numbers, other.numbers)) return false;
if (suffixDelimiter != other.suffixDelimiter) return false;
if (!suffix.equals(other.suffix)) return false;
return true;
}
@Override
public int hashCode() {
return 31 * Arrays.hashCode(numbers) + Character.hashCode(suffixDelimiter) + suffix.hashCode();
}
@Override
public int compareTo(@NotNull Version other) {
{
int commonLength = min(numbers.length, other.numbers.length);
for (int i = 0; i < commonLength; ++i) {
int result = Integer.compare(numbers[i], other.numbers[i]);
if (0 != result) return result;
}
if (commonLength < numbers.length) return 1;
if (commonLength < other.numbers.length) return -1;
}
if (suffix.equals(other.suffix)) {
return 0;
}
{
int result = Integer.compare(suffixOrder, other.suffixOrder);
if (0 == result) result = Long.compare(suffixSuborder, other.suffixSuborder);
if (0 != result) return result;
}
{
int commonLength = min(suffixTokens.length, other.suffixTokens.length);
for (int i = 0; i < commonLength; ++i) {
int result = suffixTokens[i].compareTo(other.suffixTokens[i]);
if (0 != result) return result;
}
if (commonLength < suffixTokens.length) return 1;
if (commonLength < other.suffixTokens.length) return -1;
}
{
int result = suffix.compareTo(other.suffix);
if (0 != result) return result;
}
return compare(suffixDelimiter, other.suffixDelimiter);
}
@Override
@NotNull
public Version clone() {
return new Version(numbers, suffixDelimiter, suffix, suffixTokens, suffixOrder, suffixSuborder);
}
@Immutable
private static final class SuffixToken implements Comparable, Serializable {
private static final long serialVersionUID = 1;
@NotNull
private final String string;
@Nullable
private final Long number;
private SuffixToken(@NotNull String string, @Nullable Long number) {
this.string = string;
this.number = number;
}
private SuffixToken(@NotNull String string) {
this(string, null);
}
@Override
public int compareTo(@NotNull SuffixToken other) {
if (number != null && other.number != null) {
int result = number.compareTo(other.number);
if (0 != result) return result;
}
return string.compareTo(other.string);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof SuffixToken)) return false;
SuffixToken that = (SuffixToken) o;
return Objects.equals(string, that.string)
&& Objects.equals(number, that.number);
}
@Override
public int hashCode() {
return Objects.hash(string, number);
}
@Override
public String toString() {
return SuffixToken.class.getSimpleName() + '{'
+ "string='" + string + '\''
+ ", number=" + number
+ '}';
}
}
}