mirror of
https://gitee.com/dcren/initializr.git
synced 2025-09-19 01:58:16 +08:00
Add clientId metrics
Parse the `User-Agent` http request header to determine if the client is known. If that is the case, increment the relevant `client_id` counter. A `ProjectRequest` had now a generic `parameters` array with all the request headers by default. Since we don't want to accidentally map any of those from a form input, `BasicProjectRequest` contains only "public" fields and is the type exposed at the controller level. Closes gh-193
This commit is contained in:
@@ -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.generator
|
||||
|
||||
/**
|
||||
* The base settings of a project request. Only these can be bound by user's
|
||||
* input.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 1.0
|
||||
*/
|
||||
class BasicProjectRequest {
|
||||
|
||||
List<String> style = []
|
||||
List<String> dependencies = []
|
||||
String name
|
||||
String type
|
||||
String description
|
||||
String groupId
|
||||
String artifactId
|
||||
String version
|
||||
String bootVersion
|
||||
String packaging
|
||||
String applicationName
|
||||
String language
|
||||
String packageName
|
||||
String javaVersion
|
||||
|
||||
// The base directory to create in the archive - no baseDir by default
|
||||
String baseDir
|
||||
|
||||
}
|
@@ -16,6 +16,8 @@
|
||||
|
||||
package io.spring.initializr.generator
|
||||
|
||||
import io.spring.initializr.util.UserAgentWrapper
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.actuate.metrics.CounterService
|
||||
import org.springframework.context.event.EventListener
|
||||
@@ -56,6 +58,7 @@ class ProjectGenerationMetricsListener {
|
||||
handlePackaging(request)
|
||||
handleLanguage(request)
|
||||
handleBootVersion(request)
|
||||
handleUserAgent(request)
|
||||
}
|
||||
|
||||
protected void handleDependencies(ProjectRequest request) {
|
||||
@@ -102,6 +105,17 @@ class ProjectGenerationMetricsListener {
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleUserAgent(ProjectRequest request) {
|
||||
String userAgent = request.parameters['user-agent']
|
||||
if (userAgent) {
|
||||
UserAgentWrapper wrapper = new UserAgentWrapper(userAgent)
|
||||
def information = wrapper.extractAgentInformation()
|
||||
if (information) {
|
||||
increment(key("client_id.$information.id.id"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void increment(String key) {
|
||||
counterService.increment(key)
|
||||
}
|
||||
|
@@ -33,30 +33,17 @@ import io.spring.initializr.util.VersionRange
|
||||
* @since 1.0
|
||||
*/
|
||||
@Slf4j
|
||||
class ProjectRequest {
|
||||
class ProjectRequest extends BasicProjectRequest {
|
||||
|
||||
/**
|
||||
* The id of the starter to use if no dependency is defined.
|
||||
*/
|
||||
static final DEFAULT_STARTER = 'root_starter'
|
||||
|
||||
List<String> style = []
|
||||
List<String> dependencies = []
|
||||
String name
|
||||
String type
|
||||
String description
|
||||
String groupId
|
||||
String artifactId
|
||||
String version
|
||||
String bootVersion
|
||||
String packaging
|
||||
String applicationName
|
||||
String language
|
||||
String packageName
|
||||
String javaVersion
|
||||
|
||||
// The base directory to create in the archive - no baseDir by default
|
||||
String baseDir
|
||||
/**
|
||||
* Additional parameters that can be used to further identify the request.
|
||||
*/
|
||||
final Map<String,Object> parameters = [:]
|
||||
|
||||
// Resolved dependencies based on the ids provided by either "style" or "dependencies"
|
||||
List<Dependency> resolvedDependencies
|
||||
|
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* 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.util
|
||||
|
||||
import org.springframework.util.Assert
|
||||
|
||||
/**
|
||||
* Wraps a user agent header.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 1.0
|
||||
*/
|
||||
class UserAgentWrapper {
|
||||
|
||||
private static final TOOL_REGEX = '(.*)\\/(.*)'
|
||||
|
||||
private static final STS_REGEX = 'STS (.*)'
|
||||
|
||||
private final String userAgent
|
||||
|
||||
UserAgentWrapper(String userAgent) {
|
||||
Assert.notNull(userAgent, "UserAgent must not be null")
|
||||
this.userAgent = userAgent.trim()
|
||||
}
|
||||
|
||||
boolean isSpringBootCli() {
|
||||
return userAgent.startsWith(AgentId.SPRING_BOOT_CLI.name)
|
||||
}
|
||||
|
||||
boolean isCurl() {
|
||||
return userAgent.startsWith(AgentId.CURL.name)
|
||||
}
|
||||
|
||||
boolean isHttpie() {
|
||||
return userAgent.startsWith(AgentId.HTTPIE.name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the identifier of the agent and its version. Return {@code null} if the
|
||||
* user agent managed by this instance is not recognized.
|
||||
*/
|
||||
AgentInformation extractAgentInformation() {
|
||||
def matcher = (userAgent =~ TOOL_REGEX)
|
||||
if (matcher.matches()) {
|
||||
String name = matcher.group(1)
|
||||
for (AgentId id : AgentId.values()) {
|
||||
if (name.equals(id.name)) {
|
||||
String version = matcher.group(2)
|
||||
return new AgentInformation(id, version)
|
||||
}
|
||||
}
|
||||
}
|
||||
matcher = userAgent =~ STS_REGEX
|
||||
if (matcher.matches()) {
|
||||
return new AgentInformation(AgentId.STS, matcher.group(1))
|
||||
}
|
||||
if (userAgent.equals(AgentId.INTELLIJ_IDEA.name)) {
|
||||
return new AgentInformation(AgentId.INTELLIJ_IDEA, null)
|
||||
}
|
||||
if (userAgent.contains('Mozilla/5.0')) { // Super heuristics
|
||||
return new AgentInformation(AgentId.BROWSER, null)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
static class AgentInformation {
|
||||
final AgentId id
|
||||
final String version
|
||||
|
||||
AgentInformation(AgentId id, String version) {
|
||||
this.id = id
|
||||
this.version = version
|
||||
}
|
||||
}
|
||||
|
||||
static enum AgentId {
|
||||
|
||||
CURL('curl', 'curl'),
|
||||
|
||||
HTTPIE('httpie', 'HTTPie'),
|
||||
|
||||
SPRING_BOOT_CLI('spring', 'SpringBootCli'),
|
||||
|
||||
STS('sts', 'STS'),
|
||||
|
||||
INTELLIJ_IDEA('intellijidea', 'IntelliJ IDEA'),
|
||||
|
||||
BROWSER('browser', 'Browser')
|
||||
|
||||
final String id
|
||||
final String name
|
||||
|
||||
AgentId(String id, String name) {
|
||||
this.id = id
|
||||
this.name = name
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
* Copyright 2012-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -20,6 +20,7 @@ import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
import groovy.util.logging.Slf4j
|
||||
import io.spring.initializr.generator.BasicProjectRequest
|
||||
import io.spring.initializr.generator.CommandLineHelpGenerator
|
||||
import io.spring.initializr.generator.ProjectGenerator
|
||||
import io.spring.initializr.generator.ProjectRequest
|
||||
@@ -30,6 +31,7 @@ import io.spring.initializr.mapper.InitializrMetadataV2JsonMapper
|
||||
import io.spring.initializr.mapper.InitializrMetadataVersion
|
||||
import io.spring.initializr.metadata.DependencyMetadataProvider
|
||||
import io.spring.initializr.metadata.InitializrMetadata
|
||||
import io.spring.initializr.util.UserAgentWrapper
|
||||
import io.spring.initializr.util.Version
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
@@ -71,8 +73,9 @@ class MainController extends AbstractInitializrController {
|
||||
|
||||
|
||||
@ModelAttribute
|
||||
ProjectRequest projectRequest() {
|
||||
BasicProjectRequest projectRequest(@RequestHeader Map<String,String> headers) {
|
||||
def request = new ProjectRequest()
|
||||
request.parameters << headers
|
||||
request.initialize(metadataProvider.get())
|
||||
request
|
||||
}
|
||||
@@ -97,15 +100,16 @@ class MainController extends AbstractInitializrController {
|
||||
|
||||
def builder = ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN)
|
||||
if (userAgent) {
|
||||
if (userAgent.startsWith(WebConfig.CURL_USER_AGENT_PREFIX)) {
|
||||
UserAgentWrapper wrapper = new UserAgentWrapper(userAgent)
|
||||
if (wrapper.isCurl()) {
|
||||
def content = commandLineHelpGenerator.generateCurlCapabilities(metadata, appUrl)
|
||||
return builder.eTag(createUniqueId(content)).body(content)
|
||||
}
|
||||
if (userAgent.startsWith(WebConfig.HTTPIE_USER_AGENT_PREFIX)) {
|
||||
if (wrapper.isHttpie()) {
|
||||
def content = commandLineHelpGenerator.generateHttpieCapabilities(metadata, appUrl)
|
||||
return builder.eTag(createUniqueId(content)).body(content)
|
||||
}
|
||||
if (userAgent.startsWith(WebConfig.SPRING_BOOT_CLI_AGENT_PREFIX)) {
|
||||
if (wrapper.isSpringBootCli()) {
|
||||
def content = commandLineHelpGenerator.generateSpringBootCliCapabilities(metadata, appUrl)
|
||||
return builder.eTag(createUniqueId(content)).body(content)
|
||||
}
|
||||
@@ -182,21 +186,22 @@ class MainController extends AbstractInitializrController {
|
||||
|
||||
@RequestMapping('/pom')
|
||||
@ResponseBody
|
||||
ResponseEntity<byte[]> pom(ProjectRequest request) {
|
||||
def mavenPom = projectGenerator.generateMavenPom(request)
|
||||
ResponseEntity<byte[]> pom(BasicProjectRequest request) {
|
||||
def mavenPom = projectGenerator.generateMavenPom((ProjectRequest) request)
|
||||
createResponseEntity(mavenPom, 'application/octet-stream', 'pom.xml')
|
||||
}
|
||||
|
||||
@RequestMapping('/build')
|
||||
@ResponseBody
|
||||
ResponseEntity<byte[]> gradle(ProjectRequest request) {
|
||||
def gradleBuild = projectGenerator.generateGradleBuild(request)
|
||||
ResponseEntity<byte[]> gradle(BasicProjectRequest request) {
|
||||
def gradleBuild = projectGenerator.generateGradleBuild((ProjectRequest) request)
|
||||
createResponseEntity(gradleBuild, 'application/octet-stream', 'build.gradle')
|
||||
}
|
||||
|
||||
@RequestMapping('/starter.zip')
|
||||
@ResponseBody
|
||||
ResponseEntity<byte[]> springZip(ProjectRequest request) {
|
||||
ResponseEntity<byte[]> springZip(BasicProjectRequest basicRequest) {
|
||||
ProjectRequest request = (ProjectRequest) basicRequest
|
||||
def dir = projectGenerator.generateProjectStructure(request)
|
||||
|
||||
def download = projectGenerator.createDistributionFile(dir, '.zip')
|
||||
@@ -212,7 +217,8 @@ class MainController extends AbstractInitializrController {
|
||||
|
||||
@RequestMapping(value = '/starter.tgz', produces = 'application/x-compress')
|
||||
@ResponseBody
|
||||
ResponseEntity<byte[]> springTgz(ProjectRequest request) {
|
||||
ResponseEntity<byte[]> springTgz(BasicProjectRequest basicRequest) {
|
||||
ProjectRequest request = (ProjectRequest) basicRequest
|
||||
def dir = projectGenerator.generateProjectStructure(request)
|
||||
|
||||
def download = projectGenerator.createDistributionFile(dir, '.tgz')
|
||||
|
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
* Copyright 2012-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package io.spring.initializr.web
|
||||
|
||||
import io.spring.initializr.util.UserAgentWrapper
|
||||
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.web.HttpMediaTypeNotAcceptableException
|
||||
@@ -35,12 +37,6 @@ import javax.servlet.http.HttpServletRequest
|
||||
*/
|
||||
class WebConfig extends WebMvcConfigurerAdapter {
|
||||
|
||||
static final String CURL_USER_AGENT_PREFIX = 'curl'
|
||||
|
||||
static final String HTTPIE_USER_AGENT_PREFIX = 'HTTPie'
|
||||
|
||||
static final String SPRING_BOOT_CLI_AGENT_PREFIX = 'SpringBootCli'
|
||||
|
||||
@Override
|
||||
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
|
||||
configurer.defaultContentTypeStrategy(new CommandLineContentNegotiationStrategy())
|
||||
@@ -63,7 +59,8 @@ class WebConfig extends WebMvcConfigurerAdapter {
|
||||
}
|
||||
String userAgent = request.getHeader(HttpHeaders.USER_AGENT)
|
||||
if (userAgent) {
|
||||
if (userAgent.startsWith(CURL_USER_AGENT_PREFIX) || userAgent.startsWith(HTTPIE_USER_AGENT_PREFIX)) {
|
||||
UserAgentWrapper wrapper = new UserAgentWrapper(userAgent)
|
||||
if (wrapper.isCurl() || wrapper.isHttpie()) {
|
||||
return Collections.singletonList(MediaType.TEXT_PLAIN)
|
||||
}
|
||||
}
|
||||
|
@@ -189,6 +189,15 @@ class ProjectGenerationMetricsListenerTests {
|
||||
metricsAssert.hasValue(1, 'initializr.boot_version.1_0_2_RELEASE')
|
||||
}
|
||||
|
||||
@Test
|
||||
void userAgentAvailable() {
|
||||
def request = initialize()
|
||||
request.parameters['user-agent'] = 'HTTPie/0.9.2'
|
||||
request.resolve(metadata)
|
||||
fireProjectGeneratedEvent(request)
|
||||
metricsAssert.hasValue(1, 'initializr.client_id.httpie')
|
||||
}
|
||||
|
||||
@Test
|
||||
void collectAllMetrics() {
|
||||
def request = initialize()
|
||||
@@ -198,6 +207,7 @@ class ProjectGenerationMetricsListenerTests {
|
||||
request.javaVersion = '1.6'
|
||||
request.language = 'groovy'
|
||||
request.bootVersion = '1.0.2.RELEASE'
|
||||
request.parameters['user-agent'] = 'SpringBootCli/1.3.0.RELEASE'
|
||||
|
||||
request.resolve(metadata)
|
||||
fireProjectGeneratedEvent(request)
|
||||
@@ -205,7 +215,8 @@ class ProjectGenerationMetricsListenerTests {
|
||||
'initializr.dependency.web', 'initializr.dependency.security',
|
||||
'initializr.type.gradle-project', 'initializr.packaging.jar',
|
||||
'initializr.java_version.1_6', 'initializr.language.groovy',
|
||||
'initializr.boot_version.1_0_2_RELEASE').metricsCount(8)
|
||||
'initializr.boot_version.1_0_2_RELEASE',
|
||||
'initializr.client_id.spring').metricsCount(9)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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.util
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import static org.hamcrest.CoreMatchers.equalTo
|
||||
import static org.hamcrest.CoreMatchers.is
|
||||
import static org.hamcrest.CoreMatchers.nullValue
|
||||
import static org.junit.Assert.assertThat
|
||||
|
||||
/**
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class UserAgentWrapperTests {
|
||||
|
||||
@Test
|
||||
void checkCurl() {
|
||||
UserAgentWrapper userAgent = new UserAgentWrapper('curl/1.2.4')
|
||||
assertThat(userAgent.isCurl(), is(true))
|
||||
assertThat(userAgent.isHttpie(), is(false))
|
||||
assertThat(userAgent.isSpringBootCli(), is(false))
|
||||
def information = userAgent.extractAgentInformation()
|
||||
assertThat(information.id, equalTo(UserAgentWrapper.AgentId.CURL))
|
||||
assertThat(information.version, equalTo('1.2.4'))
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkHttpie() {
|
||||
UserAgentWrapper userAgent = new UserAgentWrapper('HTTPie/0.8.0')
|
||||
assertThat(userAgent.isCurl(), is(false))
|
||||
assertThat(userAgent.isHttpie(), is(true))
|
||||
assertThat(userAgent.isSpringBootCli(), is(false))
|
||||
def information = userAgent.extractAgentInformation()
|
||||
assertThat(information.id, equalTo(UserAgentWrapper.AgentId.HTTPIE))
|
||||
assertThat(information.version, equalTo('0.8.0'))
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkSpringBootCli() {
|
||||
UserAgentWrapper userAgent = new UserAgentWrapper('SpringBootCli/1.3.1.RELEASE')
|
||||
assertThat(userAgent.isCurl(), is(false))
|
||||
assertThat(userAgent.isHttpie(), is(false))
|
||||
assertThat(userAgent.isSpringBootCli(), is(true))
|
||||
def information = userAgent.extractAgentInformation()
|
||||
assertThat(information.id, equalTo(UserAgentWrapper.AgentId.SPRING_BOOT_CLI))
|
||||
assertThat(information.version, equalTo('1.3.1.RELEASE'))
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkSts() {
|
||||
UserAgentWrapper userAgent = new UserAgentWrapper('STS 3.7.0.201506251244-RELEASE')
|
||||
assertThat(userAgent.isCurl(), is(false))
|
||||
assertThat(userAgent.isHttpie(), is(false))
|
||||
assertThat(userAgent.isSpringBootCli(), is(false))
|
||||
def information = userAgent.extractAgentInformation()
|
||||
assertThat(information.id, equalTo(UserAgentWrapper.AgentId.STS))
|
||||
assertThat(information.version, equalTo('3.7.0.201506251244-RELEASE'))
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkIntelliJIDEA() {
|
||||
UserAgentWrapper userAgent = new UserAgentWrapper('IntelliJ IDEA')
|
||||
assertThat(userAgent.isCurl(), is(false))
|
||||
assertThat(userAgent.isHttpie(), is(false))
|
||||
assertThat(userAgent.isSpringBootCli(), is(false))
|
||||
def information = userAgent.extractAgentInformation()
|
||||
assertThat(information.id, equalTo(UserAgentWrapper.AgentId.INTELLIJ_IDEA))
|
||||
assertThat(information.version, is(nullValue()))
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkGenericBrowser() {
|
||||
UserAgentWrapper userAgent =
|
||||
new UserAgentWrapper('Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5 Build/MMB29K) ')
|
||||
assertThat(userAgent.isCurl(), is(false))
|
||||
assertThat(userAgent.isHttpie(), is(false))
|
||||
assertThat(userAgent.isSpringBootCli(), is(false))
|
||||
def information = userAgent.extractAgentInformation()
|
||||
assertThat(information.id, equalTo(UserAgentWrapper.AgentId.BROWSER))
|
||||
assertThat(information.version, is(nullValue()))
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkRobot() {
|
||||
UserAgentWrapper userAgent =
|
||||
new UserAgentWrapper('Googlebot-Mobile')
|
||||
assertThat(userAgent.isCurl(), is(false))
|
||||
assertThat(userAgent.isHttpie(), is(false))
|
||||
assertThat(userAgent.isSpringBootCli(), is(false))
|
||||
def information = userAgent.extractAgentInformation()
|
||||
assertThat(information, is(nullValue()))
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user