Alternative Version implementation

Bryan Atsatt bryan.atsatt at oracle.com
Wed Mar 23 23:36:00 UTC 2016


FWIW: I wrote the attached Version type some time back for a previous 
module system effort. It assumes module system specific parsing, into a 
canonical internal form, and supports string conversions using a 
canonical format. Ordering is well defined.

// Bryan


On 3/23/16 2:04 PM, David M. Lloyd wrote:
> I guess you mean more like this:
>
>  1
>  1.0
>  1.00
>  1.000
>  1.1
>  1.01
>  1.001
>  1.10
>  1.010
>  1.11
>  1.011
>  1.100
>  1.101
>  1.110
>  1.111
>
> where the numeric value is most significant and otherwise the sort is 
> by length ascending?
>
> On 03/23/2016 03:57 PM, Paul Benedict wrote:
>> David, I accidentally missed something first time around. I am happy
>> Neil pointed it out (thank you). This is actually the order I was
>> expecting. I am treating numbers as numbers, with more precision being
>> greater in the collation.
>>
>>   1
>>   1.0
>>   1.00
>>   1.000
>>   1.01
>>   1.001
>>   1.010
>>   1.011
>>   1.100
>>   1.101
>>   1.110
>>   1.111
>>   1.1
>>   1.10
>>   1.11
>>
>>
>> Cheers,
>> Paul
>>
>> On Wed, Mar 23, 2016 at 3:44 PM, Neil Bartlett (Paremus)
>> <neil.bartlett at paremus.com <mailto:neil.bartlett at paremus.com>> wrote:
>>
>>     Thanks David, I should have awaited your reply before responding to
>>     Paul. See that email for more specifics on OSGi versions.
>>
>>     I wasn’t implying anything about agreement with Remi Forax or
>>     anybody else. In this case, my question was really just a question.
>>     However I am skeptical about the practical feasibility of creating a
>>     version scheme that can bring together all the existing practices.
>>
>>     I am also very concerned with your the suggested collation order in
>>     which 1.1 sorts before 1.00. This is subjective of course, but I
>>     find it highly counter-intuitive. If the segment looks like a number
>>     then it should act like a number, unless all segments are explicitly
>>     defined as strings with alphanumeric sorting.
>>
>>
>>     Neil
>>
>>
>>     > On 23 Mar 2016, at 20:23, David M. Lloyd 
>> <david.lloyd at redhat.com <mailto:david.lloyd at redhat.com>> wrote:
>>     >
>>     > The OSGi specification allows (from what I can tell) arbitrary 
>> strings for the last segment and that is definitely incompatible with 
>> the notion of these more general version rules... this is the only 
>> difference I can find though, and many practical examples of OSGi 
>> versions should continue to work, which at least yields the 
>> possibility of moving forward.  Are there additional scenarios you 
>> can identify?
>>     >
>>     > Maven on the other hand does not really have a specification, 
>> so I just referred to of existing examples and they seem to function 
>> as expected.
>>     >
>>     > The work I'm doing is intended as something of a bridge between 
>> what is in place now (a structure which is designed for use in the 
>> JDK and which implies a strict syntactical and semantic format which 
>> is incompatible with a very large number of existing schemes and 
>> version numbers) and a way to allow each layer to impose its own 
>> policy.  But I think what you are implying is that you share my 
>> interpretation of Rémi Forax's opinion that a plain string is a 
>> better version identifier, with no sorting/comparison or validation 
>> logic, putting 100% of the responsibility for interpretation and 
>> validation of the version string to the layer which defines the 
>> module.  Is my understanding of your position correct?
>>     >
>>     > On 03/23/2016 03:01 PM, Neil Bartlett (Paremus) wrote:
>>     >> Hi Paul and David,
>>     >>
>>     >> You may consider this collation order intuitive, but it’s 
>> clearly incompatible with existing version systems; in particular I’m 
>> thinking of those used in OSGi and Maven.
>>     >>
>>     >> I really don’t know to what extent this matters, as it was my 
>> understanding that JSR 376 would not define versioning of modules and 
>> that this are would be left to the discretion of external tools such 
>> as build systems. David can you explain the work you are doing in 
>> this context?
>>     >>
>>     >> Regards,
>>     >> Neil
>>     >>
>>     >>
>>     >>> On 23 Mar 2016, at 18:53, Paul Benedict <pbenedict at apache.org 
>> <mailto:pbenedict at apache.org>> wrote:
>>     >>>
>>     >>> For any of the EG members observing this list,
>>     >>>
>>     >>> I find David's collating order acceptable and expected. I am 
>> not privy to
>>     >>> Reiner's particular discussion, but it is my opinion that 1.0 
>> should
>>     >>> precede 1.0.0. Although both are numerically equal, one is 
>> more precise --
>>     >>> ambiguity should be first, precision last. I don't find this 
>> to be any
>>     >>> different than the alphanumerical nature of a phone book 
>> where A would
>>     >>> precede AA. That's not a perfect analogy but it gets my point 
>> across.
>>     >>>
>>     >>> Cheers,
>>     >>> Paul
>>     >>>
>>     >>> On Wed, Mar 23, 2016 at 1:46 PM, David M. Lloyd 
>> <david.lloyd at redhat.com <mailto:david.lloyd at redhat.com>>
>>     >>> wrote:
>>     >>>
>>     >>>> On 03/23/2016 09:20 AM, David M. Lloyd wrote:
>>     >>>>
>>     >>>>> I've gone ahead and written a new Version implementation 
>> that implements
>>     >>>>> the rules I've described.  It seems to work OK though I am 
>> having a hard
>>     >>>>> time running all tests locally due to some environmental 
>> problem that
>>     >>>>> I'm still working on, so I don't have a webrev yet.  But I 
>> do have a
>>     >>>>> diff that can be examined (and commented upon) at [1].
>>     >>>>>
>>     >>>>
>>     >>>> One oddity that springs up relating to numeric versions when 
>> not
>>     >>>> normalizing the version string in any way is that version 
>> segments leading
>>     >>>> zeros parse and sort strangely.  After fiddling around with 
>> various
>>     >>>> approaches, currently I've settled on this order:
>>     >>>>
>>     >>>> 1
>>     >>>> 1.0
>>     >>>> 1.1
>>     >>>> 1.00
>>     >>>> 1.01
>>     >>>> 1.10
>>     >>>> 1.11
>>     >>>> 1.000
>>     >>>> 1.001
>>     >>>> 1.010
>>     >>>> 1.011
>>     >>>> 1.100
>>     >>>> 1.101
>>     >>>> 1.110
>>     >>>> 1.111
>>     >>>>
>>     >>>> Wherein versions are sorted for length first, then for 
>> value.  However
>>     >>>> that might be counter-intuitive if your expectation is that 
>> (for example)
>>     >>>> 1.0 is equal to 1.00 or at least sorts immediately before or 
>> after it.  A
>>     >>>> good case could be made that versions should be normalized 
>> to strip leading
>>     >>>> zeros, and I believe the previous implementation did this 
>> (either
>>     >>>> intentionally or unintentionally) as an implementation 
>> side-effect.  The
>>     >>>> downside of normalization is the extra work and extra String 
>> being produced
>>     >>>> as a result.
>>     >>>>
>>     >>>> A third option would be to reject version segments with 
>> leading zeros,
>>     >>>> which prevents the problem from coming up and also avoids 
>> the extra copy
>>     >>>> work, making the "number" production look like:
>>     >>>>
>>     >>>>   number = ? Unicode decimal digit with values 1-9 ? { ? 
>> Unicode decimal
>>     >>>> digit ? }
>>     >>>>
>>     >>>> Any thoughts on this would be appreciated.
>>     >>>> --
>>     >>>> - DML
>>     >>>>
>>     >>
>>     >
>>     > --
>>     > - DML
>>
>>
>

