mirror of
https://gitee.com/dcren/initializr.git
synced 2025-04-05 17:38:06 +08:00
Initiate initializr documentation
This commit commit adds restdocs and stub generators and initiate a reference guide for Initializr. Most of the controller tests now use MockMvc via a custom version of the MockMvcClientHttpRequestFactory (from spring-test). The snippet names are auto-generated in the form <HttpMethod>/<path>[/queries(/<name-value)*][/headers](/name-value)*] when there is a comma-separated value in a header it is abbreviated as <first-value>.MORE. Wiremock stubs are generated in the same form under snippets/stubs (with ".json" as the file extension). The controller tests that stayed as full stack use a different base class AbstractFullStackInitializrIntegrationTests. A long JSON body can be broken out into separate snippets for each field (or rather a list of fields supplied by the user). This feature was already used with hard-coded snippets in the wiki. See gh-295
This commit is contained in:
parent
6efcef1186
commit
b7d8d5c813
@ -65,6 +65,11 @@
|
||||
<artifactId>json-path</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.restdocs</groupId>
|
||||
<artifactId>spring-restdocs-mockmvc</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
|
@ -17,6 +17,8 @@
|
||||
package io.spring.initializr.actuate.stat
|
||||
|
||||
import groovy.json.JsonSlurper
|
||||
|
||||
import io.spring.initializr.web.AbstractFullStackInitializrIntegrationTests;
|
||||
import io.spring.initializr.web.AbstractInitializrControllerIntegrationTests
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@ -46,7 +48,7 @@ import static org.junit.Assert.fail
|
||||
*/
|
||||
@Import(StatsMockController)
|
||||
@ActiveProfiles(['test-default', 'test-custom-stats'])
|
||||
class MainControllerStatsIntegrationTests extends AbstractInitializrControllerIntegrationTests {
|
||||
class MainControllerStatsIntegrationTests extends AbstractFullStackInitializrIntegrationTests {
|
||||
|
||||
@Autowired
|
||||
private StatsMockController statsMockController
|
||||
|
47
initializr-stubs/pom.xml
Normal file
47
initializr-stubs/pom.xml
Normal file
@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>io.spring.initializr</groupId>
|
||||
<artifactId>initializr</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>initializr-stubs</artifactId>
|
||||
<name>Spring Initializr :: Stubs</name>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>default-jar</id>
|
||||
<!-- put the default-jar in the none phase to skip it from being created -->
|
||||
<phase>none</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>stub</id>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
<inherited>false</inherited>
|
||||
<configuration>
|
||||
<attach>true</attach>
|
||||
<descriptor>${basedir}/src/assembly/stub.xml</descriptor>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
19
initializr-stubs/src/assembly/stub.xml
Normal file
19
initializr-stubs/src/assembly/stub.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<assembly
|
||||
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
|
||||
<id>stubs</id>
|
||||
<formats>
|
||||
<format>jar</format>
|
||||
</formats>
|
||||
<includeBaseDirectory>false</includeBaseDirectory>
|
||||
<fileSets>
|
||||
<fileSet>
|
||||
<directory>${basedir}/../initializr-web/target/snippets/stubs</directory>
|
||||
<outputDirectory>META-INF/${project.groupId}/${project.artifactId}/${project.version}/mappings</outputDirectory>
|
||||
<includes>
|
||||
<include>**/*</include>
|
||||
</includes>
|
||||
</fileSet>
|
||||
</fileSets>
|
||||
</assembly>
|
@ -61,6 +61,11 @@
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-contract-wiremock</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.spring.initializr</groupId>
|
||||
<artifactId>initializr-generator</artifactId>
|
||||
@ -72,6 +77,11 @@
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.restdocs</groupId>
|
||||
<artifactId>spring-restdocs-mockmvc</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>xmlunit</groupId>
|
||||
<artifactId>xmlunit</artifactId>
|
||||
@ -99,6 +109,18 @@
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-dependencies</artifactId>
|
||||
<version>Camden.RELEASE</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
|
@ -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.web
|
||||
|
||||
import io.spring.initializr.web.AbstractInitializrIntegrationTests.Config
|
||||
|
||||
import org.junit.runner.RunWith
|
||||
import org.springframework.boot.context.embedded.LocalServerPort
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.test.context.junit4.SpringRunner
|
||||
|
||||
import static org.junit.Assert.assertTrue
|
||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
|
||||
|
||||
/**
|
||||
* @author Stephane Nicoll
|
||||
* @author Dave Syer
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest(classes = Config.class, webEnvironment = RANDOM_PORT)
|
||||
abstract class AbstractFullStackInitializrIntegrationTests extends AbstractInitializrIntegrationTests {
|
||||
|
||||
@LocalServerPort
|
||||
int port
|
||||
|
||||
String host = "localhost"
|
||||
|
||||
String createUrl(String context) {
|
||||
"http://${host}:${port}" + (context.startsWith('/') ? context : '/' + context)
|
||||
}
|
||||
|
||||
}
|
@ -16,213 +16,49 @@
|
||||
|
||||
package io.spring.initializr.web
|
||||
|
||||
import java.nio.charset.Charset
|
||||
import io.spring.initializr.web.test.MockMvcClientHttpRequestFactory
|
||||
import io.spring.initializr.web.test.MockMvcClientHttpRequestFactoryTestExecutionListener
|
||||
|
||||
import io.spring.initializr.metadata.InitializrMetadata
|
||||
import io.spring.initializr.metadata.InitializrMetadataBuilder
|
||||
import io.spring.initializr.metadata.InitializrMetadataProvider
|
||||
import io.spring.initializr.metadata.InitializrProperties
|
||||
import io.spring.initializr.test.generator.ProjectAssert
|
||||
import io.spring.initializr.web.mapper.InitializrMetadataVersion
|
||||
import io.spring.initializr.web.support.DefaultInitializrMetadataProvider
|
||||
import org.json.JSONObject
|
||||
import org.junit.Rule
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import org.junit.runner.RunWith
|
||||
import org.skyscreamer.jsonassert.JSONAssert
|
||||
import org.skyscreamer.jsonassert.JSONCompareMode
|
||||
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
|
||||
import org.springframework.boot.context.embedded.LocalServerPort
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.beans.factory.BeanFactory
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
|
||||
import org.springframework.boot.web.client.RestTemplateCustomizer
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.core.io.ClassPathResource
|
||||
import org.springframework.http.HttpEntity
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.HttpMethod
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.test.context.junit4.SpringRunner
|
||||
import org.springframework.util.StreamUtils
|
||||
import org.springframework.web.client.RestTemplate
|
||||
import org.springframework.test.context.ContextConfiguration
|
||||
import org.springframework.test.context.TestExecutionListeners
|
||||
import org.springframework.test.context.TestExecutionListeners.MergeMode
|
||||
|
||||
import static org.junit.Assert.assertTrue
|
||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
|
||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment
|
||||
|
||||
/**
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest(classes = Config.class, webEnvironment = RANDOM_PORT)
|
||||
abstract class AbstractInitializrControllerIntegrationTests {
|
||||
@ContextConfiguration(classes = RestTemplateConfig)
|
||||
@TestExecutionListeners(mergeMode = MergeMode.MERGE_WITH_DEFAULTS, listeners = MockMvcClientHttpRequestFactoryTestExecutionListener)
|
||||
@AutoConfigureMockMvc
|
||||
@AutoConfigureRestDocs(outputDir="target/snippets", uriPort=80, uriHost="start.spring.io")
|
||||
abstract class AbstractInitializrControllerIntegrationTests extends AbstractInitializrIntegrationTests {
|
||||
|
||||
static final MediaType CURRENT_METADATA_MEDIA_TYPE = InitializrMetadataVersion.V2_1.mediaType
|
||||
|
||||
|
||||
@Rule
|
||||
public final TemporaryFolder folder = new TemporaryFolder()
|
||||
|
||||
@LocalServerPort
|
||||
protected int port
|
||||
|
||||
final RestTemplate restTemplate = new RestTemplate()
|
||||
String host = "start.spring.io"
|
||||
|
||||
@Autowired
|
||||
MockMvcClientHttpRequestFactory requests
|
||||
|
||||
String createUrl(String context) {
|
||||
"http://localhost:$port$context"
|
||||
}
|
||||
|
||||
String htmlHome() {
|
||||
def headers = new HttpHeaders()
|
||||
headers.setAccept([MediaType.TEXT_HTML])
|
||||
restTemplate.exchange(createUrl('/'), HttpMethod.GET, new HttpEntity<Void>(headers), String).body
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the 'Content-Type' header of the specified response.
|
||||
*/
|
||||
protected void validateContentType(ResponseEntity<String> response, MediaType expected) {
|
||||
def actual = response.headers.getContentType()
|
||||
assertTrue "Non compatible media-type, expected $expected, got $actual",
|
||||
actual.isCompatibleWith(expected)
|
||||
}
|
||||
|
||||
|
||||
protected void validateMetadata(ResponseEntity<String> response, MediaType mediaType,
|
||||
String version, JSONCompareMode compareMode) {
|
||||
validateContentType(response, mediaType)
|
||||
def json = new JSONObject(response.body)
|
||||
def expected = readMetadataJson(version)
|
||||
JSONAssert.assertEquals(expected, json, compareMode)
|
||||
}
|
||||
|
||||
protected void validateCurrentMetadata(ResponseEntity<String> response) {
|
||||
validateContentType(response, CURRENT_METADATA_MEDIA_TYPE)
|
||||
validateCurrentMetadata(new JSONObject(response.body))
|
||||
}
|
||||
|
||||
protected void validateCurrentMetadata(JSONObject json) {
|
||||
def expected = readMetadataJson('2.1.0')
|
||||
JSONAssert.assertEquals(expected, json, JSONCompareMode.STRICT)
|
||||
}
|
||||
|
||||
private JSONObject readMetadataJson(String version) {
|
||||
readJsonFrom("metadata/test-default-$version" + ".json")
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link ProjectAssert} for the following archive content.
|
||||
*/
|
||||
protected ProjectAssert zipProjectAssert(byte[] content) {
|
||||
projectAssert(content, ArchiveType.ZIP)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link ProjectAssert} for the following TGZ archive.
|
||||
*/
|
||||
protected ProjectAssert tgzProjectAssert(byte[] content) {
|
||||
projectAssert(content, ArchiveType.TGZ)
|
||||
}
|
||||
|
||||
protected ProjectAssert downloadZip(String context) {
|
||||
def body = downloadArchive(context)
|
||||
zipProjectAssert(body)
|
||||
}
|
||||
|
||||
protected ProjectAssert downloadTgz(String context) {
|
||||
def body = downloadArchive(context)
|
||||
tgzProjectAssert(body)
|
||||
}
|
||||
|
||||
protected byte[] downloadArchive(String context) {
|
||||
restTemplate.getForObject(createUrl(context), byte[])
|
||||
}
|
||||
|
||||
protected ResponseEntity<String> invokeHome(String userAgentHeader, String... acceptHeaders) {
|
||||
execute('/', String, userAgentHeader, acceptHeaders)
|
||||
}
|
||||
|
||||
protected <T> ResponseEntity<T> execute(String contextPath, Class<T> responseType,
|
||||
String userAgentHeader, String... acceptHeaders) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
if (userAgentHeader) {
|
||||
headers.set("User-Agent", userAgentHeader);
|
||||
}
|
||||
if (acceptHeaders) {
|
||||
List<MediaType> mediaTypes = new ArrayList<>()
|
||||
for (String acceptHeader : acceptHeaders) {
|
||||
mediaTypes.add(MediaType.parseMediaType(acceptHeader))
|
||||
}
|
||||
headers.setAccept(mediaTypes)
|
||||
} else {
|
||||
headers.setAccept(Collections.emptyList())
|
||||
}
|
||||
return restTemplate.exchange(createUrl(contextPath),
|
||||
HttpMethod.GET, new HttpEntity<Void>(headers), responseType)
|
||||
}
|
||||
|
||||
protected ProjectAssert projectAssert(byte[] content, ArchiveType archiveType) {
|
||||
def archiveFile = writeArchive(content)
|
||||
|
||||
def project = folder.newFolder()
|
||||
switch (archiveType) {
|
||||
case ArchiveType.ZIP:
|
||||
new AntBuilder().unzip(dest: project, src: archiveFile)
|
||||
break
|
||||
case ArchiveType.TGZ:
|
||||
new AntBuilder().untar(dest: project, src: archiveFile, compression: 'gzip')
|
||||
break
|
||||
}
|
||||
new ProjectAssert(project)
|
||||
}
|
||||
|
||||
protected File writeArchive(byte[] body) {
|
||||
def archiveFile = folder.newFile()
|
||||
def stream = new FileOutputStream(archiveFile)
|
||||
try {
|
||||
stream.write(body)
|
||||
} finally {
|
||||
stream.close()
|
||||
}
|
||||
archiveFile
|
||||
}
|
||||
|
||||
protected JSONObject readJsonFrom(String path) {
|
||||
def resource = new ClassPathResource(path)
|
||||
def stream = resource.inputStream
|
||||
try {
|
||||
def json = StreamUtils.copyToString(stream, Charset.forName('UTF-8'))
|
||||
|
||||
// Let's parse the port as it is random
|
||||
def content = json.replaceAll('@port@', String.valueOf(this.port))
|
||||
new JSONObject(content)
|
||||
} finally {
|
||||
stream.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private enum ArchiveType {
|
||||
ZIP,
|
||||
|
||||
TGZ
|
||||
context.startsWith('/') ? context : '/' + context
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableAutoConfiguration
|
||||
static class Config {
|
||||
static class RestTemplateConfig {
|
||||
|
||||
@Bean
|
||||
InitializrMetadataProvider initializrMetadataProvider(InitializrProperties properties) {
|
||||
new DefaultInitializrMetadataProvider(
|
||||
InitializrMetadataBuilder.fromInitializrProperties(properties).build(),
|
||||
new RestTemplate()) {
|
||||
@Override
|
||||
protected void updateInitializrMetadata(InitializrMetadata metadata) {
|
||||
null // Disable metadata fetching from spring.io
|
||||
}
|
||||
RestTemplateCustomizer mockMvcCustomizer(BeanFactory beanFactory) {
|
||||
{ template ->
|
||||
template.setRequestFactory(beanFactory.getBean(MockMvcClientHttpRequestFactory))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,237 @@
|
||||
/*
|
||||
* 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 io.spring.initializr.metadata.InitializrMetadata
|
||||
import io.spring.initializr.metadata.InitializrMetadataBuilder
|
||||
import io.spring.initializr.metadata.InitializrMetadataProvider
|
||||
import io.spring.initializr.metadata.InitializrProperties
|
||||
import io.spring.initializr.test.generator.ProjectAssert
|
||||
import io.spring.initializr.web.mapper.InitializrMetadataVersion
|
||||
import io.spring.initializr.web.support.DefaultInitializrMetadataProvider
|
||||
|
||||
import org.json.JSONObject
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import org.junit.runner.RunWith
|
||||
import org.skyscreamer.jsonassert.JSONAssert
|
||||
import org.skyscreamer.jsonassert.JSONCompareMode
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.core.io.ClassPathResource
|
||||
import org.springframework.http.HttpEntity
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.HttpMethod
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.test.context.junit4.SpringRunner
|
||||
import org.springframework.util.StreamUtils
|
||||
import org.springframework.web.client.RestTemplate
|
||||
|
||||
import java.nio.charset.Charset
|
||||
|
||||
import static org.junit.Assert.assertTrue
|
||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
|
||||
|
||||
/**
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest(classes = Config.class)
|
||||
abstract class AbstractInitializrIntegrationTests {
|
||||
|
||||
static final MediaType CURRENT_METADATA_MEDIA_TYPE = InitializrMetadataVersion.V2_1.mediaType
|
||||
|
||||
@Rule
|
||||
public final TemporaryFolder folder = new TemporaryFolder()
|
||||
|
||||
@Autowired
|
||||
private RestTemplateBuilder restTemplateBuilder
|
||||
|
||||
RestTemplate restTemplate
|
||||
|
||||
@Before
|
||||
void before() {
|
||||
restTemplate = restTemplateBuilder.build()
|
||||
}
|
||||
|
||||
abstract String createUrl(String context)
|
||||
|
||||
String htmlHome() {
|
||||
def headers = new HttpHeaders()
|
||||
headers.setAccept([MediaType.TEXT_HTML])
|
||||
restTemplate.exchange(createUrl('/'), HttpMethod.GET, new HttpEntity<Void>(headers), String).body
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the 'Content-Type' header of the specified response.
|
||||
*/
|
||||
protected void validateContentType(ResponseEntity<String> response, MediaType expected) {
|
||||
def actual = response.headers.getContentType()
|
||||
assertTrue "Non compatible media-type, expected $expected, got $actual",
|
||||
actual.isCompatibleWith(expected)
|
||||
}
|
||||
|
||||
|
||||
protected void validateMetadata(ResponseEntity<String> response, MediaType mediaType,
|
||||
String version, JSONCompareMode compareMode) {
|
||||
validateContentType(response, mediaType)
|
||||
def json = new JSONObject(response.body)
|
||||
def expected = readMetadataJson(version)
|
||||
JSONAssert.assertEquals(expected, json, compareMode)
|
||||
}
|
||||
|
||||
protected void validateCurrentMetadata(ResponseEntity<String> response) {
|
||||
validateContentType(response, CURRENT_METADATA_MEDIA_TYPE)
|
||||
validateCurrentMetadata(new JSONObject(response.body))
|
||||
}
|
||||
|
||||
protected void validateCurrentMetadata(JSONObject json) {
|
||||
def expected = readMetadataJson('2.1.0')
|
||||
JSONAssert.assertEquals(expected, json, JSONCompareMode.STRICT)
|
||||
}
|
||||
|
||||
private JSONObject readMetadataJson(String version) {
|
||||
readJsonFrom("metadata/test-default-$version" + ".json")
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link ProjectAssert} for the following archive content.
|
||||
*/
|
||||
protected ProjectAssert zipProjectAssert(byte[] content) {
|
||||
projectAssert(content, ArchiveType.ZIP)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link ProjectAssert} for the following TGZ archive.
|
||||
*/
|
||||
protected ProjectAssert tgzProjectAssert(byte[] content) {
|
||||
projectAssert(content, ArchiveType.TGZ)
|
||||
}
|
||||
|
||||
protected ProjectAssert downloadZip(String context) {
|
||||
def body = downloadArchive(context)
|
||||
zipProjectAssert(body)
|
||||
}
|
||||
|
||||
protected ProjectAssert downloadTgz(String context) {
|
||||
def body = downloadArchive(context)
|
||||
tgzProjectAssert(body)
|
||||
}
|
||||
|
||||
protected byte[] downloadArchive(String context) {
|
||||
restTemplate.getForObject(createUrl(context), byte[])
|
||||
}
|
||||
|
||||
protected ResponseEntity<String> invokeHome(String userAgentHeader, String... acceptHeaders) {
|
||||
execute('/', String, userAgentHeader, acceptHeaders)
|
||||
}
|
||||
|
||||
protected <T> ResponseEntity<T> execute(String contextPath, Class<T> responseType,
|
||||
String userAgentHeader, String... acceptHeaders) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
if (userAgentHeader) {
|
||||
headers.set("User-Agent", userAgentHeader);
|
||||
}
|
||||
if (acceptHeaders) {
|
||||
List<MediaType> mediaTypes = new ArrayList<>()
|
||||
for (String acceptHeader : acceptHeaders) {
|
||||
mediaTypes.add(MediaType.parseMediaType(acceptHeader))
|
||||
}
|
||||
headers.setAccept(mediaTypes)
|
||||
} else {
|
||||
headers.setAccept(Collections.emptyList())
|
||||
}
|
||||
return restTemplate.exchange(createUrl(contextPath),
|
||||
HttpMethod.GET, new HttpEntity<Void>(headers), responseType)
|
||||
}
|
||||
|
||||
protected ProjectAssert projectAssert(byte[] content, ArchiveType archiveType) {
|
||||
def archiveFile = writeArchive(content)
|
||||
|
||||
def project = folder.newFolder()
|
||||
switch (archiveType) {
|
||||
case ArchiveType.ZIP:
|
||||
new AntBuilder().unzip(dest: project, src: archiveFile)
|
||||
break
|
||||
case ArchiveType.TGZ:
|
||||
new AntBuilder().untar(dest: project, src: archiveFile, compression: 'gzip')
|
||||
break
|
||||
}
|
||||
new ProjectAssert(project)
|
||||
}
|
||||
|
||||
protected File writeArchive(byte[] body) {
|
||||
def archiveFile = folder.newFile()
|
||||
def stream = new FileOutputStream(archiveFile)
|
||||
try {
|
||||
stream.write(body)
|
||||
} finally {
|
||||
stream.close()
|
||||
}
|
||||
archiveFile
|
||||
}
|
||||
|
||||
protected JSONObject readJsonFrom(String path) {
|
||||
def resource = new ClassPathResource(path)
|
||||
def stream = resource.inputStream
|
||||
try {
|
||||
def json = StreamUtils.copyToString(stream, Charset.forName('UTF-8'))
|
||||
String placeholder = ""
|
||||
if (this.hasProperty("host")) {
|
||||
placeholder = "$host"
|
||||
}
|
||||
if (this.hasProperty("port")) {
|
||||
placeholder = "$host:$port"
|
||||
}
|
||||
// Let's parse the port as it is random
|
||||
// TODO: put the port back somehow so it appears in stubs
|
||||
def content = json.replaceAll('@host@', placeholder)
|
||||
new JSONObject(content)
|
||||
} finally {
|
||||
stream.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private enum ArchiveType {
|
||||
ZIP,
|
||||
|
||||
TGZ
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
InitializrMetadataProvider initializrMetadataProvider(InitializrProperties properties) {
|
||||
new DefaultInitializrMetadataProvider(
|
||||
InitializrMetadataBuilder.fromInitializrProperties(properties).build(),
|
||||
new RestTemplate()) {
|
||||
@Override
|
||||
protected void updateInitializrMetadata(InitializrMetadata metadata) {
|
||||
null // Disable metadata fetching from spring.io
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -45,7 +45,7 @@ class MainControllerEnvIntegrationTests extends AbstractInitializrControllerInte
|
||||
void doNotForceSsl() {
|
||||
ResponseEntity<String> response = invokeHome('curl/1.2.4', "*/*")
|
||||
String body = response.getBody()
|
||||
assertTrue "Must not force https", body.contains("http://localhost:$port/")
|
||||
assertTrue "Must not force https", body.contains("http://start.spring.io/")
|
||||
assertFalse "Must not force https", body.contains('https://')
|
||||
}
|
||||
|
||||
|
@ -74,7 +74,7 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra
|
||||
@Test
|
||||
void dependencyInRange() {
|
||||
def biz = new Dependency(id: 'biz', groupId: 'org.acme',
|
||||
artifactId: 'biz', version: '1.3.5', scope: 'runtime')
|
||||
artifactId: 'biz', version: '1.3.5', scope: 'runtime')
|
||||
downloadTgz('/starter.tgz?style=org.acme:biz&bootVersion=1.2.1.RELEASE').isJavaProject().isMavenProject()
|
||||
.hasStaticAndTemplatesResources(false).pomAssert()
|
||||
.hasDependenciesCount(2)
|
||||
@ -153,7 +153,8 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra
|
||||
}
|
||||
|
||||
@Test
|
||||
void metadataWithNoAcceptHeader() { // rest template sets application/json by default
|
||||
void metadataWithNoAcceptHeader() {
|
||||
// rest template sets application/json by default
|
||||
ResponseEntity<String> response = invokeHome(null, '*/*')
|
||||
validateCurrentMetadata(response)
|
||||
}
|
||||
@ -167,6 +168,9 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra
|
||||
|
||||
@Test
|
||||
void metadataWithV2AcceptHeader() {
|
||||
requests.setFields("_links.maven-project", "dependencies.values[0]", "type.values[0]",
|
||||
"javaVersion.values[0]", "packaging.values[0]",
|
||||
"bootVersion.values[0]", "language.values[0]");
|
||||
ResponseEntity<String> response = invokeHome(null, 'application/vnd.initializr.v2+json')
|
||||
validateMetadata(response, InitializrMetadataVersion.V2.mediaType, '2.0.0', JSONCompareMode.STRICT)
|
||||
}
|
||||
|
@ -16,15 +16,16 @@
|
||||
|
||||
package io.spring.initializr.web.project
|
||||
|
||||
import static org.junit.Assert.assertEquals
|
||||
import io.spring.initializr.metadata.InitializrMetadata
|
||||
import io.spring.initializr.metadata.InitializrMetadataBuilder
|
||||
import io.spring.initializr.metadata.InitializrMetadataProvider
|
||||
import io.spring.initializr.web.AbstractInitializrControllerIntegrationTests
|
||||
import io.spring.initializr.web.AbstractFullStackInitializrIntegrationTests
|
||||
|
||||
import org.json.JSONObject
|
||||
import org.junit.Test
|
||||
import org.skyscreamer.jsonassert.JSONAssert
|
||||
import org.skyscreamer.jsonassert.JSONCompareMode
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.core.io.UrlResource
|
||||
import org.springframework.http.HttpStatus
|
||||
@ -33,13 +34,11 @@ import org.springframework.http.ResponseEntity
|
||||
import org.springframework.test.context.ActiveProfiles
|
||||
import org.springframework.web.client.HttpClientErrorException
|
||||
|
||||
import static org.junit.Assert.assertEquals
|
||||
|
||||
/**
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
@ActiveProfiles('test-default')
|
||||
class MainControllerServiceMetadataIntegrationTests extends AbstractInitializrControllerIntegrationTests {
|
||||
class MainControllerServiceMetadataIntegrationTests extends AbstractFullStackInitializrIntegrationTests {
|
||||
|
||||
@Autowired
|
||||
private InitializrMetadataProvider metadataProvider
|
||||
|
@ -18,7 +18,7 @@ package io.spring.initializr.web.project
|
||||
|
||||
import geb.Browser
|
||||
import io.spring.initializr.test.generator.ProjectAssert
|
||||
import io.spring.initializr.web.AbstractInitializrControllerIntegrationTests
|
||||
import io.spring.initializr.web.AbstractFullStackInitializrIntegrationTests
|
||||
import io.spring.initializr.web.project.test.HomePage
|
||||
import org.junit.After
|
||||
import org.junit.Assume
|
||||
@ -40,7 +40,7 @@ import static org.junit.Assert.assertTrue
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
@ActiveProfiles('test-default')
|
||||
class ProjectGenerationSmokeTests extends AbstractInitializrControllerIntegrationTests {
|
||||
class ProjectGenerationSmokeTests extends AbstractFullStackInitializrIntegrationTests {
|
||||
|
||||
private File downloadDir
|
||||
private WebDriver driver
|
||||
|
@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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.test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* A path that identifies a field in a JSON payload.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Jeremy Rickard
|
||||
*
|
||||
*/
|
||||
//Copied from RestDocs to make it visible
|
||||
final class JsonFieldPath {
|
||||
|
||||
private static final Pattern BRACKETS_AND_ARRAY_PATTERN = Pattern
|
||||
.compile("\\[\'(.+?)\'\\]|\\[([0-9]+|\\*){0,1}\\]");
|
||||
|
||||
private static final Pattern ARRAY_INDEX_PATTERN = Pattern
|
||||
.compile("\\[([0-9]+|\\*){0,1}\\]");
|
||||
|
||||
private final String rawPath;
|
||||
|
||||
private final List<String> segments;
|
||||
|
||||
private final boolean precise;
|
||||
|
||||
private final boolean array;
|
||||
|
||||
private JsonFieldPath(String rawPath, List<String> segments, boolean precise,
|
||||
boolean array) {
|
||||
this.rawPath = rawPath;
|
||||
this.segments = segments;
|
||||
this.precise = precise;
|
||||
this.array = array;
|
||||
}
|
||||
|
||||
boolean isPrecise() {
|
||||
return this.precise;
|
||||
}
|
||||
|
||||
boolean isArray() {
|
||||
return this.array;
|
||||
}
|
||||
|
||||
List<String> getSegments() {
|
||||
return this.segments;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.rawPath;
|
||||
}
|
||||
|
||||
static JsonFieldPath compile(String path) {
|
||||
List<String> segments = extractSegments(path);
|
||||
return new JsonFieldPath(path, segments, matchesSingleValue(segments),
|
||||
isArraySegment(segments.get(segments.size() - 1)));
|
||||
}
|
||||
|
||||
static boolean isArraySegment(String segment) {
|
||||
return ARRAY_INDEX_PATTERN.matcher(segment).matches();
|
||||
}
|
||||
|
||||
static boolean matchesSingleValue(List<String> segments) {
|
||||
Iterator<String> iterator = segments.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
if (isArraySegment(iterator.next()) && iterator.hasNext()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static List<String> extractSegments(String path) {
|
||||
Matcher matcher = BRACKETS_AND_ARRAY_PATTERN.matcher(path);
|
||||
|
||||
int previous = 0;
|
||||
|
||||
List<String> segments = new ArrayList<>();
|
||||
while (matcher.find()) {
|
||||
if (previous != matcher.start()) {
|
||||
segments.addAll(extractDotSeparatedSegments(
|
||||
path.substring(previous, matcher.start())));
|
||||
}
|
||||
if (matcher.group(1) != null) {
|
||||
segments.add(matcher.group(1));
|
||||
}
|
||||
else {
|
||||
segments.add(matcher.group());
|
||||
}
|
||||
previous = matcher.end(0);
|
||||
}
|
||||
|
||||
if (previous < path.length()) {
|
||||
segments.addAll(extractDotSeparatedSegments(path.substring(previous)));
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
private static List<String> extractDotSeparatedSegments(String path) {
|
||||
List<String> segments = new ArrayList<>();
|
||||
for (String segment : path.split("\\.")) {
|
||||
if (segment.length() > 0) {
|
||||
segments.add(segment);
|
||||
}
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
}
|
@ -0,0 +1,251 @@
|
||||
/*
|
||||
* Copyright 2014-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.test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* A {@code JsonFieldProcessor} processes a payload's fields, allowing them to be
|
||||
* extracted and removed.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*
|
||||
*/
|
||||
// Copied from RestDocs to make it visible
|
||||
final class JsonFieldProcessor {
|
||||
|
||||
boolean hasField(JsonFieldPath fieldPath, Object payload) {
|
||||
final AtomicReference<Boolean> hasField = new AtomicReference<>(false);
|
||||
traverse(new ProcessingContext(payload, fieldPath), new MatchCallback() {
|
||||
|
||||
@Override
|
||||
public void foundMatch(Match match) {
|
||||
hasField.set(true);
|
||||
}
|
||||
|
||||
});
|
||||
return hasField.get();
|
||||
}
|
||||
|
||||
Object extract(JsonFieldPath path, Object payload) {
|
||||
final List<Object> matches = new ArrayList<>();
|
||||
traverse(new ProcessingContext(payload, path), new MatchCallback() {
|
||||
|
||||
@Override
|
||||
public void foundMatch(Match match) {
|
||||
matches.add(match.getValue());
|
||||
}
|
||||
|
||||
});
|
||||
if (matches.isEmpty()) {
|
||||
throw new IllegalArgumentException("Field does not exist: " + path);
|
||||
}
|
||||
if ((!path.isArray()) && path.isPrecise()) {
|
||||
return matches.get(0);
|
||||
}
|
||||
else {
|
||||
return matches;
|
||||
}
|
||||
}
|
||||
|
||||
void remove(final JsonFieldPath path, Object payload) {
|
||||
traverse(new ProcessingContext(payload, path), new MatchCallback() {
|
||||
|
||||
@Override
|
||||
public void foundMatch(Match match) {
|
||||
match.remove();
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
private void traverse(ProcessingContext context, MatchCallback matchCallback) {
|
||||
final String segment = context.getSegment();
|
||||
if (JsonFieldPath.isArraySegment(segment)) {
|
||||
if (context.getPayload() instanceof List) {
|
||||
handleListPayload(context, matchCallback);
|
||||
}
|
||||
}
|
||||
else if (context.getPayload() instanceof Map
|
||||
&& ((Map<?, ?>) context.getPayload()).containsKey(segment)) {
|
||||
handleMapPayload(context, matchCallback);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleListPayload(ProcessingContext context,
|
||||
MatchCallback matchCallback) {
|
||||
List<?> list = context.getPayload();
|
||||
final Iterator<?> items = list.iterator();
|
||||
if (context.isLeaf()) {
|
||||
while (items.hasNext()) {
|
||||
Object item = items.next();
|
||||
matchCallback.foundMatch(
|
||||
new ListMatch(items, list, item, context.getParentMatch()));
|
||||
}
|
||||
}
|
||||
else {
|
||||
while (items.hasNext()) {
|
||||
Object item = items.next();
|
||||
traverse(
|
||||
context.descend(item,
|
||||
new ListMatch(items, list, item, context.parent)),
|
||||
matchCallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMapPayload(ProcessingContext context,
|
||||
MatchCallback matchCallback) {
|
||||
Map<?, ?> map = context.getPayload();
|
||||
Object item = map.get(context.getSegment());
|
||||
MapMatch mapMatch = new MapMatch(item, map, context.getSegment(),
|
||||
context.getParentMatch());
|
||||
if (context.isLeaf()) {
|
||||
matchCallback.foundMatch(mapMatch);
|
||||
}
|
||||
else {
|
||||
traverse(context.descend(item, mapMatch), matchCallback);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class MapMatch implements Match {
|
||||
|
||||
private final Object item;
|
||||
|
||||
private final Map<?, ?> map;
|
||||
|
||||
private final String segment;
|
||||
|
||||
private final Match parent;
|
||||
|
||||
private MapMatch(Object item, Map<?, ?> map, String segment, Match parent) {
|
||||
this.item = item;
|
||||
this.map = map;
|
||||
this.segment = segment;
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValue() {
|
||||
return this.item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
this.map.remove(this.segment);
|
||||
if (this.map.isEmpty() && this.parent != null) {
|
||||
this.parent.remove();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class ListMatch implements Match {
|
||||
|
||||
private final Iterator<?> items;
|
||||
|
||||
private final List<?> list;
|
||||
|
||||
private final Object item;
|
||||
|
||||
private final Match parent;
|
||||
|
||||
private ListMatch(Iterator<?> items, List<?> list, Object item, Match parent) {
|
||||
this.items = items;
|
||||
this.list = list;
|
||||
this.item = item;
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValue() {
|
||||
return this.item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
this.items.remove();
|
||||
if (this.list.isEmpty() && this.parent != null) {
|
||||
this.parent.remove();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private interface MatchCallback {
|
||||
|
||||
void foundMatch(Match match);
|
||||
|
||||
}
|
||||
|
||||
private interface Match {
|
||||
|
||||
Object getValue();
|
||||
|
||||
void remove();
|
||||
}
|
||||
|
||||
private static final class ProcessingContext {
|
||||
|
||||
private final Object payload;
|
||||
|
||||
private final List<String> segments;
|
||||
|
||||
private final Match parent;
|
||||
|
||||
private final JsonFieldPath path;
|
||||
|
||||
private ProcessingContext(Object payload, JsonFieldPath path) {
|
||||
this(payload, path, null, null);
|
||||
}
|
||||
|
||||
private ProcessingContext(Object payload, JsonFieldPath path,
|
||||
List<String> segments, Match parent) {
|
||||
this.payload = payload;
|
||||
this.path = path;
|
||||
this.segments = segments == null ? path.getSegments() : segments;
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
private String getSegment() {
|
||||
return this.segments.get(0);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> T getPayload() {
|
||||
return (T) this.payload;
|
||||
}
|
||||
|
||||
private boolean isLeaf() {
|
||||
return this.segments.size() == 1;
|
||||
}
|
||||
|
||||
private Match getParentMatch() {
|
||||
return this.parent;
|
||||
}
|
||||
|
||||
private ProcessingContext descend(Object payload, Match match) {
|
||||
return new ProcessingContext(payload, this.path,
|
||||
this.segments.subList(1, this.segments.size()), match);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright 2012-2015 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.test;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.client.ClientHttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.mock.http.client.MockClientHttpRequest;
|
||||
import org.springframework.mock.http.client.MockClientHttpResponse;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.restdocs.snippet.Snippet;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.ResultActions;
|
||||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.RequestDispatcher;
|
||||
|
||||
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
|
||||
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
|
||||
import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.request;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
public class MockMvcClientHttpRequestFactory implements ClientHttpRequestFactory {
|
||||
|
||||
private final MockMvc mockMvc;
|
||||
|
||||
private String label = "UNKNOWN";
|
||||
|
||||
private List<String> fields = new ArrayList<>();
|
||||
|
||||
public MockMvcClientHttpRequestFactory(MockMvc mockMvc) {
|
||||
Assert.notNull(mockMvc, "MockMvc must not be null");
|
||||
this.mockMvc = mockMvc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientHttpRequest createRequest(final URI uri, final HttpMethod httpMethod)
|
||||
throws IOException {
|
||||
return new MockClientHttpRequest(httpMethod, uri) {
|
||||
@Override
|
||||
public ClientHttpResponse executeInternal() throws IOException {
|
||||
try {
|
||||
MockHttpServletRequestBuilder requestBuilder = request(httpMethod,
|
||||
uri.toString());
|
||||
requestBuilder.content(getBodyAsBytes());
|
||||
requestBuilder.headers(getHeaders());
|
||||
MockHttpServletResponse servletResponse = actions(requestBuilder)
|
||||
.andReturn().getResponse();
|
||||
HttpStatus status = HttpStatus.valueOf(servletResponse.getStatus());
|
||||
if (status.value() >= 400) {
|
||||
requestBuilder = request(HttpMethod.GET, "/error")
|
||||
.requestAttr(RequestDispatcher.ERROR_STATUS_CODE,
|
||||
status.value())
|
||||
.requestAttr(RequestDispatcher.ERROR_REQUEST_URI,
|
||||
uri.toString());
|
||||
if (servletResponse.getErrorMessage() != null) {
|
||||
requestBuilder.requestAttr(RequestDispatcher.ERROR_MESSAGE,
|
||||
servletResponse.getErrorMessage());
|
||||
}
|
||||
// Overwrites the snippets from the first request
|
||||
servletResponse = actions(requestBuilder).andReturn()
|
||||
.getResponse();
|
||||
}
|
||||
byte[] body = servletResponse.getContentAsByteArray();
|
||||
HttpHeaders headers = getResponseHeaders(servletResponse);
|
||||
MockClientHttpResponse clientResponse = new MockClientHttpResponse(
|
||||
body, status);
|
||||
clientResponse.getHeaders().putAll(headers);
|
||||
return clientResponse;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
private ResultActions actions(MockHttpServletRequestBuilder requestBuilder)
|
||||
throws Exception {
|
||||
ResultActions actions = MockMvcClientHttpRequestFactory.this.mockMvc
|
||||
.perform(requestBuilder);
|
||||
List<Snippet> snippets = new ArrayList<>();
|
||||
for (String field : this.fields) {
|
||||
snippets.add(new ResponseFieldSnippet(field));
|
||||
}
|
||||
actions.andDo(document(label, preprocessResponse(prettyPrint()), snippets.toArray(new Snippet[0])));
|
||||
this.fields = new ArrayList<>();
|
||||
return actions;
|
||||
}
|
||||
|
||||
private HttpHeaders getResponseHeaders(MockHttpServletResponse response) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
for (String name : response.getHeaderNames()) {
|
||||
List<String> values = response.getHeaders(name);
|
||||
for (String value : values) {
|
||||
headers.add(name, value);
|
||||
}
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
public void setTest(Class<?> testClass, Method testMethod) {
|
||||
this.label = testMethod.getName();
|
||||
}
|
||||
|
||||
public void setFields(String... fields) {
|
||||
this.fields = Arrays.asList(fields);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2012-2015 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.test;
|
||||
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.test.context.TestContext;
|
||||
import org.springframework.test.context.support.AbstractTestExecutionListener;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
public final class MockMvcClientHttpRequestFactoryTestExecutionListener
|
||||
extends AbstractTestExecutionListener {
|
||||
|
||||
private MockMvcClientHttpRequestFactory factory;
|
||||
|
||||
@Override
|
||||
public void beforeTestClass(TestContext testContext) throws Exception {
|
||||
ConfigurableBeanFactory beanFactory = (ConfigurableBeanFactory) testContext
|
||||
.getApplicationContext().getAutowireCapableBeanFactory();
|
||||
if (!beanFactory.containsBean("mockMvcClientHttpRequestFactory")) {
|
||||
factory = new MockMvcClientHttpRequestFactory(
|
||||
beanFactory.getBean(MockMvc.class));
|
||||
beanFactory.registerSingleton("mockMvcClientHttpRequestFactory",
|
||||
this.factory);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTestMethod(TestContext testContext) throws Exception {
|
||||
if (factory != null) {
|
||||
this.factory.setTest(testContext.getTestClass(), testContext.getTestMethod());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright 2012-2015 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.test;
|
||||
|
||||
import org.springframework.restdocs.RestDocumentationContext;
|
||||
import org.springframework.restdocs.operation.Operation;
|
||||
import org.springframework.restdocs.snippet.TemplatedSnippet;
|
||||
import org.springframework.restdocs.snippet.WriterResolver;
|
||||
import org.springframework.restdocs.templates.TemplateEngine;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
|
||||
/**
|
||||
* Creates a separate snippet for a single field in a larger payload. The output comes in
|
||||
* a sub-directory ("response-fields") of one containing the request and response
|
||||
* snippets, with a file name the same as the path. An exception to the last rule is if
|
||||
* you pick a single array element by using a path like `foo.bar[0]`, the snippet file
|
||||
* name is then just the array name (because asciidoctor cannot import snippets with
|
||||
* brackets in the name).
|
||||
*
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
public class ResponseFieldSnippet extends TemplatedSnippet {
|
||||
|
||||
private String path;
|
||||
|
||||
private final JsonFieldProcessor fieldProcessor = new JsonFieldProcessor();
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
private final Integer index;
|
||||
|
||||
private final String file;
|
||||
|
||||
public ResponseFieldSnippet(String path) {
|
||||
super("response-fields", Collections.emptyMap());
|
||||
String file = path;
|
||||
if (path.endsWith("]")) {
|
||||
// In this project we actually only need snippets whose last segment is an
|
||||
// array index, so we can deal with it as a special case here. Ideally the
|
||||
// restdocs implementation of JsonField would support this use case as well.
|
||||
String index = path.substring(path.lastIndexOf("[") + 1);
|
||||
index = index.substring(0, index.length() - 1);
|
||||
this.index = Integer.valueOf(index);
|
||||
path = path.substring(0, path.lastIndexOf("["));
|
||||
file = file.replace("]", "").replace("[", ".");
|
||||
} else {
|
||||
this.index = null;
|
||||
}
|
||||
this.file = file;
|
||||
this.path = path;
|
||||
objectMapper.configure(SerializationFeature.INDENT_OUTPUT, true);
|
||||
}
|
||||
|
||||
/*
|
||||
* Copy of super class method, but changes the path of the output file to include the
|
||||
* path
|
||||
*/
|
||||
@Override
|
||||
public void document(Operation operation) throws IOException {
|
||||
RestDocumentationContext context = (RestDocumentationContext) operation
|
||||
.getAttributes().get(RestDocumentationContext.class.getName());
|
||||
WriterResolver writerResolver = (WriterResolver) operation.getAttributes()
|
||||
.get(WriterResolver.class.getName());
|
||||
try (Writer writer = writerResolver
|
||||
.resolve(operation.getName() + "/" + getSnippetName(), file, context)) {
|
||||
Map<String, Object> model = createModel(operation);
|
||||
model.putAll(getAttributes());
|
||||
TemplateEngine templateEngine = (TemplateEngine) operation.getAttributes()
|
||||
.get(TemplateEngine.class.getName());
|
||||
writer.append(templateEngine.compileTemplate(getSnippetName()).render(model));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> createModel(Operation operation) {
|
||||
String value = "{}";
|
||||
try {
|
||||
Object object = objectMapper.readValue(
|
||||
operation.getResponse().getContentAsString(), Object.class);
|
||||
Object field = fieldProcessor.extract(JsonFieldPath.compile(path), object);
|
||||
if (field instanceof List && index != null) {
|
||||
field = ((List<?>) field).get(index);
|
||||
}
|
||||
value = objectMapper.writeValueAsString(field);
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
return Collections.singletonMap("value", value);
|
||||
}
|
||||
|
||||
}
|
@ -17,11 +17,11 @@
|
||||
package io.spring.initializr.web.ui
|
||||
|
||||
import io.spring.initializr.web.AbstractInitializrControllerIntegrationTests
|
||||
|
||||
import org.json.JSONObject
|
||||
import org.junit.Test
|
||||
import org.skyscreamer.jsonassert.JSONAssert
|
||||
import org.skyscreamer.jsonassert.JSONCompareMode
|
||||
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.test.context.ActiveProfiles
|
||||
@ -50,5 +50,4 @@ class UiControllerIntegrationTests extends AbstractInitializrControllerIntegrati
|
||||
def expected = readJsonFrom("metadata/ui/test-dependencies-$version" + ".json")
|
||||
JSONAssert.assertEquals(expected, actual, JSONCompareMode.STRICT)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,19 +1,19 @@
|
||||
{
|
||||
"_links": {
|
||||
"maven-build": {
|
||||
"href": "https://localhost:@port@/pom.xml?type=maven-build{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
|
||||
"href": "https://@host@/pom.xml?type=maven-build{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
|
||||
"templated": true
|
||||
},
|
||||
"maven-project": {
|
||||
"href": "https://localhost:@port@/starter.zip?type=maven-project{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
|
||||
"href": "https://@host@/starter.zip?type=maven-project{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
|
||||
"templated": true
|
||||
},
|
||||
"gradle-build": {
|
||||
"href": "https://localhost:@port@/build.gradle?type=gradle-build{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
|
||||
"href": "https://@host@/build.gradle?type=gradle-build{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
|
||||
"templated": true
|
||||
},
|
||||
"gradle-project": {
|
||||
"href": "https://localhost:@port@/starter.zip?type=gradle-project{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
|
||||
"href": "https://@host@/starter.zip?type=gradle-project{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
|
||||
"templated": true
|
||||
}
|
||||
},
|
||||
|
@ -1,23 +1,23 @@
|
||||
{
|
||||
"_links": {
|
||||
"dependencies": {
|
||||
"href": "https://localhost:@port@/dependencies{?bootVersion}",
|
||||
"href": "https://@host@/dependencies{?bootVersion}",
|
||||
"templated": true
|
||||
},
|
||||
"maven-build": {
|
||||
"href": "https://localhost:@port@/pom.xml?type=maven-build{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
|
||||
"href": "https://@host@/pom.xml?type=maven-build{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
|
||||
"templated": true
|
||||
},
|
||||
"maven-project": {
|
||||
"href": "https://localhost:@port@/starter.zip?type=maven-project{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
|
||||
"href": "https://@host@/starter.zip?type=maven-project{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
|
||||
"templated": true
|
||||
},
|
||||
"gradle-build": {
|
||||
"href": "https://localhost:@port@/build.gradle?type=gradle-build{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
|
||||
"href": "https://@host@/build.gradle?type=gradle-build{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
|
||||
"templated": true
|
||||
},
|
||||
"gradle-project": {
|
||||
"href": "https://localhost:@port@/starter.zip?type=gradle-project{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
|
||||
"href": "https://@host@/starter.zip?type=gradle-project{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
|
||||
"templated": true
|
||||
}
|
||||
},
|
||||
|
@ -0,0 +1,4 @@
|
||||
[source,json,options="nowrap"]
|
||||
----
|
||||
{{value}}
|
||||
----
|
24
pom.xml
24
pom.xml
@ -45,6 +45,7 @@
|
||||
<module>initializr-actuator</module>
|
||||
<module>initializr-generator</module>
|
||||
<module>initializr-web</module>
|
||||
<module>initializr-stubs</module>
|
||||
</modules>
|
||||
|
||||
<dependencyManagement>
|
||||
@ -117,6 +118,29 @@
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.asciidoctor</groupId>
|
||||
<artifactId>asciidoctor-maven-plugin</artifactId>
|
||||
<version>1.5.3</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>generate-docs</id>
|
||||
<phase>prepare-package</phase>
|
||||
<inherited>false</inherited>
|
||||
<goals>
|
||||
<goal>process-asciidoc</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<sourceDocumentName>index.adoc</sourceDocumentName>
|
||||
<backend>html</backend>
|
||||
<doctype>book</doctype>
|
||||
<attributes>
|
||||
<snippets>${basedir}/initializr-web/target/snippets</snippets>
|
||||
</attributes>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
|
127
src/main/asciidoc/configuration-format.adoc
Normal file
127
src/main/asciidoc/configuration-format.adoc
Normal file
@ -0,0 +1,127 @@
|
||||
:code: https://github.com/spring-io/initializr/tree/master/initializr
|
||||
:service: https://github.com/spring-io/initializr/tree/master/initializr-service
|
||||
|
||||
# Configuration Guide
|
||||
|
||||
This section describes the configuration structure that is used by the initializr. The metadata provided through configuration are actually used to generate the link:Project-metadata-json-output[Project metadata json format].
|
||||
|
||||
TIP: A good way to get started with the configuration is to look at the {service}/src/main/resources/application.yml[configuration of the production instance] and check the end-result on https://start.spring.io
|
||||
|
||||
The configuration is split in several sections:
|
||||
|
||||
* An `env` section used to provide various global settings
|
||||
* A `dependencies` section lists the available dependencies. This is the most important section of the service as it defines the "libraries" that the user can choose
|
||||
* The `groupId`, `artifactId`, `version`, `name`, `description` and `packageName` provide default values for these project settings
|
||||
* The `types`, `packagings`, `javaVersions`, `languages` and `bootVersions` provide the list of available option for each setting and which one is the default.
|
||||
|
||||
## Env section
|
||||
|
||||
TIP: Check {code}/src/main/groovy/io/spring/initializr/metadata/InitializrConfiguration.groovy#L113[the code] for a full list of the available configuration options.
|
||||
|
||||
The `env` element defines environment option that the service uses:
|
||||
|
||||
* `artifactRepository`: the URL of the (maven) repository that should be used to download the Spring Boot CLI distribution bundle. This is only used by the `/spring` endpoint at the moment
|
||||
* `springBootMetadataUrl` the URL of the resource that provides the list of available Spring Boot versions.
|
||||
* `forceSsl`: a boolean flag that determines if we should use `https` even when browsing a resource via `http`. This is _enabled_ by default.
|
||||
* `fallbackApplicationName`: the name of the _default_ application. Application names are generated based on the project's name. However, some user input may result in an invalid identifier for a Java class name for instance.
|
||||
* `invalidApplicationNames`: a fixed list of invalid application names. If a project generation uses one of these names, the fallback is used instead
|
||||
* `invalidPackageNames`: a fixed list of invalid package names. If a project generation uses one of these names, the default is used instead
|
||||
* `googleAnalyticsTrackingCode`: the Google Analytics code to use. If this is set, Google analytics is automatically enabled
|
||||
* `kotlin`: kotlin-specific settings. For now, only the kotlin version to use can be configured
|
||||
* `maven`: maven-specified settings. A custom maven parent POM can be defined and whether or not the `spring-boot-dependencies` BOM should be automatically added to the project
|
||||
|
||||
If some of your dependencies require a custom Bill of Materials (BOM) and/or a custom repository, you can add them here and use the id as a reference. For instance, let's say that you want to integrate with library `foo` and it requires a `foo-bom` and a `foo-repo`. You can configure things as follows:
|
||||
|
||||
```yml
|
||||
initializr:
|
||||
env:
|
||||
boms:
|
||||
foo-bom:
|
||||
groupId: com.example
|
||||
artifactId: foo-bom
|
||||
version: 1.2.3
|
||||
repositories:
|
||||
foo-repo:
|
||||
name: foo-release-repo
|
||||
url: https://repo.example.com/foo
|
||||
snapshotsEnabled: false
|
||||
```
|
||||
|
||||
You can then use the `foo-bom` and `foo-repo` in a "dependency" or "dependency group" section.
|
||||
|
||||
NOTE: The `spring-milestones` and `spring-snapshots` repositories are available by default. Please note that these are just references and won't impact the project unless you choose a dependency that explicitly refer to a bom and/or repo by id. Check the example below for more details.
|
||||
|
||||
## Dependencies section
|
||||
|
||||
The `dependencies` section allows you define a list of groups, each group having one more `dependency`. A group gather dependencies that share a common characteristics (i.e. all web-related dependencies for instance).
|
||||
|
||||
A `dependency` has the following basic characteristics:
|
||||
|
||||
* A mandatory identifier. If no further information is provided, a Spring Boot starer with that id is assumed
|
||||
* A `name` and `description` used in the generated meta-data and the web ui
|
||||
* A `groupId` and `artifactId` to define the coordinates of the dependency
|
||||
* A `version` if Spring Boot does not already provide a dependency management for that dependency
|
||||
* A `scope` (can be `compile`, `runtime`, `provided` or `test`)
|
||||
* The reference to a `bom` or a `repository` that must be added to the project once that dependency is added
|
||||
* A `versionRange` used to determine the Spring Boot versions that are compatible with the dependency
|
||||
|
||||
TIP: Check {code}/src/main/groovy/io/spring/initializr/metadata/Dependency.groovy[the code] for a full list of the available configuration options.
|
||||
|
||||
Here is the most basic dependency entry you could have
|
||||
|
||||
```yml
|
||||
initializr:
|
||||
dependencies:
|
||||
- name: Core
|
||||
content:
|
||||
- id: security
|
||||
name: Security
|
||||
description: Secure your application via spring-security
|
||||
```
|
||||
|
||||
TIP: The `security` dependency is held within a group called "Core".
|
||||
|
||||
This adds an option name `Security` with a tooltip showing the description above. If a project is generated with that dependency, the `org.springframework.boot:spring-boot-starter-security` dependency will be added to the project.
|
||||
|
||||
Let's now add a custom dependency that is not managed by Spring Boot and that only work from Spring Boot `1.2.0.RELEASE` and onwards but should not be used in the 1.3 lines and further for some reason.
|
||||
|
||||
```yml
|
||||
initializr:
|
||||
dependencies:
|
||||
- name: Core
|
||||
content:
|
||||
- id: my-lib-id
|
||||
name: My lib
|
||||
description: Secure your application via spring-security
|
||||
groupId: com.example.foo
|
||||
artifactId: foo-core
|
||||
bom: foo-bom
|
||||
repository: foo-repo
|
||||
versionRange: "[1.2.0.RELEASE,1.3.0.M1)"
|
||||
```
|
||||
|
||||
If one selects this entry, the `com.example.foo:foo-core}` dependency will be added and the Bill of Materials and repository for `foo` will be added automatically to the project as well (see the "Env section" above for a reference to those identifiers). Because the bom provides a dependency management for `foo-core` there is no need to hardcode the version in the configuration.
|
||||
|
||||
The `versionRange` syntax follows some simple rules: a square bracket "[" or "]" denotes an inclusive end of the range and a round bracket "(" or ")" denotes an exclusive end of the range. A range can also be unbounded by defining a a single version. In the example above, the dependency will be available as from `1.2.0.RELEASE` up to, not included, `1.3.0.M1` (which is the first milestone of the 1.3 line).
|
||||
|
||||
### Dependency group
|
||||
|
||||
A dependency group gather a set of dependencies as well as some common settings: `bom`, `repository` and `versionRange`. If one of them is set, it is applied for all dependencies within that group. It is still possible to override a particular value at the dependency level.
|
||||
|
||||
## Other sections
|
||||
|
||||
The other section defines the default and the list of available options in the web UI. This also drives how the meta-data for your instance are generated and tooling support is meant to react to that.
|
||||
|
||||
For instance, if you want your groupId to default to `org.acme` and the `javaVersions` to only be `1.7` and `1.8` you would write the following config:
|
||||
|
||||
```yml
|
||||
initializr:
|
||||
groupId:
|
||||
value: org.acme
|
||||
javaVersions:
|
||||
- id: 1.8
|
||||
default: true
|
||||
- id: 1.7
|
||||
default: false
|
||||
```
|
||||
|
36
src/main/asciidoc/guidelines-for-3rd-party-starters.adoc
Normal file
36
src/main/asciidoc/guidelines-for-3rd-party-starters.adoc
Normal file
@ -0,0 +1,36 @@
|
||||
# Third Party Starters
|
||||
|
||||
Spring Initializr (start.spring.io) provides an opinionated, web based generator for Spring Boot-based projects. start.spring.io provides a number of core starters as well as 3rd party contributed starters. While it’s possible to create 3rd party starter POMs without having them listed on start.spring.io, members of the community have asked for guidelines to be considered for inclusion in our production instance.
|
||||
|
||||
These guidelines aim to balance:
|
||||
|
||||
. An opinionated getting started experience
|
||||
. Alignment with the overall direction of the Spring ecosystem
|
||||
. Community contributions
|
||||
. Community confidence in the long term viability and support for 3rd party starters
|
||||
|
||||
## Guidelines
|
||||
|
||||
. Code of Conduct
|
||||
* Anyone contributing to any of the projects in the Spring portfolio, including Spring Initializr, must adhere to our Code of Conduct
|
||||
. Open sourced and available on Maven central
|
||||
* The library must be open sourced and available in Maven Central repository. Please see the Maven Central requirements for more details
|
||||
* The project must be made available with an appropriate OSS license which is deemed compatible with the Apache 2.0 license
|
||||
. Community
|
||||
* There should be a sizeable community that are actively using and maintaining the library
|
||||
* An established support forum such as Stack Overflow and issue tracker
|
||||
. Spring Boot Compliance:
|
||||
* The starter should comply with http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-custom-starter-naming[the rules of a 3rd party starter].
|
||||
* If the starter provides configuration entries, they should have proper meta-data (including documentation)
|
||||
* start.spring.io will host one starter POM per 3rd party library
|
||||
* The starter should be actively maintained and upgraded to the newest version of Spring Boot if necessary
|
||||
. Dependencies:
|
||||
* The 3rd party starter POM should not bring in any supporting dependencies that aren’t compatible with the Apache 2.0 license
|
||||
* All of these supporting dependencies must be available in Maven Central
|
||||
* Dependencies should be compatible with Spring Boot’s core starters
|
||||
. Test Coverage:
|
||||
* The 3rd party starter should have proper test coverage to validate that it works properly with Spring Boot
|
||||
Testing should encompass combinations of optional dependencies using standalone sample projects
|
||||
. Spring Engineering Review:
|
||||
* The Spring Boot team periodically reviews starter statistics and may decide to remove 3rd party starters from start.spring.io if they are not being used. The main driver is to help keep Spring Boot opinionated and remove choices that aren’t being used by large audiences.
|
||||
* Even if these requirements are met, the Spring Boot team may decide that a proposed 3rd party starter does not meet the strategic direction of Spring Boot and the overall portfolio. In this case, it’s still possible to create and share a 3rd party starters but they won’t be listed on start.spring.io.
|
23
src/main/asciidoc/index.adoc
Normal file
23
src/main/asciidoc/index.adoc
Normal file
@ -0,0 +1,23 @@
|
||||
= Spring Initializr Reference Guide
|
||||
Stéphane Nicoll; Dave Syer
|
||||
:doctype: book
|
||||
:toc:
|
||||
:toclevels: 4
|
||||
:source-highlighter: prettify
|
||||
:numbered:
|
||||
:icons: font
|
||||
:hide-uri-scheme:
|
||||
:github-tag: master
|
||||
:github-repo: spring-io/initializr
|
||||
:github-raw: https://raw.github.com/{github-repo}/{github-tag}
|
||||
:github-code: https://github.com/{github-repo}/tree/{github-tag}
|
||||
:github-wiki: https://github.com/{github-repo}/wiki
|
||||
:github-master-code: https://github.com/{github-repo}/tree/master
|
||||
// ======================================================================================
|
||||
|
||||
include::metadata-format.adoc[leveloffset=+1]
|
||||
include::using-the-stubs.adoc[leveloffset=+1]
|
||||
include::configuration-format.adoc[leveloffset=+1]
|
||||
include::guidelines-for-3rd-party-starters.adoc[leveloffset=+1]
|
||||
|
||||
// ======================================================================================
|
115
src/main/asciidoc/metadata-format.adoc
Normal file
115
src/main/asciidoc/metadata-format.adoc
Normal file
@ -0,0 +1,115 @@
|
||||
= Metadata Format
|
||||
|
||||
This section describes the hal/json structure of the metadata exposed by the initializr. Such metadata can be used by third party clients to provide a list of options and default settings that can be used to request the creation of a project.
|
||||
|
||||
Each third-party client is advised to set a `User-Agent` header for *each* request sent to the service. A good structure for a user agent is `clientId/clientVersion` (i.e. `foo/1.2.0` for the "foo" client and version `1.2.0`).
|
||||
|
||||
== Content
|
||||
|
||||
Any third party client can retrieve the capabilities of the service by issuing a `GET` on the root URL using the following `Accept` header: `application/vnd.initializr.v2+json`. Please note that the metadata may evolve in a non backward compatible way in the future so adding this header ensures the service returns the metadata format you expect.
|
||||
|
||||
This is a full example of an output for a service running at `https://start.spring.io`:
|
||||
|
||||
.request
|
||||
include::{snippets}/metadataWithV2AcceptHeader/http-request.adoc[]
|
||||
|
||||
.response
|
||||
include::{snippets}/metadataWithV2AcceptHeader/http-response.adoc[]
|
||||
|
||||
The current capabilities are the following:
|
||||
|
||||
* Project dependencies: these are the _starters_ really or actually any dependency that we might want to add to the project.
|
||||
* Project types: these define the action that can be invoked on this service and a description of what it would produce (for instance a zip holding a pre-configured Maven project). Each type may have one more tags that further define what it generates
|
||||
* Packaging: the kind of projects to generate. This merely gives a hint to the component responsible to generate the project (for instance, generate an executable _jar_ project)
|
||||
* Java version: the supported java versions
|
||||
* Language: the language to use (e.g. Java)
|
||||
* Boot version: the Spring Boot version to use
|
||||
* Additional basic information such as: `groupId`, `artifactId`, `version`, `name`, `description` and `packageName`
|
||||
|
||||
Each top-level attribute (i.e. capability) has a standard format:
|
||||
|
||||
* A `type` attribute that defines the semantic of the attribute (see below)
|
||||
* A `default` attribute that defines either the default value or the reference to the default value
|
||||
* A `values` attribute that defines the set of acceptable values (if any). This can be hierarchical (with `values` being held in `values`). Each item in a `values` array can have an `id`, `name` and `description`)
|
||||
|
||||
The following attribute `type` are supported:
|
||||
|
||||
* `text`: defines a simple text value with no option
|
||||
* `single-select`: defines a simple value to be chosen amongst the specified options
|
||||
* `hierarchical-multi-select`: defines a hierarchical set of values (values in values) with the ability to select multiple values
|
||||
* `action`: a special type that defines the attribute defining the action to use
|
||||
|
||||
Each action is defined as a HAL-compliant URL. For instance, the `maven-project` type templated URL is defined as follows:
|
||||
|
||||
._links.maven-project
|
||||
include::{snippets}/metadataWithV2AcceptHeader/response-fields/_links.maven-project.adoc[]
|
||||
|
||||
You can use Spring HATEOAS and the `UriTemplate` helper in particular to generate an URI from template variables. Note that the variables match the name of top-level attribute in the metadata document. If you can't parse such URI, the `action` attribute of each type gives you the root action to invoke on the server. This requires more manual handling on your end.
|
||||
|
||||
=== Project dependencies
|
||||
|
||||
A dependency is usually the coordinates of a _starter_ module but it can be just as well be a regular dependency. A typical dependency structure looks like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Display name",
|
||||
"id": "org.acme.project:project-starter-foo",
|
||||
"description": "What starter foo does"
|
||||
}
|
||||
```
|
||||
|
||||
The name is used as a display name to be shown in whatever UI used by the remote client. The id can be anything, really as the actual dependency definition is defined through configuration. If no id is defined, a default one is built using the `groupId` and `artifactId` of the dependency. Note in particular that the version is **never** used as part of an automatic id.
|
||||
|
||||
Each dependency belongs to a group. The idea of the group is to gather similar dependencies and order them. Here is a value containing the `core` group to illustrates the feature:
|
||||
|
||||
.dependencies.values[0]
|
||||
include::{snippets}/metadataWithV2AcceptHeader/response-fields/dependencies.values.0.adoc[]
|
||||
|
||||
=== Project types
|
||||
|
||||
The `type` element defines what kind of project can be generated and how. For instance, if the service exposes the capability to generate a Maven project, this would look like this:
|
||||
|
||||
.type.values[0]
|
||||
include::{snippets}/metadataWithV2AcceptHeader/response-fields/type.values.0.adoc[]
|
||||
|
||||
You should not rely on the output format depending that information. Always use the response headers that define a `Content-Type` and also a `Content-Disposition` header.
|
||||
|
||||
Note that each id has a related HAL-compliant link that can be used to generate a proper URI based on template variables. The top-level `type` has, as any other attribute, a `default` attribute that is a hint to select what the service consider to be a good default.
|
||||
|
||||
The `action` attribute defines the endpoint the client should contact to actually generate a project of that type if you can't use the HAL-compliant url.
|
||||
|
||||
The `tags` object is used to categorize the project type and give _hints_ to 3rd party client. For instance, the _build_ tag defines the build system the project is going to use and the _format_ tag defines the format of the generated content (i.e. here a complete project vs. a build file. Note that the `Content-type` header of the reply provides additional metadata).
|
||||
|
||||
=== Packaging
|
||||
|
||||
The `packaging` element defines the kind of project that should be generated.
|
||||
|
||||
.packaging.values[0]
|
||||
include::{snippets}/metadataWithV2AcceptHeader/response-fields/packaging.values.0.adoc[]
|
||||
|
||||
The obvious values for this element are `jar` and `war`.
|
||||
|
||||
=== Java version
|
||||
|
||||
The `javaVersion` element provides a list of possible java versions for the project:
|
||||
|
||||
.javaVersion.values[0]
|
||||
include::{snippets}/metadataWithV2AcceptHeader/response-fields/javaVersion.values.0.adoc[]
|
||||
|
||||
=== Languages
|
||||
|
||||
The `language` element provides a list of possible languages for the project:
|
||||
|
||||
.language.values[0]
|
||||
include::{snippets}/metadataWithV2AcceptHeader/response-fields/language.values.0.adoc[]
|
||||
|
||||
=== Boot version
|
||||
|
||||
The `bootVersion` element provides the list of available boot versions
|
||||
|
||||
.bootVersion.values[0]
|
||||
include::{snippets}/metadataWithV2AcceptHeader/response-fields/bootVersion.values.0.adoc[]
|
||||
|
||||
== Defaults
|
||||
|
||||
Each top-level element has a `default` attribute that should be used as a hint to provide the default value in the relevant UI component.
|
99
src/main/asciidoc/using-the-stubs.adoc
Normal file
99
src/main/asciidoc/using-the-stubs.adoc
Normal file
@ -0,0 +1,99 @@
|
||||
:project-version: 1.0.0.BUILD-SNAPSHOT
|
||||
|
||||
= Using the Stubs
|
||||
|
||||
The Initializr project publishes
|
||||
https://github.com/tomakehurst/wiremock[WireMock] stubs for all the
|
||||
JSON responses that are tested in the project. If you are writing a
|
||||
client for the Initializr service, you can use these stubs to test
|
||||
your own code. You can consume them with the raw Wiremock APIs, or via
|
||||
some features of
|
||||
https://github.com/spring-cloud/spring-cloud-contract[Spring Cloud
|
||||
Contract].
|
||||
|
||||
WireMock is an embedded web server that analyses incoming requests and
|
||||
chooses stub responses based on matching some rules (e.g. a specific
|
||||
header value). So if you send it a request which matches one of its
|
||||
stubs, it will send you a response as if it was a real Initializr
|
||||
service, and you can use that to do full stack integration testing of
|
||||
your client.
|
||||
|
||||
== Using WireMock with Spring Boot
|
||||
|
||||
A convenient way to consume the stubs in your project is to add a test
|
||||
dependency:
|
||||
|
||||
[source,xml,indent=0,subs="attributes,specialchars"]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>io.spring.initializr</groupId>
|
||||
<artifactId>initializr</artifactId>
|
||||
<classifier>stubs</classifier>
|
||||
<version>{project-version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
----
|
||||
|
||||
and then pull the stubs from the classpath. In a Spring Boot
|
||||
application, using Spring Cloud Contract, you can start a WireMock
|
||||
server and register all the stubs with it like this:
|
||||
|
||||
[source,java,subs="attributes"]
|
||||
----
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest
|
||||
@AutoConfigureWireMock(port = 0, stubs="classpath:META-INF/io.spring.initializr/initializr/{project-version}")
|
||||
public class ClientApplicationTests {
|
||||
|
||||
@Value("${wiremock.server.port}")
|
||||
private int port;
|
||||
|
||||
...
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
Alternatively you can register individual stubs with the server by
|
||||
autowiring it, and using its own API. For example:
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
|
||||
@Value("classpath:META-INF/io.spring.initializr/initializr/{project-version}/mappings/metadataWithV2AcceptHeader.json")
|
||||
private Resource root;
|
||||
|
||||
@Autowired
|
||||
private WireMockServer server;
|
||||
|
||||
@Test
|
||||
public void testDependencies() throws IOException {
|
||||
server.addStubMapping(StubMapping.buildFrom(StreamUtils.copyToString(
|
||||
root.getInputStream(), Charset.forName("UTF-8"))));
|
||||
...
|
||||
}
|
||||
|
||||
----
|
||||
|
||||
Then you have a server that returns the stub of the JSON metadata
|
||||
("metadataWithV2AcceptHeader.json") when you send it a header "Accept:
|
||||
application/vnd.initializr.v2+json" (as recommended).
|
||||
|
||||
NOTE: Spring Cloud Contract also has support for easily running stubs
|
||||
of multiple services on different ports. You might find that useful if
|
||||
your client uses other services. To just load up the Initializr stubs
|
||||
in your test case you use `@AutoConfigureStubRunner` (instead of
|
||||
`@AutoConfigureWireMock`)
|
||||
|
||||
== Names and Paths of Stubs
|
||||
|
||||
The stubs are laid out in a jar file in a form (under "/mappings")
|
||||
that can be consumed by WireMock just by setting its file source. The
|
||||
names of the individual stubs are the same as the method names of the
|
||||
test cases that generated them in the Initializr project. So for
|
||||
example there is a test case "metadataWithV2AcceptHeader" in
|
||||
`MainControllerIntegrationTests` that makes assertions about the
|
||||
response when the accept header is
|
||||
"application/vnd.initializr.v2+json". The response is recorded in the
|
||||
stub, and it will match in WireMock if the same headers and request
|
||||
parameters that were used in the Initializr test case and used in the
|
||||
client. The method name usually summarizes what those values are.
|
Loading…
Reference in New Issue
Block a user