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:
Dave Syer
2016-09-26 16:13:35 +01:00
committed by Stephane Nicoll
parent 6efcef1186
commit b7d8d5c813
27 changed files with 1543 additions and 211 deletions

View File

@@ -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>

View File

@@ -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)
}
}

View File

@@ -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))
}
}
}
}

View File

@@ -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
}
}
}
}
}

View File

@@ -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://')
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}
}

View File

@@ -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);
}
}

View File

@@ -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)
}
}

View File

@@ -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
}
},

View File

@@ -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
}
},

View File

@@ -0,0 +1,4 @@
[source,json,options="nowrap"]
----
{{value}}
----