Improve custom project request arrangement

This commit improves the use case of configuring a custom ProjectRequest
by enforcing consistently a particular type using a generic.

As a result, `ProjectGenerationInvoker` is no longer exposed as a bean
as it is the responsibility of the custom `ProjectGenerationController`
to provide one that matches the requested `ProjectRequest` type.

See gh-990
This commit is contained in:
Stephane Nicoll
2019-08-29 10:46:46 +02:00
parent f74370eb63
commit cff60c397d
12 changed files with 115 additions and 56 deletions

View File

@@ -1088,13 +1088,19 @@ include::{code-examples}/doc/generator/project/CustomProjectGenerationController
----
If you inherit from `WebProjectRequest`, defaults can be automatically applied from the
metadata as shown above but you may also chose to ignore that. If you define a `@Bean`
for that controller, the auto-configuration will back-off and use yours instead.
metadata as shown above but you may also chose to ignore that.
The next step is to make sure that those additional attributes are made available in the
`ProjectGenerationContext`. The idiomatic way of doing this is to create your own
interface that extends from `ProjectDescription` and expose your custom attributes. To
make sure your view of `ProjectGeneration` is made available in the
make sure your view of `ProjectDescription` is made available in the
`ProjectGenerationContext`, a custom `ProjectRequestToDescriptionConverter` should be
defined. When such a bean exists in the context it replaces the default that the
auto-configuration provides.
defined and could reuse `DefaultProjectRequestToDescriptionConverter` to apply general
rules for standard fields.
Finally, you should wire up everything:
[source,java,indent=0,subs="verbatim,quotes,attributes"]
----
include::{code-examples}/doc/generator/project/CustomProjectGenerationConfigurationExample.java[tag=code]
----

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2012-2019 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.doc.generator.project;
import io.spring.initializr.generator.project.MutableProjectDescription;
import io.spring.initializr.generator.project.ProjectDescription;
import io.spring.initializr.metadata.InitializrMetadata;
import io.spring.initializr.metadata.InitializrMetadataProvider;
import io.spring.initializr.web.controller.ProjectGenerationController;
import io.spring.initializr.web.project.ProjectGenerationInvoker;
import io.spring.initializr.web.project.ProjectRequestToDescriptionConverter;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Configuration example of a custom {@link ProjectGenerationController}.
*
* @author Stephane Nicoll
*/
@Configuration
public class CustomProjectGenerationConfigurationExample {
// tag::code[]
@Bean
public CustomProjectGenerationController projectGenerationController(InitializrMetadataProvider metadataProvider,
ApplicationContext applicationContext) {
ProjectGenerationInvoker<CustomProjectRequest> projectGenerationInvoker = new ProjectGenerationInvoker<>(
applicationContext, new CustomProjectRequestToDescriptionConverter());
return new CustomProjectGenerationController(metadataProvider, projectGenerationInvoker);
}
// end::code[]
static class CustomProjectRequestToDescriptionConverter
implements ProjectRequestToDescriptionConverter<CustomProjectRequest> {
@Override
public ProjectDescription convert(CustomProjectRequest request, InitializrMetadata metadata) {
return new MutableProjectDescription();
}
}
}

View File

