Fire event if a project could not be generated

Previously, only an invalid type or an invalid dependency would lead to
an exception and in such case, no event is fired at all.

This commit adds validation for language and packaging as well as a new
event that is fired when the project could not be generated.

The metrics infrastructure has been updated to handle ProjectFailedEvent;
when such an event is fired, the 'failures' counter is increased and we
still record all the other metrics.

Closes gh-188
This commit is contained in:
Stephane Nicoll 2016-02-01 15:03:03 +01:00
parent 3f1b4eca13
commit f165405b26
8 changed files with 237 additions and 49 deletions

View File

@ -0,0 +1,37 @@
/*
* 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
/**
* Event published when an error occured trying to generate a project.
*
* @author Stephane Nicoll
* @since 1.0
*/
class ProjectFailedEvent extends ProjectRequestEvent {
/**
* The cause of the failure.
*/
final Exception cause
ProjectFailedEvent(ProjectRequest projectRequest, Exception cause) {
super(projectRequest)
this.cause = cause
}
}

View File

@ -17,26 +17,15 @@
package io.spring.initializr.generator package io.spring.initializr.generator
/** /**
* Event fired when a new project has been generated successfully. * Event published when a new project has been generated successfully.
* *
* @author Stephane Nicoll * @author Stephane Nicoll
* @since 1.0 * @since 1.0
*/ */
class ProjectGeneratedEvent { class ProjectGeneratedEvent extends ProjectRequestEvent {
/**
* The {@link ProjectRequest} used to generate the project.
*/
final ProjectRequest projectRequest
/**
* The timestamp at which the project was generated
*/
final long timestamp
ProjectGeneratedEvent(ProjectRequest projectRequest) { ProjectGeneratedEvent(ProjectRequest projectRequest) {
this.projectRequest = projectRequest super(projectRequest)
this.timestamp = System.currentTimeMillis()
} }
} }

View File

