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
This commit is contained in:
Stephane Nicoll
2020-06-02 21:07:11 +02:00
parent 41f844a3ad
commit 853ee51f2b
13 changed files with 406 additions and 70 deletions

View File

@@ -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<ProjectRequest> projectGenerationController(
InitializrMetadataProvider metadataProvider, ApplicationContext applicationContext) {
InitializrMetadataProvider metadataProvider,
ObjectProvider<ProjectRequestPlatformVersionTransformer> platformVersionTransformer,
ApplicationContext applicationContext) {
ProjectGenerationInvoker<ProjectRequest> projectGenerationInvoker = new ProjectGenerationInvoker<>(
applicationContext, new DefaultProjectRequestToDescriptionConverter());
applicationContext, new DefaultProjectRequestToDescriptionConverter(platformVersionTransformer
.getIfAvailable(DefaultProjectRequestPlatformVersionTransformer::new)));
return new DefaultProjectGenerationController(metadataProvider, projectGenerationInvoker);
}

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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<ProjectRequest> {
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<Dependency> resolvedDependencies = getResolvedDependencies(request, springBootVersion, metadata);
validateDependencyRange(springBootVersion, resolvedDependencies);
Version platformVersion = getPlatformVersion(request, metadata);
List<Dependency> 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<Dependency> resolvedDependencies) {
private void validateDependencyRange(Version platformVersion, List<Dependency> 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<Dependency> getResolvedDependencies(ProjectRequest request, String springBootVersion,
private List<Dependency> getResolvedDependencies(ProjectRequest request, Version platformVersion,
InitializrMetadata metadata) {
List<String> 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());
}

View File

@@ -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);
}

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.

View File

@@ -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");
}
}

View File

@@ -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);
}
}

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.
@@ -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()

View File

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

View File

@@ -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",