ranges){
+ this.partialRanges = ranges;
+ }
+
+ @Override
+ public boolean contains(SemanticVersionNumber number) {
+ for(SemanticVersionRange range : partialRanges){
+ if(range.contains(number)) return true;
+ }
+ return false;
+ }
+
+ @Override
+ public String toString(){
+ StringBuilder sb = new StringBuilder();
+
+ for(int i = 0; i < partialRanges.size(); i++){
+ SemanticVersionRange range = partialRanges.get(i);
+ sb.append(range.toString());
+ if(i != partialRanges.size()-1) sb.append(".");
+ }
+
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/org/tudo/sse/semver/SemanticVersionNumber.java b/src/main/java/org/tudo/sse/semver/SemanticVersionNumber.java
new file mode 100644
index 0000000..496a5f2
--- /dev/null
+++ b/src/main/java/org/tudo/sse/semver/SemanticVersionNumber.java
@@ -0,0 +1,320 @@
+package org.tudo.sse.semver;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Class representing a version number as defined by the semantic versioning 2.0.0 standard. See this webpage for
+ * the formal definition. This class implements all comparison logic for version numbers as defined by the standard.
+ *
+ *
+ * Please Note: Build Metadata can be attached to semantic version numbers, but is irrelevant when comparing numbers.
+ * This is explicitly specified in the standard. This class adheres to those definitions and ignores build metadata when
+ * comparing numbers (compareTo) or checking for equality (equals, hashcode).
+ *
+ *
+ *
+ * @author Johannes Düsing
+ */
+public class SemanticVersionNumber implements Comparable {
+
+ private final int majorVersion;
+ private final int minorVersion;
+ private final int patchVersion;
+
+ private final String preReleaseData;
+ private final String buildMetadata;
+
+ private List _parsedPreReleases = null;
+
+ /**
+ * Creates a new semantic version number with the given major, minor and patch version.
+ * @param majorVersion Major version of this number
+ * @param minorVersion Minor version of this number
+ * @param patchVersion Patch version of this number
+ */
+ public SemanticVersionNumber(int majorVersion, int minorVersion, int patchVersion) {
+ this(majorVersion, minorVersion, patchVersion, null);
+ }
+
+ /**
+ * Creates a new semantic version number with the given major, minor and patch versions, as well as a prerelease string.
+ * @param majorVersion Major version of this number
+ * @param minorVersion Minor version of this number
+ * @param patchVersion Patch version of this number
+ * @param preReleaseData Prelease identifier of this number
+ */
+ public SemanticVersionNumber(int majorVersion, int minorVersion, int patchVersion, String preReleaseData) {
+ this(majorVersion, minorVersion, patchVersion, preReleaseData, null);
+ }
+
+ /**
+ * Creates a new semantic version number with the given major, minor and patch versions, as well as a prerelease and build identifier.
+ * @param majorVersion Major version of this number
+ * @param minorVersion Minor version of this number
+ * @param patchVersion Patch version of this number
+ * @param preReleaseData Prelease identifier of this number
+ * @param buildMetadata The build metadata identifier of this number - not relevant for comparisons
+ */
+ public SemanticVersionNumber(int majorVersion, int minorVersion, int patchVersion, String preReleaseData, String buildMetadata) {
+ this.majorVersion = majorVersion;
+ this.minorVersion = minorVersion;
+ this.patchVersion = patchVersion;
+
+ this.preReleaseData = preReleaseData;
+ this.buildMetadata = buildMetadata;
+ }
+
+ /**
+ * Get this number's major version.
+ * @return The major version
+ */
+ public int getMajorVersion(){
+ return this.majorVersion;
+ }
+
+ /**
+ * Get this number's minor version.
+ * @return The minor version
+ */
+ public int getMinorVersion(){
+ return this.minorVersion;
+ }
+
+ /**
+ * Get this number's patch version.
+ * @return The patch version
+ */
+ public int getPatchVersion(){
+ return this.patchVersion;
+ }
+
+ /**
+ * Checks whether this semantic version number includes a prerelease identifier.
+ * @return True if there is a prerelease identifier, false otherwise
+ */
+ public boolean hasPreRelease(){
+ return this.preReleaseData != null;
+ }
+
+ /**
+ * Checks whether this semantic version number includes build metadata.
+ * @return True if there is metadata, false otherwise
+ */
+ public boolean hasBuildMetadata(){
+ return this.buildMetadata != null;
+ }
+
+ /**
+ * Get this number's prerelease identifier, if available.
+ * @return The prerelease identifier, or null, if none is available.
+ */
+ public String getPreRelease() {
+ return this.preReleaseData;
+ }
+
+ /**
+ * Get this number's build metadata, if available.
+ * @return The build metadata, or null, if none is available.
+ */
+ public String getBuildMetadata() {
+ return this.buildMetadata;
+ }
+
+ @Override
+ public final int compareTo(SemanticVersionNumber other){
+ // The precedence of semantic version V2 numbers is defined here: https://semver.org/#spec-item-11
+ // Note that build metadata is irrelevant to precedence
+ // Major version takes precedence
+ if(this.majorVersion < other.majorVersion) return -1;
+ else if(this.majorVersion > other.majorVersion) return 1;
+ else {
+ // Minor version is second deciding factor
+ if(this.minorVersion < other.minorVersion) return -1;
+ else if(this.minorVersion > other.minorVersion) return 1;
+ else {
+ // Patch version is third deciding factor
+ if(this.patchVersion < other.patchVersion) return -1;
+ else if(this.patchVersion > other.patchVersion) return 1;
+ else {
+ // If major, minor and patch are equal, we proceed as follows:
+ // - if both versions have no prerelease data, they are equal
+ // - if one version has prerelease data and the other has not, prerelease data takes *lower* precedence, i.e. 1.0.0-alpha < 1.0.0
+ if(!this.hasPreRelease() && !other.hasPreRelease()) return 0;
+ else if(!this.hasPreRelease() && other.hasPreRelease()) return 1;
+ else if(this.hasPreRelease() && !other.hasPreRelease()) return -1;
+ else {
+ // If both versions have prerelease data, we must parse this data. SemVer specifies that it may
+ // consist of dot-separated identifiers that are either numeric or textual.
+ List thisParts = this.getParsedPreReleaseParts();
+ List otherParts = other.getParsedPreReleaseParts();
+
+ // The first difference in prerelease parts decides precedence
+ int pos = 0;
+ while(pos < thisParts.size() && pos < otherParts.size()) {
+ int compareResult = thisParts.get(pos).compareTo(otherParts.get(pos));
+ if(compareResult != 0) return compareResult;
+ pos += 1;
+ }
+
+ // If we do not find a difference for indices valid in both lists: Fewer parts equal less precedence
+ return thisParts.size() - otherParts.size();
+ }
+ }
+ }
+ }
+ }
+
+ private List getParsedPreReleaseParts(){
+ if(this._parsedPreReleases == null) {
+ this._parsedPreReleases = this.parsePreReleaseData();
+ }
+
+ return this._parsedPreReleases;
+ }
+
+ private List parsePreReleaseData(){
+ List preReleaseParts = new ArrayList<>();
+
+ StringBuilder current = new StringBuilder();
+ for(int i = 0; i < preReleaseData.length(); i++){
+ char c = preReleaseData.charAt(i);
+ if(c == '.'){
+ String currValue = current.toString();
+
+ try {
+ int currInt = Integer.parseInt(currValue);
+ preReleaseParts.add(new PreReleasePart(currInt));
+ } catch(NumberFormatException ignored){
+ preReleaseParts.add(new PreReleasePart(currValue));
+ }
+ current.setLength(0);
+ } else {
+ current.append(c);
+ }
+ }
+
+ String currValue = current.toString();
+
+ try {
+ int currInt = Integer.parseInt(currValue);
+ preReleaseParts.add(new PreReleasePart(currInt));
+ } catch(NumberFormatException ignored){
+ preReleaseParts.add(new PreReleasePart(currValue));
+ }
+
+ return preReleaseParts;
+ }
+
+ /**
+ * Class representing a part of a prerelase identifier. The SemVer v2 spec allows prerelease identifiers to be composed
+ * of dot-separated parts, which can either be numbers or strings. This class implements their comparison logic
+ * in compliance with semantic versioning rules.
+ */
+ private static final class PreReleasePart implements Comparable{
+
+ private final String stringPart;
+ private final Integer intPart;
+
+ PreReleasePart(String value){
+ this.stringPart = value;
+ this.intPart = null;
+ }
+
+ PreReleasePart(int value){
+ this.stringPart = null;
+ this.intPart = value;
+ }
+
+ boolean isNum() {
+ return intPart != null;
+ }
+
+ int numValue(){
+ if(isNum()) return intPart;
+ else throw new IllegalStateException("Not a number");
+ }
+
+ boolean isString(){
+ return stringPart != null;
+ }
+
+ String stringValue(){
+ if(isString()) return stringPart;
+ else throw new IllegalStateException("Not a string");
+ }
+
+
+ @Override
+ public int compareTo(PreReleasePart other) {
+ // If both parts are numbers, they are compared numerically
+ if(this.isNum() && other.isNum()) return this.numValue() - other.numValue();
+ // Numeric identifiers have lower precedence than text
+ else if(this.isNum()) return -1;
+ else if(other.isNum()) return 1;
+ else return this.stringValue().compareTo(other.stringValue());
+ }
+ }
+
+ /**
+ * Attempts to create a new semantic version number by parsing the given string value. If the string does not represent
+ * a valid number according to the semantic versioning 2.0.0 standard, an exception is thrown.
+ * @param value The string value to parse
+ * @return The semantic version number if parsing was successful
+ * @throws SemanticVersionParsingException If the value was invalid
+ */
+ public static SemanticVersionNumber parse(String value) throws SemanticVersionParsingException {
+ return SemanticVersionParser.parseNumber(value);
+ }
+
+ /**
+ * Attempts to create a new semantic version number by parsing the given string value. If the string does not represent
+ * a valid number according to the semantic versioning 2.0.0 standard, an empty Optional is returned.
+ * @param value The string value to parse
+ * @return Optional value containing either the parsed number (if parsing was successful), or nothing
+ */
+ public static Optional tryParse(String value) {
+ try {
+ SemanticVersionNumber number = parse(value);
+ return Optional.of(number);
+ } catch (SemanticVersionParsingException svpx){
+ return Optional.empty();
+ }
+ }
+
+ @Override
+ public String toString(){
+ StringBuilder sb = new StringBuilder();
+ sb.append(majorVersion);
+ sb.append(".");
+ sb.append(minorVersion);
+ sb.append(".");
+ sb.append(patchVersion);
+
+ if(this.hasPreRelease()){
+ sb.append("-");
+ sb.append(preReleaseData);
+ }
+
+ if(this.hasBuildMetadata()){
+ sb.append("+");
+ sb.append(buildMetadata);
+ }
+
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(Object other){
+ if(!(other instanceof SemanticVersionNumber)) return false;
+
+ return this.compareTo((SemanticVersionNumber)other) == 0;
+ }
+
+ @Override
+ public int hashCode(){
+ return Objects.hash(majorVersion, minorVersion, patchVersion, preReleaseData);
+ }
+}
diff --git a/src/main/java/org/tudo/sse/semver/SemanticVersionParser.java b/src/main/java/org/tudo/sse/semver/SemanticVersionParser.java
new file mode 100644
index 0000000..524a29a
--- /dev/null
+++ b/src/main/java/org/tudo/sse/semver/SemanticVersionParser.java
@@ -0,0 +1,193 @@
+package org.tudo.sse.semver;
+
+class SemanticVersionParser {
+
+ static SemanticVersionNumber parseNumber(String value) throws SemanticVersionParsingException {
+ if(value.isBlank()) throw new SemanticVersionParsingException(value, "Semantic version cannot be empty", 0);
+
+ ParsingState state = ParsingState.NUMBER;
+ int parsingPosition = 0;
+ StringBuilder currentValue = new StringBuilder();
+
+ int currNumberPartIdx = 0;
+ int[] numberParts = new int[] {-1, 0, 0};
+
+ String preRelease = null;
+ String buildMetadata = null;
+
+ final char[] characters = value.toCharArray();
+
+ for(char currentChar: characters) {
+
+ switch(state){
+ case NUMBER:
+ if(currentChar != '.' && currentChar != '-' && currentChar != '+') {
+ currentValue.append(currentChar);
+ } else {
+ if(currentValue.length() == 0)
+ throw new SemanticVersionParsingException(value, "Numeric identifier must have at least one digit", parsingPosition);
+
+ final String numValue = currentValue.toString();
+ currentValue.setLength(0);
+
+ if(!isNumericIdentifier(numValue))
+ throw new SemanticVersionParsingException(value, "Not a valid numeric identifier", parsingPosition);
+
+ numberParts[currNumberPartIdx] = asInt(numValue, value, parsingPosition);
+
+ if(currentChar == '.'){
+ if(currNumberPartIdx == 2)
+ throw new SemanticVersionParsingException(value, "Semantic version cannot have more than three parts", parsingPosition);
+
+ currNumberPartIdx += 1;
+ } else if(currentChar == '-'){
+ state = ParsingState.PRERELEASE;
+ } else {
+ state = ParsingState.BUILDMETADATA;
+ }
+ }
+ break;
+
+ case PRERELEASE:
+ if(currentChar != '+'){
+ currentValue.append(currentChar);
+ } else {
+ final String preReleaseValue = currentValue.toString();
+ currentValue.setLength(0);
+
+ if(!isValidPreRelease(preReleaseValue))
+ throw new SemanticVersionParsingException(value, "Not a valid prerelease identifier", parsingPosition);
+
+ preRelease = preReleaseValue;
+
+ state = ParsingState.BUILDMETADATA;
+ }
+ break;
+ case BUILDMETADATA:
+ currentValue.append(currentChar);
+
+ break;
+ }
+
+ parsingPosition += 1;
+ }
+
+ String remaining = currentValue.toString();
+
+ switch(state){
+ case NUMBER:
+ numberParts[currNumberPartIdx] = asInt(remaining, value, parsingPosition);
+ break;
+ case PRERELEASE:
+ if(!isValidPreRelease(remaining))
+ throw new SemanticVersionParsingException(value, "Not a valid prerelease identifier", parsingPosition);
+
+ preRelease = remaining;
+ break;
+ case BUILDMETADATA:
+ if(!isValidBuild(remaining))
+ throw new SemanticVersionParsingException(value, "Not a valid build identifier", parsingPosition);
+
+ buildMetadata = remaining;
+ }
+
+ return new SemanticVersionNumber(numberParts[0], numberParts[1], numberParts[2], preRelease, buildMetadata);
+ }
+
+ private static boolean isValidBuild(String value){
+ if(value.isBlank()) return false;
+
+ final String[] parts = value.split("\\.");
+
+ for(String part: parts){
+ if(!isAlphanumericIdentifier(part) && !isDigits(part))
+ return false;
+ }
+
+ return true;
+ }
+
+ private static boolean isValidPreRelease(String value){
+ if(value.isBlank()) return false;
+
+ final String[] parts = value.split("\\.");
+
+ for(String part: parts){
+ if(!isAlphanumericIdentifier(part) && !isNumericIdentifier(part))
+ return false;
+ }
+
+ return true;
+ }
+
+ private static boolean isAlphanumericIdentifier(String value) {
+ if(value.isEmpty()) return false;
+
+ boolean hasNonDigit = false;
+
+ for(char c : value.toCharArray()) {
+ if(isNonDigit(c)) hasNonDigit = true;
+
+ if(!isIdentifierCharacter(c)) return false;
+ }
+
+ return hasNonDigit;
+ }
+
+ private static boolean isNumericIdentifier(String value){
+ if(value.isEmpty()) return false;
+ if(value.equals("0")) return true;
+
+ char[] chars = value.toCharArray();
+
+ if(!isPositiveDigit(chars[0])) return false;
+
+ for(int i = 1; i < chars.length; i++){
+ if(!isDigit(chars[i])) return false;
+ }
+
+ return true;
+ }
+
+ private static boolean isDigits(String value){
+ if(value.isEmpty()) return false;
+ for(char c : value.toCharArray()){
+ if(!isDigit(c)) return false;
+ }
+ return true;
+ }
+
+ private static boolean isIdentifierCharacter(char c){
+ return isDigit(c) || isNonDigit(c);
+ }
+
+ private static boolean isNonDigit(char c){
+ return c == '-' || isLetter(c);
+ }
+
+ private static boolean isLetter(char c) {
+ return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
+ }
+
+ private static boolean isPositiveDigit(char c){
+ return (c >= '1' && c <= '9');
+ }
+
+ private static boolean isDigit(char c) {
+ return (c >= '0' && c <= '9');
+ }
+
+ private static int asInt(String value, String number, int pos) throws SemanticVersionParsingException {
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException nfx) {
+ throw new SemanticVersionParsingException(number, "Expected an integer", pos);
+ }
+ }
+
+ private enum ParsingState {
+ NUMBER,
+ PRERELEASE,
+ BUILDMETADATA
+ }
+}
diff --git a/src/main/java/org/tudo/sse/semver/SemanticVersionParsingException.java b/src/main/java/org/tudo/sse/semver/SemanticVersionParsingException.java
new file mode 100644
index 0000000..299123c
--- /dev/null
+++ b/src/main/java/org/tudo/sse/semver/SemanticVersionParsingException.java
@@ -0,0 +1,77 @@
+package org.tudo.sse.semver;
+
+/**
+ * Class representing an exception while parsing a semantic version number.
+ *
+ * @author Johannes Düsing
+ */
+public class SemanticVersionParsingException extends Exception {
+
+ /**
+ * Value that was being parsed
+ */
+ private final String parsedValue;
+
+ /**
+ * Message describing the error
+ */
+ private final String msg;
+
+ /**
+ * Position at which the error occurred
+ */
+ private final int parsingPosition;
+
+ /**
+ * Create a new parsing exception with the given description while parsing the given value.
+ * @param value The value that was being parsed
+ * @param description A description of the error that was encountered
+ */
+ public SemanticVersionParsingException(String value, String description) {
+ this(value, description, -1);
+ }
+
+ /**
+ * Create a new parsing exception with the given description, parsing position and parsed value.
+ * @param value The value that was being parsed
+ * @param description A description of the error that was encountered
+ * @param parsingPosition The parsing position at which the error occurred
+ */
+ public SemanticVersionParsingException(String value, String description, int parsingPosition) {
+ this.parsedValue = value;
+ this.msg = description;
+ this.parsingPosition = parsingPosition;
+ }
+
+ @Override
+ public String getMessage() {
+ StringBuilder sb = new StringBuilder("Error parsing value '");
+ sb.append(parsedValue);
+ sb.append("'");
+
+ if(parsingPosition != -1){
+ sb.append(" at position ").append(parsingPosition);
+ }
+
+ sb.append(" : ");
+ sb.append(msg);
+ return sb.toString();
+ }
+
+ /**
+ * Returns the position at which the error occurred in the original value.
+ * @return Parsing position
+ */
+ public int getParsingPosition() {
+ return this.parsingPosition;
+ }
+
+ /**
+ * Returns the value that was being parsed when the error occurred.
+ * @return The parsed value
+ */
+ public String getParsedValue(){
+ return this.parsedValue;
+ }
+
+}
diff --git a/src/main/java/org/tudo/sse/semver/SemanticVersionRange.java b/src/main/java/org/tudo/sse/semver/SemanticVersionRange.java
new file mode 100644
index 0000000..d11f764
--- /dev/null
+++ b/src/main/java/org/tudo/sse/semver/SemanticVersionRange.java
@@ -0,0 +1,17 @@
+package org.tudo.sse.semver;
+
+/**
+ * Interface that represents a range of semantic version numbers. The only requirement for implementations is that they
+ * must be able to decide whether any given semantic version number is contained within a range or not.
+ *
+ * @author Johannes Düsing
+ */
+public interface SemanticVersionRange {
+
+ /**
+ * Checks whether the given semantic version number falls within this range
+ * @param number The number to check
+ * @return True if the number is contained within this range, false otherwise
+ */
+ boolean contains(SemanticVersionNumber number);
+}
diff --git a/src/main/java/org/tudo/sse/semver/SimpleSemanticVersionRange.java b/src/main/java/org/tudo/sse/semver/SimpleSemanticVersionRange.java
new file mode 100644
index 0000000..e2202b3
--- /dev/null
+++ b/src/main/java/org/tudo/sse/semver/SimpleSemanticVersionRange.java
@@ -0,0 +1,152 @@
+package org.tudo.sse.semver;
+
+/**
+ * Simple implementation of semantic version ranges. A simple range is defined by two version numbers, a lower bound and
+ * an upper bound. Both are optional, in which case we have (semi-) open ranges. Bounds are either inclusive or exclusive.
+ *
+ * @author Johannes Düsing
+ */
+public class SimpleSemanticVersionRange implements SemanticVersionRange {
+
+ private final SemanticVersionNumber lowerBound;
+ private final SemanticVersionNumber upperBound;
+ private final boolean lowerBoundInclusive;
+ private final boolean upperBoundInclusive;
+
+ /**
+ * Creates a new simple semantic version range with the given bounds and their inclusive-specification. Use null to
+ * indicate that one (or both) bounds do not exist. If a bound is not set, its 'inclusive' value is ignored.
+ * @param lowerBound The lower bound for this range, or null if there is no lower bound.
+ * @param lowerBoundInclusive Whether the lower bound is inclusive. Ignored if there is no lower bound.
+ * @param upperBound The upper bound for this range, or null if there is no upper bound.
+ * @param upperBoundInclusive Whether the upper bound is inclusive. Ignored if there is no upper bound.
+ */
+ public SimpleSemanticVersionRange(SemanticVersionNumber lowerBound,
+ boolean lowerBoundInclusive,
+ SemanticVersionNumber upperBound,
+ boolean upperBoundInclusive) {
+ this.lowerBound = lowerBound;
+ this.lowerBoundInclusive = lowerBoundInclusive;
+ this.upperBound = upperBound;
+ this.upperBoundInclusive = upperBoundInclusive;
+ }
+
+ /**
+ * Creates a new simple semantic version range that is semi-open, i.e. a range that only has a lower bound.
+ * @param lowerBound Lower bound for this range.
+ * @param lowerBoundInclusive Whether the lower bound is inclusive.
+ * @return Semi-Open range with only a lower bound
+ */
+ public static SimpleSemanticVersionRange fromLowerBound(SemanticVersionNumber lowerBound, boolean lowerBoundInclusive) {
+ return new SimpleSemanticVersionRange(lowerBound, lowerBoundInclusive, null, false);
+ }
+
+ /**
+ * Creates a new simple semantic version range that is semi-open, i.e. a range that only has an upper bound.
+ * @param upperBound Upper bound for this range.
+ * @param upperBoundInclusive Whether the upper bound is inclusive.
+ * @return Semi-Open range with only an upper bound
+ */
+ public static SimpleSemanticVersionRange fromUpperBound(SemanticVersionNumber upperBound, boolean upperBoundInclusive) {
+ return new SimpleSemanticVersionRange(null, false, upperBound, upperBoundInclusive);
+ }
+
+ /**
+ * Checks whether this range has a lower bound.
+ * @return True if lower bound exists.
+ */
+ public boolean hasLowerBound() {
+ return lowerBound != null;
+ }
+
+ /**
+ * Checs whether this range has an upper bound.
+ * @return True if upper bound exists.
+ */
+ public boolean hasUpperBound() {
+ return this.upperBound != null;
+ }
+
+ /**
+ * Checks whether the lower bound of this range is inclusive.
+ * @return True if lower bound is inclusive, false if exclusive.
+ */
+ public boolean isLowerBoundInclusive() {
+ return lowerBoundInclusive;
+ }
+
+ /**
+ * Checks whether the upper bound of this range is inclusive.
+ * @return True if upper bound is inclusive, false if exclusive.
+ */
+ public boolean isUpperBoundInclusive() {
+ return upperBoundInclusive;
+ }
+
+ /**
+ * Gets the lower bound for this range.
+ * @return Lower bound, or null if non exists
+ */
+ public SemanticVersionNumber getLowerBound() {
+ return lowerBound;
+ }
+
+ /**
+ * Gets the upper bound for this range.
+ * @return Upper bound, or null if non exists.
+ */
+ public SemanticVersionNumber getUpperBound() {
+ return upperBound;
+ }
+
+ /**
+ * Checks whether this range represents a (Maven Central) hard requirement. A hard requirement is a special range
+ * where lower and upper bound are equal, and both are inclusive. This range contains exactly one version number.
+ * @return True if this range is a hard requirement
+ */
+ public boolean isHardRequirement(){
+ return this.lowerBound.equals(this.upperBound);
+ }
+
+ @Override
+ public boolean contains(SemanticVersionNumber number) {
+ if(hasLowerBound()){
+ int compareResult = number.compareTo(lowerBound);
+ if(compareResult < 0 || (!isLowerBoundInclusive() && compareResult == 0)) return false;
+ }
+
+ if(hasUpperBound()){
+ int compareResult = number.compareTo(upperBound);
+ if(compareResult > 0 || (!isUpperBoundInclusive() && compareResult == 0)) return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public String toString(){
+ StringBuilder sb = new StringBuilder();
+
+ if(isLowerBoundInclusive())
+ sb.append('[');
+ else
+ sb.append('(');
+
+ if(hasLowerBound())
+ sb.append(lowerBound.toString());
+
+ if(!isHardRequirement()){
+ sb.append(',');
+ sb.append(' ');
+ if(hasUpperBound())
+ sb.append(upperBound.toString());
+ }
+
+ if(isUpperBoundInclusive())
+ sb.append(']');
+ else
+ sb.append(')');
+
+ return sb.toString();
+ }
+}
diff --git a/src/test/java/org/tudo/sse/semver/MavenVersionRangeParserTest.java b/src/test/java/org/tudo/sse/semver/MavenVersionRangeParserTest.java
new file mode 100644
index 0000000..79114a2
--- /dev/null
+++ b/src/test/java/org/tudo/sse/semver/MavenVersionRangeParserTest.java
@@ -0,0 +1,135 @@
+package org.tudo.sse.semver;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests for version range parsing are based on the specification
+ */
+public class MavenVersionRangeParserTest {
+
+ @Test
+ @DisplayName("The parser must parse hard requirements correctly")
+ void testHardRequirement_Valid(){
+ SimpleSemanticVersionRange simpleRange = assertSimpleRange("[1.0]");
+
+ assertNotNull(simpleRange);
+ assertTrue(simpleRange.isHardRequirement());
+ assertEquals("1.0.0", simpleRange.getLowerBound().toString());
+ }
+
+ @Test
+ @DisplayName("The parser must parse semi-open simple ranges like (,1.0]")
+ void testSemiRange1(){
+ SimpleSemanticVersionRange simpleRange = assertSimpleRange("(,1.0]");
+ assertNotNull(simpleRange);
+ assertFalse(simpleRange.hasLowerBound());
+ assertTrue(simpleRange.hasUpperBound());
+ assertFalse(simpleRange.isLowerBoundInclusive());
+ assertTrue(simpleRange.isUpperBoundInclusive());
+ assertEquals("1.0.0", simpleRange.getUpperBound().toString());
+ }
+
+ @Test
+ @DisplayName("The parser must parse regular simple ranges")
+ void testFullRange1(){
+ SimpleSemanticVersionRange simpleRange = assertSimpleRange("[1.2,1.3.3-Preview+Snapshot.1]");
+ assertNotNull(simpleRange);
+ assertTrue(simpleRange.hasLowerBound());
+ assertTrue(simpleRange.hasUpperBound());
+ assertTrue(simpleRange.isLowerBoundInclusive());
+ assertTrue(simpleRange.isUpperBoundInclusive());
+ assertTrue(simpleRange.getUpperBound().hasPreRelease());
+ assertTrue(simpleRange.getUpperBound().getBuildMetadata().endsWith(".1"));
+ }
+
+ @Test
+ @DisplayName("The parser must parse inclusivity correctly for simple ranges")
+ void testFullRange2(){
+ SimpleSemanticVersionRange simpleRange = assertSimpleRange("[1.0, 2.0)");
+ assertNotNull(simpleRange);
+ assertTrue(simpleRange.hasLowerBound());
+ assertTrue(simpleRange.isLowerBoundInclusive());
+ assertTrue(simpleRange.hasUpperBound());
+ assertFalse(simpleRange.isUpperBoundInclusive());
+ assertEquals(2, simpleRange.getUpperBound().getMajorVersion());
+ }
+
+ @Test
+ @DisplayName("The parser must parse semi-open simple ranges like [1.5,)")
+ void testSemiRange2(){
+ SimpleSemanticVersionRange simpleRange = assertSimpleRange("[1.5,)");
+ assertNotNull(simpleRange);
+ assertTrue(simpleRange.hasLowerBound());
+ assertTrue(simpleRange.isLowerBoundInclusive());
+ assertFalse(simpleRange.hasUpperBound());
+ assertFalse(simpleRange.isUpperBoundInclusive());
+ assertEquals(5, simpleRange.getLowerBound().getMinorVersion());
+ }
+
+ @Test
+ @DisplayName("The parser must parse complex composite ranges correctly")
+ void complexRange1(){
+ MultiPartSemanticVersionRange multiRange = assertMultiRange("(,1.0],[1.2,)");
+ assertNotNull(multiRange);
+
+ var notContained = asSemVer("1.0.1");
+ var contained1 = asSemVer("0.0.1");
+ var contained2 = asSemVer("1.2");
+
+ assertFalse(multiRange.contains(notContained));
+ assertTrue(multiRange.contains(contained1));
+ assertTrue(multiRange.contains(contained2));
+ }
+
+ @Test
+ @DisplayName("The parser must handle inverse hard requirements correctly")
+ void testInverseHardRequirement(){
+ MultiPartSemanticVersionRange multiRange = assertMultiRange("(,1.1),(1.1,)");
+ assertNotNull(multiRange);
+
+ var notContained = asSemVer("1.1");
+ var contained = asSemVer("1.2");
+
+ assertFalse(multiRange.contains(notContained));
+ assertTrue(multiRange.contains(contained));
+ }
+
+
+ private SimpleSemanticVersionRange assertSimpleRange(String versionRange){
+ try {
+ var range = MavenVersionRangeParser.parseRange(versionRange);
+
+ assertInstanceOf(SimpleSemanticVersionRange.class, range);
+
+ return (SimpleSemanticVersionRange)range;
+ } catch (SemanticVersionParsingException svpx){
+ fail(svpx);
+ }
+ return null;
+ }
+
+ private MultiPartSemanticVersionRange assertMultiRange(String versionRange){
+ try {
+ var range = MavenVersionRangeParser.parseRange(versionRange);
+
+ assertInstanceOf(MultiPartSemanticVersionRange.class, range);
+
+ return (MultiPartSemanticVersionRange) range;
+ } catch (SemanticVersionParsingException svpx){
+ fail(svpx);
+ }
+ return null;
+ }
+
+ private SemanticVersionNumber asSemVer(String value){
+ try {
+ return SemanticVersionNumber.parse(value);
+ } catch (SemanticVersionParsingException svpx){
+ fail(svpx);
+ }
+ return null;
+ }
+}
diff --git a/src/test/java/org/tudo/sse/semver/SemanticVersionNumberTest.java b/src/test/java/org/tudo/sse/semver/SemanticVersionNumberTest.java
new file mode 100644
index 0000000..3e926c4
--- /dev/null
+++ b/src/test/java/org/tudo/sse/semver/SemanticVersionNumberTest.java
@@ -0,0 +1,68 @@
+package org.tudo.sse.semver;
+
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class SemanticVersionNumberTest {
+
+ private final String[] validNumbersSimple = new String[]{"1.0.0", "3.0.0", "1.2.0", "1.1.1", "1054.20000.1"};
+ private final int[] validNumbersSimple_Rank = new int[] {0, 3, 2, 1, 4};
+
+ private final String[] preReleasesSimple = new String[]{"1.0.0", "1.0.0-gamma-1", "1.0.0-beta", "1.0.0-alpha-1"};
+ private final int[] preReleasesSimple_Rank = new int[] {3, 2, 1, 0};
+
+ // This test case is taken from: https://semver.org/#spec-item-11
+ private final String[] preReleaseParts = new String[]{"1.0.0", "1.0.0-rc.1", "1.0.0-beta.11", "1.0.0-beta.2", "1.0.0-beta", "1.0.0-alpha.beta", "1.0.0-alpha.1", "1.0.0-alpha"};
+ private final int[] preReleaseParts_Rank = new int[] {7,6,5,4,3,2,1,0};
+
+ @Test
+ @DisplayName("Simple version numbers must be sorted correctly")
+ public void sortSimple(){
+ assertSorted(validNumbersSimple, validNumbersSimple_Rank);
+ }
+
+ @Test
+ @DisplayName("Prereleases must be sorted correctly")
+ public void sortPrereleases(){
+ assertSorted(preReleasesSimple, preReleasesSimple_Rank);
+ }
+
+ @Test
+ @DisplayName("Prerelease parts must be parsed and sorted correctly")
+ public void sortPrereleaseParts(){
+ assertSorted(preReleaseParts, preReleaseParts_Rank);
+ }
+
+ private void assertSorted(String[] numbers, int[] ranks){
+ var actual = sortSemantic(numbers);
+
+ for(int i = 0; i < numbers.length; i++){
+ var expectedNum = numbers[i];
+ var expectedPos = ranks[i];
+
+ assertEquals(expectedNum, actual.get(expectedPos).toString());
+ }
+ }
+
+ private List sortSemantic(String[] numbers) {
+ List list = new ArrayList<>();
+
+ for(String number : numbers){
+ var semVerOpt = SemanticVersionNumber.tryParse(number);
+ assert(semVerOpt.isPresent());
+ list.add(semVerOpt.get());
+ }
+
+ Collections.sort(list);
+
+ return list;
+ }
+
+}
diff --git a/src/test/java/org/tudo/sse/semver/SemanticVersionParserTest.java b/src/test/java/org/tudo/sse/semver/SemanticVersionParserTest.java
new file mode 100644
index 0000000..b6a0071
--- /dev/null
+++ b/src/test/java/org/tudo/sse/semver/SemanticVersionParserTest.java
@@ -0,0 +1,74 @@
+package org.tudo.sse.semver;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class SemanticVersionParserTest {
+
+ private final String[] invalidSyntaxNumbers = new String []{"1a.2.3", "1.2.3.4", "foo", "1+a-n+c", "1..2", "1.0.0-"};
+ private final String integerOverflow = "2131231231231231231231231231231231231231231231.1";
+
+ private final String[] validNumbersSimple = new String[]{"1.0.0", "3.0.0", "1.2.0", "1.1.1", "1054.20000.1"};
+ private final int[][] validNumbersSimple_Expected = new int[][]{new int[]{1,0,0}, new int[]{3,0,0}, new int[]{1,2,0}, new int[]{1,1,1}, new int[]{1054,20000,1}};
+
+ private final String[] validNumbersComplex = new String[]{"1-a-valid-prerelase123+000000", "1.2.3+0a-def-----", "12-0+1", "0.0.0-0+0"};
+ private final int[][] validNumbersComplex_Expected_Numbers = new int[][]{new int[]{1,0,0}, new int[]{1,2,3}, new int[]{12,0,0}, new int[]{0,0,0}};
+ private final String[][] validNumbersComplex_Expected_Data = new String[][]{new String[]{"a-valid-prerelase123", "000000"}, new String[]{null, "0a-def-----"}, new String[]{"0","1"}, new String[]{"0", "0"}};
+
+ @Test
+ @DisplayName("Invalid syntax should lead to parsing exceptions")
+ public void parseInvalidSyntax(){
+ for(String invalidSyntaxNumber: invalidSyntaxNumbers){
+ assertThrows(SemanticVersionParsingException.class, () -> SemanticVersionNumber.parse(invalidSyntaxNumber));
+ }
+ }
+
+ @Test
+ @DisplayName("Integer overflows should lead to parsing exceptions")
+ public void parseOverflow(){
+ assertThrows(SemanticVersionParsingException.class, () -> SemanticVersionNumber.parse(integerOverflow));
+ }
+
+ @Test
+ @DisplayName("Simple numbers should be parsed without exception")
+ public void parseSimpleNumbers(){
+ try {
+ for(int i = 0; i < validNumbersSimple.length; i++){
+ var semVer = SemanticVersionNumber.parse(validNumbersSimple[i]);
+ var expected = validNumbersSimple_Expected[i];
+
+ assertEquals(expected[0], semVer.getMajorVersion());
+ assertEquals(expected[1], semVer.getMinorVersion());
+ assertEquals(expected[2], semVer.getPatchVersion());
+ }
+ } catch (SemanticVersionParsingException svpx) {
+ fail(svpx);
+ }
+ }
+
+ @Test
+ @DisplayName("Complex numbers should be parsed without exception")
+ public void parseComplexNumbers(){
+ try {
+ for(int i = 0; i < validNumbersComplex.length; i++){
+ var semVer = SemanticVersionNumber.parse(validNumbersComplex[i]);
+ var expectedNumbers = validNumbersComplex_Expected_Numbers[i];
+ var expectedData = validNumbersComplex_Expected_Data[i];
+
+ assertEquals(expectedNumbers[0], semVer.getMajorVersion());
+ assertEquals(expectedNumbers[1], semVer.getMinorVersion());
+ assertEquals(expectedNumbers[2], semVer.getPatchVersion());
+
+ assertEquals(expectedData[0], semVer.getPreRelease());
+ assertEquals(expectedData[1], semVer.getBuildMetadata());
+ }
+ } catch(SemanticVersionParsingException svpx){
+ fail(svpx);
+ }
+ }
+
+}