@ -39,7 +39,16 @@ class ProjectGenerationMetricsListener {
@EventListener @EventListener
void onGeneratedProject(ProjectGeneratedEvent event) { void onGeneratedProject(ProjectGeneratedEvent event) {
def request = event.projectRequest handleProjectRequest(event.projectRequest)
}
@EventListener
void onFailedProject(ProjectFailedEvent event) {
handleProjectRequest(event.projectRequest)
increment(key('failures'))
}
protected void handleProjectRequest(ProjectRequest request) {
increment(key('requests')) // Total number of requests increment(key('requests')) // Total number of requests
handleDependencies(request) handleDependencies(request)
handleType(request) handleType(request)

View File

@ -17,6 +17,7 @@
package io.spring.initializr.generator package io.spring.initializr.generator
import groovy.util.logging.Slf4j import groovy.util.logging.Slf4j
import io.spring.initializr.InitializrException
import io.spring.initializr.metadata.Dependency import io.spring.initializr.metadata.Dependency
import io.spring.initializr.metadata.InitializrMetadataProvider import io.spring.initializr.metadata.InitializrMetadataProvider
import io.spring.initializr.util.Version import io.spring.initializr.util.Version
@ -60,20 +61,30 @@ class ProjectGenerator {
* Generate a Maven pom for the specified {@link ProjectRequest}. * Generate a Maven pom for the specified {@link ProjectRequest}.
*/ */
byte[] generateMavenPom(ProjectRequest request) { byte[] generateMavenPom(ProjectRequest request) {
def model = initializeModel(request) try {
def content = doGenerateMavenPom(model) def model = initializeModel(request)
publishProjectGeneratedEvent(request) def content = doGenerateMavenPom(model)
content publishProjectGeneratedEvent(request)
content
} catch (InitializrException ex) {
publishProjectFailedEvent(request, ex)
throw ex
}
} }
/** /**
* Generate a Gradle build file for the specified {@link ProjectRequest}. * Generate a Gradle build file for the specified {@link ProjectRequest}.
*/ */
byte[] generateGradleBuild(ProjectRequest request) { byte[] generateGradleBuild(ProjectRequest request) {
def model = initializeModel(request) try {
def content = doGenerateGradleBuild(model) def model = initializeModel(request)
publishProjectGeneratedEvent(request) def content = doGenerateGradleBuild(model)
content publishProjectGeneratedEvent(request)
content
} catch (InitializrException ex) {
publishProjectFailedEvent(request, ex)
throw ex
}
} }
/** /**
@ -81,6 +92,15 @@ class ProjectGenerator {
* a directory containing the project. * a directory containing the project.
*/ */
File generateProjectStructure(ProjectRequest request) { File generateProjectStructure(ProjectRequest request) {
try {
doGenerateProjectStructure(request)
} catch (InitializrException ex) {
publishProjectFailedEvent(request, ex)
throw ex
}
}
protected File doGenerateProjectStructure(ProjectRequest request) {
def model = initializeModel(request) def model = initializeModel(request)
def rootDir = File.createTempFile('tmp', '', new File(tmpdir)) def rootDir = File.createTempFile('tmp', '', new File(tmpdir))
@ -171,6 +191,11 @@ class ProjectGenerator {
eventPublisher.publishEvent(event) eventPublisher.publishEvent(event)
} }
private void publishProjectFailedEvent(ProjectRequest request, Exception cause) {
ProjectFailedEvent event = new ProjectFailedEvent(request, cause)
eventPublisher.publishEvent(event)
}
protected Map initializeModel(ProjectRequest request) { protected Map initializeModel(ProjectRequest request) {
Assert.notNull request.bootVersion, 'boot version must not be null' Assert.notNull request.bootVersion, 'boot version must not be null'
def model = [:] def model = [:]

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2015 the original author or authors. * Copyright 2012-2016 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -137,6 +137,18 @@ class ProjectRequest {
this.build = buildTag this.build = buildTag
} }
} }
if (this.packaging) {
def packaging = metadata.packagings.get(this.packaging)
if (!packaging) {
throw new InvalidProjectRequestException("Unknown packaging '${this.packaging}' check project metadata")
}
}
if (this.language) {
def language = metadata.languages.get(this.language)
if (!language) {
throw new InvalidProjectRequestException("Unknown language '${this.language}' check project metadata")
}
}
if (!applicationName) { if (!applicationName) {
this.applicationName = metadata.configuration.generateApplicationName(this.name) this.applicationName = metadata.configuration.generateApplicationName(this.name)

View File

@ -0,0 +1,44 @@
/*
* 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
/**
* Event published when a {@link ProjectRequest} has been processed.
*
* @author Stephane Nicoll
* @since 1.0
* @see ProjectGeneratedEvent
* @see ProjectFailedEvent
*/
abstract class ProjectRequestEvent {
/**
* The {@link ProjectRequest} used to generate the project.
*/
final ProjectRequest projectRequest
/**
* The timestamp at which the request was processed.
*/
final long timestamp
protected ProjectRequestEvent(ProjectRequest projectRequest) {
this.projectRequest = projectRequest
this.timestamp = System.currentTimeMillis()
}
}

View File

@ -47,16 +47,25 @@ class ProjectGenerationMetricsListenerTests {
void projectGenerationCount() { void projectGenerationCount() {
def request = initialize() def request = initialize()
request.resolve(metadata) request.resolve(metadata)
fireEvent(request) fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.requests') metricsAssert.hasValue(1, 'initializr.requests')
} }
@Test
void projectGenerationCountWithFailure() {
def request = initialize()
request.resolve(metadata)
fireProjectFailedEvent(request)
metricsAssert.hasValue(1, 'initializr.requests')
metricsAssert.hasValue(1, 'initializr.failures')
}
@Test @Test
void dependencies() { void dependencies() {
def request = initialize() def request = initialize()
request.style << 'security' << 'spring-data' request.style << 'security' << 'spring-data'
request.resolve(metadata) request.resolve(metadata)
fireEvent(request) fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.dependency.security', metricsAssert.hasValue(1, 'initializr.dependency.security',
'initializr.dependency.spring-data') 'initializr.dependency.spring-data')
} }
@ -65,7 +74,7 @@ class ProjectGenerationMetricsListenerTests {
void noDependencies() { void noDependencies() {
def request = initialize() def request = initialize()
request.resolve(metadata) request.resolve(metadata)
fireEvent(request) fireProjectGeneratedEvent(request)
metricsAssert.hasNoValue('initializr.dependency.') metricsAssert.hasNoValue('initializr.dependency.')
} }
@ -75,7 +84,7 @@ class ProjectGenerationMetricsListenerTests {
request.style << 'spring-data' request.style << 'spring-data'
request.packaging = 'war' request.packaging = 'war'
request.resolve(metadata) request.resolve(metadata)
fireEvent(request) fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.dependency.web', metricsAssert.hasValue(1, 'initializr.dependency.web',
'initializr.dependency.spring-data') 'initializr.dependency.spring-data')
} }
@ -91,7 +100,7 @@ class ProjectGenerationMetricsListenerTests {
request.initialize(metadata) request.initialize(metadata)
request.style << 'foo-old' request.style << 'foo-old'
request.resolve(metadata) request.resolve(metadata)
fireEvent(request) fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.dependency.foo') // standard id is used metricsAssert.hasValue(1, 'initializr.dependency.foo') // standard id is used
} }
@ -99,7 +108,7 @@ class ProjectGenerationMetricsListenerTests {
void defaultType() { void defaultType() {
def request = initialize() def request = initialize()
request.resolve(metadata) request.resolve(metadata)
fireEvent(request) fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.type.maven-project') metricsAssert.hasValue(1, 'initializr.type.maven-project')
} }
@ -108,7 +117,7 @@ class ProjectGenerationMetricsListenerTests {
def request = initialize() def request = initialize()
request.type = 'gradle-build' request.type = 'gradle-build'
request.resolve(metadata) request.resolve(metadata)
fireEvent(request) fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.type.gradle-build') metricsAssert.hasValue(1, 'initializr.type.gradle-build')
} }
@ -116,7 +125,7 @@ class ProjectGenerationMetricsListenerTests {
void defaultPackaging() { void defaultPackaging() {
def request = initialize() def request = initialize()
request.resolve(metadata) request.resolve(metadata)
fireEvent(request) fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.packaging.jar') metricsAssert.hasValue(1, 'initializr.packaging.jar')
} }
@ -125,7 +134,7 @@ class ProjectGenerationMetricsListenerTests {
def request = initialize() def request = initialize()
request.packaging = 'war' request.packaging = 'war'
request.resolve(metadata) request.resolve(metadata)
fireEvent(request) fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.packaging.war') metricsAssert.hasValue(1, 'initializr.packaging.war')
} }
@ -133,7 +142,7 @@ class ProjectGenerationMetricsListenerTests {
void defaultJavaVersion() { void defaultJavaVersion() {
def request = initialize() def request = initialize()
request.resolve(metadata) request.resolve(metadata)
fireEvent(request) fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.java_version.1_8') metricsAssert.hasValue(1, 'initializr.java_version.1_8')
} }
@ -142,7 +151,7 @@ class ProjectGenerationMetricsListenerTests {
def request = initialize() def request = initialize()
request.javaVersion = '1.7' request.javaVersion = '1.7'
request.resolve(metadata) request.resolve(metadata)
fireEvent(request) fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.java_version.1_7') metricsAssert.hasValue(1, 'initializr.java_version.1_7')
} }
@ -150,7 +159,7 @@ class ProjectGenerationMetricsListenerTests {
void defaultLanguage() { void defaultLanguage() {
def request = initialize() def request = initialize()
request.resolve(metadata) request.resolve(metadata)
fireEvent(request) fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.language.java') metricsAssert.hasValue(1, 'initializr.language.java')
} }
@ -159,7 +168,7 @@ class ProjectGenerationMetricsListenerTests {
def request = initialize() def request = initialize()
request.language = 'groovy' request.language = 'groovy'
request.resolve(metadata) request.resolve(metadata)
fireEvent(request) fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.language.groovy') metricsAssert.hasValue(1, 'initializr.language.groovy')
} }
@ -167,7 +176,7 @@ class ProjectGenerationMetricsListenerTests {
void defaultBootVersion() { void defaultBootVersion() {
def request = initialize() def request = initialize()
request.resolve(metadata) request.resolve(metadata)
fireEvent(request) fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.boot_version.1_2_3_RELEASE') metricsAssert.hasValue(1, 'initializr.boot_version.1_2_3_RELEASE')
} }
@ -176,7 +185,7 @@ class ProjectGenerationMetricsListenerTests {
def request = initialize() def request = initialize()
request.bootVersion = '1.0.2.RELEASE' request.bootVersion = '1.0.2.RELEASE'
request.resolve(metadata) request.resolve(metadata)
fireEvent(request) fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.boot_version.1_0_2_RELEASE') metricsAssert.hasValue(1, 'initializr.boot_version.1_0_2_RELEASE')
} }
@ -191,7 +200,7 @@ class ProjectGenerationMetricsListenerTests {
request.bootVersion = '1.0.2.RELEASE' request.bootVersion = '1.0.2.RELEASE'
request.resolve(metadata) request.resolve(metadata)
fireEvent(request) fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.requests', metricsAssert.hasValue(1, 'initializr.requests',
'initializr.dependency.web', 'initializr.dependency.security', 'initializr.dependency.web', 'initializr.dependency.security',
'initializr.type.gradle-project', 'initializr.packaging.jar', 'initializr.type.gradle-project', 'initializr.packaging.jar',
@ -204,24 +213,28 @@ class ProjectGenerationMetricsListenerTests {
def request = initialize() def request = initialize()
request.style << 'security' << 'spring-data' request.style << 'security' << 'spring-data'
request.resolve(metadata) request.resolve(metadata)
fireEvent(request) fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.requests', metricsAssert.hasValue(1, 'initializr.requests',
'initializr.dependency.security', 'initializr.dependency.spring-data') 'initializr.dependency.security', 'initializr.dependency.spring-data')
def anotherRequest = initialize() def anotherRequest = initialize()
anotherRequest.style << 'web' << 'spring-data' anotherRequest.style << 'web' << 'spring-data'
anotherRequest.resolve(metadata) anotherRequest.resolve(metadata)
fireEvent(anotherRequest) fireProjectGeneratedEvent(anotherRequest)
metricsAssert.hasValue(2, 'initializr.dependency.spring-data', metricsAssert.hasValue(2, 'initializr.dependency.spring-data',
'initializr.dependency.spring-data') 'initializr.dependency.spring-data')
metricsAssert.hasValue(1, 'initializr.dependency.web', metricsAssert.hasValue(1, 'initializr.dependency.web',
'initializr.dependency.security') 'initializr.dependency.security')
} }
private fireEvent(ProjectRequest projectRequest) { private fireProjectGeneratedEvent(ProjectRequest projectRequest) {
listener.onGeneratedProject(new ProjectGeneratedEvent(projectRequest)) listener.onGeneratedProject(new ProjectGeneratedEvent(projectRequest))
} }
private fireProjectFailedEvent(ProjectRequest projectRequest) {
listener.onFailedProject(new ProjectFailedEvent(projectRequest, null))
}
private ProjectRequest initialize() { private ProjectRequest initialize() {
def request = new ProjectRequest() def request = new ProjectRequest()
request.initialize(metadata) request.initialize(metadata)

View File

@ -36,6 +36,9 @@ import org.springframework.context.ApplicationEventPublisher
import org.springframework.context.annotation.ComponentScan import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import static org.hamcrest.CoreMatchers.containsString
import static org.junit.Assert.assertThat
import static org.junit.Assert.fail
import static org.mockito.Matchers.argThat import static org.mockito.Matchers.argThat
import static org.mockito.Mockito.mock import static org.mockito.Mockito.mock
import static org.mockito.Mockito.times import static org.mockito.Mockito.times
@ -67,14 +70,14 @@ class ProjectGeneratorTests {
def request = createProjectRequest('web') def request = createProjectRequest('web')
generateMavenPom(request).hasNoRepository() generateMavenPom(request).hasNoRepository()
.hasSpringBootStarterDependency('web') .hasSpringBootStarterDependency('web')
verify(eventPublisher, times(1)).publishEvent(argThat(new EventMatcher(request))) verify(eventPublisher, times(1)).publishEvent(argThat(new ProjectGeneratedEventMatcher(request)))
} }
@Test @Test
void defaultGradleBuild() { void defaultGradleBuild() {
def request = createProjectRequest('web') def request = createProjectRequest('web')
generateGradleBuild(request) generateGradleBuild(request)
verify(eventPublisher, times(1)).publishEvent(argThat(new EventMatcher(request))) verify(eventPublisher, times(1)).publishEvent(argThat(new ProjectGeneratedEventMatcher(request)))
} }
@Test @Test
@ -82,7 +85,7 @@ class ProjectGeneratorTests {
def request = createProjectRequest('web') def request = createProjectRequest('web')
generateProject(request).isJavaProject().isMavenProject().pomAssert() generateProject(request).isJavaProject().isMavenProject().pomAssert()
.hasNoRepository().hasSpringBootStarterDependency('web') .hasNoRepository().hasSpringBootStarterDependency('web')
verify(eventPublisher, times(1)).publishEvent(argThat(new EventMatcher(request))) verify(eventPublisher, times(1)).publishEvent(argThat(new ProjectGeneratedEventMatcher(request)))
} }
@Test @Test
@ -523,6 +526,45 @@ class ProjectGeneratorTests {
.hasDependenciesCount(3) .hasDependenciesCount(3)
} }
@Test
void invalidType() {
def request = createProjectRequest('web')
request.type = 'foo-bar'
try {
generateMavenPom(request)
fail("Should have failed to generate project")
} catch (InvalidProjectRequestException ex) {
assertThat ex.message, containsString('foo-bar')
verify(eventPublisher, times(1)).publishEvent(argThat(new ProjectFailedEventMatcher(request, ex)))
}
}
@Test
void invalidPackaging() {
def request = createProjectRequest('web')
request.packaging = 'foo-bar'
try {
generateGradleBuild(request)
fail("Should have failed to generate project")
} catch (InvalidProjectRequestException ex) {
assertThat ex.message, containsString('foo-bar')
verify(eventPublisher, times(1)).publishEvent(argThat(new ProjectFailedEventMatcher(request, ex)))
}
}
@Test
void invalidLanguage() {
def request = createProjectRequest('web')
request.language = 'foo-bar'
try {
generateProject(request)
fail("Should have failed to generate project")
} catch (InvalidProjectRequestException ex) {
assertThat ex.message, containsString('foo-bar')
verify(eventPublisher, times(1)).publishEvent(argThat(new ProjectFailedEventMatcher(request, ex)))
}
}
PomAssert generateMavenPom(ProjectRequest request) { PomAssert generateMavenPom(ProjectRequest request) {
def content = new String(projectGenerator.generateMavenPom(request)) def content = new String(projectGenerator.generateMavenPom(request))
@ -555,18 +597,35 @@ class ProjectGeneratorTests {
} }
} }
private static class EventMatcher extends ArgumentMatcher<ProjectGeneratedEvent> { private static class ProjectGeneratedEventMatcher extends ArgumentMatcher<ProjectGeneratedEvent> {
private final ProjectRequest request private final ProjectRequest request
EventMatcher(ProjectRequest request) { ProjectGeneratedEventMatcher(ProjectRequest request) {
this.request = request this.request = request
} }
@Override @Override
boolean matches(Object argument) { boolean matches(Object argument) {
ProjectGeneratedEvent event = (ProjectGeneratedEvent) argument ProjectGeneratedEvent event = (ProjectGeneratedEvent) argument
return request.equals(event.getProjectRequest()) return request.equals(event.projectRequest)
}
}
private static class ProjectFailedEventMatcher extends ArgumentMatcher<ProjectFailedEvent> {
private final ProjectRequest request
private final Exception cause
ProjectFailedEventMatcher(ProjectRequest request, Exception cause) {
this.request = request
this.cause = cause
}
@Override
boolean matches(Object argument) {
ProjectFailedEvent event = (ProjectFailedEvent) argument
return request.equals(event.projectRequest) && cause.equals(event.cause)
} }
} }