Commit 2e07f003 authored by Stephane Nicoll's avatar Stephane Nicoll

Init command takes the output as last argument

This commit moves the --output switch to a regular argument. This aligns
to other command, i.e. spring init my-project.zip would save the project
to "my-project.zip" in the current directory.

This commit also auto-detects the --extract option if the location ends
with a slash, i.e. spring init demo/ would extract the content of the
project in a demo directory that is local to the current directory.

Fixes gh-1802
parent 96198e29
...@@ -17,7 +17,9 @@ ...@@ -17,7 +17,9 @@
package org.springframework.boot.cli.command.init; package org.springframework.boot.cli.command.init;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
import joptsimple.OptionSet; import joptsimple.OptionSet;
import joptsimple.OptionSpec; import joptsimple.OptionSpec;
...@@ -27,6 +29,7 @@ import org.springframework.boot.cli.command.OptionParsingCommand; ...@@ -27,6 +29,7 @@ import org.springframework.boot.cli.command.OptionParsingCommand;
import org.springframework.boot.cli.command.options.OptionHandler; import org.springframework.boot.cli.command.options.OptionHandler;
import org.springframework.boot.cli.command.status.ExitStatus; import org.springframework.boot.cli.command.status.ExitStatus;
import org.springframework.boot.cli.util.Log; import org.springframework.boot.cli.util.Log;
import org.springframework.util.Assert;
/** /**
* {@link Command} that initializes a project using Spring initializr. * {@link Command} that initializes a project using Spring initializr.
...@@ -45,6 +48,11 @@ public class InitCommand extends OptionParsingCommand { ...@@ -45,6 +48,11 @@ public class InitCommand extends OptionParsingCommand {
+ "Initialzr (start.spring.io)", handler); + "Initialzr (start.spring.io)", handler);
} }
@Override
public String getUsageHelp() {
return "[options] [location]";
}
static class InitOptionHandler extends OptionHandler { static class InitOptionHandler extends OptionHandler {
private final ServiceCapabilitiesReportGenerator serviceCapabilitiesReport; private final ServiceCapabilitiesReportGenerator serviceCapabilitiesReport;
...@@ -73,8 +81,6 @@ public class InitCommand extends OptionParsingCommand { ...@@ -73,8 +81,6 @@ public class InitCommand extends OptionParsingCommand {
private OptionSpec<Void> force; private OptionSpec<Void> force;
private OptionSpec<String> output;
InitOptionHandler(InitializrService initializrService) { InitOptionHandler(InitializrService initializrService) {
this.serviceCapabilitiesReport = new ServiceCapabilitiesReportGenerator( this.serviceCapabilitiesReport = new ServiceCapabilitiesReportGenerator(
initializrService); initializrService);
...@@ -123,14 +129,9 @@ public class InitCommand extends OptionParsingCommand { ...@@ -123,14 +129,9 @@ public class InitCommand extends OptionParsingCommand {
private void otherOptions() { private void otherOptions() {
this.extract = option(Arrays.asList("extract", "x"), this.extract = option(Arrays.asList("extract", "x"),
"Extract the project archive"); "Extract the project archive. Inferred if a location is specified and ends with /");
this.force = option(Arrays.asList("force", "f"), this.force = option(Arrays.asList("force", "f"),
"Force overwrite of existing files"); "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 @Override
...@@ -160,12 +161,17 @@ public class InitCommand extends OptionParsingCommand { ...@@ -160,12 +161,17 @@ public class InitCommand extends OptionParsingCommand {
protected void generateProject(OptionSet options) throws IOException { protected void generateProject(OptionSet options) throws IOException {
ProjectGenerationRequest request = createProjectGenerationRequest(options); ProjectGenerationRequest request = createProjectGenerationRequest(options);
this.projectGenerator.generateProject(request, options.has(this.force), this.projectGenerator.generateProject(request, options.has(this.force));
options.has(this.extract), options.valueOf(this.output));
} }
protected ProjectGenerationRequest createProjectGenerationRequest( protected ProjectGenerationRequest createProjectGenerationRequest(
OptionSet options) { OptionSet options) {
List<?> nonOptionArguments = new ArrayList<Object>(
options.nonOptionArguments());
Assert.isTrue(nonOptionArguments.size() <= 1,
"Only the target location may be specified");
ProjectGenerationRequest request = new ProjectGenerationRequest(); ProjectGenerationRequest request = new ProjectGenerationRequest();
request.setServiceUrl(options.valueOf(this.target)); request.setServiceUrl(options.valueOf(this.target));
if (options.has(this.bootVersion)) { if (options.has(this.bootVersion)) {
...@@ -188,8 +194,10 @@ public class InitCommand extends OptionParsingCommand { ...@@ -188,8 +194,10 @@ public class InitCommand extends OptionParsingCommand {
if (options.has(this.type)) { if (options.has(this.type)) {
request.setType(options.valueOf(this.type)); request.setType(options.valueOf(this.type));
} }
if (options.has(this.output)) { request.setExtract(options.has(this.extract));
request.setOutput(options.valueOf(this.output)); if (nonOptionArguments.size() == 1) {
String output = (String) nonOptionArguments.get(0);
request.setOutput(output);
} }
return request; return request;
} }
......
...@@ -40,6 +40,8 @@ class ProjectGenerationRequest { ...@@ -40,6 +40,8 @@ class ProjectGenerationRequest {
private String output; private String output;
private boolean extract;
private String bootVersion; private String bootVersion;
private List<String> dependencies = new ArrayList<String>(); private List<String> dependencies = new ArrayList<String>();
...@@ -76,7 +78,25 @@ class ProjectGenerationRequest { ...@@ -76,7 +78,25 @@ class ProjectGenerationRequest {
} }
public void setOutput(String output) { public void setOutput(String output) {
this.output = output; if (output != null && output.endsWith("/")) {
this.output = output.substring(0, output.length() - 1);
this.extract = true;
} else {
this.output = output;
}
}
/**
* Specify if the project archive should be extract in the output location. If
* the {@link #getOutput() output} ends with "/", the project is extracted
* automatically.
*/
public boolean isExtract() {
return extract;
}
public void setExtract(boolean extract) {
this.extract = extract;
} }
/** /**
......
...@@ -41,24 +41,23 @@ public class ProjectGenerator { ...@@ -41,24 +41,23 @@ public class ProjectGenerator {
this.initializrService = initializrService; this.initializrService = initializrService;
} }
public void generateProject(ProjectGenerationRequest request, boolean force, public void generateProject(ProjectGenerationRequest request, boolean force) throws IOException {
boolean extract, String output) throws IOException {
ProjectGenerationResponse response = this.initializrService.generate(request); ProjectGenerationResponse response = this.initializrService.generate(request);
if (extract) { String fileName = (request.getOutput() != null ? request.getOutput() : response.getFileName());
if (request.isExtract()) {
if (isZipArchive(response)) { if (isZipArchive(response)) {
extractProject(response, output, force); extractProject(response, request.getOutput(), force);
return; return;
} }
else { else {
Log.info("Could not extract '" + response.getContentType() + "'"); Log.info("Could not extract '" + response.getContentType() + "'");
fileName = response.getFileName(); // Use value from the server since we can't extract it
} }
} }
String fileName = response.getFileName();
fileName = (fileName != null ? fileName : output);
if (fileName == null) { if (fileName == null) {
throw new ReportableException( throw new ReportableException(
"Could not save the project, the server did not set a preferred " "Could not save the project, the server did not set a preferred "
+ "file name. Use --output to specify the output location " + "file name and no location was set. Specify the output location "
+ "for the project."); + "for the project.");
} }
writeProject(response, fileName, force); writeProject(response, fileName, force);
...@@ -102,7 +101,7 @@ public class ProjectGenerator { ...@@ -102,7 +101,7 @@ public class ProjectGenerator {
throw new ReportableException(file.isDirectory() ? "Directory" : "File" throw new ReportableException(file.isDirectory() ? "Directory" : "File"
+ " '" + file.getName() + " '" + file.getName()
+ "' already exists. Use --force if you want to overwrite or " + "' already exists. Use --force if you want to overwrite or "
+ "--output to specify an alternate location."); + "specify an alternate location.");
} }
if (!entry.isDirectory()) { if (!entry.isDirectory()) {
FileCopyUtils.copy(StreamUtils.nonClosing(zipStream), FileCopyUtils.copy(StreamUtils.nonClosing(zipStream),
...@@ -123,7 +122,7 @@ public class ProjectGenerator { ...@@ -123,7 +122,7 @@ public class ProjectGenerator {
if (!overwrite) { if (!overwrite) {
throw new ReportableException("File '" + outputFile.getName() throw new ReportableException("File '" + outputFile.getName()
+ "' already exists. Use --force if you want to " + "' already exists. Use --force if you want to "
+ "overwrite or --output to specify an alternate location."); + "overwrite or specify an alternate location.");
} }
if (!outputFile.delete()) { if (!outputFile.delete()) {
throw new ReportableException("Failed to delete existing file " throw new ReportableException("Failed to delete existing file "
......
...@@ -95,7 +95,21 @@ public class InitCommandTests extends AbstractHttpClientMockTests { ...@@ -95,7 +95,21 @@ public class InitCommandTests extends AbstractHttpClientMockTests {
"application/zip", "demo.zip", archive); "application/zip", "demo.zip", archive);
mockSuccessfulProjectGeneration(request); mockSuccessfulProjectGeneration(request);
assertEquals(ExitStatus.OK, assertEquals(ExitStatus.OK,
this.command.run("--extract", "--output=" + folder.getAbsolutePath())); this.command.run("--extract", folder.getAbsolutePath()));
File archiveFile = new File(folder, "test.txt");
assertTrue("Archive not extracted properly " + folder.getAbsolutePath()
+ " not found", archiveFile.exists());
}
@Test
public void generateProjectAndExtractWithConvention() throws Exception {
File folder = this.temporaryFolder.newFolder();
byte[] archive = createFakeZipArchive("test.txt", "Fake content");
MockHttpProjectGenerationRequest request = new MockHttpProjectGenerationRequest(
"application/zip", "demo.zip", archive);
mockSuccessfulProjectGeneration(request);
assertEquals(ExitStatus.OK,
this.command.run(folder.getAbsolutePath()+ "/"));
File archiveFile = new File(folder, "test.txt"); File archiveFile = new File(folder, "test.txt");
assertTrue("Archive not extracted properly " + folder.getAbsolutePath() assertTrue("Archive not extracted properly " + folder.getAbsolutePath()
+ " not found", archiveFile.exists()); + " not found", archiveFile.exists());
...@@ -113,7 +127,7 @@ public class InitCommandTests extends AbstractHttpClientMockTests { ...@@ -113,7 +127,7 @@ public class InitCommandTests extends AbstractHttpClientMockTests {
"application/foobar", fileName, archive); "application/foobar", fileName, archive);
mockSuccessfulProjectGeneration(request); mockSuccessfulProjectGeneration(request);
assertEquals(ExitStatus.OK, assertEquals(ExitStatus.OK,
this.command.run("--extract", "--output=" + folder.getAbsolutePath())); this.command.run("--extract", folder.getAbsolutePath()));
assertTrue("file should have been saved instead", file.exists()); assertTrue("file should have been saved instead", file.exists());
} }
finally { finally {
...@@ -133,7 +147,7 @@ public class InitCommandTests extends AbstractHttpClientMockTests { ...@@ -133,7 +147,7 @@ public class InitCommandTests extends AbstractHttpClientMockTests {
null, fileName, archive); null, fileName, archive);
mockSuccessfulProjectGeneration(request); mockSuccessfulProjectGeneration(request);
assertEquals(ExitStatus.OK, assertEquals(ExitStatus.OK,
this.command.run("--extract", "--output=" + folder.getAbsolutePath())); this.command.run("--extract", folder.getAbsolutePath()));
assertTrue("file should have been saved instead", file.exists()); assertTrue("file should have been saved instead", file.exists());
} }
finally { finally {
...@@ -175,7 +189,7 @@ public class InitCommandTests extends AbstractHttpClientMockTests { ...@@ -175,7 +189,7 @@ public class InitCommandTests extends AbstractHttpClientMockTests {
"application/zip", "demo.zip", archive); "application/zip", "demo.zip", archive);
mockSuccessfulProjectGeneration(request); mockSuccessfulProjectGeneration(request);
assertEquals(ExitStatus.ERROR, assertEquals(ExitStatus.ERROR,
this.command.run("--extract", "--output=" + folder.getAbsolutePath())); this.command.run("--extract", folder.getAbsolutePath()));
assertEquals("File should not have changed", fileLength, conflict.length()); assertEquals("File should not have changed", fileLength, conflict.length());
} }
...@@ -192,8 +206,7 @@ public class InitCommandTests extends AbstractHttpClientMockTests { ...@@ -192,8 +206,7 @@ public class InitCommandTests extends AbstractHttpClientMockTests {
mockSuccessfulProjectGeneration(request); mockSuccessfulProjectGeneration(request);
assertEquals( assertEquals(
ExitStatus.OK, ExitStatus.OK,
this.command.run("--force", "--extract", this.command.run("--force", "--extract", folder.getAbsolutePath()));
"--output=" + folder.getAbsolutePath()));
assertTrue("File should have changed", fileLength != conflict.length()); assertTrue("File should have changed", fileLength != conflict.length());
} }
...@@ -245,12 +258,26 @@ public class InitCommandTests extends AbstractHttpClientMockTests { ...@@ -245,12 +258,26 @@ public class InitCommandTests extends AbstractHttpClientMockTests {
} }
@Test @Test
public void parseOutput() throws Exception { public void parseLocation() throws Exception {
this.handler.disableProjectGeneration(); this.handler.disableProjectGeneration();
this.command.run("--output=foobar.zip"); this.command.run("foobar.zip");
assertEquals("foobar.zip", this.handler.lastRequest.getOutput()); assertEquals("foobar.zip", this.handler.lastRequest.getOutput());
} }
@Test
public void parseLocationWithSlash() throws Exception {
this.handler.disableProjectGeneration();
this.command.run("foobar/");
assertEquals("foobar", this.handler.lastRequest.getOutput());
assertTrue(this.handler.lastRequest.isExtract());
}
@Test
public void parseMoreThanOneArg() throws Exception {
this.handler.disableProjectGeneration();
assertEquals(ExitStatus.ERROR, this.command.run("foobar", "barfoo"));
}
private byte[] createFakeZipArchive(String fileName, String content) private byte[] createFakeZipArchive(String fileName, String content)
throws IOException { throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream(); ByteArrayOutputStream bos = new ByteArrayOutputStream();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment