Polish CLI init command

Rename a few classes and methods and extract some logic into helper
classes. Also change 2 char shortcuts to a single char.

Closes gh-1751
This commit is contained in:
Phillip Webb
2014-10-31 16:52:50 -07:00
parent b89e5e0ab7
commit 830ce80824
22 changed files with 811 additions and 861 deletions

View File

@@ -22,36 +22,30 @@ package org.springframework.boot.cli.command.init;
* @author Stephane Nicoll
* @since 1.2.0
*/
class Dependency {
final class Dependency {
private String id;
private final String id;
private String name;
private final String name;
private String description;
private final String description;
public Dependency(String id, String name, String description) {
this.id = id;
this.name = name;
this.description = description;
}
public String getId() {
return this.id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return this.description;
}
public void setDescription(String description) {
this.description = description;
}
}

View File

@@ -16,9 +16,18 @@
package org.springframework.boot.cli.command.init;
import java.io.IOException;
import java.util.Arrays;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.boot.cli.command.Command;
import org.springframework.boot.cli.command.OptionParsingCommand;
import org.springframework.boot.cli.command.options.OptionHandler;
import org.springframework.boot.cli.command.status.ExitStatus;
import org.springframework.boot.cli.util.Log;
/**
* {@link Command} that initializes a project using Spring initializr.
@@ -28,13 +37,168 @@ import org.springframework.boot.cli.command.OptionParsingCommand;
*/
public class InitCommand extends OptionParsingCommand {
InitCommand(InitCommandOptionHandler handler) {
super("init", "Initialize a new project structure from Spring Initializr",
handler);
public InitCommand() {
this(new InitOptionHandler(getInitializrService()));
}
public InitCommand() {
this(new InitCommandOptionHandler(HttpClientBuilder.create().build()));
public InitCommand(InitOptionHandler handler) {
super("init", "Initialize a new project using Spring "
+ "Initialzr (start.spring.io)", handler);
}
private static InitializrService getInitializrService() {
return new InitializrService(HttpClientBuilder.create().build());
}
static class InitOptionHandler extends OptionHandler {
private final ServiceCapabilitiesReportGenerator serviceCapabilitiesReport;
private final ProjectGenerator projectGenerator;
private OptionSpec<String> target;
private OptionSpec<Void> listCapabilities;
private OptionSpec<String> bootVersion;
private OptionSpec<String> dependencies;
private OptionSpec<String> javaVersion;
private OptionSpec<String> packaging;
private OptionSpec<String> build;
private OptionSpec<String> format;
private OptionSpec<String> type;
private OptionSpec<Void> extract;
private OptionSpec<Void> force;
private OptionSpec<String> output;
InitOptionHandler(InitializrService initializrService) {
this.serviceCapabilitiesReport = new ServiceCapabilitiesReportGenerator(
initializrService);
this.projectGenerator = new ProjectGenerator(initializrService);
}
@Override
protected void options() {
this.target = option(Arrays.asList("target"), "URL of the service to use")
.withRequiredArg().defaultsTo(
ProjectGenerationRequest.DEFAULT_SERVICE_URL);
this.listCapabilities = option(Arrays.asList("list", "l"),
"List the capabilities of the service. Use it to discover the "
+ "dependencies and the types that are available");
projectGenerationOptions();
otherOptions();
}
private void projectGenerationOptions() {
this.bootVersion = option(Arrays.asList("boot-version", "b"),
"Spring Boot version to use (for example '1.2.0.RELEASE')")
.withRequiredArg();
this.dependencies = option(
Arrays.asList("dependencies", "d"),
"Comma separated list of dependencies to include in the "
+ "generated project").withRequiredArg();
this.javaVersion = option(Arrays.asList("java-version", "j"),
"Java version to use (for example '1.8')").withRequiredArg();
this.packaging = option(Arrays.asList("packaging", "p"),
"Packaging type to use (for example 'jar')").withRequiredArg();
this.build = option("build",
"The build system to use (for example 'maven' or 'gradle')")
.withRequiredArg().defaultsTo("maven");
this.format = option(
"format",
"The format of the generated content (for example 'build' for a build file, "
+ "'project' for a project archive)").withRequiredArg()
.defaultsTo("project");
this.type = option(
Arrays.asList("type", "t"),
"The project type to use. Not normally needed if you use --build "
+ "and/or --format. Check the capabilities of the service "
+ "(--list) for more details").withRequiredArg();
}
private void otherOptions() {
this.extract = option(Arrays.asList("extract", "x"),
"Extract the project archive");
this.force = option(Arrays.asList("force", "f"),
"Force overwrite of existing files");
this.output = option(
Arrays.asList("output", "o"),
"Location of the generated project. Can be an absolute or a "
+ "relative reference and should refer to a directory when "
+ "--extract is used").withRequiredArg();
}
@Override
protected ExitStatus run(OptionSet options) throws Exception {
try {
if (options.has(this.listCapabilities)) {
generateReport(options);
}
else {
generateProject(options);
}
return ExitStatus.OK;
}
catch (ReportableException ex) {
Log.error(ex.getMessage());
return ExitStatus.ERROR;
}
catch (Exception ex) {
Log.error(ex);
return ExitStatus.ERROR;
}
}
private void generateReport(OptionSet options) throws IOException {
Log.info(this.serviceCapabilitiesReport.generate(options.valueOf(this.target)));
}
protected void generateProject(OptionSet options) throws IOException {
ProjectGenerationRequest request = createProjectGenerationRequest(options);
this.projectGenerator.generateProject(request, options.has(this.force),
options.has(this.extract), options.valueOf(this.output));
}
protected ProjectGenerationRequest createProjectGenerationRequest(
OptionSet options) {
ProjectGenerationRequest request = new ProjectGenerationRequest();
request.setServiceUrl(options.valueOf(this.target));
if (options.has(this.bootVersion)) {
request.setBootVersion(options.valueOf(this.bootVersion));
}
if (options.has(this.dependencies)) {
for (String dep : options.valueOf(this.dependencies).split(",")) {
request.getDependencies().add(dep.trim());
}
}
if (options.has(this.javaVersion)) {
request.setJavaVersion(options.valueOf(this.javaVersion));
}
if (options.has(this.packaging)) {
request.setPackaging(options.valueOf(this.packaging));
}
request.setBuild(options.valueOf(this.build));
request.setFormat(options.valueOf(this.format));
request.setDetectType(options.has(this.build) || options.has(this.format));
if (options.has(this.type)) {
request.setType(options.valueOf(this.type));
}
if (options.has(this.output)) {
request.setOutput(options.valueOf(this.output));
}
return request;
}
}
}

View File

@@ -1,306 +0,0 @@
/*
* Copyright 2012-2014 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.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.boot.cli.command.options.OptionHandler;
import org.springframework.boot.cli.command.status.ExitStatus;
import org.springframework.boot.cli.util.Log;
import org.springframework.util.StreamUtils;
/**
* The {@link OptionHandler} implementation for the init command.
*
* @author Stephane Nicoll
* @since 1.2.0
*/
public class InitCommandOptionHandler extends OptionHandler {
private final CloseableHttpClient httpClient;
private OptionSpec<String> target;
private OptionSpec<Void> listMetadata;
// Project generation options
private OptionSpec<String> bootVersion;
private OptionSpec<String> dependencies;
private OptionSpec<String> javaVersion;
private OptionSpec<String> packaging;
private OptionSpec<String> build;
private OptionSpec<String> format;
private OptionSpec<String> type;
// Other options
private OptionSpec<Void> extract;
private OptionSpec<Void> force;
private OptionSpec<String> output;
InitCommandOptionHandler(CloseableHttpClient httpClient) {
this.httpClient = httpClient;
}
@Override
protected void options() {
this.target = option(Arrays.asList("target"), "URL of the service to use")
.withRequiredArg().defaultsTo(
ProjectGenerationRequest.DEFAULT_SERVICE_URL);
this.listMetadata = option(Arrays.asList("list", "l"),
"List the capabilities of the service. Use it to "
+ "discover the dependencies and the types that are available.");
// Project generation settings
this.bootVersion = option(Arrays.asList("boot-version", "bv"),
"Spring Boot version to use (e.g. 1.2.0.RELEASE)").withRequiredArg();
this.dependencies = option(Arrays.asList("dependencies", "d"),
"Comma separated list of dependencies to include in the generated project")
.withRequiredArg();
this.javaVersion = option(Arrays.asList("java-version", "jv"),
"Java version to use (e.g. 1.8)").withRequiredArg();
this.packaging = option(Arrays.asList("packaging", "p"),
"Packaging type to use (e.g. jar)").withRequiredArg();
this.build = option(
"build",
"The build system to use (e.g. maven, gradle). To be used alongside "
+ "--format to uniquely identify one type that is supported by the service. "
+ "Use --type in case of conflict").withRequiredArg().defaultsTo(
"maven");
this.format = option(
"format",
"The format of the generated content (e.g. build for a build file, "
+ "project for a project archive). To be used alongside --build to uniquely identify one type "
+ "that is supported by the service. Use --type in case of conflict")
.withRequiredArg().defaultsTo("project");
this.type = option(
Arrays.asList("type", "t"),
"The project type to use. Not normally needed if you "
+ "use --build and/or --format. Check the capabilities of the service (--list) for "
+ "more details.").withRequiredArg();
// Others
this.extract = option(Arrays.asList("extract", "x"),
"Extract the project archive");
this.force = option(Arrays.asList("force", "f"),
"Force overwrite of existing files");
this.output = option(
Arrays.asList("output", "o"),
"Location of the generated project. Can be an absolute or a relative reference and "
+ "should refer to a directory when --extract is used.")
.withRequiredArg();
}
@Override
protected ExitStatus run(OptionSet options) throws Exception {
if (options.has(this.listMetadata)) {
return listServiceCapabilities(options, this.httpClient);
}
else {
return generateProject(options, this.httpClient);
}
}
public ProjectGenerationRequest createProjectGenerationRequest(OptionSet options) {
ProjectGenerationRequest request = new ProjectGenerationRequest();
request.setServiceUrl(determineServiceUrl(options));
if (options.has(this.bootVersion)) {
request.setBootVersion(options.valueOf(this.bootVersion));
}
if (options.has(this.dependencies)) {
for (String dep : options.valueOf(this.dependencies).split(",")) {
request.getDependencies().add(dep.trim());
}
}
if (options.has(this.javaVersion)) {
request.setJavaVersion(options.valueOf(this.javaVersion));
}
if (options.has(this.packaging)) {
request.setPackaging(options.valueOf(this.packaging));
}
request.setBuild(options.valueOf(this.build));
request.setFormat(options.valueOf(this.format));
request.setDetectType(options.has(this.build) || options.has(this.format));
if (options.has(this.type)) {
request.setType(options.valueOf(this.type));
}
if (options.has(this.output)) {
request.setOutput(options.valueOf(this.output));
}
return request;
}
protected ExitStatus listServiceCapabilities(OptionSet options,
CloseableHttpClient httpClient) throws IOException {
ListMetadataCommand command = new ListMetadataCommand(httpClient);
Log.info(command.generateReport(determineServiceUrl(options)));
return ExitStatus.OK;
}
protected ExitStatus generateProject(OptionSet options, CloseableHttpClient httpClient) {
ProjectGenerationRequest request = createProjectGenerationRequest(options);
boolean forceValue = options.has(this.force);
try {
ProjectGenerationResponse entity = new InitializrServiceHttpInvoker(
httpClient).generate(request);
if (options.has(this.extract)) {
if (isZipArchive(entity)) {
return extractProject(entity, options.valueOf(this.output),
forceValue);
}
else {
Log.info("Could not extract '" + entity.getContentType() + "'");
}
}
String outputFileName = entity.getFileName() != null ? entity.getFileName()
: options.valueOf(this.output);
if (outputFileName == null) {
Log.error("Could not save the project, the server did not set a preferred "
+ "file name. Use --output to specify the output location for the project.");
return ExitStatus.ERROR;
}
return writeProject(entity, outputFileName, forceValue);
}
catch (ProjectGenerationException ex) {
Log.error(ex.getMessage());
return ExitStatus.ERROR;
}
catch (Exception ex) {
Log.error(ex);
return ExitStatus.ERROR;
}
}
private String determineServiceUrl(OptionSet options) {
return options.valueOf(this.target);
}
private ExitStatus writeProject(ProjectGenerationResponse entity,
String outputFileName, boolean overwrite) throws IOException {
File f = new File(outputFileName);
if (f.exists()) {
if (overwrite) {
if (!f.delete()) {
throw new IllegalStateException("Failed to delete existing file "
+ f.getPath());
}
}
else {
Log.error("File '" + f.getName()
+ "' already exists. Use --force if you want to "
+ "overwrite or --output to specify an alternate location.");
return ExitStatus.ERROR;
}
}
FileOutputStream stream = new FileOutputStream(f);
try {
StreamUtils.copy(entity.getContent(), stream);
Log.info("Content saved to '" + outputFileName + "'");
return ExitStatus.OK;
}
finally {
stream.close();
}
}
private boolean isZipArchive(ProjectGenerationResponse entity) {
if (entity.getContentType() == null) {
return false;
}
try {
return "application/zip".equals(entity.getContentType().getMimeType());
}
catch (Exception e) {
return false;
}
}
private ExitStatus extractProject(ProjectGenerationResponse entity,
String outputValue, boolean overwrite) throws IOException {
File output = outputValue != null ? new File(outputValue) : new File(
System.getProperty("user.dir"));
if (!output.exists()) {
output.mkdirs();
}
ZipInputStream zipIn = new ZipInputStream(new ByteArrayInputStream(
entity.getContent()));
try {
ZipEntry entry = zipIn.getNextEntry();
while (entry != null) {
File f = new File(output, entry.getName());
if (f.exists() && !overwrite) {
StringBuilder sb = new StringBuilder();
sb.append(f.isDirectory() ? "Directory" : "File")
.append(" '")
.append(f.getName())
.append("' already exists. Use --force if you want to "
+ "overwrite or --output to specify an alternate location.");
Log.error(sb.toString());
return ExitStatus.ERROR;
}
if (!entry.isDirectory()) {
extractZipEntry(zipIn, f);
}
else {
f.mkdir();
}
zipIn.closeEntry();
entry = zipIn.getNextEntry();
}
Log.info("Project extracted to '" + output.getAbsolutePath() + "'");
return ExitStatus.OK;
}
finally {
zipIn.close();
}
}
private void extractZipEntry(ZipInputStream in, File outputFile) throws IOException {
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(
outputFile));
try {
StreamUtils.copy(in, out);
}
finally {
out.close();
}
}
}

View File

@@ -0,0 +1,195 @@
/*
* Copyright 2012-2014 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.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");
private final CloseableHttpClient http;
/**
* Create a new instance with the given {@link CloseableHttpClient HTTP client}.
*/
public InitializrService(CloseableHttpClient http) {
this.http = http;
}
/**
* Generate a project based on the specified {@link ProjectGenerationRequest}
* @return an entity defining the project
*/
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();
if (httpEntity == null) {
throw new ReportableException("No content received from server '" + url + "'");
}
if (httpResponse.getStatusLine().getStatusCode() != 200) {
throw createException(request.getServiceUrl(), httpResponse);
}
return createResponse(httpResponse, httpEntity);
}
/**
* Load the {@link InitializrServiceMetadata} at the specified url.
*/
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);
}
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,
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
*/
private CloseableHttpResponse executeProjectGenerationRequest(URI url) {
return execute(new HttpGet(url), url, "generate project");
}
/**
* Retrieves the meta-data of the service at the specified URL
*/
private CloseableHttpResponse executeInitializrMetadataRetrieval(String url) {
HttpGet request = new HttpGet(url);
request.setHeader(new BasicHeader(HttpHeaders.ACCEPT, "application/json"));
return execute(request, url, "retrieve metadata");
}
private CloseableHttpResponse execute(HttpUriRequest request, Object url,
String description) {
try {
return this.http.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.toString());
}
private String extractMessage(HttpEntity entity) {
if (entity != null) {
try {
JSONObject error = getContentAsJson(entity);
if (error.has("message")) {
return error.getString("message");
}
}
catch (Exception ex) {
}
}
return null;
}
private JSONObject getContentAsJson(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));
}
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(),
value.length());
int end = value.indexOf("\"");
if (end != -1) {
return value.substring(0, end);
}
}
}
return null;
}
}

