From bcc70551bb669ca988856bf9732c0a2d85e8bf37 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 2 Jun 2020 16:52:06 +0200 Subject: [PATCH 1/4] 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 --- .../initializr/generator/version/Version.java | 99 +++++++++++++++++-- .../generator/version/VersionRange.java | 20 +++- .../generator/version/VersionRangeTests.java | 33 ++++++- .../generator/version/VersionTests.java | 51 ++++++++++ 4 files changed, 192 insertions(+), 11 deletions(-) diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/version/Version.java b/initializr-generator/src/main/java/io/spring/initializr/generator/version/Version.java index 0a1c47e4..78bc4b57 100644 --- a/initializr-generator/src/main/java/io/spring/initializr/generator/version/Version.java +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/version/Version.java @@ -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. *

* 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). *

- * 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). + *

+ * 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 { 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 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 { 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 { } + /** + * 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 { static final String RELEASE = "RELEASE"; diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/version/VersionRange.java b/initializr-generator/src/main/java/io/spring/initializr/generator/version/VersionRange.java index 8b1d11fa..506a475f 100644 --- a/initializr-generator/src/main/java/io/spring/initializr/generator/version/VersionRange.java +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/version/VersionRange.java @@ -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; *

* * @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; } diff --git a/initializr-generator/src/test/java/io/spring/initializr/generator/version/VersionRangeTests.java b/initializr-generator/src/test/java/io/spring/initializr/generator/version/VersionRangeTests.java index 1d68de48..ccb06861 100755 --- a/initializr-generator/src/test/java/io/spring/initializr/generator/version/VersionRangeTests.java +++ b/initializr-generator/src/test/java/io/spring/initializr/generator/version/VersionRangeTests.java @@ -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 match(String range) { return match(range, new VersionParser(Collections.emptyList())); } diff --git a/initializr-generator/src/test/java/io/spring/initializr/generator/version/VersionTests.java b/initializr-generator/src/test/java/io/spring/initializr/generator/version/VersionTests.java index 4aef3f48..9366482f 100755 --- a/initializr-generator/src/test/java/io/spring/initializr/generator/version/VersionTests.java +++ b/initializr-generator/src/test/java/io/spring/initializr/generator/version/VersionTests.java @@ -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); } From 41f844a3adddd17e9800c99fc544754c4260de7a Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 2 Jun 2020 18:06:23 +0200 Subject: [PATCH 2/4] Serve version format that is backward compatible This commit makes sure the metadata format uses a backward compatible version format even if the new format is used. It also introduces a new metadata version (2.2) that can be used by clients that support the new version format. See gh-1092 --- .../src/main/asciidoc/metadata-format.adoc | 8 +- .../src/main/asciidoc/using-the-stubs.adoc | 2 +- .../controller/ProjectMetadataController.java | 10 +- .../InitializrMetadataV21JsonMapper.java | 12 +- .../InitializrMetadataV22JsonMapper.java | 42 ++++ .../InitializrMetadataV2JsonMapper.java | 27 +- .../web/mapper/InitializrMetadataVersion.java | 9 +- .../AbstractInitializrIntegrationTests.java | 25 +- ...ineMetadataControllerIntegrationTests.java | 14 +- ...trollerCustomDefaultsIntegrationTests.java | 12 +- ...ectMetadataControllerIntegrationTests.java | 30 ++- ...InitializrMetadataV21JsonMapperTests.java} | 46 +++- .../InitializrMetadataV22JsonMapperTests.java | 86 +++++++ .../resources/application-test-default.yml | 4 +- .../metadata/config/test-default.json | 4 +- ....2.1.json => test-dependencies-2.4.1.json} | 2 +- .../metadata/test-default-2.0.0-ssl.json | 2 +- .../metadata/test-default-2.0.0.json | 2 +- .../metadata/test-default-2.1.0-ssl.json | 4 +- .../metadata/test-default-2.1.0.json | 4 +- .../metadata/test-default-2.2.0.json | 232 ++++++++++++++++++ 21 files changed, 523 insertions(+), 54 deletions(-) create mode 100644 initializr-web/src/main/java/io/spring/initializr/web/mapper/InitializrMetadataV22JsonMapper.java rename initializr-web/src/test/java/io/spring/initializr/web/mapper/{InitializrMetadataJsonMapperTests.java => InitializrMetadataV21JsonMapperTests.java} (61%) create mode 100755 initializr-web/src/test/java/io/spring/initializr/web/mapper/InitializrMetadataV22JsonMapperTests.java rename initializr-web/src/test/resources/metadata/dependencies/{test-dependencies-2.2.1.json => test-dependencies-2.4.1.json} (97%) create mode 100644 initializr-web/src/test/resources/metadata/test-default-2.2.0.json diff --git a/initializr-docs/src/main/asciidoc/metadata-format.adoc b/initializr-docs/src/main/asciidoc/metadata-format.adoc index 928593ff..35a6b25e 100644 --- a/initializr-docs/src/main/asciidoc/metadata-format.adoc +++ b/initializr-docs/src/main/asciidoc/metadata-format.adoc @@ -13,10 +13,16 @@ sent to the service. A good structure for a user agent is `clientId/clientVersio == Service Capabilities Any third party client can retrieve the capabilities of the service by issuing a `GET` on the root URL using the following `Accept` header: -`application/vnd.initializr.v2.1+json`. Please note that the metadata may evolve in a +`application/vnd.initializr.v2.2+json`. Please note that the metadata may evolve in a non backward compatible way in the future so adding this header ensures the service returns the metadata format you expect. +The following versions are supported: + +* `v2` initial version, with support of V1 version format only +* `v2.1` support compatibility range and dependencies links +* `v2.2` (current) support for V1 and V2 version formats. + This is an example output for a service running at `https://start.spring.io`: .request diff --git a/initializr-docs/src/main/asciidoc/using-the-stubs.adoc b/initializr-docs/src/main/asciidoc/using-the-stubs.adoc index 8837586e..b282384f 100644 --- a/initializr-docs/src/main/asciidoc/using-the-stubs.adoc +++ b/initializr-docs/src/main/asciidoc/using-the-stubs.adoc @@ -110,7 +110,7 @@ include::{test-examples}/stub/ClientApplicationTests.java[tag=test] Then you have a server that returns the stub of the JSON metadata (`metadataWithCurrentAcceptHeader.json`) when you send it a header -`Accept:application/vnd.initializr.v2.1+json` (as recommended). +`Accept:application/vnd.initializr.v2.2+json` (as recommended). diff --git a/initializr-web/src/main/java/io/spring/initializr/web/controller/ProjectMetadataController.java b/initializr-web/src/main/java/io/spring/initializr/web/controller/ProjectMetadataController.java index 8bc7a1ac..c5ec6691 100644 --- a/initializr-web/src/main/java/io/spring/initializr/web/controller/ProjectMetadataController.java +++ b/initializr-web/src/main/java/io/spring/initializr/web/controller/ProjectMetadataController.java @@ -31,6 +31,7 @@ import io.spring.initializr.metadata.InvalidInitializrMetadataException; import io.spring.initializr.web.mapper.DependencyMetadataV21JsonMapper; import io.spring.initializr.web.mapper.InitializrMetadataJsonMapper; import io.spring.initializr.web.mapper.InitializrMetadataV21JsonMapper; +import io.spring.initializr.web.mapper.InitializrMetadataV22JsonMapper; import io.spring.initializr.web.mapper.InitializrMetadataV2JsonMapper; import io.spring.initializr.web.mapper.InitializrMetadataVersion; import io.spring.initializr.web.project.InvalidProjectRequestException; @@ -77,6 +78,11 @@ public class ProjectMetadataController extends AbstractMetadataController { return serviceCapabilitiesFor(InitializrMetadataVersion.V2_1, HAL_JSON_CONTENT_TYPE); } + @RequestMapping(path = { "/", "/metadata/client" }, produces = { "application/vnd.initializr.v2.2+json" }) + public ResponseEntity serviceCapabilitiesV22() { + return serviceCapabilitiesFor(InitializrMetadataVersion.V2_2); + } + @RequestMapping(path = { "/", "/metadata/client" }, produces = { "application/vnd.initializr.v2.1+json", "application/json" }) public ResponseEntity serviceCapabilitiesV21() { @@ -147,8 +153,10 @@ public class ProjectMetadataController extends AbstractMetadataController { switch (version) { case V2: return new InitializrMetadataV2JsonMapper(); - default: + case V2_1: return new InitializrMetadataV21JsonMapper(); + default: + return new InitializrMetadataV22JsonMapper(); } } diff --git a/initializr-web/src/main/java/io/spring/initializr/web/mapper/InitializrMetadataV21JsonMapper.java b/initializr-web/src/main/java/io/spring/initializr/web/mapper/InitializrMetadataV21JsonMapper.java index 06b77156..196172b5 100644 --- a/initializr-web/src/main/java/io/spring/initializr/web/mapper/InitializrMetadataV21JsonMapper.java +++ b/initializr-web/src/main/java/io/spring/initializr/web/mapper/InitializrMetadataV21JsonMapper.java @@ -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,6 +19,8 @@ package io.spring.initializr.web.mapper; import java.util.List; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.spring.initializr.generator.version.Version.Format; +import io.spring.initializr.generator.version.VersionRange; import io.spring.initializr.metadata.Dependency; import io.spring.initializr.metadata.Type; @@ -55,8 +57,8 @@ public class InitializrMetadataV21JsonMapper extends InitializrMetadataV2JsonMap @Override protected ObjectNode mapDependency(Dependency dependency) { ObjectNode content = mapValue(dependency); - if (dependency.getCompatibilityRange() != null) { - content.put("versionRange", dependency.getCompatibilityRange()); + if (dependency.getRange() != null) { + content.put("versionRange", formatVersionRange(dependency.getRange())); } if (dependency.getLinks() != null && !dependency.getLinks().isEmpty()) { content.set("_links", LinkMapper.mapLinks(dependency.getLinks())); @@ -64,6 +66,10 @@ public class InitializrMetadataV21JsonMapper extends InitializrMetadataV2JsonMap return content; } + protected String formatVersionRange(VersionRange versionRange) { + return versionRange.format(Format.V1).toRangeString(); + } + private ObjectNode dependenciesLink(String appUrl) { String uri = (appUrl != null) ? appUrl + "/dependencies" : "/dependencies"; UriTemplate uriTemplate = UriTemplate.of(uri, this.dependenciesVariables); diff --git a/initializr-web/src/main/java/io/spring/initializr/web/mapper/InitializrMetadataV22JsonMapper.java b/initializr-web/src/main/java/io/spring/initializr/web/mapper/InitializrMetadataV22JsonMapper.java new file mode 100644 index 00000000..c1d34122 --- /dev/null +++ b/initializr-web/src/main/java/io/spring/initializr/web/mapper/InitializrMetadataV22JsonMapper.java @@ -0,0 +1,42 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://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 io.spring.initializr.web.mapper; + +import io.spring.initializr.generator.version.Version; +import io.spring.initializr.generator.version.VersionRange; + +/** + * A {@link InitializrMetadataJsonMapper} handling the metadata format for v2.2 + *

+ * Version 2.2 adds support for {@linkplain Version.Format#V2 SemVer version format}. Any + * previous version formats versions to {@link Version.Format#V1}. + * + * @author Stephane Nicoll + */ +public class InitializrMetadataV22JsonMapper extends InitializrMetadataV21JsonMapper { + + @Override + protected String formatVersion(String versionId) { + return versionId; + } + + @Override + protected String formatVersionRange(VersionRange versionRange) { + return versionRange.toRangeString(); + } + +} diff --git a/initializr-web/src/main/java/io/spring/initializr/web/mapper/InitializrMetadataV2JsonMapper.java b/initializr-web/src/main/java/io/spring/initializr/web/mapper/InitializrMetadataV2JsonMapper.java index c0864fb7..4d99813b 100644 --- a/initializr-web/src/main/java/io/spring/initializr/web/mapper/InitializrMetadataV2JsonMapper.java +++ b/initializr-web/src/main/java/io/spring/initializr/web/mapper/InitializrMetadataV2JsonMapper.java @@ -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. @@ -17,12 +17,16 @@ package io.spring.initializr.web.mapper; import java.util.List; +import java.util.function.Function; import java.util.stream.Collectors; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.spring.initializr.generator.version.Version; +import io.spring.initializr.generator.version.Version.Format; +import io.spring.initializr.generator.version.VersionParser; import io.spring.initializr.metadata.DefaultMetadataElement; import io.spring.initializr.metadata.DependenciesCapability; import io.spring.initializr.metadata.Dependency; @@ -79,7 +83,7 @@ public class InitializrMetadataV2JsonMapper implements InitializrMetadataJsonMap singleSelect(delegate, metadata.getPackagings()); singleSelect(delegate, metadata.getJavaVersions()); singleSelect(delegate, metadata.getLanguages()); - singleSelect(delegate, metadata.getBootVersions()); + singleSelect(delegate, metadata.getBootVersions(), this::mapVersionMetadata); text(delegate, metadata.getGroupId()); text(delegate, metadata.getArtifactId()); text(delegate, metadata.getVersion()); @@ -133,6 +137,11 @@ public class InitializrMetadataV2JsonMapper implements InitializrMetadataJsonMap } protected void singleSelect(ObjectNode parent, SingleSelectCapability capability) { + singleSelect(parent, capability, this::mapValue); + } + + protected void singleSelect(ObjectNode parent, SingleSelectCapability capability, + Function valueMapper) { ObjectNode single = nodeFactory.objectNode(); single.put("type", capability.getType().getName()); DefaultMetadataElement defaultType = capability.getDefault(); @@ -140,7 +149,7 @@ public class InitializrMetadataV2JsonMapper implements InitializrMetadataJsonMap single.put("default", defaultType.getId()); } ArrayNode values = nodeFactory.arrayNode(); - values.addAll(capability.getContent().stream().map(this::mapValue).collect(Collectors.toList())); + values.addAll(capability.getContent().stream().map(valueMapper).collect(Collectors.toList())); single.set("values", values); parent.set(capability.getId(), single); } @@ -189,6 +198,18 @@ public class InitializrMetadataV2JsonMapper implements InitializrMetadataJsonMap return result; } + private ObjectNode mapVersionMetadata(MetadataElement value) { + ObjectNode result = nodeFactory.objectNode(); + result.put("id", formatVersion(value.getId())); + result.put("name", value.getName()); + return result; + } + + protected String formatVersion(String versionId) { + Version version = VersionParser.DEFAULT.safeParse(versionId); + return (version != null) ? version.format(Format.V1).toString() : versionId; + } + protected ObjectNode mapValue(MetadataElement value) { ObjectNode result = nodeFactory.objectNode(); result.put("id", value.getId()); diff --git a/initializr-web/src/main/java/io/spring/initializr/web/mapper/InitializrMetadataVersion.java b/initializr-web/src/main/java/io/spring/initializr/web/mapper/InitializrMetadataVersion.java index 237e1692..c635394e 100644 --- a/initializr-web/src/main/java/io/spring/initializr/web/mapper/InitializrMetadataVersion.java +++ b/initializr-web/src/main/java/io/spring/initializr/web/mapper/InitializrMetadataVersion.java @@ -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. @@ -35,7 +35,12 @@ public enum InitializrMetadataVersion { * versions are compatible with it. Also provide a separate "dependencies" endpoint to * query dependencies metadata. */ - V2_1("application/vnd.initializr.v2.1+json"); + V2_1("application/vnd.initializr.v2.1+json"), + + /** + * Add support for SemVer compliant version format. + */ + V2_2("application/vnd.initializr.v2.2+json"); private final MediaType mediaType; diff --git a/initializr-web/src/test/java/io/spring/initializr/web/AbstractInitializrIntegrationTests.java b/initializr-web/src/test/java/io/spring/initializr/web/AbstractInitializrIntegrationTests.java index f7b9a3bd..27633523 100755 --- a/initializr-web/src/test/java/io/spring/initializr/web/AbstractInitializrIntegrationTests.java +++ b/initializr-web/src/test/java/io/spring/initializr/web/AbstractInitializrIntegrationTests.java @@ -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. @@ -72,7 +72,9 @@ import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(classes = Config.class) public abstract class AbstractInitializrIntegrationTests { - protected static final MediaType CURRENT_METADATA_MEDIA_TYPE = InitializrMetadataVersion.V2_1.getMediaType(); + protected static final MediaType DEFAULT_METADATA_MEDIA_TYPE = InitializrMetadataVersion.V2_1.getMediaType(); + + protected static final MediaType CURRENT_METADATA_MEDIA_TYPE = InitializrMetadataVersion.V2_2.getMediaType(); private static final ObjectMapper objectMapper = new ObjectMapper(); @@ -125,14 +127,23 @@ public abstract class AbstractInitializrIntegrationTests { } } - protected void validateCurrentMetadata(ResponseEntity response) { - validateContentType(response, CURRENT_METADATA_MEDIA_TYPE); - validateCurrentMetadata(response.getBody()); + protected void validateDefaultMetadata(ResponseEntity response) { + validateContentType(response, DEFAULT_METADATA_MEDIA_TYPE); + validateMetadata(response.getBody(), "2.1.0"); } - protected void validateCurrentMetadata(String json) { + protected void validateCurrentMetadata(ResponseEntity response) { + validateContentType(response, CURRENT_METADATA_MEDIA_TYPE); + validateMetadata(response.getBody(), "2.2.0"); + } + + protected void validateDefaultMetadata(String json) { + validateMetadata(json, "2.1.0"); + } + + protected void validateMetadata(String json, String version) { try { - JSONObject expected = readMetadataJson("2.1.0"); + JSONObject expected = readMetadataJson(version); JSONAssert.assertEquals(expected, new JSONObject(json), JSONCompareMode.STRICT); } catch (JSONException ex) { diff --git a/initializr-web/src/test/java/io/spring/initializr/web/controller/CommandLineMetadataControllerIntegrationTests.java b/initializr-web/src/test/java/io/spring/initializr/web/controller/CommandLineMetadataControllerIntegrationTests.java index d94af304..e73fb337 100644 --- a/initializr-web/src/test/java/io/spring/initializr/web/controller/CommandLineMetadataControllerIntegrationTests.java +++ b/initializr-web/src/test/java/io/spring/initializr/web/controller/CommandLineMetadataControllerIntegrationTests.java @@ -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. @@ -45,8 +45,8 @@ public class CommandLineMetadataControllerIntegrationTests extends AbstractIniti // make sure curl can still receive metadata with json void curlWithAcceptHeaderJson() { ResponseEntity response = invokeHome("curl/1.2.4", "application/json"); - validateContentType(response, AbstractInitializrIntegrationTests.CURRENT_METADATA_MEDIA_TYPE); - validateCurrentMetadata(response.getBody()); + validateContentType(response, AbstractInitializrIntegrationTests.DEFAULT_METADATA_MEDIA_TYPE); + validateDefaultMetadata(response.getBody()); } @Test @@ -65,8 +65,8 @@ public class CommandLineMetadataControllerIntegrationTests extends AbstractIniti // make sure curl can still receive metadata with json void httpieWithAcceptHeaderJson() { ResponseEntity response = invokeHome("HTTPie/0.8.0", "application/json"); - validateContentType(response, AbstractInitializrIntegrationTests.CURRENT_METADATA_MEDIA_TYPE); - validateCurrentMetadata(response.getBody()); + validateContentType(response, AbstractInitializrIntegrationTests.DEFAULT_METADATA_MEDIA_TYPE); + validateDefaultMetadata(response.getBody()); } @Test @@ -84,8 +84,8 @@ public class CommandLineMetadataControllerIntegrationTests extends AbstractIniti @Test void springBootCliReceivesJsonByDefault() { ResponseEntity response = invokeHome("SpringBootCli/1.2.0", "*/*"); - validateContentType(response, AbstractInitializrIntegrationTests.CURRENT_METADATA_MEDIA_TYPE); - validateCurrentMetadata(response.getBody()); + validateContentType(response, AbstractInitializrIntegrationTests.DEFAULT_METADATA_MEDIA_TYPE); + validateDefaultMetadata(response.getBody()); } @Test diff --git a/initializr-web/src/test/java/io/spring/initializr/web/controller/ProjectMetadataControllerCustomDefaultsIntegrationTests.java b/initializr-web/src/test/java/io/spring/initializr/web/controller/ProjectMetadataControllerCustomDefaultsIntegrationTests.java index b01a53be..0dfc64fa 100755 --- a/initializr-web/src/test/java/io/spring/initializr/web/controller/ProjectMetadataControllerCustomDefaultsIntegrationTests.java +++ b/initializr-web/src/test/java/io/spring/initializr/web/controller/ProjectMetadataControllerCustomDefaultsIntegrationTests.java @@ -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. @@ -84,24 +84,24 @@ class ProjectMetadataControllerCustomDefaultsIntegrationTests extends AbstractFu @Test void metadataClientEndpoint() { ResponseEntity response = execute("/metadata/client", String.class, null, "application/json"); - validateCurrentMetadata(response); + validateDefaultMetadata(response); } @Test void noBootVersion() throws JSONException { ResponseEntity response = execute("/dependencies", String.class, null, "application/json"); assertThat(response.getHeaders().getFirst(HttpHeaders.ETAG)).isNotNull(); - validateContentType(response, CURRENT_METADATA_MEDIA_TYPE); + validateContentType(response, DEFAULT_METADATA_MEDIA_TYPE); validateDependenciesOutput("2.1.4", response.getBody()); } @Test void filteredDependencies() throws JSONException { - ResponseEntity response = execute("/dependencies?bootVersion=2.2.1.RELEASE", String.class, null, + ResponseEntity response = execute("/dependencies?bootVersion=2.4.1.RELEASE", String.class, null, "application/json"); assertThat(response.getHeaders().getFirst(HttpHeaders.ETAG)).isNotNull(); - validateContentType(response, CURRENT_METADATA_MEDIA_TYPE); - validateDependenciesOutput("2.2.1", response.getBody()); + validateContentType(response, DEFAULT_METADATA_MEDIA_TYPE); + validateDependenciesOutput("2.4.1", response.getBody()); } protected void validateDependenciesOutput(String version, String actual) throws JSONException { diff --git a/initializr-web/src/test/java/io/spring/initializr/web/controller/ProjectMetadataControllerIntegrationTests.java b/initializr-web/src/test/java/io/spring/initializr/web/controller/ProjectMetadataControllerIntegrationTests.java index 55472cba..9a9df149 100644 --- a/initializr-web/src/test/java/io/spring/initializr/web/controller/ProjectMetadataControllerIntegrationTests.java +++ b/initializr-web/src/test/java/io/spring/initializr/web/controller/ProjectMetadataControllerIntegrationTests.java @@ -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. @@ -43,7 +43,7 @@ public class ProjectMetadataControllerIntegrationTests extends AbstractInitializ void metadataWithNoAcceptHeader() { // rest template sets application/json by default ResponseEntity response = invokeHome(null, "*/*"); - validateCurrentMetadata(response); + validateDefaultMetadata(response); } @Test @@ -60,6 +60,18 @@ public class ProjectMetadataControllerIntegrationTests extends AbstractInitializ validateMetadata(response, InitializrMetadataVersion.V2.getMediaType(), "2.0.0", JSONCompareMode.STRICT); } + @Test + void metadataWithV21AcceptHeader() { + ResponseEntity response = invokeHome(null, "application/vnd.initializr.v2.1+json"); + validateMetadata(response, InitializrMetadataVersion.V2_1.getMediaType(), "2.1.0", JSONCompareMode.STRICT); + } + + @Test + void metadataWithV22AcceptHeader() { + ResponseEntity response = invokeHome(null, "application/vnd.initializr.v2.2+json"); + validateMetadata(response, InitializrMetadataVersion.V2_2.getMediaType(), "2.2.0", JSONCompareMode.STRICT); + } + @Test void metadataWithInvalidPlatformVersion() { try { @@ -76,18 +88,18 @@ public class ProjectMetadataControllerIntegrationTests extends AbstractInitializ void metadataWithCurrentAcceptHeader() { getRequests().setFields("_links.maven-project", "dependencies.values[0]", "type.values[0]", "javaVersion.values[0]", "packaging.values[0]", "bootVersion.values[0]", "language.values[0]"); - ResponseEntity response = invokeHome(null, "application/vnd.initializr.v2.1+json"); + ResponseEntity response = invokeHome(null, "application/vnd.initializr.v2.2+json"); assertThat(response.getHeaders().getFirst(HttpHeaders.ETAG)).isNotNull(); validateContentType(response, AbstractInitializrIntegrationTests.CURRENT_METADATA_MEDIA_TYPE); - validateCurrentMetadata(response.getBody()); + validateMetadata(response.getBody(), "2.2.0"); } @Test void metadataWithSeveralAcceptHeader() { - ResponseEntity response = invokeHome(null, "application/vnd.initializr.v2.1+json", + ResponseEntity response = invokeHome(null, "application/vnd.initializr.v2.2+json", "application/vnd.initializr.v2+json"); validateContentType(response, AbstractInitializrIntegrationTests.CURRENT_METADATA_MEDIA_TYPE); - validateCurrentMetadata(response.getBody()); + validateCurrentMetadata(response); } @Test @@ -95,7 +107,7 @@ public class ProjectMetadataControllerIntegrationTests extends AbstractInitializ ResponseEntity response = invokeHome(null, "application/hal+json"); assertThat(response.getHeaders().getFirst(HttpHeaders.ETAG)).isNotNull(); validateContentType(response, ProjectMetadataController.HAL_JSON_CONTENT_TYPE); - validateCurrentMetadata(response.getBody()); + validateDefaultMetadata(response.getBody()); } @Test @@ -117,13 +129,13 @@ public class ProjectMetadataControllerIntegrationTests extends AbstractInitializ @Test void unknownAgentReceivesJsonByDefault() { ResponseEntity response = invokeHome("foo/1.0", "*/*"); - validateCurrentMetadata(response); + validateDefaultMetadata(response); } @Test // Test that the current output is exactly what we expect void validateCurrentProjectMetadata() { - validateCurrentMetadata(getMetadataJson()); + validateDefaultMetadata(getMetadataJson()); } private String getMetadataJson() { diff --git a/initializr-web/src/test/java/io/spring/initializr/web/mapper/InitializrMetadataJsonMapperTests.java b/initializr-web/src/test/java/io/spring/initializr/web/mapper/InitializrMetadataV21JsonMapperTests.java similarity index 61% rename from initializr-web/src/test/java/io/spring/initializr/web/mapper/InitializrMetadataJsonMapperTests.java rename to initializr-web/src/test/java/io/spring/initializr/web/mapper/InitializrMetadataV21JsonMapperTests.java index 4e00c79a..3e508b5d 100755 --- a/initializr-web/src/test/java/io/spring/initializr/web/mapper/InitializrMetadataJsonMapperTests.java +++ b/initializr-web/src/test/java/io/spring/initializr/web/mapper/InitializrMetadataV21JsonMapperTests.java @@ -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. @@ -18,8 +18,10 @@ package io.spring.initializr.web.mapper; import java.io.IOException; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import io.spring.initializr.generator.test.InitializrMetadataTestBuilder; import io.spring.initializr.metadata.Dependency; import io.spring.initializr.metadata.InitializrMetadata; @@ -29,13 +31,15 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** + * Tests for {@link InitializrMetadataV21JsonMapper}. + * * @author Stephane Nicoll */ -class InitializrMetadataJsonMapperTests { +class InitializrMetadataV21JsonMapperTests { private static final ObjectMapper objectMapper = new ObjectMapper(); - private final InitializrMetadataJsonMapper jsonMapper = new InitializrMetadataV21JsonMapper(); + private final InitializrMetadataV21JsonMapper jsonMapper = new InitializrMetadataV21JsonMapper(); @Test void withNoAppUrl() throws IOException { @@ -74,6 +78,42 @@ class InitializrMetadataJsonMapperTests { assertThat(second).isGreaterThan(0); } + @Test + void versionRangesUsingSemVerUseBackwardCompatibleFormat() { + Dependency dependency = Dependency.withId("test"); + dependency.setCompatibilityRange("[1.1.1-RC1,1.2.0-M1)"); + dependency.resolve(); + ObjectNode node = this.jsonMapper.mapDependency(dependency); + assertThat(node.get("versionRange").textValue()).isEqualTo("[1.1.1.RC1,1.2.0.M1)"); + } + + @Test + void versionRangesUsingSemVerSnapshotReplacedByBackwardCompatibleSnapshotQualifier() { + Dependency dependency = Dependency.withId("test"); + dependency.setCompatibilityRange("1.2.0-SNAPSHOT"); + dependency.resolve(); + ObjectNode node = this.jsonMapper.mapDependency(dependency); + assertThat(node.get("versionRange").textValue()).isEqualTo("1.2.0.BUILD-SNAPSHOT"); + } + + @Test + void platformVersionUsingSemVerUseBackwardCompatibleFormat() throws JsonProcessingException { + InitializrMetadata metadata = new InitializrMetadataTestBuilder().addBootVersion("2.5.0-SNAPSHOT", false) + .addBootVersion("2.5.0-M2", false).addBootVersion("2.4.2", true).build(); + String json = this.jsonMapper.write(metadata, null); + JsonNode result = objectMapper.readTree(json); + JsonNode versions = result.get("bootVersion").get("values"); + assertThat(versions).hasSize(3); + assertVersionMetadata(versions.get(0), "2.5.0.BUILD-SNAPSHOT", "2.5.0-SNAPSHOT"); + assertVersionMetadata(versions.get(1), "2.5.0.M2", "2.5.0-M2"); + assertVersionMetadata(versions.get(2), "2.4.2.RELEASE", "2.4.2"); + } + + private void assertVersionMetadata(JsonNode node, String id, String name) { + assertThat(node.get("id").textValue()).isEqualTo(id); + assertThat(node.get("name").textValue()).isEqualTo(name); + } + private Object get(JsonNode result, String path) { String[] nodes = path.split("\\."); for (int i = 0; i < nodes.length - 1; i++) { diff --git a/initializr-web/src/test/java/io/spring/initializr/web/mapper/InitializrMetadataV22JsonMapperTests.java b/initializr-web/src/test/java/io/spring/initializr/web/mapper/InitializrMetadataV22JsonMapperTests.java new file mode 100755 index 00000000..0c5b64fb --- /dev/null +++ b/initializr-web/src/test/java/io/spring/initializr/web/mapper/InitializrMetadataV22JsonMapperTests.java @@ -0,0 +1,86 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://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 io.spring.initializr.web.mapper; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.spring.initializr.generator.test.InitializrMetadataTestBuilder; +import io.spring.initializr.metadata.Dependency; +import io.spring.initializr.metadata.InitializrMetadata; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link InitializrMetadataV22JsonMapper}. + * + * @author Stephane Nicoll + */ +class InitializrMetadataV22JsonMapperTests { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private final InitializrMetadataV22JsonMapper jsonMapper = new InitializrMetadataV22JsonMapper(); + + @Test + void versionRangesUsingSemVerIsNotChanged() { + Dependency dependency = Dependency.withId("test"); + dependency.setCompatibilityRange("[1.1.1-RC1,1.2.0-M1)"); + dependency.resolve(); + ObjectNode node = this.jsonMapper.mapDependency(dependency); + assertThat(node.get("versionRange").textValue()).isEqualTo("[1.1.1-RC1,1.2.0-M1)"); + } + + @Test + void versionRangesUsingSemVerSnapshotIsNotChanged() { + Dependency dependency = Dependency.withId("test"); + dependency.setCompatibilityRange("1.2.0-SNAPSHOT"); + dependency.resolve(); + ObjectNode node = this.jsonMapper.mapDependency(dependency); + assertThat(node.get("versionRange").textValue()).isEqualTo("1.2.0-SNAPSHOT"); + } + + @Test + void platformVersionUsingSemVerUIsNotChanged() throws JsonProcessingException { + InitializrMetadata metadata = new InitializrMetadataTestBuilder().addBootVersion("2.5.0-SNAPSHOT", false) + .addBootVersion("2.5.0-M2", false).addBootVersion("2.4.2", true).build(); + String json = this.jsonMapper.write(metadata, null); + JsonNode result = objectMapper.readTree(json); + JsonNode versions = result.get("bootVersion").get("values"); + assertThat(versions).hasSize(3); + assertVersionMetadata(versions.get(0), "2.5.0-SNAPSHOT", "2.5.0-SNAPSHOT"); + assertVersionMetadata(versions.get(1), "2.5.0-M2", "2.5.0-M2"); + assertVersionMetadata(versions.get(2), "2.4.2", "2.4.2"); + } + + private void assertVersionMetadata(JsonNode node, String id, String name) { + assertThat(node.get("id").textValue()).isEqualTo(id); + assertThat(node.get("name").textValue()).isEqualTo(name); + } + + private Object get(JsonNode result, String path) { + String[] nodes = path.split("\\."); + for (int i = 0; i < nodes.length - 1; i++) { + String node = nodes[i]; + result = result.path(node); + } + return result.get(nodes[nodes.length - 1]).textValue(); + } + +} diff --git a/initializr-web/src/test/resources/application-test-default.yml b/initializr-web/src/test/resources/application-test-default.yml index 948dc2b0..ca97aaf8 100644 --- a/initializr-web/src/test/resources/application-test-default.yml +++ b/initializr-web/src/test/resources/application-test-default.yml @@ -90,7 +90,7 @@ initializr: id: org.acme:bur version: 2.1.0 scope: test - compatibilityRange: "[2.1.4.RELEASE,2.2.0.BUILD-SNAPSHOT)" + compatibilityRange: "[2.1.4.RELEASE,2.4.0-SNAPSHOT)" - name: My API id : my-api groupId: org.acme @@ -152,7 +152,7 @@ initializr: default: false bootVersions: - name : Latest SNAPSHOT - id: 2.2.0.BUILD-SNAPSHOT + id: 2.4.0-SNAPSHOT default: false - name: 2.1.4 id: 2.1.4.RELEASE diff --git a/initializr-web/src/test/resources/metadata/config/test-default.json b/initializr-web/src/test/resources/metadata/config/test-default.json index 142d1e2c..f81d0648 100644 --- a/initializr-web/src/test/resources/metadata/config/test-default.json +++ b/initializr-web/src/test/resources/metadata/config/test-default.json @@ -10,7 +10,7 @@ "content": [ { "default": false, - "id": "2.2.0.BUILD-SNAPSHOT", + "id": "2.4.0-SNAPSHOT", "name": "Latest SNAPSHOT" }, { @@ -230,7 +230,7 @@ "name": "Bur", "scope": "test", "version": "2.1.0", - "compatibilityRange": "[2.1.4.RELEASE,2.2.0.BUILD-SNAPSHOT)" + "compatibilityRange": "[2.1.4.RELEASE,2.4.0-SNAPSHOT)" }, { "starter": true, diff --git a/initializr-web/src/test/resources/metadata/dependencies/test-dependencies-2.2.1.json b/initializr-web/src/test/resources/metadata/dependencies/test-dependencies-2.4.1.json similarity index 97% rename from initializr-web/src/test/resources/metadata/dependencies/test-dependencies-2.2.1.json rename to initializr-web/src/test/resources/metadata/dependencies/test-dependencies-2.4.1.json index 565656e6..596f7148 100644 --- a/initializr-web/src/test/resources/metadata/dependencies/test-dependencies-2.2.1.json +++ b/initializr-web/src/test/resources/metadata/dependencies/test-dependencies-2.4.1.json @@ -1,5 +1,5 @@ { - "bootVersion": "2.2.1.RELEASE", + "bootVersion": "2.4.1.RELEASE", "repositories": { "my-api-repo-2": { "name": "repo2", diff --git a/initializr-web/src/test/resources/metadata/test-default-2.0.0-ssl.json b/initializr-web/src/test/resources/metadata/test-default-2.0.0-ssl.json index 3f1b431e..5ed2dcb5 100644 --- a/initializr-web/src/test/resources/metadata/test-default-2.0.0-ssl.json +++ b/initializr-web/src/test/resources/metadata/test-default-2.0.0-ssl.json @@ -154,7 +154,7 @@ "default": "2.1.4.RELEASE", "values": [ { - "id": "2.2.0.BUILD-SNAPSHOT", + "id": "2.4.0.BUILD-SNAPSHOT", "name": "Latest SNAPSHOT" }, { diff --git a/initializr-web/src/test/resources/metadata/test-default-2.0.0.json b/initializr-web/src/test/resources/metadata/test-default-2.0.0.json index ed3c0989..8b2c3263 100644 --- a/initializr-web/src/test/resources/metadata/test-default-2.0.0.json +++ b/initializr-web/src/test/resources/metadata/test-default-2.0.0.json @@ -154,7 +154,7 @@ "default": "2.1.4.RELEASE", "values": [ { - "id": "2.2.0.BUILD-SNAPSHOT", + "id": "2.4.0.BUILD-SNAPSHOT", "name": "Latest SNAPSHOT" }, { diff --git a/initializr-web/src/test/resources/metadata/test-default-2.1.0-ssl.json b/initializr-web/src/test/resources/metadata/test-default-2.1.0-ssl.json index aaf50df4..d005ee74 100644 --- a/initializr-web/src/test/resources/metadata/test-default-2.1.0-ssl.json +++ b/initializr-web/src/test/resources/metadata/test-default-2.1.0-ssl.json @@ -85,7 +85,7 @@ { "id": "org.acme:bur", "name": "Bur", - "versionRange": "[2.1.4.RELEASE,2.2.0.BUILD-SNAPSHOT)" + "versionRange": "[2.1.4.RELEASE,2.4.0.BUILD-SNAPSHOT)" }, { "id": "my-api", @@ -192,7 +192,7 @@ "default": "2.1.4.RELEASE", "values": [ { - "id": "2.2.0.BUILD-SNAPSHOT", + "id": "2.4.0.BUILD-SNAPSHOT", "name": "Latest SNAPSHOT" }, { diff --git a/initializr-web/src/test/resources/metadata/test-default-2.1.0.json b/initializr-web/src/test/resources/metadata/test-default-2.1.0.json index 7d0abdf0..150752ef 100644 --- a/initializr-web/src/test/resources/metadata/test-default-2.1.0.json +++ b/initializr-web/src/test/resources/metadata/test-default-2.1.0.json @@ -85,7 +85,7 @@ { "id": "org.acme:bur", "name": "Bur", - "versionRange": "[2.1.4.RELEASE,2.2.0.BUILD-SNAPSHOT)" + "versionRange": "[2.1.4.RELEASE,2.4.0.BUILD-SNAPSHOT)" }, { "id": "my-api", @@ -192,7 +192,7 @@ "default": "2.1.4.RELEASE", "values": [ { - "id": "2.2.0.BUILD-SNAPSHOT", + "id": "2.4.0.BUILD-SNAPSHOT", "name": "Latest SNAPSHOT" }, { diff --git a/initializr-web/src/test/resources/metadata/test-default-2.2.0.json b/initializr-web/src/test/resources/metadata/test-default-2.2.0.json new file mode 100644 index 00000000..df76991c --- /dev/null +++ b/initializr-web/src/test/resources/metadata/test-default-2.2.0.json @@ -0,0 +1,232 @@ +{ + "_links": { + "dependencies": { + "href": "http://@host@/dependencies{?bootVersion}", + "templated": true + }, + "maven-build": { + "href": "http://@host@/pom.xml?type=maven-build{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}", + "templated": true + }, + "maven-project": { + "href": "http://@host@/starter.zip?type=maven-project{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}", + "templated": true + }, + "gradle-build": { + "href": "http://@host@/build.gradle?type=gradle-build{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}", + "templated": true + }, + "gradle-project": { + "href": "http://@host@/starter.zip?type=gradle-project{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}", + "templated": true + } + }, + "dependencies": { + "type": "hierarchical-multi-select", + "values": [ + { + "name": "Core", + "values": [ + { + "id": "web", + "name": "Web", + "description": "Web dependency description", + "_links": { + "guide": { + "href": "https://example.com/guide", + "title": "Building a RESTful Web Service" + }, + "reference": { + "href": "https://example.com/doc" + } + } + }, + { + "id": "security", + "name": "Security" + }, + { + "id": "data-jpa", + "name": "Data JPA" + } + ] + }, + { + "name": "Other", + "values": [ + { + "id": "org.acme:foo", + "name": "Foo", + "_links": { + "guide": [ + { + "href": "https://example.com/guide1" + }, + { + "href": "https://example.com/guide2", + "title": "Some guide for foo" + } + ], + "reference": { + "href": "https://example.com/{bootVersion}/doc", + "templated": true + } + } + }, + { + "id": "org.acme:bar", + "name": "Bar" + }, + { + "id": "org.acme:biz", + "name": "Biz", + "versionRange": "2.2.0.BUILD-SNAPSHOT" + }, + { + "id": "org.acme:bur", + "name": "Bur", + "versionRange": "[2.1.4.RELEASE,2.4.0-SNAPSHOT)" + }, + { + "id": "my-api", + "name": "My API" + } + ] + } + ] + }, + "type": { + "type": "action", + "default": "maven-project", + "values": [ + { + "id": "maven-build", + "name": "Maven POM", + "action": "/pom.xml", + "tags": { + "build": "maven", + "format": "build" + } + }, + { + "id": "maven-project", + "name": "Maven Project", + "action": "/starter.zip", + "tags": { + "build": "maven", + "format": "project" + } + }, + { + "id": "gradle-build", + "name": "Gradle Config", + "action": "/build.gradle", + "tags": { + "build": "gradle", + "format": "build" + } + }, + { + "id": "gradle-project", + "name": "Gradle Project", + "action": "/starter.zip", + "tags": { + "build": "gradle", + "format": "project" + } + } + ] + }, + "packaging": { + "type": "single-select", + "default": "jar", + "values": [ + { + "id": "jar", + "name": "Jar" + }, + { + "id": "war", + "name": "War" + } + ] + }, + "javaVersion": { + "type": "single-select", + "default": "1.8", + "values": [ + { + "id": "1.6", + "name": "1.6" + }, + { + "id": "1.7", + "name": "1.7" + }, + { + "id": "1.8", + "name": "1.8" + } + ] + }, + "language": { + "type": "single-select", + "default": "java", + "values": [ + { + "id": "groovy", + "name": "Groovy" + }, + { + "id": "java", + "name": "Java" + }, + { + "id": "kotlin", + "name": "Kotlin" + } + ] + }, + "bootVersion": { + "type": "single-select", + "default": "2.1.4.RELEASE", + "values": [ + { + "id": "2.4.0-SNAPSHOT", + "name": "Latest SNAPSHOT" + }, + { + "id": "2.1.4.RELEASE", + "name": "2.1.4" + }, + { + "id": "1.5.17.RELEASE", + "name": "1.5.17" + } + ] + }, + "groupId": { + "type": "text", + "default": "com.example" + }, + "artifactId": { + "type": "text", + "default": "demo" + }, + "version": { + "type": "text", + "default": "0.0.1-SNAPSHOT" + }, + "name": { + "type": "text", + "default": "demo" + }, + "description": { + "type": "text", + "default": "Demo project for Spring Boot" + }, + "packageName": { + "type": "text", + "default": "com.example.demo" + } +} \ No newline at end of file From 853ee51f2b096200ffe55772a80e992c44da518a Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 2 Jun 2020 21:07:11 +0200 Subject: [PATCH 3/4] Add support for transforming the chosen platform version This commit improves the ProjectRequest converter to invoke a ProjectRequestPlatformVersionTransformer. A default implementation does the conversion based on configurable ranges for the V1 and V2 format respectively. This complement our backward compatible support with 2.1 metadata on an instance using the new version format. All that is required is to configure which versions are using which format. See gh-1092 --- .../test/InitializrMetadataTestBuilder.java | 12 +- .../metadata/InitializrConfiguration.java | 157 ++++++++++++++---- .../InitializrAutoConfiguration.java | 9 +- .../controller/ProjectMetadataController.java | 8 +- ...jectRequestPlatformVersionTransformer.java | 35 ++++ ...tProjectRequestToDescriptionConverter.java | 61 ++++--- ...jectRequestPlatformVersionTransformer.java | 38 +++++ ...onControllerCustomEnvIntegrationTests.java | 2 +- ...tomVersionTransformerIntegrationTests.java | 45 +++++ ...equestPlatformVersionTransformerTests.java | 80 +++++++++ ...ectRequestToDescriptionConverterTests.java | 18 +- .../resources/application-test-custom-env.yml | 5 +- .../metadata/config/test-default.json | 6 +- 13 files changed, 406 insertions(+), 70 deletions(-) create mode 100644 initializr-web/src/main/java/io/spring/initializr/web/project/DefaultProjectRequestPlatformVersionTransformer.java create mode 100644 initializr-web/src/main/java/io/spring/initializr/web/project/ProjectRequestPlatformVersionTransformer.java create mode 100644 initializr-web/src/test/java/io/spring/initializr/web/controller/ProjectGenerationControllerCustomVersionTransformerIntegrationTests.java create mode 100644 initializr-web/src/test/java/io/spring/initializr/web/project/DefaultProjectRequestPlatformVersionTransformerTests.java diff --git a/initializr-generator-test/src/main/java/io/spring/initializr/generator/test/InitializrMetadataTestBuilder.java b/initializr-generator-test/src/main/java/io/spring/initializr/generator/test/InitializrMetadataTestBuilder.java index 627ec2f0..70fcc547 100644 --- a/initializr-generator-test/src/main/java/io/spring/initializr/generator/test/InitializrMetadataTestBuilder.java +++ b/initializr-generator-test/src/main/java/io/spring/initializr/generator/test/InitializrMetadataTestBuilder.java @@ -26,6 +26,7 @@ import io.spring.initializr.metadata.Dependency; import io.spring.initializr.metadata.DependencyGroup; import io.spring.initializr.metadata.InitializrConfiguration.Env.Kotlin; import io.spring.initializr.metadata.InitializrConfiguration.Env.Maven.ParentPom; +import io.spring.initializr.metadata.InitializrConfiguration.Platform; import io.spring.initializr.metadata.InitializrMetadata; import io.spring.initializr.metadata.InitializrMetadataBuilder; import io.spring.initializr.metadata.Repository; @@ -188,7 +189,16 @@ public class InitializrMetadataTestBuilder { public InitializrMetadataTestBuilder setPlatformCompatibilityRange(String platformCompatibilityRange) { this.builder.withCustomizer( - (it) -> it.getConfiguration().getEnv().setPlatformCompatibilityRange(platformCompatibilityRange)); + (it) -> it.getConfiguration().getEnv().getPlatform().setCompatibilityRange(platformCompatibilityRange)); + return this; + } + + public InitializrMetadataTestBuilder setPlatformVersionFormatCompatibilityRange(String v1Range, String v2Range) { + this.builder.withCustomizer((it) -> { + Platform platform = it.getConfiguration().getEnv().getPlatform(); + platform.setV1FormatCompatibilityRange(v1Range); + platform.setV2FormatCompatibilityRange(v2Range); + }); return this; } diff --git a/initializr-metadata/src/main/java/io/spring/initializr/metadata/InitializrConfiguration.java b/initializr-metadata/src/main/java/io/spring/initializr/metadata/InitializrConfiguration.java index ab70810e..f8260f58 100644 --- a/initializr-metadata/src/main/java/io/spring/initializr/metadata/InitializrConfiguration.java +++ b/initializr-metadata/src/main/java/io/spring/initializr/metadata/InitializrConfiguration.java @@ -30,6 +30,7 @@ import javax.lang.model.SourceVersion; import com.fasterxml.jackson.annotation.JsonIgnore; import io.spring.initializr.generator.version.InvalidVersionException; import io.spring.initializr.generator.version.Version; +import io.spring.initializr.generator.version.Version.Format; import io.spring.initializr.generator.version.VersionParser; import io.spring.initializr.generator.version.VersionRange; @@ -210,16 +211,6 @@ public class InitializrConfiguration { */ private boolean forceSsl; - /** - * Compatibility range of supported platform versions. Requesting metadata or - * project generation with a platform version that does not match this range is - * not supported. - */ - private String platformCompatibilityRange; - - @JsonIgnore - private VersionRange compatibilityRange; - /** * The "BillOfMaterials" that are referenced in this instance, identified by an * arbitrary identifier that can be used in the dependencies definition. @@ -250,6 +241,12 @@ public class InitializrConfiguration { @NestedConfigurationProperty private final Maven maven = new Maven(); + /** + * Platform-specific settings. + */ + @NestedConfigurationProperty + private final Platform platform = new Platform(); + public Env() { try { this.repositories.put("spring-snapshots", @@ -310,14 +307,6 @@ public class InitializrConfiguration { this.forceSsl = forceSsl; } - public String getPlatformCompatibilityRange() { - return this.platformCompatibilityRange; - } - - public void setPlatformCompatibilityRange(String platformCompatibilityRange) { - this.platformCompatibilityRange = platformCompatibilityRange; - } - public String getArtifactRepository() { return this.artifactRepository; } @@ -342,6 +331,10 @@ public class InitializrConfiguration { return this.maven; } + public Platform getPlatform() { + return this.platform; + } + public void setArtifactRepository(String artifactRepository) { if (!artifactRepository.endsWith("/")) { artifactRepository = artifactRepository + "/"; @@ -359,8 +352,7 @@ public class InitializrConfiguration { public void updateCompatibilityRange(VersionParser versionParser) { this.getBoms().values().forEach((it) -> it.updateCompatibilityRange(versionParser)); this.getKotlin().updateCompatibilityRange(versionParser); - this.compatibilityRange = (this.platformCompatibilityRange != null) - ? versionParser.parseRange(this.platformCompatibilityRange) : null; + this.getPlatform().updateCompatibilityRange(versionParser); } public void merge(Env other) { @@ -370,29 +362,14 @@ public class InitializrConfiguration { this.fallbackApplicationName = other.fallbackApplicationName; this.invalidApplicationNames = other.invalidApplicationNames; this.forceSsl = other.forceSsl; - this.platformCompatibilityRange = other.platformCompatibilityRange; - this.compatibilityRange = other.compatibilityRange; this.gradle.merge(other.gradle); this.kotlin.merge(other.kotlin); this.maven.merge(other.maven); + this.platform.merge(other.platform); other.boms.forEach(this.boms::putIfAbsent); other.repositories.forEach(this.repositories::putIfAbsent); } - /** - * Specify whether the specified {@linkplain Version platform version} is - * supported. - * @param platformVersion the platform version to check - * @return {@code true} if this version is supported, {@code false} otherwise - */ - public boolean isCompatiblePlatformVersion(Version platformVersion) { - return (this.compatibilityRange == null || this.compatibilityRange.match(platformVersion)); - } - - public String determinePlatformCompatibilityRangeRequirement() { - return this.compatibilityRange.toString(); - } - /** * Gradle details. */ @@ -662,4 +639,112 @@ public class InitializrConfiguration { } + /** + * Platform-specific settings. + */ + public static class Platform { + + /** + * Compatibility range of supported platform versions. Requesting metadata or + * project generation with a platform version that does not match this range is + * not supported. + */ + private String compatibilityRange; + + @JsonIgnore + private VersionRange range; + + /** + * Compatibility range of platform versions using the first version format. + */ + private String v1FormatCompatibilityRange; + + @JsonIgnore + private VersionRange v1FormatRange; + + /** + * Compatibility range of platform versions using the second version format. + */ + private String v2FormatCompatibilityRange; + + @JsonIgnore + private VersionRange v2FormatRange; + + public void updateCompatibilityRange(VersionParser versionParser) { + this.range = (this.compatibilityRange != null) ? versionParser.parseRange(this.compatibilityRange) : null; + this.v1FormatRange = (this.v1FormatCompatibilityRange != null) + ? versionParser.parseRange(this.v1FormatCompatibilityRange) : null; + this.v2FormatRange = (this.v2FormatCompatibilityRange != null) + ? versionParser.parseRange(this.v2FormatCompatibilityRange) : null; + } + + private void merge(Platform other) { + this.compatibilityRange = other.compatibilityRange; + this.range = other.range; + this.v1FormatCompatibilityRange = other.v1FormatCompatibilityRange; + this.v1FormatRange = other.v1FormatRange; + this.v2FormatCompatibilityRange = other.v2FormatCompatibilityRange; + this.v2FormatRange = other.v2FormatRange; + } + + /** + * Specify whether the specified {@linkplain Version platform version} is + * supported. + * @param platformVersion the platform version to check + * @return {@code true} if this version is supported, {@code false} otherwise + */ + public boolean isCompatibleVersion(Version platformVersion) { + return (this.range == null || this.range.match(platformVersion)); + } + + public String determineCompatibilityRangeRequirement() { + return this.range.toString(); + } + + /** + * Format the expected {@link Version platform version}. + * @param platformVersion a platform version + * @return a platform version in the suitable format + */ + public Version formatPlatformVersion(Version platformVersion) { + Format format = getExpectedVersionFormat(platformVersion); + return platformVersion.format(format); + } + + private Format getExpectedVersionFormat(Version version) { + if (this.v2FormatRange != null && this.v2FormatRange.match(version)) { + return Format.V2; + } + if (this.v1FormatRange != null && this.v1FormatRange.match(version)) { + return Format.V1; + } + return version.getFormat(); + } + + public String getCompatibilityRange() { + return this.compatibilityRange; + } + + public void setCompatibilityRange(String compatibilityRange) { + this.compatibilityRange = compatibilityRange; + } + + public String getV1FormatCompatibilityRange() { + return this.v1FormatCompatibilityRange; + } + + public void setV1FormatCompatibilityRange(String v1FormatCompatibilityRange) { + this.v1FormatCompatibilityRange = v1FormatCompatibilityRange; + } + + public String getV2FormatCompatibilityRange() { + return this.v2FormatCompatibilityRange; + } + + public void setV2FormatCompatibilityRange(String v2FormatCompatibilityRange) { + this.v2FormatCompatibilityRange = v2FormatCompatibilityRange; + } + + } + } diff --git a/initializr-web/src/main/java/io/spring/initializr/web/autoconfigure/InitializrAutoConfiguration.java b/initializr-web/src/main/java/io/spring/initializr/web/autoconfigure/InitializrAutoConfiguration.java index 552fc3bd..5b2c6f92 100644 --- a/initializr-web/src/main/java/io/spring/initializr/web/autoconfigure/InitializrAutoConfiguration.java +++ b/initializr-web/src/main/java/io/spring/initializr/web/autoconfigure/InitializrAutoConfiguration.java @@ -38,9 +38,11 @@ import io.spring.initializr.web.controller.DefaultProjectGenerationController; import io.spring.initializr.web.controller.ProjectGenerationController; import io.spring.initializr.web.controller.ProjectMetadataController; import io.spring.initializr.web.controller.SpringCliDistributionController; +import io.spring.initializr.web.project.DefaultProjectRequestPlatformVersionTransformer; import io.spring.initializr.web.project.DefaultProjectRequestToDescriptionConverter; import io.spring.initializr.web.project.ProjectGenerationInvoker; import io.spring.initializr.web.project.ProjectRequest; +import io.spring.initializr.web.project.ProjectRequestPlatformVersionTransformer; import io.spring.initializr.web.support.DefaultDependencyMetadataProvider; import io.spring.initializr.web.support.DefaultInitializrMetadataProvider; import io.spring.initializr.web.support.DefaultInitializrMetadataUpdateStrategy; @@ -144,9 +146,12 @@ public class InitializrAutoConfiguration { @Bean @ConditionalOnMissingBean ProjectGenerationController projectGenerationController( - InitializrMetadataProvider metadataProvider, ApplicationContext applicationContext) { + InitializrMetadataProvider metadataProvider, + ObjectProvider platformVersionTransformer, + ApplicationContext applicationContext) { ProjectGenerationInvoker projectGenerationInvoker = new ProjectGenerationInvoker<>( - applicationContext, new DefaultProjectRequestToDescriptionConverter()); + applicationContext, new DefaultProjectRequestToDescriptionConverter(platformVersionTransformer + .getIfAvailable(DefaultProjectRequestPlatformVersionTransformer::new))); return new DefaultProjectGenerationController(metadataProvider, projectGenerationInvoker); } diff --git a/initializr-web/src/main/java/io/spring/initializr/web/controller/ProjectMetadataController.java b/initializr-web/src/main/java/io/spring/initializr/web/controller/ProjectMetadataController.java index c5ec6691..692d9fec 100644 --- a/initializr-web/src/main/java/io/spring/initializr/web/controller/ProjectMetadataController.java +++ b/initializr-web/src/main/java/io/spring/initializr/web/controller/ProjectMetadataController.java @@ -24,7 +24,7 @@ import javax.servlet.http.HttpServletResponse; import io.spring.initializr.generator.version.Version; import io.spring.initializr.metadata.DependencyMetadata; import io.spring.initializr.metadata.DependencyMetadataProvider; -import io.spring.initializr.metadata.InitializrConfiguration.Env; +import io.spring.initializr.metadata.InitializrConfiguration.Platform; import io.spring.initializr.metadata.InitializrMetadata; import io.spring.initializr.metadata.InitializrMetadataProvider; import io.spring.initializr.metadata.InvalidInitializrMetadataException; @@ -126,10 +126,10 @@ public class ProjectMetadataController extends AbstractMetadataController { InitializrMetadata metadata = this.metadataProvider.get(); Version v = (bootVersion != null) ? Version.parse(bootVersion) : Version.parse(metadata.getBootVersions().getDefault().getId()); - Env env = metadata.getConfiguration().getEnv(); - if (!env.isCompatiblePlatformVersion(v)) { + Platform platform = metadata.getConfiguration().getEnv().getPlatform(); + if (!platform.isCompatibleVersion(v)) { throw new InvalidProjectRequestException("Invalid Spring Boot version '" + bootVersion - + "', Spring Boot compatibility range is " + env.determinePlatformCompatibilityRangeRequirement()); + + "', Spring Boot compatibility range is " + platform.determineCompatibilityRangeRequirement()); } DependencyMetadata dependencyMetadata = this.dependencyMetadataProvider.get(metadata, v); String content = new DependencyMetadataV21JsonMapper().write(dependencyMetadata); diff --git a/initializr-web/src/main/java/io/spring/initializr/web/project/DefaultProjectRequestPlatformVersionTransformer.java b/initializr-web/src/main/java/io/spring/initializr/web/project/DefaultProjectRequestPlatformVersionTransformer.java new file mode 100644 index 00000000..cc9299e6 --- /dev/null +++ b/initializr-web/src/main/java/io/spring/initializr/web/project/DefaultProjectRequestPlatformVersionTransformer.java @@ -0,0 +1,35 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://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 io.spring.initializr.web.project; + +import io.spring.initializr.generator.version.Version; +import io.spring.initializr.metadata.InitializrMetadata; + +/** + * A default {@link DefaultProjectRequestPlatformVersionTransformer} that uses configured + * ranges to format the version if necessary. + * + * @author Stephane Nicoll + */ +public class DefaultProjectRequestPlatformVersionTransformer implements ProjectRequestPlatformVersionTransformer { + + @Override + public Version transform(Version platformVersion, InitializrMetadata metadata) { + return metadata.getConfiguration().getEnv().getPlatform().formatPlatformVersion(platformVersion); + } + +} diff --git a/initializr-web/src/main/java/io/spring/initializr/web/project/DefaultProjectRequestToDescriptionConverter.java b/initializr-web/src/main/java/io/spring/initializr/web/project/DefaultProjectRequestToDescriptionConverter.java index d5b93870..a066b504 100644 --- a/initializr-web/src/main/java/io/spring/initializr/web/project/DefaultProjectRequestToDescriptionConverter.java +++ b/initializr-web/src/main/java/io/spring/initializr/web/project/DefaultProjectRequestToDescriptionConverter.java @@ -27,22 +27,38 @@ import io.spring.initializr.generator.project.ProjectDescription; import io.spring.initializr.generator.version.Version; import io.spring.initializr.metadata.DefaultMetadataElement; import io.spring.initializr.metadata.Dependency; -import io.spring.initializr.metadata.InitializrConfiguration.Env; +import io.spring.initializr.metadata.InitializrConfiguration.Platform; import io.spring.initializr.metadata.InitializrMetadata; import io.spring.initializr.metadata.Type; import io.spring.initializr.metadata.support.MetadataBuildItemMapper; +import org.springframework.util.Assert; + /** * A default {@link ProjectRequestToDescriptionConverter} implementation that uses the * {@link InitializrMetadata metadata} to set default values for missing attributes if - * necessary. + * necessary. Transparently transform the platform version if necessary using a + * {@link ProjectRequestPlatformVersionTransformer}. * * @author Madhura Bhave * @author HaiTao Zhang + * @author Stephane Nicoll */ public class DefaultProjectRequestToDescriptionConverter implements ProjectRequestToDescriptionConverter { + private final ProjectRequestPlatformVersionTransformer platformVersionTransformer; + + public DefaultProjectRequestToDescriptionConverter() { + this((version, metadata) -> version); + } + + public DefaultProjectRequestToDescriptionConverter( + ProjectRequestPlatformVersionTransformer platformVersionTransformer) { + Assert.notNull(platformVersionTransformer, "PlatformVersionTransformer must not be null"); + this.platformVersionTransformer = platformVersionTransformer; + } + @Override public ProjectDescription convert(ProjectRequest request, InitializrMetadata metadata) { MutableProjectDescription description = new MutableProjectDescription(); @@ -60,9 +76,9 @@ public class DefaultProjectRequestToDescriptionConverter */ public void convert(ProjectRequest request, MutableProjectDescription description, InitializrMetadata metadata) { validate(request, metadata); - String springBootVersion = getSpringBootVersion(request, metadata); - List resolvedDependencies = getResolvedDependencies(request, springBootVersion, metadata); - validateDependencyRange(springBootVersion, resolvedDependencies); + Version platformVersion = getPlatformVersion(request, metadata); + List resolvedDependencies = getResolvedDependencies(request, platformVersion, metadata); + validateDependencyRange(platformVersion, resolvedDependencies); description.setApplicationName(request.getApplicationName()); description.setArtifactId(request.getArtifactId()); @@ -74,26 +90,26 @@ public class DefaultProjectRequestToDescriptionConverter description.setName(request.getName()); description.setPackageName(request.getPackageName()); description.setPackaging(Packaging.forId(request.getPackaging())); - description.setPlatformVersion(Version.parse(springBootVersion)); + description.setPlatformVersion(platformVersion); description.setVersion(request.getVersion()); resolvedDependencies.forEach((dependency) -> description.addDependency(dependency.getId(), MetadataBuildItemMapper.toDependency(dependency))); } private void validate(ProjectRequest request, InitializrMetadata metadata) { - validateSpringBootVersion(request, metadata); + validatePlatformVersion(request, metadata); validateType(request.getType(), metadata); validateLanguage(request.getLanguage(), metadata); validatePackaging(request.getPackaging(), metadata); validateDependencies(request, metadata); } - private void validateSpringBootVersion(ProjectRequest request, InitializrMetadata metadata) { - Version bootVersion = Version.safeParse(request.getBootVersion()); - Env env = metadata.getConfiguration().getEnv(); - if (bootVersion != null && !env.isCompatiblePlatformVersion(bootVersion)) { - throw new InvalidProjectRequestException("Invalid Spring Boot version '" + bootVersion - + "', Spring Boot compatibility range is " + env.determinePlatformCompatibilityRangeRequirement()); + private void validatePlatformVersion(ProjectRequest request, InitializrMetadata metadata) { + Version platformVersion = Version.safeParse(request.getBootVersion()); + Platform platform = metadata.getConfiguration().getEnv().getPlatform(); + if (platformVersion != null && !platform.isCompatibleVersion(platformVersion)) { + throw new InvalidProjectRequestException("Invalid Spring Boot version '" + platformVersion + + "', Spring Boot compatibility range is " + platform.determineCompatibilityRangeRequirement()); } } @@ -139,11 +155,11 @@ public class DefaultProjectRequestToDescriptionConverter }); } - private void validateDependencyRange(String springBootVersion, List resolvedDependencies) { + private void validateDependencyRange(Version platformVersion, List resolvedDependencies) { resolvedDependencies.forEach((dep) -> { - if (!dep.match(Version.parse(springBootVersion))) { - throw new InvalidProjectRequestException("Dependency '" + dep.getId() + "' is not compatible " - + "with Spring Boot " + springBootVersion); + if (!dep.match(platformVersion)) { + throw new InvalidProjectRequestException( + "Dependency '" + dep.getId() + "' is not compatible " + "with Spring Boot " + platformVersion); } }); } @@ -153,18 +169,19 @@ public class DefaultProjectRequestToDescriptionConverter return BuildSystem.forId(typeFromMetadata.getTags().get("build")); } - private String getSpringBootVersion(ProjectRequest request, InitializrMetadata metadata) { - return (request.getBootVersion() != null) ? request.getBootVersion() + private Version getPlatformVersion(ProjectRequest request, InitializrMetadata metadata) { + String versionText = (request.getBootVersion() != null) ? request.getBootVersion() : metadata.getBootVersions().getDefault().getId(); + Version version = Version.parse(versionText); + return this.platformVersionTransformer.transform(version, metadata); } - private List getResolvedDependencies(ProjectRequest request, String springBootVersion, + private List getResolvedDependencies(ProjectRequest request, Version platformVersion, InitializrMetadata metadata) { List depIds = request.getDependencies(); - Version requestedVersion = Version.parse(springBootVersion); return depIds.stream().map((it) -> { Dependency dependency = metadata.getDependencies().get(it); - return dependency.resolve(requestedVersion); + return dependency.resolve(platformVersion); }).collect(Collectors.toList()); } diff --git a/initializr-web/src/main/java/io/spring/initializr/web/project/ProjectRequestPlatformVersionTransformer.java b/initializr-web/src/main/java/io/spring/initializr/web/project/ProjectRequestPlatformVersionTransformer.java new file mode 100644 index 00000000..45538bf0 --- /dev/null +++ b/initializr-web/src/main/java/io/spring/initializr/web/project/ProjectRequestPlatformVersionTransformer.java @@ -0,0 +1,38 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://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 io.spring.initializr.web.project; + +import io.spring.initializr.generator.version.Version; +import io.spring.initializr.metadata.InitializrMetadata; + +/** + * Strategy interface to transform the platform version of a {@link ProjectRequest}. + * + * @author Stephane Nicoll + */ +@FunctionalInterface +public interface ProjectRequestPlatformVersionTransformer { + + /** + * Transform the platform version of a {@link ProjectRequest} if necessary. + * @param platformVersion the candidate platform version + * @param metadata the metadata instance to use + * @return the platform version to use + */ + Version transform(Version platformVersion, InitializrMetadata metadata); + +} diff --git a/initializr-web/src/test/java/io/spring/initializr/web/controller/ProjectGenerationControllerCustomEnvIntegrationTests.java b/initializr-web/src/test/java/io/spring/initializr/web/controller/ProjectGenerationControllerCustomEnvIntegrationTests.java index b0c07e7a..e6be8d59 100755 --- a/initializr-web/src/test/java/io/spring/initializr/web/controller/ProjectGenerationControllerCustomEnvIntegrationTests.java +++ b/initializr-web/src/test/java/io/spring/initializr/web/controller/ProjectGenerationControllerCustomEnvIntegrationTests.java @@ -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. diff --git a/initializr-web/src/test/java/io/spring/initializr/web/controller/ProjectGenerationControllerCustomVersionTransformerIntegrationTests.java b/initializr-web/src/test/java/io/spring/initializr/web/controller/ProjectGenerationControllerCustomVersionTransformerIntegrationTests.java new file mode 100644 index 00000000..92294949 --- /dev/null +++ b/initializr-web/src/test/java/io/spring/initializr/web/controller/ProjectGenerationControllerCustomVersionTransformerIntegrationTests.java @@ -0,0 +1,45 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://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 io.spring.initializr.web.controller; + +import io.spring.initializr.generator.test.project.ProjectStructure; +import io.spring.initializr.web.AbstractInitializrControllerIntegrationTests; +import org.junit.jupiter.api.Test; + +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link ProjectGenerationController} with a custom platform + * version compatibility range. + * + * @author Stephane Nicoll + */ +@ActiveProfiles("test-default") +@TestPropertySource(properties = "initializr.env.platform.v2-format-compatibility-range=2.4.0-M1") +class ProjectGenerationControllerCustomVersionTransformerIntegrationTests + extends AbstractInitializrControllerIntegrationTests { + + @Test + void projectGenerationInvokeProjectRequestVersionTransformer() { + ProjectStructure project = downloadZip("/starter.zip?bootVersion=2.4.0.RELEASE"); + assertThat(project).mavenBuild().hasParent("org.springframework.boot", "spring-boot-starter-parent", "2.4.0"); + } + +} diff --git a/initializr-web/src/test/java/io/spring/initializr/web/project/DefaultProjectRequestPlatformVersionTransformerTests.java b/initializr-web/src/test/java/io/spring/initializr/web/project/DefaultProjectRequestPlatformVersionTransformerTests.java new file mode 100644 index 00000000..886f52f8 --- /dev/null +++ b/initializr-web/src/test/java/io/spring/initializr/web/project/DefaultProjectRequestPlatformVersionTransformerTests.java @@ -0,0 +1,80 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://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 io.spring.initializr.web.project; + +import io.spring.initializr.generator.test.InitializrMetadataTestBuilder; +import io.spring.initializr.generator.version.Version; +import io.spring.initializr.metadata.InitializrMetadata; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DefaultProjectRequestPlatformVersionTransformer}. + * + * @author Stephane Nicoll + */ +class DefaultProjectRequestPlatformVersionTransformerTests { + + private final DefaultProjectRequestPlatformVersionTransformer transformer = new DefaultProjectRequestPlatformVersionTransformer(); + + @Test + void formatV1WhenV2IsExpected() { + InitializrMetadata metadata = InitializrMetadataTestBuilder.withDefaults() + .setPlatformVersionFormatCompatibilityRange("[2.0.0.RELEASE,2.4.0-M1)", "2.4.0-M1").build(); + assertThat(this.transformer.transform(Version.parse("2.4.0.RELEASE"), metadata)).hasToString("2.4.0"); + } + + @Test + void formatV1WhenV1IsExpected() { + InitializrMetadata metadata = InitializrMetadataTestBuilder.withDefaults() + .setPlatformVersionFormatCompatibilityRange("[2.0.0.RELEASE,2.4.0-M1)", "2.4.0-M1").build(); + Version version = Version.parse("2.2.0.RELEASE"); + assertThat(this.transformer.transform(version, metadata)).isSameAs(version); + } + + @Test + void formatV2WhenV1IsExpected() { + InitializrMetadata metadata = InitializrMetadataTestBuilder.withDefaults() + .setPlatformVersionFormatCompatibilityRange("[2.0.0.RELEASE,2.4.0-M1)", "2.4.0-M1").build(); + assertThat(this.transformer.transform(Version.parse("2.3.0-SNAPSHOT"), metadata)) + .hasToString("2.3.0.BUILD-SNAPSHOT"); + } + + @Test + void formatV2WhenV2IsExpected() { + InitializrMetadata metadata = InitializrMetadataTestBuilder.withDefaults() + .setPlatformVersionFormatCompatibilityRange("[2.0.0.RELEASE,2.4.0-M1)", "2.4.0-M1").build(); + Version version = Version.parse("2.4.0"); + assertThat(this.transformer.transform(version, metadata)).isSameAs(version); + } + + @Test + void formatV1WhenNoRangeIsConfigured() { + InitializrMetadata metadata = InitializrMetadataTestBuilder.withDefaults().build(); + Version version = Version.parse("2.4.0.RELEASE"); + assertThat(this.transformer.transform(version, metadata)).isSameAs(version); + } + + @Test + void formatV2WhenNoRangeIsConfigured() { + InitializrMetadata metadata = InitializrMetadataTestBuilder.withDefaults().build(); + Version version = Version.parse("2.2.0-SNAPSHOT"); + assertThat(this.transformer.transform(version, metadata)).isSameAs(version); + } + +} diff --git a/initializr-web/src/test/java/io/spring/initializr/web/project/DefaultProjectRequestToDescriptionConverterTests.java b/initializr-web/src/test/java/io/spring/initializr/web/project/DefaultProjectRequestToDescriptionConverterTests.java index bd2b1902..f6d42b5f 100644 --- a/initializr-web/src/test/java/io/spring/initializr/web/project/DefaultProjectRequestToDescriptionConverterTests.java +++ b/initializr-web/src/test/java/io/spring/initializr/web/project/DefaultProjectRequestToDescriptionConverterTests.java @@ -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. @@ -30,6 +30,9 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * Tests for {@link DefaultProjectRequestToDescriptionConverter}. @@ -74,6 +77,19 @@ class DefaultProjectRequestToDescriptionConverterTests { .isEqualTo(Version.parse("1.5.9.RELEASE")); } + @Test + void convertShouldCallProjectRequestVersionTransformer() { + ProjectRequestPlatformVersionTransformer transformer = mock(ProjectRequestPlatformVersionTransformer.class); + Version v1Format = Version.parse("2.4.0.RELEASE"); + given(transformer.transform(v1Format, this.metadata)).willReturn(Version.parse("2.4.0")); + ProjectRequest request = createProjectRequest(); + request.setBootVersion("2.4.0.RELEASE"); + ProjectDescription description = new DefaultProjectRequestToDescriptionConverter(transformer).convert(request, + this.metadata); + assertThat(description.getPlatformVersion()).hasToString("2.4.0"); + verify(transformer).transform(v1Format, this.metadata); + } + @Test void convertWhenSpringBootVersionInvalidShouldThrowException() { this.metadata = InitializrMetadataTestBuilder.withDefaults() diff --git a/initializr-web/src/test/resources/application-test-custom-env.yml b/initializr-web/src/test/resources/application-test-custom-env.yml index 8676d91f..de11a9cb 100644 --- a/initializr-web/src/test/resources/application-test-custom-env.yml +++ b/initializr-web/src/test/resources/application-test-custom-env.yml @@ -5,6 +5,7 @@ initializr: fallbackApplicationName: FooBarApplication invalidApplicationNames: - InvalidApplication - platform-compatibility-range: "2.0.0.RELEASE" kotlin: - default-version: 1.0.0-beta-2423 \ No newline at end of file + default-version: 1.0.0-beta-2423 + platform: + compatibility-range: "2.0.0.RELEASE" diff --git a/initializr-web/src/test/resources/metadata/config/test-default.json b/initializr-web/src/test/resources/metadata/config/test-default.json index f81d0648..da57c7a6 100644 --- a/initializr-web/src/test/resources/metadata/config/test-default.json +++ b/initializr-web/src/test/resources/metadata/config/test-default.json @@ -33,7 +33,6 @@ "artifactRepository": "https://repo.spring.io/release/", "fallbackApplicationName": "Application", "forceSsl": false, - "platformCompatibilityRange": null, "gradle": { "dependencyManagementPluginVersion": "1.0.0.RELEASE" }, @@ -58,6 +57,11 @@ "includeSpringBootBom": false } }, + "platform": { + "compatibilityRange": null, + "v1FormatCompatibilityRange": null, + "v2FormatCompatibilityRange": null + }, "googleAnalyticsTrackingCode": null, "invalidApplicationNames": [ "SpringApplication", From b3f5ca9aabd42b976c3c8589ab4f705c98e887b0 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 3 Jun 2020 10:58:54 +0200 Subject: [PATCH 4/4] Document version format support See gh-1092 --- .../main/asciidoc/configuration-guide.adoc | 56 +++++++++++++++---- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/initializr-docs/src/main/asciidoc/configuration-guide.adoc b/initializr-docs/src/main/asciidoc/configuration-guide.adoc index 0f67ec5d..efe58772 100644 --- a/initializr-docs/src/main/asciidoc/configuration-guide.adoc +++ b/initializr-docs/src/main/asciidoc/configuration-guide.adoc @@ -542,21 +542,30 @@ it. The versions are *not* applied to the dependency itself, but rather used to the dependency, or modify it, when different versions of Spring Boot are selected for the generated project. -A typical version is composed of four parts: a major revision, a minor revision, a patch -revision and a qualifier. Qualifiers are ordered as follows: +A version is composed of four parts: a major revision, a minor revision, a patch +revision and an optional qualifier. Spring Initializr supports two version formats: + +* `V1` is the original format where the qualifier is separated from the version by a dot. +It also uses well-defined qualifiers for snapshots (`BUILD-SNAPSHOT`) and General +Availability (`RELEASE`). +* `V2` is an improved format that is SemVer compliant, and therefore uses a dash to +separate the qualifier. There is no qualifier for GAs. + +Speaking of qualifiers, they are ordered as follows: * `M` for milestones (e.g. `2.0.0.M1` is the first milestone of the upcoming 2.0.0 release): can be seen as "beta" release -* `RC` for release candidates (e.g. `2.0.0.RC2` is the second release candidate of +* `RC` for release candidates (e.g. `2.0.0-RC2` is the second release candidate of upcoming 2.0.0 release) -* `RELEASE` for general availability (e.g. `2.0.0.RELEASE` is 2.0.0 proper) * `BUILD-SNAPSHOT` for development build (`2.1.0.BUILD-SNAPSHOT` represents the latest -available development build of the upcoming 2.1.0 release). - -TIP: snapshots are in a bit special in that scheme as they always represents the "latest -state" of a release. `M1` represents the most oldest version for a given major, minor and -patch revisions. +available development build of the upcoming 2.1.0 release). For the `V2` format, it is +simply `SNAPSHOT`, i.e. `2.1.0-SNAPSHOT`. +* `RELEASE` for general availability (e.g. `2.0.0.RELEASE` is 2.0.0 proper) +TIP: snapshots are in a bit special in that scheme as they always represent the "latest +state" of a release. `M1` represents the oldest version for a given major, minor and +patch revisions, and it can therefore be safely used when referring to the "first" release +in that line. A version range has a lower and an upper bound, and if the bound is inclusive it is denoted as a square bracket (`[` or `]`), otherwise it is exclusive and denoted by a @@ -572,7 +581,7 @@ an hard-coded version. For instance, `1.4.x.BUILD-SNAPSHOT` is the latest snapsh of the 1.4.x line. For instance, if you want to restrict a dependency from `1.1.0.RELEASE` to the latest stable release of the 1.3.x line, you'd use `[1.1.0.RELEASE,1.3.x.RELEASE]`. -Snapshots are naturally ordered higher than released versions, so if you are looking to +Snapshots are naturally the most recent version of a given line, so if you are looking to match a dependency to only the latest snapshots of Spring Boot, you could use a version range of `1.5.x.BUILD-SNAPSHOT` (assuming 1.5 was the latest). @@ -580,7 +589,8 @@ TIP: Remember to quote the values of a version range in YAML configuration files double quotes ""). See below in the section on <> for more examples -and idioms. +and idioms. See also how the <> can +be configured. @@ -1017,6 +1027,30 @@ These dependencies, by default, will be available only for Spring Boot versions +[[howto-platform-version-format]] +=== Configure platform version format +Spring Initializr supports two formats: `V1` is the original format defined by metadata +up to `2.1`. `V2` is the SemVer format provided alongside `V1` as of metadata `2.2`. In +order to serve backward compatible content, the version range for each format should be +configured so that translations can happen accordingly. + +Let's assume that an instance only supports `2.0.0` and later and the platform version is +using the original format up to `2.4.0` (excluded). As of `2.4.0`, the improved, SemVer +format is used. The following configures the instance to adapt version format +automatically: + +[source,yaml,indent=0] +---- + initializr: + env: + platform: + compatibility-range: "2.0.0.RELEASE" + v1-format-compatibility-range: "[2.0.0.RELEASE,2.4.0-M1)" + v2-format-compatibility-range: "2.4.0-M1" +---- + + + [[create-instance-advanced-config]] == Advanced configuration