diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/InitializrService.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/InitializrService.java index ed4606e1bd..9c0d1aa34d 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/InitializrService.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/InitializrService.java @@ -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. + *

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) { diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/ServiceCapabilitiesReportGenerator.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/ServiceCapabilitiesReportGenerator.java index 885722ae44..02e2f52ee7 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/ServiceCapabilitiesReportGenerator.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/ServiceCapabilitiesReportGenerator.java @@ -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); diff --git a/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/AbstractHttpClientMockTests.java b/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/AbstractHttpClientMockTests.java index 180e90f4d3..8731e0a109 100644 --- a/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/AbstractHttpClientMockTests.java +++ b/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/AbstractHttpClientMockTests.java @@ -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 getForJsonMetadata() { - return new HasAcceptHeader("application/vnd.initializr.v2+json", true); + private Matcher getForMetadata(boolean serviceCapabilities) { + if (serviceCapabilities) { + return new HasAcceptHeader(InitializrService.ACCEPT_SERVICE_CAPABILITIES, true); + } + else { + return new HasAcceptHeader(InitializrService.ACCEPT_META_DATA, true); + } } - protected Matcher getForNonJsonMetadata() { - return new HasAcceptHeader("application/vnd.initializr.v2+json", false); + private Matcher getForNonMetadata() { + return new HasAcceptHeader(InitializrService.ACCEPT_META_DATA, false); } private String contentDispositionValue(String fileName) { diff --git a/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/InitCommandTests.java b/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/InitCommandTests.java index 378c9eab1e..24f0b5b12b 100644 --- a/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/InitCommandTests.java +++ b/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/InitCommandTests.java @@ -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"); } diff --git a/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/InitializrServiceTests.java b/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/InitializrServiceTests.java index 808ba842c5..a2d14457ac 100644 --- a/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/InitializrServiceTests.java +++ b/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/InitializrServiceTests.java @@ -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); diff --git a/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/ServiceCapabilitiesReportGeneratorTests.java b/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/ServiceCapabilitiesReportGeneratorTests.java index 0779006824..2ef12a028a 100644 --- a/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/ServiceCapabilitiesReportGeneratorTests.java +++ b/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/ServiceCapabilitiesReportGeneratorTests.java @@ -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")); } } diff --git a/spring-boot-cli/src/test/resources/metadata/service-metadata-2.1.0.json b/spring-boot-cli/src/test/resources/metadata/service-metadata-2.1.0.json new file mode 100644 index 0000000000..292808df8f --- /dev/null +++ b/spring-boot-cli/src/test/resources/metadata/service-metadata-2.1.0.json @@ -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" + } +} diff --git a/spring-boot-cli/src/test/resources/metadata/service-metadata-2.1.0.txt b/spring-boot-cli/src/test/resources/metadata/service-metadata-2.1.0.txt new file mode 100644 index 0000000000..e69de29bb2