@@ -31,7 +31,7 @@ import io.spring.initializr.web.project.ProjectGenerationInvoker;
public class CustomProjectGenerationController extends ProjectGenerationController<CustomProjectRequest> {
public CustomProjectGenerationController(InitializrMetadataProvider metadataProvider,
ProjectGenerationInvoker projectGenerationInvoker) {
ProjectGenerationInvoker<CustomProjectRequest> projectGenerationInvoker) {
super(metadataProvider, projectGenerationInvoker);
}

View File

@@ -40,7 +40,7 @@ import io.spring.initializr.web.controller.ProjectMetadataController;
import io.spring.initializr.web.controller.SpringCliDistributionController;
import io.spring.initializr.web.project.DefaultProjectRequestToDescriptionConverter;
import io.spring.initializr.web.project.ProjectGenerationInvoker;
import io.spring.initializr.web.project.ProjectRequestToDescriptionConverter;
import io.spring.initializr.web.project.ProjectRequest;
import io.spring.initializr.web.support.DefaultDependencyMetadataProvider;
import io.spring.initializr.web.support.DefaultInitializrMetadataProvider;
import io.spring.initializr.web.support.DefaultInitializrMetadataUpdateStrategy;
@@ -61,7 +61,6 @@ import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.support.NoOpCache;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
@@ -145,7 +144,9 @@ public class InitializrAutoConfiguration {
@Bean
@ConditionalOnMissingBean
ProjectGenerationController projectGenerationController(InitializrMetadataProvider metadataProvider,
ProjectGenerationInvoker projectGenerationInvoker) {
ApplicationContext applicationContext) {
ProjectGenerationInvoker<ProjectRequest> projectGenerationInvoker = new ProjectGenerationInvoker<>(
applicationContext, new DefaultProjectRequestToDescriptionConverter());
return new DefaultProjectGenerationController(metadataProvider, projectGenerationInvoker);
}
@@ -169,15 +170,6 @@ public class InitializrAutoConfiguration {
return new SpringCliDistributionController(metadataProvider);
}
@Bean
@ConditionalOnMissingBean
ProjectGenerationInvoker projectGenerationInvoker(ApplicationContext applicationContext,
ApplicationEventPublisher eventPublisher,
ObjectProvider<ProjectRequestToDescriptionConverter> projectRequestToDescriptionConverter) {
return new ProjectGenerationInvoker(applicationContext, eventPublisher, projectRequestToDescriptionConverter
.getIfAvailable(DefaultProjectRequestToDescriptionConverter::new));
}
@Bean
InitializrModule InitializrJacksonModule() {
return new InitializrModule();

View File

@@ -32,7 +32,7 @@ import io.spring.initializr.web.project.WebProjectRequest;
public class DefaultProjectGenerationController extends ProjectGenerationController<ProjectRequest> {
public DefaultProjectGenerationController(InitializrMetadataProvider metadataProvider,
ProjectGenerationInvoker projectGenerationInvoker) {
ProjectGenerationInvoker<ProjectRequest> projectGenerationInvoker) {
super(metadataProvider, projectGenerationInvoker);
}

View File

@@ -73,10 +73,10 @@ public abstract class ProjectGenerationController<R extends ProjectRequest> {
private final InitializrMetadataProvider metadataProvider;
private final ProjectGenerationInvoker projectGenerationInvoker;
private final ProjectGenerationInvoker<R> projectGenerationInvoker;
public ProjectGenerationController(InitializrMetadataProvider metadataProvider,
ProjectGenerationInvoker projectGenerationInvoker) {
ProjectGenerationInvoker<R> projectGenerationInvoker) {
this.metadataProvider = metadataProvider;
this.projectGenerationInvoker = projectGenerationInvoker;
}

View File

@@ -42,7 +42,8 @@ import org.springframework.util.StringUtils;
* @author Madhura Bhave
* @author HaiTao Zhang
*/
public class DefaultProjectRequestToDescriptionConverter implements ProjectRequestToDescriptionConverter {
public class DefaultProjectRequestToDescriptionConverter
implements ProjectRequestToDescriptionConverter<ProjectRequest> {
private static final Version VERSION_1_5_0 = Version.parse("1.5.0.RELEASE");

View File

@@ -45,20 +45,26 @@ import org.springframework.util.FileSystemUtils;
* Invokes the project generation API. This is an intermediate layer that can consume a
* {@link ProjectRequest} and trigger project generation based on the request.
*
* @param <R> the concrete {@link ProjectRequest} type
* @author Madhura Bhave
*/
public class ProjectGenerationInvoker {
public class ProjectGenerationInvoker<R extends ProjectRequest> {
private final ApplicationContext parentApplicationContext;
private final ApplicationEventPublisher eventPublisher;
private final ProjectRequestToDescriptionConverter requestConverter;
private final ProjectRequestToDescriptionConverter<R> requestConverter;
private transient Map<Path, List<Path>> temporaryFiles = new LinkedHashMap<>();
public ProjectGenerationInvoker(ApplicationContext parentApplicationContext,
ApplicationEventPublisher eventPublisher, ProjectRequestToDescriptionConverter requestConverter) {
ProjectRequestToDescriptionConverter<R> requestConverter) {
this(parentApplicationContext, parentApplicationContext, requestConverter);
}
protected ProjectGenerationInvoker(ApplicationContext parentApplicationContext,
ApplicationEventPublisher eventPublisher, ProjectRequestToDescriptionConverter<R> requestConverter) {
this.parentApplicationContext = parentApplicationContext;
this.eventPublisher = eventPublisher;
this.requestConverter = requestConverter;
@@ -70,7 +76,7 @@ public class ProjectGenerationInvoker {
* @param request the project request
* @return the {@link ProjectGenerationResult}
*/
public ProjectGenerationResult invokeProjectStructureGeneration(ProjectRequest request) {
public ProjectGenerationResult invokeProjectStructureGeneration(R request) {
InitializrMetadata metadata = this.parentApplicationContext.getBean(InitializrMetadataProvider.class).get();
try {
ProjectDescription description = this.requestConverter.convert(request, metadata);
@@ -86,7 +92,7 @@ public class ProjectGenerationInvoker {
}
}
private ProjectAssetGenerator<ProjectGenerationResult> generateProject(ProjectRequest request) {
private ProjectAssetGenerator<ProjectGenerationResult> generateProject(R request) {
return (context) -> {
Path projectDir = new DefaultProjectAssetGenerator().generate(context);
publishProjectGeneratedEvent(request, context);
@@ -101,7 +107,7 @@ public class ProjectGenerationInvoker {
* @param request the project request
* @return the generated build content
*/
public byte[] invokeBuildGeneration(ProjectRequest request) {
public byte[] invokeBuildGeneration(R request) {
InitializrMetadata metadata = this.parentApplicationContext.getBean(InitializrMetadataProvider.class).get();
try {
ProjectDescription description = this.requestConverter.convert(request, metadata);
@@ -115,7 +121,7 @@ public class ProjectGenerationInvoker {
}
}
private ProjectAssetGenerator<byte[]> generateBuild(ProjectRequest request) {
private ProjectAssetGenerator<byte[]> generateBuild(R request) {
return (context) -> {
byte[] content = generateBuild(context);
publishProjectGeneratedEvent(request, context);
@@ -180,13 +186,13 @@ public class ProjectGenerationInvoker {
context.getBean(ProjectDescription.class).getPlatformVersion()));
}
private void publishProjectGeneratedEvent(ProjectRequest request, ProjectGenerationContext context) {
private void publishProjectGeneratedEvent(R request, ProjectGenerationContext context) {
InitializrMetadata metadata = context.getBean(InitializrMetadata.class);
ProjectGeneratedEvent event = new ProjectGeneratedEvent(request, metadata);
this.eventPublisher.publishEvent(event);
}
private void publishProjectFailedEvent(ProjectRequest request, InitializrMetadata metadata, Exception cause) {
private void publishProjectFailedEvent(R request, InitializrMetadata metadata, Exception cause) {
ProjectFailedEvent event = new ProjectFailedEvent(request, metadata, cause);
this.eventPublisher.publishEvent(event);
}

View File

@@ -22,10 +22,11 @@ import io.spring.initializr.metadata.InitializrMetadata;
/**
* Convert a {@link ProjectRequest} to a {@link ProjectDescription}.
*
* @param <R> the concrete {@link ProjectRequest} type
* @author Stephane Nicoll
*/
@FunctionalInterface
public interface ProjectRequestToDescriptionConverter {
public interface ProjectRequestToDescriptionConverter<R extends ProjectRequest> {
/**
* Validate and convert the specified {@link ProjectRequest} to a
@@ -35,6 +36,6 @@ public interface ProjectRequestToDescriptionConverter {
* @return a validated {@link ProjectDescription} to use to generate a project that
* matches the specified {@code request}
*/
ProjectDescription convert(ProjectRequest request, InitializrMetadata metadata);
ProjectDescription convert(R request, InitializrMetadata metadata);
}

View File

@@ -23,8 +23,6 @@ import io.spring.initializr.web.controller.CommandLineMetadataController;
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.ProjectGenerationInvoker;
import io.spring.initializr.web.project.ProjectRequestToDescriptionConverter;
import io.spring.initializr.web.support.DefaultInitializrMetadataUpdateStrategy;
import io.spring.initializr.web.support.InitializrMetadataUpdateStrategy;
import org.junit.jupiter.api.Test;
@@ -129,7 +127,6 @@ class InitializrAutoConfigurationTests {
.withConfiguration(BASIC_AUTO_CONFIGURATIONS);
webContextRunner.run((context) -> {
assertThat(context).hasSingleBean(InitializrWebConfig.class);
assertThat(context).hasSingleBean(ProjectGenerationInvoker.class);
assertThat(context).hasSingleBean(ProjectGenerationController.class);
assertThat(context).hasSingleBean(ProjectMetadataController.class);
assertThat(context).hasSingleBean(CommandLineMetadataController.class);
@@ -138,12 +135,12 @@ class InitializrAutoConfigurationTests {
}
@Test
void autoConfigWithCustomProjectRequestConverter() {
void autoConfigWithCustomProjectGenerationController() {
new WebApplicationContextRunner().withConfiguration(BASIC_AUTO_CONFIGURATIONS)
.withUserConfiguration(CustomProjectRequestToDescriptionConverter.class).run((context) -> {
assertThat(context).hasSingleBean(ProjectGenerationInvoker.class);
assertThat(context.getBean(ProjectGenerationInvoker.class)).hasFieldOrPropertyWithValue(
"requestConverter", context.getBean("testProjectRequestToDescriptionConverter"));
.withUserConfiguration(CustomProjectGenerationController.class).run((context) -> {
assertThat(context).hasSingleBean(ProjectGenerationController.class);
assertThat(context.getBean(ProjectGenerationController.class))
.isSameAs(context.getBean("testProjectGenerationController"));
});
}
@@ -223,11 +220,11 @@ class InitializrAutoConfigurationTests {
}
@Configuration
static class CustomProjectRequestToDescriptionConverter {
static class CustomProjectGenerationController {
@Bean
ProjectRequestToDescriptionConverter testProjectRequestToDescriptionConverter() {
return mock(ProjectRequestToDescriptionConverter.class);
ProjectGenerationController<?> testProjectGenerationController() {
return mock(ProjectGenerationController.class);
}
}

View File

@@ -31,7 +31,7 @@ import io.spring.initializr.web.project.ProjectGenerationInvoker;
class CustomProjectGenerationController extends ProjectGenerationController<CustomProjectRequest> {
CustomProjectGenerationController(InitializrMetadataProvider metadataProvider,
ProjectGenerationInvoker projectGenerationInvoker) {
ProjectGenerationInvoker<CustomProjectRequest> projectGenerationInvoker) {
super(metadataProvider, projectGenerationInvoker);
}

View File

@@ -25,10 +25,10 @@ import io.spring.initializr.web.controller.ProjectGenerationController;
import io.spring.initializr.web.controller.custom.ProjectGenerationControllerCustomRequestIntegrationTests.CustomProjectGenerationConfiguration;
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.ProjectRequestToDescriptionConverter;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@@ -73,25 +73,22 @@ public class ProjectGenerationControllerCustomRequestIntegrationTests
@Bean
CustomProjectGenerationController customProjectGenerationController(InitializrMetadataProvider metadataProvider,
ProjectGenerationInvoker projectGenerationInvoker) {
ApplicationContext applicationContext) {
ProjectGenerationInvoker<CustomProjectRequest> projectGenerationInvoker = new ProjectGenerationInvoker<>(
applicationContext, new CustomProjectRequestToDescriptionConverter());
return new CustomProjectGenerationController(metadataProvider, projectGenerationInvoker);
}
@Bean
ProjectRequestToDescriptionConverter customProjectRequestToDescriptionConverter() {
return new CustomProjectRequestToDescriptionConverter();
}
}
static class CustomProjectRequestToDescriptionConverter implements ProjectRequestToDescriptionConverter {
static class CustomProjectRequestToDescriptionConverter
implements ProjectRequestToDescriptionConverter<CustomProjectRequest> {
@Override
public ProjectDescription convert(ProjectRequest request, InitializrMetadata metadata) {
CustomProjectRequest customRequest = (CustomProjectRequest) request;
public ProjectDescription convert(CustomProjectRequest request, InitializrMetadata metadata) {
CustomProjectDescription description = new CustomProjectDescription();
new DefaultProjectRequestToDescriptionConverter().convert(request, description, metadata);
description.setCustomFlag(customRequest.isCustomFlag());
description.setCustomFlag(request.isCustomFlag());
// Override attributes for test purposes
description.setPackageName("org.example.custom");
description.setApplicationName("CustomApp");