Add Version and VersionRange utilities

Parse a version using our version format or any version that complies
with Major.Minor.Patch. Also add a VersionRange utility that can
determine if a given version is withing that range.

Closes gh-2494
pull/2150/merge
Stephane Nicoll 10 years ago
parent e8e39e4bcf
commit 34ede2f31f

@ -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);
}
}

@ -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.
* <p>
* 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)
* <p>
* 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<Version> {
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}.
* <p>
* 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<Qualifier> {
static final String RELEASE = "RELEASE";
static final String SNAPSHOT = "BUILD-SNAPSHOT";
static final String MILESTONE = "M";
static final String RC = "RC";
static final List<String> 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);
}
}
}

@ -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.
* <ul>
* <li>"[1.2.0.RELEASE,1.3.0.RELEASE)" version 1.2.0 and any version after
* this, up to, but not including, version 1.3.0.</li>
* <li>"(2.0.0.RELEASE,3.2.0.RELEASE]" any version after 2.0.0 up to and
* including version 3.2.0.</li>
* <li>"1.4.5.RELEASE", version 1.4.5 and all later versions.</li>
* </ul>
*
* @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);
}
}

@ -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<String> {
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());
}
}
}

@ -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")));
}
}
Loading…
Cancel
Save