diff --git a/spring-boot-tools/spring-boot-dependency-tools/src/main/java/org/springframework/boot/dependency/tools/InvalidVersionException.java b/spring-boot-tools/spring-boot-dependency-tools/src/main/java/org/springframework/boot/dependency/tools/InvalidVersionException.java new file mode 100644 index 0000000000..1a8d9f73cc --- /dev/null +++ b/spring-boot-tools/spring-boot-dependency-tools/src/main/java/org/springframework/boot/dependency/tools/InvalidVersionException.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.dependency.tools; + +/** + * Thrown if a input represents an invalid version. + * + * @author Stephane Nicoll + * @since 1.2.2 + */ +@SuppressWarnings("serial") +public class InvalidVersionException extends RuntimeException { + + public InvalidVersionException(String message) { + super(message); + } +} diff --git a/spring-boot-tools/spring-boot-dependency-tools/src/main/java/org/springframework/boot/dependency/tools/Version.java b/spring-boot-tools/spring-boot-dependency-tools/src/main/java/org/springframework/boot/dependency/tools/Version.java new file mode 100644 index 0000000000..8539d9758b --- /dev/null +++ b/spring-boot-tools/spring-boot-dependency-tools/src/main/java/org/springframework/boot/dependency/tools/Version.java @@ -0,0 +1,275 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.dependency.tools; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Define the version number of a module. A typical version is represented + * as {@code MAJOR.MINOR.PATCH.QUALIFIER} where the qualifier can have an + * extra version. + *

+ * For example: {@code 1.2.0.RC1} is the first release candidate of 1.2.0 + * and {@code 1.5.0.M4} is the fourth milestone of 1.5.0. The special + * {@code RELEASE} qualifier indicates a final release (a.k.a. GA) + *

