From c79ec24ca5ca3fb5323ae8417f102c3d877f7a0e Mon Sep 17 00:00:00 2001 From: George Gastaldi Date: Wed, 17 Jul 2024 11:59:37 -0300 Subject: [PATCH 1/9] Introduce `VersionRange` This introduces a predicate that can be used for comparing if given a version is contained inside a version range pattern --- .../io/smallrye/common/version/Messages.java | 3 + .../smallrye/common/version/VersionRange.java | 69 +++++++++++++++++++ .../common/version/VersionRangeTest.java | 58 ++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 version/src/main/java/io/smallrye/common/version/VersionRange.java create mode 100644 version/src/test/java/io/smallrye/common/version/VersionRangeTest.java 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 893c2e82..e61c8fc5 100644 --- a/version/src/main/java/io/smallrye/common/version/Messages.java +++ b/version/src/main/java/io/smallrye/common/version/Messages.java @@ -44,4 +44,7 @@ interface Messages { @Message(id = 3010, value = "Build string may not be empty") VersionSyntaxException emptyBuild(); + + @Message(id = 3011, value = "Invalid range pattern: %s") + IllegalArgumentException invalidRangePattern(String pattern); } diff --git a/version/src/main/java/io/smallrye/common/version/VersionRange.java b/version/src/main/java/io/smallrye/common/version/VersionRange.java new file mode 100644 index 00000000..4ac8807b --- /dev/null +++ b/version/src/main/java/io/smallrye/common/version/VersionRange.java @@ -0,0 +1,69 @@ +package io.smallrye.common.version; + +import static io.smallrye.common.version.Messages.msg; + +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A {@link VersionRange} is a predicate that tests if a version string is within a specified range. + */ +public class VersionRange implements Predicate { + + /** + * Range in format "[1.0,2.0)" + */ + private static final Pattern RANGE_PATTERN = Pattern.compile("([\\[\\(])(.*),(.*)([\\]\\)])"); + + private final VersionScheme versionScheme; + private final Bound lowerBound; + private final Bound upperBound; + + public VersionRange(VersionScheme versionScheme, String rangePattern) { + this.versionScheme = versionScheme; + // Range pattern is in format "[1.0,2.0)" + Matcher matcher = RANGE_PATTERN.matcher(rangePattern); + if (!matcher.matches()) { + throw msg.invalidRangePattern(rangePattern); + } + if (matcher.group(2).isBlank()) { + this.lowerBound = null; + } else { + this.lowerBound = new Bound(matcher.group(2), matcher.group(1).charAt(0) == '['); + } + if (matcher.group(3).isBlank()) { + this.upperBound = null; + } else { + this.upperBound = new Bound(matcher.group(3), matcher.group(4).charAt(0) == ']'); + } + } + + @Override + public boolean test(String s) { + if (lowerBound != null) { + int comparison = versionScheme.compare(s, lowerBound.version); + if (comparison < 0 || (!lowerBound.inclusive && comparison == 0)) { + return false; + } + } + if (upperBound != null) { + int comparison = versionScheme.compare(s, upperBound.version); + if (comparison > 0 || (!upperBound.inclusive && comparison == 0)) { + return false; + } + } + return true; + } + + private class Bound { + + private final String version; + private final boolean inclusive; + + public Bound(String version, boolean inclusive) { + this.version = version; + this.inclusive = inclusive; + } + } +} diff --git a/version/src/test/java/io/smallrye/common/version/VersionRangeTest.java b/version/src/test/java/io/smallrye/common/version/VersionRangeTest.java new file mode 100644 index 00000000..ef794818 --- /dev/null +++ b/version/src/test/java/io/smallrye/common/version/VersionRangeTest.java @@ -0,0 +1,58 @@ +package io.smallrye.common.version; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class VersionRangeTest { + + @Test + void testVersionRangeWithInclusive() { + VersionRange versionRange = new VersionRange(VersionScheme.MAVEN, "[1.0,2.0]"); + assertTrue(versionRange.test("1.0.0")); + assertTrue(versionRange.test("1.1.0")); + assertTrue(versionRange.test("1.899.0")); + assertTrue(versionRange.test("2.0")); + assertTrue(versionRange.test("2.0.0")); + assertFalse(versionRange.test("2.0.1")); + } + + @Test + void testVersionRangeWithExclusive() { + VersionRange versionRange = new VersionRange(VersionScheme.MAVEN, "(1.0,2.0)"); + assertFalse(versionRange.test("1.0.0")); + assertTrue(versionRange.test("1.1.0")); + assertTrue(versionRange.test("1.899.0")); + assertFalse(versionRange.test("2.0")); + assertFalse(versionRange.test("2.0.0")); + assertFalse(versionRange.test("2.0.1")); + } + + @Test + void testVersionRangeWithLowerBoundExclusive() { + VersionRange versionRange = new VersionRange(VersionScheme.MAVEN, "(1.0,2.0]"); + assertFalse(versionRange.test("1.0.0")); + assertTrue(versionRange.test("1.1.0")); + assertTrue(versionRange.test("1.899.0")); + assertTrue(versionRange.test("2.0")); + assertTrue(versionRange.test("2.0.0")); + assertFalse(versionRange.test("2.0.1")); + } + + @Test + void testVersionRangeWithUpperBoundExclusive() { + VersionRange versionRange = new VersionRange(VersionScheme.MAVEN, "[1.0,2.0)"); + assertTrue(versionRange.test("1.0.0")); + assertTrue(versionRange.test("1.1.0")); + assertTrue(versionRange.test("1.899.0")); + assertFalse(versionRange.test("2.0")); + assertFalse(versionRange.test("2.0.0")); + assertFalse(versionRange.test("2.0.1")); + } + + @Test + public void testVersionRangeWithInvalidRangePattern() { + assertThrows(IllegalArgumentException.class, () -> new VersionRange(VersionScheme.MAVEN, "1.0,2.0"), + "Invalid range pattern: 1.0,2.0"); + } +} From e67d611234f6e33544b780c9a776d70c1b714b63 Mon Sep 17 00:00:00 2001 From: George Gastaldi Date: Wed, 17 Jul 2024 17:03:39 -0300 Subject: [PATCH 2/9] Borrowed code from maven-artifact --- .../smallrye/common/version/VersionRange.java | 134 ++++++++++++------ .../common/version/VersionRestriction.java | 89 ++++++++++++ .../common/version/VersionRangeTest.java | 13 +- 3 files changed, 184 insertions(+), 52 deletions(-) create mode 100644 version/src/main/java/io/smallrye/common/version/VersionRestriction.java diff --git a/version/src/main/java/io/smallrye/common/version/VersionRange.java b/version/src/main/java/io/smallrye/common/version/VersionRange.java index 4ac8807b..f4392fd4 100644 --- a/version/src/main/java/io/smallrye/common/version/VersionRange.java +++ b/version/src/main/java/io/smallrye/common/version/VersionRange.java @@ -1,69 +1,113 @@ package io.smallrye.common.version; -import static io.smallrye.common.version.Messages.msg; - +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; import java.util.function.Predicate; -import java.util.regex.Matcher; -import java.util.regex.Pattern; /** * A {@link VersionRange} is a predicate that tests if a version string is within a specified range. */ public class VersionRange implements Predicate { - /** - * Range in format "[1.0,2.0)" - */ - private static final Pattern RANGE_PATTERN = Pattern.compile("([\\[\\(])(.*),(.*)([\\]\\)])"); + private static final Map CACHE_SPEC = Collections.synchronizedMap(new WeakHashMap<>()); - private final VersionScheme versionScheme; - private final Bound lowerBound; - private final Bound upperBound; + private final List restrictions; - public VersionRange(VersionScheme versionScheme, String rangePattern) { - this.versionScheme = versionScheme; - // Range pattern is in format "[1.0,2.0)" - Matcher matcher = RANGE_PATTERN.matcher(rangePattern); - if (!matcher.matches()) { - throw msg.invalidRangePattern(rangePattern); - } - if (matcher.group(2).isBlank()) { - this.lowerBound = null; - } else { - this.lowerBound = new Bound(matcher.group(2), matcher.group(1).charAt(0) == '['); - } - if (matcher.group(3).isBlank()) { - this.upperBound = null; - } else { - this.upperBound = new Bound(matcher.group(3), matcher.group(4).charAt(0) == ']'); - } + VersionRange(List restrictions) { + this.restrictions = restrictions; } @Override public boolean test(String s) { - if (lowerBound != null) { - int comparison = versionScheme.compare(s, lowerBound.version); - if (comparison < 0 || (!lowerBound.inclusive && comparison == 0)) { - return false; + for (VersionRestriction restriction : restrictions) { + if (restriction.test(s)) { + return true; } } - if (upperBound != null) { - int comparison = versionScheme.compare(s, upperBound.version); - if (comparison > 0 || (!upperBound.inclusive && comparison == 0)) { - return false; - } - } - return true; + return false; } - private class Bound { + /** + *

+ * 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; + } + String cacheKey = scheme.getClass().getName() + "#" + spec; + VersionRange cached = CACHE_SPEC.get(cacheKey); + if (cached != null) { + return cached; + } + List restrictions = new ArrayList<>(); + String process = spec; + String upperBound = null; + String lowerBound = null; - private final String version; - private final boolean inclusive; + while (process.startsWith("[") || process.startsWith("(")) { + int index1 = process.indexOf(')'); + int index2 = process.indexOf(']'); - public Bound(String version, boolean inclusive) { - this.version = version; - this.inclusive = inclusive; + int index = index2; + if (index2 < 0 || index1 < index2) { + if (index1 >= 0) { + index = index1; + } + } + + if (index < 0) { + throw new IllegalArgumentException("Unbounded range: " + 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.compare(restriction.getLowerBound(), upperBound) < 0) { + throw new IllegalArgumentException("Ranges overlap: " + 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 new IllegalArgumentException( + "Only fully-qualified sets allowed in multiple set scenario: " + spec); + } else { + restrictions.add(VersionRestriction.EVERYTHING); + } } + + cached = new VersionRange(restrictions); + CACHE_SPEC.put(cacheKey, cached); + return cached; } } diff --git a/version/src/main/java/io/smallrye/common/version/VersionRestriction.java b/version/src/main/java/io/smallrye/common/version/VersionRestriction.java new file mode 100644 index 00000000..bb5e0638 --- /dev/null +++ b/version/src/main/java/io/smallrye/common/version/VersionRestriction.java @@ -0,0 +1,89 @@ +package io.smallrye.common.version; + +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 new IllegalArgumentException("Single version must be surrounded by []: " + 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 new IllegalArgumentException("Range defies version ordering: " + spec); + } + } + restriction = new VersionRestriction(versionScheme, lowerVersion, lowerBoundInclusive, upperVersion, + upperBoundInclusive); + } + return restriction; + } +} 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 ef794818..a0cf9960 100644 --- a/version/src/test/java/io/smallrye/common/version/VersionRangeTest.java +++ b/version/src/test/java/io/smallrye/common/version/VersionRangeTest.java @@ -8,7 +8,7 @@ class VersionRangeTest { @Test void testVersionRangeWithInclusive() { - VersionRange versionRange = new VersionRange(VersionScheme.MAVEN, "[1.0,2.0]"); + VersionRange versionRange = VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "[1.0,2.0]"); assertTrue(versionRange.test("1.0.0")); assertTrue(versionRange.test("1.1.0")); assertTrue(versionRange.test("1.899.0")); @@ -19,7 +19,7 @@ void testVersionRangeWithInclusive() { @Test void testVersionRangeWithExclusive() { - VersionRange versionRange = new VersionRange(VersionScheme.MAVEN, "(1.0,2.0)"); + VersionRange versionRange = VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "(1.0,2.0)"); assertFalse(versionRange.test("1.0.0")); assertTrue(versionRange.test("1.1.0")); assertTrue(versionRange.test("1.899.0")); @@ -30,7 +30,7 @@ void testVersionRangeWithExclusive() { @Test void testVersionRangeWithLowerBoundExclusive() { - VersionRange versionRange = new VersionRange(VersionScheme.MAVEN, "(1.0,2.0]"); + VersionRange versionRange = VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "(1.0,2.0]"); assertFalse(versionRange.test("1.0.0")); assertTrue(versionRange.test("1.1.0")); assertTrue(versionRange.test("1.899.0")); @@ -41,7 +41,7 @@ void testVersionRangeWithLowerBoundExclusive() { @Test void testVersionRangeWithUpperBoundExclusive() { - VersionRange versionRange = new VersionRange(VersionScheme.MAVEN, "[1.0,2.0)"); + VersionRange versionRange = VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "[1.0,2.0)"); assertTrue(versionRange.test("1.0.0")); assertTrue(versionRange.test("1.1.0")); assertTrue(versionRange.test("1.899.0")); @@ -51,8 +51,7 @@ void testVersionRangeWithUpperBoundExclusive() { } @Test - public void testVersionRangeWithInvalidRangePattern() { - assertThrows(IllegalArgumentException.class, () -> new VersionRange(VersionScheme.MAVEN, "1.0,2.0"), - "Invalid range pattern: 1.0,2.0"); + public void testUnboundedRange() { + assertThrows(IllegalArgumentException.class, () -> VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "[1.0,2.0")); } } From 4161db8d3acb29ed87a28a86c97a21d58bba1d87 Mon Sep 17 00:00:00 2001 From: George Gastaldi Date: Wed, 17 Jul 2024 17:09:00 -0300 Subject: [PATCH 3/9] Remove cache --- .../io/smallrye/common/version/VersionRange.java | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/version/src/main/java/io/smallrye/common/version/VersionRange.java b/version/src/main/java/io/smallrye/common/version/VersionRange.java index f4392fd4..5b93fb9a 100644 --- a/version/src/main/java/io/smallrye/common/version/VersionRange.java +++ b/version/src/main/java/io/smallrye/common/version/VersionRange.java @@ -1,10 +1,7 @@ package io.smallrye.common.version; import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import java.util.Map; -import java.util.WeakHashMap; import java.util.function.Predicate; /** @@ -12,8 +9,6 @@ */ public class VersionRange implements Predicate { - private static final Map CACHE_SPEC = Collections.synchronizedMap(new WeakHashMap<>()); - private final List restrictions; VersionRange(List restrictions) { @@ -52,11 +47,7 @@ public static VersionRange createFromVersionSpec(VersionScheme scheme, String sp if (spec == null) { return null; } - String cacheKey = scheme.getClass().getName() + "#" + spec; - VersionRange cached = CACHE_SPEC.get(cacheKey); - if (cached != null) { - return cached; - } + List restrictions = new ArrayList<>(); String process = spec; String upperBound = null; @@ -106,8 +97,6 @@ public static VersionRange createFromVersionSpec(VersionScheme scheme, String sp } } - cached = new VersionRange(restrictions); - CACHE_SPEC.put(cacheKey, cached); - return cached; + return new VersionRange(restrictions); } } From d88dbd1dd9a2f54f342c3c53180f6c6dccc9ec76 Mon Sep 17 00:00:00 2001 From: George Gastaldi Date: Thu, 18 Jul 2024 16:03:45 -0300 Subject: [PATCH 4/9] Add more tests --- .../common/version/VersionRangeTest.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) 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 a0cf9960..c547469b 100644 --- a/version/src/test/java/io/smallrye/common/version/VersionRangeTest.java +++ b/version/src/test/java/io/smallrye/common/version/VersionRangeTest.java @@ -54,4 +54,45 @@ void testVersionRangeWithUpperBoundExclusive() { public void testUnboundedRange() { assertThrows(IllegalArgumentException.class, () -> VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "[1.0,2.0")); } + + @Test + public void testAlphaVersion() { + VersionRange versionRange = VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "[1.0,)"); + assertFalse(versionRange.test("1.0.0.Alpha")); + } + + @Test + public void testAlphaVersionInbound() { + VersionRange versionRange = VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "[1.0.0.Alpha1,)"); + assertTrue(versionRange.test("1.0.0.Alpha1")); + assertTrue(versionRange.test("1.0.0.Beta")); + } + + @Test + public void testAlphaVersionInboundExclusive() { + VersionRange versionRange = VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "(1.0.0.Alpha1,)"); + assertFalse(versionRange.test("1.0.0.Alpha1")); + assertTrue(versionRange.test("1.0.0.Beta")); + } + + @Test + public void testMultipleRanges() { + VersionRange versionRange = VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "(,1.0],[1.2,)"); + // Should return true for Versions up to 1.0 (included) and 1.2 or higher + assertTrue(versionRange.test("1.0.0.Alpha1")); + assertTrue(versionRange.test("1.0.0")); + assertFalse(versionRange.test("1.1.0")); + assertTrue(versionRange.test("1.2.0")); + assertFalse(versionRange.test("1.2.0.Alpha1")); + } + + @Test + public void testQualifiers() { + VersionRange versionRange = VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "[3.8,3.8.5)"); + assertTrue(versionRange.test("3.8.4.SP1-redhat-00001")); + assertTrue(versionRange.test("3.8.4.SP2-redhat-00001")); + assertTrue(versionRange.test("3.8.4.redhat-00002")); + assertFalse(versionRange.test("3.8.5.redhat-00003")); + } + } From f0ac2ec8c2014a8058fb1d57e37fa3ffc24cee67 Mon Sep 17 00:00:00 2001 From: George Gastaldi Date: Thu, 18 Jul 2024 17:34:38 -0300 Subject: [PATCH 5/9] Using Messages in exceptions --- .../io/smallrye/common/version/Messages.java | 16 ++++++++++++++-- .../io/smallrye/common/version/VersionRange.java | 9 +++++---- .../common/version/VersionRestriction.java | 6 ++++-- .../common/version/VersionRangeTest.java | 8 ++++++++ 4 files changed, 31 insertions(+), 8 deletions(-) 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 e61c8fc5..533c2b93 100644 --- a/version/src/main/java/io/smallrye/common/version/Messages.java +++ b/version/src/main/java/io/smallrye/common/version/Messages.java @@ -45,6 +45,18 @@ interface Messages { @Message(id = 3010, value = "Build string may not be empty") VersionSyntaxException emptyBuild(); - @Message(id = 3011, value = "Invalid range pattern: %s") - IllegalArgumentException invalidRangePattern(String pattern); + @Message(id = 3011, value = "Unbounded range: %s") + IllegalArgumentException unboundedRange(String pattern); + + @Message(id = 3012, value = "Ranges overlap: %s") + IllegalArgumentException rangesOverlap(String version); + + @Message(id = 3013, value = "Only fully-qualified sets allowed in multiple set scenario: %s") + IllegalArgumentException onlyFullyQualifiedSetsAllowed(String version); + + @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); } diff --git a/version/src/main/java/io/smallrye/common/version/VersionRange.java b/version/src/main/java/io/smallrye/common/version/VersionRange.java index 5b93fb9a..195c9af5 100644 --- a/version/src/main/java/io/smallrye/common/version/VersionRange.java +++ b/version/src/main/java/io/smallrye/common/version/VersionRange.java @@ -1,5 +1,7 @@ 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; @@ -65,7 +67,7 @@ public static VersionRange createFromVersionSpec(VersionScheme scheme, String sp } if (index < 0) { - throw new IllegalArgumentException("Unbounded range: " + spec); + throw msg.unboundedRange(spec); } VersionRestriction restriction = VersionRestriction.parse(scheme, process.substring(0, index + 1)); @@ -75,7 +77,7 @@ public static VersionRange createFromVersionSpec(VersionScheme scheme, String sp if (upperBound != null) { if (restriction.getLowerBound() == null || scheme.compare(restriction.getLowerBound(), upperBound) < 0) { - throw new IllegalArgumentException("Ranges overlap: " + spec); + throw msg.rangesOverlap(spec); } } restrictions.add(restriction); @@ -90,8 +92,7 @@ public static VersionRange createFromVersionSpec(VersionScheme scheme, String sp if (!process.isEmpty()) { if (!restrictions.isEmpty()) { - throw new IllegalArgumentException( - "Only fully-qualified sets allowed in multiple set scenario: " + spec); + throw msg.onlyFullyQualifiedSetsAllowed(spec); } else { restrictions.add(VersionRestriction.EVERYTHING); } diff --git a/version/src/main/java/io/smallrye/common/version/VersionRestriction.java b/version/src/main/java/io/smallrye/common/version/VersionRestriction.java index bb5e0638..112b71c7 100644 --- a/version/src/main/java/io/smallrye/common/version/VersionRestriction.java +++ b/version/src/main/java/io/smallrye/common/version/VersionRestriction.java @@ -1,5 +1,7 @@ package io.smallrye.common.version; +import static io.smallrye.common.version.Messages.msg; + import java.util.function.Predicate; class VersionRestriction implements Predicate { @@ -58,7 +60,7 @@ static VersionRestriction parse(VersionScheme versionScheme, String spec) { if (index < 0) { if (!lowerBoundInclusive || !upperBoundInclusive) { - throw new IllegalArgumentException("Single version must be surrounded by []: " + spec); + throw msg.singleVersionMustBeSurroundedByBrackets(spec); } restriction = new VersionRestriction(versionScheme, process, true, process, true); } else { @@ -78,7 +80,7 @@ static VersionRestriction parse(VersionScheme versionScheme, String spec) { if (upperVersion != null && lowerVersion != null) { int result = versionScheme.compare(upperVersion, lowerVersion); if (result < 0 || (result == 0 && (!lowerBoundInclusive || !upperBoundInclusive))) { - throw new IllegalArgumentException("Range defies version ordering: " + spec); + throw msg.rangeDefiesVersionOrdering(spec); } } restriction = new VersionRestriction(versionScheme, lowerVersion, lowerBoundInclusive, upperVersion, 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 c547469b..57c433b2 100644 --- a/version/src/test/java/io/smallrye/common/version/VersionRangeTest.java +++ b/version/src/test/java/io/smallrye/common/version/VersionRangeTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; class VersionRangeTest { @@ -95,4 +96,11 @@ public void testQualifiers() { assertFalse(versionRange.test("3.8.5.redhat-00003")); } + @Test + @Disabled("This test is failing") + public void testRangeQualifier() { + VersionRange versionRange = VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "[3.8.0.redhat-00001,)"); + assertTrue(versionRange.test("3.8.0.SP1-redhat-00001")); + } + } From 2ac85cd613fd3df8062382cab2e172c808c08391 Mon Sep 17 00:00:00 2001 From: George Gastaldi Date: Thu, 18 Jul 2024 20:30:56 -0300 Subject: [PATCH 6/9] Using AssertJ for better readability --- version/pom.xml | 9 ++- .../common/version/VersionRangeTest.java | 70 ++++++++----------- 2 files changed, 37 insertions(+), 42 deletions(-) diff --git a/version/pom.xml b/version/pom.xml index 2628c114..1b34f10d 100644 --- a/version/pom.xml +++ b/version/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 @@ -39,6 +40,12 @@ junit-jupiter test + + org.assertj + assertj-core + test + + org.apache.maven.resolver 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 57c433b2..00fcf6f0 100644 --- a/version/src/test/java/io/smallrye/common/version/VersionRangeTest.java +++ b/version/src/test/java/io/smallrye/common/version/VersionRangeTest.java @@ -1,6 +1,7 @@ package io.smallrye.common.version; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -10,97 +11,84 @@ class VersionRangeTest { @Test void testVersionRangeWithInclusive() { VersionRange versionRange = VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "[1.0,2.0]"); - assertTrue(versionRange.test("1.0.0")); - assertTrue(versionRange.test("1.1.0")); - assertTrue(versionRange.test("1.899.0")); - assertTrue(versionRange.test("2.0")); - assertTrue(versionRange.test("2.0.0")); - assertFalse(versionRange.test("2.0.1")); + assertThat(versionRange) + .accepts("1.0.0", "1.1.0", "1.899.0", "2.0", "2.0.0") + .rejects("2.0.1"); } @Test void testVersionRangeWithExclusive() { VersionRange versionRange = VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "(1.0,2.0)"); - assertFalse(versionRange.test("1.0.0")); - assertTrue(versionRange.test("1.1.0")); - assertTrue(versionRange.test("1.899.0")); - assertFalse(versionRange.test("2.0")); - assertFalse(versionRange.test("2.0.0")); - assertFalse(versionRange.test("2.0.1")); + assertThat(versionRange) + .accepts("1.1.0", "1.899.0") + .rejects("1.0.0", "2.0", "2.0.0", "2.0.1"); } @Test void testVersionRangeWithLowerBoundExclusive() { VersionRange versionRange = VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "(1.0,2.0]"); - assertFalse(versionRange.test("1.0.0")); - assertTrue(versionRange.test("1.1.0")); - assertTrue(versionRange.test("1.899.0")); - assertTrue(versionRange.test("2.0")); - assertTrue(versionRange.test("2.0.0")); - assertFalse(versionRange.test("2.0.1")); + assertThat(versionRange) + .accepts("1.1.0", "1.899.0", "2.0", "2.0.0") + .rejects("1.0.0", "2.0.1"); } @Test void testVersionRangeWithUpperBoundExclusive() { VersionRange versionRange = VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "[1.0,2.0)"); - assertTrue(versionRange.test("1.0.0")); - assertTrue(versionRange.test("1.1.0")); - assertTrue(versionRange.test("1.899.0")); - assertFalse(versionRange.test("2.0")); - assertFalse(versionRange.test("2.0.0")); - assertFalse(versionRange.test("2.0.1")); + assertThat(versionRange) + .accepts("1.0.0", "1.1.0", "1.899.0") + .rejects("2.0", "2.0.0", "2.0.1"); } @Test public void testUnboundedRange() { - assertThrows(IllegalArgumentException.class, () -> VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "[1.0,2.0")); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "[1.0,2.0")) + .withMessageStartingWith("SRCOM03011"); } @Test public void testAlphaVersion() { VersionRange versionRange = VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "[1.0,)"); - assertFalse(versionRange.test("1.0.0.Alpha")); + assertThat(versionRange).rejects("1.0.0.Alpha1"); } @Test public void testAlphaVersionInbound() { VersionRange versionRange = VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "[1.0.0.Alpha1,)"); - assertTrue(versionRange.test("1.0.0.Alpha1")); - assertTrue(versionRange.test("1.0.0.Beta")); + 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,)"); - assertFalse(versionRange.test("1.0.0.Alpha1")); - assertTrue(versionRange.test("1.0.0.Beta")); + assertThat(versionRange) + .accepts("1.0.0.Beta") + .rejects("1.0.0.Alpha1"); } @Test public void testMultipleRanges() { VersionRange versionRange = VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "(,1.0],[1.2,)"); // Should return true for Versions up to 1.0 (included) and 1.2 or higher - assertTrue(versionRange.test("1.0.0.Alpha1")); - assertTrue(versionRange.test("1.0.0")); - assertFalse(versionRange.test("1.1.0")); - assertTrue(versionRange.test("1.2.0")); - assertFalse(versionRange.test("1.2.0.Alpha1")); + assertThat(versionRange) + .accepts("1.0.0.Alpha1", "1.0.0", "1.2.0") + .rejects("1.1.0", "1.2.0.Alpha1"); } @Test public void testQualifiers() { VersionRange versionRange = VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "[3.8,3.8.5)"); - assertTrue(versionRange.test("3.8.4.SP1-redhat-00001")); - assertTrue(versionRange.test("3.8.4.SP2-redhat-00001")); - assertTrue(versionRange.test("3.8.4.redhat-00002")); - assertFalse(versionRange.test("3.8.5.redhat-00003")); + 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"); } @Test @Disabled("This test is failing") public void testRangeQualifier() { VersionRange versionRange = VersionRange.createFromVersionSpec(VersionScheme.MAVEN, "[3.8.0.redhat-00001,)"); - assertTrue(versionRange.test("3.8.0.SP1-redhat-00001")); + assertThat(versionRange).accepts("3.8.0.SP1-redhat-00001"); } } From a22b7c91178ccc39d18a1196c94351337cc960da Mon Sep 17 00:00:00 2001 From: George Gastaldi Date: Sat, 20 Jul 2024 22:48:37 -0300 Subject: [PATCH 7/9] Introduce composable sub-predicates --- .../smallrye/common/version/VersionRange.java | 2 +- .../common/version/VersionScheme.java | 95 +++++++++++++++++++ .../common/version/VersionRangeTest.java | 15 +++ 3 files changed, 111 insertions(+), 1 deletion(-) diff --git a/version/src/main/java/io/smallrye/common/version/VersionRange.java b/version/src/main/java/io/smallrye/common/version/VersionRange.java index 195c9af5..f9c98e3e 100644 --- a/version/src/main/java/io/smallrye/common/version/VersionRange.java +++ b/version/src/main/java/io/smallrye/common/version/VersionRange.java @@ -76,7 +76,7 @@ public static VersionRange createFromVersionSpec(VersionScheme scheme, String sp } if (upperBound != null) { if (restriction.getLowerBound() == null - || scheme.compare(restriction.getLowerBound(), upperBound) < 0) { + || scheme.lt(restriction.getLowerBound(), upperBound)) { throw msg.rangesOverlap(spec); } } 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 0401b4c5..8010f55b 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.function.Predicate; /** * A versioning scheme, which has distinct sorting, iteration, and canonicalization rules. @@ -17,6 +18,100 @@ public interface VersionScheme extends Comparator { */ int compare(String v1, String v2); + /** + * Determine if the first version is less than the second version according to this version scheme. + * + * @param base the base version + * @param other the other version + * @return {@code true} if the first version is less than the second version, or {@code false} otherwise + */ + default boolean lt(String base, String other) { + return compare(base, other) < 0; + } + + /** + * Determine if the first version is less than or equal to the second version according to this version scheme. + * + * @param base the base version + * @param other the other version + * @return {@code true} if the first version is less than or equal to the second version, or {@code false} otherwise + */ + default boolean le(String base, String other) { + return compare(base, other) <= 0; + } + + /** + * Determine if the first version is greater than or equal to the second version according to this version scheme. + * + * @param base the base version + * @param other the other version + * @return {@code true} if the first version is greater than or equal to the second version, or {@code false} otherwise + */ + default boolean gt(String base, String other) { + return compare(base, other) > 0; + } + + /** + * Determine if the first version is greater than the second version according to this version scheme. + * + * @param base the base version + * @param other the other version + * @return {@code true} if the first version is greater than the second version, or {@code false} otherwise + */ + default boolean ge(String base, String other) { + return compare(base, other) >= 0; + } + + /** + * Returns a predicate that tests if the version is equal to the base version. + * + * @param other the other version + * @return {@code true} if the first version is equal to the second version, or {@code false} otherwise + */ + default Predicate whenEquals(String other) { + return base -> equals(base, other); + } + + /** + * Returns a predicate that tests if the version is greater than or equal to the base version. + * + * @param other the other version + * @return {@code true} if the first version is less than the second version, or {@code false} otherwise + */ + default Predicate whenGt(String other) { + return base -> gt(base, other); + } + + /** + * Returns a predicate that tests if the version is greater than or equal to the base version. + * + * @param other the other version + * @return a predicate that tests if the version is greater than or equal to the base version + */ + default Predicate whenGe(String other) { + return base -> ge(base, other); + } + + /** + * Returns a predicate that tests if the version is less than or equal to the base version. + * + * @param other the other version + * @return a predicate that tests if the version is less than or equal to the base version + */ + default Predicate whenLe(String other) { + return base -> le(base, other); + } + + /** + * Returns a predicate that tests if the version is less than the base version. + * + * @param other the other version + * @return a predicate that tests if the version is less than the base version + */ + default Predicate whenLt(String other) { + return base -> lt(base, other); + } + /** * Determine if two versions are equal according to this version scheme. * 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 00fcf6f0..f9b0e1ac 100644 --- a/version/src/test/java/io/smallrye/common/version/VersionRangeTest.java +++ b/version/src/test/java/io/smallrye/common/version/VersionRangeTest.java @@ -5,6 +5,8 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; class VersionRangeTest { @@ -91,4 +93,17 @@ public void testRangeQualifier() { assertThat(versionRange).accepts("3.8.0.SP1-redhat-00001"); } + @ParameterizedTest + @MethodSource("schemes") + public void testComposablePredicates(VersionScheme scheme) { + assertThat(scheme.whenGe("1.0.0").and(scheme.whenLt("2.0.0"))) + .accepts("1.0.0", "1.1.0").rejects("2.0.0", "2.0.1", "2.1.0"); + assertThat(scheme.whenGt("1.0.0").and(scheme.whenLe("2.0.0"))) + .accepts("1.0.1", "2.0.0").rejects("1.0.0", "2.0.1", "2.1.0"); + } + + static VersionScheme[] schemes() { + return new VersionScheme[] { VersionScheme.BASIC, VersionScheme.MAVEN, VersionScheme.JPMS }; + } + } From 2e5eedd28c93eddc618e5718636dafaf62f902c1 Mon Sep 17 00:00:00 2001 From: "David M. Lloyd" Date: Mon, 22 Jul 2024 14:29:09 -0500 Subject: [PATCH 8/9] Unify parsing with predicate methods on `VersionScheme` Intoduce a left-to-right recursive-descent parser for version ranges. Use the predicates on `VersionScheme` to compose the resultant range. --- .../io/smallrye/common/version/Messages.java | 15 +- .../smallrye/common/version/VersionRange.java | 103 -------- .../common/version/VersionRestriction.java | 91 ------- .../common/version/VersionScheme.java | 232 ++++++++++++++++++ .../common/version/VersionRangeTest.java | 24 +- 5 files changed, 256 insertions(+), 209 deletions(-) delete mode 100644 version/src/main/java/io/smallrye/common/version/VersionRange.java delete mode 100644 version/src/main/java/io/smallrye/common/version/VersionRestriction.java 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 533c2b93..c31a8a6f 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 f9c98e3e..00000000 --- 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 112b71c7..00000000 --- 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 8010f55b..a66bfa4b 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 f9b0e1ac..d989d8d9 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"); } From 64a83fec8edc1a4ae13f3338a71231472a1e251e Mon Sep 17 00:00:00 2001 From: "David M. Lloyd" Date: Tue, 23 Jul 2024 12:43:26 -0500 Subject: [PATCH 9/9] Formatting --- .../main/java/io/smallrye/common/version/VersionScheme.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 a66bfa4b..3de0ad6d 100644 --- a/version/src/main/java/io/smallrye/common/version/VersionScheme.java +++ b/version/src/main/java/io/smallrye/common/version/VersionScheme.java @@ -145,6 +145,7 @@ default Predicate fromRangeString(String range) { return fromRangeString(range, 0, range.length()); } + // @formatter:off /** * Parse a range specification and return it as a predicate. * Version ranges are governed by the following general syntax: @@ -187,6 +188,7 @@ default Predicate fromRangeString(String range) { * @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 */ + // @formatter:on default Predicate fromRangeString(String range, int start, int end) { Objects.checkFromToIndex(start, end, range.length()); return parseRange(range, start, end); @@ -418,7 +420,7 @@ private Predicate parseRangeMax(Predicate min, String range, int } case ']': { String high = range.substring(start, i); - if (min != null && ! min.test(high)) { + if (min != null && !min.test(high)) { // low end must be higher than high end throw Messages.msg.rangeDefiesVersionOrdering(range.substring(start, i + cnt)); }