diff --git a/initializr/pom.xml b/initializr/pom.xml
index 73e77248..e424cb20 100644
--- a/initializr/pom.xml
+++ b/initializr/pom.xml
@@ -30,6 +30,10 @@
com.fasterxml.jackson.core
jackson-core
+
+ org.springframework.hateoas
+ spring-hateoas
+
com.google.guava
guava
diff --git a/initializr/src/main/groovy/io/spring/initializr/InitializrMetadata.groovy b/initializr/src/main/groovy/io/spring/initializr/InitializrMetadata.groovy
index f80c5867..39a496dd 100644
--- a/initializr/src/main/groovy/io/spring/initializr/InitializrMetadata.groovy
+++ b/initializr/src/main/groovy/io/spring/initializr/InitializrMetadata.groovy
@@ -66,6 +66,8 @@ class InitializrMetadata {
@JsonIgnore
private final Map indexedDependencies = [:]
+ private final transient InitializrMetadataJsonMapper jsonMapper = new InitializrMetadataJsonMapper()
+
/**
* Return the {@link Dependency} with the specified id or {@code null} if
* no such dependency exists.
@@ -121,6 +123,15 @@ class InitializrMetadata {
refreshDefaults()
}
+ /**
+ * Generate a JSON representation of the current metadata
+ *
+ * @param appUrl the application url
+ */
+ String generateJson(String appUrl) {
+ jsonMapper.write(this, appUrl)
+ }
+
/**
* Initialize and validate the configuration.
*/
@@ -261,6 +272,14 @@ class InitializrMetadata {
String action
+ void setAction(String action) {
+ String actionToUse = action
+ if (!actionToUse.startsWith("/")) {
+ actionToUse = "/" + actionToUse
+ }
+ this.action = actionToUse
+ }
+
final Map tags = [:]
}
diff --git a/initializr/src/main/groovy/io/spring/initializr/InitializrMetadataJsonMapper.groovy b/initializr/src/main/groovy/io/spring/initializr/InitializrMetadataJsonMapper.groovy
new file mode 100644
index 00000000..f8b6334c
--- /dev/null
+++ b/initializr/src/main/groovy/io/spring/initializr/InitializrMetadataJsonMapper.groovy
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2012-2014 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
+
+import groovy.json.JsonBuilder
+
+import org.springframework.hateoas.TemplateVariable
+import org.springframework.hateoas.TemplateVariables
+import org.springframework.hateoas.UriTemplate
+
+/**
+ * Generate a JSON representation of the metadata.
+ *
+ * @author Stephane Nicoll
+ * @since 1.0
+ */
+class InitializrMetadataJsonMapper {
+
+ private final TemplateVariables templateVariables
+
+ InitializrMetadataJsonMapper() {
+ this.templateVariables = new TemplateVariables(
+ new TemplateVariable('type', TemplateVariable.VariableType.REQUEST_PARAM),
+ new TemplateVariable('packaging', TemplateVariable.VariableType.REQUEST_PARAM),
+ new TemplateVariable('javaVersion', TemplateVariable.VariableType.REQUEST_PARAM),
+ new TemplateVariable('language', TemplateVariable.VariableType.REQUEST_PARAM),
+ new TemplateVariable('bootVersion', TemplateVariable.VariableType.REQUEST_PARAM),
+ new TemplateVariable('groupId', TemplateVariable.VariableType.REQUEST_PARAM),
+ new TemplateVariable('artifactId', TemplateVariable.VariableType.REQUEST_PARAM),
+ new TemplateVariable('version', TemplateVariable.VariableType.REQUEST_PARAM),
+ new TemplateVariable('name', TemplateVariable.VariableType.REQUEST_PARAM),
+ new TemplateVariable('description', TemplateVariable.VariableType.REQUEST_PARAM),
+ new TemplateVariable('packageName', TemplateVariable.VariableType.REQUEST_PARAM)
+ )
+ }
+
+ String write(InitializrMetadata metadata, String appUrl) {
+ JsonBuilder json = new JsonBuilder()
+ json {
+ links(delegate, metadata.types, appUrl)
+ dependencies(delegate, metadata.dependencies)
+ type(delegate, metadata.defaults.type, metadata.types)
+ singleSelect(delegate, 'packaging', metadata.defaults.packaging, metadata.packagings)
+ singleSelect(delegate, 'javaVersion', metadata.defaults.javaVersion, metadata.javaVersions)
+ singleSelect(delegate, 'language', metadata.defaults.language, metadata.languages)
+ singleSelect(delegate, 'bootVersion', metadata.defaults.bootVersion, metadata.bootVersions)
+ text(delegate, 'groupId', metadata.defaults.groupId)
+ text(delegate, 'artifactId', metadata.defaults.artifactId)
+ text(delegate, 'version', metadata.defaults.version)
+ text(delegate, 'name', metadata.defaults.name)
+ text(delegate, 'description', metadata.defaults.description)
+ text(delegate, 'packageName', metadata.defaults.packageName)
+ }
+ json.toString()
+ }
+
+ private links(parent, types, appUrl) {
+ def content = [:]
+ types.each {
+ content[it.id] = link(appUrl, it)
+ }
+ parent._links content
+ }
+
+ private link(appUrl, type) {
+ def result = [:]
+ result.href = generateTemplatedUri(appUrl, type.action)
+ result.templated = true
+ result
+ }
+
+ private generateTemplatedUri(appUrl, action) {
+ String uri = appUrl != null ? appUrl + action : action
+ UriTemplate uriTemplate = new UriTemplate(uri + '?style={dependencies}', this.templateVariables)
+ uriTemplate.toString()
+ }
+
+
+ private static dependencies(parent, groups) {
+ parent.dependencies {
+ type 'hierarchical-multi-select'
+ values groups.collect {
+ processDependencyGroup(it)
+ }
+ }
+
+ }
+
+ private static type(parent, defaultValue, dependencies) {
+ parent.type {
+ type 'action'
+ if (defaultValue) {
+ 'default' defaultValue
+ }
+ values dependencies.collect {
+ processType(it)
+ }
+ }
+ }
+
+ private static singleSelect(parent, name, defaultValue, itemValues) {
+ parent."$name" {
+ type 'single-select'
+ if (defaultValue) {
+ 'default' defaultValue
+ }
+ values itemValues.collect {
+ processValue(it)
+ }
+ }
+ }
+
+ private static text(parent, name, value) {
+ parent."$name" {
+ type 'text'
+ if (value) {
+ 'default' value
+ }
+ }
+ }
+
+
+ private static processDependencyGroup(group) {
+ def result = [:]
+ result.name = group.name
+ if (group.hasProperty('description') && group.description) {
+ result.description = group.description
+ }
+ def items = []
+ group.content.collect {
+ items << processValue(it)
+ }
+ result.values = items
+ result
+ }
+
+ private static processType(type) {
+ def result = processValue(type)
+ result.action = type.action
+ result.tags = type.tags
+ result
+ }
+
+ private static processValue(value) {
+ def result = [:]
+ result.id = value.id
+ result.name = value.name
+ if (value.hasProperty('description') && value.description) {
+ result.description = value.description
+ }
+ result
+ }
+
+}
diff --git a/initializr/src/main/groovy/io/spring/initializr/web/MainController.groovy b/initializr/src/main/groovy/io/spring/initializr/web/MainController.groovy
index d38af95e..0a27bdc8 100644
--- a/initializr/src/main/groovy/io/spring/initializr/web/MainController.groovy
+++ b/initializr/src/main/groovy/io/spring/initializr/web/MainController.groovy
@@ -29,6 +29,7 @@ import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.ModelAttribute
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.ResponseBody
+import org.springframework.web.servlet.support.ServletUriComponentsBuilder
/**
* The main initializr controller provides access to the configured
@@ -53,12 +54,20 @@ class MainController extends AbstractInitializrController {
request
}
- @RequestMapping(value = "/")
+ @RequestMapping(value = "/", headers = "user-agent=SpringBootCli/1.2.0.RC1")
@ResponseBody
- InitializrMetadata metadata() {
+ @Deprecated
+ InitializrMetadata oldMetadata() {
metadataProvider.get()
}
+ @RequestMapping(value = "/", produces = ["application/vnd.initializr.v2+json","application/json"])
+ @ResponseBody
+ String metadata() {
+ String appUrl = ServletUriComponentsBuilder.fromCurrentServletMapping().build()
+ metadataProvider.get().generateJson(appUrl)
+ }
+
@RequestMapping(value = '/', produces = 'text/html')
@ResponseBody
String home() {
diff --git a/initializr/src/test/groovy/io/spring/initializr/InitializrMetadataJsonMapperTests.groovy b/initializr/src/test/groovy/io/spring/initializr/InitializrMetadataJsonMapperTests.groovy
new file mode 100644
index 00000000..c6a0d4ed
--- /dev/null
+++ b/initializr/src/test/groovy/io/spring/initializr/InitializrMetadataJsonMapperTests.groovy
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2012-2014 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
+
+import groovy.json.JsonSlurper
+import io.spring.initializr.support.InitializrMetadataBuilder
+import org.junit.Test
+
+import static org.junit.Assert.assertEquals
+
+/**
+ * @author Stephane Nicoll
+ */
+class InitializrMetadataJsonMapperTests {
+
+ private final InitializrMetadataJsonMapper jsonMapper = new InitializrMetadataJsonMapper()
+ private final JsonSlurper slurper = new JsonSlurper()
+
+ @Test
+ void withNoAppUrl() {
+ InitializrMetadata metadata = new InitializrMetadataBuilder().addType('foo', true, '/foo.zip', 'none', 'test')
+ .addDependencyGroup('foo', 'one', 'two').validateAndGet()
+ def json = jsonMapper.write(metadata, null)
+ def result = slurper.parseText(json)
+ assertEquals '/foo.zip?style={dependencies}{&type,packaging,javaVersion,language,bootVersion,' +
+ 'groupId,artifactId,version,name,description,packageName}', result._links.foo.href
+ }
+
+ @Test
+ void withAppUrl() {
+ InitializrMetadata metadata = new InitializrMetadataBuilder().addType('foo', true, '/foo.zip', 'none', 'test')
+ .addDependencyGroup('foo', 'one', 'two').validateAndGet()
+ def json = jsonMapper.write(metadata, 'http://server:8080/my-app')
+ def result = slurper.parseText(json)
+ assertEquals 'http://server:8080/my-app/foo.zip?style={dependencies}{&type,packaging,javaVersion,' +
+ 'language,bootVersion,groupId,artifactId,version,name,description,packageName}',
+ result._links.foo.href
+ }
+
+}
diff --git a/initializr/src/test/groovy/io/spring/initializr/InitializrMetadataTests.groovy b/initializr/src/test/groovy/io/spring/initializr/InitializrMetadataTests.groovy
index ec0dcee4..96994e91 100644
--- a/initializr/src/test/groovy/io/spring/initializr/InitializrMetadataTests.groovy
+++ b/initializr/src/test/groovy/io/spring/initializr/InitializrMetadataTests.groovy
@@ -177,6 +177,13 @@ class InitializrMetadataTests {
assertEquals metadata.defaults.groupId, request.groupId
}
+ @Test
+ void validateAction() {
+ def metadata = new InitializrMetadataBuilder()
+ .addType('foo', false, 'my-action.zip', 'none', 'none').validateAndGet()
+ assertEquals '/my-action.zip', metadata.getType('foo').action
+ }
+
@Test
void validateArtifactRepository() {
def metadata = InitializrMetadataBuilder.withDefaults().instance()
diff --git a/initializr/src/test/groovy/io/spring/initializr/web/AbstractInitializrControllerIntegrationTests.groovy b/initializr/src/test/groovy/io/spring/initializr/web/AbstractInitializrControllerIntegrationTests.groovy
index 883f4499..dae97a8d 100644
--- a/initializr/src/test/groovy/io/spring/initializr/web/AbstractInitializrControllerIntegrationTests.groovy
+++ b/initializr/src/test/groovy/io/spring/initializr/web/AbstractInitializrControllerIntegrationTests.groovy
@@ -50,7 +50,7 @@ abstract class AbstractInitializrControllerIntegrationTests {
public final TemporaryFolder folder = new TemporaryFolder()
@Value('${local.server.port}')
- private int port
+ protected int port
final RestTemplate restTemplate = new RestTemplate()
diff --git a/initializr/src/test/groovy/io/spring/initializr/web/MainControllerIntegrationTests.groovy b/initializr/src/test/groovy/io/spring/initializr/web/MainControllerIntegrationTests.groovy
index b9009e8f..914cf62f 100644
--- a/initializr/src/test/groovy/io/spring/initializr/web/MainControllerIntegrationTests.groovy
+++ b/initializr/src/test/groovy/io/spring/initializr/web/MainControllerIntegrationTests.groovy
@@ -26,7 +26,11 @@ import org.skyscreamer.jsonassert.JSONAssert
import org.skyscreamer.jsonassert.JSONCompareMode
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.HttpStatus
+import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.test.context.ActiveProfiles
import org.springframework.util.StreamUtils
@@ -40,8 +44,10 @@ import static org.junit.Assert.*
@ActiveProfiles('test-default')
class MainControllerIntegrationTests extends AbstractInitializrControllerIntegrationTests {
- private final def slurper = new JsonSlurper()
+ private static final MediaType CURRENT_METADATA_MEDIA_TYPE =
+ MediaType.parseMediaType('application/vnd.initializr.v2+json')
+ private final def slurper = new JsonSlurper()
@Test
void simpleZipProject() {
@@ -104,25 +110,50 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra
}
+ @Test
+ void metadataWithNoAcceptHeader() { // rest template sets application/json by default
+ ResponseEntity response = getMetadata(null, '*/*')
+ assertEquals CURRENT_METADATA_MEDIA_TYPE, response.getHeaders().getContentType()
+ validateCurrentMetadata(new JSONObject(response.body))
+ }
+
+ @Test
+ void metadataWithCurrentAcceptHeader() {
+ ResponseEntity response = getMetadata(null, 'application/vnd.initializr.v2+json')
+ assertEquals CURRENT_METADATA_MEDIA_TYPE, response.getHeaders().getContentType()
+ validateCurrentMetadata(new JSONObject(response.body))
+ }
+
@Test // Test that the current output is exactly what we expect
void validateCurrentProjectMetadata() {
- def json = restTemplate.getForObject(createUrl('/'), String.class)
- def expected = readJson('1.1.0')
- JSONAssert.assertEquals(expected, new JSONObject(json), JSONCompareMode.STRICT)
+ def json = getMetadataJson()
+ validateCurrentMetadata(json)
+ }
+
+ private void validateCurrentMetadata(JSONObject json) {
+ def expected = readJson('2.0.0')
+ JSONAssert.assertEquals(expected, json, JSONCompareMode.STRICT)
+ }
+
+ @Test // Test that the current code complies exactly with 1.1.0
+ void validateProjectMetadata110() {
+ JSONObject json = getMetadataJson("SpringBootCli/1.2.0.RC1", null)
+ def expected = readJson('1.0.1')
+ JSONAssert.assertEquals(expected, json, JSONCompareMode.LENIENT)
}
@Test // Test that the current code complies "at least" with 1.0.1
void validateProjectMetadata101() {
- def json = restTemplate.getForObject(createUrl('/'), String.class)
+ JSONObject json = getMetadataJson("SpringBootCli/1.2.0.RC1", null)
def expected = readJson('1.0.1')
- JSONAssert.assertEquals(expected, new JSONObject(json), JSONCompareMode.LENIENT)
+ JSONAssert.assertEquals(expected, json, JSONCompareMode.LENIENT)
}
@Test // Test that the current code complies "at least" with 1.0.0
void validateProjectMetadata100() {
- def json = restTemplate.getForObject(createUrl('/'), String.class)
+ JSONObject json = getMetadataJson("SpringBootCli/1.2.0.RC1", null)
def expected = readJson('1.0.0')
- JSONAssert.assertEquals(expected, new JSONObject(json), JSONCompareMode.LENIENT)
+ JSONAssert.assertEquals(expected, json, JSONCompareMode.LENIENT)
}
@Test
@@ -186,7 +217,7 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra
@Test
void homeIsJson() {
def body = restTemplate.getForObject(createUrl('/'), String)
- assertTrue("Wrong body:\n$body", body.contains('{"dependencies"'))
+ assertTrue("Wrong body:\n$body", body.contains('"dependencies"'))
}
@Test
@@ -237,6 +268,27 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra
assertNotNull(response.body)
}
+ private JSONObject getMetadataJson() {
+ getMetadataJson(null, null)
+ }
+
+ private JSONObject getMetadataJson(String userAgentHeader, String acceptHeader) {
+ String json = getMetadata(userAgentHeader, acceptHeader).body
+ return new JSONObject(json)
+ }
+
+ private ResponseEntity getMetadata(String userAgentHeader, String acceptHeader) {
+ HttpHeaders headers = new HttpHeaders();
+ if (userAgentHeader) {
+ headers.set("User-Agent", userAgentHeader);
+ }
+ if (acceptHeader) {
+ headers.setAccept(Collections.singletonList(MediaType.parseMediaType(acceptHeader)))
+ }
+ return restTemplate.exchange(createUrl('/'),
+ HttpMethod.GET, new HttpEntity(headers), String.class)
+ }
+
private byte[] invoke(String context) {
restTemplate.getForObject(createUrl(context), byte[])
@@ -252,12 +304,15 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra
tgzProjectAssert(body)
}
- private static JSONObject readJson(String version) {
+ private JSONObject readJson(String version) {
def resource = new ClassPathResource("metadata/test-default-$version" + ".json")
def stream = resource.inputStream
try {
def json = StreamUtils.copyToString(stream, Charset.forName('UTF-8'))
- new JSONObject(json)
+
+ // Let's parse the port as it is random
+ def content = json.replaceAll('@port@', String.valueOf(this.port))
+ new JSONObject(content)
} finally {
stream.close()
}
diff --git a/initializr/src/test/resources/metadata/test-default-2.0.0.json b/initializr/src/test/resources/metadata/test-default-2.0.0.json
new file mode 100644
index 00000000..de896065
--- /dev/null
+++ b/initializr/src/test/resources/metadata/test-default-2.0.0.json
@@ -0,0 +1,186 @@
+{
+ "_links": {
+ "maven-build": {
+ "href": "http://localhost:@port@/pom.xml?style={dependencies}{&type,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
+ "templated": true
+ },
+ "maven-project": {
+ "href": "http://localhost:@port@/starter.zip?style={dependencies}{&type,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
+ "templated": true
+ },
+ "gradle-build": {
+ "href": "http://localhost:@port@/build.gradle?style={dependencies}{&type,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
+ "templated": true
+ },
+ "gradle-project": {
+ "href": "http://localhost:@port@/starter.zip?style={dependencies}{&type,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
+ "templated": true
+ }
+ },
+ "dependencies": {
+ "type": "hierarchical-multi-select",
+ "values": [
+ {
+ "name": "Core",
+ "values": [
+ {
+ "id": "web",
+ "name": "Web",
+ "description": "Web dependency description"
+ },
+ {
+ "id": "security",
+ "name": "Security"
+ },
+ {
+ "id": "data-jpa",
+ "name": "Data JPA"
+ }
+ ]
+ },
+ {
+ "name": "Other",
+ "values": [
+ {
+ "id": "org.acme:foo",
+ "name": "Foo"
+ },
+ {
+ "id": "org.acme:bar",
+ "name": "Bar"
+ }
+ ]
+ }
+ ]
+ },
+ "type": {
+ "type": "action",
+ "default": "maven-project",
+ "values": [
+ {
+ "id": "maven-build",
+ "name": "Maven POM",
+ "action": "/pom.xml",
+ "tags": {
+ "build": "maven",
+ "format": "build"
+ }
+ },
+ {
+ "id": "maven-project",
+ "name": "Maven Project",
+ "action": "/starter.zip",
+ "tags": {
+ "build": "maven",
+ "format": "project"
+ }
+ },
+ {
+ "id": "gradle-build",
+ "name": "Gradle Config",
+ "action": "/build.gradle",
+ "tags": {
+ "build": "gradle",
+ "format": "build"
+ }
+ },
+ {
+ "id": "gradle-project",
+ "name": "Gradle Project",
+ "action": "/starter.zip",
+ "tags": {
+ "build": "gradle",
+ "format": "project"
+ }
+ }
+ ]
+ },
+ "packaging": {
+ "type": "single-select",
+ "default": "jar",
+ "values": [
+ {
+ "id": "jar",
+ "name": "Jar"
+ },
+ {
+ "id": "war",
+ "name": "War"
+ }
+ ]
+ },
+ "javaVersion": {
+ "type": "single-select",
+ "default": "1.7",
+ "values": [
+ {
+ "id": "1.6",
+ "name": "1.6"
+ },
+ {
+ "id": "1.7",
+ "name": "1.7"
+ },
+ {
+ "id": "1.8",
+ "name": "1.8"
+ }
+ ]
+ },
+ "language": {
+ "type": "single-select",
+ "default": "java",
+ "values": [
+ {
+ "id": "groovy",
+ "name": "Groovy"
+ },
+ {
+ "id": "java",
+ "name": "Java"
+ }
+ ]
+ },
+ "bootVersion": {
+ "type": "single-select",
+ "default": "1.1.4.RELEASE",
+ "values": [
+ {
+ "id": "1.2.0.BUILD-SNAPSHOT",
+ "name": "Latest SNAPSHOT"
+ },
+ {
+ "id": "1.1.4.RELEASE",
+ "name": "1.1.4"
+ },
+ {
+ "id": "1.0.2.RELEASE",
+ "name": "1.0.2"
+ }
+ ]
+ },
+ "groupId": {
+ "type": "text",
+ "default": "org.test"
+ },
+ "artifactId": {
+ "type": "text",
+ "default": "demo"
+ },
+ "version": {
+ "type": "text",
+ "default": "0.0.1-SNAPSHOT"
+ },
+ "name": {
+ "type": "text",
+ "default": "demo"
+ },
+ "description": {
+ "type": "text",
+ "default": "Demo project for Spring Boot"
+ },
+ "packageName": {
+ "type": "text",
+ "default": "demo"
+ }
+}
\ No newline at end of file