-------------- next part --------------
/*
 * Copyright 2009 Oracle Corporation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package java.lang.module;

import java.util.regex.Pattern;

/**
 * A canonical, syntax agnostic version number type that supports ordered comparison.
 * <p/>
 * Instances may contain any number of positive numeric components and an optional
 * 'preview' or 'update' string qualifier. An unqualified instance is considered a
 * 'release' version. Qualifier strings may contain any of the posix visible characters
 * (i.e. '\u0021' through '\u006E').
 * <p/>
 * During comparison:
 * <ul>
 * <li>Numeric components of differing lengths are normalized by zero padding.</li>
 * <li>Types are compared only if numeric components are equal: PREVIEW < RELEASE < UPDATE.</li>
 * <li>Qualifiers are compared,
 * <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/lang/String.html#compareTo(java.lang.String)">
 * lexicographically</a>, only if types and all numeric components are equal.</li>
 * </ul>
 * Lexicographic qualifier comparison can result in unexpected ordering (e.g. "100" < "20");
 * therefore, version schemes should be mapped to use additional numeric components where
 * possible.
 *
 * @author Bryan Atsatt
 * @since Dec 10, 2008
 */
public class Version implements Matcher<Version>, Comparable<Version> {
    private static final Pattern DOT_SEP = Pattern.compile("\\.");

    public enum Type {

        /**
         * A PREVIEW version may have a qualifier and is less than a RELEASE
         * or UPDATE version with equal components.
         */
        PREVIEW,

