diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/version/VersionParser.java b/initializr-generator/src/main/java/io/spring/initializr/generator/version/VersionParser.java index b62f859159..371583b697 100644 --- a/initializr-generator/src/main/java/io/spring/initializr/generator/version/VersionParser.java +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/version/VersionParser.java @@ -18,6 +18,7 @@ import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -67,19 +68,14 @@ public VersionParser(List latestVersions) { * @see #safeParse(java.lang.String) */ public Version parse(String text) { - Assert.notNull(text, "Text must not be null"); - Matcher matcher = VERSION_REGEX.matcher(text.trim()); - if (!matcher.matches()) { - throw new InvalidVersionException("Could not determine version based on '" + text + "': version format " - + "is Major.Minor.Patch and an optional Qualifier " + "(e.g. 1.0.5.RELEASE)"); - } + Matcher matcher = getVersionMatcher(text); Integer major = Integer.valueOf(matcher.group(1)); String minor = matcher.group(2); String patch = matcher.group(3); Qualifier qualifier = parseQualifier(matcher); if ("x".equals(minor) || "x".equals(patch)) { Integer minorInt = ("x".equals(minor) ? null : Integer.parseInt(minor)); - Version latest = findLatestVersion(major, minorInt, qualifier); + Version latest = findUniqueVersion(major, minorInt, qualifier); if (latest == null) { return new Version(major, ("x".equals(minor) ? 999 : Integer.parseInt(minor)), ("x".equals(patch) ? 999 : Integer.parseInt(patch)), qualifier); @@ -102,6 +98,37 @@ public Version parse(String text) { return null; } + /** + * Parse the string representation of a {@link Version}, resolving variable minor or + * patch values to the latest matching configured version. Throws an + * {@link InvalidVersionException} if the version could not be parsed or no latest + * version could be found. + * @param text the version text + * @return a Version instance for the specified version text + * @throws InvalidVersionException if the version text could not be parsed or resolved + * @see #parse(java.lang.String) + */ + public Version parseLatest(String text) { + Matcher matcher = getVersionMatcher(text); + String minor = matcher.group(2); + String patch = matcher.group(3); + if (!"x".equals(minor) && !"x".equals(patch)) { + return parse(text); + } + if ("x".equals(minor) && !"x".equals(patch)) { + throw new InvalidVersionException("Could not determine latest version based on '" + text + + "': wildcard minor requires wildcard patch"); + } + Integer major = Integer.valueOf(matcher.group(1)); + Integer minorInt = ("x".equals(minor) ? null : Integer.parseInt(minor)); + Qualifier qualifier = parseQualifier(matcher); + Version latest = findLatestVersion(major, minorInt, qualifier); + if (latest == null) { + throw new InvalidVersionException("Could not determine latest version based on '" + text + "'"); + } + return latest; + } + /** * Parse safely the specified string representation of a {@link Version}. *

@@ -119,6 +146,25 @@ public Version parse(String text) { } } + /** + * Parse safely the specified string representation of a {@link Version}, resolving + * variable minor or patch values to the latest matching configured version. + *

+ * Return {@code null} if the text represents an invalid version or cannot be + * resolved. + * @param text the version text + * @return a Version instance for the specified version text + * @see #parseLatest(java.lang.String) + */ + public @Nullable Version safeParseLatest(String text) { + try { + return parseLatest(text); + } + catch (InvalidVersionException ex) { + return null; + } + } + /** * Parse the string representation of a {@link VersionRange}. Throws an * {@link InvalidVersionException} if the range could not be parsed. @@ -141,21 +187,43 @@ public VersionRange parseRange(String text) { return new VersionRange(lowerVersion, lowerInclusive, higherVersion, higherInclusive); } - private @Nullable Version findLatestVersion(@Nullable Integer major, @Nullable Integer minor, - Version.@Nullable Qualifier qualifier) { - List matches = this.latestVersions.stream().filter((it) -> { - if (major != null && !major.equals(it.getMajor())) { + private Matcher getVersionMatcher(String text) { + Assert.notNull(text, "Text must not be null"); + Matcher matcher = VERSION_REGEX.matcher(text.trim()); + if (!matcher.matches()) { + throw new InvalidVersionException("Could not determine version based on '" + text + "': version format " + + "is Major.Minor.Patch and an optional Qualifier " + "(e.g. 1.0.5.RELEASE)"); + } + return matcher; + } + + private @Nullable Version findLatestVersion(Integer major, @Nullable Integer minor, @Nullable Qualifier qualifier) { + List matches = findMatchingVersions(major, minor, qualifier, true); + return matches.stream().max(Version::compareTo).orElse(null); + } + + private @Nullable Version findUniqueVersion(Integer major, @Nullable Integer minor, @Nullable Qualifier qualifier) { + List matches = findMatchingVersions(major, minor, qualifier, false); + return (matches.size() != 1) ? null : matches.get(0); + } + + private List findMatchingVersions(Integer major, @Nullable Integer minor, @Nullable Qualifier qualifier, + boolean exactQualifier) { + return this.latestVersions.stream().filter((it) -> { + if (!major.equals(it.getMajor())) { return false; } if (minor != null && !minor.equals(it.getMinor())) { return false; } - if (qualifier != null && !qualifier.equals(it.getQualifier())) { + if (exactQualifier && !Objects.equals(qualifier, it.getQualifier())) { + return false; + } + if (!exactQualifier && qualifier != null && !qualifier.equals(it.getQualifier())) { return false; } return true; }).toList(); - return (matches.size() != 1) ? null : matches.get(0); } } diff --git a/initializr-generator/src/test/java/io/spring/initializr/generator/version/VersionParserTests.java b/initializr-generator/src/test/java/io/spring/initializr/generator/version/VersionParserTests.java index f5bf89a0c8..a0f8060573 100755 --- a/initializr-generator/src/test/java/io/spring/initializr/generator/version/VersionParserTests.java +++ b/initializr-generator/src/test/java/io/spring/initializr/generator/version/VersionParserTests.java @@ -74,6 +74,11 @@ void safeParseInvalidVersion() { assertThat(this.parser.safeParse("foo")).isNull(); } + @Test + void safeParseLatestReturnsNullForInvalidVersion() { + assertThat(this.parser.safeParseLatest("foo")).isNull(); + } + @Test void parseVersionWithSpaces() { assertThat(this.parser.parse(" 1.2.0.RC3 ")).isLessThan(this.parser.parse("1.3.0.RELEASE")); @@ -127,6 +132,63 @@ void parseVariableVersionNoQualifierNoMatch() { assertThat(this.parser.parse("1.2.x").toString()).isEqualTo("1.2.999"); } + @Test + void parseLatestParsesExactVersion() { + assertThat(this.parser.parseLatest("1.2.0").toString()).isEqualTo("1.2.0"); + } + + @Test + void parseLatestResolvesPatchWildcardToLatestMatchingVersion() { + List currentVersions = Arrays.asList(this.parser.parse("1.3.8"), this.parser.parse("1.3.9"), + this.parser.parse("1.4.0")); + this.parser = new VersionParser(currentVersions); + assertThat(this.parser.parseLatest("1.3.x").toString()).isEqualTo("1.3.9"); + } + + @Test + void parseLatestResolvesMinorAndPatchWildcardsToLatestMatchingVersion() { + List currentVersions = Arrays.asList(this.parser.parse("1.3.8"), this.parser.parse("1.4.0")); + this.parser = new VersionParser(currentVersions); + assertThat(this.parser.parseLatest("1.x.x").toString()).isEqualTo("1.4.0"); + } + + @Test + void parseLatestResolvesPatchWildcardWithQualifier() { + List currentVersions = Arrays.asList(this.parser.parse("1.3.8.GA2"), this.parser.parse("1.3.9.GA2"), + this.parser.parse("1.4.0.GA2")); + this.parser = new VersionParser(currentVersions); + assertThat(this.parser.parseLatest("1.3.x.GA2").toString()).isEqualTo("1.3.9.GA2"); + } + + @Test + void parseLatestResolvesUnqualifiedWildcardVersion() { + List currentVersions = Arrays.asList(this.parser.parse("3.0.1"), this.parser.parse("3.0.2-SNAPSHOT"), + this.parser.parse("3.0.0-M1")); + this.parser = new VersionParser(currentVersions); + assertThat(this.parser.parseLatest("3.x.x").toString()).isEqualTo("3.0.1"); + } + + @Test + void parseLatestRejectsWildcardMinorWithSpecificPatch() { + assertThatExceptionOfType(InvalidVersionException.class).isThrownBy(() -> this.parser.parseLatest("5.x.3")) + .withMessage("Could not determine latest version based on '5.x.3': wildcard minor requires wildcard patch"); + } + + @Test + void parseLatestThrowsExceptionWhenNoVersionMatches() { + List currentVersions = Arrays.asList(this.parser.parse("1.3.8"), this.parser.parse("1.4.0")); + this.parser = new VersionParser(currentVersions); + assertThatExceptionOfType(InvalidVersionException.class).isThrownBy(() -> this.parser.parseLatest("2.x.x")) + .withMessage("Could not determine latest version based on '2.x.x'"); + } + + @Test + void safeParseLatestReturnsNullWhenNoVersionMatches() { + List currentVersions = Arrays.asList(this.parser.parse("1.3.8"), this.parser.parse("1.4.0")); + this.parser = new VersionParser(currentVersions); + assertThat(this.parser.safeParseLatest("2.x.x")).isNull(); + } + @Test void invalidRange() { assertThatExceptionOfType(InvalidVersionException.class).isThrownBy(() -> this.parser.parseRange("foo-bar")); diff --git a/initializr-metadata/src/main/java/io/spring/initializr/metadata/InitializrMetadata.java b/initializr-metadata/src/main/java/io/spring/initializr/metadata/InitializrMetadata.java index 2ef5736ecd..1fd350d3a7 100644 --- a/initializr-metadata/src/main/java/io/spring/initializr/metadata/InitializrMetadata.java +++ b/initializr-metadata/src/main/java/io/spring/initializr/metadata/InitializrMetadata.java @@ -20,13 +20,10 @@ import java.util.List; import java.util.Map; -import io.spring.initializr.generator.version.Version; import io.spring.initializr.generator.version.VersionParser; import io.spring.initializr.generator.version.VersionProperty; import org.jspecify.annotations.Nullable; -import org.springframework.util.Assert; - /** * Meta-data used to generate a project. * @@ -41,7 +38,7 @@ public class InitializrMetadata { private final TypeCapability types = new TypeCapability(); - private final SingleSelectCapability bootVersions = new SingleSelectCapability("bootVersion", "Spring Boot Version", + private final VersionCapability bootVersions = new VersionCapability("bootVersion", "Spring Boot Version", "spring boot version"); private final SingleSelectCapability packagings = new SingleSelectCapability("packaging", "Packaging", @@ -88,7 +85,7 @@ public TypeCapability getTypes() { return this.types; } - public SingleSelectCapability getBootVersions() { + public VersionCapability getBootVersions() { return this.bootVersions; } @@ -211,12 +208,7 @@ public void validate() { */ public void updateSpringBootVersions(List versionsMetadata) { this.bootVersions.setContent(versionsMetadata); - List bootVersions = this.bootVersions.getContent().stream().map((it) -> { - String id = it.getId(); - Assert.state(id != null, "'id' must not be null"); - return Version.parse(id); - }).toList(); - VersionParser parser = new VersionParser(bootVersions); + VersionParser parser = this.bootVersions.getVersionParser(); this.dependencies.updateCompatibilityRange(parser); this.configuration.getEnv().updateCompatibilityRange(parser); } diff --git a/initializr-metadata/src/main/java/io/spring/initializr/metadata/VersionCapability.java b/initializr-metadata/src/main/java/io/spring/initializr/metadata/VersionCapability.java new file mode 100644 index 0000000000..0efe25bf2b --- /dev/null +++ b/initializr-metadata/src/main/java/io/spring/initializr/metadata/VersionCapability.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012 - present 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.metadata; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.spring.initializr.generator.version.Version; +import io.spring.initializr.generator.version.VersionParser; +import org.jspecify.annotations.Nullable; + +import org.springframework.util.Assert; + +/** + * A single select capability whose values can be parsed as versions. + * + * @author Sijun Yang + */ +public class VersionCapability extends SingleSelectCapability { + + @JsonCreator + VersionCapability(@JsonProperty("id") String id) { + this(id, null, null); + } + + public VersionCapability(String id, @Nullable String title, @Nullable String description) { + super(id, title, description); + } + + public Version parseVersion(String text) { + return getVersionParser().parseLatest(text); + } + + public @Nullable Version safeParseVersion(@Nullable String text) { + if (text == null) { + return null; + } + return getVersionParser().safeParseLatest(text); + } + + protected VersionParser getVersionParser() { + List versions = getContent().stream().map((it) -> { + String id = it.getId(); + Assert.state(id != null, "'id' must not be null"); + return Version.parse(id); + }).toList(); + return new VersionParser(versions); + } + +} diff --git a/initializr-metadata/src/test/java/io/spring/initializr/metadata/InitializrMetadataTests.java b/initializr-metadata/src/test/java/io/spring/initializr/metadata/InitializrMetadataTests.java index 0704ddeb95..592c2ee276 100755 --- a/initializr-metadata/src/test/java/io/spring/initializr/metadata/InitializrMetadataTests.java +++ b/initializr-metadata/src/test/java/io/spring/initializr/metadata/InitializrMetadataTests.java @@ -191,6 +191,19 @@ void updateSpringBootVersions() { .resolveKotlinVersion(Version.parse("1.3.7.BUILD-SNAPSHOT"))).isEqualTo("1.2"); } + @Test + void bootVersionsParseWildcardToLatestMatchingVersion() { + InitializrMetadata metadata = initializeMetadata(); + List bootVersions = Arrays.asList(DefaultMetadataElement.create("1.3.6", false), + DefaultMetadataElement.create("1.3.7", false), DefaultMetadataElement.create("1.4.0", false)); + metadata.updateSpringBootVersions(bootVersions); + assertThat(metadata.getBootVersions().parseVersion("1.3.x")).hasToString("1.3.7"); + + metadata.updateSpringBootVersions(List.of(DefaultMetadataElement.create("1.3.8", false))); + assertThat(metadata.getBootVersions().parseVersion("1.3.x")).hasToString("1.3.8"); + assertThat(metadata.getBootVersions().safeParseVersion("2.x.x")).isNull(); + } + @Test void invalidParentMissingVersion() { InitializrMetadata metadata = initializeMetadata(); diff --git a/initializr-metadata/src/test/java/io/spring/initializr/metadata/VersionCapabilityTests.java b/initializr-metadata/src/test/java/io/spring/initializr/metadata/VersionCapabilityTests.java new file mode 100644 index 0000000000..baee43160a --- /dev/null +++ b/initializr-metadata/src/test/java/io/spring/initializr/metadata/VersionCapabilityTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2012 - present 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.metadata; + +import java.util.Arrays; +import java.util.List; + +import io.spring.initializr.generator.version.InvalidVersionException; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link VersionCapability}. + */ +class VersionCapabilityTests { + + @Test + void parseVersionResolvesWildcardToLatestMatchingVersion() { + VersionCapability capability = new VersionCapability("test", "test", "test"); + capability.setContent(Arrays.asList(DefaultMetadataElement.create("1.3.6", false), + DefaultMetadataElement.create("1.3.7", false), DefaultMetadataElement.create("1.4.0", false))); + assertThat(capability.parseVersion("1.3.x")).hasToString("1.3.7"); + } + + @Test + void parseVersionThrowsExceptionWhenNoVersionMatches() { + VersionCapability capability = new VersionCapability("test", "test", "test"); + capability.setContent(Arrays.asList(DefaultMetadataElement.create("1.3.6", false), + DefaultMetadataElement.create("1.4.0", false))); + assertThatExceptionOfType(InvalidVersionException.class).isThrownBy(() -> capability.parseVersion("2.x.x")) + .withMessage("Could not determine latest version based on '2.x.x'"); + } + + @Test + void safeParseVersionReturnsNullWhenVersionCannotBeParsed() { + VersionCapability capability = new VersionCapability("test", "test", "test"); + capability.setContent(Arrays.asList(DefaultMetadataElement.create("1.3.6", false), + DefaultMetadataElement.create("1.4.0", false))); + assertThat(capability.safeParseVersion(null)).isNull(); + assertThat(capability.safeParseVersion("invalid")).isNull(); + assertThat(capability.safeParseVersion("2.x.x")).isNull(); + } + + @Test + void parseVersionUsesAddedContent() { + VersionCapability capability = new VersionCapability("test", "test", "test"); + capability.addContent(DefaultMetadataElement.create("1.3.7", false)); + assertThat(capability.parseVersion("1.3.x")).hasToString("1.3.7"); + } + + @Test + void parseVersionUsesReplacedContent() { + VersionCapability capability = new VersionCapability("test", "test", "test"); + capability.setContent(List.of(DefaultMetadataElement.create("1.3.7", false))); + assertThat(capability.parseVersion("1.3.x")).hasToString("1.3.7"); + + capability.setContent(List.of(DefaultMetadataElement.create("1.3.8", false))); + assertThat(capability.parseVersion("1.3.x")).hasToString("1.3.8"); + } + + @Test + void parseVersionUsesMergedContent() { + VersionCapability capability = new VersionCapability("test", "test", "test"); + capability.setContent(List.of(DefaultMetadataElement.create("1.3.7", false))); + capability.merge(List.of(DefaultMetadataElement.create("1.3.8", false))); + assertThat(capability.parseVersion("1.3.x")).hasToString("1.3.8"); + } + +} 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 4eceb5bf5a..c5d165f621 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 @@ -150,7 +150,7 @@ protected CacheControl determineCacheControlFor(InitializrMetadata metadata) { protected ResponseEntity dependenciesFor(InitializrMetadataVersion metadataVersion, @Nullable String bootVersion) { InitializrMetadata metadata = this.metadataProvider.get(); - Version effectiveBootVersion = (bootVersion != null) ? Version.parse(bootVersion) + Version effectiveBootVersion = (bootVersion != null) ? metadata.getBootVersions().parseVersion(bootVersion) : getDefaultBootVersion(metadata); Platform platform = metadata.getConfiguration().getEnv().getPlatform(); if (!platform.isCompatibleVersion(effectiveBootVersion)) { 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 86a13f7c3d..20f4b1af9c 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 @@ -128,7 +128,7 @@ private void validate(ProjectRequest request, InitializrMetadata metadata) { } private void validatePlatformVersion(ProjectRequest request, InitializrMetadata metadata) { - Version platformVersion = Version.safeParse(request.getBootVersion()); + Version platformVersion = metadata.getBootVersions().safeParseVersion(request.getBootVersion()); Platform platform = metadata.getConfiguration().getEnv().getPlatform(); if (platformVersion != null && !platform.isCompatibleVersion(platformVersion)) { throw new InvalidProjectRequestException("Invalid Spring Boot version '" + platformVersion @@ -213,7 +213,7 @@ private BuildSystem getBuildSystem(ProjectRequest request, InitializrMetadata me private Version getPlatformVersion(ProjectRequest request, InitializrMetadata metadata) { String versionText = (request.getBootVersion() != null) ? request.getBootVersion() : getDefaultBootVersion(metadata); - Version version = Version.parse(versionText); + Version version = metadata.getBootVersions().parseVersion(versionText); return this.platformVersionTransformer.transform(version, metadata); } 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 fbd0bdb937..ffbf80c92a 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 @@ -91,6 +91,13 @@ void metadataWithInvalidPlatformVersion() { } } + @Test + void dependenciesWithWildcardBootVersion() { + ResponseEntity response = execute("/dependencies?bootVersion=2.4.x", String.class, null, + "application/vnd.initializr.v2.1+json"); + assertThat(response.getBody()).contains("\"bootVersion\":\"2.4.4\""); + } + @Test void metadataWithCurrentAcceptHeader() { getRequests().setFields("_links.maven-project", "dependencies.values[0]", "type.values[0]", 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 3821655c4e..316f934cf3 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 @@ -21,6 +21,7 @@ import io.spring.initializr.generator.buildsystem.gradle.GradleBuildSystem; import io.spring.initializr.generator.project.ProjectDescription; import io.spring.initializr.generator.test.InitializrMetadataTestBuilder; +import io.spring.initializr.generator.version.InvalidVersionException; import io.spring.initializr.generator.version.Version; import io.spring.initializr.generator.version.VersionRange; import io.spring.initializr.metadata.Dependency; @@ -159,6 +160,15 @@ void convertWhenSpringBootVersionInvalidShouldThrowException() { "Invalid Spring Boot version '1.5.9.RELEASE', Spring Boot compatibility range is >=2.0.0.RELEASE and <2.3.0.M1"); } + @Test + void convertWhenWildcardSpringBootVersionDoesNotMatchShouldThrowException() { + ProjectRequest request = createProjectRequest(); + request.setBootVersion("2.6.x"); + assertThatExceptionOfType(InvalidVersionException.class) + .isThrownBy(() -> this.converter.convert(request, this.metadata)) + .withMessage("Could not determine latest version based on '2.6.x'"); + } + @Test void convertWhenPackagingIsInvalidShouldThrowException() { ProjectRequest request = createProjectRequest(); @@ -271,6 +281,14 @@ void convertShouldSetPlatformVersionFromRequest() { assertThat(description.getPlatformVersion()).isEqualTo(Version.parse("2.0.3")); } + @Test + void convertShouldSetLatestMatchingPlatformVersionFromWildcardBootVersion() { + ProjectRequest request = createProjectRequest(); + request.setBootVersion("2.4.x"); + ProjectDescription description = this.converter.convert(request, this.metadata); + assertThat(description.getPlatformVersion()).isEqualTo(Version.parse("2.4.1")); + } + @Test void convertShouldSetLanguageForProjectDescriptionFromRequest() { ProjectRequest request = createProjectRequest();