mirror of
https://gitee.com/dcren/initializr.git
synced 2025-11-08 02:14:55 +08:00
Publish stats on project generation
If an elastic instance is available, publish a document to a configurable index every time a `ProjectRequest` is handled by the service. In practice, this means that every attempt to generate a project leads to a new document in the index. The document gathers the settings of the required project, including invalid ones if any. If an exception is thrown, the message of the cause is made available. CloudFlare is explicitely supported and the IP and country of the request is added to the document. If that information is not available and the request contains a `X-Forwarded-For` header, the value is also associated with the document. If an IPv4 is detected, it is set in a separate `requestIpv4` property. If for some reason the document could not be indexed, we attempt to retry a configurable amount of times. Closes gh-185
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
package app
|
||||
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.scheduling.annotation.AsyncConfigurerSupport
|
||||
import org.springframework.scheduling.annotation.EnableAsync
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
|
||||
|
||||
import io.spring.initializr.web.LegacyStsController
|
||||
|
||||
@Grab('io.spring.initalizr:initializr:1.0.0.BUILD-SNAPSHOT')
|
||||
@@ -11,4 +18,20 @@ class InitializerService {
|
||||
LegacyStsController legacyStsController() {
|
||||
new LegacyStsController()
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableAsync
|
||||
static class AsyncConfiguration extends AsyncConfigurerSupport {
|
||||
|
||||
@Override
|
||||
Executor getAsyncExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor()
|
||||
executor.setCorePoolSize(1)
|
||||
executor.setMaxPoolSize(5)
|
||||
executor.setThreadNamePrefix("initializr-")
|
||||
executor.initialize()
|
||||
executor
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,11 @@
|
||||
<artifactId>spring-boot-starter-redis</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.retry</groupId>
|
||||
<artifactId>spring-retry</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-core</artifactId>
|
||||
@@ -85,6 +90,10 @@
|
||||
<artifactId>jsonassert</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.jayway.jsonpath</groupId>
|
||||
<artifactId>json-path</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.gebish</groupId>
|
||||
<artifactId>geb-core</artifactId>
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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.SpringApplication
|
||||
import org.springframework.boot.context.config.ConfigFileApplicationListener
|
||||
import org.springframework.boot.env.EnvironmentPostProcessor
|
||||
import org.springframework.core.Ordered
|
||||
import org.springframework.core.env.ConfigurableEnvironment
|
||||
import org.springframework.core.env.MapPropertySource
|
||||
import org.springframework.core.env.MutablePropertySources
|
||||
import org.springframework.core.env.PropertySource
|
||||
import org.springframework.util.StringUtils
|
||||
import org.springframework.web.util.UriComponents
|
||||
import org.springframework.web.util.UriComponentsBuilder
|
||||
|
||||
/**
|
||||
* Post-process the environment to extract the service credentials provided by
|
||||
* CloudFoundry. Injects the elastic service URI if present.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 1.0
|
||||
*/
|
||||
class CloudfoundryEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {
|
||||
|
||||
private static final String PROPERTY_SOURCE_NAME = "defaultProperties";
|
||||
|
||||
private final int order = ConfigFileApplicationListener.DEFAULT_ORDER + 1;
|
||||
|
||||
@Override
|
||||
public void postProcessEnvironment(ConfigurableEnvironment environment,
|
||||
SpringApplication springApplication) {
|
||||
|
||||
Map<String,Object> map = [:]
|
||||
String uri = environment.getProperty('vcap.services.stats-index.credentials.uri')
|
||||
if (StringUtils.hasText(uri)) {
|
||||
UriComponents uriComponents = UriComponentsBuilder.fromUriString(uri).build()
|
||||
def userInfo = uriComponents.getUserInfo()
|
||||
if (userInfo) {
|
||||
String[] credentials = userInfo.split(':')
|
||||
map['initializr.stats.elastic.username'] = credentials[0]
|
||||
map['initializr.stats.elastic.password'] = credentials[1]
|
||||
}
|
||||
map['initializr.stats.elastic.uri'] = UriComponentsBuilder.fromUriString(uri)
|
||||
.userInfo(null).build().toString()
|
||||
|
||||
addOrReplace(environment.getPropertySources(), map);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return this.order
|
||||
}
|
||||
|
||||
private static void addOrReplace(MutablePropertySources propertySources,
|
||||
Map<String, Object> map) {
|
||||
MapPropertySource target = null
|
||||
if (propertySources.contains(PROPERTY_SOURCE_NAME)) {
|
||||
PropertySource<?> source = propertySources.get(PROPERTY_SOURCE_NAME)
|
||||
if (source instanceof MapPropertySource) {
|
||||
target = (MapPropertySource) source
|
||||
for (String key : map.keySet()) {
|
||||
if (!target.containsProperty(key)) {
|
||||
target.getSource().put(key, map.get(key))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (target == null) {
|
||||
target = new MapPropertySource(PROPERTY_SOURCE_NAME, map);
|
||||
}
|
||||
if (!propertySources.contains(PROPERTY_SOURCE_NAME)) {
|
||||
propertySources.addLast(target);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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 io.spring.initializr.metadata.InitializrMetadataProvider
|
||||
import io.spring.initializr.stat.ProjectGenerationStatPublisher
|
||||
import io.spring.initializr.stat.ProjectRequestDocumentFactory
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.retry.backoff.ExponentialBackOffPolicy
|
||||
import org.springframework.retry.policy.SimpleRetryPolicy
|
||||
import org.springframework.retry.support.RetryTemplate
|
||||
|
||||
/**
|
||||
* {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration
|
||||
* Auto-configuration} to publish statistics of each generated project.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 1.0
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(StatsProperties)
|
||||
@AutoConfigureAfter(InitializrAutoConfiguration)
|
||||
@ConditionalOnProperty('initializr.stats.elastic.uri')
|
||||
class InitializrStatsAutoConfiguration {
|
||||
|
||||
@Autowired
|
||||
private StatsProperties statsProperties
|
||||
|
||||
@Bean
|
||||
ProjectGenerationStatPublisher projectRequestStatHandler(InitializrMetadataProvider provider) {
|
||||
new ProjectGenerationStatPublisher(new ProjectRequestDocumentFactory(provider),
|
||||
statsProperties, statsRetryTemplate())
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(name = "statsRetryTemplate")
|
||||
RetryTemplate statsRetryTemplate() {
|
||||
RetryTemplate retryTemplate = new RetryTemplate()
|
||||
def backOffPolicy = new ExponentialBackOffPolicy(initialInterval: 3000L, multiplier: 3)
|
||||
def retryPolicy = new SimpleRetryPolicy(statsProperties.elastic.maxAttempts, Collections
|
||||
.<Class<? extends Throwable>, Boolean> singletonMap(Exception.class, true))
|
||||
retryTemplate.setBackOffPolicy(backOffPolicy)
|
||||
retryTemplate.setRetryPolicy(retryPolicy)
|
||||
retryTemplate
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
* <p>
|
||||
* 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<ClientHttpRequestInterceptor> interceptors = Collections
|
||||
.<ClientHttpRequestInterceptor> 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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String> request = RequestEntity
|
||||
.post(this.statsProperties.elastic.entityUrl)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(json)
|
||||
|
||||
this.retryTemplate.execute(new RetryCallback<Void, RuntimeException>() {
|
||||
@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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String> dependencies = []
|
||||
|
||||
String errorMessage
|
||||
boolean invalid
|
||||
boolean invalidJavaVersion
|
||||
boolean invalidLanguage
|
||||
boolean invalidPackaging
|
||||
boolean invalidType
|
||||
final List<String> invalidDependencies = []
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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<io.spring.initializr.metadata.Type>",
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
io.spring.initializr.config.InitializrAutoConfiguration,\
|
||||
io.spring.initializr.config.InitializrMetricsExporterAutoConfiguration
|
||||
io.spring.initializr.config.InitializrMetricsExporterAutoConfiguration,\
|
||||
io.spring.initializr.config.InitializrStatsAutoConfiguration
|
||||
|
||||
org.springframework.boot.env.EnvironmentPostProcessor=\
|
||||
io.spring.initializr.config.CloudfoundryEnvironmentPostProcessor
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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'))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.<Class<? extends Throwable>, 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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Content> stats = []
|
||||
|
||||
@RequestMapping(path = '/elastic/test/my-entity', method = RequestMethod.POST)
|
||||
void handleProjectRequestDocument(RequestEntity<String> 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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user