diff --git a/version/src/main/java/io/smallrye/common/version/Messages.java b/version/src/main/java/io/smallrye/common/version/Messages.java index 533c2b9..c31a8a6 100644 --- a/version/src/main/java/io/smallrye/common/version/Messages.java +++ b/version/src/main/java/io/smallrye/common/version/Messages.java @@ -48,15 +48,22 @@ interface Messages { @Message(id = 3011, value = "Unbounded range: %s") IllegalArgumentException unboundedRange(String pattern); - @Message(id = 3012, value = "Ranges overlap: %s") - IllegalArgumentException rangesOverlap(String version); + // 3012 - @Message(id = 3013, value = "Only fully-qualified sets allowed in multiple set scenario: %s") - IllegalArgumentException onlyFullyQualifiedSetsAllowed(String version); + // 3013 @Message(id = 3014, value = "Single version must be surrounded by []: %s") IllegalArgumentException singleVersionMustBeSurroundedByBrackets(String version); @Message(id = 3015, value = "Range defies version ordering: %s") IllegalArgumentException rangeDefiesVersionOrdering(String version); + + @Message(id = 3016, value = "Unexpected version range character: %s") + IllegalArgumentException rangeUnexpected(String version); + + @Message(id = 3017, value = "Standalone version cannot have an upper bound") + IllegalArgumentException standaloneVersionCannotBeBound(); + + @Message(id = 3018, value = "Inclusive versions cannot be empty") + IllegalArgumentException inclusiveVersionCannotBeEmpty(); } diff --git a/version/src/main/java/io/smallrye/common/version/VersionRange.java b/version/src/main/java/io/smallrye/common/version/VersionRange.java deleted file mode 100644 index f9c98e3..0000000 --- a/version/src/main/java/io/smallrye/common/version/VersionRange.java +++ /dev/null @@ -1,103 +0,0 @@ -package io.smallrye.common.version; - -import static io.smallrye.common.version.Messages.msg; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Predicate; - -/** - * A {@link VersionRange} is a predicate that tests if a version string is within a specified range. - */ -public class VersionRange implements Predicate { - - private final List restrictions; - - VersionRange(List restrictions) { - this.restrictions = restrictions; - } - - @Override - public boolean test(String s) { - for (VersionRestriction restriction : restrictions) { - if (restriction.test(s)) { - return true; - } - } - return false; - } - - /** - *

- * Create a version range from a string representation - *

- * Some spec examples are: - *
    - *
  • 1.0 Version 1.0 as a recommended version
  • - *
  • [1.0] Version 1.0 explicitly only
  • - *
  • [1.0,2.0) Versions 1.0 (included) to 2.0 (not included)
  • - *
  • [1.0,2.0] Versions 1.0 to 2.0 (both included)
  • - *
  • [1.5,) Versions 1.5 and higher
  • - *
  • (,1.0],[1.2,) Versions up to 1.0 (included) and 1.2 or higher
  • - *
- * - * @param spec string representation of a version or version range - * @return a new {@link VersionRange} object that represents the spec - * @return null if the spec is null - */ - public static VersionRange createFromVersionSpec(VersionScheme scheme, String spec) { - if (spec == null) { - return null; - } - - List restrictions = new ArrayList<>(); - String process = spec; - String upperBound = null; - String lowerBound = null; - - while (process.startsWith("[") || process.startsWith("(")) { - int index1 = process.indexOf(')'); - int index2 = process.indexOf(']'); - - int index = index2; - if (index2 < 0 || index1 < index2) { - if (index1 >= 0) { - index = index1; - } - } - - if (index < 0) { - throw msg.unboundedRange(spec); - } - - VersionRestriction restriction = VersionRestriction.parse(scheme, process.substring(0, index + 1)); - if (lowerBound == null) { - lowerBound = restriction.getLowerBound(); - } - if (upperBound != null) { - if (restriction.getLowerBound() == null - || scheme.lt(restriction.getLowerBound(), upperBound)) { - throw msg.rangesOverlap(spec); - } - } - restrictions.add(restriction); - upperBound = restriction.getUpperBound(); - - process = process.substring(index + 1).trim(); - - if (process.startsWith(",")) { - process = process.substring(1).trim(); - } - } - - if (!process.isEmpty()) { - if (!restrictions.isEmpty()) { - throw msg.onlyFullyQualifiedSetsAllowed(spec); - } else { - restrictions.add(VersionRestriction.EVERYTHING); - } - } - - return new VersionRange(restrictions); - } -} diff --git a/version/src/main/java/io/smallrye/common/version/VersionRestriction.java b/version/src/main/java/io/smallrye/common/version/VersionRestriction.java deleted file mode 100644 index 112b71c..0000000 --- a/version/src/main/java/io/smallrye/common/version/VersionRestriction.java +++ /dev/null @@ -1,91 +0,0 @@ -package io.smallrye.common.version; - -import static io.smallrye.common.version.Messages.msg; - -import java.util.function.Predicate; - -class VersionRestriction implements Predicate { - - public static final VersionRestriction EVERYTHING = new VersionRestriction(null, null, false, null, false); - - private final VersionScheme versionScheme; - private final String lowerBound; - private final boolean lowerVersionInclusive; - private final String upperBound; - private final boolean upperBoundInclusive; - - VersionRestriction(VersionScheme versionScheme, String lowerBound, boolean lowerVersionInclusive, String upperBound, - boolean upperBoundInclusive) { - this.versionScheme = versionScheme; - this.lowerBound = lowerBound; - this.lowerVersionInclusive = lowerVersionInclusive; - this.upperBound = upperBound; - this.upperBoundInclusive = upperBoundInclusive; - } - - public String getLowerBound() { - return lowerBound; - } - - public String getUpperBound() { - return upperBound; - } - - @Override - public boolean test(String s) { - if (lowerBound != null) { - int comparison = versionScheme.compare(s, lowerBound); - if (comparison < 0 || (!lowerVersionInclusive && comparison == 0)) { - return false; - } - } - if (upperBound != null) { - int comparison = versionScheme.compare(s, upperBound); - if (comparison > 0 || (!upperBoundInclusive && comparison == 0)) { - return false; - } - } - return true; - } - - static VersionRestriction parse(VersionScheme versionScheme, String spec) { - boolean lowerBoundInclusive = spec.startsWith("["); - boolean upperBoundInclusive = spec.endsWith("]"); - - String process = spec.substring(1, spec.length() - 1).trim(); - - final VersionRestriction restriction; - - int index = process.indexOf(','); - - if (index < 0) { - if (!lowerBoundInclusive || !upperBoundInclusive) { - throw msg.singleVersionMustBeSurroundedByBrackets(spec); - } - restriction = new VersionRestriction(versionScheme, process, true, process, true); - } else { - String lowerBound = process.substring(0, index).trim(); - String upperBound = process.substring(index + 1).trim(); - - String lowerVersion = null; - String upperVersion = null; - - if (!lowerBound.isEmpty()) { - lowerVersion = lowerBound; - } - if (!upperBound.isEmpty()) { - upperVersion = upperBound; - } - - if (upperVersion != null && lowerVersion != null) { - int result = versionScheme.compare(upperVersion, lowerVersion); - if (result < 0 || (result == 0 && (!lowerBoundInclusive || !upperBoundInclusive))) { - throw msg.rangeDefiesVersionOrdering(spec); - } - } - restriction = new VersionRestriction(versionScheme, lowerVersion, lowerBoundInclusive, upperVersion, - upperBoundInclusive); - } - return restriction; - } -} diff --git a/version/src/main/java/io/smallrye/common/version/VersionScheme.java b/version/src/main/java/io/smallrye/common/version/VersionScheme.java index 8010f55..a66bfa4 100644 --- a/version/src/main/java/io/smallrye/common/version/VersionScheme.java +++ b/version/src/main/java/io/smallrye/common/version/VersionScheme.java @@ -1,6 +1,7 @@ package io.smallrye.common.version; import java.util.Comparator; +import java.util.Objects; import java.util.function.Predicate; /** @@ -62,6 +63,26 @@ default boolean ge(String base, String other) { return compare(base, other) >= 0; } + /** + * {@return the lesser (earlier) of the two versions} + * + * @param a the first version (must not be {@code null}) + * @param b the second version (must not be {@code null}) + */ + default String min(String a, String b) { + return le(a, b) ? a : b; + } + + /** + * {@return the greater (later) of the two versions} + * + * @param a the first version (must not be {@code null}) + * @param b the second version (must not be {@code null}) + */ + default String max(String a, String b) { + return ge(a, b) ? a : b; + } + /** * Returns a predicate that tests if the version is equal to the base version. * @@ -112,6 +133,65 @@ default Predicate whenLt(String other) { return base -> lt(base, other); } + /** + * Parse a range specification and return it as a predicate. + * This method behaves as a call to {@link #fromRangeString(String, int, int) fromRangeString(range, 0, range.length())}. + * + * @param range the range string to parse (must not be {@code null}) + * @return the parsed range (not {@code null}) + * @throws IllegalArgumentException if there is a syntax error in the range or the range cannot match any version + */ + default Predicate fromRangeString(String range) { + return fromRangeString(range, 0, range.length()); + } + + /** + * Parse a range specification and return it as a predicate. + * Version ranges are governed by the following general syntax: + *
+range ::= range-spec ',' range
+        | range-spec
+
+range-spec ::= '[' version ']
+             | min-version ',' max-version
+
+min-version ::= '[' version
+              | '(' version
+              | '('
+
+max-version ::= version ']'
+              | version ')'
+              | ')'
+
+ * This is aligned with the syntax used by Maven, however it can be applied to any + * supported version scheme. + *

+ * It is important to note that within a range specification, the {@code ,} separator + * indicates a logical "and" or "intersection" operation, whereas the {@code ,} separator + * found in between range specifications acts as a logical "or" or "union" operation. + *

+ * Here are some examples of valid version range specifications: + *

    + *
  • 1.0 Version 1.0 as a recommended version (like {@code whenEquals("1.0")})
  • + *
  • [1.0] Version 1.0 explicitly only (like {@code whenEquals("1.0")})
  • + *
  • [1.0,2.0) Versions 1.0 (included) to 2.0 (not included) (like {@code whenGe("1.0").and(whenLt("2.0"))})
  • + *
  • [1.0,2.0] Versions 1.0 to 2.0 (both included) (like {@code whenGe("1.0").and(whenLe("2.0"))})
  • + *
  • [1.5,) Versions 1.5 and higher (like {@code whenGe("1.5")})
  • + *
  • (,1.0],[1.2,) Versions up to 1.0 (included) and 1.2 or higher (like {@code whenLe("1.0").or(whenGe("1.2"))})
  • + *
+ * + * @param range the range string to parse (must not be {@code null}) + * @param start the start of the range within the string (inclusive) + * @param end the end of the range within the string (exclusive) + * @return the parsed range (not {@code null}) + * @throws IllegalArgumentException if there is a syntax error in the range or the range cannot match any version + * @throws IndexOutOfBoundsException if the values for {@code start} or {@code end} are not valid + */ + default Predicate fromRangeString(String range, int start, int end) { + Objects.checkFromToIndex(start, end, range.length()); + return parseRange(range, start, end); + } + /** * Determine if two versions are equal according to this version scheme. * @@ -226,4 +306,156 @@ default void validate(String version) throws VersionSyntaxException { * This versioning scheme is based approximately on semantic versioning but with a few differences. */ VersionScheme JPMS = new JpmsVersionScheme(); + + private Predicate parseRange(final String range, int start, final int end) { + if (start == end) { + return whenEquals(""); + } + int cp = range.codePointAt(start); + int cnt = Character.charCount(cp); + switch (cp) { + case '[': { + return parseMinIncl(range, start + cnt, end); + } + case '(': { + return parseMinExcl(range, start + cnt, end); + } + case ',': { + return parseMore(whenEquals(""), range, start + cnt, end); + } + default: { + return parseSingle(range, start + cnt, end); + } + } + } + + private Predicate parseSingle(String range, int start, int end) { + int i = start; + int cp, cnt; + do { + cp = range.codePointAt(i); + cnt = Character.charCount(cp); + switch (cp) { + case ',': { + return parseMore(whenEquals(range.substring(start, i)), range, i + cnt, end); + } + case ']': + case ')': { + throw Messages.msg.standaloneVersionCannotBeBound(); + } + } + i += cnt; + } while (i < end); + // just a single version + return whenEquals(range.substring(start, end)); + } + + private Predicate parseMinIncl(String range, int start, int end) { + int i = start; + int cp; + do { + cp = range.codePointAt(i); + int cnt = Character.charCount(cp); + switch (cp) { + case ',': { + if (i == start) { + throw Messages.msg.inclusiveVersionCannotBeEmpty(); + } + return parseRangeMax(whenGe(range.substring(start, i)), range, i + cnt, end); + } + case ']': { + return parseMore(whenEquals(range.substring(start, i)), range, i + cnt, end); + } + case ')': { + throw Messages.msg.singleVersionMustBeSurroundedByBrackets(range.substring(start, i + cnt)); + } + } + i += cnt; + } while (i < end); + // ended short, so treat it as open-ended + return whenGe(range.substring(start, end)); + } + + private Predicate parseMinExcl(String range, int start, int end) { + int i = start; + int cp; + do { + cp = range.codePointAt(i); + int cnt = Character.charCount(cp); + switch (cp) { + case ',': { + if (i == start) { + // include all + return parseRangeMax(null, range, i + cnt, end); + } else { + return parseRangeMax(whenGt(range.substring(start, i)), range, i + cnt, end); + } + } + case ']': + case ')': { + throw Messages.msg.singleVersionMustBeSurroundedByBrackets(range.substring(start, i + cnt)); + } + } + i += cnt; + } while (i < end); + // ended short, so treat it as open-ended + return whenGt(range.substring(start, end)); + } + + private Predicate parseRangeMax(Predicate min, String range, int start, int end) { + int i = start; + int cp; + do { + cp = range.codePointAt(i); + int cnt = Character.charCount(cp); + switch (cp) { + case ')': { + if (i == start) { + // empty upper range; only consider the minimum range + return parseMore(min, range, i + cnt, end); + } + // fall through + } + case ']': { + String high = range.substring(start, i); + if (min != null && ! min.test(high)) { + // low end must be higher than high end + throw Messages.msg.rangeDefiesVersionOrdering(range.substring(start, i + cnt)); + } + Predicate max = cp == ']' ? whenLe(high) : whenLt(high); + return parseMore(min == null ? max : min.and(max), range, i + cnt, end); + } + case ',': { + throw Messages.msg.rangeUnexpected(range.substring(start, i + cnt)); + } + } + i += cnt; + } while (i < end); + // ended short + throw Messages.msg.unboundedRange(range.substring(start, end)); + } + + /** + * Parse the end context (make sure there is no trailing garbage, combine subsequent predicates). + * + * @param predicate the predicate to return + * @param range the range string + * @param start the remaining start + * @param end the end + * @return the predicate + */ + private Predicate parseMore(Predicate predicate, final String range, int start, int end) { + if (start < end) { + int cp = range.codePointAt(start); + int cnt = Character.charCount(cp); + if (cp == ',') { + // composed version ranges + Predicate nextRange = parseRange(range, start + cnt, end); + return predicate == null ? nextRange : predicate.or(nextRange); + } + throw Messages.msg.rangeUnexpected(range.substring(start, start + cnt)); + } else { + return predicate; + } + } } diff --git a/version/src/test/java/io/smallrye/common/version/VersionRangeTest.java b/version/src/test/java/io/smallrye/common/version/VersionRangeTest.java index f9b0e1a..d989d8d 100644 --- a/version/src/test/java/io/smallrye/common/version/VersionRangeTest.java +++ b/version/src/test/java/io/smallrye/common/version/VersionRangeTest.java @@ -3,6 +3,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import java.util.function.Predicate; + import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -12,7 +14,7 @@ class VersionRangeTest { @Test void testVersionRangeWithInclusive() { - VersionRange versionRange = VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "[1.0,2.0]"); + Predicate versionRange = VersionScheme.MAVEN.fromRangeString("[1.0,2.0]"); assertThat(versionRange) .accepts("1.0.0", "1.1.0", "1.899.0", "2.0", "2.0.0") .rejects("2.0.1"); @@ -20,7 +22,7 @@ void testVersionRangeWithInclusive() { @Test void testVersionRangeWithExclusive() { - VersionRange versionRange = VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "(1.0,2.0)"); + Predicate versionRange = VersionScheme.MAVEN.fromRangeString("(1.0,2.0)"); assertThat(versionRange) .accepts("1.1.0", "1.899.0") .rejects("1.0.0", "2.0", "2.0.0", "2.0.1"); @@ -28,7 +30,7 @@ void testVersionRangeWithExclusive() { @Test void testVersionRangeWithLowerBoundExclusive() { - VersionRange versionRange = VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "(1.0,2.0]"); + Predicate versionRange = VersionScheme.MAVEN.fromRangeString("(1.0,2.0]"); assertThat(versionRange) .accepts("1.1.0", "1.899.0", "2.0", "2.0.0") .rejects("1.0.0", "2.0.1"); @@ -36,7 +38,7 @@ void testVersionRangeWithLowerBoundExclusive() { @Test void testVersionRangeWithUpperBoundExclusive() { - VersionRange versionRange = VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "[1.0,2.0)"); + Predicate versionRange = VersionScheme.MAVEN.fromRangeString("[1.0,2.0)"); assertThat(versionRange) .accepts("1.0.0", "1.1.0", "1.899.0") .rejects("2.0", "2.0.0", "2.0.1"); @@ -45,25 +47,25 @@ void testVersionRangeWithUpperBoundExclusive() { @Test public void testUnboundedRange() { assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "[1.0,2.0")) + .isThrownBy(() -> VersionScheme.MAVEN.fromRangeString("[1.0,2.0")) .withMessageStartingWith("SRCOM03011"); } @Test public void testAlphaVersion() { - VersionRange versionRange = VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "[1.0,)"); + Predicate versionRange = VersionScheme.MAVEN.fromRangeString("[1.0,)"); assertThat(versionRange).rejects("1.0.0.Alpha1"); } @Test public void testAlphaVersionInbound() { - VersionRange versionRange = VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "[1.0.0.Alpha1,)"); + Predicate versionRange = VersionScheme.MAVEN.fromRangeString("[1.0.0.Alpha1,)"); assertThat(versionRange).accepts("1.0.0.Alpha1", "1.0.0.Beta1"); } @Test public void testAlphaVersionInboundExclusive() { - VersionRange versionRange = VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "(1.0.0.Alpha1,)"); + Predicate versionRange = VersionScheme.MAVEN.fromRangeString("(1.0.0.Alpha1,)"); assertThat(versionRange) .accepts("1.0.0.Beta") .rejects("1.0.0.Alpha1"); @@ -71,7 +73,7 @@ public void testAlphaVersionInboundExclusive() { @Test public void testMultipleRanges() { - VersionRange versionRange = VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "(,1.0],[1.2,)"); + Predicate versionRange = VersionScheme.MAVEN.fromRangeString("(,1.0],[1.2,)"); // Should return true for Versions up to 1.0 (included) and 1.2 or higher assertThat(versionRange) .accepts("1.0.0.Alpha1", "1.0.0", "1.2.0") @@ -80,7 +82,7 @@ public void testMultipleRanges() { @Test public void testQualifiers() { - VersionRange versionRange = VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "[3.8,3.8.5)"); + Predicate versionRange = VersionScheme.MAVEN.fromRangeString("[3.8,3.8.5)"); assertThat(versionRange) .accepts("3.8.4.SP1-redhat-00001", "3.8.4.SP2-redhat-00001", "3.8.4.redhat-00002") .rejects("3.8.5.redhat-00003"); @@ -89,7 +91,7 @@ public void testQualifiers() { @Test @Disabled("This test is failing") public void testRangeQualifier() { - VersionRange versionRange = VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "[3.8.0.redhat-00001,)"); + Predicate versionRange = VersionScheme.MAVEN.fromRangeString("[3.8.0.redhat-00001,)"); assertThat(versionRange).accepts("3.8.0.SP1-redhat-00001"); }