+ * The main purpose of parsing a version is to compare it with another + * version, see {@link Comparable}. + * + * @author Stephane Nicoll + * @since 1.2.2 + */ +public class Version implements Comparable { + + private static final Pattern VERSION_PATTERN = + Pattern.compile("^(\\d+)\\.(\\d+)\\.(\\d+)(?:\\.([^0-9]+)(\\d+)?)?$"); + + private static final VersionQualifierComparator qualifierComparator = new VersionQualifierComparator(); + + private final Integer major; + + private final Integer minor; + + private final Integer patch; + + private final Qualifier qualifier; + + public Version(Integer major, Integer minor, Integer patch, Qualifier qualifier) { + this.major = major; + this.minor = minor; + this.patch = patch; + this.qualifier = qualifier; + } + + public Integer getMajor() { + return major; + } + + public Integer getMinor() { + return minor; + } + + public Integer getPatch() { + return patch; + } + + public Qualifier getQualifier() { + return qualifier; + } + + /** + * Parse the string representation of a {@link Version}. Throws an + * {@link InvalidVersionException} if the version could not be parsed. + * @param text the version text + * @return a Version instance for the specified version text + * @throws InvalidVersionException if the version text could not be parsed + * @see #safeParse(java.lang.String) + */ + public static Version parse(String text) { + Assert.notNull(text, "Text must not be null"); + Matcher matcher = VERSION_PATTERN.matcher(text.trim()); + if (!matcher.matches()) { + throw new InvalidVersionException("Could not determine version based on '" + text + "': version format " + + "is Minor.Major.Patch.Qualifier (i.e. 1.0.5.RELEASE"); + } + Integer major = Integer.valueOf(matcher.group(1)); + Integer minor = Integer.valueOf(matcher.group(2)); + Integer patch = Integer.valueOf(matcher.group(3)); + Qualifier qualifier = null; + String qualifierId = matcher.group(4); + if (qualifierId != null) { + String qualifierVersion = matcher.group(5); + if (qualifierVersion != null) { + qualifier = new Qualifier(qualifierId, Integer.valueOf(qualifierVersion)); + } + else { + qualifier = new Qualifier(qualifierId); + } + } + return new Version(major, minor, patch, qualifier); + } + + /** + * Parse safely the specified string representation of a {@link Version}. + *

+ * Return {@code null} if the text represents an invalid version. + * @param text the version text + * @return a Version instance for the specified version text + * @see #parse(java.lang.String) + */ + public static Version safeParse(String text) { + try { + return parse(text); + } + catch (InvalidVersionException e) { + return null; + } + } + + @Override + public int compareTo(Version other) { + if (other == null) { + return 1; + } + int majorDiff = safeCompare(this.major, other.major); + if (majorDiff != 0) { + return majorDiff; + } + int minorDiff = safeCompare(this.minor, other.minor); + if (minorDiff != 0) { + return minorDiff; + } + int patch = safeCompare(this.patch, other.patch); + if (patch != 0) { + return patch; + } + return qualifierComparator.compare(this.qualifier, other.qualifier); + } + + private static int safeCompare(Integer first, Integer second) { + Integer firstIndex = (first != null ? first : 0); + Integer secondIndex = (second != null ? second : 0); + return firstIndex.compareTo(secondIndex); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("Version{"); + sb.append("major=").append(major); + sb.append(", minor=").append(minor); + sb.append(", patch=").append(patch); + sb.append(", qualifier=").append(qualifier); + sb.append('}'); + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Version version = (Version) o; + + if (major != null ? !major.equals(version.major) : version.major != null) return false; + if (minor != null ? !minor.equals(version.minor) : version.minor != null) return false; + if (patch != null ? !patch.equals(version.patch) : version.patch != null) return false; + return !(qualifier != null ? !qualifier.equals(version.qualifier) : version.qualifier != null); + + } + + @Override + public int hashCode() { + int result = major != null ? major.hashCode() : 0; + result = 31 * result + (minor != null ? minor.hashCode() : 0); + result = 31 * result + (patch != null ? patch.hashCode() : 0); + result = 31 * result + (qualifier != null ? qualifier.hashCode() : 0); + return result; + } + + public static class Qualifier { + private final String id; + + private final Integer version; + + public Qualifier(String id, Integer version) { + this.id = id; + this.version = version; + } + + public Qualifier(String id) { + this(id, null); + } + + public String getId() { + return id; + } + + public Integer getVersion() { + return version; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Qualifier qualifier1 = (Qualifier) o; + + if (!id.equals(qualifier1.id)) return false; + return !(version != null ? !version.equals(qualifier1.version) : qualifier1.version != null); + + } + + @Override + public int hashCode() { + int result = id.hashCode(); + result = 31 * result + (version != null ? version.hashCode() : 0); + return result; + } + } + + + private static class VersionQualifierComparator implements Comparator { + + static final String RELEASE = "RELEASE"; + + static final String SNAPSHOT = "BUILD-SNAPSHOT"; + + static final String MILESTONE = "M"; + + static final String RC = "RC"; + + static final List KNOWN_QUALIFIERS = Arrays.asList(MILESTONE, RC, SNAPSHOT, RELEASE); + + @Override + public int compare(Qualifier o1, Qualifier o2) { + Qualifier first = (o1 != null ? o1 : new Qualifier(RELEASE)); + Qualifier second = (o2 != null ? o2 : new Qualifier(RELEASE)); + + int qualifier = compareQualifier(first, second); + return (qualifier != 0 ? qualifier : compareQualifierVersion(first, second)); + } + + private static int compareQualifierVersion(Qualifier first, Qualifier second) { + Integer firstVersion = (first.getVersion() != null ? first.getVersion() : 0); + Integer secondVersion = (second.getVersion() != null ? second.getVersion() : 0); + return firstVersion.compareTo(secondVersion); + } + + private static int compareQualifier(Qualifier first, Qualifier second) { + Integer firstIndex = getQualifierIndex(first.id); + Integer secondIndex = getQualifierIndex(second.id); + + if (firstIndex == -1 && secondIndex == -1) { // Unknown qualifier, alphabetic ordering + return first.id.compareTo(second.id); + } + else { + return firstIndex.compareTo(secondIndex); + } + } + + private static int getQualifierIndex(String qualifier) { + String q = (qualifier != null ? qualifier : RELEASE); + return KNOWN_QUALIFIERS.indexOf(q); + } + } + +} diff --git a/spring-boot-tools/spring-boot-dependency-tools/src/main/java/org/springframework/boot/dependency/tools/VersionRange.java b/spring-boot-tools/spring-boot-dependency-tools/src/main/java/org/springframework/boot/dependency/tools/VersionRange.java new file mode 100644 index 0000000000..e4c0bbd8c6 --- /dev/null +++ b/spring-boot-tools/spring-boot-dependency-tools/src/main/java/org/springframework/boot/dependency/tools/VersionRange.java @@ -0,0 +1,102 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.dependency.tools; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Define a {@link Version} range. A square bracket "[" or "]" denotes an inclusive + * end of the range and a round bracket "(" or ")" denotes an exclusive end of the + * range. A range can also be unbounded by defining a single {@link Version}. The + * examples below make this hopefully more clear. + *

+ * + * @author Stephane Nicoll + * @since 1.2.2 + */ +public class VersionRange { + + private static final Pattern RANGE_PATTERN = Pattern.compile("(\\(|\\[)(.*),(.*)(\\)|\\])"); + + private final Version lowerVersion; + private final boolean lowerInclusive; + private final Version higherVersion; + private final boolean higherInclusive; + + public VersionRange(Version lowerVersion, boolean lowerInclusive, + Version higherVersion, boolean higherInclusive) { + this.lowerVersion = lowerVersion; + this.lowerInclusive = lowerInclusive; + this.higherVersion = higherVersion; + this.higherInclusive = higherInclusive; + } + + /** + * Specify if the {@link Version} matches this range. Returns {@code true} + * if the version is contained within this range, {@code false} otherwise. + * @param version the version to match this range + * @return {@code true} if this version is contained within this range + */ + public boolean match(Version version) { + Assert.notNull(version, "Version must not be null"); + int lower = lowerVersion.compareTo(version); + if (lower > 0) { + return false; + } else if (!lowerInclusive && lower == 0) { + return false; + } + if (higherVersion != null) { + int higher = higherVersion.compareTo(version); + if (higher < 0) { + return false; + } else if (!higherInclusive && higher == 0) { + return false; + } + } + return true; + } + + /** + * Parse the string representation of a {@link VersionRange}. Throws an + * {@link InvalidVersionException} if the range could not be parsed. + * @param text the range text + * @return a VersionRange instance for the specified range text + * @throws InvalidVersionException if the range text could not be parsed + */ + public static VersionRange parse(String text) { + Assert.notNull(text, "Text must not be null"); + Matcher matcher = RANGE_PATTERN.matcher(text.trim()); + if (!matcher.matches()) { + // Try to read it as simple version + Version version = Version.parse(text); + return new VersionRange(version, true, null, true); + } + boolean lowerInclusive = matcher.group(1).equals("["); + Version lowerVersion = Version.parse(matcher.group(2)); + Version higherVersion = Version.parse(matcher.group(3)); + boolean higherInclusive = matcher.group(4).equals("]"); + return new VersionRange(lowerVersion, lowerInclusive, higherVersion, higherInclusive); + } + +} diff --git a/spring-boot-tools/spring-boot-dependency-tools/src/test/java/org/springframework/boot/dependency/tools/VersionRangeTests.java b/spring-boot-tools/spring-boot-dependency-tools/src/test/java/org/springframework/boot/dependency/tools/VersionRangeTests.java new file mode 100644 index 0000000000..b893aca17f --- /dev/null +++ b/spring-boot-tools/spring-boot-dependency-tools/src/test/java/org/springframework/boot/dependency/tools/VersionRangeTests.java @@ -0,0 +1,123 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.dependency.tools; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.hamcrest.core.IsNot.not; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link VersionRange}. + * + * @author Stephane Nicoll + */ +public class VersionRangeTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Test + public void matchSimpleRange() { + assertThat("1.2.0.RC3", match("[1.2.0.RC1,1.2.0.RC5]")); + } + + @Test + public void matchSimpleRangeBefore() { + assertThat("1.1.9.RC3", not(match("[1.2.0.RC1,1.2.0.RC5]"))); + } + + @Test + public void matchSimpleRangeAfter() { + assertThat("1.2.0.RC6", not(match("[1.2.0.RC1,1.2.0.RC5]"))); + } + + @Test + public void matchInclusiveLowerRange() { + assertThat("1.2.0.RC1", match("[1.2.0.RC1,1.2.0.RC5]")); + } + + @Test + public void matchInclusiveHigherRange() { + assertThat("1.2.0.RC5", match("[1.2.0.RC1,1.2.0.RC5]")); + } + + @Test + public void matchExclusiveLowerRange() { + assertThat("1.2.0.RC1", not(match("(1.2.0.RC1,1.2.0.RC5)"))); + } + + @Test + public void matchExclusiveHigherRange() { + assertThat("1.2.0.RC5", not(match("[1.2.0.RC1,1.2.0.RC5)"))); + } + + @Test + public void matchUnboundedRangeEqual() { + assertThat("1.2.0.RELEASE", match("1.2.0.RELEASE")); + } + + @Test + public void matchUnboundedRangeAfter() { + assertThat("2.2.0.RELEASE", match("1.2.0.RELEASE")); + } + + @Test + public void matchUnboundedRangeBefore() { + assertThat("1.1.9.RELEASE", not(match("1.2.0.RELEASE"))); + } + + @Test + public void invalidRange() { + thrown.expect(InvalidVersionException.class); + VersionRange.parse("foo-bar"); + } + + @Test + public void rangeWithSpaces() { + assertThat("1.2.0.RC3", match("[ 1.2.0.RC1 , 1.2.0.RC5]")); + } + + private static VersionRangeMatcher match(String range) { + return new VersionRangeMatcher(range); + } + + + static class VersionRangeMatcher extends BaseMatcher { + + private final VersionRange range; + + VersionRangeMatcher(String text) { + this.range = VersionRange.parse(text); + } + + @Override + public boolean matches(Object item) { + return item instanceof String && this.range.match(Version.parse((String) item)); + } + + @Override + public void describeTo(Description description) { + description.appendText(range.toString()); + } + } + +} diff --git a/spring-boot-tools/spring-boot-dependency-tools/src/test/java/org/springframework/boot/dependency/tools/VersionTests.java b/spring-boot-tools/spring-boot-dependency-tools/src/test/java/org/springframework/boot/dependency/tools/VersionTests.java new file mode 100644 index 0000000000..5fe994591a --- /dev/null +++ b/spring-boot-tools/spring-boot-dependency-tools/src/test/java/org/springframework/boot/dependency/tools/VersionTests.java @@ -0,0 +1,166 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.dependency.tools; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.comparesEqualTo; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.lessThan; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.springframework.boot.dependency.tools.Version.parse; +import static org.springframework.boot.dependency.tools.Version.safeParse; + +/** + * Tests for {@link Version}. + * + * @author Stephane Nicoll + */ +public class VersionTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Test + public void parseVersion() { + Version v = parse("1.2.0.M4"); + assertEquals(new Integer(1), v.getMajor()); + assertEquals(new Integer(2), v.getMinor()); + assertEquals(new Integer(0), v.getPatch()); + assertNotNull(v.getQualifier()); + assertEquals("M", v.getQualifier().getId()); + assertEquals(new Integer(4), v.getQualifier().getVersion()); + } + + @Test + public void equalNoQualifier() { + Version first = parse("1.2.0"); + Version second = parse("1.2.0"); + assertThat(first, comparesEqualTo(second)); + assertThat(first, equalTo(second)); + } + + @Test + public void equalQualifierNoVersion() { + Version first = parse("1.2.0.RELEASE"); + Version second = parse("1.2.0.RELEASE"); + assertThat(first, comparesEqualTo(second)); + assertThat(first, equalTo(second)); + } + + @Test + public void equalQualifierVersion() { + Version first = parse("1.2.0.RC1"); + Version second = parse("1.2.0.RC1"); + assertThat(first, comparesEqualTo(second)); + assertThat(first, equalTo(second)); + } + + @Test + public void compareMajorOnly() { + assertThat(parse("2.2.0"), greaterThan(parse("1.8.0"))); + } + + @Test + public void compareMinorOnly() { + assertThat(parse("2.2.0"), greaterThan(parse("2.1.9"))); + } + + @Test + public void comparePatchOnly() { + assertThat(parse("2.2.4"), greaterThan(parse("2.2.3"))); + } + + @Test + public void compareHigherVersion() { + assertThat(parse("1.2.0.RELEASE"), greaterThan(parse("1.1.9.RELEASE"))); + } + + @Test + public void compareHigherQualifier() { + assertThat(parse("1.2.0.RC1"), greaterThan(parse("1.2.0.M1"))); + } + + @Test + public void compareHigherQualifierVersion() { + assertThat(parse("1.2.0.RC2"), greaterThan(parse("1.2.0.RC1"))); + } + + @Test + public void compareLowerVersion() { + assertThat(parse("1.0.5.RELEASE"), lessThan(parse("1.1.9.RELEASE"))); + } + + @Test + public void compareLowerQualifier() { + assertThat(parse("1.2.0.RC1"), lessThan(parse("1.2.0.RELEASE"))); + } + + @Test + public void compareLessQualifierVersion() { + assertThat(parse("1.2.0.RC2"), lessThan(parse("1.2.0.RC3"))); + } + + @Test + public void compareWithNull() { + Version nullValue = null; + assertThat(parse("1.2.0.RC2"), greaterThan(nullValue)); + } + + @Test + public void compareUnknownQualifier() { + assertThat(parse("1.2.0.Beta"), lessThan(parse("1.2.0.CR"))); + } + + @Test + public void compareUnknownQualifierVersion() { + assertThat(parse("1.2.0.Beta1"), lessThan(parse("1.2.0.Beta2"))); + } + + @Test + public void snapshotGreaterThanRC() { + assertThat(parse("1.2.0.BUILD-SNAPSHOT"), greaterThan(parse("1.2.0.RC1"))); + } + + @Test + public void snapshotLowerThanRelease() { + assertThat(parse("1.2.0.BUILD-SNAPSHOT"), lessThan(parse("1.2.0.RELEASE"))); + } + + @Test + public void parseInvalidVersion() { + thrown.expect(InvalidVersionException.class); + parse("foo"); + } + + @Test + public void safeParseInvalidVersion() { + assertNull(safeParse("foo")); + } + + @Test + public void parseVersionWithSpaces() { + assertThat(parse(" 1.2.0.RC3 "), lessThan(parse("1.3.0.RELEASE"))); + } + +}