Add platform compatibility range support

This commit adds a new `platformCompatibilityRange` in the metadata that
can be used to restrict the valid platform versions. If a project is
requested or metadata needs to be resolved against a version that does
not match the range, an exception is thrown.

Closes gh-1048
This commit is contained in:
Stephane Nicoll
2020-01-03 14:08:37 +01:00
parent e25fb74d23
commit eef529aa7c
9 changed files with 119 additions and 20 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -186,6 +186,12 @@ public class InitializrMetadataTestBuilder {
return this;
}
public InitializrMetadataTestBuilder setPlatformCompatibilityRange(String platformCompatibilityRange) {
this.builder.withCustomizer(
(it) -> it.getConfiguration().getEnv().setPlatformCompatibilityRange(platformCompatibilityRange));
return this;
}
public InitializrMetadataTestBuilder setGradleEnv(String dependencyManagementPluginVersion) {
this.builder.withCustomizer((it) -> it.getConfiguration().getEnv().getGradle()
.setDependencyManagementPluginVersion(dependencyManagementPluginVersion));

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -210,6 +210,16 @@ 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.
@@ -300,6 +310,14 @@ 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;
}
@@ -335,6 +353,14 @@ public class InitializrConfiguration {
this.maven.parent.validate();
this.boms.forEach((k, v) -> v.validate());
this.kotlin.validate();
updateCompatibilityRange(VersionParser.DEFAULT);
}
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;
}
public void merge(Env other) {
@@ -344,6 +370,8 @@ 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);
@@ -351,6 +379,20 @@ public class InitializrConfiguration {
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.
*/
@@ -421,18 +463,17 @@ public class InitializrConfiguration {
}
public void validate() {
VersionParser simpleParser = new VersionParser(Collections.emptyList());
this.mappings.forEach((m) -> {
if (m.compatibilityRange == null) {
throw new InvalidInitializrMetadataException(
"CompatibilityRange is mandatory, invalid version mapping for " + this);
}
m.range = simpleParser.parseRange(m.compatibilityRange);
if (m.version == null) {
throw new InvalidInitializrMetadataException(
"Version is mandatory, invalid version mapping for " + this);
}
});
updateCompatibilityRange(VersionParser.DEFAULT);
}
public void updateCompatibilityRange(VersionParser versionParser) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -206,8 +206,7 @@ public class InitializrMetadata {
.collect(Collectors.toList());
VersionParser parser = new VersionParser(bootVersions);
this.dependencies.updateCompatibilityRange(parser);
this.configuration.getEnv().getBoms().values().forEach((it) -> it.updateCompatibilityRange(parser));
this.configuration.getEnv().getKotlin().updateCompatibilityRange(parser);
this.configuration.getEnv().updateCompatibilityRange(parser);
}
/**

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -24,6 +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.InitializrMetadata;
import io.spring.initializr.metadata.InitializrMetadataProvider;
import io.spring.initializr.metadata.InvalidInitializrMetadataException;
@@ -32,6 +33,7 @@ import io.spring.initializr.web.mapper.InitializrMetadataJsonMapper;
import io.spring.initializr.web.mapper.InitializrMetadataV21JsonMapper;
import io.spring.initializr.web.mapper.InitializrMetadataV2JsonMapper;
import io.spring.initializr.web.mapper.InitializrMetadataVersion;
import io.spring.initializr.web.project.InvalidProjectRequestException;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpStatus;
@@ -97,6 +99,12 @@ public class ProjectMetadataController extends AbstractMetadataController {
response.sendError(HttpStatus.BAD_REQUEST.value(), ex.getMessage());
}
@ExceptionHandler
public void invalidProjectRequest(HttpServletResponse response, InvalidProjectRequestException ex)
throws IOException {
response.sendError(HttpStatus.BAD_REQUEST.value(), ex.getMessage());
}
/**
* Return the {@link CacheControl} response headers to use for the specified
* {@link InitializrMetadata metadata}. If no cache should be applied
@@ -112,6 +120,11 @@ 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)) {
throw new InvalidProjectRequestException("Invalid Spring Boot version '" + bootVersion
+ "', Spring Boot compatibility range is " + env.determinePlatformCompatibilityRangeRequirement());
}
DependencyMetadata dependencyMetadata = this.dependencyMetadataProvider.get(metadata, v);
String content = new DependencyMetadataV21JsonMapper().write(dependencyMetadata);
return ResponseEntity.ok().contentType(version.getMediaType()).eTag(createUniqueId(content))

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -27,6 +27,7 @@ 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.InitializrMetadata;
import io.spring.initializr.metadata.Type;
import io.spring.initializr.metadata.support.MetadataBuildItemMapper;
@@ -42,8 +43,6 @@ import io.spring.initializr.metadata.support.MetadataBuildItemMapper;
public class DefaultProjectRequestToDescriptionConverter
implements ProjectRequestToDescriptionConverter<ProjectRequest> {
private static final Version VERSION_1_5_0 = Version.parse("1.5.0.RELEASE");
@Override
public ProjectDescription convert(ProjectRequest request, InitializrMetadata metadata) {
MutableProjectDescription description = new MutableProjectDescription();
@@ -82,18 +81,19 @@ public class DefaultProjectRequestToDescriptionConverter
}
private void validate(ProjectRequest request, InitializrMetadata metadata) {
validateSpringBootVersion(request);
validateSpringBootVersion(request, metadata);
validateType(request.getType(), metadata);
validateLanguage(request.getLanguage(), metadata);
validatePackaging(request.getPackaging(), metadata);
validateDependencies(request, metadata);
}
private void validateSpringBootVersion(ProjectRequest request) {
private void validateSpringBootVersion(ProjectRequest request, InitializrMetadata metadata) {
Version bootVersion = Version.safeParse(request.getBootVersion());
if (bootVersion != null && bootVersion.compareTo(VERSION_1_5_0) < 0) {
throw new InvalidProjectRequestException(
"Invalid Spring Boot version " + bootVersion + " must be 1.5.0 or higher");
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());
}
}

View File

@@ -21,7 +21,9 @@ import io.spring.initializr.metadata.Dependency;
import io.spring.initializr.web.AbstractInitializrControllerIntegrationTests;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.web.client.HttpClientErrorException;
import static org.assertj.core.api.Assertions.assertThat;
@@ -46,4 +48,29 @@ class ProjectGenerationControllerCustomEnvIntegrationTests extends AbstractIniti
.hasDependency(Dependency.createSpringBootStarter("test", Dependency.SCOPE_TEST));
}
@Test
void generateProjectWithUnsupportedPlatformVersion() {
try {
execute("/starter.zip?bootVersion=1.5.12.RELEASE", byte[].class, null, (String[]) null);
}
catch (HttpClientErrorException ex) {
assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
assertThat(ex.getMessage()).contains("Invalid Spring Boot version",
"Spring Boot compatibility range is >=2.0.0.RELEASE");
}
}
@Test
void getDependenciesMetadataWithUnsupportedPlatformVersion() {
try {
execute("/dependencies?bootVersion=1.5.12.RELEASE", String.class, "application/vnd.initializr.v2.1+json",
"application/json");
}
catch (HttpClientErrorException ex) {
assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
assertThat(ex.getMessage()).contains("Invalid Spring Boot version",
"Spring Boot compatibility range is >=2.0.0.RELEASE");
}
}
}

View File

@@ -66,12 +66,23 @@ class DefaultProjectRequestToDescriptionConverterTests {
}
@Test
void convertWhenSpringBootVersionInvalidShouldThrowException() {
void convertWhenPlatformCompatiblityRangeIsNotSetShouldNotThrowException() {
this.metadata = InitializrMetadataTestBuilder.withDefaults().setPlatformCompatibilityRange(null).build();
ProjectRequest request = createProjectRequest();
request.setBootVersion("1.2.3.M4");
request.setBootVersion("1.5.9.RELEASE");
assertThat(this.converter.convert(request, this.metadata).getPlatformVersion())
.isEqualTo(Version.parse("1.5.9.RELEASE"));
}
@Test
void convertWhenSpringBootVersionInvalidShouldThrowException() {
this.metadata = InitializrMetadataTestBuilder.withDefaults()
.setPlatformCompatibilityRange("[2.0.0.RELEASE,2.3.0.M1)").build();
ProjectRequest request = createProjectRequest();
request.setBootVersion("1.5.9.RELEASE");
assertThatExceptionOfType(InvalidProjectRequestException.class)
.isThrownBy(() -> this.converter.convert(request, this.metadata))
.withMessage("Invalid Spring Boot version 1.2.3.M4 must be 1.5.0 or higher");
.isThrownBy(() -> this.converter.convert(request, this.metadata)).withMessage(
"Invalid Spring Boot version '1.5.9.RELEASE', Spring Boot compatibility range is >=2.0.0.RELEASE and <2.3.0.M1");
}
@Test

View File

@@ -5,5 +5,6 @@ initializr:
fallbackApplicationName: FooBarApplication
invalidApplicationNames:
- InvalidApplication
platform-compatibility-range: "2.0.0.RELEASE"
kotlin:
default-version: 1.0.0-beta-2423

View File

@@ -33,6 +33,7 @@
"artifactRepository": "https://repo.spring.io/release/",
"fallbackApplicationName": "Application",
"forceSsl": false,
"platformCompatibilityRange": null,
"gradle": {
"dependencyManagementPluginVersion": "1.0.0.RELEASE"
},