All Downloads are FREE. Search and download functionalities are using the official Maven repository.

name.remal.version.Version Maven / Gradle / Ivy

There is a newer version: 1.26.147
Show newest version
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
                + '}';
        }

    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy