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

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();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy