diff --git a/spring-boot-cli/pom.xml b/spring-boot-cli/pom.xml
index 01d524423e..11e5a74ad8 100644
--- a/spring-boot-cli/pom.xml
+++ b/spring-boot-cli/pom.xml
@@ -57,6 +57,10 @@
org.springframework
spring-core
+
+ org.json
+ json
+
org.apache.maven
maven-aether-provider
diff --git a/spring-boot-cli/src/it/java/org/springframework/boot/cli/CommandLineIT.java b/spring-boot-cli/src/it/java/org/springframework/boot/cli/CommandLineIT.java
index d52bd1afea..8f0b4fd0c3 100644
--- a/spring-boot-cli/src/it/java/org/springframework/boot/cli/CommandLineIT.java
+++ b/spring-boot-cli/src/it/java/org/springframework/boot/cli/CommandLineIT.java
@@ -43,7 +43,7 @@ public class CommandLineIT {
Invocation cli = this.cli.invoke("hint");
assertThat(cli.await(), equalTo(0));
assertThat(cli.getErrorOutput().length(), equalTo(0));
- assertThat(cli.getStandardOutputLines().size(), equalTo(9));
+ assertThat(cli.getStandardOutputLines().size(), equalTo(10));
}
@Test
diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/DefaultCommandFactory.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/DefaultCommandFactory.java
index 1d3c7035c2..a06406ec3c 100644
--- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/DefaultCommandFactory.java
+++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/DefaultCommandFactory.java
@@ -24,6 +24,7 @@ import org.springframework.boot.cli.command.Command;
import org.springframework.boot.cli.command.CommandFactory;
import org.springframework.boot.cli.command.core.VersionCommand;
import org.springframework.boot.cli.command.grab.GrabCommand;
+import org.springframework.boot.cli.command.init.InitCommand;
import org.springframework.boot.cli.command.install.InstallCommand;
import org.springframework.boot.cli.command.install.UninstallCommand;
import org.springframework.boot.cli.command.jar.JarCommand;
@@ -39,7 +40,7 @@ public class DefaultCommandFactory implements CommandFactory {
private static final List DEFAULT_COMMANDS = Arrays. asList(
new VersionCommand(), new RunCommand(), new TestCommand(), new GrabCommand(),
- new JarCommand(), new InstallCommand(), new UninstallCommand());
+ new JarCommand(), new InstallCommand(), new UninstallCommand(), new InitCommand());
@Override
public Collection getCommands() {
diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/Dependency.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/Dependency.java
new file mode 100644
index 0000000000..5cfc24f6c1
--- /dev/null
+++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/Dependency.java
@@ -0,0 +1,56 @@
+/*
+ * 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;
+
+/**
+ * Provide some basic information about a dependency.
+ *
+ * @author Stephane Nicoll
+ * @since 1.2.0
+ */
+class Dependency {
+
+ private String id;
+
+ private String name;
+
+ private String description;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+}
diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/InitCommand.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/InitCommand.java
new file mode 100644
index 0000000000..0339cbc1e0
--- /dev/null
+++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/InitCommand.java
@@ -0,0 +1,40 @@
+/*
+ * 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 org.apache.http.impl.client.HttpClientBuilder;
+
+import org.springframework.boot.cli.command.Command;
+import org.springframework.boot.cli.command.OptionParsingCommand;
+
+/**
+ * {@link Command} that initializes a project using Spring initializr.
+ *
+ * @author Stephane Nicoll
+ * @since 1.2.0
+ */
+public class InitCommand extends OptionParsingCommand {
+
+ InitCommand(InitCommandOptionHandler handler) {
+ super("init", "Initialize a new project structure using Spring Initializr", handler);
+ }
+
+ public InitCommand() {
+ this(new InitCommandOptionHandler(HttpClientBuilder.create().build()));
+ }
+
+}
diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/InitCommandOptionHandler.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/InitCommandOptionHandler.java
new file mode 100644
index 0000000000..7fefe8341b
--- /dev/null
+++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/InitCommandOptionHandler.java
@@ -0,0 +1,280 @@
+/*
+ * 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 target;
+
+ private OptionSpec listMetadata;
+
+ // Project generation options
+
+ private OptionSpec bootVersion;
+
+ private OptionSpec dependencies;
+
+ private OptionSpec javaVersion;
+
+ private OptionSpec packaging;
+
+ private OptionSpec build;
+
+ private OptionSpec format;
+
+ private OptionSpec type;
+
+ // Other options
+
+ private OptionSpec extract;
+
+ private OptionSpec force;
+
+ private OptionSpec 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(listMetadata)) {
+ return listServiceCapabilities(options, httpClient);
+ }
+ else {
+ return generateProject(options, 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();
+ }
+ }
+
+}
diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/InitializrServiceHttpInvoker.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/InitializrServiceHttpInvoker.java
new file mode 100644
index 0000000000..2a5cbfbe39
--- /dev/null
+++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/InitializrServiceHttpInvoker.java
@@ -0,0 +1,215 @@
+/*
+ * 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;
+ }
+
+}
diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/InitializrServiceMetadata.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/InitializrServiceMetadata.java
new file mode 100644
index 0000000000..2559f5720e
--- /dev/null
+++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/InitializrServiceMetadata.java
@@ -0,0 +1,236 @@
+/*
+ * 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.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+/**
+ * Define the metadata available for a particular service instance.
+ *
+ * @author Stephane Nicoll
+ * @since 1.2.0
+ */
+class InitializrServiceMetadata {
+
+ private static final String DEPENDENCIES_EL = "dependencies";
+
+ private static final String TYPES_EL = "types";
+
+ private static final String DEFAULTS_EL = "defaults";
+
+ private static final String CONTENT_EL = "content";
+
+ private static final String NAME_ATTRIBUTE = "name";
+
+ private static final String ID_ATTRIBUTE = "id";
+
+ private static final String DESCRIPTION_ATTRIBUTE = "description";
+
+ private static final String ACTION_ATTRIBUTE = "action";
+
+ private static final String DEFAULT_ATTRIBUTE = "default";
+
+
+ private final Map dependencies;
+
+ private final MetadataHolder projectTypes;
+
+ private final Map defaults;
+
+
+ /**
+ * Creates a new instance using the specified root {@link JSONObject}.
+ */
+ InitializrServiceMetadata(JSONObject root) {
+ this.dependencies = parseDependencies(root);
+ this.projectTypes = parseProjectTypes(root);
+ this.defaults = Collections.unmodifiableMap(parseDefaults(root));
+ }
+
+ InitializrServiceMetadata(ProjectType defaultProjectType) {
+ this.dependencies = new HashMap();
+ this.projectTypes = new MetadataHolder();
+ this.projectTypes.getContent().put(defaultProjectType.getId(), defaultProjectType);
+ this.projectTypes.setDefaultItem(defaultProjectType);
+ this.defaults = new HashMap();
+ }
+
+ /**
+ * Return the dependencies supported by the service.
+ */
+ public Collection getDependencies() {
+ return dependencies.values();
+ }
+
+ /**
+ * Return the dependency with the specified id or {@code null} if no
+ * such dependency exists.
+ */
+ public Dependency getDependency(String id) {
+ return dependencies.get(id);
+ }
+
+ /**
+ * Return the project types supported by the service.
+ */
+ public Map getProjectTypes() {
+ return projectTypes.getContent();
+ }
+
+ /**
+ * Return the default type to use or {@code null} or the metadata does
+ * not define any default.
+ */
+ public ProjectType getDefaultType() {
+ if (projectTypes.getDefaultItem() != null) {
+ return projectTypes.getDefaultItem();
+ }
+ String defaultTypeId = getDefaults().get("type");
+ if (defaultTypeId != null) {
+ return projectTypes.getContent().get(defaultTypeId);
+ }
+ return null;
+ }
+
+ /**
+ * Returns the defaults applicable to the service.
+ */
+ public Map getDefaults() {
+ return defaults;
+ }
+
+ private Map parseDependencies(JSONObject root) {
+ Map result = new HashMap();
+ if (!root.has(DEPENDENCIES_EL)) {
+ return result;
+ }
+ JSONArray array = root.getJSONArray(DEPENDENCIES_EL);
+ for (int i = 0; i < array.length(); i++) {
+ JSONObject group = array.getJSONObject(i);
+ parseGroup(group, result);
+ }
+ return result;
+ }
+
+ private MetadataHolder parseProjectTypes(JSONObject root) {
+ MetadataHolder result = new MetadataHolder();
+ if (!root.has(TYPES_EL)) {
+ return result;
+ }
+ JSONArray array = root.getJSONArray(TYPES_EL);
+ for (int i = 0; i < array.length(); i++) {
+ JSONObject typeJson = array.getJSONObject(i);
+ ProjectType projectType = parseType(typeJson);
+ result.getContent().put(projectType.getId(), projectType);
+ if (projectType.isDefaultType()) {
+ result.setDefaultItem(projectType);
+ }
+ }
+ return result;
+ }
+
+ private Map parseDefaults(JSONObject root) {
+ Map result = new HashMap();
+ if (!root.has(DEFAULTS_EL)) {
+ return result;
+ }
+ JSONObject defaults = root.getJSONObject(DEFAULTS_EL);
+ result.putAll(parseStringItems(defaults));
+ return result;
+ }
+
+ private void parseGroup(JSONObject group, Map dependencies) {
+ if (group.has(CONTENT_EL)) {
+ JSONArray content = group.getJSONArray(CONTENT_EL);
+ for (int i = 0; i < content.length(); i++) {
+ Dependency dependency = parseDependency(content.getJSONObject(i));
+ dependencies.put(dependency.getId(), dependency);
+ }
+ }
+ }
+
+ 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;
+ }
+
+ private ProjectType parseType(JSONObject object) {
+ String id = getStringValue(object, ID_ATTRIBUTE, null);
+ String name = getStringValue(object, NAME_ATTRIBUTE, null);
+ String action = getStringValue(object, ACTION_ATTRIBUTE, null);
+ boolean defaultType = getBooleanValue(object, DEFAULT_ATTRIBUTE, false);
+ Map tags = new HashMap();
+ if (object.has("tags")) {
+ JSONObject jsonTags = object.getJSONObject("tags");
+ tags.putAll(parseStringItems(jsonTags));
+ }
+ return new ProjectType(id, name, action, defaultType, tags);
+ }
+
+ private String getStringValue(JSONObject object, String name, String defaultValue) {
+ return object.has(name) ? object.getString(name) : defaultValue;
+ }
+
+ private boolean getBooleanValue(JSONObject object, String name, boolean defaultValue) {
+ return object.has(name) ? object.getBoolean(name) : defaultValue;
+ }
+
+ private Map parseStringItems(JSONObject json) {
+ Map result = new HashMap();
+ for (Object k : json.keySet()) {
+ String key = (String) k;
+ Object value = json.get(key);
+ if (value instanceof String) {
+ result.put(key, (String) value);
+ }
+ }
+ return result;
+ }
+
+ private static class MetadataHolder {
+
+ private final Map content;
+
+ private T defaultItem;
+
+ private MetadataHolder() {
+ this.content = new HashMap();
+ }
+
+ public Map getContent() {
+ return content;
+ }
+
+ public T getDefaultItem() {
+ return defaultItem;
+ }
+
+ public void setDefaultItem(T defaultItem) {
+ this.defaultItem = defaultItem;
+ }
+ }
+
+}
diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/ListMetadataCommand.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/ListMetadataCommand.java
new file mode 100644
index 0000000000..9062c7bdbc
--- /dev/null
+++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/ListMetadataCommand.java
@@ -0,0 +1,119 @@
+/*
+ * 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 = 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 dependencies = new ArrayList(metadata.getDependencies());
+ Collections.sort(dependencies, new Comparator() {
+ @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 typeIds = new ArrayList(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> it = type.getTags().entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry 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 defaultsKeys = new ArrayList(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();
+ }
+
+}
diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/ProjectGenerationException.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/ProjectGenerationException.java
new file mode 100644
index 0000000000..7237e0ff86
--- /dev/null
+++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/ProjectGenerationException.java
@@ -0,0 +1,31 @@
+/*
+ * 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;
+
+/**
+ * Thrown when a project could not be generated.
+ *
+ * @author Stephane Nicoll
+ * @since 1.2.0
+ */
+public class ProjectGenerationException extends RuntimeException {
+
+ public ProjectGenerationException(String message) {
+ super(message);
+ }
+
+}
diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/ProjectGenerationRequest.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/ProjectGenerationRequest.java
new file mode 100644
index 0000000000..527903cfdc
--- /dev/null
+++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/ProjectGenerationRequest.java
@@ -0,0 +1,257 @@
+/*
+ * 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.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.http.client.utils.URIBuilder;
+
+/**
+ * Represent the settings to apply to generating the project.
+ *
+ * @author Stephane Nicoll
+ * @since 1.2.0
+ */
+class ProjectGenerationRequest {
+
+ public static final String DEFAULT_SERVICE_URL = "https://start.spring.io";
+
+ private String serviceUrl = DEFAULT_SERVICE_URL;
+
+ private String output;
+
+ private String bootVersion;
+
+ private List dependencies = new ArrayList();
+
+ private String javaVersion;
+
+ private String packaging;
+
+ private String build;
+
+ private String format;
+
+ private boolean detectType;
+
+ private String type;
+
+ /**
+ * The url of the service to use.
+ * @see #DEFAULT_SERVICE_URL
+ */
+ public String getServiceUrl() {
+ return serviceUrl;
+ }
+
+ public void setServiceUrl(String serviceUrl) {
+ this.serviceUrl = serviceUrl;
+ }
+
+ /**
+ * The location of the generated project.
+ */
+ public String getOutput() {
+ return output;
+ }
+
+ public void setOutput(String output) {
+ this.output = output;
+ }
+
+ /**
+ * The Spring Boot version to use or {@code null} if it should not be customized.
+ */
+ public String getBootVersion() {
+ return bootVersion;
+ }
+
+ public void setBootVersion(String bootVersion) {
+ this.bootVersion = bootVersion;
+ }
+
+ /**
+ * The identifiers of the dependencies to include in the project.
+ */
+ public List getDependencies() {
+ return dependencies;
+ }
+
+ /**
+ * The Java version to use or {@code null} if it should not be customized.
+ */
+ public String getJavaVersion() {
+ return javaVersion;
+ }
+
+ public void setJavaVersion(String javaVersion) {
+ this.javaVersion = javaVersion;
+ }
+
+ /**
+ * The packaging type or {@code null} if it should not be customized.
+ */
+ public String getPackaging() {
+ return packaging;
+ }
+
+ public void setPackaging(String packaging) {
+ this.packaging = packaging;
+ }
+
+ /**
+ * The build type to use. Ignored if a type is set. Can be used alongside
+ * the {@link #getFormat() format} to identify the type to use.
+ */
+ public String getBuild() {
+ return build;
+ }
+
+ public void setBuild(String build) {
+ this.build = build;
+ }
+
+ /**
+ * The project format to use. Ignored if a type is set. Can be used alongside
+ * the {@link #getBuild() build} to identify the type to use.
+ */
+ public String getFormat() {
+ return format;
+ }
+
+ public void setFormat(String format) {
+ this.format = format;
+ }
+
+ /**
+ * Specify if the type should be detected based on the build
+ * and format value.
+ */
+ public boolean isDetectType() {
+ return detectType;
+ }
+
+ public void setDetectType(boolean detectType) {
+ this.detectType = detectType;
+ }
+
+ /**
+ * The type of project to generate. Should match one of the advertized type
+ * that the service supports. If not set, the default is retrieved from
+ * the service metadata.
+ */
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ /**
+ * Generates the URL to use to generate a project represented
+ * by this request
+ */
+ URI generateUrl(InitializrServiceMetadata metadata) {
+ try {
+ URIBuilder builder = new URIBuilder(serviceUrl);
+ StringBuilder sb = new StringBuilder();
+ if (builder.getPath() != null) {
+ sb.append(builder.getPath());
+ }
+
+ ProjectType projectType = determineProjectType(metadata);
+ this.type = projectType.getId();
+ sb.append(projectType.getAction());
+ builder.setPath(sb.toString());
+
+ if (this.bootVersion != null) {
+ builder.setParameter("bootVersion", this.bootVersion);
+ }
+ for (String dependency : dependencies) {
+ builder.addParameter("style", dependency);
+ }
+ if (this.javaVersion != null) {
+ builder.setParameter("javaVersion", this.javaVersion);
+ }
+ if (this.packaging != null) {
+ builder.setParameter("packaging", this.packaging);
+ }
+ if (this.type != null) {
+ builder.setParameter("type", projectType.getId());
+ }
+
+ return builder.build();
+ }
+ catch (URISyntaxException e) {
+ throw new ProjectGenerationException("Invalid service URL (" + e.getMessage() + ")");
+ }
+ }
+
+ protected ProjectType determineProjectType(InitializrServiceMetadata metadata) {
+ if (this.type != null) {
+ ProjectType result = metadata.getProjectTypes().get(this.type);
+ if (result == null) {
+ throw new ProjectGenerationException(("No project type with id '" + this.type +
+ "' - check the service capabilities (--list)"));
+ }
+ }
+ if (isDetectType()) {
+ Map types = new HashMap(metadata.getProjectTypes());
+ if (this.build != null) {
+ filter(types, "build", this.build);
+ }
+ if (this.format != null) {
+ filter(types, "format", this.format);
+ }
+ if (types.size() == 1) {
+ return types.values().iterator().next();
+ }
+ else if (types.size() == 0) {
+ throw new ProjectGenerationException("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 '" + 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(("No project type is set and no default is defined. " +
+ "Check the service capabilities (--list)"));
+ }
+ return defaultType;
+ }
+
+ private static void filter(Map projects, String tag, String tagValue) {
+ for (Iterator> it = projects.entrySet().iterator(); it.hasNext(); ) {
+ Map.Entry entry = it.next();
+ String value = entry.getValue().getTags().get(tag);
+ if (!tagValue.equals(value)) {
+ it.remove();
+ }
+ }
+ }
+
+}
diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/ProjectGenerationResponse.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/ProjectGenerationResponse.java
new file mode 100644
index 0000000000..d0c7b44e99
--- /dev/null
+++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/ProjectGenerationResponse.java
@@ -0,0 +1,72 @@
+/*
+ * 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 org.apache.http.entity.ContentType;
+
+/**
+ * Represent the response of a {@link ProjectGenerationRequest}
+ *
+ * @author Stephane Nicoll
+ * @since 1.2.0
+ */
+class ProjectGenerationResponse {
+
+ private ContentType contentType;
+
+ private byte[] content;
+
+ private String fileName;
+
+ ProjectGenerationResponse() {
+ }
+
+ /**
+ * Return the {@link ContentType} of this instance
+ */
+ public ContentType getContentType() {
+ return this.contentType;
+ }
+
+ public void setContentType(ContentType contentType) {
+ this.contentType = contentType;
+ }
+
+ /**
+ * The generated project archive or file.
+ */
+ public byte[] getContent() {
+ return content;
+ }
+
+ public void setContent(byte[] content) {
+ this.content = content;
+ }
+
+ /**
+ * The preferred file name to use to store the entity on disk or {@code null}
+ * if no preferred value has been set.
+ */
+ public String getFileName() {
+ return fileName;
+ }
+
+ public void setFileName(String fileName) {
+ this.fileName = fileName;
+ }
+
+}
diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/ProjectType.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/ProjectType.java
new file mode 100644
index 0000000000..e4f0aea3db
--- /dev/null
+++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/ProjectType.java
@@ -0,0 +1,70 @@
+/*
+ * 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.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Represent a project type that is supported by a service.
+ *
+ * @author Stephane Nicoll
+ * @since 1.2.0
+ */
+class ProjectType {
+
+ private final String id;
+
+ private final String name;
+
+ private final String action;
+
+ private final boolean defaultType;
+
+ private final Map tags = new HashMap();
+
+ public ProjectType(String id, String name, String action, boolean defaultType, Map tags) {
+ this.id = id;
+ this.name = name;
+ this.action = action;
+ this.defaultType = defaultType;
+ if (tags != null) {
+ this.tags.putAll(tags);
+ }
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getAction() {
+ return action;
+ }
+
+ public boolean isDefaultType() {
+ return defaultType;
+ }
+
+ public Map getTags() {
+ return Collections.unmodifiableMap(tags);
+ }
+}
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
new file mode 100644
index 0000000000..6e3d54e2e1
--- /dev/null
+++ b/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/AbstractHttpClientMockTests.java
@@ -0,0 +1,183 @@
+/*
+ * 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.IOException;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpHeaders;
+import org.apache.http.StatusLine;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.message.BasicHeader;
+import org.hamcrest.Matcher;
+import org.json.JSONObject;
+import org.mockito.ArgumentMatcher;
+
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+import org.springframework.util.StreamUtils;
+
+import static org.mockito.Mockito.*;
+
+/**
+ *
+ * @author Stephane Nicoll
+ */
+public abstract class AbstractHttpClientMockTests {
+
+ protected final CloseableHttpClient httpClient = mock(CloseableHttpClient.class);
+
+ protected void mockSuccessfulMetadataGet() throws IOException {
+ mockSuccessfulMetadataGet("1.1.0");
+ }
+
+ protected void mockSuccessfulMetadataGet(String version) 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/json");
+ mockStatus(response, 200);
+ when(httpClient.execute(argThat(getForJsonData()))).thenReturn(response);
+ }
+
+ protected void mockSuccessfulProjectGeneration(MockHttpProjectGenerationRequest request) throws IOException {
+ // Required for project generation as the metadata is read first
+ mockSuccessfulMetadataGet();
+
+ 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);
+ when(httpClient.execute(argThat(getForNonJsonData()))).thenReturn(response);
+ }
+
+ protected void mockProjectGenerationError(int status, String message) throws IOException {
+ // Required for project generation as the metadata is read first
+ mockSuccessfulMetadataGet();
+
+ CloseableHttpResponse response = mock(CloseableHttpResponse.class);
+ mockHttpEntity(response, createJsonError(status, message).getBytes(), "application/json");
+ mockStatus(response, status);
+ when(httpClient.execute(isA(HttpGet.class))).thenReturn(response);
+ }
+
+ protected void mockMetadataGetError(int status, String message) throws IOException {
+ CloseableHttpResponse response = mock(CloseableHttpResponse.class);
+ mockHttpEntity(response, createJsonError(status, message).getBytes(), "application/json");
+ mockStatus(response, status);
+ when(httpClient.execute(isA(HttpGet.class))).thenReturn(response);
+ }
+
+ protected HttpEntity mockHttpEntity(CloseableHttpResponse response, byte[] content, String contentType) {
+ try {
+ HttpEntity entity = mock(HttpEntity.class);
+ when(entity.getContent()).thenReturn(new ByteArrayInputStream(content));
+ Header contentTypeHeader = contentType != null ? new BasicHeader("Content-Type", contentType) : null;
+ when(entity.getContentType()).thenReturn(contentTypeHeader);
+ when(response.getEntity()).thenReturn(entity);
+ return entity;
+ }
+ catch (IOException e) {
+ throw new IllegalStateException("Should not happen", e);
+ }
+ }
+
+ protected void mockStatus(CloseableHttpResponse response, int status) {
+ StatusLine statusLine = mock(StatusLine.class);
+ when(statusLine.getStatusCode()).thenReturn(status);
+ when(response.getStatusLine()).thenReturn(statusLine);
+ }
+
+ protected void mockHttpHeader(CloseableHttpResponse response, String headerName, String value) {
+ Header header = value != null ? new BasicHeader(headerName, value) : null;
+ when(response.getFirstHeader(headerName)).thenReturn(header);
+ }
+
+ protected Matcher getForJsonData() {
+ return new HasAcceptHeader("application/json", true);
+ }
+
+ protected Matcher getForNonJsonData() {
+ return new HasAcceptHeader("application/json", false);
+ }
+
+ private String contentDispositionValue(String fileName) {
+ return "attachment; filename=\"" + fileName + "\"";
+ }
+
+ private String createJsonError(int status, String message) {
+ JSONObject json = new JSONObject();
+ json.put("status", status);
+ if (message != null) {
+ json.put("message", message);
+ }
+ return json.toString();
+ }
+
+ protected static class MockHttpProjectGenerationRequest {
+
+ String contentType;
+
+ String fileName;
+
+ byte[] content = new byte[] {0, 0, 0, 0};
+
+ public MockHttpProjectGenerationRequest(String contentType, String fileName, byte[] content) {
+ this.contentType = contentType;
+ this.fileName = fileName;
+ this.content = content;
+ }
+
+ public MockHttpProjectGenerationRequest(String contentType, String fileName) {
+ this(contentType, fileName, new byte[] {0, 0, 0, 0});
+ }
+ }
+
+ private static class HasAcceptHeader extends ArgumentMatcher {
+
+ private final String value;
+
+ private final boolean shouldMatch;
+
+ public HasAcceptHeader(String value, boolean shouldMatch) {
+ this.value = value;
+ this.shouldMatch = shouldMatch;
+ }
+
+ @Override
+ public boolean matches(Object argument) {
+ if (!(argument instanceof HttpGet)) {
+ return false;
+ }
+ HttpGet get = (HttpGet) argument;
+ Header acceptHeader = get.getFirstHeader(HttpHeaders.ACCEPT);
+ if (shouldMatch) {
+ return acceptHeader != null && value.equals(acceptHeader.getValue());
+ }
+ else {
+ return acceptHeader == null || !value.equals(acceptHeader.getValue());
+ }
+ }
+ }
+
+}
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
new file mode 100644
index 0000000000..cf91898abb
--- /dev/null
+++ b/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/InitCommandTests.java
@@ -0,0 +1,296 @@
+/*
+ * 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.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.UUID;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import joptsimple.OptionSet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import org.springframework.boot.cli.command.status.ExitStatus;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link InitCommand}
+ *
+ * @author Stephane Nicoll
+ */
+public class InitCommandTests extends AbstractHttpClientMockTests {
+
+ @Rule
+ public final TemporaryFolder folder = new TemporaryFolder();
+
+ private final TestableInitCommandOptionHandler handler = new TestableInitCommandOptionHandler(httpClient);
+
+ private final InitCommand command = new InitCommand(handler);
+
+ @Test
+ public void listServiceCapabilities() throws Exception {
+ mockSuccessfulMetadataGet();
+ command.run("--list", "--target=http://fake-service");
+ }
+
+ @Test
+ public void generateProject() throws Exception {
+ String fileName = UUID.randomUUID().toString() + ".zip";
+ File f = new File(fileName);
+ assertFalse("file should not exist", f.exists());
+
+ MockHttpProjectGenerationRequest mockHttpRequest =
+ new MockHttpProjectGenerationRequest("application/zip", fileName);
+ mockSuccessfulProjectGeneration(mockHttpRequest);
+
+ try {
+ assertEquals(ExitStatus.OK, command.run());
+ assertTrue("file should have been created", f.exists());
+ }
+ finally {
+ assertTrue("failed to delete test file", f.delete());
+ }
+ }
+
+ @Test
+ public void generateProjectNoFileNameAvailable() throws Exception {
+ MockHttpProjectGenerationRequest mockHttpRequest =
+ new MockHttpProjectGenerationRequest("application/zip", null);
+ mockSuccessfulProjectGeneration(mockHttpRequest);
+ assertEquals(ExitStatus.ERROR, command.run());
+ }
+
+ @Test
+ public void generateProjectAndExtract() throws Exception {
+ File f = folder.newFolder();
+
+ byte[] archive = createFakeZipArchive("test.txt", "Fake content");
+ MockHttpProjectGenerationRequest mockHttpRequest =
+ new MockHttpProjectGenerationRequest("application/zip", "demo.zip", archive);
+ mockSuccessfulProjectGeneration(mockHttpRequest);
+
+ assertEquals(ExitStatus.OK, command.run("--extract", "--output=" + f.getAbsolutePath()));
+ File archiveFile = new File(f, "test.txt");
+ assertTrue("Archive not extracted properly " + f.getAbsolutePath() + " not found", archiveFile.exists());
+ }
+
+ @Test
+ public void generateProjectAndExtractUnsupportedArchive() throws Exception {
+ File f = folder.newFolder();
+ String fileName = UUID.randomUUID().toString() + ".zip";
+ File archiveFile = new File(fileName);
+ assertFalse("file should not exist", archiveFile.exists());
+
+ try {
+ byte[] archive = createFakeZipArchive("test.txt", "Fake content");
+ MockHttpProjectGenerationRequest mockHttpRequest =
+ new MockHttpProjectGenerationRequest("application/foobar", fileName, archive);
+ mockSuccessfulProjectGeneration(mockHttpRequest);
+
+ assertEquals(ExitStatus.OK, command.run("--extract", "--output=" + f.getAbsolutePath()));
+ assertTrue("file should have been saved instead", archiveFile.exists());
+ }
+ finally {
+ assertTrue("failed to delete test file", archiveFile.delete());
+ }
+ }
+
+ @Test
+ public void generateProjectAndExtractUnknownContentType() throws Exception {
+ File f = folder.newFolder();
+ String fileName = UUID.randomUUID().toString() + ".zip";
+ File archiveFile = new File(fileName);
+ assertFalse("file should not exist", archiveFile.exists());
+
+ try {
+ byte[] archive = createFakeZipArchive("test.txt", "Fake content");
+ MockHttpProjectGenerationRequest mockHttpRequest =
+ new MockHttpProjectGenerationRequest(null, fileName, archive);
+ mockSuccessfulProjectGeneration(mockHttpRequest);
+
+ assertEquals(ExitStatus.OK, command.run("--extract", "--output=" + f.getAbsolutePath()));
+ assertTrue("file should have been saved instead", archiveFile.exists());
+ }
+ finally {
+ assertTrue("failed to delete test file", archiveFile.delete());
+ }
+ }
+
+ @Test
+ public void fileNotOverwrittenByDefault() throws Exception {
+ File f = folder.newFile();
+ long fileLength = f.length();
+
+ MockHttpProjectGenerationRequest mockHttpRequest =
+ new MockHttpProjectGenerationRequest("application/zip", f.getAbsolutePath());
+ mockSuccessfulProjectGeneration(mockHttpRequest);
+
+ assertEquals("Should have failed", ExitStatus.ERROR, command.run());
+ assertEquals("File should not have changed", fileLength, f.length());
+ }
+
+ @Test
+ public void overwriteFile() throws Exception {
+ File f = folder.newFile();
+ long fileLength = f.length();
+
+ MockHttpProjectGenerationRequest mockHttpRequest =
+ new MockHttpProjectGenerationRequest("application/zip", f.getAbsolutePath());
+ mockSuccessfulProjectGeneration(mockHttpRequest);
+ assertEquals("Should not have failed", ExitStatus.OK, command.run("--force"));
+ assertTrue("File should have changed", fileLength != f.length());
+ }
+
+ @Test
+ public void fileInArchiveNotOverwrittenByDefault() throws Exception {
+ File f = folder.newFolder();
+ File conflict = new File(f, "test.txt");
+ assertTrue("Should have been able to create file", conflict.createNewFile());
+ long fileLength = conflict.length();
+
+ // also contains test.txt
+ byte[] archive = createFakeZipArchive("test.txt", "Fake content");
+ MockHttpProjectGenerationRequest mockHttpRequest =
+ new MockHttpProjectGenerationRequest("application/zip", "demo.zip", archive);
+ mockSuccessfulProjectGeneration(mockHttpRequest);
+
+ assertEquals(ExitStatus.ERROR, command.run("--extract", "--output=" + f.getAbsolutePath()));
+ assertEquals("File should not have changed", fileLength, conflict.length());
+ }
+
+ @Test
+ public void overwriteFileInArchive() throws Exception {
+ File f = folder.newFolder();
+ File conflict = new File(f, "test.txt");
+ assertTrue("Should have been able to create file", conflict.createNewFile());
+ long fileLength = conflict.length();
+
+ // also contains test.txt
+ byte[] archive = createFakeZipArchive("test.txt", "Fake content");
+ MockHttpProjectGenerationRequest mockHttpRequest =
+ new MockHttpProjectGenerationRequest("application/zip", "demo.zip", archive);
+ mockSuccessfulProjectGeneration(mockHttpRequest);
+
+ assertEquals(ExitStatus.OK, command.run("--force", "--extract", "--output=" + f.getAbsolutePath()));
+ assertTrue("File should have changed", fileLength != conflict.length());
+ }
+
+ @Test
+ public void parseProjectOptions() throws Exception {
+ handler.disableProjectGeneration();
+ command.run("-bv=1.2.0.RELEASE", "-d=web,data-jpa", "-jv=1.9", "-p=war",
+ "--build=grunt", "--format=web", "-t=ant-project");
+
+ assertEquals("1.2.0.RELEASE", handler.lastRequest.getBootVersion());
+ List dependencies = handler.lastRequest.getDependencies();
+ assertEquals(2, dependencies.size());
+ assertTrue(dependencies.contains("web"));
+ assertTrue(dependencies.contains("data-jpa"));
+ assertEquals("1.9", handler.lastRequest.getJavaVersion());
+ assertEquals("war", handler.lastRequest.getPackaging());
+ assertEquals("grunt", handler.lastRequest.getBuild());
+ assertEquals("web", handler.lastRequest.getFormat());
+ assertEquals("ant-project", handler.lastRequest.getType());
+ }
+
+ @Test
+ public void parseTypeOnly() throws Exception {
+ handler.disableProjectGeneration();
+ command.run("-t=ant-project");
+ assertEquals("maven", handler.lastRequest.getBuild());
+ assertEquals("project", handler.lastRequest.getFormat());
+ assertFalse(handler.lastRequest.isDetectType());
+ assertEquals("ant-project", handler.lastRequest.getType());
+ }
+
+ @Test
+ public void parseBuildOnly() throws Exception {
+ handler.disableProjectGeneration();
+ command.run("--build=ant");
+ assertEquals("ant", handler.lastRequest.getBuild());
+ assertEquals("project", handler.lastRequest.getFormat());
+ assertTrue(handler.lastRequest.isDetectType());
+ assertNull(handler.lastRequest.getType());
+ }
+
+ @Test
+ public void parseFormatOnly() throws Exception {
+ handler.disableProjectGeneration();
+ command.run("--format=web");
+ assertEquals("maven", handler.lastRequest.getBuild());
+ assertEquals("web", handler.lastRequest.getFormat());
+ assertTrue(handler.lastRequest.isDetectType());
+ assertNull(handler.lastRequest.getType());
+ }
+
+ @Test
+ public void parseOutput() throws Exception {
+ handler.disableProjectGeneration();
+ command.run("--output=foobar.zip");
+
+ assertEquals("foobar.zip", handler.lastRequest.getOutput());
+ }
+
+ private byte[] createFakeZipArchive(String fileName, String content) throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ZipOutputStream zos = new ZipOutputStream(out);
+ try {
+ ZipEntry entry = new ZipEntry(fileName);
+
+ zos.putNextEntry(entry);
+ zos.write(content.getBytes());
+ zos.closeEntry();
+ }
+ finally {
+ out.close();
+ }
+ return out.toByteArray();
+ }
+
+
+ private static class TestableInitCommandOptionHandler extends InitCommandOptionHandler {
+
+ private boolean disableProjectGeneration;
+
+ ProjectGenerationRequest lastRequest;
+
+ TestableInitCommandOptionHandler(CloseableHttpClient httpClient) {
+ super(httpClient);
+ }
+
+ void disableProjectGeneration() {
+ disableProjectGeneration = true;
+ }
+
+ @Override
+ protected ExitStatus generateProject(OptionSet options, CloseableHttpClient httpClient) {
+ lastRequest = createProjectGenerationRequest(options);
+ if (!disableProjectGeneration) {
+ return super.generateProject(options, httpClient);
+ }
+ return ExitStatus.OK;
+ }
+ }
+
+}
diff --git a/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/InitializrServiceHttpInvokerTests.java b/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/InitializrServiceHttpInvokerTests.java
new file mode 100644
index 0000000000..8a35497a6c
--- /dev/null
+++ b/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/InitializrServiceHttpInvokerTests.java
@@ -0,0 +1,177 @@
+/*
+ * 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 org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static junit.framework.TestCase.assertNotNull;
+import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Matchers.isA;
+import static org.mockito.Mockito.*;
+
+/**
+ * Tests for {@link InitializrServiceHttpInvoker}
+ *
+ * @author Stephane Nicoll
+ */
+public class InitializrServiceHttpInvokerTests extends AbstractHttpClientMockTests {
+
+ @Rule
+ public final ExpectedException thrown = ExpectedException.none();
+
+ private final InitializrServiceHttpInvoker invoker = new InitializrServiceHttpInvoker(httpClient);
+
+ @Test
+ public void loadMetadata() throws IOException {
+ mockSuccessfulMetadataGet();
+ InitializrServiceMetadata metadata = invoker.loadMetadata("http://foo/bar");
+ assertNotNull(metadata);
+ }
+
+ @Test
+ public void generateSimpleProject() throws IOException {
+ ProjectGenerationRequest request = new ProjectGenerationRequest();
+ MockHttpProjectGenerationRequest mockHttpRequest =
+ new MockHttpProjectGenerationRequest("application/xml", "foo.zip");
+ ProjectGenerationResponse entity = generateProject(request, mockHttpRequest);
+ assertProjectEntity(entity, mockHttpRequest.contentType, mockHttpRequest.fileName);
+ }
+
+ @Test
+ public void generateProjectCustomTargetFilename() throws IOException {
+ ProjectGenerationRequest request = new ProjectGenerationRequest();
+ request.setOutput("bar.zip");
+ MockHttpProjectGenerationRequest mockHttpRequest =
+ new MockHttpProjectGenerationRequest("application/xml", null);
+ ProjectGenerationResponse entity = generateProject(request, mockHttpRequest);
+ assertProjectEntity(entity, mockHttpRequest.contentType, null);
+ }
+
+ @Test
+ public void generateProjectNoDefaultFileName() throws IOException {
+ ProjectGenerationRequest request = new ProjectGenerationRequest();
+ MockHttpProjectGenerationRequest mockHttpRequest =
+ new MockHttpProjectGenerationRequest("application/xml", null);
+ ProjectGenerationResponse entity = generateProject(request, mockHttpRequest);
+ assertProjectEntity(entity, mockHttpRequest.contentType, null);
+ }
+
+ @Test
+ public void generateProjectBadRequest() throws IOException {
+ String jsonMessage = "Unknown dependency foo:bar";
+ mockProjectGenerationError(400, jsonMessage);
+ ProjectGenerationRequest request = new ProjectGenerationRequest();
+ request.getDependencies().add("foo:bar");
+
+ thrown.expect(ProjectGenerationException.class);
+ thrown.expectMessage(jsonMessage);
+ invoker.generate(request);
+ }
+
+ @Test
+ public void generateProjectBadRequestNoExtraMessage() throws IOException {
+ mockProjectGenerationError(400, null);
+
+ ProjectGenerationRequest request = new ProjectGenerationRequest();
+ thrown.expect(ProjectGenerationException.class);
+ thrown.expectMessage("unexpected 400 error");
+ invoker.generate(request);
+ }
+
+ @Test
+ public void generateProjectNoContent() throws IOException {
+ mockSuccessfulMetadataGet();
+
+ CloseableHttpResponse response = mock(CloseableHttpResponse.class);
+ mockStatus(response, 500);
+ when(httpClient.execute(isA(HttpGet.class))).thenReturn(response);
+
+ ProjectGenerationRequest request = new ProjectGenerationRequest();
+
+ thrown.expect(ProjectGenerationException.class);
+ thrown.expectMessage("No content received from server");
+ invoker.generate(request);
+ }
+
+
+ @Test
+ public void loadMetadataBadRequest() throws IOException {
+ String jsonMessage = "whatever error on the server";
+ mockMetadataGetError(500, jsonMessage);
+ ProjectGenerationRequest request = new ProjectGenerationRequest();
+
+ thrown.expect(ProjectGenerationException.class);
+ thrown.expectMessage(jsonMessage);
+ invoker.generate(request);
+ }
+
+ @Test
+ public void loadMetadataInvalidJson() throws IOException {
+ CloseableHttpResponse response = mock(CloseableHttpResponse.class);
+ mockHttpEntity(response, "Foo-Bar-Not-JSON".getBytes(), "application/json");
+ mockStatus(response, 200);
+ when(httpClient.execute(isA(HttpGet.class))).thenReturn(response);
+
+ ProjectGenerationRequest request = new ProjectGenerationRequest();
+ thrown.expect(ProjectGenerationException.class);
+ thrown.expectMessage("Invalid content received from server");
+ invoker.generate(request);
+ }
+
+ @Test
+ public void loadMetadataNoContent() throws IOException {
+ CloseableHttpResponse response = mock(CloseableHttpResponse.class);
+ mockStatus(response, 500);
+ when(httpClient.execute(isA(HttpGet.class))).thenReturn(response);
+
+ ProjectGenerationRequest request = new ProjectGenerationRequest();
+
+ thrown.expect(ProjectGenerationException.class);
+ thrown.expectMessage("No content received from server");
+ invoker.generate(request);
+ }
+
+
+
+
+ private ProjectGenerationResponse generateProject(ProjectGenerationRequest request,
+ MockHttpProjectGenerationRequest mockRequest) throws IOException {
+ mockSuccessfulProjectGeneration(mockRequest);
+ ProjectGenerationResponse entity = invoker.generate(request);
+ assertArrayEquals("wrong body content", mockRequest.content, entity.getContent());
+ return entity;
+ }
+
+ private static void assertProjectEntity(ProjectGenerationResponse entity, String mimeType, String fileName) {
+ if (mimeType == null) {
+ assertNull("No content type expected", entity.getContentType());
+ }
+ else {
+ assertEquals("wrong mime type", mimeType, entity.getContentType().getMimeType());
+ }
+ assertEquals("wrong filename", fileName, entity.getFileName());
+ }
+
+}
diff --git a/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/InitializrServiceMetadataTests.java b/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/InitializrServiceMetadataTests.java
new file mode 100644
index 0000000000..a7eeb9ff2d
--- /dev/null
+++ b/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/InitializrServiceMetadataTests.java
@@ -0,0 +1,101 @@
+/*
+ * 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.nio.charset.Charset;
+
+import org.json.JSONObject;
+import org.junit.Test;
+
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+import org.springframework.util.StreamUtils;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link InitializrServiceMetadata}
+ *
+ * @author Stephane Nicoll
+ */
+public class InitializrServiceMetadataTests {
+
+
+ @Test
+ public void parseDefaults() {
+ InitializrServiceMetadata metadata = createInstance("1.0.0");
+ assertEquals("maven-project", metadata.getDefaults().get("type"));
+ assertEquals("jar", metadata.getDefaults().get("packaging"));
+ assertEquals("java", metadata.getDefaults().get("language"));
+ }
+
+ @Test
+ public void parseDependencies() {
+ InitializrServiceMetadata metadata = createInstance("1.0.0");
+ assertEquals(5, metadata.getDependencies().size());
+
+ // Security description
+ assertEquals("AOP", metadata.getDependency("aop").getName());
+ assertEquals("Security", metadata.getDependency("security").getName());
+ assertEquals("Security description", metadata.getDependency("security").getDescription());
+ assertEquals("JDBC", metadata.getDependency("jdbc").getName());
+ assertEquals("JPA", metadata.getDependency("data-jpa").getName());
+ assertEquals("MongoDB", metadata.getDependency("data-mongodb").getName());
+ }
+
+ @Test
+ public void parseTypesNoTag() {
+ InitializrServiceMetadata metadata = createInstance("1.0.0");
+ ProjectType projectType = metadata.getProjectTypes().get("maven-project");
+ assertNotNull(projectType);
+ assertEquals(0, projectType.getTags().size());
+ }
+
+ @Test
+ public void parseTypesWithTags() {
+ InitializrServiceMetadata metadata = createInstance("1.1.0");
+ ProjectType projectType = metadata.getProjectTypes().get("maven-project");
+ assertNotNull(projectType);
+ assertEquals("maven", projectType.getTags().get("build"));
+ assertEquals("project", projectType.getTags().get("format"));
+ }
+
+
+ private static InitializrServiceMetadata createInstance(String version) {
+ try {
+ return new InitializrServiceMetadata(readJson(version));
+ }
+ catch (IOException e) {
+ throw new IllegalStateException("Failed to read json", e);
+ }
+ }
+
+ private static JSONObject readJson(String version) throws IOException {
+ Resource resource = new ClassPathResource("metadata/service-metadata-" + version + ".json");
+ InputStream stream = resource.getInputStream();
+ try {
+ String json = StreamUtils.copyToString(stream, Charset.forName("UTF-8"));
+ return new JSONObject(json);
+ }
+ finally {
+ stream.close();
+ }
+ }
+
+}
diff --git a/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/ListMetadataCommandTests.java b/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/ListMetadataCommandTests.java
new file mode 100644
index 0000000000..dea940a799
--- /dev/null
+++ b/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/ListMetadataCommandTests.java
@@ -0,0 +1,45 @@
+/*
+ * 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 org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link ListMetadataCommand}
+ *
+ * @author Stephane Nicoll
+ */
+public class ListMetadataCommandTests extends AbstractHttpClientMockTests {
+
+ private final ListMetadataCommand command = new ListMetadataCommand(httpClient);
+
+ @Test
+ public void listMetadata() throws IOException {
+ mockSuccessfulMetadataGet();
+ String content = command.generateReport("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"));
+ }
+
+}
diff --git a/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/ProjectGenerationRequestTests.java b/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/ProjectGenerationRequestTests.java
new file mode 100644
index 0000000000..9c8f9e625a
--- /dev/null
+++ b/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/ProjectGenerationRequestTests.java
@@ -0,0 +1,194 @@
+/*
+ * 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.net.URISyntaxException;
+import java.nio.charset.Charset;
+import java.util.Collections;
+import java.util.Map;
+
+import org.json.JSONObject;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+import org.springframework.util.StreamUtils;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link ProjectGenerationRequest}
+ *
+ * @author Stephane Nicoll
+ */
+public class ProjectGenerationRequestTests {
+
+ public static final Map EMPTY_TAGS = Collections.emptyMap();
+
+ @Rule
+ public final ExpectedException thrown = ExpectedException.none();
+
+ private final ProjectGenerationRequest request = new ProjectGenerationRequest();
+
+ @Test
+ public void defaultSettings() {
+ assertEquals(createDefaultUrl("?type=test-type"), request.generateUrl(createDefaultMetadata()));
+ }
+
+ @Test
+ public void customServer() throws URISyntaxException {
+ String customServerUrl = "http://foo:8080/initializr";
+ request.setServiceUrl(customServerUrl);
+ request.getDependencies().add("security");
+ assertEquals(new URI(customServerUrl + "/starter.zip?style=security&type=test-type"),
+ request.generateUrl(createDefaultMetadata()));
+ }
+
+ @Test
+ public void customBootVersion() {
+ request.setBootVersion("1.2.0.RELEASE");
+ assertEquals(createDefaultUrl("?bootVersion=1.2.0.RELEASE&type=test-type"),
+ request.generateUrl(createDefaultMetadata()));
+ }
+
+ @Test
+ public void singleDependency() {
+ request.getDependencies().add("web");
+ assertEquals(createDefaultUrl("?style=web&type=test-type"),
+ request.generateUrl(createDefaultMetadata()));
+ }
+
+ @Test
+ public void multipleDependencies() {
+ request.getDependencies().add("web");
+ request.getDependencies().add("data-jpa");
+ assertEquals(createDefaultUrl("?style=web&style=data-jpa&type=test-type"),
+ request.generateUrl(createDefaultMetadata()));
+ }
+
+ @Test
+ public void customJavaVersion() {
+ request.setJavaVersion("1.8");
+ assertEquals(createDefaultUrl("?javaVersion=1.8&type=test-type"),
+ request.generateUrl(createDefaultMetadata()));
+ }
+
+ @Test
+ public void customPackaging() {
+ request.setPackaging("war");
+ assertEquals(createDefaultUrl("?packaging=war&type=test-type"),
+ request.generateUrl(createDefaultMetadata()));
+ }
+
+ @Test
+ public void customType() throws URISyntaxException {
+ ProjectType projectType = new ProjectType("custom", "Custom Type", "/foo", true, EMPTY_TAGS);
+ InitializrServiceMetadata metadata = new InitializrServiceMetadata(projectType);
+
+ request.setType("custom");
+ request.getDependencies().add("data-rest");
+ assertEquals(new URI(ProjectGenerationRequest.DEFAULT_SERVICE_URL + "/foo?style=data-rest&type=custom"),
+ request.generateUrl(metadata));
+ }
+
+ @Test
+ public void buildNoMatch() {
+ InitializrServiceMetadata metadata = readMetadata();
+ setBuildAndFormat("does-not-exist", null);
+
+ thrown.expect(ProjectGenerationException.class);
+ thrown.expectMessage("does-not-exist");
+ request.generateUrl(metadata);
+ }
+
+ @Test
+ public void buildMultipleMatch() {
+ InitializrServiceMetadata metadata = readMetadata("types-conflict");
+ setBuildAndFormat("gradle", null);
+
+ thrown.expect(ProjectGenerationException.class);
+ thrown.expectMessage("gradle-project");
+ thrown.expectMessage("gradle-project-2");
+ request.generateUrl(metadata);
+ }
+
+ @Test
+ public void buildOneMatch() {
+ InitializrServiceMetadata metadata = readMetadata();
+ setBuildAndFormat("gradle", null);
+
+ assertEquals(createDefaultUrl("?type=gradle-project"), request.generateUrl(metadata));
+ }
+
+ @Test
+ public void invalidType() throws URISyntaxException {
+ request.setType("does-not-exist");
+
+ thrown.expect(ProjectGenerationException.class);
+ request.generateUrl(createDefaultMetadata());
+ }
+
+ @Test
+ public void noTypeAndNoDefault() throws URISyntaxException {
+
+ thrown.expect(ProjectGenerationException.class);
+ thrown.expectMessage("no default is defined");
+ request.generateUrl(readMetadata("types-conflict"));
+ }
+
+
+ private static URI createDefaultUrl(String param) {
+ try {
+ return new URI(ProjectGenerationRequest.DEFAULT_SERVICE_URL + "/starter.zip" + param);
+ }
+ catch (URISyntaxException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ public void setBuildAndFormat(String build, String format) {
+ request.setBuild(build != null ? build : "maven");
+ request.setFormat(format != null ? format : "project");
+ request.setDetectType(true);
+ }
+
+ private static InitializrServiceMetadata createDefaultMetadata() {
+ ProjectType projectType = new ProjectType("test-type", "The test type", "/starter.zip", true, EMPTY_TAGS);
+ return new InitializrServiceMetadata(projectType);
+ }
+
+ private static InitializrServiceMetadata readMetadata() {
+ return readMetadata("1.1.0");
+ }
+
+ private static InitializrServiceMetadata readMetadata(String version) {
+ try {
+ Resource resource = new ClassPathResource("metadata/service-metadata-" + version + ".json");
+ String content = StreamUtils.copyToString(resource.getInputStream(), Charset.forName("UTF-8"));
+ JSONObject json = new JSONObject(content);
+ return new InitializrServiceMetadata(json);
+ }
+ catch (IOException e) {
+ throw new IllegalStateException("Failed to read metadata", e);
+ }
+ }
+
+}
diff --git a/spring-boot-cli/src/test/resources/metadata/service-metadata-1.0.0.json b/spring-boot-cli/src/test/resources/metadata/service-metadata-1.0.0.json
new file mode 100644
index 0000000000..8f53480af1
--- /dev/null
+++ b/spring-boot-cli/src/test/resources/metadata/service-metadata-1.0.0.json
@@ -0,0 +1,134 @@
+{"dependencies": [
+ {
+ "name": "Core",
+ "content": [
+ {
+ "name": "Security",
+ "id": "security",
+ "description": "Security description"
+ },
+ {
+ "name": "AOP",
+ "id": "aop"
+ }
+ ]
+ },
+ {
+ "name": "Data",
+ "content": [
+ {
+ "name": "JDBC",
+ "id": "jdbc"
+ },
+ {
+ "name": "JPA",
+ "id": "data-jpa"
+ },
+ {
+ "name": "MongoDB",
+ "id": "data-mongodb"
+ }
+ ]
+ }
+], "types": [
+ {
+ "name": "Maven POM",
+ "id": "maven-build",
+ "action": "/pom.xml",
+ "default": false
+ },
+ {
+ "name": "Maven Project",
+ "id": "maven-project",
+ "action": "/starter.zip",
+ "default": true
+ },
+ {
+ "name": "Gradle Config",
+ "id": "gradle-build",
+ "action": "/build.gradle",
+ "default": false
+ },
+ {
+ "name": "Gradle Project",
+ "id": "gradle-project",
+ "action": "/starter.zip",
+ "default": false
+ }
+], "packagings": [
+ {
+ "name": "Jar",
+ "id": "jar",
+ "default": true
+ },
+ {
+ "name": "War",
+ "id": "war",
+ "default": false
+ }
+], "javaVersions": [
+ {
+ "name": "1.6",
+ "id": "1.6",
+ "default": false
+ },
+ {
+ "name": "1.7",
+ "id": "1.7",
+ "default": true
+ },
+ {
+ "name": "1.8",
+ "id": "1.8",
+ "default": false
+ }
+], "languages": [
+ {
+ "name": "Groovy",
+ "id": "groovy",
+ "default": false
+ },
+ {
+ "name": "Java",
+ "id": "java",
+ "default": true
+ }
+], "bootVersions": [
+ {
+ "name": "1.2.0 M2",
+ "id": "1.2.0.M2",
+ "default": false
+ },
+ {
+ "name": "1.2.0 (SNAPSHOT)",
+ "id": "1.2.0.BUILD-SNAPSHOT",
+ "default": false
+ },
+ {
+ "name": "1.1.8",
+ "id": "1.1.8.RELEASE",
+ "default": true
+ },
+ {
+ "name": "1.1.8 (SNAPSHOT)",
+ "id": "1.1.8.BUILD-SNAPSHOT",
+ "default": false
+ },
+ {
+ "name": "1.0.2",
+ "id": "1.0.2.RELEASE",
+ "default": false
+ }
+], "defaults": {
+ "groupId": "org.test",
+ "artifactId": "demo",
+ "version": "0.0.1-SNAPSHOT",
+ "name": "demo",
+ "description": "Demo project for Spring Boot",
+ "packageName": "demo",
+ "type": "maven-project",
+ "packaging": "jar",
+ "javaVersion": "1.7",
+ "language": "java",
+ "bootVersion": "1.1.8.RELEASE"
+}}
\ No newline at end of file
diff --git a/spring-boot-cli/src/test/resources/metadata/service-metadata-1.1.0.json b/spring-boot-cli/src/test/resources/metadata/service-metadata-1.1.0.json
new file mode 100644
index 0000000000..e76a7f0f39
--- /dev/null
+++ b/spring-boot-cli/src/test/resources/metadata/service-metadata-1.1.0.json
@@ -0,0 +1,150 @@
+{"dependencies": [
+ {
+ "name": "Core",
+ "content": [
+ {
+ "name": "Security",
+ "id": "security",
+ "description": "Security description"
+ },
+ {
+ "name": "AOP",
+ "id": "aop"
+ }
+ ]
+ },
+ {
+ "name": "Data",
+ "content": [
+ {
+ "name": "JDBC",
+ "id": "jdbc"
+ },
+ {
+ "name": "JPA",
+ "id": "data-jpa"
+ },
+ {
+ "name": "MongoDB",
+ "id": "data-mongodb"
+ }
+ ]
+ }
+], "types": [
+ {
+ "name": "Maven POM",
+ "id": "maven-build",
+ "action": "/pom.xml",
+ "tags": {
+ "build": "maven",
+ "format": "build"
+ },
+ "default": false
+ },
+ {
+ "name": "Maven Project",
+ "id": "maven-project",
+ "action": "/starter.zip",
+ "tags": {
+ "build": "maven",
+ "format": "project"
+ },
+ "default": true
+ },
+ {
+ "name": "Gradle Config",
+ "id": "gradle-build",
+ "action": "/build.gradle",
+ "tags": {
+ "build": "gradle",
+ "format": "build"
+ },
+ "default": false
+ },
+ {
+ "name": "Gradle Project",
+ "id": "gradle-project",
+ "action": "/starter.zip",
+ "tags": {
+ "build": "gradle",
+ "format": "project"
+ },
+ "default": false
+ }
+], "packagings": [
+ {
+ "name": "Jar",
+ "id": "jar",
+ "default": true
+ },
+ {
+ "name": "War",
+ "id": "war",
+ "default": false
+ }
+], "javaVersions": [
+ {
+ "name": "1.6",
+ "id": "1.6",
+ "default": false
+ },
+ {
+ "name": "1.7",
+ "id": "1.7",
+ "default": true
+ },
+ {
+ "name": "1.8",
+ "id": "1.8",
+ "default": false
+ }
+], "languages": [
+ {
+ "name": "Groovy",
+ "id": "groovy",
+ "default": false
+ },
+ {
+ "name": "Java",
+ "id": "java",
+ "default": true
+ }
+], "bootVersions": [
+ {
+ "name": "1.2.0 M2",
+ "id": "1.2.0.M2",
+ "default": false
+ },
+ {
+ "name": "1.2.0 (SNAPSHOT)",
+ "id": "1.2.0.BUILD-SNAPSHOT",
+ "default": false
+ },
+ {
+ "name": "1.1.8",
+ "id": "1.1.8.RELEASE",
+ "default": true
+ },
+ {
+ "name": "1.1.8 (SNAPSHOT)",
+ "id": "1.1.8.BUILD-SNAPSHOT",
+ "default": false
+ },
+ {
+ "name": "1.0.2",
+ "id": "1.0.2.RELEASE",
+ "default": false
+ }
+], "defaults": {
+ "groupId": "org.test",
+ "artifactId": "demo",
+ "version": "0.0.1-SNAPSHOT",
+ "name": "demo",
+ "description": "Demo project for Spring Boot",
+ "packageName": "demo",
+ "type": "maven-project",
+ "packaging": "jar",
+ "javaVersion": "1.7",
+ "language": "java",
+ "bootVersion": "1.1.8.RELEASE"
+}}
\ No newline at end of file
diff --git a/spring-boot-cli/src/test/resources/metadata/service-metadata-types-conflict.json b/spring-boot-cli/src/test/resources/metadata/service-metadata-types-conflict.json
new file mode 100644
index 0000000000..c9f593a806
--- /dev/null
+++ b/spring-boot-cli/src/test/resources/metadata/service-metadata-types-conflict.json
@@ -0,0 +1,169 @@
+{"dependencies": [
+ {
+ "name": "Core",
+ "content": [
+ {
+ "name": "Security",
+ "id": "security",
+ "description": "Security description"
+ },
+ {
+ "name": "AOP",
+ "id": "aop"
+ }
+ ]
+ },
+ {
+ "name": "Data",
+ "content": [
+ {
+ "name": "JDBC",
+ "id": "jdbc"
+ },
+ {
+ "name": "JPA",
+ "id": "data-jpa"
+ },
+ {
+ "name": "MongoDB",
+ "id": "data-mongodb"
+ }
+ ]
+ }
+], "types": [
+ {
+ "name": "Maven POM",
+ "id": "maven-build",
+ "action": "/pom.xml",
+ "tags": {
+ "build": "maven",
+ "format": "build"
+ },
+ "default": false
+ },
+ {
+ "name": "Maven Project",
+ "id": "maven-project",
+ "action": "/starter.zip",
+ "tags": {
+ "build": "maven",
+ "format": "project"
+ },
+ "default": false
+ },
+ {
+ "name": "Another Maven Project",
+ "id": "maven-project-2",
+ "action": "/starter.zip",
+ "tags": {
+ "build": "maven",
+ "format": "project"
+ },
+ "default": false
+ },
+ {
+ "name": "Gradle Config",
+ "id": "gradle-build",
+ "action": "/build.gradle",
+ "tags": {
+ "build": "gradle",
+ "format": "build"
+ },
+ "default": false
+ },
+ {
+ "name": "Gradle Project",
+ "id": "gradle-project",
+ "action": "/starter.zip",
+ "tags": {
+ "build": "gradle",
+ "format": "project"
+ },
+ "default": false
+ },
+ {
+ "name": "Another gradle Project",
+ "id": "gradle-project-2",
+ "action": "/starter.zip",
+ "tags": {
+ "build": "gradle",
+ "format": "project"
+ },
+ "default": false
+ }
+], "packagings": [
+ {
+ "name": "Jar",
+ "id": "jar",
+ "default": true
+ },
+ {
+ "name": "War",
+ "id": "war",
+ "default": false
+ }
+], "javaVersions": [
+ {
+ "name": "1.6",
+ "id": "1.6",
+ "default": false
+ },
+ {
+ "name": "1.7",
+ "id": "1.7",
+ "default": true
+ },
+ {
+ "name": "1.8",
+ "id": "1.8",
+ "default": false
+ }
+], "languages": [
+ {
+ "name": "Groovy",
+ "id": "groovy",
+ "default": false
+ },
+ {
+ "name": "Java",
+ "id": "java",
+ "default": true
+ }
+], "bootVersions": [
+ {
+ "name": "1.2.0 M2",
+ "id": "1.2.0.M2",
+ "default": false
+ },
+ {
+ "name": "1.2.0 (SNAPSHOT)",
+ "id": "1.2.0.BUILD-SNAPSHOT",
+ "default": false
+ },
+ {
+ "name": "1.1.8",
+ "id": "1.1.8.RELEASE",
+ "default": true
+ },
+ {
+ "name": "1.1.8 (SNAPSHOT)",
+ "id": "1.1.8.BUILD-SNAPSHOT",
+ "default": false
+ },
+ {
+ "name": "1.0.2",
+ "id": "1.0.2.RELEASE",
+ "default": false
+ }
+], "defaults": {
+ "groupId": "org.test",
+ "artifactId": "demo",
+ "version": "0.0.1-SNAPSHOT",
+ "name": "demo",
+ "description": "Demo project for Spring Boot",
+ "packageName": "demo",
+ "packaging": "jar",
+ "javaVersion": "1.7",
+ "language": "java",
+ "bootVersion": "1.1.8.RELEASE"
+}}
\ No newline at end of file
diff --git a/spring-boot-dependencies/pom.xml b/spring-boot-dependencies/pom.xml
index bf807de5ff..7c0190e0eb 100644
--- a/spring-boot-dependencies/pom.xml
+++ b/spring-boot-dependencies/pom.xml
@@ -91,6 +91,7 @@
2.13
2.4
1.2.2
+ 20140107
0.9.1
1.2
4.11
@@ -987,6 +988,11 @@
jdom2
${jdom2.version}
+
+ org.json
+ json
+ ${json.version}
+
org.liquibase
liquibase-core