        /**
         * A RELEASE version has no qualifier and is greater than a PREVIEW
         * version with equal components and less than an UPDATE version with
         * equal components.
         */
        RELEASE,

        /**
         * An UPDATE version may have a qualifier and is greater than a RELEASE
         * version with equal components.
         */
        UPDATE,
    }

    /**
     * An instance of the minimum value.
     */
    public static final Version MIN_VALUE = new Version(true) {
        public int compareTo(Version other) {
            return other == this ? 0 : -1;
        }

        public String toString() {
            return "min";
        }
    };

    /**
     * An instance of the maximum value.
     */
    public static final Version MAX_VALUE = new Version(false) {
        public int compareTo(Version other) {
            return other == this ? 0 : 1;
        }

        public String toString() {
            return "max";
        }
    };

    /**
     * An instance of the release version zero.
     */
    public static Version ZERO = newReleaseVersion(0);

    private static final Pattern visibleCharacters = Pattern.compile("([\\p{Graph}])+");

    private Type type;
    private int[] components;
    private String qualifier;

    /**
     * Create a new PREVIEW version.
     *
     * @param components The components.
     * @param qualifier  The qualifier.
     * @return The new instance.
     */
    public static Version newPreviewVersion(int[] components, String qualifier) {
        return new Version(Type.PREVIEW, components, qualifier);
    }

    /**
     * Create a new RELEASE version.
     *
     * @param components The components.
     * @return The new instance.
     */
    public static Version newReleaseVersion(int... components) {
        return new Version(Type.RELEASE, components, null);
    }

    /**
     * Create a new UPDATE version.
     *
     * @param components The components.
     * @param qualifier  The qualifier.
     * @return The new instance.
     */
    public static Version newUpdateVersion(int[] components, String qualifier) {
        return new Version(Type.UPDATE, components, qualifier);
    }

    /**
     * Create a new version.
     *
     * @param type       The type.
     * @param components The components.
     * @param qualifier  The qualifier. Must be null if type == RELEASE, otherwise
     *                   must be non-zero length.
     * @return The new instance.
     */
    public static Version newVersion(Type type, int[] components, String qualifier) {
        return new Version(type, components, qualifier);
    }

    /**
     * Returns the type of this instance.
     *
     * @return The type.
     */
    public Type getType() {
        return type;
    }

    /**
     * Returns the number of components in this instance.
     *
     * @return The count.
     */
    public int getComponentCount() {
        return components.length;
    }

    /**
     * Return the component at the specified index.
     *
     * @param index The index.
     * @return The component.
     * @throws IndexOutOfBoundsException if index is out of bounds.
     */
    public int getComponentAt(int index) {
        return components[index];
    }

    /**
     * Returns the qualifier, if any.
     *
     * @return The qualifier if type is PREVIEW or UPDATE, null if RELEASE;
     */
    public String getQualifier() {
        return qualifier;
    }

    /**
     * Compare two {@code Version} objects.
     *
     * @param version the {@code Version} to be compared.
     * @return the value 0 if the this {@code Version} is equal to the
     *         {@code Version} argument; a value less than 0 if this
     *         {@code Version} is less than the {@code Version} argument; and a
     *         value greater than 0 if this {@code Version} is greater than the
     *         {@code Version} argument.
     */
    public int compareTo(Version version) {
        int result = 0;
        if (this != version) {
            if (version == MAX_VALUE) {
                result = -1;
            } else if (version == MIN_VALUE) {
                result = 1;
            } else {

                // Are the components equal?

                result = compare(components, version.components);
                if (result == 0) {

                    // Yes. Are the types equal?

                    result = type.ordinal() - version.type.ordinal();
                    if (result == 0 && type != Type.RELEASE) {

                        // Yes, and there is a qualifier, so the result depends
                        // solely on comparing them...

                        result = qualifier.compareTo(version.qualifier);
                    }
                }
            }
        }
        return result;
    }

    /**
     * Compare two {@code Version} objects for equality. The result is
     * {@code true} if and only if the argument is not {@code null} and is a
     * {@code Version} object for which compareTo() returns 0.
     *
     * @param obj the object to compare with.
     * @return whether or not two {@code Version} objects are equal.
     */
    @Override
    public boolean equals(Object obj) {
        return this == obj || (obj instanceof Version && (compareTo((Version) obj) == 0));
    }

    /**
     * Returns a hash code for this {@code Version}.
     *
     * @return a hash code value for this {@code Version}.
     */
    @Override
    public int hashCode() {
        int result = 17 * (type.ordinal() + 1);
        for (int n : components) {
            result = 37 * result + n;
        }
        result = 37 * result + (qualifier == null ? 0 : qualifier.hashCode());
        return result;
    }