View File

@@ -1,225 +0,0 @@
/*
* Copyright 2012-2014 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.io.InputStream;
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.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient;
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.StreamUtils;
import org.springframework.util.StringUtils;
/**
* Invokes the initializr service over HTTP.
*
* @author Stephane Nicoll
* @since 1.2.0
*/
class InitializrServiceHttpInvoker {
private final CloseableHttpClient httpClient;
/**
* Create a new instance with the given {@link CloseableHttpClient http client}.
*/
InitializrServiceHttpInvoker(CloseableHttpClient httpClient) {
this.httpClient = httpClient;
}
/**
* Generate a project based on the specified {@link ProjectGenerationRequest}
* @return an entity defining the project
*/
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();
if (httpEntity == null) {
throw new ProjectGenerationException(
"No content received from server using '" + url + "'");
}
if (httpResponse.getStatusLine().getStatusCode() != 200) {
throw buildProjectGenerationException(request.getServiceUrl(), httpResponse);
}
return createResponse(httpResponse, httpEntity);
}
/**
* Load the {@link InitializrServiceMetadata} at the specified url.
*/
InitializrServiceMetadata loadMetadata(String serviceUrl) throws IOException {
CloseableHttpResponse httpResponse = executeInitializrMetadataRetrieval(serviceUrl);
if (httpResponse.getEntity() == null) {
throw new ProjectGenerationException(
"No content received from server using '" + serviceUrl + "'");
}
if (httpResponse.getStatusLine().getStatusCode() != 200) {
throw buildProjectGenerationException(serviceUrl, httpResponse);
}
try {
HttpEntity httpEntity = httpResponse.getEntity();
JSONObject root = getContentAsJson(getContent(httpEntity),
getContentType(httpEntity));
return new InitializrServiceMetadata(root);
}
catch (JSONException e) {
throw new ProjectGenerationException("Invalid content received from server ("
+ e.getMessage() + ")");
}
}
private ProjectGenerationResponse createResponse(CloseableHttpResponse httpResponse,
HttpEntity httpEntity) throws IOException {
ProjectGenerationResponse response = new ProjectGenerationResponse();
ContentType contentType = ContentType.getOrDefault(httpEntity);
response.setContentType(contentType);
InputStream in = httpEntity.getContent();
try {
response.setContent(StreamUtils.copyToByteArray(in));
}
finally {
in.close();
}
String detectedFileName = extractFileName(httpResponse
.getFirstHeader("Content-Disposition"));
if (detectedFileName != null) {
response.setFileName(detectedFileName);
}
return response;
}
/**
* Request the creation of the project using the specified url
*/
private CloseableHttpResponse executeProjectGenerationRequest(URI url) {
try {
HttpGet get = new HttpGet(url);
return this.httpClient.execute(get);
}
catch (IOException e) {
throw new ProjectGenerationException("Failed to invoke server at '" + url
+ "' (" + e.getMessage() + ")");
}
}
/**
* Retrieves the metadata of the service at the specified url
*/
private CloseableHttpResponse executeInitializrMetadataRetrieval(String serviceUrl) {
try {
HttpGet get = new HttpGet(serviceUrl);
get.setHeader(new BasicHeader(HttpHeaders.ACCEPT, "application/json"));
return this.httpClient.execute(get);
}
catch (IOException e) {
throw new ProjectGenerationException(
"Failed to retrieve metadata from service at '" + serviceUrl + "' ("
+ e.getMessage() + ")");
}
}
private byte[] getContent(HttpEntity httpEntity) throws IOException {
InputStream in = httpEntity.getContent();
try {
return StreamUtils.copyToByteArray(in);
}
finally {
in.close();
}
}
private ContentType getContentType(HttpEntity httpEntity) {
return ContentType.getOrDefault(httpEntity);
}
private JSONObject getContentAsJson(byte[] content, ContentType contentType) {
Charset charset = contentType.getCharset() != null ? contentType.getCharset()
: Charset.forName("UTF-8");
String data = new String(content, charset);
return new JSONObject(data);
}
private ProjectGenerationException buildProjectGenerationException(String url,
CloseableHttpResponse httpResponse) {
StringBuilder sb = new StringBuilder("Project generation failed using '");
sb.append(url).append("' - service returned ")
.append(httpResponse.getStatusLine().getReasonPhrase());
String error = extractMessage(httpResponse.getEntity());
if (StringUtils.hasText(error)) {
sb.append(": '").append(error).append("'");
}
else {
sb.append(" (unexpected ")
.append(httpResponse.getStatusLine().getStatusCode())
.append(" error)");
}
throw new ProjectGenerationException(sb.toString());
}
private String extractMessage(HttpEntity entity) {
if (entity == null) {
return null;
}
try {
JSONObject error = getContentAsJson(getContent(entity),
getContentType(entity));
if (error.has("message")) {
return error.getString("message");
}
return null;
}
catch (Exception e) {
return null;
}
}
private static String extractFileName(Header h) {
if (h == null) {
return null;
}
String value = h.getValue();
String prefix = "filename=\"";
int start = value.indexOf(prefix);
if (start != -1) {
value = value.substring(start + prefix.length(), value.length());
int end = value.indexOf("\"");
if (end != -1) {
return value.substring(0, end);
}
}
return null;
}
}

