/* * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package jdk; import java.math.BigInteger; import java.util.ArrayList; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.Collections; import java.util.List; import java.util.Optional; import sun.security.action.GetPropertyAction; /** * A representation of the JDK version-string which contains a version * number optionally followed by pre-release and build information. * *

Version numbers

* * A version number, {@code $VNUM}, is a non-empty sequence of * non-negative integer numerals, without leading or trailing zeroes, * separated by period characters (U+002E); i.e., it matches the regular * expression {@code ^[1-9][0-9]*(((\.0)*\.[1-9][0-9]*)*)*$}. The sequence may * be of arbitrary length but the first three elements are assigned specific * meanings, as follows: * *
 *     $MAJOR.$MINOR.$SECURITY
 * 
* * * *

The fourth and later elements of a version number are free for use by * downstream consumers of the JDK code base. Such a consumer may, * e.g., use the fourth element to identify patch releases which * contain a small number of critical non-security fixes in addition to the * security fixes in the corresponding security release.

* *

The version number does not include trailing zero elements; * i.e., {@code $SECURITY} is omitted if it has the value zero, and * {@code $MINOR} is omitted if both {@code $MINOR} and {@code $SECURITY} have * the value zero.

* *

The sequence of numerals in a version number is compared to another * such sequence in numerical, pointwise fashion; e.g., {@code 9.9.1} * is less than {@code 9.10.0}. If one sequence is shorter than another then * the missing elements of the shorter sequence are considered to be zero; * e.g., {@code 9.1.2} is equal to {@code 9.1.2.0} but less than * {@code 9.1.2.1}.

* *

Version strings

* *

A version string {@code $VSTR} consists of a version number * {@code $VNUM}, as described above, optionally followed by pre-release and * build information, in the format

* *
 *     $VNUM(-$PRE)?(\+($BUILD)?(-$OPT)?)?
 * 
* *

where:

* * * *

When comparing two version strings the value of {@code $OPT}, if * present, may or may not be significant depending on the chosen comparison * method. The comparison methods {@link #compareTo(Version) compareTo()} and * {@link #compareToIgnoreOpt(Version) compareToIgnoreOpt{}} should be used * consistently with the corresponding methods {@link #equals(Object) equals()} * and {@link #equalsIgnoreOpt(Object) equalsIgnoreOpt()}.

* *

A short version string ({@code $SVSTR}), often useful in less * formal contexts, is simply {@code $VNUM} optionally ended with {@code * -$PRE}.

* * @since 9 */ public final class Version implements Comparable { private final List version; private final Optional pre; private final Optional build; private final Optional optional; private static Version current; // $VNUM(-$PRE)?(\+($BUILD)?(\-$OPT)?)? // RE limits the format of version strings // ([1-9][0-9]*(?:(?:\.0)*\.[1-9][0-9]*)*)(?:-([a-zA-Z0-9]+))?(?:(\+)(0|[1-9][0-9]*)?)?(?:-([-a-zA-Z0-9.]+))? private static final String VNUM = "(?[1-9][0-9]*(?:(?:\\.0)*\\.[1-9][0-9]*)*)"; private static final String VNUM_GROUP = "VNUM"; private static final String PRE = "(?:-(?
[a-zA-Z0-9]+))?";
    private static final String PRE_GROUP   = "PRE";

    private static final String BUILD
        = "(?:(?\\+)(?0|[1-9][0-9]*)?)?";
    private static final String PLUS_GROUP  = "PLUS";
    private static final String BUILD_GROUP = "BUILD";

    private static final String OPT      = "(?:-(?[-a-zA-Z0-9.]+))?";
    private static final String OPT_GROUP   = "OPT";

    private static final String VSTR_FORMAT
        = "^" + VNUM + PRE + BUILD + OPT + "$";
    private static final Pattern VSTR_PATTERN = Pattern.compile(VSTR_FORMAT);

    /**
     * Constructs a valid JDK version string containing a
     * version number followed by pre-release and build
     * information.
     *
     * @param  s
     *         A string to be interpreted as a version
     *
     * @throws  IllegalArgumentException
     *          If the given string cannot be interpreted a valid version
     *
     * @throws  NullPointerException
     *          If {@code s} is {@code null}
     *
     * @throws  NumberFormatException
     *          If an element of the version number or the build number cannot
     *          be represented as an {@link Integer}
     */
    private Version(String s) {
        if (s == null)
            throw new NullPointerException();

        Matcher m = VSTR_PATTERN.matcher(s);
        if (!m.matches())
            throw new IllegalArgumentException("Invalid version string: '"
                                               + s + "'");

        // $VNUM is a dot-separated list of integers of arbitrary length
        List list = new ArrayList<>();
        for (String i : m.group(VNUM_GROUP).split("\\."))
            list.add(Integer.parseInt(i));
        version = Collections.unmodifiableList(list);

        pre = Optional.ofNullable(m.group(PRE_GROUP));

        String b = m.group(BUILD_GROUP);
        // $BUILD is an integer
        build = (b == null)
             ? Optional.empty()
             : Optional.ofNullable(Integer.parseInt(b));

        optional = Optional.ofNullable(m.group(OPT_GROUP));

        // empty '+'
        if ((m.group(PLUS_GROUP) != null) && !build.isPresent()) {
            if (optional.isPresent()) {
                if (pre.isPresent())
                    throw new IllegalArgumentException("'+' found with"
                        + " pre-release and optional components:'" + s + "'");
            } else {
                throw new IllegalArgumentException("'+' found with neither"
                    + " build or optional components: '" + s + "'");
            }
        }
    }

    /**
     * Parses the given string as a valid JDK version string containing a version number followed by pre-release and
     * build information.
     *
     * @param  s
     *         A string to interpret as a version
     *
     * @throws  IllegalArgumentException
     *          If the given string cannot be interpreted a valid version
     *
     * @throws  NullPointerException
     *          If the given string is {@code null}
     *
     * @throws  NumberFormatException
     *          If an element of the version number or the build number cannot
     *          be represented as an {@link Integer}
     *
     * @return  This version
     */
    public static Version parse(String s) {
        return new Version(s);
    }

    /**
     * Returns {@code System.getProperty("java.version")} as a Version.
     *
     * @throws  SecurityException
     *          If a security manager exists and its {@link
     *          SecurityManager#checkPropertyAccess(String)
     *          checkPropertyAccess} method does not allow access to the
     *          system property "java.version"
     *
     * @return  {@code System.getProperty("java.version")} as a Version
     */
    public static Version current() {
        if (current == null) {
            current = parse(GetPropertyAction.getProperty("java.version"));
        }
        return current;
    }

    /**
     * Returns the major version number.
     *
     * @return  The major version number
     */
    public int major() {
        return version.get(0);
    }

    /**
     * Returns the minor version number or zero if it was
     * not set.
     *
     * @return  The minor version number or zero if it was not set
     */
    public int minor() {
        return (version.size() > 1 ? version.get(1) : 0);
    }

    /**
     * Returns the security version number or zero if
     * it was not set.
     *
     * @return  The security version number or zero if it was not set
     */
    public int security() {
        return (version.size() > 2 ? version.get(2) : 0);
    }

    /**
     * Returns an unmodifiable {@link java.util.List List} of the
     * integer numerals contained in the version
     * number.  The {@code List} always contains at least one
     * element corresponding to the major version
     * number.
     *
     * @return  An unmodifiable list of the integer numerals
     *          contained in the version number
     */
    public List version() {
        return version;
    }

    /**
     * Returns the optional pre-release information.
     *
     * @return  The optional pre-release information as a String
     */
    public Optional pre() {
        return pre;
    }

    /**
     * Returns the build number.
     *
     * @return The optional build number.
     */
    public Optional build() {
        return build;
    }

    /**
     * Returns optional additional identifying build
     * information.
     *
     * @return  Additional build information as a String
     */
    public Optional optional() {
        return optional;
    }

    /**
     * Compares this version to another.
     *
     * 

Each of the components in the version is * compared in the follow order of precedence: version numbers, * pre-release identifiers, build numbers, optional build information.

* *

Comparison begins by examining the sequence of version numbers. If * one sequence is shorter than another, then the missing elements of the * shorter sequence are considered to be zero.

* *

A version with a pre-release identifier is always considered to be * less than a version without one. Pre-release identifiers are compared * numerically when they consist only of digits, and lexicographically * otherwise. Numeric identifiers are considered to be less than * non-numeric identifiers.

* *

A version without a build number is always less than one with a * build number; otherwise build numbers are compared numerically.

* *

The optional build information is compared lexicographically. * During this comparison, a version with optional build information is * considered to be greater than a version without one.

* *

A version is not comparable to any other type of object. * * @param ob * The object to be compared * * @return A negative integer, zero, or a positive integer if this * {@code Version} is less than, equal to, or greater than the * given {@code Version} * * @throws NullPointerException * If the given object is {@code null} */ @Override public int compareTo(Version ob) { return compare(ob, false); } /** * Compares this version to another disregarding optional build * information. * *

Two versions are compared by examining the version string as * described in {@link #compareTo(Version)} with the exception that the * optional build information is always ignored.

* *

A version is not comparable to any other type of object. * * @param ob * The object to be compared * * @return A negative integer, zero, or a positive integer if this * {@code Version} is less than, equal to, or greater than the * given {@code Version} * * @throws NullPointerException * If the given object is {@code null} */ public int compareToIgnoreOpt(Version ob) { return compare(ob, true); } private int compare(Version ob, boolean ignoreOpt) { if (ob == null) throw new NullPointerException("Invalid argument"); int ret = compareVersion(ob); if (ret != 0) return ret; ret = comparePre(ob); if (ret != 0) return ret; ret = compareBuild(ob); if (ret != 0) return ret; if (!ignoreOpt) return compareOpt(ob); return 0; } private int compareVersion(Version ob) { int size = version.size(); int oSize = ob.version().size(); int min = Math.min(size, oSize); for (int i = 0; i < min; i++) { Integer val = version.get(i); Integer oVal = ob.version().get(i); if (val != oVal) return val - oVal; } if (size != oSize) return size - oSize; return 0; } private int comparePre(Version ob) { Optional oPre = ob.pre(); if (!pre.isPresent()) { if (oPre.isPresent()) return 1; } else { if (!oPre.isPresent()) return -1; String val = pre.getWhenPresent(); String oVal = oPre.getWhenPresent(); if (val.matches("\\d+")) { return (oVal.matches("\\d+") ? (new BigInteger(val)).compareTo(new BigInteger(oVal)) : -1); } else { return (oVal.matches("\\d+") ? 1 : val.compareTo(oVal)); } } return 0; } private int compareBuild(Version ob) { Optional oBuild = ob.build(); if (oBuild.isPresent()) { return (build.isPresent() ? build.getWhenPresent().compareTo(oBuild.getWhenPresent()) : 1); } else if (build.isPresent()) { return -1; } return 0; } private int compareOpt(Version ob) { Optional oOpt = ob.optional(); if (!optional.isPresent()) { if (oOpt.isPresent()) return -1; } else { if (!oOpt.isPresent()) return 1; return optional.getWhenPresent().compareTo(oOpt.getWhenPresent()); } return 0; } /** * Returns a string representation of this version. * * @return The version string */ @Override public String toString() { StringBuilder sb = new StringBuilder(version.stream() .map(Object::toString) .collect(Collectors.joining("."))); pre.ifPresent(v -> sb.append("-").append(v)); if (build.isPresent()) { sb.append("+").append(build.getWhenPresent()); if (optional.isPresent()) sb.append("-").append(optional.getWhenPresent()); } else { if (optional.isPresent()) { sb.append(pre.isPresent() ? "-" : "+-"); sb.append(optional.getWhenPresent()); } } return sb.toString(); } /** * Determines whether this {@code Version} is equal to another object. * *

Two {@code Version}s are equal if and only if they represent the * same version string. * *

This method satisfies the general contract of the {@link * Object#equals(Object) Object.equals} method.

* * @param ob * The object to which this {@code Version} is to be compared * * @return {@code true} if, and only if, the given object is a {@code * Version} that is identical to this {@code Version} * */ @Override public boolean equals(Object ob) { boolean ret = equalsIgnoreOpt(ob); if (!ret) return false; Version that = (Version)ob; return (this.optional().equals(that.optional())); } /** * Determines whether this {@code Version} is equal to another * disregarding optional build information. * *

Two {@code Version}s are equal if and only if they represent the * same version string disregarding the optional build information. * * @param ob * The object to which this {@code Version} is to be compared * * @return {@code true} if, and only if, the given object is a {@code * Version} that is identical to this {@code Version} * ignoring the optinal build information * */ public boolean equalsIgnoreOpt(Object ob) { if (this == ob) return true; if (!(ob instanceof Version)) return false; Version that = (Version)ob; return (this.version().equals(that.version()) && this.pre().equals(that.pre()) && this.build().equals(that.build())); } /** * Returns the hash code of this version. * *

This method satisfies the general contract of the {@link * Object#hashCode Object.hashCode} method. * * @return The hashcode of this version */ @Override public int hashCode() { int h = 1; int p = 17; h = p * h + version.hashCode(); h = p * h + pre.hashCode(); h = p * h + build.hashCode(); h = p * h + optional.hashCode(); return h; } }