, Boolean> singletonMap(Exception.class, true))
+ retryTemplate.setBackOffPolicy(backOffPolicy)
+ retryTemplate.setRetryPolicy(retryPolicy)
+ retryTemplate
+ }
+
+}
diff --git a/initializr/src/main/groovy/io/spring/initializr/config/StatsProperties.groovy b/initializr/src/main/groovy/io/spring/initializr/config/StatsProperties.groovy
new file mode 100644
index 00000000..fb8f2845
--- /dev/null
+++ b/initializr/src/main/groovy/io/spring/initializr/config/StatsProperties.groovy
@@ -0,0 +1,83 @@
+/*
+ * 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.config
+
+import org.springframework.boot.context.properties.ConfigurationProperties
+import org.springframework.util.StringUtils
+
+/**
+ * Statistics-related properties.
+ *
+ * @author Stephane Nicoll
+ * @since 1.0
+ */
+@ConfigurationProperties("initializr.stats")
+class StatsProperties {
+
+ final Elastic elastic = new Elastic()
+
+ static final class Elastic {
+
+ /**
+ * Elastic service uri.
+ */
+ String uri
+
+ /**
+ * Elastic service username.
+ */
+ String username
+
+ /**
+ * Elastic service password
+ */
+ String password
+
+ /**
+ * Name of the index.
+ */
+ String indexName = 'initializr'
+
+ /**
+ * Name of the entity to use to publish stats.
+ */
+ String entityName = 'request'
+
+ /**
+ * Number of attempts before giving up.
+ */
+ int maxAttempts = 3
+
+ void setUri(String uri) {
+ this.uri = cleanUri(uri)
+ }
+
+ URI getEntityUrl() {
+ def string = "$uri/$indexName/$entityName"
+ new URI(string)
+ }
+
+ private static String cleanUri(String contextPath) {
+ if (StringUtils.hasText(contextPath) && contextPath.endsWith("/")) {
+ return contextPath.substring(0, contextPath.length() - 1)
+ }
+ return contextPath
+ }
+
+ }
+
+}
diff --git a/initializr/src/main/groovy/io/spring/initializr/generator/ProjectRequest.groovy b/initializr/src/main/groovy/io/spring/initializr/generator/ProjectRequest.groovy
index 9972734c..ba75efd6 100644
--- a/initializr/src/main/groovy/io/spring/initializr/generator/ProjectRequest.groovy
+++ b/initializr/src/main/groovy/io/spring/initializr/generator/ProjectRequest.groovy
@@ -16,6 +16,7 @@
package io.spring.initializr.generator
+import groovy.transform.ToString
import groovy.util.logging.Slf4j
import io.spring.initializr.metadata.BillOfMaterials
import io.spring.initializr.metadata.Dependency
@@ -33,6 +34,7 @@ import io.spring.initializr.util.VersionRange
* @since 1.0
*/
@Slf4j
+@ToString(ignoreNulls = true, includePackage = false, includeNames = true)
class ProjectRequest extends BasicProjectRequest {
/**
diff --git a/initializr/src/main/groovy/io/spring/initializr/stat/BasicAuthRestTemplate.groovy b/initializr/src/main/groovy/io/spring/initializr/stat/BasicAuthRestTemplate.groovy
new file mode 100644
index 00000000..42dc1f47
--- /dev/null
+++ b/initializr/src/main/groovy/io/spring/initializr/stat/BasicAuthRestTemplate.groovy
@@ -0,0 +1,82 @@
+/*
+ * 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.stat
+
+import java.nio.charset.StandardCharsets
+
+import org.springframework.http.HttpRequest
+import org.springframework.http.client.ClientHttpRequestExecution
+import org.springframework.http.client.ClientHttpRequestInterceptor
+import org.springframework.http.client.ClientHttpResponse
+import org.springframework.http.client.InterceptingClientHttpRequestFactory
+import org.springframework.util.Base64Utils
+import org.springframework.web.client.RestTemplate
+
+/**
+ * A simple {@link RestTemplate} extension that automatically provides the
+ * {@code Authorization} header if credentials are provided.
+ *
+ * Largely inspired from Spring Boot's {@code TestRestTemplate}.
+ *
+ * @author Stephane Nicoll
+ * @since 1.0
+ */
+class BasicAuthRestTemplate extends RestTemplate {
+
+ /**
+ * Create a new instance. {@code username} and {@code password} can be
+ * {@code null} if no authentication is necessary.
+ */
+ BasicAuthRestTemplate(String username, String password) {
+ addAuthentication(username, password)
+ }
+
+ private void addAuthentication(String username, String password) {
+ if (!username) {
+ return;
+ }
+ List interceptors = Collections
+ . singletonList(
+ new BasicAuthorizationInterceptor(username, password))
+ setRequestFactory(new InterceptingClientHttpRequestFactory(getRequestFactory(),
+ interceptors))
+ }
+
+ private static class BasicAuthorizationInterceptor
+ implements ClientHttpRequestInterceptor {
+
+ private final String username
+
+ private final String password
+
+ BasicAuthorizationInterceptor(String username, String password) {
+ this.username = username;
+ this.password = (password == null ? "" : password)
+ }
+
+ @Override
+ public ClientHttpResponse intercept(HttpRequest request, byte[] body,
+ ClientHttpRequestExecution execution) throws IOException {
+ String token = Base64Utils.encodeToString(
+ (this.username + ":" + this.password).getBytes(StandardCharsets.UTF_8))
+ request.getHeaders().add("Authorization", "Basic " + token)
+ return execution.execute(request, body)
+ }
+
+ }
+
+}
diff --git a/initializr/src/main/groovy/io/spring/initializr/stat/ProjectGenerationStatPublisher.groovy b/initializr/src/main/groovy/io/spring/initializr/stat/ProjectGenerationStatPublisher.groovy
new file mode 100644
index 00000000..54c48fe2
--- /dev/null
+++ b/initializr/src/main/groovy/io/spring/initializr/stat/ProjectGenerationStatPublisher.groovy
@@ -0,0 +1,99 @@
+/*
+ * 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.stat
+
+import com.fasterxml.jackson.annotation.JsonInclude
+import com.fasterxml.jackson.databind.ObjectMapper
+import groovy.util.logging.Slf4j
+import io.spring.initializr.config.StatsProperties
+import io.spring.initializr.generator.ProjectRequestEvent
+
+import org.springframework.context.event.EventListener
+import org.springframework.http.MediaType
+import org.springframework.http.RequestEntity
+import org.springframework.retry.RetryCallback
+import org.springframework.retry.RetryContext
+import org.springframework.retry.support.RetryTemplate
+import org.springframework.scheduling.annotation.Async
+import org.springframework.web.client.RestTemplate
+
+/**
+ * Publish stats for each project generated to an Elastic index.
+ *
+ * @author Stephane Nicoll
+ * @since 1.0
+ */
+@Slf4j
+class ProjectGenerationStatPublisher {
+
+ private final ProjectRequestDocumentFactory documentFactory
+ private final StatsProperties statsProperties
+ private final ObjectMapper objectMapper
+ private final RestTemplate restTemplate
+ private final RetryTemplate retryTemplate
+
+ ProjectGenerationStatPublisher(ProjectRequestDocumentFactory documentFactory,
+ StatsProperties statsProperties,
+ RetryTemplate retryTemplate) {
+ this.documentFactory = documentFactory
+ this.statsProperties = statsProperties
+ this.objectMapper = createObjectMapper()
+ this.restTemplate = new BasicAuthRestTemplate(
+ statsProperties.elastic.username, statsProperties.elastic.password)
+ this.retryTemplate = retryTemplate
+ }
+
+ @EventListener
+ @Async
+ void handleEvent(ProjectRequestEvent event) {
+ String json = null
+ try {
+ ProjectRequestDocument document = documentFactory.createDocument(event)
+ if (log.isDebugEnabled()) {
+ log.debug("Publishing $document")
+ }
+ json = toJson(document)
+
+ RequestEntity request = RequestEntity
+ .post(this.statsProperties.elastic.entityUrl)
+ .contentType(MediaType.APPLICATION_JSON)
+ .body(json)
+
+ this.retryTemplate.execute(new RetryCallback() {
+ @Override
+ Void doWithRetry(RetryContext context) {
+ restTemplate.exchange(request, String)
+ return null
+ }
+ })
+ } catch (Exception ex) {
+ log.warn(String.format(
+ "Failed to publish stat to index, document follows %n%n%s%n", json), ex)
+ }
+ }
+
+ private String toJson(ProjectRequestDocument stats) {
+ this.objectMapper.writeValueAsString(stats)
+ }
+
+ private static ObjectMapper createObjectMapper() {
+ def mapper = new ObjectMapper()
+ mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL)
+ mapper
+ }
+
+}
diff --git a/initializr/src/main/groovy/io/spring/initializr/stat/ProjectRequestDocument.groovy b/initializr/src/main/groovy/io/spring/initializr/stat/ProjectRequestDocument.groovy
new file mode 100644
index 00000000..cf252a6a
--- /dev/null
+++ b/initializr/src/main/groovy/io/spring/initializr/stat/ProjectRequestDocument.groovy
@@ -0,0 +1,56 @@
+/*
+ * 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.stat
+
+import groovy.transform.ToString
+
+/**
+ * Define the statistics of a project generation.
+ *
+ * @author Stephane Nicoll
+ * @since 1.0
+ */
+@ToString(ignoreNulls = true, includePackage = false, includeNames = true)
+class ProjectRequestDocument {
+
+ long generationTimestamp
+
+ String requestIp
+ String requestIpv4
+ String requestCountry
+ String clientId
+ String clientVersion
+
+ String groupId
+ String artifactId
+ String packageName
+ String bootVersion
+ String javaVersion
+ String language
+ String packaging
+ String type
+ final List dependencies = []
+
+ String errorMessage
+ boolean invalid
+ boolean invalidJavaVersion
+ boolean invalidLanguage
+ boolean invalidPackaging
+ boolean invalidType
+ final List invalidDependencies = []
+
+}
diff --git a/initializr/src/main/groovy/io/spring/initializr/stat/ProjectRequestDocumentFactory.groovy b/initializr/src/main/groovy/io/spring/initializr/stat/ProjectRequestDocumentFactory.groovy
new file mode 100644
index 00000000..012fc4c5
--- /dev/null
+++ b/initializr/src/main/groovy/io/spring/initializr/stat/ProjectRequestDocumentFactory.groovy
@@ -0,0 +1,149 @@
+/*
+ * 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.stat
+
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+import io.spring.initializr.generator.ProjectFailedEvent
+import io.spring.initializr.generator.ProjectRequest
+import io.spring.initializr.generator.ProjectRequestEvent
+import io.spring.initializr.metadata.InitializrMetadataProvider
+import io.spring.initializr.util.UserAgentWrapper
+
+import static io.spring.initializr.util.UserAgentWrapper.AgentInformation
+
+/**
+ * Create {@link ProjectRequestDocument} instances.
+ *
+ * @author Stephane Nicoll
+ * @since 1.0
+ */
+class ProjectRequestDocumentFactory {
+
+ private static final IP_PATTERN = Pattern.compile("[0-9]*\\.[0-9]*\\.[0-9]*\\.[0-9]*")
+
+ private final InitializrMetadataProvider metadataProvider
+
+ ProjectRequestDocumentFactory(InitializrMetadataProvider metadataProvider) {
+ this.metadataProvider = metadataProvider
+ }
+
+ ProjectRequestDocument createDocument(ProjectRequestEvent event) {
+ def metadata = metadataProvider.get()
+ def request = event.projectRequest
+
+ ProjectRequestDocument document = new ProjectRequestDocument()
+ document.generationTimestamp = event.timestamp
+
+ handleCloudFlareHeaders(request, document)
+ def candidate = request.parameters['x-forwarded-for']
+ if (!document.requestIp && candidate) {
+ document.requestIp = candidate
+ document.requestIpv4 = extractIpv4(candidate)
+ }
+
+ AgentInformation agentInfo = extractAgentInformation(request)
+ if (agentInfo) {
+ document.clientId = agentInfo.id.id
+ document.clientVersion = agentInfo.version
+ }
+
+ document.groupId = request.groupId
+ document.artifactId = request.artifactId
+ document.packageName = request.packageName
+ document.bootVersion = request.bootVersion
+
+ document.javaVersion = request.javaVersion
+ if (request.javaVersion && !metadata.javaVersions.get(request.javaVersion)) {
+ document.invalid = true
+ document.invalidJavaVersion = true
+ }
+
+ document.language = request.language
+ if (request.language && !metadata.languages.get(request.language)) {
+ document.invalid = true
+ document.invalidLanguage = true
+ }
+
+ document.packaging = request.packaging
+ if (request.packaging && !metadata.packagings.get(request.packaging)) {
+ document.invalid = true
+ document.invalidPackaging = true
+ }
+
+ document.type = request.type
+ if (request.type && !metadata.types.get(request.type)) {
+ document.invalid = true
+ document.invalidType = true
+ }
+
+ // Let's not rely on the resolved dependencies here
+ def dependencies = []
+ dependencies.addAll(request.style)
+ dependencies.addAll(request.dependencies)
+ dependencies.each { id ->
+ if (metadata.dependencies.get(id)) {
+ document.dependencies << id
+ } else {
+ document.invalid = true
+ document.invalidDependencies << id
+ }
+ }
+
+ // Let's make sure that the document is flagged as invalid no matter what
+ if (event instanceof ProjectFailedEvent) {
+ document.invalid = true
+ if (event.cause) {
+ document.errorMessage = event.cause.message
+ }
+ }
+
+ document
+ }
+
+ private static void handleCloudFlareHeaders(ProjectRequest request, ProjectRequestDocument document) {
+ def candidate = request.parameters['cf-connecting-ip']
+ if (candidate) {
+ document.requestIp = candidate
+ document.requestIpv4 = extractIpv4(candidate)
+ }
+ String country = request.parameters['cf-ipcountry']
+ if (country && !country.toLowerCase().equals('xx')) {
+ document.requestCountry = country
+ }
+ }
+
+ private static AgentInformation extractAgentInformation(ProjectRequest request) {
+ String userAgent = request.parameters['user-agent']
+ if (userAgent) {
+ return new UserAgentWrapper(userAgent).extractAgentInformation()
+ }
+ return null
+ }
+
+ private static String extractIpv4(def candidate) {
+ if (candidate) {
+ Matcher matcher = IP_PATTERN.matcher(candidate)
+ if (matcher.find()) {
+ return matcher.group()
+ }
+ }
+ return null
+ }
+
+}
diff --git a/initializr/src/main/groovy/io/spring/initializr/web/MainController.groovy b/initializr/src/main/groovy/io/spring/initializr/web/MainController.groovy
index 137bea8f..6ec0a524 100644
--- a/initializr/src/main/groovy/io/spring/initializr/web/MainController.groovy
+++ b/initializr/src/main/groovy/io/spring/initializr/web/MainController.groovy
@@ -145,7 +145,7 @@ class MainController extends AbstractInitializrController {
}
private static InitializrMetadataJsonMapper getJsonMapper(InitializrMetadataVersion version) {
- switch(version) {
+ switch (version) {
case InitializrMetadataVersion.V2: return new InitializrMetadataV2JsonMapper();
default: return new InitializrMetadataV21JsonMapper();
}
diff --git a/initializr/src/main/resources/META-INF/spring-configuration-metadata.json b/initializr/src/main/resources/META-INF/spring-configuration-metadata.json
index 73cf097b..c4a3e505 100644
--- a/initializr/src/main/resources/META-INF/spring-configuration-metadata.json
+++ b/initializr/src/main/resources/META-INF/spring-configuration-metadata.json
@@ -41,6 +41,17 @@
"sourceType": "io.spring.initializr.metadata.InitializrProperties",
"sourceMethod": "getPackageName()"
},
+ {
+ "name": "initializr.stats",
+ "type": "io.spring.initializr.config.StatsProperties",
+ "sourceType": "io.spring.initializr.config.StatsProperties"
+ },
+ {
+ "name": "initializr.stats.elastic",
+ "type": "io.spring.initializr.config.StatsProperties$Elastic",
+ "sourceType": "io.spring.initializr.config.StatsProperties",
+ "sourceMethod": "getElastic()"
+ },
{
"name": "initializr.version",
"type": "io.spring.initializr.metadata.InitializrProperties$SimpleElement",
@@ -234,6 +245,43 @@
"description": "Available packaging types.",
"sourceType": "io.spring.initializr.metadata.InitializrProperties"
},
+ {
+ "name": "initializr.stats.elastic.uri",
+ "type": "java.lang.String",
+ "description": "Elastic service uri.",
+ "sourceType": "io.spring.initializr.config.StatsProperties$Elastic"
+ },
+ {
+ "name": "initializr.stats.elastic.username",
+ "type": "java.lang.String",
+ "description": "Elastic service username.",
+ "sourceType": "io.spring.initializr.config.StatsProperties$Elastic"
+ },
+ {
+ "name": "initializr.stats.elastic.password",
+ "type": "java.lang.String",
+ "description": "Elastic service password.",
+ "sourceType": "io.spring.initializr.config.StatsProperties$Elastic"
+ },
+ {
+ "name": "initializr.stats.elastic.index-name",
+ "type": "java.lang.String",
+ "description": "Name of the index.",
+ "sourceType": "io.spring.initializr.config.StatsProperties$Elastic"
+ },
+ {
+ "name": "initializr.stats.elastic.entity-name",
+ "type": "java.lang.String",
+ "description": "Name of the entity to use to publish stats.",
+ "sourceType": "io.spring.initializr.config.StatsProperties$Elastic"
+ },
+ {
+ "name": "initializr.stats.elastic.max-attempts",
+ "type": "java.lang.Integer",
+ "description": "Number of attempts before giving up.",
+ "defaultValue": 3,
+ "sourceType": "io.spring.initializr.config.StatsProperties$Elastic"
+ },
{
"name": "initializr.types",
"type": "java.util.List",
diff --git a/initializr/src/main/resources/META-INF/spring.factories b/initializr/src/main/resources/META-INF/spring.factories
index 6c786bad..7339ecf6 100644
--- a/initializr/src/main/resources/META-INF/spring.factories
+++ b/initializr/src/main/resources/META-INF/spring.factories
@@ -1,3 +1,7 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
io.spring.initializr.config.InitializrAutoConfiguration,\
-io.spring.initializr.config.InitializrMetricsExporterAutoConfiguration
\ No newline at end of file
+io.spring.initializr.config.InitializrMetricsExporterAutoConfiguration,\
+io.spring.initializr.config.InitializrStatsAutoConfiguration
+
+org.springframework.boot.env.EnvironmentPostProcessor=\
+io.spring.initializr.config.CloudfoundryEnvironmentPostProcessor
diff --git a/initializr/src/test/groovy/io/spring/initializr/config/CloudfoundryEnvironmentPostProcessorTests.groovy b/initializr/src/test/groovy/io/spring/initializr/config/CloudfoundryEnvironmentPostProcessorTests.groovy
new file mode 100644
index 00000000..ddcd8058
--- /dev/null
+++ b/initializr/src/test/groovy/io/spring/initializr/config/CloudfoundryEnvironmentPostProcessorTests.groovy
@@ -0,0 +1,70 @@
+/*
+ * 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.config
+
+import org.junit.Test
+
+import org.springframework.boot.SpringApplication
+import org.springframework.mock.env.MockEnvironment
+
+import static org.hamcrest.CoreMatchers.nullValue
+import static org.junit.Assert.assertThat
+import static org.hamcrest.CoreMatchers.is
+
+/**
+ * @author Stephane Nicoll
+ */
+class CloudfoundryEnvironmentPostProcessorTests {
+
+ private final CloudfoundryEnvironmentPostProcessor postProcessor = new CloudfoundryEnvironmentPostProcessor()
+ private final MockEnvironment environment = new MockEnvironment();
+ private final SpringApplication application = new SpringApplication()
+
+ @Test
+ void parseCredentials() {
+ environment.setProperty('vcap.services.stats-index.credentials.uri',
+ 'http://user:pass@example.com/bar/biz?param=one')
+ postProcessor.postProcessEnvironment(environment, application)
+
+ assertThat(environment.getProperty('initializr.stats.elastic.uri'),
+ is('http://example.com/bar/biz?param=one'))
+ assertThat(environment.getProperty('initializr.stats.elastic.username'), is('user'))
+ assertThat(environment.getProperty('initializr.stats.elastic.password'), is('pass'))
+ }
+
+ @Test
+ void parseNoCredentials() {
+ environment.setProperty('vcap.services.stats-index.credentials.uri',
+ 'http://example.com/bar/biz?param=one')
+ postProcessor.postProcessEnvironment(environment, application)
+
+ assertThat(environment.getProperty('initializr.stats.elastic.uri'),
+ is('http://example.com/bar/biz?param=one'))
+ assertThat(environment.getProperty('initializr.stats.elastic.username'), is(nullValue()))
+ assertThat(environment.getProperty('initializr.stats.elastic.password'), is(nullValue()))
+ }
+
+ @Test
+ void parseNoVcapUri() {
+ postProcessor.postProcessEnvironment(environment, application)
+
+ assertThat(environment.getProperty('initializr.stats.elastic.uri'), is(nullValue()))
+ assertThat(environment.getProperty('initializr.stats.elastic.username'), is(nullValue()))
+ assertThat(environment.getProperty('initializr.stats.elastic.password'), is(nullValue()))
+ }
+
+}
diff --git a/initializr/src/test/groovy/io/spring/initializr/config/StatsPropertiesTests.groovy b/initializr/src/test/groovy/io/spring/initializr/config/StatsPropertiesTests.groovy
new file mode 100644
index 00000000..b2aff988
--- /dev/null
+++ b/initializr/src/test/groovy/io/spring/initializr/config/StatsPropertiesTests.groovy
@@ -0,0 +1,46 @@
+/*
+ * 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.config
+
+import org.junit.Test
+
+import static org.junit.Assert.assertThat
+import static org.hamcrest.CoreMatchers.is
+
+/**
+ * @author Stephane Nicoll
+ */
+class StatsPropertiesTests {
+
+ private final StatsProperties properties = new StatsProperties()
+
+ @Test
+ void cleanTrailingSlash() {
+ properties.elastic.uri = 'http://example.com/'
+ assertThat(properties.elastic.uri, is('http://example.com'))
+ }
+
+ @Test
+ void provideEntityUrl() {
+ properties.elastic.uri = 'http://example.com/'
+ properties.elastic.indexName = 'my-index'
+ properties.elastic.entityName = 'foo'
+ assertThat(properties.elastic.entityUrl.toString(),
+ is('http://example.com/my-index/foo'))
+ }
+
+}
diff --git a/initializr/src/test/groovy/io/spring/initializr/stat/AbstractInitializrStatTests.groovy b/initializr/src/test/groovy/io/spring/initializr/stat/AbstractInitializrStatTests.groovy
new file mode 100644
index 00000000..afe21d98
--- /dev/null
+++ b/initializr/src/test/groovy/io/spring/initializr/stat/AbstractInitializrStatTests.groovy
@@ -0,0 +1,52 @@
+/*
+ * 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.stat
+
+import io.spring.initializr.generator.ProjectRequest
+import io.spring.initializr.metadata.InitializrMetadata
+import io.spring.initializr.metadata.InitializrMetadataProvider
+import io.spring.initializr.test.InitializrMetadataTestBuilder
+
+/**
+ * @author Stephane Nicoll
+ */
+abstract class AbstractInitializrStatTests {
+
+ def metadata = InitializrMetadataTestBuilder
+ .withDefaults()
+ .addDependencyGroup('core', 'security', 'validation', 'aop')
+ .addDependencyGroup('web', 'web', 'data-rest', 'jersey')
+ .addDependencyGroup('data', 'data-jpa', 'jdbc')
+ .addDependencyGroup('database', 'h2', 'mysql')
+ .build()
+
+ protected InitializrMetadataProvider createProvider(def metadata) {
+ new InitializrMetadataProvider() {
+ @Override
+ InitializrMetadata get() {
+ return metadata
+ }
+ }
+ }
+
+ protected ProjectRequest createProjectRequest() {
+ ProjectRequest request = new ProjectRequest()
+ request.initialize(metadata)
+ request
+ }
+
+}
diff --git a/initializr/src/test/groovy/io/spring/initializr/stat/ProjectGenerationStatPublisherTests.groovy b/initializr/src/test/groovy/io/spring/initializr/stat/ProjectGenerationStatPublisherTests.groovy
new file mode 100644
index 00000000..0b886fa8
--- /dev/null
+++ b/initializr/src/test/groovy/io/spring/initializr/stat/ProjectGenerationStatPublisherTests.groovy
@@ -0,0 +1,130 @@
+/*
+ * 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.stat
+
+import io.spring.initializr.config.StatsProperties
+import io.spring.initializr.generator.ProjectGeneratedEvent
+import io.spring.initializr.generator.ProjectRequest
+import org.junit.Before
+import org.junit.Test
+
+import org.springframework.http.HttpMethod
+import org.springframework.http.HttpStatus
+import org.springframework.http.MediaType
+import org.springframework.retry.policy.SimpleRetryPolicy
+import org.springframework.retry.support.RetryTemplate
+import org.springframework.test.web.client.MockRestServiceServer
+
+import static org.springframework.test.web.client.match.MockRestRequestMatchers.jsonPath
+import static org.springframework.test.web.client.match.MockRestRequestMatchers.method
+import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo
+import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
+
+/**
+ * @author Stephane Nicoll
+ */
+class ProjectGenerationStatPublisherTests extends AbstractInitializrStatTests {
+
+ private StatsProperties properties
+ private RetryTemplate retryTemplate
+ private ProjectGenerationStatPublisher statPublisher
+ private MockRestServiceServer mockServer
+
+
+ @Before
+ public void setUp() {
+ this.properties = createProperties()
+ ProjectRequestDocumentFactory documentFactory =
+ new ProjectRequestDocumentFactory(createProvider(metadata))
+ this.retryTemplate = new RetryTemplate()
+ this.statPublisher = new ProjectGenerationStatPublisher(documentFactory, properties, retryTemplate)
+ mockServer = MockRestServiceServer.createServer(this.statPublisher.restTemplate);
+ }
+
+ @Test
+ public void publishSimpleDocument() {
+ ProjectRequest request = createProjectRequest()
+ request.groupId = 'com.example.foo'
+ request.artifactId = 'my-project'
+
+ mockServer.expect(requestTo('http://example.com/elastic/initializr/request'))
+ .andExpect(method(HttpMethod.POST))
+ .andExpect(jsonPath('$.groupId').value('com.example.foo'))
+ .andExpect(jsonPath('$.artifactId').value('my-project'))
+ .andRespond(withStatus(HttpStatus.CREATED)
+ .body(mockResponse(UUID.randomUUID().toString(), true))
+ .contentType(MediaType.APPLICATION_JSON)
+ )
+
+ this.statPublisher.handleEvent(new ProjectGeneratedEvent(request))
+ mockServer.verify()
+ }
+
+ @Test
+ public void recoverFromError() {
+ ProjectRequest request = createProjectRequest()
+
+ mockServer.expect(requestTo('http://example.com/elastic/initializr/request'))
+ .andExpect(method(HttpMethod.POST))
+ .andRespond(withStatus(HttpStatus.INTERNAL_SERVER_ERROR))
+
+ mockServer.expect(requestTo('http://example.com/elastic/initializr/request'))
+ .andExpect(method(HttpMethod.POST))
+ .andRespond(withStatus(HttpStatus.INTERNAL_SERVER_ERROR))
+
+ mockServer.expect(requestTo('http://example.com/elastic/initializr/request'))
+ .andExpect(method(HttpMethod.POST))
+ .andRespond(withStatus(HttpStatus.CREATED)
+ .body(mockResponse(UUID.randomUUID().toString(), true))
+ .contentType(MediaType.APPLICATION_JSON))
+
+ this.statPublisher.handleEvent(new ProjectGeneratedEvent(request))
+ mockServer.verify()
+ }
+
+ @Test
+ public void fatalErrorOnlyLogs() {
+ ProjectRequest request = createProjectRequest()
+ this.retryTemplate.setRetryPolicy(new SimpleRetryPolicy(2,
+ Collections., Boolean> singletonMap(Exception.class, true)))
+
+ mockServer.expect(requestTo('http://example.com/elastic/initializr/request'))
+ .andExpect(method(HttpMethod.POST))
+ .andRespond(withStatus(HttpStatus.INTERNAL_SERVER_ERROR))
+
+ mockServer.expect(requestTo('http://example.com/elastic/initializr/request'))
+ .andExpect(method(HttpMethod.POST))
+ .andRespond(withStatus(HttpStatus.INTERNAL_SERVER_ERROR))
+
+ this.statPublisher.handleEvent(new ProjectGeneratedEvent(request))
+ mockServer.verify()
+ }
+
+ private static String mockResponse(String id, boolean created) {
+ '{"_index":"initializr","_type":"request","_id":"' + id + '","_version":1,"_shards"' +
+ ':{"total":1,"successful":1,"failed":0},"created":' + created + '}'
+ }
+
+ private static StatsProperties createProperties() {
+ def properties = new StatsProperties()
+ properties.elastic.uri = 'http://example.com/elastic'
+ properties.elastic.username = 'foo'
+ properties.elastic.password = 'bar'
+ properties
+ }
+
+}
diff --git a/initializr/src/test/groovy/io/spring/initializr/stat/ProjectRequestDocumentFactoryTests.groovy b/initializr/src/test/groovy/io/spring/initializr/stat/ProjectRequestDocumentFactoryTests.groovy
new file mode 100644
index 00000000..ccf631c0
--- /dev/null
+++ b/initializr/src/test/groovy/io/spring/initializr/stat/ProjectRequestDocumentFactoryTests.groovy
@@ -0,0 +1,219 @@
+/*
+ * 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.stat
+
+import io.spring.initializr.generator.ProjectFailedEvent
+import io.spring.initializr.generator.ProjectGeneratedEvent
+import io.spring.initializr.generator.ProjectRequest
+import org.junit.Test
+
+import static org.junit.Assert.assertEquals
+import static org.junit.Assert.assertFalse
+import static org.junit.Assert.assertNull
+import static org.junit.Assert.assertTrue
+
+/**
+ *
+ * @author Stephane Nicoll
+ */
+class ProjectRequestDocumentFactoryTests extends AbstractInitializrStatTests {
+
+ private final ProjectRequestDocumentFactory factory =
+ new ProjectRequestDocumentFactory(createProvider(metadata))
+
+ @Test
+ void createDocumentForSimpleProject() {
+ ProjectRequest request = createProjectRequest()
+ def event = new ProjectGeneratedEvent(request)
+ def document = factory.createDocument(event)
+ assertEquals event.timestamp, document.generationTimestamp
+ assertEquals null, document.requestIp
+ assertEquals 'com.example', document.groupId
+ assertEquals 'demo', document.artifactId
+ assertEquals 'com.example', document.packageName
+ assertEquals '1.2.3.RELEASE', document.bootVersion
+ assertEquals '1.8', document.javaVersion
+ assertEquals 'java', document.language
+ assertEquals 'jar', document.packaging
+ assertEquals 'maven-project', document.type
+ assertEquals 0, document.dependencies.size()
+ assertValid document
+ }
+
+ @Test
+ void createDocumentWithRequestIp() {
+ ProjectRequest request = createProjectRequest()
+ request.parameters['x-forwarded-for'] = '10.0.0.123'
+ def event = new ProjectGeneratedEvent(request)
+ def document = factory.createDocument(event)
+ assertEquals '10.0.0.123', document.requestIp
+ assertEquals '10.0.0.123', document.requestIpv4
+ assertNull document.requestCountry
+ }
+
+ @Test
+ void createDocumentWithRequestIpv6() {
+ ProjectRequest request = createProjectRequest()
+ request.parameters['x-forwarded-for'] = '2001:db8:a0b:12f0::1'
+ def event = new ProjectGeneratedEvent(request)
+ def document = factory.createDocument(event)
+ assertEquals '2001:db8:a0b:12f0::1', document.requestIp
+ assertNull document.requestIpv4
+ assertNull document.requestCountry
+ }
+
+ @Test
+ void createDocumentWithCloudFlareHeaders() {
+ ProjectRequest request = createProjectRequest()
+ request.parameters['cf-connecting-ip'] = '10.0.0.123'
+ request.parameters['cf-ipcountry'] = 'BE'
+ def event = new ProjectGeneratedEvent(request)
+ def document = factory.createDocument(event)
+ assertEquals '10.0.0.123', document.requestIp
+ assertEquals '10.0.0.123', document.requestIpv4
+ assertEquals 'BE', document.requestCountry
+ }
+
+ @Test
+ void createDocumentWithCloudFlareIpv6() {
+ ProjectRequest request = createProjectRequest()
+ request.parameters['cf-connecting-ip'] = '2001:db8:a0b:12f0::1'
+ def event = new ProjectGeneratedEvent(request)
+ def document = factory.createDocument(event)
+ assertEquals '2001:db8:a0b:12f0::1', document.requestIp
+ assertNull document.requestIpv4
+ assertNull document.requestCountry
+ }
+
+ @Test
+ void createDocumentWithCloudFlareHeadersAndOtherHeaders() {
+ ProjectRequest request = createProjectRequest()
+ request.parameters['cf-connecting-ip'] = '10.0.0.123'
+ request.parameters['x-forwarded-for'] = '192.168.1.101'
+ def event = new ProjectGeneratedEvent(request)
+ def document = factory.createDocument(event)
+ assertEquals '10.0.0.123', document.requestIp
+ assertEquals '10.0.0.123', document.requestIpv4
+ assertNull document.requestCountry
+ }
+
+ @Test
+ void createDocumentWithCloudFlareCountrySetToXX() {
+ ProjectRequest request = createProjectRequest()
+ request.parameters['cf-connecting-ip'] = 'Xx' // case insensitive
+ def event = new ProjectGeneratedEvent(request)
+ def document = factory.createDocument(event)
+ assertNull document.requestCountry
+ }
+
+ @Test
+ void createDocumentWithUserAgent() {
+ ProjectRequest request = createProjectRequest()
+ request.parameters['user-agent'] = 'HTTPie/0.8.0'
+ def event = new ProjectGeneratedEvent(request)
+ def document = factory.createDocument(event)
+ assertEquals 'httpie', document.clientId
+ assertEquals '0.8.0', document.clientVersion
+ }
+
+ @Test
+ void createDocumentWithUserAgentNoVersion() {
+ ProjectRequest request = createProjectRequest()
+ request.parameters['user-agent'] = 'IntelliJ IDEA'
+ def event = new ProjectGeneratedEvent(request)
+ def document = factory.createDocument(event)
+ assertEquals 'intellijidea', document.clientId
+ assertEquals null, document.clientVersion
+ }
+
+ @Test
+ void createDocumentInvalidJavaVersion() {
+ ProjectRequest request = createProjectRequest()
+ request.javaVersion = '1.2'
+ def event = new ProjectGeneratedEvent(request)
+ def document = factory.createDocument(event)
+ assertEquals '1.2', document.javaVersion
+ assertTrue document.invalid
+ assertTrue document.invalidJavaVersion
+ }
+
+ @Test
+ void createDocumentInvalidLanguage() {
+ ProjectRequest request = createProjectRequest()
+ request.language = 'c++'
+ def event = new ProjectGeneratedEvent(request)
+ def document = factory.createDocument(event)
+ assertEquals 'c++', document.language
+ assertTrue document.invalid
+ assertTrue document.invalidLanguage
+ }
+
+ @Test
+ void createDocumentInvalidPackaging() {
+ ProjectRequest request = createProjectRequest()
+ request.packaging = 'ear'
+ def event = new ProjectGeneratedEvent(request)
+ def document = factory.createDocument(event)
+ assertEquals 'ear', document.packaging
+ assertTrue document.invalid
+ assertTrue document.invalidPackaging
+ }
+
+ @Test
+ void createDocumentInvalidType() {
+ ProjectRequest request = createProjectRequest()
+ request.type = 'ant-project'
+ def event = new ProjectGeneratedEvent(request)
+ def document = factory.createDocument(event)
+ assertEquals 'ant-project', document.type
+ assertTrue document.invalid
+ assertTrue document.invalidType
+ }
+
+ @Test
+ void createDocumentInvalidDependency() {
+ ProjectRequest request = createProjectRequest()
+ request.dependencies << 'web' << 'invalid' << 'data-jpa' << 'invalid-2'
+ def event = new ProjectGeneratedEvent(request)
+ def document = factory.createDocument(event)
+ assertEquals 'web', document.dependencies[0]
+ assertEquals 'data-jpa', document.dependencies[1]
+ assertEquals 2, document.dependencies.size()
+ assertTrue document.invalid
+ assertEquals 'invalid', document.invalidDependencies[0]
+ assertEquals 'invalid-2', document.invalidDependencies[1]
+ assertEquals 2, document.invalidDependencies.size()
+ }
+
+ @Test
+ void createDocumentWithProjectFailedEvent() {
+ ProjectRequest request = createProjectRequest()
+ def event = new ProjectFailedEvent(request, new IllegalStateException('my test message'))
+ def document = factory.createDocument(event)
+ assertTrue document.invalid
+ assertEquals 'my test message', document.errorMessage
+ }
+
+ private static void assertValid(ProjectRequestDocument document) {
+ assertFalse document.invalid
+ assertFalse document.invalidJavaVersion
+ assertFalse document.invalidLanguage
+ assertFalse document.invalidPackaging
+ assertEquals 0, document.invalidDependencies.size()
+ }
+
+}
diff --git a/initializr/src/test/groovy/io/spring/initializr/web/MainControllerStatsIntegrationTests.groovy b/initializr/src/test/groovy/io/spring/initializr/web/MainControllerStatsIntegrationTests.groovy
new file mode 100644
index 00000000..fc13b382
--- /dev/null
+++ b/initializr/src/test/groovy/io/spring/initializr/web/MainControllerStatsIntegrationTests.groovy
@@ -0,0 +1,195 @@
+/*
+ * 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.web
+
+import groovy.json.JsonSlurper
+import io.spring.initializr.config.StatsProperties
+import org.junit.Before
+import org.junit.Test
+
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.SpringApplicationConfiguration
+import org.springframework.http.HttpHeaders
+import org.springframework.http.HttpStatus
+import org.springframework.http.RequestEntity
+import org.springframework.test.context.ActiveProfiles
+import org.springframework.util.Base64Utils
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RequestMethod
+import org.springframework.web.bind.annotation.RestController
+import org.springframework.web.client.HttpClientErrorException
+
+import static org.junit.Assert.assertEquals
+import static org.junit.Assert.assertNotNull
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse
+import static org.junit.Assert.fail;
+
+/**
+ * @author Stephane Nicoll
+ */
+@SpringApplicationConfiguration(StatsMockController.class)
+@ActiveProfiles(['test-default', 'test-custom-stats'])
+class MainControllerStatsIntegrationTests extends AbstractInitializrControllerIntegrationTests {
+
+ @Autowired
+ private StatsMockController statsMockController
+
+ @Autowired
+ private StatsProperties statsProperties
+
+ private final JsonSlurper slurper = new JsonSlurper()
+
+ @Before
+ public void setup() {
+ this.statsMockController.stats.clear()
+ // Make sure our mock is going to be invoked with the stats
+ this.statsProperties.elastic.uri = "http://localhost:$port/elastic"
+ }
+
+ @Test
+ void simpleProject() {
+ downloadArchive('/starter.zip?groupId=com.foo&artifactId=bar&dependencies=web')
+ assertEquals 'No stat got generated', 1, statsMockController.stats.size()
+ def content = statsMockController.stats[0]
+
+ def json = slurper.parseText(content.json)
+ assertEquals 'com.foo', json.groupId
+ assertEquals 'bar', json.artifactId
+ assertEquals 1, json.dependencies.size()
+ assertEquals 'web', json.dependencies[0]
+ }
+
+ @Test
+ void authorizationHeaderIsSet() {
+ downloadArchive('/starter.zip')
+ assertEquals 'No stat got generated', 1, statsMockController.stats.size()
+ def content = statsMockController.stats[0]
+
+ def authorization = content.authorization
+ assertNotNull 'Authorization header must be set', authorization
+ assertTrue 'Wrong value for authorization header', authorization.startsWith('Basic ')
+ def token = authorization.substring('Basic '.length(), authorization.size())
+ def data = new String(Base64Utils.decodeFromString(token)).split(':')
+ assertEquals "Wrong user from $token", 'test-user', data[0]
+ assertEquals "Wrong password $token", 'test-password', data[1]
+ }
+
+ @Test
+ void requestIpNotSetByDefault() {
+ downloadArchive('/starter.zip?groupId=com.foo&artifactId=bar&dependencies=web')
+ assertEquals 'No stat got generated', 1, statsMockController.stats.size()
+ def content = statsMockController.stats[0]
+
+ def json = slurper.parseText(content.json)
+ assertFalse 'requestIp property should not be set', json.containsKey('requestIp')
+ }
+
+ @Test
+ void requestIpIsSetWhenHeaderIsPresent() {
+ RequestEntity> request = RequestEntity.get(new URI(createUrl('/starter.zip')))
+ .header('X-FORWARDED-FOR', '10.0.0.123').build()
+ restTemplate.exchange(request, String)
+ assertEquals 'No stat got generated', 1, statsMockController.stats.size()
+ def content = statsMockController.stats[0]
+
+ def json = slurper.parseText(content.json)
+ assertEquals 'Wrong requestIp', '10.0.0.123', json.requestIp
+ }
+
+ @Test
+ void requestIpv4IsNotSetWhenHeaderHasGarbage() {
+ RequestEntity> request = RequestEntity.get(new URI(createUrl('/starter.zip')))
+ .header('x-forwarded-for', 'foo-bar').build()
+ restTemplate.exchange(request, String)
+ assertEquals 'No stat got generated', 1, statsMockController.stats.size()
+ def content = statsMockController.stats[0]
+
+ def json = slurper.parseText(content.json)
+ assertFalse 'requestIpv4 property should not be set if value is not a valid IPv4',
+ json.containsKey('requestIpv4')
+ }
+
+ @Test
+ void requestCountryIsNotSetWhenHeaderIsSetToXX() {
+ RequestEntity> request = RequestEntity.get(new URI(createUrl('/starter.zip')))
+ .header('cf-ipcountry', 'XX').build()
+ restTemplate.exchange(request, String)
+ assertEquals 'No stat got generated', 1, statsMockController.stats.size()
+ def content = statsMockController.stats[0]
+
+ def json = slurper.parseText(content.json)
+ assertFalse 'requestCountry property should not be set if value is set to xx',
+ json.containsKey('requestCountry')
+ }
+
+ @Test
+ void invalidProjectSillHasStats() {
+ try {
+ downloadArchive('/starter.zip?type=invalid-type')
+ fail("Should have failed to generate project with invalid type")
+ } catch (HttpClientErrorException ex) {
+ assertEquals HttpStatus.BAD_REQUEST, ex.statusCode
+ }
+ assertEquals 'No stat got generated', 1, statsMockController.stats.size()
+ def content = statsMockController.stats[0]
+
+ def json = slurper.parseText(content.json)
+ assertEquals 'com.example', json.groupId
+ assertEquals 'demo', json.artifactId
+ assertEquals true, json.invalid
+ assertEquals true, json.invalidType
+ assertNotNull json.errorMessage
+ assertTrue json.errorMessage.contains('invalid-type')
+ }
+
+ @Test
+ void errorPublishingStatsDoesNotBubbleUp() {
+ this.statsProperties.elastic.uri = "http://localhost:$port/elastic-error"
+ downloadArchive('/starter.zip')
+ assertEquals 'No stat should be available', 0, statsMockController.stats.size()
+ }
+
+
+ @RestController
+ static class StatsMockController {
+
+ private final List stats = []
+
+ @RequestMapping(path = '/elastic/test/my-entity', method = RequestMethod.POST)
+ void handleProjectRequestDocument(RequestEntity input) {
+ def authorization = input.headers.getFirst(HttpHeaders.AUTHORIZATION)
+ def content = new Content(authorization: authorization, json: input.body)
+ this.stats << content
+ }
+
+ @RequestMapping(path = '/elastic-error/test/my-entity', method = RequestMethod.POST)
+ void handleExpectedError() {
+ throw new IllegalStateException('Expected exception')
+ }
+
+ static class Content {
+
+ String authorization
+
+ String json
+
+ }
+
+ }
+
+}
diff --git a/initializr/src/test/resources/application-test-custom-stats.yml b/initializr/src/test/resources/application-test-custom-stats.yml
new file mode 100644
index 00000000..8868dc38
--- /dev/null
+++ b/initializr/src/test/resources/application-test-custom-stats.yml
@@ -0,0 +1,9 @@
+initializr:
+ stats:
+ elastic:
+ uri: http://localhost:${server.port}/elastic
+ indexName: test
+ entityName: my-entity
+ username: test-user
+ password: test-password
+ max-attempts: 1
\ No newline at end of file