diff --git a/core/src/main/java/org/springframework/security/core/ComparableVersion.java b/core/src/main/java/org/springframework/security/core/ComparableVersion.java index bce13b7ac1..7e01b042aa 100644 --- a/core/src/main/java/org/springframework/security/core/ComparableVersion.java +++ b/core/src/main/java/org/springframework/security/core/ComparableVersion.java @@ -17,27 +17,26 @@ package org.springframework.security.core; import java.math.BigInteger; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; +import java.util.Deque; import java.util.Iterator; import java.util.List; -import java.util.ListIterator; import java.util.Locale; import java.util.Properties; -import java.util.Stack; /** + *

* Generic implementation of version comparison. + *

* * NOTE: This is a copy from - * https://svn.apache.org/repos/asf/maven/maven-3/tags/maven-3.1.0 - * /maven-artifact/src/main/ - * java/org/apache/maven/artifact/versioning/ComparableVersion.java + * https://github.com/apache/maven/blob/maven-3.6.3/maven-artifact/src/main/java/org/apache/maven/artifact/versioning/ComparableVersion.java * - *

* Features: *

* Unknown qualifiers are considered after known qualifiers, with lexical order (always * case insensitive), - *
  • a dash usually precedes a qualifier, and is always less important than something + *
  • a hyphen usually precedes a qualifier, and is always less important than something * preceded with a dot.
  • * - *

    * - * @author Kenney Westerhof - * @author Hervé Boutemy * @see "Versioning" on Maven * Wiki + * @author Kenney Westerhof + * @author Hervé Boutemy */ class ComparableVersion implements Comparable { + private static final int MAX_INTITEM_LENGTH = 9; + + private static final int MAX_LONGITEM_LENGTH = 18; + private String value; private String canonical; @@ -76,7 +78,11 @@ class ComparableVersion implements Comparable { private interface Item { - int INTEGER_ITEM = 0; + int INT_ITEM = 3; + + int LONG_ITEM = 4; + + int BIGINTEGER_ITEM = 0; int STRING_ITEM = 1; @@ -91,43 +97,194 @@ class ComparableVersion implements Comparable { } /** - * Represents a numeric item in the version item list. + * Represents a numeric item in the version item list that can be represented with an + * int. */ - private static class IntegerItem implements Item { + private static class IntItem implements Item { - private static final BigInteger BigInteger_ZERO = new BigInteger("0"); + private final int value; - private final BigInteger value; + public static final IntItem ZERO = new IntItem(); + + private IntItem() { + this.value = 0; + } + + IntItem(String str) { + this.value = Integer.parseInt(str); + } + + @Override + public int getType() { + return INT_ITEM; + } + + @Override + public boolean isNull() { + return value == 0; + } + + @Override + public int compareTo(Item item) { + if (item == null) { + return (value == 0) ? 0 : 1; // 1.0 == 1, 1.1 > 1 + } + + switch (item.getType()) { + case INT_ITEM: + int itemValue = ((IntItem) item).value; + return (value < itemValue) ? -1 : ((value == itemValue) ? 0 : 1); + case LONG_ITEM: + case BIGINTEGER_ITEM: + return -1; + + case STRING_ITEM: + return 1; // 1.1 > 1-sp + + case LIST_ITEM: + return 1; // 1.1 > 1-1 + + default: + throw new IllegalStateException("invalid item: " + item.getClass()); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + IntItem intItem = (IntItem) o; + + return value == intItem.value; + + } + + @Override + public int hashCode() { + return value; + } + + @Override + public String toString() { + return Integer.toString(value); + } + + } + + /** + * Represents a numeric item in the version item list that can be represented with a + * long. + */ + private static class LongItem implements Item { + + private final long value; + + LongItem(String str) { + this.value = Long.parseLong(str); + } + + @Override + public int getType() { + return LONG_ITEM; + } + + @Override + public boolean isNull() { + return value == 0; + } + + @Override + public int compareTo(Item item) { + if (item == null) { + return (value == 0) ? 0 : 1; // 1.0 == 1, 1.1 > 1 + } + + switch (item.getType()) { + case INT_ITEM: + return 1; + case LONG_ITEM: + long itemValue = ((LongItem) item).value; + return (value < itemValue) ? -1 : ((value == itemValue) ? 0 : 1); + case BIGINTEGER_ITEM: + return -1; + + case STRING_ITEM: + return 1; // 1.1 > 1-sp - public static final IntegerItem ZERO = new IntegerItem(); + case LIST_ITEM: + return 1; // 1.1 > 1-1 + + default: + throw new IllegalStateException("invalid item: " + item.getClass()); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } - private IntegerItem() { - this.value = BigInteger_ZERO; + LongItem longItem = (LongItem) o; + + return value == longItem.value; + + } + + @Override + public int hashCode() { + return (int) (value ^ (value >>> 32)); } - IntegerItem(String str) { + @Override + public String toString() { + return Long.toString(value); + } + + } + + /** + * Represents a numeric item in the version item list. + */ + private static class BigIntegerItem implements Item { + + private final BigInteger value; + + BigIntegerItem(String str) { this.value = new BigInteger(str); } @Override public int getType() { - return INTEGER_ITEM; + return BIGINTEGER_ITEM; } @Override public boolean isNull() { - return BigInteger_ZERO.equals(this.value); + return BigInteger.ZERO.equals(value); } @Override public int compareTo(Item item) { if (item == null) { - return BigInteger_ZERO.equals(this.value) ? 0 : 1; // 1.0 == 1, 1.1 > 1 + return BigInteger.ZERO.equals(value) ? 0 : 1; // 1.0 == 1, 1.1 > 1 } switch (item.getType()) { - case INTEGER_ITEM: - return this.value.compareTo(((IntegerItem) item).value); + case INT_ITEM: + case LONG_ITEM: + return 1; + + case BIGINTEGER_ITEM: + return value.compareTo(((BigIntegerItem) item).value); case STRING_ITEM: return 1; // 1.1 > 1-sp @@ -136,13 +293,32 @@ class ComparableVersion implements Comparable { return 1; // 1.1 > 1-1 default: - throw new RuntimeException("invalid item: " + item.getClass()); + throw new IllegalStateException("invalid item: " + item.getClass()); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; } + + BigIntegerItem that = (BigIntegerItem) o; + + return value.equals(that.value); + } @Override + public int hashCode() { + return value.hashCode(); + } + public String toString() { - return this.value.toString(); + return value.toString(); } } @@ -152,14 +328,14 @@ class ComparableVersion implements Comparable { */ private static class StringItem implements Item { - private static final String[] QUALIFIERS = { "alpha", "beta", "milestone", "rc", "snapshot", "", "sp" }; - - private static final List _QUALIFIERS = Arrays.asList(QUALIFIERS); + private static final List QUALIFIERS = Arrays.asList("alpha", "beta", "milestone", "rc", "snapshot", "", + "sp"); private static final Properties ALIASES = new Properties(); static { ALIASES.put("ga", ""); ALIASES.put("final", ""); + ALIASES.put("release", ""); ALIASES.put("cr", "rc"); } @@ -168,9 +344,9 @@ class ComparableVersion implements Comparable { * determine if a given qualifier makes the version older than one without a * qualifier, or more recent. */ - private static final String RELEASE_VERSION_INDEX = String.valueOf(_QUALIFIERS.indexOf("")); + private static final String RELEASE_VERSION_INDEX = String.valueOf(QUALIFIERS.indexOf("")); - private String value; + private final String value; StringItem(String value, boolean followedByDigit) { if (followedByDigit && value.length() == 1) { @@ -185,6 +361,7 @@ class ComparableVersion implements Comparable { case 'm': value = "milestone"; break; + default: } } this.value = ALIASES.getProperty(value, value); @@ -197,7 +374,7 @@ class ComparableVersion implements Comparable { @Override public boolean isNull() { - return (comparableQualifier(this.value).compareTo(RELEASE_VERSION_INDEX) == 0); + return (comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX) == 0); } /** @@ -214,35 +391,56 @@ class ComparableVersion implements Comparable { * @return an equivalent value that can be used with lexical comparison */ public static String comparableQualifier(String qualifier) { - int i = _QUALIFIERS.indexOf(qualifier); + int i = QUALIFIERS.indexOf(qualifier); - return i == -1 ? (_QUALIFIERS.size() + "-" + qualifier) : String.valueOf(i); + return i == -1 ? (QUALIFIERS.size() + "-" + qualifier) : String.valueOf(i); } @Override public int compareTo(Item item) { if (item == null) { // 1-rc < 1, 1-ga > 1 - return comparableQualifier(this.value).compareTo(RELEASE_VERSION_INDEX); + return comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX); } switch (item.getType()) { - case INTEGER_ITEM: + case INT_ITEM: + case LONG_ITEM: + case BIGINTEGER_ITEM: return -1; // 1.any < 1.1 ? case STRING_ITEM: - return comparableQualifier(this.value).compareTo(comparableQualifier(((StringItem) item).value)); + return comparableQualifier(value).compareTo(comparableQualifier(((StringItem) item).value)); case LIST_ITEM: return -1; // 1.any < 1-1 default: - throw new RuntimeException("invalid item: " + item.getClass()); + throw new IllegalStateException("invalid item: " + item.getClass()); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; } + if (o == null || getClass() != o.getClass()) { + return false; + } + + StringItem that = (StringItem) o; + + return value.equals(that.value); + } @Override + public int hashCode() { + return value.hashCode(); + } + public String toString() { - return this.value; + return value; } } @@ -264,12 +462,14 @@ class ComparableVersion implements Comparable { } void normalize() { - for (ListIterator iterator = listIterator(size()); iterator.hasPrevious();) { - Item item = iterator.previous(); - if (item.isNull()) { - iterator.remove(); // remove null trailing items: 0, "", empty list + for (int i = size() - 1; i >= 0; i--) { + Item lastItem = get(i); + + if (lastItem.isNull()) { + // remove null trailing items: 0, "", empty list + remove(i); } - else { + else if (!(lastItem instanceof ListItem)) { break; } } @@ -285,7 +485,9 @@ class ComparableVersion implements Comparable { return first.compareTo(null); } switch (item.getType()) { - case INTEGER_ITEM: + case INT_ITEM: + case LONG_ITEM: + case BIGINTEGER_ITEM: return -1; // 1-1 < 1.0.x case STRING_ITEM: @@ -300,7 +502,7 @@ class ComparableVersion implements Comparable { Item r = right.hasNext() ? right.next() : null; // if this is shorter, then invert the compare and mul with -1 - int result = l == null ? -1 * r.compareTo(l) : l.compareTo(r); + int result = l == null ? (r == null ? 0 : -1 * r.compareTo(l)) : l.compareTo(r); if (result != 0) { return result; @@ -310,39 +512,39 @@ class ComparableVersion implements Comparable { return 0; default: - throw new RuntimeException("invalid item: " + item.getClass()); + throw new IllegalStateException("invalid item: " + item.getClass()); } } @Override public String toString() { - StringBuilder buffer = new StringBuilder("("); - for (Iterator iter = iterator(); iter.hasNext();) { - buffer.append(iter.next()); - if (iter.hasNext()) { - buffer.append(','); + StringBuilder buffer = new StringBuilder(); + for (Item item : this) { + if (buffer.length() > 0) { + buffer.append((item instanceof ListItem) ? '-' : '.'); } + buffer.append(item); } - buffer.append(')'); return buffer.toString(); } } - ComparableVersion(String version) { + public ComparableVersion(String version) { parseVersion(version); } + @SuppressWarnings("checkstyle:innerassignment") public final void parseVersion(String version) { this.value = version; - this.items = new ListItem(); + items = new ListItem(); version = version.toLowerCase(Locale.ENGLISH); - ListItem list = this.items; + ListItem list = items; - Stack stack = new Stack<>(); + Deque stack = new ArrayDeque<>(); stack.push(list); boolean isDigit = false; @@ -354,7 +556,7 @@ class ComparableVersion implements Comparable { if (c == '.') { if (i == startIndex) { - list.add(IntegerItem.ZERO); + list.add(IntItem.ZERO); } else { list.add(parseItem(isDigit, version.substring(startIndex, i))); @@ -363,30 +565,23 @@ class ComparableVersion implements Comparable { } else if (c == '-') { if (i == startIndex) { - list.add(IntegerItem.ZERO); + list.add(IntItem.ZERO); } else { list.add(parseItem(isDigit, version.substring(startIndex, i))); } startIndex = i + 1; - if (isDigit) { - list.normalize(); // 1.0-* = 1-* - - if ((i + 1 < version.length()) && Character.isDigit(version.charAt(i + 1))) { - // new ListItem only if previous were digits and new char is a - // digit, - // ie need to differentiate only 1.1 from 1-1 - list.add(list = new ListItem()); - - stack.push(list); - } - } + list.add(list = new ListItem()); + stack.push(list); } else if (Character.isDigit(c)) { if (!isDigit && i > startIndex) { list.add(new StringItem(version.substring(startIndex, i), true)); startIndex = i; + + list.add(list = new ListItem()); + stack.push(list); } isDigit = true; @@ -395,6 +590,9 @@ class ComparableVersion implements Comparable { if (isDigit && i > startIndex) { list.add(parseItem(true, version.substring(startIndex, i))); startIndex = i; + + list.add(list = new ListItem()); + stack.push(list); } isDigit = false; @@ -409,32 +607,101 @@ class ComparableVersion implements Comparable { list = (ListItem) stack.pop(); list.normalize(); } - - this.canonical = this.items.toString(); } private static Item parseItem(boolean isDigit, String buf) { - return isDigit ? new IntegerItem(buf) : new StringItem(buf, false); + if (isDigit) { + buf = stripLeadingZeroes(buf); + if (buf.length() <= MAX_INTITEM_LENGTH) { + // lower than 2^31 + return new IntItem(buf); + } + else if (buf.length() <= MAX_LONGITEM_LENGTH) { + // lower than 2^63 + return new LongItem(buf); + } + return new BigIntegerItem(buf); + } + return new StringItem(buf, false); + } + + private static String stripLeadingZeroes(String buf) { + if (buf == null || buf.isEmpty()) { + return "0"; + } + for (int i = 0; i < buf.length(); ++i) { + char c = buf.charAt(i); + if (c != '0') { + return buf.substring(i); + } + } + return buf; } @Override public int compareTo(ComparableVersion o) { - return this.items.compareTo(o.items); + return items.compareTo(o.items); } @Override public String toString() { - return this.value; + return value; + } + + public String getCanonical() { + if (canonical == null) { + canonical = items.toString(); + } + return canonical; } @Override public boolean equals(Object o) { - return (o instanceof ComparableVersion) && this.canonical.equals(((ComparableVersion) o).canonical); + return (o instanceof ComparableVersion) && items.equals(((ComparableVersion) o).items); } @Override public int hashCode() { - return this.canonical.hashCode(); + return items.hashCode(); + } + + // CHECKSTYLE_OFF: LineLength + /** + * Main to test version parsing and comparison. + *

    + * To check how "1.2.7" compares to "1.2-SNAPSHOT", for example, you can issue + *

    java -jar ${maven.repo.local}/org/apache/maven/maven-artifact/${maven.version}/maven-artifact-${maven.version}.jar "1.2.7" "1.2-SNAPSHOT"
    + * command to command line. Result of given command will be something like this:
    +	 * Display parameters as parsed by Maven (in canonical form) and comparison result:
    +	 * 1. 1.2.7 == 1.2.7
    +	 *    1.2.7 > 1.2-SNAPSHOT
    +	 * 2. 1.2-SNAPSHOT == 1.2-snapshot
    +	 * 
    + * @param args the version strings to parse and compare. You can pass arbitrary number + * of version strings and always two adjacent will be compared + */ + // CHECKSTYLE_ON: LineLength + public static void main(String... args) { + System.out.println("Display parameters as parsed by Maven (in canonical form) and comparison result:"); + if (args.length == 0) { + return; + } + + ComparableVersion prev = null; + int i = 1; + for (String version : args) { + ComparableVersion c = new ComparableVersion(version); + + if (prev != null) { + int compare = prev.compareTo(c); + System.out.println(" " + prev.toString() + ' ' + ((compare == 0) ? "==" : ((compare < 0) ? "<" : ">")) + + ' ' + version); + } + + System.out.println(String.valueOf(i++) + ". " + version + " == " + c.getCanonical()); + + prev = c; + } } }