/* * Copyright 2012-2017 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 org.springframework.boot.cli.command.init; import java.io.IOException; import java.net.URI; import java.nio.charset.Charset; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpHeaders; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.entity.ContentType; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.message.BasicHeader; import org.json.JSONException; import org.json.JSONObject; import org.springframework.boot.cli.util.Log; import org.springframework.util.FileCopyUtils; import org.springframework.util.StringUtils; /** * Invokes the initializr service over HTTP. * * @author Stephane Nicoll * @since 1.2.0 */ class InitializrService { private static final String FILENAME_HEADER_PREFIX = "filename=\""; 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. */ private CloseableHttpClient http; InitializrService() { } InitializrService(CloseableHttpClient http) { this.http = http; } protected CloseableHttpClient getHttp() { if (this.http == null) { this.http = HttpClientBuilder.create().useSystemProperties().build(); } return this.http; } /** * Generate a project based on the specified {@link ProjectGenerationRequest}. * @param request the generation request * @return an entity defining the project * @throws IOException if generation fails */ public ProjectGenerationResponse generate(ProjectGenerationRequest request) throws IOException { Log.info("Using service at " + request.getServiceUrl()); InitializrServiceMetadata metadata = loadMetadata(request.getServiceUrl()); URI url = request.generateUrl(metadata); CloseableHttpResponse httpResponse = executeProjectGenerationRequest(url); HttpEntity httpEntity = httpResponse.getEntity(); validateResponse(httpResponse, request.getServiceUrl()); return createResponse(httpResponse, httpEntity); } /** * Load the {@link InitializrServiceMetadata} at the specified url. * @param serviceUrl to url of the initializer service * @return the metadata describing the service * @throws IOException if the service's metadata cannot be loaded */ 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, * otherwise {@link InitializrServiceMetadata} is returned. * @param serviceUrl to url of the initializer service * @return the service capabilities (as a String) or the * {@link InitializrServiceMetadata} describing the service * @throws IOException if the service capabilities cannot be loaded */ public Object loadServiceCapabilities(String serviceUrl) throws IOException { HttpGet request = new HttpGet(serviceUrl); request.setHeader( new BasicHeader(HttpHeaders.ACCEPT, ACCEPT_SERVICE_CAPABILITIES)); CloseableHttpResponse httpResponse = execute(request, serviceUrl, "retrieve help"); validateResponse(httpResponse, serviceUrl); HttpEntity httpEntity = httpResponse.getEntity(); ContentType contentType = ContentType.getOrDefault(httpEntity); if (contentType.getMimeType().equals("text/plain")) { return getContent(httpEntity); } 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 + "'"); } if (httpResponse.getStatusLine().getStatusCode() != 200) { throw createException(serviceUrl, httpResponse); } } private ProjectGenerationResponse createResponse(CloseableHttpResponse httpResponse, HttpEntity httpEntity) throws IOException { ProjectGenerationResponse response = new ProjectGenerationResponse( ContentType.getOrDefault(httpEntity)); response.setContent(FileCopyUtils.copyToByteArray(httpEntity.getContent())); String fileName = extractFileName( httpResponse.getFirstHeader("Content-Disposition")); if (fileName != null) { response.setFileName(fileName); } return response; } /** * Request the creation of the project using the specified URL. * @param url the URL * @return the response */ private CloseableHttpResponse executeProjectGenerationRequest(URI url) { return execute(new HttpGet(url), url, "generate project"); } /** * Retrieves the meta-data of the service at the specified URL. * @param url the URL * @return the response */ private CloseableHttpResponse executeInitializrMetadataRetrieval(String url) { HttpGet request = new HttpGet(url); request.setHeader(new BasicHeader(HttpHeaders.ACCEPT, ACCEPT_META_DATA)); return execute(request, url, "retrieve metadata"); } private CloseableHttpResponse execute(HttpUriRequest request, Object url, String description) { try { request.addHeader("User-Agent", "SpringBootCli/" + getClass().getPackage().getImplementationVersion()); return getHttp().execute(request); } catch (IOException ex) { throw new ReportableException("Failed to " + description + " from service at '" + url + "' (" + ex.getMessage() + ")"); } } private ReportableException createException(String url, CloseableHttpResponse httpResponse) { String message = "Initializr service call failed using '" + url + "' - service returned " + httpResponse.getStatusLine().getReasonPhrase(); String error = extractMessage(httpResponse.getEntity()); if (StringUtils.hasText(error)) { message += ": '" + error + "'"; } else { int statusCode = httpResponse.getStatusLine().getStatusCode(); message += " (unexpected " + statusCode + " error)"; } throw new ReportableException(message); } private String extractMessage(HttpEntity entity) { if (entity != null) { try { JSONObject error = getContentAsJson(entity); if (error.has("message")) { return error.getString("message"); } } catch (Exception ex) { // Ignore } } return null; } private JSONObject getContentAsJson(HttpEntity entity) throws IOException, JSONException { 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 String(content, charset); } private String extractFileName(Header header) { if (header != null) { String value = header.getValue(); int start = value.indexOf(FILENAME_HEADER_PREFIX); if (start != -1) { value = value.substring(start + FILENAME_HEADER_PREFIX.length()); int end = value.indexOf("\""); if (end != -1) { return value.substring(0, end); } } } return null; } }