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
This commit is contained in:
@@ -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,6 +105,42 @@ class InitializrService {
|
||||
*/
|
||||
public InitializrServiceMetadata loadMetadata(String serviceUrl) throws IOException {
|
||||
CloseableHttpResponse httpResponse = executeInitializrMetadataRetrieval(serviceUrl);
|
||||
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 {
|
||||
return new InitializrServiceMetadata(getContentAsJson(httpEntity));
|
||||
}
|
||||
catch (JSONException ex) {
|
||||
throw new ReportableException("Invalid content received from server ("
|
||||
+ ex.getMessage() + ")", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateResponse(CloseableHttpResponse httpResponse, String serviceUrl) {
|
||||
if (httpResponse.getEntity() == null) {
|
||||
throw new ReportableException("No content received from server '"
|
||||
+ serviceUrl + "'");
|
||||
@@ -104,14 +148,6 @@ class InitializrService {
|
||||
if (httpResponse.getStatusLine().getStatusCode() != 200) {
|
||||
throw createException(serviceUrl, httpResponse);
|
||||
}
|
||||
try {
|
||||
HttpEntity httpEntity = httpResponse.getEntity();
|
||||
return new InitializrServiceMetadata(getContentAsJson(httpEntity));
|
||||
}
|
||||
catch (JSONException ex) {
|
||||
throw new ReportableException("Invalid content received from server ("
|
||||
+ ex.getMessage() + ")", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private ProjectGenerationResponse createResponse(CloseableHttpResponse httpResponse,
|
||||
@@ -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}
|
||||
@@ -33,13 +35,31 @@ public class ServiceCapabilitiesReportGeneratorTests extends AbstractHttpClientM
|
||||
new InitializrService(this.http));
|
||||
|
||||
@Test
|
||||
public void listMetadata() throws IOException {
|
||||
mockSuccessfulMetadataGet();
|
||||
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");
|
||||
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, equalTo(expected));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listMetadata() throws IOException {
|
||||
mockSuccessfulMetadataGet(true);
|
||||
doTestGenerateCapabilitiesFromJson();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listMetadataV2() throws IOException {
|
||||
mockSuccessfulMetadataGetV2(true);
|
||||
doTestGenerateCapabilitiesFromJson();
|
||||
}
|
||||
|
||||
private void doTestGenerateCapabilitiesFromJson() throws IOException {
|
||||
String content = this.command.generate("http://localhost");
|
||||
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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user