View File

@@ -59,13 +59,13 @@ class InitializrServiceMetadata {
/**
* Creates a new instance using the specified root {@link JSONObject}.
*/
InitializrServiceMetadata(JSONObject root) {
public InitializrServiceMetadata(JSONObject root) {
this.dependencies = parseDependencies(root);
this.projectTypes = parseProjectTypes(root);
this.defaults = Collections.unmodifiableMap(parseDefaults(root));
}
InitializrServiceMetadata(ProjectType defaultProjectType) {
public InitializrServiceMetadata(ProjectType defaultProjectType) {
this.dependencies = new HashMap<String, Dependency>();
this.projectTypes = new MetadataHolder<String, ProjectType>();
this.projectTypes.getContent()
@@ -169,11 +169,10 @@ class InitializrServiceMetadata {
}
private Dependency parseDependency(JSONObject object) {
Dependency dependency = new Dependency();
dependency.setName(getStringValue(object, NAME_ATTRIBUTE, null));
dependency.setId(getStringValue(object, ID_ATTRIBUTE, null));
dependency.setDescription(getStringValue(object, DESCRIPTION_ATTRIBUTE, null));
return dependency;
String id = getStringValue(object, ID_ATTRIBUTE, null);
String name = getStringValue(object, NAME_ATTRIBUTE, null);
String description = getStringValue(object, DESCRIPTION_ATTRIBUTE, null);
return new Dependency(id, name, description);
}
private ProjectType parseType(JSONObject object) {
@@ -230,6 +229,7 @@ class InitializrServiceMetadata {
public void setDefaultItem(T defaultItem) {
this.defaultItem = defaultItem;
}
}
}

View File

@@ -1,119 +0,0 @@
/*
* Copyright 2012-2014 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.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.http.impl.client.CloseableHttpClient;
import org.codehaus.plexus.util.StringUtils;
/**
* A helper class generating a report from the metadata of a particular service.
*
* @author Stephane Nicoll
* @since 1.2.0
*/
class ListMetadataCommand {
private static final String NEW_LINE = System.getProperty("line.separator");
private final InitializrServiceHttpInvoker initializrServiceInvoker;
/**
* Creates an instance using the specified {@link CloseableHttpClient}.
*/
ListMetadataCommand(CloseableHttpClient httpClient) {
this.initializrServiceInvoker = new InitializrServiceHttpInvoker(httpClient);
}
/**
* Generate a report for the specified service. The report contains the available
* capabilities as advertized by the root endpoint.
*/
String generateReport(String serviceUrl) throws IOException {
InitializrServiceMetadata metadata = this.initializrServiceInvoker
.loadMetadata(serviceUrl);
String header = "Capabilities of " + serviceUrl;
int size = header.length();
StringBuilder sb = new StringBuilder();
sb.append(StringUtils.repeat("=", size)).append(NEW_LINE).append(header)
.append(NEW_LINE).append(StringUtils.repeat("=", size)).append(NEW_LINE)
.append(NEW_LINE).append("Available dependencies:").append(NEW_LINE)
.append("-----------------------").append(NEW_LINE);
List<Dependency> dependencies = new ArrayList<Dependency>(
metadata.getDependencies());
Collections.sort(dependencies, new Comparator<Dependency>() {
@Override
public int compare(Dependency o1, Dependency o2) {
return o1.getId().compareTo(o2.getId());
}
});
for (Dependency dependency : dependencies) {
sb.append(dependency.getId()).append(" - ").append(dependency.getName());
if (dependency.getDescription() != null) {
sb.append(": ").append(dependency.getDescription());
}
sb.append(NEW_LINE);
}
sb.append(NEW_LINE).append("Available project types:").append(NEW_LINE)
.append("------------------------").append(NEW_LINE);
List<String> typeIds = new ArrayList<String>(metadata.getProjectTypes().keySet());
Collections.sort(typeIds);
for (String typeId : typeIds) {
ProjectType type = metadata.getProjectTypes().get(typeId);
sb.append(typeId).append(" - ").append(type.getName());
if (!type.getTags().isEmpty()) {
sb.append(" [");
Iterator<Map.Entry<String, String>> it = type.getTags().entrySet()
.iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
sb.append(entry.getKey()).append(":").append(entry.getValue());
if (it.hasNext()) {
sb.append(", ");
}
}
sb.append("]");
}
if (type.isDefaultType()) {
sb.append(" (default)");
}
sb.append(NEW_LINE);
}
sb.append(NEW_LINE).append("Defaults:").append(NEW_LINE).append("---------")
.append(NEW_LINE);
List<String> defaultsKeys = new ArrayList<String>(metadata.getDefaults().keySet());
Collections.sort(defaultsKeys);
for (String defaultsKey : defaultsKeys) {
sb.append(defaultsKey).append(": ")
.append(metadata.getDefaults().get(defaultsKey)).append(NEW_LINE);
}
return sb.toString();
}
}

View File

@@ -201,7 +201,7 @@ class ProjectGenerationRequest {
return builder.build();
}
catch (URISyntaxException e) {
throw new ProjectGenerationException("Invalid service URL (" + e.getMessage()
throw new ReportableException("Invalid service URL (" + e.getMessage()
+ ")");
}
}
@@ -210,7 +210,7 @@ class ProjectGenerationRequest {
if (this.type != null) {
ProjectType result = metadata.getProjectTypes().get(this.type);
if (result == null) {
throw new ProjectGenerationException(("No project type with id '"
throw new ReportableException(("No project type with id '"
+ this.type + "' - check the service capabilities (--list)"));
}
}
@@ -227,19 +227,19 @@ class ProjectGenerationRequest {
return types.values().iterator().next();
}
else if (types.size() == 0) {
throw new ProjectGenerationException("No type found with build '"
throw new ReportableException("No type found with build '"
+ this.build + "' and format '" + this.format
+ "' check the service capabilities (--list)");
}
else {
throw new ProjectGenerationException("Multiple types found with build '"
throw new ReportableException("Multiple types found with build '"
+ this.build + "' and format '" + this.format
+ "' use --type with a more specific value " + types.keySet());
}
}
ProjectType defaultType = metadata.getDefaultType();
if (defaultType == null) {
throw new ProjectGenerationException(
throw new ReportableException(
("No project type is set and no default is defined. "
+ "Check the service capabilities (--list)"));
}

View File

@@ -26,13 +26,14 @@ import org.apache.http.entity.ContentType;
*/
class ProjectGenerationResponse {
private ContentType contentType;
private final ContentType contentType;
private byte[] content;
private String fileName;
ProjectGenerationResponse() {
public ProjectGenerationResponse(ContentType contentType) {
this.contentType = contentType;
}
/**
@@ -42,10 +43,6 @@ class ProjectGenerationResponse {
return this.contentType;
}
public void setContentType(ContentType contentType) {
this.contentType = contentType;
}
/**
* The generated project archive or file.
*/

View File

@@ -0,0 +1,137 @@
/*
* Copyright 2012-2014 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.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.springframework.boot.cli.util.Log;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StreamUtils;
/**
* @author Stephane Nicoll
* @since 1.2.0
*/
public class ProjectGenerator {
private static final String ZIP_MIME_TYPE = "application/zip";
private final InitializrService initializrService;
public ProjectGenerator(InitializrService initializrService) {
this.initializrService = initializrService;
}
public void generateProject(ProjectGenerationRequest request, boolean force,
boolean extract, String output) throws IOException {
ProjectGenerationResponse response = this.initializrService.generate(request);
if (extract) {
if (isZipArchive(response)) {
extractProject(response, output, force);
return;
}
else {
Log.info("Could not extract '" + response.getContentType() + "'");
}
}
String fileName = response.getFileName();
fileName = (fileName != null ? fileName : output);
if (fileName == null) {
throw new ReportableException(
"Could not save the project, the server did not set a preferred "
+ "file name. Use --output to specify the output location "
+ "for the project.");
}
writeProject(response, fileName, force);
}
private boolean isZipArchive(ProjectGenerationResponse entity) {
if (entity.getContentType() != null) {
try {
return ZIP_MIME_TYPE.equals(entity.getContentType().getMimeType());
}
catch (Exception ex) {
}
}
return false;
}
private void extractProject(ProjectGenerationResponse entity, String output,
boolean overwrite) throws IOException {
File outputFolder = (output != null ? new File(output) : new File(
System.getProperty("user.dir")));
if (!outputFolder.exists()) {
outputFolder.mkdirs();
}
ZipInputStream zipStream = new ZipInputStream(new ByteArrayInputStream(
entity.getContent()));
try {
extractFromStream(zipStream, overwrite, outputFolder);
Log.info("Project extracted to '" + outputFolder.getAbsolutePath() + "'");
}
finally {
zipStream.close();
}
}
private void extractFromStream(ZipInputStream zipStream, boolean overwrite,
File outputFolder) throws IOException {
ZipEntry entry = zipStream.getNextEntry();
while (entry != null) {
File file = new File(outputFolder, entry.getName());
if (file.exists() && !overwrite) {
throw new ReportableException(file.isDirectory() ? "Directory" : "File"
+ " '" + file.getName()
+ "' already exists. Use --force if you want to overwrite or "
+ "--output to specify an alternate location.");
}
if (!entry.isDirectory()) {
FileCopyUtils.copy(StreamUtils.nonClosing(zipStream),
new FileOutputStream(file));
}
else {
file.mkdir();
}
zipStream.closeEntry();
entry = zipStream.getNextEntry();
}
}
private void writeProject(ProjectGenerationResponse entity, String output,
boolean overwrite) throws IOException {
File outputFile = new File(output);
if (outputFile.exists()) {
if (!overwrite) {
throw new ReportableException("File '" + outputFile.getName()
+ "' already exists. Use --force if you want to "
+ "overwrite or --output to specify an alternate location.");
}
if (!outputFile.delete()) {
throw new ReportableException("Failed to delete existing file "
+ outputFile.getPath());
}
}
FileCopyUtils.copy(entity.getContent(), outputFile);
Log.info("Content saved to '" + output + "'");
}
}

View File

@@ -68,4 +68,5 @@ class ProjectType {
public Map<String, String> getTags() {
return Collections.unmodifiableMap(this.tags);
}
}

View File

@@ -17,15 +17,19 @@
package org.springframework.boot.cli.command.init;
/**
* Thrown when a project could not be generated.
* Exception with a message that can be reported to the user.
*
* @author Stephane Nicoll
* @since 1.2.0
*/
public class ProjectGenerationException extends RuntimeException {
public class ReportableException extends RuntimeException {
public ProjectGenerationException(String message) {
public ReportableException(String message) {
super(message);
}
public ReportableException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,139 @@
/*
* Copyright 2012-2014 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.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.http.impl.client.CloseableHttpClient;
import org.codehaus.plexus.util.StringUtils;
/**
* A helper class generating a report from the meta-data of a particular service.
*
* @author Stephane Nicoll
* @since 1.2.0
*/
class ServiceCapabilitiesReportGenerator {
private static final String NEW_LINE = System.getProperty("line.separator");
private final InitializrService initializrService;
/**
* Creates an instance using the specified {@link CloseableHttpClient}.
*/
ServiceCapabilitiesReportGenerator(InitializrService initializrService) {
this.initializrService = initializrService;
}
/**
* Generate a report for the specified service. The report contains the available
* capabilities as advertized by the root endpoint.
*/
public String generate(String url) throws IOException {
InitializrServiceMetadata metadata = this.initializrService.loadMetadata(url);
String header = "Capabilities of " + url;
StringBuilder report = new StringBuilder();
report.append(StringUtils.repeat("=", header.length()) + NEW_LINE);
report.append(header + NEW_LINE);
report.append(StringUtils.repeat("=", header.length()) + NEW_LINE);
report.append(NEW_LINE);
reportAvailableDependencies(metadata, report);
report.append(NEW_LINE);
reportAvilableProjectTypes(metadata, report);
report.append(NEW_LINE);
z(metadata, report);
return report.toString();
}
private void reportAvailableDependencies(InitializrServiceMetadata metadata,
StringBuilder report) {
report.append("Available dependencies:" + NEW_LINE);
report.append("-----------------------" + NEW_LINE);
List<Dependency> dependencies = getSortedDependencies(metadata);
for (Dependency dependency : dependencies) {
report.append(dependency.getId() + " - " + dependency.getName());
if (dependency.getDescription() != null) {
report.append(": " + dependency.getDescription());
}
report.append(NEW_LINE);
}
}
private List<Dependency> getSortedDependencies(InitializrServiceMetadata metadata) {
ArrayList<Dependency> dependencies = new ArrayList<Dependency>(
metadata.getDependencies());
Collections.sort(dependencies, new Comparator<Dependency>() {
@Override
public int compare(Dependency o1, Dependency o2) {
return o1.getId().compareTo(o2.getId());
}
});
return dependencies;
}
private void reportAvilableProjectTypes(InitializrServiceMetadata metadata,
StringBuilder report) {
report.append("Available project types:" + NEW_LINE);
report.append("------------------------" + NEW_LINE);
List<String> typeIds = new ArrayList<String>(metadata.getProjectTypes().keySet());
Collections.sort(typeIds);
for (String typeId : typeIds) {
ProjectType type = metadata.getProjectTypes().get(typeId);
report.append(typeId + " - " + type.getName());
if (!type.getTags().isEmpty()) {
reportTags(report, type);
}
if (type.isDefaultType()) {
report.append(" (default)");
}
report.append(NEW_LINE);
}
}
private void reportTags(StringBuilder report, ProjectType type) {
Map<String, String> tags = type.getTags();
Iterator<Map.Entry<String, String>> iterator = tags.entrySet().iterator();
report.append(" [");
while (iterator.hasNext()) {
Map.Entry<String, String> entry = iterator.next();
report.append(entry.getKey() + ":" + entry.getValue());
if (iterator.hasNext()) {
report.append(", ");
}
}
report.append("]");
}
private void z(InitializrServiceMetadata metadata, StringBuilder report) {
report.append("Defaults:" + NEW_LINE);
report.append("---------" + NEW_LINE);
List<String> defaultsKeys = new ArrayList<String>(metadata.getDefaults().keySet());
Collections.sort(defaultsKeys);
for (String defaultsKey : defaultsKeys) {
String defaultsValue = metadata.getDefaults().get(defaultsKey);
report.append(defaultsKey + ": " + defaultsValue + NEW_LINE);
}
}
}

View File

@@ -95,4 +95,9 @@ public final class ExitStatus {
return new ExitStatus(this.code, this.name, true);
}
@Override
public String toString() {
return getName() + ":" + getCode();
}
}