diff --git a/initializr-actuator/pom.xml b/initializr-actuator/pom.xml
index 2c7240e1..82d9289b 100644
--- a/initializr-actuator/pom.xml
+++ b/initializr-actuator/pom.xml
@@ -65,6 +65,11 @@
json-pathtest
+
+ org.springframework.restdocs
+ spring-restdocs-mockmvc
+ test
+ org.springframework.bootspring-boot-starter-web
diff --git a/initializr-actuator/src/test/groovy/io/spring/initializr/actuate/stat/MainControllerStatsIntegrationTests.groovy b/initializr-actuator/src/test/groovy/io/spring/initializr/actuate/stat/MainControllerStatsIntegrationTests.groovy
index 6638cf51..30b47018 100644
--- a/initializr-actuator/src/test/groovy/io/spring/initializr/actuate/stat/MainControllerStatsIntegrationTests.groovy
+++ b/initializr-actuator/src/test/groovy/io/spring/initializr/actuate/stat/MainControllerStatsIntegrationTests.groovy
@@ -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
diff --git a/initializr-stubs/pom.xml b/initializr-stubs/pom.xml
new file mode 100644
index 00000000..9f0dc094
--- /dev/null
+++ b/initializr-stubs/pom.xml
@@ -0,0 +1,47 @@
+
+
+ 4.0.0
+
+ io.spring.initializr
+ initializr
+ 1.0.0.BUILD-SNAPSHOT
+
+ initializr-stubs
+ Spring Initializr :: Stubs
+
+
+
+
+ maven-jar-plugin
+
+
+ default-jar
+
+ none
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+
+
+ stub
+ prepare-package
+
+ single
+
+ false
+
+ true
+ ${basedir}/src/assembly/stub.xml
+ false
+
+
+
+
+
+
+
+
diff --git a/initializr-stubs/src/assembly/stub.xml b/initializr-stubs/src/assembly/stub.xml
new file mode 100644
index 00000000..60e95033
--- /dev/null
+++ b/initializr-stubs/src/assembly/stub.xml
@@ -0,0 +1,19 @@
+
+ stubs
+
+ jar
+
+ false
+
+
+ ${basedir}/../initializr-web/target/snippets/stubs
+ META-INF/${project.groupId}/${project.artifactId}/${project.version}/mappings
+
+ **/*
+
+
+
+
diff --git a/initializr-web/pom.xml b/initializr-web/pom.xml
index 94059c52..2e34812b 100644
--- a/initializr-web/pom.xml
+++ b/initializr-web/pom.xml
@@ -61,6 +61,11 @@
spring-boot-starter-testtest
+
+ org.springframework.cloud
+ spring-cloud-contract-wiremock
+ test
+ io.spring.initializrinitializr-generator
@@ -72,6 +77,11 @@
spring-boot-starter-webtest
+
+ org.springframework.restdocs
+ spring-restdocs-mockmvc
+ test
+ xmlunitxmlunit
@@ -99,6 +109,18 @@
+
+
+
+ org.springframework.cloud
+ spring-cloud-dependencies
+ Camden.RELEASE
+ pom
+ import
+
+
+
+
diff --git a/initializr-web/src/test/groovy/io/spring/initializr/web/AbstractFullStackInitializrIntegrationTests.groovy b/initializr-web/src/test/groovy/io/spring/initializr/web/AbstractFullStackInitializrIntegrationTests.groovy
new file mode 100644
index 00000000..574e4354
--- /dev/null
+++ b/initializr-web/src/test/groovy/io/spring/initializr/web/AbstractFullStackInitializrIntegrationTests.groovy
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2012-2016 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.spring.initializr.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)
+ }
+
+}
diff --git a/initializr-web/src/test/groovy/io/spring/initializr/web/AbstractInitializrControllerIntegrationTests.groovy b/initializr-web/src/test/groovy/io/spring/initializr/web/AbstractInitializrControllerIntegrationTests.groovy
index c41f83f9..3c5c32fb 100644
--- a/initializr-web/src/test/groovy/io/spring/initializr/web/AbstractInitializrControllerIntegrationTests.groovy
+++ b/initializr-web/src/test/groovy/io/spring/initializr/web/AbstractInitializrControllerIntegrationTests.groovy
@@ -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(headers), String).body
- }
-
- /**
- * Validate the 'Content-Type' header of the specified response.
- */
- protected void validateContentType(ResponseEntity response, MediaType expected) {
- def actual = response.headers.getContentType()
- assertTrue "Non compatible media-type, expected $expected, got $actual",
- actual.isCompatibleWith(expected)
- }
-
-
- protected void validateMetadata(ResponseEntity 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 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 invokeHome(String userAgentHeader, String... acceptHeaders) {
- execute('/', String, userAgentHeader, acceptHeaders)
- }
-
- protected ResponseEntity execute(String contextPath, Class responseType,
- String userAgentHeader, String... acceptHeaders) {
- HttpHeaders headers = new HttpHeaders();
- if (userAgentHeader) {
- headers.set("User-Agent", userAgentHeader);
- }
- if (acceptHeaders) {
- List 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(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))
}
}
-
}
}
diff --git a/initializr-web/src/test/groovy/io/spring/initializr/web/AbstractInitializrIntegrationTests.groovy b/initializr-web/src/test/groovy/io/spring/initializr/web/AbstractInitializrIntegrationTests.groovy
new file mode 100644
index 00000000..76ea941c
--- /dev/null
+++ b/initializr-web/src/test/groovy/io/spring/initializr/web/AbstractInitializrIntegrationTests.groovy
@@ -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(headers), String).body
+ }
+
+ /**
+ * Validate the 'Content-Type' header of the specified response.
+ */
+ protected void validateContentType(ResponseEntity response, MediaType expected) {
+ def actual = response.headers.getContentType()
+ assertTrue "Non compatible media-type, expected $expected, got $actual",
+ actual.isCompatibleWith(expected)
+ }
+
+
+ protected void validateMetadata(ResponseEntity 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 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 invokeHome(String userAgentHeader, String... acceptHeaders) {
+ execute('/', String, userAgentHeader, acceptHeaders)
+ }
+
+ protected ResponseEntity execute(String contextPath, Class responseType,
+ String userAgentHeader, String... acceptHeaders) {
+ HttpHeaders headers = new HttpHeaders();
+ if (userAgentHeader) {
+ headers.set("User-Agent", userAgentHeader);
+ }
+ if (acceptHeaders) {
+ List 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(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
+ }
+ }
+ }
+
+ }
+}
diff --git a/initializr-web/src/test/groovy/io/spring/initializr/web/project/MainControllerEnvIntegrationTests.groovy b/initializr-web/src/test/groovy/io/spring/initializr/web/project/MainControllerEnvIntegrationTests.groovy
index 4b638495..7e3d7e73 100644
--- a/initializr-web/src/test/groovy/io/spring/initializr/web/project/MainControllerEnvIntegrationTests.groovy
+++ b/initializr-web/src/test/groovy/io/spring/initializr/web/project/MainControllerEnvIntegrationTests.groovy
@@ -45,7 +45,7 @@ class MainControllerEnvIntegrationTests extends AbstractInitializrControllerInte
void doNotForceSsl() {
ResponseEntity 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://')
}
diff --git a/initializr-web/src/test/groovy/io/spring/initializr/web/project/MainControllerIntegrationTests.groovy b/initializr-web/src/test/groovy/io/spring/initializr/web/project/MainControllerIntegrationTests.groovy
index fe9c403d..9c741a8f 100644
--- a/initializr-web/src/test/groovy/io/spring/initializr/web/project/MainControllerIntegrationTests.groovy
+++ b/initializr-web/src/test/groovy/io/spring/initializr/web/project/MainControllerIntegrationTests.groovy
@@ -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 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 response = invokeHome(null, 'application/vnd.initializr.v2+json')
validateMetadata(response, InitializrMetadataVersion.V2.mediaType, '2.0.0', JSONCompareMode.STRICT)
}
diff --git a/initializr-web/src/test/groovy/io/spring/initializr/web/project/MainControllerServiceMetadataIntegrationTests.groovy b/initializr-web/src/test/groovy/io/spring/initializr/web/project/MainControllerServiceMetadataIntegrationTests.groovy
index 1d65e116..a9c24a19 100644
--- a/initializr-web/src/test/groovy/io/spring/initializr/web/project/MainControllerServiceMetadataIntegrationTests.groovy
+++ b/initializr-web/src/test/groovy/io/spring/initializr/web/project/MainControllerServiceMetadataIntegrationTests.groovy
@@ -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
diff --git a/initializr-web/src/test/groovy/io/spring/initializr/web/project/ProjectGenerationSmokeTests.groovy b/initializr-web/src/test/groovy/io/spring/initializr/web/project/ProjectGenerationSmokeTests.groovy
index 52839878..43d59588 100644
--- a/initializr-web/src/test/groovy/io/spring/initializr/web/project/ProjectGenerationSmokeTests.groovy
+++ b/initializr-web/src/test/groovy/io/spring/initializr/web/project/ProjectGenerationSmokeTests.groovy
@@ -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
diff --git a/initializr-web/src/test/groovy/io/spring/initializr/web/test/JsonFieldPath.java b/initializr-web/src/test/groovy/io/spring/initializr/web/test/JsonFieldPath.java
new file mode 100644
index 00000000..4a40f2ed
--- /dev/null
+++ b/initializr-web/src/test/groovy/io/spring/initializr/web/test/JsonFieldPath.java
@@ -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 segments;
+
+ private final boolean precise;
+
+ private final boolean array;
+
+ private JsonFieldPath(String rawPath, List 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 getSegments() {
+ return this.segments;
+ }
+
+ @Override
+ public String toString() {
+ return this.rawPath;
+ }
+
+ static JsonFieldPath compile(String path) {
+ List 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 segments) {
+ Iterator iterator = segments.iterator();
+ while (iterator.hasNext()) {
+ if (isArraySegment(iterator.next()) && iterator.hasNext()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static List extractSegments(String path) {
+ Matcher matcher = BRACKETS_AND_ARRAY_PATTERN.matcher(path);
+
+ int previous = 0;
+
+ List 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 extractDotSeparatedSegments(String path) {
+ List segments = new ArrayList<>();
+ for (String segment : path.split("\\.")) {
+ if (segment.length() > 0) {
+ segments.add(segment);
+ }
+ }
+ return segments;
+ }
+}
diff --git a/initializr-web/src/test/groovy/io/spring/initializr/web/test/JsonFieldProcessor.java b/initializr-web/src/test/groovy/io/spring/initializr/web/test/JsonFieldProcessor.java
new file mode 100644
index 00000000..c20929bf
--- /dev/null
+++ b/initializr-web/src/test/groovy/io/spring/initializr/web/test/JsonFieldProcessor.java
@@ -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 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
+
+ org.asciidoctor
+ asciidoctor-maven-plugin
+ 1.5.3
+
+
+ generate-docs
+ prepare-package
+ false
+
+ process-asciidoc
+
+
+ index.adoc
+ html
+ book
+
+ ${basedir}/initializr-web/target/snippets
+
+
+
+
+
diff --git a/src/main/asciidoc/configuration-format.adoc b/src/main/asciidoc/configuration-format.adoc
new file mode 100644
index 00000000..601052de
--- /dev/null
+++ b/src/main/asciidoc/configuration-format.adoc
@@ -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
+```
+
diff --git a/src/main/asciidoc/guidelines-for-3rd-party-starters.adoc b/src/main/asciidoc/guidelines-for-3rd-party-starters.adoc
new file mode 100644
index 00000000..cd05e352
--- /dev/null
+++ b/src/main/asciidoc/guidelines-for-3rd-party-starters.adoc
@@ -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.
\ No newline at end of file
diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc
new file mode 100644
index 00000000..2fb8db96
--- /dev/null
+++ b/src/main/asciidoc/index.adoc
@@ -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]
+
+// ======================================================================================
diff --git a/src/main/asciidoc/metadata-format.adoc b/src/main/asciidoc/metadata-format.adoc
new file mode 100644
index 00000000..e4fbb26d
--- /dev/null
+++ b/src/main/asciidoc/metadata-format.adoc
@@ -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.
diff --git a/src/main/asciidoc/using-the-stubs.adoc b/src/main/asciidoc/using-the-stubs.adoc
new file mode 100644
index 00000000..468e6b2e
--- /dev/null
+++ b/src/main/asciidoc/using-the-stubs.adoc
@@ -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"]
+----
+
+ io.spring.initializr
+ initializr
+ stubs
+ {project-version}
+ test
+
+----
+
+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.
\ No newline at end of file