Commit 9d0e50c6 authored by Stephane Nicoll's avatar Stephane Nicoll

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
parent 9af30450
...@@ -48,6 +48,19 @@ class InitializrService { ...@@ -48,6 +48,19 @@ class InitializrService {
private static final Charset UTF_8 = Charset.forName("UTF-8"); 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. * Late binding HTTP client.
*/ */
...@@ -80,12 +93,7 @@ class InitializrService { ...@@ -80,12 +93,7 @@ class InitializrService {
URI url = request.generateUrl(metadata); URI url = request.generateUrl(metadata);
CloseableHttpResponse httpResponse = executeProjectGenerationRequest(url); CloseableHttpResponse httpResponse = executeProjectGenerationRequest(url);
HttpEntity httpEntity = httpResponse.getEntity(); HttpEntity httpEntity = httpResponse.getEntity();
if (httpEntity == null) { validateResponse(httpResponse, request.getServiceUrl());
throw new ReportableException("No content received from server '" + url + "'");
}
if (httpResponse.getStatusLine().getStatusCode() != 200) {
throw createException(request.getServiceUrl(), httpResponse);
}
return createResponse(httpResponse, httpEntity); return createResponse(httpResponse, httpEntity);
} }
...@@ -97,15 +105,33 @@ class InitializrService { ...@@ -97,15 +105,33 @@ class InitializrService {
*/ */
public InitializrServiceMetadata loadMetadata(String serviceUrl) throws IOException { public InitializrServiceMetadata loadMetadata(String serviceUrl) throws IOException {
CloseableHttpResponse httpResponse = executeInitializrMetadataRetrieval(serviceUrl); CloseableHttpResponse httpResponse = executeInitializrMetadataRetrieval(serviceUrl);
if (httpResponse.getEntity() == null) { validateResponse(httpResponse, serviceUrl);
throw new ReportableException("No content received from server '" return parseJsonMetadata(httpResponse.getEntity());
+ serviceUrl + "'"); }
}
if (httpResponse.getStatusLine().getStatusCode() != 200) { /**
throw createException(serviceUrl, httpResponse); * 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 { try {
HttpEntity httpEntity = httpResponse.getEntity();
return new InitializrServiceMetadata(getContentAsJson(httpEntity)); return new InitializrServiceMetadata(getContentAsJson(httpEntity));
} }
catch (JSONException ex) { catch (JSONException ex) {
...@@ -114,6 +140,16 @@ class InitializrService { ...@@ -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, private ProjectGenerationResponse createResponse(CloseableHttpResponse httpResponse,
HttpEntity httpEntity) throws IOException { HttpEntity httpEntity) throws IOException {
ProjectGenerationResponse response = new ProjectGenerationResponse( ProjectGenerationResponse response = new ProjectGenerationResponse(
...@@ -139,11 +175,19 @@ class InitializrService { ...@@ -139,11 +175,19 @@ class InitializrService {
*/ */
private CloseableHttpResponse executeInitializrMetadataRetrieval(String url) { private CloseableHttpResponse executeInitializrMetadataRetrieval(String url) {
HttpGet request = new HttpGet(url); HttpGet request = new HttpGet(url);
request.setHeader(new BasicHeader(HttpHeaders.ACCEPT, request.setHeader(new BasicHeader(HttpHeaders.ACCEPT, ACCEPT_META_DATA));
"application/vnd.initializr.v2+json"));
return execute(request, url, "retrieve metadata"); 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, private CloseableHttpResponse execute(HttpUriRequest request, Object url,
String description) { String description) {
try { try {
...@@ -188,11 +232,15 @@ class InitializrService { ...@@ -188,11 +232,15 @@ class InitializrService {
} }
private JSONObject getContentAsJson(HttpEntity entity) throws IOException { private JSONObject getContentAsJson(HttpEntity entity) throws IOException {
return new JSONObject(getContent(entity));
}
private String getContent(HttpEntity entity) throws IOException {
ContentType contentType = ContentType.getOrDefault(entity); ContentType contentType = ContentType.getOrDefault(entity);
Charset charset = contentType.getCharset(); Charset charset = contentType.getCharset();
charset = (charset != null ? charset : UTF_8); charset = (charset != null ? charset : UTF_8);
byte[] content = FileCopyUtils.copyToByteArray(entity.getContent()); byte[] content = FileCopyUtils.copyToByteArray(entity.getContent());
return new JSONObject(new String(content, charset)); return new String(content, charset);
} }
private String extractFileName(Header header) { private String extractFileName(Header header) {
......
...@@ -54,7 +54,15 @@ class ServiceCapabilitiesReportGenerator { ...@@ -54,7 +54,15 @@ class ServiceCapabilitiesReportGenerator {
* @throws IOException if the report cannot be generated * @throws IOException if the report cannot be generated
*/ */
public String generate(String url) throws IOException { 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; String header = "Capabilities of " + url;
StringBuilder report = new StringBuilder(); StringBuilder report = new StringBuilder();
report.append(StringUtils.repeat("=", header.length()) + NEW_LINE); report.append(StringUtils.repeat("=", header.length()) + NEW_LINE);
......
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -48,37 +48,51 @@ public abstract class AbstractHttpClientMockTests { ...@@ -48,37 +48,51 @@ public abstract class AbstractHttpClientMockTests {
protected final CloseableHttpClient http = mock(CloseableHttpClient.class); protected final CloseableHttpClient http = mock(CloseableHttpClient.class);
protected void mockSuccessfulMetadataGet() throws IOException { protected void mockSuccessfulMetadataTextGet() throws IOException {
mockSuccessfulMetadataGet("2.0.0"); 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); CloseableHttpResponse response = mock(CloseableHttpResponse.class);
Resource resource = new ClassPathResource("metadata/service-metadata-" + version byte[] content = readClasspathResource(contentPath);
+ ".json"); mockHttpEntity(response, content, contentType);
byte[] content = StreamUtils.copyToByteArray(resource.getInputStream());
mockHttpEntity(response, content, "application/vnd.initializr.v2+json");
mockStatus(response, 200); 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( protected void mockSuccessfulProjectGeneration(
MockHttpProjectGenerationRequest request) throws IOException { MockHttpProjectGenerationRequest request) throws IOException {
// Required for project generation as the metadata is read first // Required for project generation as the metadata is read first
mockSuccessfulMetadataGet(); mockSuccessfulMetadataGet(false);
CloseableHttpResponse response = mock(CloseableHttpResponse.class); CloseableHttpResponse response = mock(CloseableHttpResponse.class);
mockHttpEntity(response, request.content, request.contentType); mockHttpEntity(response, request.content, request.contentType);
mockStatus(response, 200); mockStatus(response, 200);
String header = (request.fileName != null ? contentDispositionValue(request.fileName) String header = (request.fileName != null ? contentDispositionValue(request.fileName)
: null); : null);
mockHttpHeader(response, "Content-Disposition", header); 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) protected void mockProjectGenerationError(int status, String message)
throws IOException { throws IOException {
// Required for project generation as the metadata is read first // Required for project generation as the metadata is read first
mockSuccessfulMetadataGet(); mockSuccessfulMetadataGet(false);
CloseableHttpResponse response = mock(CloseableHttpResponse.class); CloseableHttpResponse response = mock(CloseableHttpResponse.class);
mockHttpEntity(response, createJsonError(status, message).getBytes(), mockHttpEntity(response, createJsonError(status, message).getBytes(),
"application/json"); "application/json");
...@@ -122,12 +136,17 @@ public abstract class AbstractHttpClientMockTests { ...@@ -122,12 +136,17 @@ public abstract class AbstractHttpClientMockTests {
given(response.getFirstHeader(headerName)).willReturn(header); given(response.getFirstHeader(headerName)).willReturn(header);
} }
protected Matcher<HttpGet> getForJsonMetadata() { private Matcher<HttpGet> getForMetadata(boolean serviceCapabilities) {
return new HasAcceptHeader("application/vnd.initializr.v2+json", true); if (serviceCapabilities) {
return new HasAcceptHeader(InitializrService.ACCEPT_SERVICE_CAPABILITIES, true);
}
else {
return new HasAcceptHeader(InitializrService.ACCEPT_META_DATA, true);
}
} }
protected Matcher<HttpGet> getForNonJsonMetadata() { private Matcher<HttpGet> getForNonMetadata() {
return new HasAcceptHeader("application/vnd.initializr.v2+json", false); return new HasAcceptHeader(InitializrService.ACCEPT_META_DATA, false);
} }
private String contentDispositionValue(String fileName) { private String contentDispositionValue(String fileName) {
......
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -73,9 +73,21 @@ public class InitCommandTests extends AbstractHttpClientMockTests { ...@@ -73,9 +73,21 @@ public class InitCommandTests extends AbstractHttpClientMockTests {
this.command = new InitCommand(this.handler); this.command = new InitCommand(this.handler);
} }
@Test
public void listServiceCapabilitiesText() throws Exception {
mockSuccessfulMetadataTextGet();
this.command.run("--list", "--target=http://fake-service");
}
@Test @Test
public void listServiceCapabilities() throws Exception { 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"); this.command.run("--list", "--target=http://fake-service");
} }
......
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -46,7 +46,7 @@ public class InitializrServiceTests extends AbstractHttpClientMockTests { ...@@ -46,7 +46,7 @@ public class InitializrServiceTests extends AbstractHttpClientMockTests {
@Test @Test
public void loadMetadata() throws IOException { public void loadMetadata() throws IOException {
mockSuccessfulMetadataGet(); mockSuccessfulMetadataGet(false);
InitializrServiceMetadata metadata = this.invoker.loadMetadata("http://foo/bar"); InitializrServiceMetadata metadata = this.invoker.loadMetadata("http://foo/bar");
assertNotNull(metadata); assertNotNull(metadata);
} }
...@@ -101,7 +101,7 @@ public class InitializrServiceTests extends AbstractHttpClientMockTests { ...@@ -101,7 +101,7 @@ public class InitializrServiceTests extends AbstractHttpClientMockTests {
@Test @Test
public void generateProjectNoContent() throws IOException { public void generateProjectNoContent() throws IOException {
mockSuccessfulMetadataGet(); mockSuccessfulMetadataGet(false);
CloseableHttpResponse response = mock(CloseableHttpResponse.class); CloseableHttpResponse response = mock(CloseableHttpResponse.class);
mockStatus(response, 500); mockStatus(response, 500);
when(this.http.execute(isA(HttpGet.class))).thenReturn(response); when(this.http.execute(isA(HttpGet.class))).thenReturn(response);
......
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -20,7 +20,9 @@ import java.io.IOException; ...@@ -20,7 +20,9 @@ import java.io.IOException;
import org.junit.Test; 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} * Tests for {@link ServiceCapabilitiesReportGenerator}
...@@ -32,14 +34,32 @@ public class ServiceCapabilitiesReportGeneratorTests extends AbstractHttpClientM ...@@ -32,14 +34,32 @@ public class ServiceCapabilitiesReportGeneratorTests extends AbstractHttpClientM
private final ServiceCapabilitiesReportGenerator command = new ServiceCapabilitiesReportGenerator( private final ServiceCapabilitiesReportGenerator command = new ServiceCapabilitiesReportGenerator(
new InitializrService(this.http)); 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 @Test
public void listMetadata() throws IOException { 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"); String content = this.command.generate("http://localhost");
assertTrue(content.contains("aop - AOP")); assertThat(content, containsString("aop - AOP"));
assertTrue(content.contains("security - Security: Security description")); assertThat(content, containsString("security - Security: Security description"));
assertTrue(content.contains("type: maven-project")); assertThat(content, containsString("type: maven-project"));
assertTrue(content.contains("packaging: jar")); assertThat(content, containsString("packaging: jar"));
} }
} }
{
"_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"
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment