Skip to content
Merged
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 @@ -55,7 +55,7 @@ public ProjectRequestDocument createDocument(ProjectRequestEvent event) {
document.setGroupId(request.getGroupId());
document.setArtifactId(request.getArtifactId());
document.setPackageName(request.getPackageName());
document.setVersion(determineVersionInformation(request));
document.setVersion(determineVersionInformation(request, metadata));
document.setClient(determineClientInformation(request));

document.setJavaVersion(request.getJavaVersion());
Expand Down Expand Up @@ -120,8 +120,13 @@ public ProjectRequestDocument createDocument(ProjectRequestEvent event) {
return null;
}

private @Nullable VersionInformation determineVersionInformation(ProjectRequest request) {
Version version = Version.safeParse(request.getBootVersion());
private @Nullable VersionInformation determineVersionInformation(ProjectRequest request,
InitializrMetadata metadata) {
String bootVersion = request.getBootVersion();
if (bootVersion == null) {
return null;
}
Version version = metadata.resolveBootVersion(bootVersion);
if (version != null && version.getMajor() != null) {
return new VersionInformation(version);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,26 @@ void createDocumentInvalidJavaVersion() {
assertThat(errorState.getDependencies()).isNull();
}

@Test
void createDocumentWithWildcardPlatformVersionRecordsResolvedVersion() {
ProjectRequest request = createProjectRequest();
request.setBootVersion("2.4.x");
ProjectGeneratedEvent event = createProjectGeneratedEvent(request);
ProjectRequestDocument document = this.factory.createDocument(event);
ProjectRequestDocument.VersionInformation version = document.getVersion();
assertThat(version).isNotNull();
assertThat(version.getId()).isEqualTo("2.4.1");
}

@Test
void createDocumentWithWildcardPlatformVersionThatCannotBeResolved() {
ProjectRequest request = createProjectRequest();
request.setBootVersion("9.9.x");
ProjectGeneratedEvent event = createProjectGeneratedEvent(request);
ProjectRequestDocument document = this.factory.createDocument(event);
assertThat(document.getVersion()).isNull();
}

@Test
void createDocumentInvalidLanguage() {
ProjectRequest request = createProjectRequest();
Expand Down
23 changes: 23 additions & 0 deletions initializr-docs/src/main/asciidoc/configuration-guide.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -907,6 +907,29 @@ $ http https://localhost:8080/starter.zip dependencies==web,devtools \
NOTE: `HTTPie` reads the same hint as the browser so it will store a `demo.zip` file in
the current directory, with the same differences discussed above.

The `bootVersion` parameter also accepts an abbreviated form, where the minor and patch
numbers are either omitted or specified as `x`. The service then selects the latest
generally available version that matches, ignoring milestones and snapshots. `4` and
`4.x.x` both resolve to the latest generally available `4.x` version, and `4.0` and `4.0.x`
both resolve to the latest generally available `4.0` version:

[source]
----
$ curl -G http://localhost:8080/starter.zip -d bootVersion=4.0.x -o my-project.zip
----

The same forms are supported by the `/dependencies` endpoint.

Only the trailing numbers can be replaced by `x`, and an abbreviated form cannot be
combined with a qualifier. Values such as `4.x.3`, `4.0.x-M1` or `4.x.x-SNAPSHOT` are
therefore rejected. Any other value is used as is, so `4.0.2-M1` selects that exact
version.

The request is rejected with a `400` response if no available version matches the
abbreviated form, or if the version it resolves to is outside the platform compatibility
range that the service is configured with. In the latter case, the error message refers to
the resolved version rather than the abbreviated form that was requested.



[[configuration-howto]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,15 @@ public Format getFormat() {
return this.format;
}

/**
* Specify if this version is a general availability (GA) release, that is a version
* with no qualifier or with the special {@code RELEASE} qualifier.
* @return {@code true} if this version is a GA release
*/
public boolean isGeneralAvailability() {
return (this.qualifier == null) || VersionQualifierComparator.RELEASE.equals(this.qualifier.getId());
}

/**
* Parse the string representation of a {@link Version}. Throws an
* {@link InvalidVersionException} if the version could not be parsed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,16 @@ public class VersionParser {
*/
public static final VersionParser DEFAULT = new VersionParser(Collections.emptyList());

private static final String WILDCARD = "x";

// Numbers are limited to 9 digits so that they always fit in an int
private static final Pattern VERSION_REGEX = Pattern
.compile("^(\\d+)\\.(\\d+|x)\\.(\\d+|x)(?:([.|-])([^0-9]+)(\\d+)?)?$");
.compile("^(\\d{1,9})\\.(\\d{1,9}|x)\\.(\\d{1,9}|x)(?:([.|-])([^0-9]+)(\\d{1,9})?)?$");

// Matches anything that starts with a number, so that an abbreviated form can be
// detected. The last group captures whatever follows the numbers, if anything
private static final Pattern LEADING_VERSION_REGEX = Pattern
.compile("^(\\d{1,9})(?:\\.(\\d{1,9}|x))?(?:\\.(\\d{1,9}|x))?(.*)$");

private static final Pattern RANGE_REGEX = Pattern.compile("([(\\[])(.*),(.*)([)\\]])");

Expand Down Expand Up @@ -77,12 +85,12 @@ public Version parse(String text) {
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));
if (WILDCARD.equals(minor) || WILDCARD.equals(patch)) {
Integer minorInt = (WILDCARD.equals(minor) ? null : Integer.parseInt(minor));
Version latest = findLatestVersion(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);
return new Version(major, (WILDCARD.equals(minor) ? 999 : Integer.parseInt(minor)),
(WILDCARD.equals(patch) ? 999 : Integer.parseInt(patch)), qualifier);
}
return new Version(major, latest.getMinor(), latest.getPatch(), latest.getQualifier());
}
Expand Down Expand Up @@ -119,6 +127,56 @@ public Version parse(String text) {
}
}

/**
* Resolve the specified string representation of a {@link Version} against the
* configured "latest versions".
* <p>
* The minor and patch numbers can be omitted or specified as {@code x}, in which case
* the latest matching general availability version is returned, for instance
* {@code 4}, {@code 4.x.x} or {@code 4.0.x}. A {@code x} in any other position, such
* as {@code 4.x.3} or {@code 4.0.x-M1}, cannot be resolved. Any other value is parsed
* as is, see {@link #safeParse(String)}.
* <p>
* Contrary to {@link #parse(String)}, a wildcard that matches no configured version
* does not fall back to an arbitrary version number.
* @param text the version text
* @return a Version instance for the specified version text or {@code null} if the
* version is invalid, if it uses a wildcard that cannot be resolved or if no
* configured version matches
*/
public @Nullable Version resolveLatest(String text) {
Assert.notNull(text, "Text must not be null");
Matcher matcher = LEADING_VERSION_REGEX.matcher(text.trim());
if (!matcher.matches()) {
// Does not even start with a number, there is nothing to resolve
return safeParse(text);
}
String minor = matcher.group(2);
String patch = matcher.group(3);
boolean qualified = StringUtils.hasText(matcher.group(4));
if (!qualified && (patch == null || WILDCARD.equals(patch))) {
// An abbreviated form, such as 4, 4.x, 4.0, 4.x.x or 4.0.x
Integer major = Integer.valueOf(matcher.group(1));
return findLatestGeneralAvailability(major,
(minor == null || WILDCARD.equals(minor)) ? null : Integer.valueOf(minor));
}
if (WILDCARD.equals(minor) || WILDCARD.equals(patch)) {
// A wildcard that is not trailing, such as 4.x.3, or one combined with a
// qualifier, such as 4.0.x-M1: no latest version to resolve it against
return null;
}
return safeParse(text);
}

private @Nullable Version findLatestGeneralAvailability(Integer major, @Nullable Integer minor) {
return this.latestVersions.stream()
.filter(Version::isGeneralAvailability)
.filter((candidate) -> major.equals(candidate.getMajor())
&& (minor == null || minor.equals(candidate.getMinor())))
.max(Version::compareTo)
.orElse(null);
}

/**
* Parse the string representation of a {@link VersionRange}. Throws an
* {@link InvalidVersionException} if the range could not be parsed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import java.util.List;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
Expand Down Expand Up @@ -127,9 +129,98 @@ void parseVariableVersionNoQualifierNoMatch() {
assertThat(this.parser.parse("1.2.x").toString()).isEqualTo("1.2.999");
}

@Test
void parseVersionWithLargestNumbersThatFitInAnInteger() {
assertThat(this.parser.parse("999999999.999999999.999999999.M999999999"))
.hasToString("999999999.999999999.999999999.M999999999");
}

@ParameterizedTest
@ValueSource(
strings = { "99999999999999.0.0", "1.99999999999999.0", "1.0.99999999999999", "1.0.0.M99999999999999" })
void parseVersionWithNumberThatDoesNotFitInAnIntegerIsInvalid(String text) {
assertThatExceptionOfType(InvalidVersionException.class).isThrownBy(() -> this.parser.parse(text));
}

@Test
void resolveLatestWithExactVersionParsesIt() {
this.parser = createParser("4.0.1", "4.1.2");
assertThat(this.parser.resolveLatest("4.0.1")).hasToString("4.0.1");
}

@Test
void resolveLatestWithExactVersionThatIsNotConfiguredParsesIt() {
this.parser = createParser("4.0.1", "4.1.2");
assertThat(this.parser.resolveLatest("3.5.0")).hasToString("3.5.0");
}

@ParameterizedTest
@ValueSource(strings = { "4", "4.x", "4.x.x" })
void resolveLatestWithMajorWildcardUsesLatestVersionOfThatMajor(String text) {
this.parser = createParser("3.5.9", "4.0.1", "4.1.2");
assertThat(this.parser.resolveLatest(text)).hasToString("4.1.2");
}

@ParameterizedTest
@ValueSource(strings = { "4.0", "4.0.x" })
void resolveLatestWithMinorWildcardUsesLatestVersionOfThatMinor(String text) {
this.parser = createParser("4.0.1", "4.0.2", "4.1.2");
assertThat(this.parser.resolveLatest(text)).hasToString("4.0.2");
}

@Test
void resolveLatestWithWildcardIgnoresPreReleaseVersions() {
this.parser = createParser("4.0.1", "4.1.0-M2", "4.1.0-SNAPSHOT");
assertThat(this.parser.resolveLatest("4.x.x")).hasToString("4.0.1");
}

@Test
void resolveLatestWithWildcardConsidersReleaseQualifier() {
this.parser = createParser("1.5.21.RELEASE", "1.5.22.RELEASE");
assertThat(this.parser.resolveLatest("1.x.x")).hasToString("1.5.22.RELEASE");
}

@Test
void resolveLatestWithWildcardComparesPatchNumerically() {
this.parser = createParser("4.0.2", "4.0.10");
assertThat(this.parser.resolveLatest("4.0.x")).hasToString("4.0.10");
}

@ParameterizedTest
@ValueSource(strings = { "5.x.x", "4.2.x" })
void resolveLatestWithWildcardWhenNoVersionMatchesReturnsNull(String text) {
this.parser = createParser("4.0.1", "4.1.2");
assertThat(this.parser.resolveLatest(text)).isNull();
}

@ParameterizedTest
@ValueSource(strings = { "4.x.3", "4.0.x-M1", "4.x.x-SNAPSHOT", "4.x.3.RELEASE" })
void resolveLatestWithWildcardThatCannotBeResolvedReturnsNull(String text) {
this.parser = createParser("4.0.1", "4.0.2-M1");
assertThat(this.parser.resolveLatest(text)).isNull();
}

@Test
void resolveLatestWithWildcardAndSpaces() {
this.parser = createParser("4.0.1", "4.0.2");
assertThat(this.parser.resolveLatest(" 4.0.x ")).hasToString("4.0.2");
}

@ParameterizedTest
@ValueSource(strings = { "", " ", "nope", "4.", "4.0.0.", "99999999999999", "4.99999999999999",
"99999999999999.0.0", "4.0.0.M99999999999999" })
void resolveLatestWithInvalidVersionReturnsNull(String text) {
this.parser = createParser("4.0.1");
assertThat(this.parser.resolveLatest(text)).isNull();
}

@Test
void invalidRange() {
assertThatExceptionOfType(InvalidVersionException.class).isThrownBy(() -> this.parser.parseRange("foo-bar"));
}

private VersionParser createParser(String... versions) {
return new VersionParser(Arrays.stream(versions).map(Version::parse).toList());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

import io.spring.initializr.generator.version.Version.Format;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import static org.assertj.core.api.Assertions.assertThat;

Expand Down Expand Up @@ -212,6 +214,18 @@ void formatNoQualifierToV2() {
assertThat(version.format(Format.V2)).hasToString("1.2.0");
}

@ParameterizedTest
@ValueSource(strings = { "1.2.0", "1.2.0.RELEASE" })
void isGeneralAvailabilityWithGaVersion(String text) {
assertThat(Version.parse(text).isGeneralAvailability()).isTrue();
}

@ParameterizedTest
@ValueSource(strings = { "1.2.0-M1", "1.2.0.RC1", "1.2.0-SNAPSHOT", "1.2.0.BUILD-SNAPSHOT" })
void isGeneralAvailabilityWithPreReleaseVersion(String text) {
assertThat(Version.parse(text).isGeneralAvailability()).isFalse();
}

private Version parse(String text) {
return this.parser.parse(text);
}
Expand Down
Loading
Loading