Support of spring initializr meta-data v2.1

Update the `init` command to support the latest meta-data format. Recent
Spring Initializr version also supports Spring Boot CLI now and generates
a textual service capabilities when requested. The command no longer
generates the capabilities of the service unless said service does not
support it.

Closes gh-2515
pull/9264/head
Stephane Nicoll 10 years ago
parent 9af30450c4
commit 9d0e50c6ac

@ -48,6 +48,19 @@ class InitializrService {
private static final Charset UTF_8 = Charset.forName("UTF-8");
/**
* Accept header to use to retrieve the json meta-data.
*/
public static final String ACCEPT_META_DATA =
"application/vnd.initializr.v2.1+json,application/vnd.initializr.v2+json";
/**
* Accept header to use to retrieve the service capabilities of the service. If the
* service does not offer such feature, the json meta-data are retrieved instead.
*/
public static final String ACCEPT_SERVICE_CAPABILITIES =
"text/plain," + ACCEPT_META_DATA;
/**
* Late binding HTTP client.
*/
@ -80,12 +93,7 @@ class InitializrService {
URI url = request.generateUrl(metadata);
CloseableHttpResponse httpResponse = executeProjectGenerationRequest(url);
HttpEntity httpEntity = httpResponse.getEntity();
if (httpEntity == null) {
throw new ReportableException("No content received from server '" + url + "'");
}
if (httpResponse.getStatusLine().getStatusCode() != 200) {
throw createException(request.getServiceUrl(), httpResponse);
}
validateResponse(httpResponse, request.getServiceUrl());
return createResponse(httpResponse, httpEntity);
}
@ -97,15 +105,33 @@ class InitializrService {
*/
public InitializrServiceMetadata loadMetadata(String serviceUrl) throws IOException {
CloseableHttpResponse httpResponse = executeInitializrMetadataRetrieval(serviceUrl);
if (httpResponse.getEntity() == null) {
throw new ReportableException("No content received from server '"
+ serviceUrl + "'");
}
if (httpResponse.getStatusLine().getStatusCode() != 200) {
throw createException(serviceUrl, httpResponse);
validateResponse(httpResponse, serviceUrl);
return parseJsonMetadata(httpResponse.getEntity());
}
/**
* Loads the service capabilities of the service at the specified url.
* <p>If the service supports generating a textual representation of the
* capabilities, it is returned. Otherwhise the json meta-data as a
* {@link JSONObject} is returned.
* @param serviceUrl to url of the initializer service
* @return the service capabilities (as a String) or the metadata describing the service
* @throws IOException if the service capabilities cannot be loaded
*/
public Object loadServiceCapabilities(String serviceUrl) throws IOException {
CloseableHttpResponse httpResponse = executeServiceCapabilitiesRetrieval(serviceUrl);
validateResponse(httpResponse, serviceUrl);
HttpEntity httpEntity = httpResponse.getEntity();
ContentType contentType = ContentType.getOrDefault(httpEntity);
if (contentType.getMimeType().equals("text/plain")) {
return getContent(httpEntity);
} else {
return parseJsonMetadata(httpEntity);
}
}
private InitializrServiceMetadata parseJsonMetadata(HttpEntity httpEntity) throws IOException {
try {
HttpEntity httpEntity = httpResponse.getEntity();
return new InitializrServiceMetadata(getContentAsJson(httpEntity));
}
catch (JSONException ex) {
@ -114,6 +140,16 @@ class InitializrService {
}
}
private void validateResponse(CloseableHttpResponse httpResponse, String serviceUrl) {
if (httpResponse.getEntity() == null) {
throw new ReportableException("No content received from server '"
+ serviceUrl + "'");
}
if (httpResponse.getStatusLine().getStatusCode() != 200) {
throw createException(serviceUrl, httpResponse);
}
}
private ProjectGenerationResponse createResponse(CloseableHttpResponse httpResponse,
HttpEntity httpEntity) throws IOException {
ProjectGenerationResponse response = new ProjectGenerationResponse(
@ -139,11 +175,19 @@ class InitializrService {
*/
private CloseableHttpResponse executeInitializrMetadataRetrieval(String url) {
HttpGet request = new HttpGet(url);
request.setHeader(new BasicHeader(HttpHeaders.ACCEPT,
"application/vnd.initializr.v2+json"));
request.setHeader(new BasicHeader(HttpHeaders.ACCEPT, ACCEPT_META_DATA));
return execute(request, url, "retrieve metadata");
}
/**
* Retrieves the service capabilities of the service at the specified URL
*/
private CloseableHttpResponse executeServiceCapabilitiesRetrieval(String url) {
HttpGet request = new HttpGet(url);
request.setHeader(new BasicHeader(HttpHeaders.ACCEPT, ACCEPT_SERVICE_CAPABILITIES));
return execute(request, url, "retrieve help");
}
private CloseableHttpResponse execute(HttpUriRequest request, Object url,
String description) {
try {
@ -188,11 +232,15 @@ class InitializrService {
}
private JSONObject getContentAsJson(HttpEntity entity) throws IOException {
return new JSONObject(getContent(entity));
}
private String getContent(HttpEntity entity) throws IOException {
ContentType contentType = ContentType.getOrDefault(entity);
Charset charset = contentType.getCharset();
charset = (charset != null ? charset : UTF_8);
byte[] content = FileCopyUtils.copyToByteArray(entity.getContent());
return new JSONObject(new String(content, charset));
return new String(content, charset);
}
private String extractFileName(Header header) {

@ -54,7 +54,15 @@ class ServiceCapabilitiesReportGenerator {
* @throws IOException if the report cannot be generated
*/
public String generate(String url) throws IOException {
InitializrServiceMetadata metadata = this.initializrService.loadMetadata(url);
Object content = this.initializrService.loadServiceCapabilities(url);
if (content instanceof InitializrServiceMetadata) {
return generateHelp(url, (InitializrServiceMetadata) content);
} else {
return content.toString();
}
}
private String generateHelp(String url, InitializrServiceMetadata metadata) {
String header = "Capabilities of " + url;
StringBuilder report = new StringBuilder();
report.append(StringUtils.repeat("=", header.length()) + NEW_LINE);

@ -1,5 +1,5 @@
/*
* Copyright 2012-2014 the original author or authors.
* 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.
@ -48,37 +48,51 @@ public abstract class AbstractHttpClientMockTests {
protected final CloseableHttpClient http = mock(CloseableHttpClient.class);
protected void mockSuccessfulMetadataGet() throws IOException {
mockSuccessfulMetadataGet("2.0.0");
protected void mockSuccessfulMetadataTextGet() throws IOException {
mockSuccessfulMetadataGet("metadata/service-metadata-2.1.0.txt", "text/plain", true);
}
protected void mockSuccessfulMetadataGet(String version) throws IOException {
protected void mockSuccessfulMetadataGet(boolean serviceCapabilities) throws IOException {
mockSuccessfulMetadataGet("metadata/service-metadata-2.1.0.json",
"application/vnd.initializr.v2.1+json", serviceCapabilities);
}
protected void mockSuccessfulMetadataGetV2(boolean serviceCapabilities) throws IOException {
mockSuccessfulMetadataGet("metadata/service-metadata-2.0.0.json",
"application/vnd.initializr.v2+json", serviceCapabilities);
}
protected void mockSuccessfulMetadataGet(String contentPath, String contentType,
boolean serviceCapabilities) throws IOException {
CloseableHttpResponse response = mock(CloseableHttpResponse.class);
Resource resource = new ClassPathResource("metadata/service-metadata-" + version
+ ".json");
byte[] content = StreamUtils.copyToByteArray(resource.getInputStream());
mockHttpEntity(response, content, "application/vnd.initializr.v2+json");
byte[] content = readClasspathResource(contentPath);
mockHttpEntity(response, content, contentType);
mockStatus(response, 200);
given(this.http.execute(argThat(getForJsonMetadata()))).willReturn(response);
given(this.http.execute(argThat(getForMetadata(serviceCapabilities)))).willReturn(response);
}
protected byte[] readClasspathResource(String contentPath) throws IOException {
Resource resource = new ClassPathResource(contentPath);
return StreamUtils.copyToByteArray(resource.getInputStream());
}
protected void mockSuccessfulProjectGeneration(
MockHttpProjectGenerationRequest request) throws IOException {
// Required for project generation as the metadata is read first
mockSuccessfulMetadataGet();
mockSuccessfulMetadataGet(false);
CloseableHttpResponse response = mock(CloseableHttpResponse.class);
mockHttpEntity(response, request.content, request.contentType);
mockStatus(response, 200);
String header = (request.fileName != null ? contentDispositionValue(request.fileName)
: null);
mockHttpHeader(response, "Content-Disposition", header);
given(this.http.execute(argThat(getForNonJsonMetadata()))).willReturn(response);
given(this.http.execute(argThat(getForNonMetadata()))).willReturn(response);
}
protected void mockProjectGenerationError(int status, String message)
throws IOException {
// Required for project generation as the metadata is read first
mockSuccessfulMetadataGet();
mockSuccessfulMetadataGet(false);
CloseableHttpResponse response = mock(CloseableHttpResponse.class);
mockHttpEntity(response, createJsonError(status, message).getBytes(),
"application/json");
@ -122,12 +136,17 @@ public abstract class AbstractHttpClientMockTests {
given(response.getFirstHeader(headerName)).willReturn(header);
}
protected Matcher<HttpGet> getForJsonMetadata() {
return new HasAcceptHeader("application/vnd.initializr.v2+json", true);
private Matcher<HttpGet> getForMetadata(boolean serviceCapabilities) {
if (serviceCapabilities) {
return new HasAcceptHeader(InitializrService.ACCEPT_SERVICE_CAPABILITIES, true);
}
else {
return new HasAcceptHeader(InitializrService.ACCEPT_META_DATA, true);
}
}
protected Matcher<HttpGet> getForNonJsonMetadata() {
return new HasAcceptHeader("application/vnd.initializr.v2+json", false);
private Matcher<HttpGet> getForNonMetadata() {
return new HasAcceptHeader(InitializrService.ACCEPT_META_DATA, false);
}
private String contentDispositionValue(String fileName) {

@ -1,5 +1,5 @@
/*
* Copyright 2012-2014 the original author or authors.
* 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.
@ -73,9 +73,21 @@ public class InitCommandTests extends AbstractHttpClientMockTests {
this.command = new InitCommand(this.handler);
}
@Test
public void listServiceCapabilitiesText() throws Exception {
mockSuccessfulMetadataTextGet();
this.command.run("--list", "--target=http://fake-service");
}
@Test
public void listServiceCapabilities() throws Exception {
mockSuccessfulMetadataGet();
mockSuccessfulMetadataGet(true);
this.command.run("--list", "--target=http://fake-service");
}
@Test
public void listServiceCapabilitiesV2() throws Exception {
mockSuccessfulMetadataGetV2(true);
this.command.run("--list", "--target=http://fake-service");
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2014 the original author or authors.
* 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.
@ -46,7 +46,7 @@ public class InitializrServiceTests extends AbstractHttpClientMockTests {
@Test
public void loadMetadata() throws IOException {
mockSuccessfulMetadataGet();
mockSuccessfulMetadataGet(false);
InitializrServiceMetadata metadata = this.invoker.loadMetadata("http://foo/bar");
assertNotNull(metadata);
}
@ -101,7 +101,7 @@ public class InitializrServiceTests extends AbstractHttpClientMockTests {
@Test
public void generateProjectNoContent() throws IOException {
mockSuccessfulMetadataGet();
mockSuccessfulMetadataGet(false);
CloseableHttpResponse response = mock(CloseableHttpResponse.class);
mockStatus(response, 500);
when(this.http.execute(isA(HttpGet.class))).thenReturn(response);

@ -1,5 +1,5 @@
/*
* Copyright 2012-2014 the original author or authors.
* 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.
@ -20,7 +20,9 @@ import java.io.IOException;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.core.StringContains.containsString;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link ServiceCapabilitiesReportGenerator}
@ -32,14 +34,32 @@ public class ServiceCapabilitiesReportGeneratorTests extends AbstractHttpClientM
private final ServiceCapabilitiesReportGenerator command = new ServiceCapabilitiesReportGenerator(
new InitializrService(this.http));
@Test
public void listMetadataFromServer() throws IOException {
mockSuccessfulMetadataTextGet();
String expected = new String(readClasspathResource("metadata/service-metadata-2.1.0.txt"));
String content = this.command.generate("http://localhost");
assertThat(content, equalTo(expected));
}
@Test
public void listMetadata() throws IOException {
mockSuccessfulMetadataGet();
mockSuccessfulMetadataGet(true);
doTestGenerateCapabilitiesFromJson();
}
@Test
public void listMetadataV2() throws IOException {
mockSuccessfulMetadataGetV2(true);
doTestGenerateCapabilitiesFromJson();
}
private void doTestGenerateCapabilitiesFromJson() throws IOException {
String content = this.command.generate("http://localhost");
assertTrue(content.contains("aop - AOP"));
assertTrue(content.contains("security - Security: Security description"));
assertTrue(content.contains("type: maven-project"));
assertTrue(content.contains("packaging: jar"));
assertThat(content, containsString("aop - AOP"));
assertThat(content, containsString("security - Security: Security description"));
assertThat(content, containsString("type: maven-project"));
assertThat(content, containsString("packaging: jar"));
}
}

@ -0,0 +1,191 @@
{
"_links": {
"maven-build": {
"href": "http://localhost:8080/pom.xml?type=maven-build{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
"templated": true
},
"maven-project": {
"href": "http://localhost:8080/starter.zip?type=maven-project{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
"templated": true
},
"gradle-build": {
"href": "http://localhost:8080/build.gradle?type=gradle-build{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
"templated": true
},
"gradle-project": {
"href": "http://localhost:8080/starter.zip?type=gradle-project{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
"templated": true
}
},
"dependencies": {
"type": "hierarchical-multi-select",
"values": [
{
"name": "Core",
"values": [
{
"name": "Security",
"id": "security",
"description": "Security description"
},
{
"name": "AOP",
"id": "aop"
}
]
},
{
"name": "Data",
"values": [
{
"name": "JDBC",
"id": "jdbc"
},
{
"name": "JPA",
"id": "data-jpa"
},
{
"name": "MongoDB",
"id": "data-mongodb",
"versionRange": "1.1.7.RELEASE"
}
]
}
]
},
"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.8.RELEASE",
"values": [
{
"id": "1.2.0.BUILD-SNAPSHOT",
"name": "1.2.0 (SNAPSHOT)"
},
{
"id": "1.1.8.RELEASE",
"name": "1.1.8"
},
{
"id": "1.1.8.BUILD-SNAPSHOT",
"name": "1.1.8 (SNAPSHOT)"
},
{
"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"
}
}
Loading…
Cancel
Save