From 56210d9bf2ffe88ad57d85ff1dc0520f50e1774d Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 19 Jul 2016 11:37:38 +0200 Subject: [PATCH] Add project request post processing This commit introduces `ProjectRequestPostProcessor` as a mean to modify an incoming `ProjectRequest` before and after its resolution against the meta-data. By default, all beans of that type are invoked automatically and they are ordered against their `@Order` or `Ordered` settings. Closes gh-260 --- .../generator/ProjectGenerator.groovy | 9 +- .../ProjectRequestPostProcessor.groovy | 53 ++++++++++++ .../ProjectRequestPostProcessorAdapter.groovy | 21 +++++ .../generator/ProjectRequestResolver.groovy | 40 +++++++++ .../AbstractProjectGeneratorTests.groovy | 1 + .../generator/ProjectGeneratorTests.groovy | 29 +++++++ .../ProjectRequestResolverTests.groovy | 86 +++++++++++++++++++ .../InitializrAutoConfiguration.groovy | 12 +++ ...ProjectGenerationPostProcessorTests.groovy | 59 +++++++++++++ 9 files changed, 307 insertions(+), 3 deletions(-) create mode 100644 initializr-generator/src/main/groovy/io/spring/initializr/generator/ProjectRequestPostProcessor.groovy create mode 100644 initializr-generator/src/main/groovy/io/spring/initializr/generator/ProjectRequestPostProcessorAdapter.groovy create mode 100644 initializr-generator/src/main/groovy/io/spring/initializr/generator/ProjectRequestResolver.groovy create mode 100644 initializr-generator/src/test/groovy/io/spring/initializr/generator/ProjectRequestResolverTests.groovy create mode 100644 initializr-web/src/test/groovy/io/spring/initializr/web/project/ProjectGenerationPostProcessorTests.groovy diff --git a/initializr-generator/src/main/groovy/io/spring/initializr/generator/ProjectGenerator.groovy b/initializr-generator/src/main/groovy/io/spring/initializr/generator/ProjectGenerator.groovy index 05aceecc..1a71acf1 100644 --- a/initializr-generator/src/main/groovy/io/spring/initializr/generator/ProjectGenerator.groovy +++ b/initializr-generator/src/main/groovy/io/spring/initializr/generator/ProjectGenerator.groovy @@ -55,6 +55,9 @@ class ProjectGenerator { @Autowired InitializrMetadataProvider metadataProvider + @Autowired + ProjectRequestResolver requestResolver + @Autowired ProjectResourceLocator projectResourceLocator = new ProjectResourceLocator() @@ -210,12 +213,12 @@ class ProjectGenerator { * @param request the request to handle * @return a model for that request */ - protected Map resolveModel(ProjectRequest request) { - Assert.notNull request.bootVersion, 'boot version must not be null' + protected Map resolveModel(ProjectRequest originalRequest) { + Assert.notNull originalRequest.bootVersion, 'boot version must not be null' def model = [:] def metadata = metadataProvider.get() - request.resolve(metadata) + ProjectRequest request = requestResolver.resolve(originalRequest, metadata) // request resolved so we can log what has been requested def dependencies = request.resolvedDependencies diff --git a/initializr-generator/src/main/groovy/io/spring/initializr/generator/ProjectRequestPostProcessor.groovy b/initializr-generator/src/main/groovy/io/spring/initializr/generator/ProjectRequestPostProcessor.groovy new file mode 100644 index 00000000..fa67a008 --- /dev/null +++ b/initializr-generator/src/main/groovy/io/spring/initializr/generator/ProjectRequestPostProcessor.groovy @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2016 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 + * + * http://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.generator + +import io.spring.initializr.metadata.InitializrMetadata + +/** + * Project generation hook that allows for custom modification of {@link ProjectRequest} + * instances, e.g. adding custom dependencies or forcing certain settings based on custom + * logic. + * + * @author Stephane Nicoll + */ +interface ProjectRequestPostProcessor { + + /** + * Apply this post processor to the given {@code ProjectRequest} before it + * gets resolved against the specified {@code InitializrMetadata}. + *

Consider using this hook to customize basic settings of the {@code request}; + * for more advanced logic (in particular with regards to dependencies), consider + * using {@code postProcessAfterResolution}. + * @param request an unresolved {@link ProjectRequest} + * @param metadata the metadata to use to resolve this request + * @see ProjectRequest#resolve(InitializrMetadata) + */ + void postProcessBeforeResolution(ProjectRequest request, InitializrMetadata metadata); + + /** + * Apply this post processor to the given {@code ProjectRequest} after it has + * been resolved against the specified {@code InitializrMetadata}. + *

Dependencies, repositories, bills of materials, default properties and others + * aspects of the request will have been resolved prior to invocation. In particular, + * note that no further validation checks will be performed. + * @param request an resolved {@code ProjectRequest} + * @param metadata the metadata that were used to resolve this request + */ + void postProcessAfterResolution(ProjectRequest request, InitializrMetadata metadata); + +} diff --git a/initializr-generator/src/main/groovy/io/spring/initializr/generator/ProjectRequestPostProcessorAdapter.groovy b/initializr-generator/src/main/groovy/io/spring/initializr/generator/ProjectRequestPostProcessorAdapter.groovy new file mode 100644 index 00000000..b6dbefe5 --- /dev/null +++ b/initializr-generator/src/main/groovy/io/spring/initializr/generator/ProjectRequestPostProcessorAdapter.groovy @@ -0,0 +1,21 @@ +package io.spring.initializr.generator + +import io.spring.initializr.metadata.InitializrMetadata + +/** + * An implementation of {@link ProjectRequestPostProcessor} with empty methods allowing + * sub-classes to override only the methods they're interested in. + * + * @author Stephane Nicoll + */ +class ProjectRequestPostProcessorAdapter implements ProjectRequestPostProcessor { + + @Override + void postProcessBeforeResolution(ProjectRequest request, InitializrMetadata metadata) { + } + + @Override + void postProcessAfterResolution(ProjectRequest request, InitializrMetadata metadata) { + } + +} diff --git a/initializr-generator/src/main/groovy/io/spring/initializr/generator/ProjectRequestResolver.groovy b/initializr-generator/src/main/groovy/io/spring/initializr/generator/ProjectRequestResolver.groovy new file mode 100644 index 00000000..7d708c9e --- /dev/null +++ b/initializr-generator/src/main/groovy/io/spring/initializr/generator/ProjectRequestResolver.groovy @@ -0,0 +1,40 @@ +package io.spring.initializr.generator + +import io.spring.initializr.metadata.InitializrMetadata + +import org.springframework.util.Assert + +/** + * Resolve {@link ProjectRequest} instances, honoring callback hook points. + * + * @author Stephane Nicoll + */ +class ProjectRequestResolver { + + private final List postProcessors + + ProjectRequestResolver(List postProcessors) { + this.postProcessors = new ArrayList<>(postProcessors) + } + + ProjectRequest resolve(ProjectRequest request, InitializrMetadata metadata) { + Assert.notNull(request, "Request must not be null") + applyPostProcessBeforeResolution(request, metadata) + request.resolve(metadata) + applyPostProcessAfterResolution(request, metadata) + request + } + + private void applyPostProcessBeforeResolution(ProjectRequest request, InitializrMetadata metadata) { + for (ProjectRequestPostProcessor processor : postProcessors) { + processor.postProcessBeforeResolution(request, metadata) + } + } + + private void applyPostProcessAfterResolution(ProjectRequest request, InitializrMetadata metadata) { + for (ProjectRequestPostProcessor processor : postProcessors) { + processor.postProcessAfterResolution(request, metadata) + } + } + +} diff --git a/initializr-generator/src/test/groovy/io/spring/initializr/generator/AbstractProjectGeneratorTests.groovy b/initializr-generator/src/test/groovy/io/spring/initializr/generator/AbstractProjectGeneratorTests.groovy index 9c480b45..6fb22d38 100644 --- a/initializr-generator/src/test/groovy/io/spring/initializr/generator/AbstractProjectGeneratorTests.groovy +++ b/initializr-generator/src/test/groovy/io/spring/initializr/generator/AbstractProjectGeneratorTests.groovy @@ -57,6 +57,7 @@ abstract class AbstractProjectGeneratorTests { .addDependencyGroup('test', 'security', 'data-jpa', 'aop', 'batch', 'integration').build() applyMetadata(metadata) projectGenerator.eventPublisher = eventPublisher + projectGenerator.requestResolver = new ProjectRequestResolver([]) projectGenerator.tmpdir = folder.newFolder().absolutePath } diff --git a/initializr-generator/src/test/groovy/io/spring/initializr/generator/ProjectGeneratorTests.groovy b/initializr-generator/src/test/groovy/io/spring/initializr/generator/ProjectGeneratorTests.groovy index 5049a784..3334f4b4 100644 --- a/initializr-generator/src/test/groovy/io/spring/initializr/generator/ProjectGeneratorTests.groovy +++ b/initializr-generator/src/test/groovy/io/spring/initializr/generator/ProjectGeneratorTests.groovy @@ -18,6 +18,7 @@ package io.spring.initializr.generator import io.spring.initializr.metadata.BillOfMaterials import io.spring.initializr.metadata.Dependency +import io.spring.initializr.metadata.InitializrMetadata import io.spring.initializr.test.metadata.InitializrMetadataTestBuilder import org.junit.Rule import org.junit.Test @@ -642,6 +643,34 @@ class ProjectGeneratorTests extends AbstractProjectGeneratorTests { .doesNotContain('ignore.property') } + @Test + void versionRangeWithPostProcessor() { + Dependency foo = new Dependency(id: 'foo', groupId: 'org.acme', artifactId: 'foo') + foo.mappings << new Dependency.Mapping(versionRange: '[1.2.0.RELEASE,1.3.0.M1)', version: '1.0.0') + foo.mappings << new Dependency.Mapping(versionRange: '1.3.0.M1', version: '1.2.0') + def metadata = InitializrMetadataTestBuilder.withDefaults() + .addDependencyGroup('foo', foo).build() + applyMetadata(metadata) + + // First without processor, get the correct version + def request = createProjectRequest('foo') + request.bootVersion = '1.2.5.RELEASE' + generateMavenPom(request).hasDependency( + new Dependency(id: 'foo', groupId: 'org.acme', artifactId: 'foo', version: '1.0.0')) + + // First after processor that flips Spring Boot version + projectGenerator.requestResolver = new ProjectRequestResolver(Collections.singletonList( + new ProjectRequestPostProcessorAdapter() { + @Override + void postProcessBeforeResolution(ProjectRequest r, InitializrMetadata m) { + r.bootVersion = '1.3.0.M2' + } + } + )) + generateMavenPom(request).hasDependency( + new Dependency(id: 'foo', groupId: 'org.acme', artifactId: 'foo', version: '1.2.0')) + } + @Test void invalidProjectTypeMavenPom() { def request = createProjectRequest('web') diff --git a/initializr-generator/src/test/groovy/io/spring/initializr/generator/ProjectRequestResolverTests.groovy b/initializr-generator/src/test/groovy/io/spring/initializr/generator/ProjectRequestResolverTests.groovy new file mode 100644 index 00000000..1a6be5b1 --- /dev/null +++ b/initializr-generator/src/test/groovy/io/spring/initializr/generator/ProjectRequestResolverTests.groovy @@ -0,0 +1,86 @@ +package io.spring.initializr.generator + +import io.spring.initializr.metadata.InitializrMetadata +import io.spring.initializr.test.metadata.InitializrMetadataTestBuilder +import org.junit.Before +import org.junit.Test + +import static org.junit.Assert.assertEquals + +/** + * Tests for {@link ProjectRequestResolver}. + * + * @author Stephane Nicoll + */ +class ProjectRequestResolverTests { + + def metadata = InitializrMetadataTestBuilder.withDefaults() + .addDependencyGroup('test', 'web', 'security', 'data-jpa') + .build() + + final List postProcessors = [] + final GenericProjectRequestPostProcessor processor = new GenericProjectRequestPostProcessor() + + @Before + void setup() { + this.postProcessors << processor + } + + @Test + void beforeResolution() { + processor.before['javaVersion'] = '1.2' + ProjectRequest request = resolve(createMavenProjectRequest(), postProcessors) + assertEquals '1.2', request.javaVersion + assertEquals '1.2', request.buildProperties.versions['java.version'].call() + } + + @Test + void afterResolution() { + postProcessors << new ProjectRequestPostProcessorAdapter() { + @Override + void postProcessAfterResolution(ProjectRequest request, InitializrMetadata metadata) { + request.buildProperties.maven.clear() + request.buildProperties.maven['foo'] = { 'bar' } + } + } + ProjectRequest request = resolve(createMavenProjectRequest(), postProcessors) + assertEquals 1, request.buildProperties.maven.size() + assertEquals 'bar', request.buildProperties.maven['foo'].call() + } + + ProjectRequest resolve(def request, def processors) { + new ProjectRequestResolver(processors) + .resolve(request, metadata) + } + + ProjectRequest createMavenProjectRequest(String... styles) { + def request = createProjectRequest(styles) + request.type = 'maven-project' + request + } + + ProjectRequest createProjectRequest(String... styles) { + def request = new ProjectRequest() + request.initialize(metadata) + request.style.addAll Arrays.asList(styles) + request + } + + static class GenericProjectRequestPostProcessor implements ProjectRequestPostProcessor { + + final Map before = [:] + final Map after = [:] + + @Override + void postProcessBeforeResolution(ProjectRequest request, InitializrMetadata metadata) { + before.forEach { k, v -> request.setProperty(k, v) } + } + + @Override + void postProcessAfterResolution(ProjectRequest request, InitializrMetadata metadata) { + after.forEach { k, v -> request.setProperty(k, v) } + } + + } + +} diff --git a/initializr-web/src/main/groovy/io/spring/initializr/web/autoconfigure/InitializrAutoConfiguration.groovy b/initializr-web/src/main/groovy/io/spring/initializr/web/autoconfigure/InitializrAutoConfiguration.groovy index 5442bb34..e309a53b 100644 --- a/initializr-web/src/main/groovy/io/spring/initializr/web/autoconfigure/InitializrAutoConfiguration.groovy +++ b/initializr-web/src/main/groovy/io/spring/initializr/web/autoconfigure/InitializrAutoConfiguration.groovy @@ -20,6 +20,8 @@ import java.util.concurrent.TimeUnit import com.google.common.cache.CacheBuilder import io.spring.initializr.generator.ProjectGenerator +import io.spring.initializr.generator.ProjectRequestPostProcessor +import io.spring.initializr.generator.ProjectRequestResolver import io.spring.initializr.generator.ProjectResourceLocator import io.spring.initializr.metadata.DependencyMetadataProvider import io.spring.initializr.metadata.InitializrMetadataBuilder @@ -30,6 +32,7 @@ import io.spring.initializr.web.support.DefaultDependencyMetadataProvider import io.spring.initializr.web.support.DefaultInitializrMetadataProvider import io.spring.initializr.web.ui.UiController +import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.cache.CacheManager @@ -56,6 +59,9 @@ import org.springframework.context.annotation.Configuration @EnableConfigurationProperties(InitializrProperties) class InitializrAutoConfiguration { + @Autowired(required = false) + List postProcessors = [] + @Bean WebConfig webConfig() { new WebConfig() @@ -79,6 +85,12 @@ class InitializrAutoConfiguration { new ProjectGenerator() } + @Bean + @ConditionalOnMissingBean + ProjectRequestResolver projectRequestResolver() { + new ProjectRequestResolver(postProcessors) + } + @Bean ProjectResourceLocator projectResourceLocator() { return new ProjectResourceLocator() diff --git a/initializr-web/src/test/groovy/io/spring/initializr/web/project/ProjectGenerationPostProcessorTests.groovy b/initializr-web/src/test/groovy/io/spring/initializr/web/project/ProjectGenerationPostProcessorTests.groovy new file mode 100644 index 00000000..cfc7a372 --- /dev/null +++ b/initializr-web/src/test/groovy/io/spring/initializr/web/project/ProjectGenerationPostProcessorTests.groovy @@ -0,0 +1,59 @@ +package io.spring.initializr.web.project + +import io.spring.initializr.generator.ProjectRequest +import io.spring.initializr.generator.ProjectRequestPostProcessor +import io.spring.initializr.generator.ProjectRequestPostProcessorAdapter +import io.spring.initializr.metadata.InitializrMetadata +import io.spring.initializr.web.AbstractInitializrControllerIntegrationTests +import org.junit.Test + +import org.springframework.boot.test.SpringApplicationConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.annotation.Order +import org.springframework.test.context.ActiveProfiles + +@ActiveProfiles('test-default') +@SpringApplicationConfiguration(classes = [Config, ProjectRequestPostProcessorConfiguration]) +class ProjectGenerationPostProcessorTests extends AbstractInitializrControllerIntegrationTests { + + + @Test + void postProcessorsInvoked() { + downloadZip('/starter.zip?bootVersion=1.2.4.RELEASE&javaVersion=1.6') + .isJavaProject() + .isMavenProject().pomAssert() + .hasSpringBootParent('1.2.3.RELEASE') + .hasProperty('java.version', '1.7') + } + + + @Configuration + static class ProjectRequestPostProcessorConfiguration { + + @Bean + @Order(2) + ProjectRequestPostProcessor secondPostProcessor() { + new ProjectRequestPostProcessorAdapter() { + @Override + void postProcessBeforeResolution(ProjectRequest request, InitializrMetadata metadata) { + request.javaVersion = '1.7' + } + } + } + + @Bean + @Order(1) + ProjectRequestPostProcessor firstPostProcessor() { + new ProjectRequestPostProcessorAdapter() { + @Override + void postProcessBeforeResolution(ProjectRequest request, InitializrMetadata metadata) { + request.javaVersion = '1.2' + request.bootVersion = '1.2.3.RELEASE' + } + } + } + + } + +}