    /**
     * Returns a {@code String} object representing this {@code Version}'s
     * value.
     *
     * @return a string representation of the value of this {@code Version}.
     */
    @Override
    public String toString() {
        return toString(" preview '", " update '", "'");
    }

    /**
     * Returns a {@code String} object representing this {@code Version}'s
     * value, in canonical form:
     * <pre>
     *     digit[.digit]... [ -<previewQualifier> | _<updateQualifier> ]
     * </pre>
     *
     * @return a string representation of the value of this {@code Version}, in
     *         canonical form.
     */
    public String toCanonicalForm() {
        if (this == MIN_VALUE) return "min";
        if (this == MAX_VALUE) return "max";
        return toString("-", "_", null);
    }

    /**
     * Returns a {@code Version} object from the specified canonical
     * version string.
     *
     * @param version The canonical version string.
     * @return The {@code Version}.
     * @see #toCanonicalForm()
     */
    public static Version fromCanonicalForm(String version) {
        if (version.equals("min")) return MIN_VALUE;
        if (version.equals("max")) return MAX_VALUE;
        int pi = version.indexOf("-");
        int ui = version.indexOf("_");
        if (pi > 0 && ui > 0) {
            if (pi < ui) {
                ui = -1;
            } else {
                pi = -1;
            }
        }
        if (pi > 0) {
            int[] components = toComponents(version.substring(0, pi));
            return newPreviewVersion(components, version.substring(pi + 1));
        } else if (ui > 0) {
            int[] components = toComponents(version.substring(0, ui));
            return newUpdateVersion(components, version.substring(ui + 1));
        }
        return newReleaseVersion(toComponents(version));
    }

    /**
     * Tests if the specified version is equal to this instance.
     *
     * @param version The target version.
     * @return {@code true} if the specified version is equal to this instance;
     *         {@code false} otherwise.
     */
    public boolean matches(Version version) {
        return equals(version);
    }

    private String toString(String preview, String update, String trailing) {
        StringBuilder buf = new StringBuilder();
        int length = components.length;
        for (int i = 0; i < length; i++) {
            if (i > 0) {
                buf.append('.');
            }
            buf.append(components[i]);
        }
        if (length == 1) {
            buf.append(".0");
        }
        if (type != Type.RELEASE) {
            if (type == Type.PREVIEW) {
                buf.append(preview);
            } else {
                buf.append(update);
            }
            buf.append(qualifier);
            if (trailing != null) {
                buf.append(trailing);
            }
        }
        return buf.toString();
    }

    private Version(Type type, int[] components, String qualifier) {
        assert (type != null);
        this.type = type;
        this.components = components;
        this.qualifier = qualifier;
        validateComponents();
        validateQualifier();
    }

    private Version(boolean minValue) { // Special MIN/MAX value ctor.
        if (minValue) {
            type = Type.PREVIEW;
            components = new int[]{0};
            qualifier = "!";
        } else {
            type = Type.UPDATE;
            components = new int[]{Integer.MAX_VALUE};
            qualifier = "~";
        }
    }

    private static int compare(int[] right, int[] left) {
        int rLen = right.length;
        int lLen = left.length;
        int len = Math.max(rLen, lLen);
        for (int i = 0; i < len; i++) {
            int rValue = i < rLen ? right[i] : 0;
            int lValue = i < lLen ? left[i] : 0;
            int result = rValue - lValue;
            if (result != 0) {
                return result;
            }
        }
        return 0;
    }

    private void validateComponents() {
        if (components == null) {
            throw new IllegalArgumentException("Version components must not be null.");
        }
        if (components.length == 0) {
            throw new IllegalArgumentException("Version components must not be empty.");
        }
        for (int c : components) {
            if (c < 0) {
                throw new IllegalArgumentException("Version components must not be negative: " + this);
            }
        }
    }

    private void validateQualifier() {
        if (type == Type.RELEASE) {
            if (qualifier != null) {
                throw new IllegalArgumentException("Version qualifier must be null.");
            }
        } else {
            if (qualifier == null) {
                throw new IllegalArgumentException("Version qualifier must not be null.");
            }
            if (qualifier.length() == 0) {
                throw new IllegalArgumentException("Version qualifier must not be empty: " + this);
            }
            if (!visibleCharacters.matcher(qualifier).matches()) {
                throw new IllegalArgumentException(
                        "Version qualifier must contain only posix visible characters: " + this);
            }
        }
    }

    private static int[] toComponents(String componentStr) {
        String[] componentStrings = DOT_SEP.split(componentStr);
        int[] components = new int[componentStrings.length];
        for (int i = 0; i < components.length; i++) {
            components[i] = Integer.parseInt(componentStrings[i].trim());
        }
        return components;
    }
}


More information about the jpms-spec-observers mailing list