Support version format

This commit clarifies that Version now handles two different formats,
the original one (flagged V1) and a SemVer compliant format (flagged
V2). Both Version and VersionRange can switch from one format to the
other to produce backward compatible content.

See gh-1092
This commit is contained in:
Stephane Nicoll
2020-06-02 16:52:06 +02:00
parent 1bfa7f6f47
commit bcc70551bb
4 changed files with 192 additions and 11 deletions

View File

@@ -23,19 +23,25 @@ import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.StringJoiner;
import java.util.function.Function;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* 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.
* Define a version. A typical version is represented as
* {@code MAJOR.MINOR.PATCH[QUALIFIER]} where the qualifier is optional and 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)
* {@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}.
* Two formats are currently supported, {@link Format#V1} that uses a dot to separate the
* qualifier from the version itself and {@link Format#V2} that is SemVer compliant (and
* therefore uses a dash to separate the qualifier).
* <p>
* The main purpose of parsing a version is to compare it with another version.
*
* @author Stephane Nicoll
*/
@@ -54,11 +60,68 @@ public final class Version implements Serializable, Comparable<Version> {
private final Qualifier qualifier;
private final Format format;
public Version(Integer major, Integer minor, Integer patch, Qualifier qualifier) {
this.major = major;
this.minor = minor;
this.patch = patch;
this.qualifier = qualifier;
this.format = determineFormat(qualifier);
}
private static Format determineFormat(Qualifier qualifier) {
if (qualifier == null) {
return Format.V2;
}
return (qualifier.getSeparator().equals(".")) ? Format.V1 : Format.V2;
}
/**
* Format this version to the specified {@link Format}.
* @param format the format to use
* @return a version compliant with the specified format.
*/
public Version format(Format format) {
Assert.notNull(format, () -> "Format must not be null");
if (this.format == format) {
return this;
}
if (format == Format.V1) {
Qualifier qualifier = formatQualifier(".", this::toV1Qualifier);
return new Version(this.major, this.minor, this.patch, qualifier);
}
Qualifier qualifier = formatQualifier("-", this::toV2Qualifier);
return new Version(this.major, this.minor, this.patch, qualifier);
}
private Qualifier formatQualifier(String newSeparator, Function<String, String> idTransformer) {
String originalQualifier = (this.qualifier != null) ? this.qualifier.getId() : null;
String newId = idTransformer.apply(originalQualifier);
if (newId != null) {
return new Qualifier(newId, (this.qualifier != null) ? this.qualifier.getVersion() : null, newSeparator);
}
return null;
}
private String toV1Qualifier(String id) {
if ("SNAPSHOT".equals(id)) {
return "BUILD-SNAPSHOT";
}
if (id == null) {
return "RELEASE";
}
return id;
}
private String toV2Qualifier(String id) {
if ("BUILD-SNAPSHOT".equals(id)) {
return "SNAPSHOT";
}
if ("RELEASE".equals(id)) {
return null;
}
return id;
}
public Integer getMajor() {
@@ -77,6 +140,10 @@ public final class Version implements Serializable, Comparable<Version> {
return this.qualifier;
}
public Format getFormat() {
return this.format;
}
/**
* Parse the string representation of a {@link Version}. Throws an
* {@link InvalidVersionException} if the version could not be parsed.
@@ -262,6 +329,26 @@ public final class Version implements Serializable, Comparable<Version> {
}
/**
* Define the supported version format.
*/
public enum Format {
/**
* Original version format, i.e. {@code Major.Minor.Patch.Qualifier} using
* {@code BUILD-SNAPSHOT} as the qualifier for snapshots and {@code RELEASE} for
* GAs.
*/
V1,
/**
* SemVer-compliant format, i.e. {@code Major.Minor.Patch-Qualifier} using
* {@code SNAPSHOT} as the qualifier for snapshots and no qualifier for GAs.
*/
V2;
}
private static class VersionQualifierComparator implements Comparator<Qualifier> {
static final String RELEASE = "RELEASE";

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@@ -16,6 +16,8 @@
package io.spring.initializr.generator.version;
import io.spring.initializr.generator.version.Version.Format;
import org.springframework.util.Assert;
/**
@@ -27,9 +29,8 @@ import org.springframework.util.Assert;
* <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>
* <li>"(2.0.0,3.2.0]" any version after 2.0.0 up to and including version 3.2.0.</li>
* <li>"2.5.0-M1", the first milestone of 2.5.0 and any version after that.</li>
* </ul>
*
* @author Stephane Nicoll
@@ -83,6 +84,17 @@ public class VersionRange {
return true;
}
/**
* Format this version range to the specified {@link Format}.
* @param format the version format to use
* @return a version range whose boundaries are compliant with the specified format.
*/
public VersionRange format(Format format) {
Version lower = this.lowerVersion.format(format);
Version higher = (this.higherVersion != null) ? this.higherVersion.format(format) : null;
return new VersionRange(lower, this.lowerInclusive, higher, this.higherInclusive);
}
public Version getLowerVersion() {
return this.lowerVersion;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@@ -19,12 +19,15 @@ package io.spring.initializr.generator.version;
import java.util.Arrays;
import java.util.Collections;
import io.spring.initializr.generator.version.Version.Format;
import org.assertj.core.api.Condition;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link VersionRange}.
*
* @author Stephane Nicoll
*/
class VersionRangeTests {
@@ -148,6 +151,34 @@ class VersionRangeTests {
assertThat(range.toRangeString()).isEqualTo("(1.3.5.RELEASE,1.5.5.RELEASE)");
}
@Test
void formatLowerOnlyV1toV2() {
VersionRange range = parse("1.2.0.RELEASE").format(Format.V2);
assertThat(range.toRangeString()).isEqualTo("1.2.0");
}
@Test
void formatV1toV2() {
VersionRange range = parse("[1.2.0.RELEASE,1.3.0.M1)").format(Format.V2);
assertThat(range.toRangeString()).isEqualTo("[1.2.0,1.3.0-M1)");
}
@Test
void formatLowerOnlyV2toV1() {
VersionRange range = parse("1.2.0").format(Format.V1);
assertThat(range.toRangeString()).isEqualTo("1.2.0.RELEASE");
}
@Test
void formatV2toV1() {
VersionRange range = parse("[1.2.0,1.3.0-M1)").format(Format.V1);
assertThat(range.toRangeString()).isEqualTo("[1.2.0.RELEASE,1.3.0.M1)");
}
private static VersionRange parse(String text) {
return new VersionParser(Collections.emptyList()).parseRange(text);
}
private static Condition<String> match(String range) {
return match(range, new VersionParser(Collections.emptyList()));
}

View File

@@ -21,11 +21,14 @@ import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import io.spring.initializr.generator.version.Version.Format;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link Version}.
*
* @author Stephane Nicoll
*/
class VersionTests {
@@ -153,6 +156,54 @@ class VersionTests {
"2020.0.0-SNAPSHOT", "2020.0.0");
}
@Test
void formatV1toV1() {
Version version = Version.parse("1.2.0.RELEASE");
assertThat(version.format(Format.V1)).isSameAs(version);
}
@Test
void formatV1SnapshotToV2() {
Version version = Version.parse("1.2.0.BUILD-SNAPSHOT");
assertThat(version.format(Format.V2)).hasToString("1.2.0-SNAPSHOT");
}
@Test
void formatV1GAToV2() {
Version version = Version.parse("1.2.0.RELEASE");
assertThat(version.format(Format.V2)).hasToString("1.2.0");
}
@Test
void formatNoQualifierToV1() {
Version version = Version.parse("1.2.0");
assertThat(version.format(Format.V1)).hasToString("1.2.0.RELEASE");
}
@Test
void formatV2toV2() {
Version version = Version.parse("1.2.0-RC1");
assertThat(version.format(Format.V2)).isSameAs(version);
}
@Test
void formatV2SnapshotToV1() {
Version version = Version.parse("1.2.0-SNAPSHOT");
assertThat(version.format(Format.V1)).hasToString("1.2.0.BUILD-SNAPSHOT");
}
@Test
void formatV2GAToV1() {
Version version = Version.parse("1.2.0");
assertThat(version.format(Format.V1)).hasToString("1.2.0.RELEASE");
}
@Test
void formatNoQualifierToV2() {
Version version = Version.parse("1.2.0");
assertThat(version.format(Format.V2)).hasToString("1.2.0");
}
private Version parse(String text) {
return this.parser.parse(text);
}