Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -67,19 +68,14 @@ public VersionParser(List<Version> 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);
Expand All @@ -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}.
* <p>
Expand All @@ -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.
* <p>
* 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.
Expand All @@ -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<Version> 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<Version> 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<Version> matches = findMatchingVersions(major, minor, qualifier, false);
return (matches.size() != 1) ? null : matches.get(0);
}

private List<Version> 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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down Expand Up @@ -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<Version> 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<Version> 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<Version> 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<Version> 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<Version> 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<Version> 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"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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",
Expand Down Expand Up @@ -88,7 +85,7 @@ public TypeCapability getTypes() {
return this.types;
}

public SingleSelectCapability getBootVersions() {
public VersionCapability getBootVersions() {
return this.bootVersions;
}

Expand Down Expand Up @@ -211,12 +208,7 @@ public void validate() {
*/
public void updateSpringBootVersions(List<DefaultMetadataElement> versionsMetadata) {
this.bootVersions.setContent(versionsMetadata);
List<Version> 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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Version> 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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,19 @@ void updateSpringBootVersions() {
.resolveKotlinVersion(Version.parse("1.3.7.BUILD-SNAPSHOT"))).isEqualTo("1.2");
}

@Test
void bootVersionsParseWildcardToLatestMatchingVersion() {
InitializrMetadata metadata = initializeMetadata();
List<DefaultMetadataElement> 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();
Expand Down
Loading
Loading