Move spring-boot-cli into spring-boot-tools
Closes gh-32492
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2012-2022 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
|
||||
*
|
||||
* https://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;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import org.springframework.boot.cli.infrastructure.CommandLineInvoker;
|
||||
import org.springframework.boot.cli.infrastructure.CommandLineInvoker.Invocation;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration Tests for the command line application.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class CommandLineIT {
|
||||
|
||||
private CommandLineInvoker cli;
|
||||
|
||||
@BeforeEach
|
||||
void setup(@TempDir File tempDir) {
|
||||
this.cli = new CommandLineInvoker(tempDir);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hintProducesListOfValidCommands() throws IOException, InterruptedException {
|
||||
Invocation cli = this.cli.invoke("hint");
|
||||
assertThat(cli.await()).isEqualTo(0);
|
||||
assertThat(cli.getErrorOutput()).isEmpty();
|
||||
assertThat(cli.getStandardOutputLines()).hasSize(5);
|
||||
}
|
||||
|
||||
@Test
|
||||
void invokingWithNoArgumentsDisplaysHelp() throws IOException, InterruptedException {
|
||||
Invocation cli = this.cli.invoke();
|
||||
assertThat(cli.await()).isEqualTo(1);
|
||||
assertThat(cli.getErrorOutput()).isEmpty();
|
||||
assertThat(cli.getStandardOutput()).startsWith("usage:");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unrecognizedCommandsAreHandledGracefully() throws IOException, InterruptedException {
|
||||
Invocation cli = this.cli.invoke("not-a-real-command");
|
||||
assertThat(cli.await()).isEqualTo(1);
|
||||
assertThat(cli.getErrorOutput()).contains("'not-a-real-command' is not a valid command");
|
||||
assertThat(cli.getStandardOutput()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void version() throws IOException, InterruptedException {
|
||||
Invocation cli = this.cli.invoke("version");
|
||||
assertThat(cli.await()).isEqualTo(0);
|
||||
assertThat(cli.getErrorOutput()).isEmpty();
|
||||
assertThat(cli.getStandardOutput()).startsWith("Spring CLI v");
|
||||
}
|
||||
|
||||
@Test
|
||||
void help() throws IOException, InterruptedException {
|
||||
Invocation cli = this.cli.invoke("help");
|
||||
assertThat(cli.await()).isEqualTo(1);
|
||||
assertThat(cli.getErrorOutput()).isEmpty();
|
||||
assertThat(cli.getStandardOutput()).startsWith("usage:");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* Copyright 2012-2021 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
|
||||
*
|
||||
* https://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.infrastructure;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringReader;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import org.springframework.boot.testsupport.BuildOutput;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
/**
|
||||
* Utility to invoke the command line in the same way as a user would, i.e. via the shell
|
||||
* script in the package's bin directory.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public final class CommandLineInvoker {
|
||||
|
||||
private final File workingDirectory;
|
||||
|
||||
private final File temp;
|
||||
|
||||
public CommandLineInvoker(File temp) {
|
||||
this(new File("."), temp);
|
||||
}
|
||||
|
||||
public CommandLineInvoker(File workingDirectory, File temp) {
|
||||
this.workingDirectory = workingDirectory;
|
||||
this.temp = temp;
|
||||
}
|
||||
|
||||
public Invocation invoke(String... args) throws IOException {
|
||||
return new Invocation(runCliProcess(args));
|
||||
}
|
||||
|
||||
private Process runCliProcess(String... args) throws IOException {
|
||||
Path m2 = this.temp.toPath().resolve(".m2");
|
||||
Files.createDirectories(m2);
|
||||
Files.copy(Paths.get("src", "intTest", "resources", "settings.xml"), m2.resolve("settings.xml"),
|
||||
StandardCopyOption.REPLACE_EXISTING);
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(findLaunchScript().getAbsolutePath());
|
||||
command.addAll(Arrays.asList(args));
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(command).directory(this.workingDirectory);
|
||||
processBuilder.environment().put("JAVA_OPTS", "-Duser.home=" + this.temp);
|
||||
processBuilder.environment().put("JAVA_HOME", System.getProperty("java.home"));
|
||||
return processBuilder.start();
|
||||
}
|
||||
|
||||
private File findLaunchScript() throws IOException {
|
||||
File unpacked = new File(this.temp, "unpacked-cli");
|
||||
if (!unpacked.isDirectory()) {
|
||||
File zip = new File(new BuildOutput(getClass()).getRootLocation(),
|
||||
"distributions/spring-boot-cli-" + Versions.getBootVersion() + "-bin.zip");
|
||||
try (ZipInputStream input = new ZipInputStream(new FileInputStream(zip))) {
|
||||
ZipEntry entry;
|
||||
while ((entry = input.getNextEntry()) != null) {
|
||||
File file = new File(unpacked, entry.getName());
|
||||
if (entry.isDirectory()) {
|
||||
file.mkdirs();
|
||||
}
|
||||
else {
|
||||
file.getParentFile().mkdirs();
|
||||
try (FileOutputStream output = new FileOutputStream(file)) {
|
||||
StreamUtils.copy(input, output);
|
||||
if (entry.getName().endsWith("/bin/spring")) {
|
||||
file.setExecutable(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File bin = new File(unpacked.listFiles()[0], "bin");
|
||||
File launchScript = new File(bin, isWindows() ? "spring.bat" : "spring");
|
||||
Assert.state(launchScript.exists() && launchScript.isFile(),
|
||||
() -> "Could not find CLI launch script " + launchScript.getAbsolutePath());
|
||||
return launchScript;
|
||||
}
|
||||
|
||||
private boolean isWindows() {
|
||||
return File.separatorChar == '\\';
|
||||
}
|
||||
|
||||
/**
|
||||
* An ongoing Process invocation.
|
||||
*/
|
||||
public static final class Invocation {
|
||||
|
||||
private final StringBuffer err = new StringBuffer();
|
||||
|
||||
private final StringBuffer out = new StringBuffer();
|
||||
|
||||
private final StringBuffer combined = new StringBuffer();
|
||||
|
||||
private final Process process;
|
||||
|
||||
private final List<Thread> streamReaders = new ArrayList<>();
|
||||
|
||||
public Invocation(Process process) {
|
||||
this.process = process;
|
||||
this.streamReaders
|
||||
.add(new Thread(new StreamReadingRunnable(this.process.getErrorStream(), this.err, this.combined)));
|
||||
this.streamReaders
|
||||
.add(new Thread(new StreamReadingRunnable(this.process.getInputStream(), this.out, this.combined)));
|
||||
for (Thread streamReader : this.streamReaders) {
|
||||
streamReader.start();
|
||||
}
|
||||
}
|
||||
|
||||
public String getOutput() {
|
||||
return postProcessLines(getLines(this.combined));
|
||||
}
|
||||
|
||||
public String getErrorOutput() {
|
||||
return postProcessLines(getLines(this.err));
|
||||
}
|
||||
|
||||
public String getStandardOutput() {
|
||||
return postProcessLines(getStandardOutputLines());
|
||||
}
|
||||
|
||||
public List<String> getStandardOutputLines() {
|
||||
return getLines(this.out);
|
||||
}
|
||||
|
||||
private String postProcessLines(List<String> lines) {
|
||||
StringWriter out = new StringWriter();
|
||||
PrintWriter printOut = new PrintWriter(out);
|
||||
for (String line : lines) {
|
||||
if (!line.startsWith("Maven settings decryption failed")) {
|
||||
printOut.println(line);
|
||||
}
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
private List<String> getLines(StringBuffer buffer) {
|
||||
BufferedReader reader = new BufferedReader(new StringReader(buffer.toString()));
|
||||
return reader.lines().filter((line) -> !line.startsWith("Picked up ")).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public int await() throws InterruptedException {
|
||||
for (Thread streamReader : this.streamReaders) {
|
||||
streamReader.join();
|
||||
}
|
||||
return this.process.waitFor();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Runnable} to copy stream output.
|
||||
*/
|
||||
private final class StreamReadingRunnable implements Runnable {
|
||||
|
||||
private final InputStream stream;
|
||||
|
||||
private final StringBuffer[] outputs;
|
||||
|
||||
private final byte[] buffer = new byte[4096];
|
||||
|
||||
private StreamReadingRunnable(InputStream stream, StringBuffer... outputs) {
|
||||
this.stream = stream;
|
||||
this.outputs = outputs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
int read;
|
||||
try {
|
||||
while ((read = this.stream.read(this.buffer)) > 0) {
|
||||
for (StringBuffer output : this.outputs) {
|
||||
output.append(new String(this.buffer, 0, read));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
// Allow thread to die
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2012-2021 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
|
||||
*
|
||||
* https://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.infrastructure;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* Provides access to the current Boot version by referring to {@code gradle.properties}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
final class Versions {
|
||||
|
||||
private Versions() {
|
||||
}
|
||||
|
||||
static String getBootVersion() {
|
||||
Properties gradleProperties = new Properties();
|
||||
try (FileInputStream input = new FileInputStream("../../../gradle.properties")) {
|
||||
gradleProperties.load(input);
|
||||
return gradleProperties.getProperty("version");
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<settings>
|
||||
<localRepository>../../../../build/local-m2-repository</localRepository>
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>cli-test-repo</id>
|
||||
<activation>
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>local.central</id>
|
||||
<url>file:../../../../build/test-repository</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>thymeleaf-snapshot</id>
|
||||
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
</profile>
|
||||
</profiles>
|
||||
</settings>
|
||||
@@ -0,0 +1,44 @@
|
||||
SPRING BOOT CLI - INSTALLATION
|
||||
==============================
|
||||
|
||||
Thank you for downloading the Spring Boot CLI tool. Please follow these instructions
|
||||
in order to complete your installation.
|
||||
|
||||
|
||||
Prerequisites
|
||||
-------------
|
||||
Spring Boot CLI requires Java JDK v1.8 or above in order to run. Groovy v${groovy.version}
|
||||
is packaged as part of this distribution, and therefore does not need to be installed (any
|
||||
existing Groovy installation is ignored).
|
||||
|
||||
The CLI will use whatever JDK it finds on your path, to check that you have an appropriate
|
||||
version you should run:
|
||||
|
||||
java -version
|
||||
|
||||
Alternatively, you can set the JAVA_HOME environment variable to point a suitable JDK.
|
||||
|
||||
|
||||
Environment Variables
|
||||
---------------------
|
||||
No specific environment variables are required to run the CLI, however, you may want to
|
||||
set SPRING_HOME to point to a specific installation. You should also add SPRING_HOME/bin
|
||||
to your PATH environment variable.
|
||||
|
||||
|
||||
Shell Completion
|
||||
----------------
|
||||
Shell auto-completion scripts are provided for BASH and ZSH. Add symlinks to the appropriate
|
||||
location for your environment. For example, something like:
|
||||
|
||||
ln -s ./shell-completion/bash/spring /etc/bash_completion.d/spring
|
||||
ln -s ./shell-completion/zsh/_spring /usr/local/share/zsh/site-functions/_spring
|
||||
|
||||
|
||||
Checking Your Installation
|
||||
--------------------------
|
||||
To test if you have successfully installed the CLI you can run the following command:
|
||||
|
||||
spring --version
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
Copyright 2012-2013 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
|
||||
|
||||
https://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.
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
:setSpringHome
|
||||
@rem Setup SPRING_HOME if not already defined
|
||||
if defined SPRING_HOME goto setJavaHome
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set SPRING_HOME=%DIRNAME%\..
|
||||
|
||||
:setJavaHome
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto runSpring
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
if exist "%JAVA_EXE%" goto runSpring
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
goto fail
|
||||
|
||||
:runSpring
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
if "%@eval[2+2]" == "4" goto 4NT_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
goto execute
|
||||
|
||||
:4NT_args
|
||||
@rem Get arguments from the 4NT Shell from JP Software
|
||||
set CMD_LINE_ARGS=%$
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%SPRING_HOME%\lib\*
|
||||
"%JAVA_EXE%" %JAVA_OPTS% -cp "%CLASSPATH%" org.springframework.boot.loader.JarLauncher %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable SPRING_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%SPRING_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
open_source_licenses.txt
|
||||
|
||||
Spring Boot CLI
|
||||
==================================================================
|
||||
|
||||
Pivotal makes available all content in this download ("Content").
|
||||
Unless otherwise indicated below, the Content is provided to you under
|
||||
the terms and conditions of the Apache License 2.0 (the "License"). A
|
||||
copy of the license is available in the file called LICENSE.txt or you
|
||||
may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
The following copyright statements and licenses apply to various open
|
||||
source software packages (or portions thereof) that are distributed with
|
||||
this content.
|
||||
|
||||
|
||||
=================================================================
|
||||
TABLE OF CONTENTS
|
||||
=================================================================
|
||||
|
||||
|
||||
The following is a listing of the open source components detailed in this
|
||||
document. This list is provided for your convenience; please read further if
|
||||
you wish to review the copyright notice(s) and the full text of the license
|
||||
associated with each component.
|
||||
|
||||
|
||||
SECTION 1: BSD-STYLE, MIT-STYLE, OR SIMILAR STYLE LICENSES
|
||||
|
||||
>>> JLine (jline:jline)
|
||||
>>> JOpt Simple (net.sf.jopt-simple:jopt-simple)
|
||||
>>> ASM 4.0 (org.ow2.asm:asm)
|
||||
|
||||
|
||||
SECTION 2: Apache License, V2.0
|
||||
|
||||
>>> JSON library from Android SDK (com.vaadin.external.google:android-json)
|
||||
>>> Apache Commons Codec (commons-codec:commons-codec)
|
||||
>>> Apache HttpClient (org.apache.httpcomponents:httpclient)
|
||||
>>> Apache HttpCore (org.apache.httpcomponents:httpcore)
|
||||
>>> Plexus Cipher: encryption/decryption Component (org.sonatype.plexus:plexus-cipher)
|
||||
>>> Plexus Security Dispatcher Component (org.sonatype.plexus:plexus-sec-dispatcher)
|
||||
>>> Apache Commons Logging (commons-logging:commons-logging)
|
||||
>>> Apache Groovy (org.apache.groovy:groovy)
|
||||
>>> Maven Aether Provider (org.apache.maven:maven-aether-provider)
|
||||
>>> Maven Model (org.apache.maven:maven-model)
|
||||
>>> Maven Model Builder (org.apache.maven:maven-model-builder)
|
||||
>>> Maven Repository Metadata Model (org.apache.maven:maven-repository-metadata)
|
||||
>>> Maven Settings (org.apache.maven:maven-settings)
|
||||
>>> Maven Settings Builder (org.apache.maven:maven-settings-builder)
|
||||
>>> Plexus :: Component Annotations (org.codehaus.plexus:plexus-component-annotations)
|
||||
>>> Plexus Common Utilities (org.codehaus.plexus:plexus-utils)
|
||||
>>> Plexus Component API (org.codehaus.plexus:plexus-component-api)
|
||||
>>> Plexus Interpolation API (org.codehaus.plexus:plexus-interpolation)
|
||||
|
||||
|
||||
SECTION 3: Eclipse Public License, Version 1.0
|
||||
|
||||
>>> Aether API (org.eclipse.aether:aether-api)
|
||||
>>> Aether Connector Basic (org.eclipse.aether:aether-connector-basic)
|
||||
>>> Aether Implementation (org.eclipse.aether:aether-impl)
|
||||
>>> Aether SPI (org.eclipse.aether:aether-spi)
|
||||
>>> Aether Transport File (org.eclipse.aether:aether-transport-file)
|
||||
>>> Aether Transport HTTP (org.eclipse.aether:aether-transport-http)
|
||||
>>> Aether Utilities (org.eclipse.aether:aether-util)
|
||||
|
||||
|
||||
|
||||
--------------- SECTION 1: BSD-STYLE, MIT-STYLE, OR SIMILAR STYLE LICENSES ----------
|
||||
|
||||
BSD-STYLE, MIT-STYLE, OR SIMILAR STYLE LICENSES are applicable to the following component(s).
|
||||
|
||||
|
||||
>>> JLine (jline:jline)
|
||||
|
||||
Copyright (c) 2002-2006, Marc Prud'hommeaux <mwp1@cornell.edu>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or
|
||||
without modification, are permitted provided that the following
|
||||
conditions are met:
|
||||
|
||||
Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with
|
||||
the distribution.
|
||||
|
||||
Neither the name of JLine nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this
|
||||
software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
|
||||
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
|
||||
EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
|
||||
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
|
||||
AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
||||
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
|
||||
OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
>>> net.sf.jopt-simple:jopt-simple:4.5
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) <year> <copyright holders>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
>>> org.ow2.asm:asm
|
||||
|
||||
Copyright (c) 2000-2011 INRIA, France Telecom
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holders nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
||||
THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
|
||||
--------------- SECTION 2: Apache License, V2.0 ----------
|
||||
|
||||
Apache License, V2.0 is applicable to the following component(s).
|
||||
|
||||
|
||||
>>> org.apache.httpcomponents:httpclient
|
||||
>>> org.apache.httpcomponents:httpcore
|
||||
>>> org.sonatype.plexus:plexus-cipher
|
||||
>>> org.sonatype.plexus:plexus-sec-dispatcher
|
||||
>>> commons-logging:commons-logging
|
||||
>>> org.apache.groovy:groovy
|
||||
>>> org.apache.maven:maven-aether-provider
|
||||
>>> org.apache.maven:maven-model
|
||||
>>> org.apache.maven:maven-model-builder
|
||||
>>> org.apache.maven:maven-repository-metadata
|
||||
>>> org.apache.maven:maven-settings
|
||||
>>> org.apache.maven:maven-settings-builder
|
||||
>>> org.codehaus.plexus:plexus-component-annotations
|
||||
>>> org.codehaus.plexus:plexus-utils
|
||||
>>> org.codehaus.plexus:plexus-component-api
|
||||
>>> org.codehaus.plexus:plexus-interpolation
|
||||
|
||||
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
|
||||
|
||||
https://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
|
||||
|
||||
>>> CGLIB 3.0 (cglib:cglib:3.0):
|
||||
|
||||
Per the LICENSE file in the CGLIB JAR distribution downloaded from
|
||||
https://sourceforge.net/projects/cglib/files/cglib3/3.0/cglib-3.0.jar/download,
|
||||
CGLIB 3.0 is licensed under the Apache License, version 2.0, the text of which
|
||||
is included above.
|
||||
|
||||
|
||||
--------------- SECTION 3: Eclipse Public License, Version 1.0 ----------
|
||||
|
||||
Eclipse Public License, Version 1.0 is applicable to the following component(s).
|
||||
|
||||
>>> org.eclipse.aether:aether-api
|
||||
>>> org.eclipse.aether:aether-connector-basic
|
||||
>>> org.eclipse.aether:aether-impl
|
||||
>>> org.eclipse.aether:aether-spi
|
||||
>>> org.eclipse.aether:aether-transport-file
|
||||
>>> org.eclipse.aether:aether-transport-http
|
||||
>>> org.eclipse.aether:aether-util
|
||||
|
||||
The Eclipse Foundation makes available all content in this plug-in ("Content").
|
||||
Unless otherwise indicated below, the Content is provided to you under the terms
|
||||
and conditions of the Eclipse Public License Version 1.0 ("EPL"). A copy of the
|
||||
EPL is available at https://www.eclipse.org/legal/epl-v10.html.
|
||||
|
||||
For purposes of the EPL, "Program" will mean the Content.
|
||||
|
||||
If you did not receive this Content directly from the Eclipse Foundation, the
|
||||
Content is being redistributed by another party ("Redistributor") and different
|
||||
terms and conditions may apply to your use of any object code in the Content.
|
||||
Check the Redistributor's license that was provided with the Content. If no such
|
||||
license exists, contact the Redistributor. Unless otherwise indicated below, the
|
||||
terms and conditions of the EPL still apply to any source code in the Content and
|
||||
such source code may be obtained at https://www.eclipse.org/
|
||||
|
||||
|
||||
|
||||
===========================================================================
|
||||
|
||||
To the extent any open source subcomponents are licensed under the EPL and/or
|
||||
other similar licenses that require the source code and/or modifications to
|
||||
source code to be made available (as would be noted above), you may obtain a
|
||||
copy of the source code corresponding to the binaries for such open source
|
||||
components and modifications thereto, if any, (the "Source Files"), by
|
||||
downloading the Source Files from https://github.com/spring-projects/spring-boot,
|
||||
or by sending a request, with your name and address to:
|
||||
|
||||
Pivotal, Inc., 875 Howard St,
|
||||
San Francisco, CA 94103
|
||||
United States of America
|
||||
|
||||
or email info@pivotal.io. All such requests should clearly specify:
|
||||
|
||||
OPEN SOURCE FILES REQUEST
|
||||
Attention General Counsel
|
||||
@@ -0,0 +1,22 @@
|
||||
# bash completion for spring
|
||||
# Installation: source this file locally in a terminal or from
|
||||
# ~/.bashrc or put it in /etc/bash_completions.d (debian)
|
||||
|
||||
_spring()
|
||||
{
|
||||
local cur prev help helps words cword command commands i
|
||||
|
||||
_get_comp_words_by_ref cur prev words cword
|
||||
|
||||
COMPREPLY=()
|
||||
|
||||
while read -r line; do
|
||||
reply=`echo "$line" | awk '{print $1;}'`
|
||||
COMPREPLY+=("$reply")
|
||||
done < <(spring hint ${cword} ${words[*]})
|
||||
|
||||
if [ $cword -ne 1 ]; then
|
||||
_filedir
|
||||
fi
|
||||
|
||||
} && complete -F _spring spring
|
||||
@@ -0,0 +1,29 @@
|
||||
#compdef spring 'spring'
|
||||
#autoload
|
||||
|
||||
_spring() {
|
||||
|
||||
local cword
|
||||
let cword=CURRENT-1
|
||||
|
||||
local hints
|
||||
hints=()
|
||||
|
||||
local reply
|
||||
while read -r line; do
|
||||
reply=`echo "$line" | awk '{printf $1 ":"; for (i=2; i<NF; i++) printf $i " "; print $NF}'`
|
||||
hints+=("$reply")
|
||||
done < <(spring hint ${cword} ${words[*]})
|
||||
|
||||
if ((cword == 1)) {
|
||||
_describe -t commands 'commands' hints
|
||||
return 0
|
||||
}
|
||||
|
||||
_describe -t options 'options' hints
|
||||
_files
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
_spring "$@"
|
||||
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
darwin=false
|
||||
case "$(uname)" in
|
||||
CYGWIN*)
|
||||
cygwin=true
|
||||
;;
|
||||
|
||||
MINGW*)
|
||||
cygwin=true
|
||||
;;
|
||||
|
||||
Darwin*)
|
||||
darwin=true
|
||||
;;
|
||||
esac
|
||||
|
||||
# For Cygwin, ensure paths are in UNIX format before anything is touched.
|
||||
if $cygwin ; then
|
||||
[ -n "${JAVA_HOME}" ] && JAVA_HOME=$(cygpath --unix "${JAVA_HOME}")
|
||||
fi
|
||||
|
||||
# Attempt to find JAVA_HOME if not already set
|
||||
if [ -z "${JAVA_HOME}" ]; then
|
||||
if ${darwin} ; then
|
||||
if [[ -z "${JAVA_HOME}" && -f "/usr/libexec/java_home" ]]; then
|
||||
JAVA_HOME=$(/usr/libexec/java_home)
|
||||
export JAVA_HOME
|
||||
fi
|
||||
if [[ -z "${JAVA_HOME}" && -d "/Library/Java/Home" ]]; then
|
||||
export JAVA_HOME="/Library/Java/Home"
|
||||
fi
|
||||
if [[ -z "${JAVA_HOME}" && -d "/System/Library/Frameworks/JavaVM.framework/Home" ]]; then
|
||||
export JAVA_HOME="/System/Library/Frameworks/JavaVM.framework/Home"
|
||||
fi
|
||||
else
|
||||
javaExecutable="$(command -v javac)"
|
||||
if [[ -z "$javaExecutable" || "$(expr "${javaExecutable}" : '\([^ ]*\)')" = "no" ]]; then
|
||||
echo "JAVA_HOME not set and cannot find javac to deduce location, please set JAVA_HOME."
|
||||
exit 1
|
||||
fi
|
||||
# readlink(1) is not available as standard on Solaris 10.
|
||||
readLink="$(command -v readlink)"
|
||||
[ "$(expr "${readLink}" : '\([^ ]*\)')" = "no" ] && {
|
||||
echo "JAVA_HOME not set and readlink not available, please set JAVA_HOME."
|
||||
exit 1
|
||||
}
|
||||
javaExecutable="$(readlink -f "${javaExecutable}")"
|
||||
javaHome="$(dirname "${javaExecutable}")"
|
||||
javaHome=$(expr "$javaHome" : '\(.*\)/bin')
|
||||
JAVA_HOME="${javaHome}"
|
||||
export JAVA_HOME
|
||||
fi
|
||||
fi
|
||||
|
||||
# Sanity check that we have java
|
||||
if [ ! -f "${JAVA_HOME}/bin/java" ]; then
|
||||
cat <<-JAVA_HOME_NOT_SET_TXT
|
||||
|
||||
======================================================================================================
|
||||
Please ensure that your JAVA_HOME points to a valid Java SDK.
|
||||
You are currently pointing to:
|
||||
|
||||
${JAVA_HOME}
|
||||
|
||||
This does not seem to be valid. Please rectify and restart.
|
||||
======================================================================================================
|
||||
|
||||
JAVA_HOME_NOT_SET_TXT
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Attempt to find SPRING_HOME if not already set
|
||||
if [ -z "${SPRING_HOME}" ]; then
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=$(ls -ld "$PRG")
|
||||
link=$(expr "$ls" : '.*-> \(.*\)$')
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=$(dirname "$PRG")"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="$(pwd)"
|
||||
cd "$(dirname "${PRG}")/../" > /dev/null || exit 1
|
||||
SPRING_HOME="$(pwd -P)"
|
||||
export SPRING_HOME
|
||||
cd "$SAVED" > /dev/null || exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "${SPRING_HOME}" ]; then
|
||||
echo "Not a directory: SPRING_HOME=${SPRING_HOME}"
|
||||
echo "Please rectify and restart."
|
||||
exit 2
|
||||
fi
|
||||
|
||||
[[ "${cygwin}" == "true" ]] && SPRINGPATH=$(cygpath "${SPRING_HOME}") || SPRINGPATH=$SPRING_HOME
|
||||
CLASSPATH=${SPRINGPATH}/bin
|
||||
if [ -d "${SPRINGPATH}/ext" ]; then
|
||||
CLASSPATH=$CLASSPATH:${SPRINGPATH}/ext
|
||||
fi
|
||||
for f in "${SPRINGPATH}"/lib/*; do
|
||||
[[ "${cygwin}" == "true" ]] && LIBFILE=$(cygpath "$f") || LIBFILE=$f
|
||||
CLASSPATH=$CLASSPATH:$LIBFILE
|
||||
done
|
||||
|
||||
if $cygwin; then
|
||||
SPRING_HOME=$(cygpath --path --mixed "$SPRING_HOME")
|
||||
CLASSPATH=$(cygpath --path --mixed "$CLASSPATH")
|
||||
fi
|
||||
|
||||
IFS=" " read -r -a javaOpts <<< "$JAVA_OPTS"
|
||||
exec "${JAVA_HOME}/bin/java" "${javaOpts[@]}" -cp "$CLASSPATH" org.springframework.boot.loader.JarLauncher "$@"
|
||||
@@ -0,0 +1,27 @@
|
||||
require 'formula'
|
||||
|
||||
class SpringBoot < Formula
|
||||
homepage 'https://spring.io/projects/spring-boot'
|
||||
url 'https://repo.spring.io/${repo}/org/springframework/boot/spring-boot-cli/${project.version}/spring-boot-cli-${project.version}-bin.tar.gz'
|
||||
version '${project.version}'
|
||||
sha256 '${hash}'
|
||||
head 'https://github.com/spring-projects/spring-boot.git'
|
||||
|
||||
if build.head?
|
||||
depends_on 'maven' => :build
|
||||
end
|
||||
|
||||
def install
|
||||
if build.head?
|
||||
Dir.chdir('spring-boot-cli') { system 'mvn -U -DskipTests=true package' }
|
||||
root = 'spring-boot-cli/target/spring-boot-cli-*-bin/spring-*'
|
||||
else
|
||||
root = '.'
|
||||
end
|
||||
|
||||
bin.install Dir["#{root}/bin/spring"]
|
||||
lib.install Dir["#{root}/lib/spring-boot-cli-*.jar"]
|
||||
bash_completion.install Dir["#{root}/shell-completion/bash/spring"]
|
||||
zsh_completion.install Dir["#{root}/shell-completion/zsh/_spring"]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2012-2022 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
|
||||
*
|
||||
* https://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;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
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.encodepassword.EncodePasswordCommand;
|
||||
import org.springframework.boot.cli.command.init.InitCommand;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link CommandFactory}.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class DefaultCommandFactory implements CommandFactory {
|
||||
|
||||
private static final List<Command> DEFAULT_COMMANDS;
|
||||
|
||||
static {
|
||||
List<Command> defaultCommands = new ArrayList<>();
|
||||
defaultCommands.add(new VersionCommand());
|
||||
defaultCommands.add(new InitCommand());
|
||||
defaultCommands.add(new EncodePasswordCommand());
|
||||
DEFAULT_COMMANDS = Collections.unmodifiableList(defaultCommands);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Command> getCommands() {
|
||||
return DEFAULT_COMMANDS;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.ServiceLoader;
|
||||
|
||||
import org.springframework.boot.cli.command.CommandFactory;
|
||||
import org.springframework.boot.cli.command.CommandRunner;
|
||||
import org.springframework.boot.cli.command.core.HelpCommand;
|
||||
import org.springframework.boot.cli.command.core.HintCommand;
|
||||
import org.springframework.boot.cli.command.core.VersionCommand;
|
||||
import org.springframework.boot.cli.command.shell.ShellCommand;
|
||||
import org.springframework.boot.loader.tools.LogbackInitializer;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.SystemPropertyUtils;
|
||||
|
||||
/**
|
||||
* Spring Command Line Interface. This is the main entry-point for the Spring command line
|
||||
* application.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.0.0
|
||||
* @see #main(String...)
|
||||
* @see CommandRunner
|
||||
*/
|
||||
public final class SpringCli {
|
||||
|
||||
private SpringCli() {
|
||||
}
|
||||
|
||||
public static void main(String... args) {
|
||||
System.setProperty("java.awt.headless", Boolean.toString(true));
|
||||
LogbackInitializer.initialize();
|
||||
|
||||
CommandRunner runner = new CommandRunner("spring");
|
||||
ClassUtils.overrideThreadContextClassLoader(createExtendedClassLoader(runner));
|
||||
runner.addCommand(new HelpCommand(runner));
|
||||
addServiceLoaderCommands(runner);
|
||||
runner.addCommand(new ShellCommand());
|
||||
runner.addCommand(new HintCommand(runner));
|
||||
runner.setOptionCommands(HelpCommand.class, VersionCommand.class);
|
||||
runner.setHiddenCommands(HintCommand.class);
|
||||
|
||||
int exitCode = runner.runAndHandleErrors(args);
|
||||
if (exitCode != 0) {
|
||||
// If successful, leave it to run in case it's a server app
|
||||
System.exit(exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addServiceLoaderCommands(CommandRunner runner) {
|
||||
ServiceLoader<CommandFactory> factories = ServiceLoader.load(CommandFactory.class);
|
||||
for (CommandFactory factory : factories) {
|
||||
runner.addCommands(factory.getCommands());
|
||||
}
|
||||
}
|
||||
|
||||
private static URLClassLoader createExtendedClassLoader(CommandRunner runner) {
|
||||
return new URLClassLoader(getExtensionURLs(), runner.getClass().getClassLoader());
|
||||
}
|
||||
|
||||
private static URL[] getExtensionURLs() {
|
||||
List<URL> urls = new ArrayList<>();
|
||||
String home = SystemPropertyUtils.resolvePlaceholders("${spring.home:${SPRING_HOME:.}}");
|
||||
File extDirectory = new File(new File(home, "lib"), "ext");
|
||||
if (extDirectory.isDirectory()) {
|
||||
for (File file : extDirectory.listFiles()) {
|
||||
if (file.getName().endsWith(".jar")) {
|
||||
try {
|
||||
urls.add(file.toURI().toURL());
|
||||
}
|
||||
catch (MalformedURLException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return urls.toArray(new URL[0]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.springframework.boot.cli.command.options.OptionHelp;
|
||||
|
||||
/**
|
||||
* Abstract {@link Command} implementation.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Dave Syer
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public abstract class AbstractCommand implements Command {
|
||||
|
||||
private final String name;
|
||||
|
||||
private final String description;
|
||||
|
||||
/**
|
||||
* Create a new {@link AbstractCommand} instance.
|
||||
* @param name the name of the command
|
||||
* @param description the command description
|
||||
*/
|
||||
protected AbstractCommand(String name, String description) {
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageHelp() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelp() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<OptionHelp> getOptionsHelp() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<HelpExample> getExamples() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.boot.cli.command.options.OptionHelp;
|
||||
import org.springframework.boot.cli.command.status.ExitStatus;
|
||||
|
||||
/**
|
||||
* A single command that can be run from the CLI.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Dave Syer
|
||||
* @author Stephane Nicoll
|
||||
* @since 1.0.0
|
||||
* @see #run(String...)
|
||||
*/
|
||||
public interface Command {
|
||||
|
||||
/**
|
||||
* Returns the name of the command.
|
||||
* @return the command's name
|
||||
*/
|
||||
String getName();
|
||||
|
||||
/**
|
||||
* Returns a description of the command.
|
||||
* @return the command's description
|
||||
*/
|
||||
String getDescription();
|
||||
|
||||
/**
|
||||
* Returns usage help for the command. This should be a simple one-line string
|
||||
* describing basic usage. e.g. '[options] <file>'. Do not include the name of
|
||||
* the command in this string.
|
||||
* @return the command's usage help
|
||||
*/
|
||||
String getUsageHelp();
|
||||
|
||||
/**
|
||||
* Gets full help text for the command, e.g. a longer description and one line per
|
||||
* option.
|
||||
* @return the command's help text
|
||||
*/
|
||||
String getHelp();
|
||||
|
||||
/**
|
||||
* Returns help for each supported option.
|
||||
* @return help for each of the command's options
|
||||
*/
|
||||
Collection<OptionHelp> getOptionsHelp();
|
||||
|
||||
/**
|
||||
* Return some examples for the command.
|
||||
* @return the command's examples
|
||||
*/
|
||||
Collection<HelpExample> getExamples();
|
||||
|
||||
/**
|
||||
* Run the command.
|
||||
* @param args command arguments (this will not include the command itself)
|
||||
* @return the outcome of the command
|
||||
* @throws Exception if the command fails
|
||||
*/
|
||||
ExitStatus run(String... args) throws Exception;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Runtime exception wrapper that defines additional {@link Option}s that are understood
|
||||
* by the {@link CommandRunner}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class CommandException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 0L;
|
||||
|
||||
private final EnumSet<Option> options;
|
||||
|
||||
/**
|
||||
* Create a new {@link CommandException} with the specified options.
|
||||
* @param options the exception options
|
||||
*/
|
||||
public CommandException(Option... options) {
|
||||
this.options = asEnumSet(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link CommandException} with the specified options.
|
||||
* @param message the exception message to display to the user
|
||||
* @param options the exception options
|
||||
*/
|
||||
public CommandException(String message, Option... options) {
|
||||
super(message);
|
||||
this.options = asEnumSet(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link CommandException} with the specified options.
|
||||
* @param message the exception message to display to the user
|
||||
* @param cause the underlying cause
|
||||
* @param options the exception options
|
||||
*/
|
||||
public CommandException(String message, Throwable cause, Option... options) {
|
||||
super(message, cause);
|
||||
this.options = asEnumSet(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link CommandException} with the specified options.
|
||||
* @param cause the underlying cause
|
||||
* @param options the exception options
|
||||
*/
|
||||
public CommandException(Throwable cause, Option... options) {
|
||||
super(cause);
|
||||
this.options = asEnumSet(options);
|
||||
}
|
||||
|
||||
private EnumSet<Option> asEnumSet(Option[] options) {
|
||||
if (options == null || options.length == 0) {
|
||||
return EnumSet.noneOf(Option.class);
|
||||
}
|
||||
return EnumSet.copyOf(Arrays.asList(options));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a set of options that are understood by the {@link CommandRunner}.
|
||||
* @return the options understood by the runner
|
||||
*/
|
||||
public Set<Option> getOptions() {
|
||||
return Collections.unmodifiableSet(this.options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specific options understood by the {@link CommandRunner}.
|
||||
*/
|
||||
public enum Option {
|
||||
|
||||
/**
|
||||
* Hide the exception message.
|
||||
*/
|
||||
HIDE_MESSAGE,
|
||||
|
||||
/**
|
||||
* Print basic CLI usage information.
|
||||
*/
|
||||
SHOW_USAGE,
|
||||
|
||||
/**
|
||||
* Print the stack-trace of the exception.
|
||||
*/
|
||||
STACK_TRACE,
|
||||
|
||||
/**
|
||||
* Re-throw the exception rather than dealing with it.
|
||||
*/
|
||||
RETHROW
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.ServiceLoader;
|
||||
|
||||
/**
|
||||
* Factory used to create CLI {@link Command}s. Intended for use with a Java
|
||||
* {@link ServiceLoader}.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CommandFactory {
|
||||
|
||||
/**
|
||||
* Returns the CLI {@link Command}s.
|
||||
* @return the commands
|
||||
*/
|
||||
Collection<Command> getCommands();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
/*
|
||||
* Copyright 2012-2022 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
|
||||
*
|
||||
* https://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;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.boot.cli.command.status.ExitStatus;
|
||||
import org.springframework.boot.cli.util.Log;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Main class used to run {@link Command}s.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.0.0
|
||||
* @see #addCommand(Command)
|
||||
* @see CommandRunner#runAndHandleErrors(String[])
|
||||
*/
|
||||
public class CommandRunner implements Iterable<Command> {
|
||||
|
||||
private static final Set<CommandException.Option> NO_EXCEPTION_OPTIONS = EnumSet
|
||||
.noneOf(CommandException.Option.class);
|
||||
|
||||
private final String name;
|
||||
|
||||
private final List<Command> commands = new ArrayList<>();
|
||||
|
||||
private Class<?>[] optionCommandClasses = {};
|
||||
|
||||
private Class<?>[] hiddenCommandClasses = {};
|
||||
|
||||
/**
|
||||
* Create a new {@link CommandRunner} instance.
|
||||
* @param name the name of the runner or {@code null}
|
||||
*/
|
||||
public CommandRunner(String name) {
|
||||
this.name = StringUtils.hasLength(name) ? name + " " : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the name of the runner or an empty string. Non-empty names will include a
|
||||
* trailing space character so that they can be used as a prefix.
|
||||
* @return the name of the runner
|
||||
*/
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the specified commands.
|
||||
* @param commands the commands to add
|
||||
*/
|
||||
public void addCommands(Iterable<Command> commands) {
|
||||
Assert.notNull(commands, "Commands must not be null");
|
||||
for (Command command : commands) {
|
||||
addCommand(command);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the specified command.
|
||||
* @param command the command to add.
|
||||
*/
|
||||
public void addCommand(Command command) {
|
||||
Assert.notNull(command, "Command must not be null");
|
||||
this.commands.add(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the command classes which should be considered option commands. An option
|
||||
* command is a special type of command that usually makes more sense to present as if
|
||||
* it is an option. For example '--version'.
|
||||
* @param commandClasses the classes of option commands.
|
||||
* @see #isOptionCommand(Command)
|
||||
*/
|
||||
public void setOptionCommands(Class<?>... commandClasses) {
|
||||
Assert.notNull(commandClasses, "CommandClasses must not be null");
|
||||
this.optionCommandClasses = commandClasses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the command classes which should be hidden (i.e. executed but not displayed in
|
||||
* the available commands list).
|
||||
* @param commandClasses the classes of hidden commands
|
||||
*/
|
||||
public void setHiddenCommands(Class<?>... commandClasses) {
|
||||
Assert.notNull(commandClasses, "CommandClasses must not be null");
|
||||
this.hiddenCommandClasses = commandClasses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the specified command is an option command.
|
||||
* @param command the command to test
|
||||
* @return {@code true} if the command is an option command
|
||||
* @see #setOptionCommands(Class...)
|
||||
*/
|
||||
public boolean isOptionCommand(Command command) {
|
||||
return isCommandInstanceOf(command, this.optionCommandClasses);
|
||||
}
|
||||
|
||||
private boolean isHiddenCommand(Command command) {
|
||||
return isCommandInstanceOf(command, this.hiddenCommandClasses);
|
||||
}
|
||||
|
||||
private boolean isCommandInstanceOf(Command command, Class<?>[] commandClasses) {
|
||||
for (Class<?> commandClass : commandClasses) {
|
||||
if (commandClass.isInstance(command)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Command> iterator() {
|
||||
return getCommands().iterator();
|
||||
}
|
||||
|
||||
protected final List<Command> getCommands() {
|
||||
return Collections.unmodifiableList(this.commands);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a command by name.
|
||||
* @param name the name of the command
|
||||
* @return the command or {@code null} if not found
|
||||
*/
|
||||
public Command findCommand(String name) {
|
||||
for (Command candidate : this.commands) {
|
||||
String candidateName = candidate.getName();
|
||||
if (candidateName.equals(name) || (isOptionCommand(candidate) && ("--" + candidateName).equals(name))) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the appropriate and handle and errors.
|
||||
* @param args the input arguments
|
||||
* @return a return status code (non boot is used to indicate an error)
|
||||
*/
|
||||
public int runAndHandleErrors(String... args) {
|
||||
String[] argsWithoutDebugFlags = removeDebugFlags(args);
|
||||
boolean debug = argsWithoutDebugFlags.length != args.length;
|
||||
if (debug) {
|
||||
System.setProperty("debug", "true");
|
||||
}
|
||||
try {
|
||||
ExitStatus result = run(argsWithoutDebugFlags);
|
||||
// The caller will hang up if it gets a non-zero status
|
||||
if (result != null && result.isHangup()) {
|
||||
return (result.getCode() > 0) ? result.getCode() : 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
catch (NoArgumentsException ex) {
|
||||
showUsage();
|
||||
return 1;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
return handleError(debug, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private String[] removeDebugFlags(String[] args) {
|
||||
List<String> rtn = new ArrayList<>(args.length);
|
||||
boolean appArgsDetected = false;
|
||||
for (String arg : args) {
|
||||
// Allow apps to have a --debug argument
|
||||
appArgsDetected |= "--".equals(arg);
|
||||
if ("--debug".equals(arg) && !appArgsDetected) {
|
||||
continue;
|
||||
}
|
||||
rtn.add(arg);
|
||||
}
|
||||
return StringUtils.toStringArray(rtn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the arguments and run a suitable command.
|
||||
* @param args the arguments
|
||||
* @return the outcome of the command
|
||||
* @throws Exception if the command fails
|
||||
*/
|
||||
protected ExitStatus run(String... args) throws Exception {
|
||||
if (args.length == 0) {
|
||||
throw new NoArgumentsException();
|
||||
}
|
||||
String commandName = args[0];
|
||||
String[] commandArguments = Arrays.copyOfRange(args, 1, args.length);
|
||||
Command command = findCommand(commandName);
|
||||
if (command == null) {
|
||||
throw new NoSuchCommandException(commandName);
|
||||
}
|
||||
beforeRun(command);
|
||||
try {
|
||||
return command.run(commandArguments);
|
||||
}
|
||||
finally {
|
||||
afterRun(command);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclass hook called before a command is run.
|
||||
* @param command the command about to run
|
||||
*/
|
||||
protected void beforeRun(Command command) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclass hook called after a command has run.
|
||||
* @param command the command that has run
|
||||
*/
|
||||
protected void afterRun(Command command) {
|
||||
}
|
||||
|
||||
private int handleError(boolean debug, Exception ex) {
|
||||
Set<CommandException.Option> options = NO_EXCEPTION_OPTIONS;
|
||||
if (ex instanceof CommandException commandException) {
|
||||
options = commandException.getOptions();
|
||||
if (options.contains(CommandException.Option.RETHROW)) {
|
||||
throw (CommandException) ex;
|
||||
}
|
||||
}
|
||||
boolean couldNotShowMessage = false;
|
||||
if (!options.contains(CommandException.Option.HIDE_MESSAGE)) {
|
||||
couldNotShowMessage = !errorMessage(ex.getMessage());
|
||||
}
|
||||
if (options.contains(CommandException.Option.SHOW_USAGE)) {
|
||||
showUsage();
|
||||
}
|
||||
if (debug || couldNotShowMessage || options.contains(CommandException.Option.STACK_TRACE)) {
|
||||
printStackTrace(ex);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
protected boolean errorMessage(String message) {
|
||||
Log.error((message != null) ? message : "Unexpected error");
|
||||
return message != null;
|
||||
}
|
||||
|
||||
protected void showUsage() {
|
||||
Log.infoPrint("usage: " + this.name);
|
||||
for (Command command : this.commands) {
|
||||
if (isOptionCommand(command)) {
|
||||
Log.infoPrint("[--" + command.getName() + "] ");
|
||||
}
|
||||
}
|
||||
Log.info("");
|
||||
Log.info(" <command> [<args>]");
|
||||
Log.info("");
|
||||
Log.info("Available commands are:");
|
||||
for (Command command : this.commands) {
|
||||
if (!isOptionCommand(command) && !isHiddenCommand(command)) {
|
||||
String usageHelp = command.getUsageHelp();
|
||||
String description = command.getDescription();
|
||||
Log.info(String.format("%n %1$s %2$-15s%n %3$s", command.getName(),
|
||||
(usageHelp != null) ? usageHelp : "", (description != null) ? description : ""));
|
||||
}
|
||||
}
|
||||
Log.info("");
|
||||
Log.info("Common options:");
|
||||
Log.info(String.format("%n %1$s %2$-15s%n %3$s", "--debug", "Verbose mode",
|
||||
"Print additional status information for the command you are running"));
|
||||
Log.info("");
|
||||
Log.info("");
|
||||
Log.info("See '" + this.name + "help <command>' for more information on a specific command.");
|
||||
}
|
||||
|
||||
protected void printStackTrace(Exception ex) {
|
||||
Log.error("");
|
||||
Log.error(ex);
|
||||
Log.error("");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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;
|
||||
|
||||
/**
|
||||
* An example that can be displayed in the help.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.2.0
|
||||
*/
|
||||
public class HelpExample {
|
||||
|
||||
private final String description;
|
||||
|
||||
private final String example;
|
||||
|
||||
/**
|
||||
* Create a new {@link HelpExample} instance.
|
||||
* @param description the description (in the form "to ....")
|
||||
* @param example the example
|
||||
*/
|
||||
public HelpExample(String description, String example) {
|
||||
this.description = description;
|
||||
this.example = example;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
public String getExample() {
|
||||
return this.example;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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;
|
||||
|
||||
/**
|
||||
* Exception used to indicate that no arguments were specified.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class NoArgumentsException extends CommandException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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;
|
||||
|
||||
/**
|
||||
* Exception used to when the help command is called without arguments.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class NoHelpCommandArgumentsException extends CommandException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public NoHelpCommandArgumentsException() {
|
||||
super(Option.SHOW_USAGE, Option.HIDE_MESSAGE);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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;
|
||||
|
||||
/**
|
||||
* Exception used when a command is not found.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class NoSuchCommandException extends CommandException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public NoSuchCommandException(String name) {
|
||||
super(String.format("'%1$s' is not a valid command. See 'help'.", name));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.boot.cli.command.options.OptionHandler;
|
||||
import org.springframework.boot.cli.command.options.OptionHelp;
|
||||
import org.springframework.boot.cli.command.status.ExitStatus;
|
||||
|
||||
/**
|
||||
* Base class for a {@link Command} that parse options using an {@link OptionHandler}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Dave Syer
|
||||
* @since 1.0.0
|
||||
* @see OptionHandler
|
||||
*/
|
||||
public abstract class OptionParsingCommand extends AbstractCommand {
|
||||
|
||||
private final OptionHandler handler;
|
||||
|
||||
protected OptionParsingCommand(String name, String description, OptionHandler handler) {
|
||||
super(name, description);
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelp() {
|
||||
return this.handler.getHelp();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<OptionHelp> getOptionsHelp() {
|
||||
return this.handler.getOptionsHelp();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final ExitStatus run(String... args) throws Exception {
|
||||
return this.handler.run(args);
|
||||
}
|
||||
|
||||
protected OptionHandler getHandler() {
|
||||
return this.handler;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.core;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.boot.cli.command.AbstractCommand;
|
||||
import org.springframework.boot.cli.command.Command;
|
||||
import org.springframework.boot.cli.command.CommandRunner;
|
||||
import org.springframework.boot.cli.command.HelpExample;
|
||||
import org.springframework.boot.cli.command.NoHelpCommandArgumentsException;
|
||||
import org.springframework.boot.cli.command.NoSuchCommandException;
|
||||
import org.springframework.boot.cli.command.options.OptionHelp;
|
||||
import org.springframework.boot.cli.command.status.ExitStatus;
|
||||
import org.springframework.boot.cli.util.Log;
|
||||
|
||||
/**
|
||||
* Internal {@link Command} used for 'help' requests.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class HelpCommand extends AbstractCommand {
|
||||
|
||||
private final CommandRunner commandRunner;
|
||||
|
||||
public HelpCommand(CommandRunner commandRunner) {
|
||||
super("help", "Get help on commands");
|
||||
this.commandRunner = commandRunner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageHelp() {
|
||||
return "command";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelp() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<OptionHelp> getOptionsHelp() {
|
||||
List<OptionHelp> help = new ArrayList<>();
|
||||
for (Command command : this.commandRunner) {
|
||||
if (isHelpShown(command)) {
|
||||
help.add(new OptionHelp() {
|
||||
|
||||
@Override
|
||||
public Set<String> getOptions() {
|
||||
return Collections.singleton(command.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageHelp() {
|
||||
return command.getDescription();
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
return help;
|
||||
}
|
||||
|
||||
private boolean isHelpShown(Command command) {
|
||||
if (command instanceof HelpCommand || command instanceof HintCommand) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExitStatus run(String... args) throws Exception {
|
||||
if (args.length == 0) {
|
||||
throw new NoHelpCommandArgumentsException();
|
||||
}
|
||||
String commandName = args[0];
|
||||
for (Command command : this.commandRunner) {
|
||||
if (command.getName().equals(commandName)) {
|
||||
Log.info(this.commandRunner.getName() + command.getName() + " - " + command.getDescription());
|
||||
Log.info("");
|
||||
if (command.getUsageHelp() != null) {
|
||||
Log.info("usage: " + this.commandRunner.getName() + command.getName() + " "
|
||||
+ command.getUsageHelp());
|
||||
Log.info("");
|
||||
}
|
||||
if (command.getHelp() != null) {
|
||||
Log.info(command.getHelp());
|
||||
}
|
||||
Collection<HelpExample> examples = command.getExamples();
|
||||
if (examples != null) {
|
||||
Log.info((examples.size() != 1) ? "examples:" : "example:");
|
||||
Log.info("");
|
||||
for (HelpExample example : examples) {
|
||||
Log.info(" " + example.getDescription() + ":");
|
||||
Log.info(" $ " + example.getExample());
|
||||
Log.info("");
|
||||
}
|
||||
Log.info("");
|
||||
}
|
||||
return ExitStatus.OK;
|
||||
}
|
||||
}
|
||||
throw new NoSuchCommandException(commandName);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.core;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.cli.command.AbstractCommand;
|
||||
import org.springframework.boot.cli.command.Command;
|
||||
import org.springframework.boot.cli.command.CommandRunner;
|
||||
import org.springframework.boot.cli.command.options.OptionHelp;
|
||||
import org.springframework.boot.cli.command.status.ExitStatus;
|
||||
import org.springframework.boot.cli.util.Log;
|
||||
|
||||
/**
|
||||
* Internal {@link Command} to provide hints for shell auto-completion. Expects to be
|
||||
* called with the current index followed by a list of arguments already typed.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class HintCommand extends AbstractCommand {
|
||||
|
||||
private final CommandRunner commandRunner;
|
||||
|
||||
public HintCommand(CommandRunner commandRunner) {
|
||||
super("hint", "Provides hints for shell auto-completion");
|
||||
this.commandRunner = commandRunner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExitStatus run(String... args) throws Exception {
|
||||
try {
|
||||
int index = (args.length != 0) ? Integer.parseInt(args[0]) - 1 : 0;
|
||||
List<String> arguments = new ArrayList<>(args.length);
|
||||
for (int i = 2; i < args.length; i++) {
|
||||
arguments.add(args[i]);
|
||||
}
|
||||
String starting = "";
|
||||
if (index < arguments.size()) {
|
||||
starting = arguments.remove(index);
|
||||
}
|
||||
if (index == 0) {
|
||||
showCommandHints(starting);
|
||||
}
|
||||
else if (!arguments.isEmpty() && !starting.isEmpty()) {
|
||||
String command = arguments.remove(0);
|
||||
showCommandOptionHints(command, Collections.unmodifiableList(arguments), starting);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// Swallow and provide no hints
|
||||
return ExitStatus.ERROR;
|
||||
}
|
||||
return ExitStatus.OK;
|
||||
}
|
||||
|
||||
private void showCommandHints(String starting) {
|
||||
for (Command command : this.commandRunner) {
|
||||
if (isHintMatch(command, starting)) {
|
||||
Log.info(command.getName() + " " + command.getDescription());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isHintMatch(Command command, String starting) {
|
||||
if (command instanceof HintCommand) {
|
||||
return false;
|
||||
}
|
||||
return command.getName().startsWith(starting)
|
||||
|| (this.commandRunner.isOptionCommand(command) && ("--" + command.getName()).startsWith(starting));
|
||||
}
|
||||
|
||||
private void showCommandOptionHints(String commandName, List<String> specifiedArguments, String starting) {
|
||||
Command command = this.commandRunner.findCommand(commandName);
|
||||
if (command != null) {
|
||||
for (OptionHelp help : command.getOptionsHelp()) {
|
||||
if (!alreadyUsed(help, specifiedArguments)) {
|
||||
for (String option : help.getOptions()) {
|
||||
if (option.startsWith(starting)) {
|
||||
Log.info(option + " " + help.getUsageHelp());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean alreadyUsed(OptionHelp help, List<String> specifiedArguments) {
|
||||
for (String argument : specifiedArguments) {
|
||||
if (help.getOptions().contains(argument)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.core;
|
||||
|
||||
import org.springframework.boot.cli.command.AbstractCommand;
|
||||
import org.springframework.boot.cli.command.Command;
|
||||
import org.springframework.boot.cli.command.status.ExitStatus;
|
||||
import org.springframework.boot.cli.util.Log;
|
||||
|
||||
/**
|
||||
* {@link Command} to display the 'version' number.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class VersionCommand extends AbstractCommand {
|
||||
|
||||
public VersionCommand() {
|
||||
super("version", "Show the version");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExitStatus run(String... args) {
|
||||
Log.info("Spring CLI v" + getClass().getPackage().getImplementationVersion());
|
||||
return ExitStatus.OK;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Core CLI commands.
|
||||
*/
|
||||
package org.springframework.boot.cli.command.core;
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.encodepassword;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import joptsimple.OptionSet;
|
||||
import joptsimple.OptionSpec;
|
||||
|
||||
import org.springframework.boot.cli.command.Command;
|
||||
import org.springframework.boot.cli.command.HelpExample;
|
||||
import org.springframework.boot.cli.command.OptionParsingCommand;
|
||||
import org.springframework.boot.cli.command.options.OptionHandler;
|
||||
import org.springframework.boot.cli.command.status.ExitStatus;
|
||||
import org.springframework.boot.cli.util.Log;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@link Command} to encode passwords for use with Spring Security.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class EncodePasswordCommand extends OptionParsingCommand {
|
||||
|
||||
private static final Map<String, Supplier<PasswordEncoder>> ENCODERS;
|
||||
|
||||
static {
|
||||
Map<String, Supplier<PasswordEncoder>> encoders = new LinkedHashMap<>();
|
||||
encoders.put("default", PasswordEncoderFactories::createDelegatingPasswordEncoder);
|
||||
encoders.put("bcrypt", BCryptPasswordEncoder::new);
|
||||
encoders.put("pbkdf2", Pbkdf2PasswordEncoder::new);
|
||||
ENCODERS = Collections.unmodifiableMap(encoders);
|
||||
}
|
||||
|
||||
public EncodePasswordCommand() {
|
||||
super("encodepassword", "Encode a password for use with Spring Security", new EncodePasswordOptionHandler());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageHelp() {
|
||||
return "[options] <password to encode>";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<HelpExample> getExamples() {
|
||||
List<HelpExample> examples = new ArrayList<>();
|
||||
examples.add(
|
||||
new HelpExample("To encode a password with the default encoder", "spring encodepassword mypassword"));
|
||||
examples.add(new HelpExample("To encode a password with pbkdf2", "spring encodepassword -a pbkdf2 mypassword"));
|
||||
return examples;
|
||||
}
|
||||
|
||||
private static final class EncodePasswordOptionHandler extends OptionHandler {
|
||||
|
||||
private OptionSpec<String> algorithm;
|
||||
|
||||
@Override
|
||||
protected void options() {
|
||||
this.algorithm = option(Arrays.asList("algorithm", "a"), "The algorithm to use").withRequiredArg()
|
||||
.defaultsTo("default");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ExitStatus run(OptionSet options) throws Exception {
|
||||
if (options.nonOptionArguments().size() != 1) {
|
||||
Log.error("A single password option must be provided");
|
||||
return ExitStatus.ERROR;
|
||||
}
|
||||
String algorithm = options.valueOf(this.algorithm);
|
||||
String password = (String) options.nonOptionArguments().get(0);
|
||||
Supplier<PasswordEncoder> encoder = ENCODERS.get(algorithm);
|
||||
if (encoder == null) {
|
||||
Log.error("Unknown algorithm, valid options are: "
|
||||
+ StringUtils.collectionToCommaDelimitedString(ENCODERS.keySet()));
|
||||
return ExitStatus.ERROR;
|
||||
}
|
||||
Log.info(encoder.get().encode(password));
|
||||
return ExitStatus.OK;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* CLI command for password encoding.
|
||||
*/
|
||||
package org.springframework.boot.cli.command.encodepassword;
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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
|
||||
*/
|
||||
final class Dependency {
|
||||
|
||||
private final String id;
|
||||
|
||||
private final String name;
|
||||
|
||||
private final String description;
|
||||
|
||||
Dependency(String id, String name, String description) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
String getDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
/*
|
||||
* Copyright 2012-2021 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
|
||||
*
|
||||
* https://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.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import joptsimple.OptionSet;
|
||||
import joptsimple.OptionSpec;
|
||||
|
||||
import org.springframework.boot.cli.command.Command;
|
||||
import org.springframework.boot.cli.command.HelpExample;
|
||||
import org.springframework.boot.cli.command.OptionParsingCommand;
|
||||
import org.springframework.boot.cli.command.options.OptionHandler;
|
||||
import org.springframework.boot.cli.command.status.ExitStatus;
|
||||
import org.springframework.boot.cli.util.Log;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link Command} that initializes a project using Spring initializr.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Eddú Meléndez
|
||||
* @author Vignesh Thangavel Ilangovan
|
||||
* @since 1.2.0
|
||||
*/
|
||||
public class InitCommand extends OptionParsingCommand {
|
||||
|
||||
public InitCommand() {
|
||||
this(new InitOptionHandler(new InitializrService()));
|
||||
}
|
||||
|
||||
public InitCommand(InitOptionHandler handler) {
|
||||
super("init", "Initialize a new project using Spring Initializr (start.spring.io)", handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageHelp() {
|
||||
return "[options] [location]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<HelpExample> getExamples() {
|
||||
List<HelpExample> examples = new ArrayList<>();
|
||||
examples.add(new HelpExample("To list all the capabilities of the service", "spring init --list"));
|
||||
examples.add(new HelpExample("To creates a default project", "spring init"));
|
||||
examples.add(new HelpExample("To create a web my-app.zip", "spring init -d=web my-app.zip"));
|
||||
examples.add(new HelpExample("To create a web/data-jpa gradle project unpacked",
|
||||
"spring init -d=web,jpa --build=gradle my-dir"));
|
||||
return examples;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link OptionHandler} for {@link InitCommand}.
|
||||
*/
|
||||
static class InitOptionHandler extends OptionHandler {
|
||||
|
||||
/**
|
||||
* Mapping from camelCase options advertised by the service to our kebab-case
|
||||
* options.
|
||||
*/
|
||||
private static final Map<String, String> CAMEL_CASE_OPTIONS;
|
||||
static {
|
||||
Map<String, String> options = new HashMap<>();
|
||||
options.put("--groupId", "--group-id");
|
||||
options.put("--artifactId", "--artifact-id");
|
||||
options.put("--packageName", "--package-name");
|
||||
options.put("--javaVersion", "--java-version");
|
||||
options.put("--bootVersion", "--boot-version");
|
||||
CAMEL_CASE_OPTIONS = Collections.unmodifiableMap(options);
|
||||
}
|
||||
|
||||
private final ServiceCapabilitiesReportGenerator serviceCapabilitiesReport;
|
||||
|
||||
private final ProjectGenerator projectGenerator;
|
||||
|
||||
private OptionSpec<String> target;
|
||||
|
||||
private OptionSpec<Void> listCapabilities;
|
||||
|
||||
private OptionSpec<String> groupId;
|
||||
|
||||
private OptionSpec<String> artifactId;
|
||||
|
||||
private OptionSpec<String> version;
|
||||
|
||||
private OptionSpec<String> name;
|
||||
|
||||
private OptionSpec<String> description;
|
||||
|
||||
private OptionSpec<String> packageName;
|
||||
|
||||
private OptionSpec<String> type;
|
||||
|
||||
private OptionSpec<String> packaging;
|
||||
|
||||
private OptionSpec<String> build;
|
||||
|
||||
private OptionSpec<String> format;
|
||||
|
||||
private OptionSpec<String> javaVersion;
|
||||
|
||||
private OptionSpec<String> language;
|
||||
|
||||
private OptionSpec<String> bootVersion;
|
||||
|
||||
private OptionSpec<String> dependencies;
|
||||
|
||||
private OptionSpec<Void> extract;
|
||||
|
||||
private OptionSpec<Void> force;
|
||||
|
||||
InitOptionHandler(InitializrService initializrService) {
|
||||
super(InitOptionHandler::processArgument);
|
||||
this.serviceCapabilitiesReport = new ServiceCapabilitiesReportGenerator(initializrService);
|
||||
this.projectGenerator = new ProjectGenerator(initializrService);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void options() {
|
||||
this.target = option(Arrays.asList("target"), "URL of the service to use").withRequiredArg()
|
||||
.defaultsTo(ProjectGenerationRequest.DEFAULT_SERVICE_URL);
|
||||
this.listCapabilities = option(Arrays.asList("list"),
|
||||
"List the capabilities of the service. Use it to discover the "
|
||||
+ "dependencies and the types that are available");
|
||||
projectGenerationOptions();
|
||||
otherOptions();
|
||||
}
|
||||
|
||||
private void projectGenerationOptions() {
|
||||
this.groupId = option(Arrays.asList("group-id", "g"), "Project coordinates (for example 'org.test')")
|
||||
.withRequiredArg();
|
||||
this.artifactId = option(Arrays.asList("artifact-id", "a"),
|
||||
"Project coordinates; infer archive name (for example 'test')").withRequiredArg();
|
||||
this.version = option(Arrays.asList("version", "v"), "Project version (for example '0.0.1-SNAPSHOT')")
|
||||
.withRequiredArg();
|
||||
this.name = option(Arrays.asList("name", "n"), "Project name; infer application name").withRequiredArg();
|
||||
this.description = option("description", "Project description").withRequiredArg();
|
||||
this.packageName = option(Arrays.asList("package-name"), "Package name").withRequiredArg();
|
||||
this.type = option(Arrays.asList("type", "t"),
|
||||
"Project type. Not normally needed if you use --build "
|
||||
+ "and/or --format. Check the capabilities of the service (--list) for more details")
|
||||
.withRequiredArg();
|
||||
this.packaging = option(Arrays.asList("packaging", "p"), "Project packaging (for example 'jar')")
|
||||
.withRequiredArg();
|
||||
this.build = option("build", "Build system to use (for example 'maven' or 'gradle')").withRequiredArg()
|
||||
.defaultsTo("maven");
|
||||
this.format = option("format", "Format of the generated content (for example 'build' for a build file, "
|
||||
+ "'project' for a project archive)").withRequiredArg().defaultsTo("project");
|
||||
this.javaVersion = option(Arrays.asList("java-version", "j"), "Language level (for example '1.8')")
|
||||
.withRequiredArg();
|
||||
this.language = option(Arrays.asList("language", "l"), "Programming language (for example 'java')")
|
||||
.withRequiredArg();
|
||||
this.bootVersion = option(Arrays.asList("boot-version", "b"),
|
||||
"Spring Boot version (for example '1.2.0.RELEASE')").withRequiredArg();
|
||||
this.dependencies = option(Arrays.asList("dependencies", "d"),
|
||||
"Comma-separated list of dependency identifiers to include in the generated project")
|
||||
.withRequiredArg();
|
||||
}
|
||||
|
||||
private void otherOptions() {
|
||||
this.extract = option(Arrays.asList("extract", "x"),
|
||||
"Extract the project archive. Inferred if a location is specified without an extension");
|
||||
this.force = option(Arrays.asList("force", "f"), "Force overwrite of existing files");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ExitStatus run(OptionSet options) throws Exception {
|
||||
try {
|
||||
if (options.has(this.listCapabilities)) {
|
||||
generateReport(options);
|
||||
}
|
||||
else {
|
||||
generateProject(options);
|
||||
}
|
||||
return ExitStatus.OK;
|
||||
}
|
||||
catch (ReportableException ex) {
|
||||
Log.error(ex.getMessage());
|
||||
return ExitStatus.ERROR;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
Log.error(ex);
|
||||
return ExitStatus.ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
private void generateReport(OptionSet options) throws IOException {
|
||||
Log.info(this.serviceCapabilitiesReport.generate(options.valueOf(this.target)));
|
||||
}
|
||||
|
||||
protected void generateProject(OptionSet options) throws IOException {
|
||||
ProjectGenerationRequest request = createProjectGenerationRequest(options);
|
||||
this.projectGenerator.generateProject(request, options.has(this.force));
|
||||
}
|
||||
|
||||
protected ProjectGenerationRequest createProjectGenerationRequest(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();
|
||||
request.setServiceUrl(options.valueOf(this.target));
|
||||
if (options.has(this.bootVersion)) {
|
||||
request.setBootVersion(options.valueOf(this.bootVersion));
|
||||
}
|
||||
if (options.has(this.dependencies)) {
|
||||
for (String dep : options.valueOf(this.dependencies).split(",")) {
|
||||
request.getDependencies().add(dep.trim());
|
||||
}
|
||||
}
|
||||
if (options.has(this.javaVersion)) {
|
||||
request.setJavaVersion(options.valueOf(this.javaVersion));
|
||||
}
|
||||
if (options.has(this.packageName)) {
|
||||
request.setPackageName(options.valueOf(this.packageName));
|
||||
}
|
||||
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.packaging)) {
|
||||
request.setPackaging(options.valueOf(this.packaging));
|
||||
}
|
||||
if (options.has(this.language)) {
|
||||
request.setLanguage(options.valueOf(this.language));
|
||||
}
|
||||
if (options.has(this.groupId)) {
|
||||
request.setGroupId(options.valueOf(this.groupId));
|
||||
}
|
||||
if (options.has(this.artifactId)) {
|
||||
request.setArtifactId(options.valueOf(this.artifactId));
|
||||
}
|
||||
if (options.has(this.name)) {
|
||||
request.setName(options.valueOf(this.name));
|
||||
}
|
||||
if (options.has(this.version)) {
|
||||
request.setVersion(options.valueOf(this.version));
|
||||
}
|
||||
if (options.has(this.description)) {
|
||||
request.setDescription(options.valueOf(this.description));
|
||||
}
|
||||
request.setExtract(options.has(this.extract));
|
||||
if (nonOptionArguments.size() == 1) {
|
||||
String output = (String) nonOptionArguments.get(0);
|
||||
request.setOutput(output);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
private static String processArgument(String argument) {
|
||||
for (Map.Entry<String, String> entry : CAMEL_CASE_OPTIONS.entrySet()) {
|
||||
String name = entry.getKey();
|
||||
if (argument.startsWith(name + " ") || argument.startsWith(name + "=")) {
|
||||
return entry.getValue() + argument.substring(name.length());
|
||||
}
|
||||
}
|
||||
return argument;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cli.command.init;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
import org.apache.http.entity.ContentType;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.apache.http.message.BasicHeader;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import org.springframework.boot.cli.util.Log;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Invokes the initializr service over HTTP.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class InitializrService {
|
||||
|
||||
private static final String FILENAME_HEADER_PREFIX = "filename=\"";
|
||||
|
||||
/**
|
||||
* Accept header to use to retrieve the json meta-data.
|
||||
*/
|
||||
public static final String ACCEPT_META_DATA = "application/vnd.initializr.v2.1+"
|
||||
+ "json,application/vnd.initializr.v2+json";
|
||||
|
||||
/**
|
||||
* Accept header to use to retrieve the service capabilities of the service. If the
|
||||
* service does not offer such feature, the json meta-data are retrieved instead.
|
||||
*/
|
||||
public static final String ACCEPT_SERVICE_CAPABILITIES = "text/plain," + ACCEPT_META_DATA;
|
||||
|
||||
/**
|
||||
* Late binding HTTP client.
|
||||
*/
|
||||
private CloseableHttpClient http;
|
||||
|
||||
InitializrService() {
|
||||
}
|
||||
|
||||
InitializrService(CloseableHttpClient http) {
|
||||
this.http = http;
|
||||
}
|
||||
|
||||
protected CloseableHttpClient getHttp() {
|
||||
if (this.http == null) {
|
||||
this.http = HttpClientBuilder.create().useSystemProperties().build();
|
||||
}
|
||||
return this.http;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a project based on the specified {@link ProjectGenerationRequest}.
|
||||
* @param request the generation request
|
||||
* @return an entity defining the project
|
||||
* @throws IOException if generation fails
|
||||
*/
|
||||
ProjectGenerationResponse generate(ProjectGenerationRequest request) throws IOException {
|
||||
Log.info("Using service at " + request.getServiceUrl());
|
||||
InitializrServiceMetadata metadata = loadMetadata(request.getServiceUrl());
|
||||
URI url = request.generateUrl(metadata);
|
||||
CloseableHttpResponse httpResponse = executeProjectGenerationRequest(url);
|
||||
HttpEntity httpEntity = httpResponse.getEntity();
|
||||
validateResponse(httpResponse, request.getServiceUrl());
|
||||
return createResponse(httpResponse, httpEntity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the {@link InitializrServiceMetadata} at the specified url.
|
||||
* @param serviceUrl to url of the initializer service
|
||||
* @return the metadata describing the service
|
||||
* @throws IOException if the service's metadata cannot be loaded
|
||||
*/
|
||||
InitializrServiceMetadata loadMetadata(String serviceUrl) throws IOException {
|
||||
CloseableHttpResponse httpResponse = executeInitializrMetadataRetrieval(serviceUrl);
|
||||
validateResponse(httpResponse, serviceUrl);
|
||||
return parseJsonMetadata(httpResponse.getEntity());
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the service capabilities of the service at the specified URL. If the service
|
||||
* supports generating a textual representation of the capabilities, it is returned,
|
||||
* otherwise {@link InitializrServiceMetadata} is returned.
|
||||
* @param serviceUrl to url of the initializer service
|
||||
* @return the service capabilities (as a String) or the
|
||||
* {@link InitializrServiceMetadata} describing the service
|
||||
* @throws IOException if the service capabilities cannot be loaded
|
||||
*/
|
||||
Object loadServiceCapabilities(String serviceUrl) throws IOException {
|
||||
HttpGet request = new HttpGet(serviceUrl);
|
||||
request.setHeader(new BasicHeader(HttpHeaders.ACCEPT, ACCEPT_SERVICE_CAPABILITIES));
|
||||
CloseableHttpResponse httpResponse = execute(request, serviceUrl, "retrieve help");
|
||||
validateResponse(httpResponse, serviceUrl);
|
||||
HttpEntity httpEntity = httpResponse.getEntity();
|
||||
ContentType contentType = ContentType.getOrDefault(httpEntity);
|
||||
if (contentType.getMimeType().equals("text/plain")) {
|
||||
return getContent(httpEntity);
|
||||
}
|
||||
return parseJsonMetadata(httpEntity);
|
||||
}
|
||||
|
||||
private InitializrServiceMetadata parseJsonMetadata(HttpEntity httpEntity) throws IOException {
|
||||
try {
|
||||
return new InitializrServiceMetadata(getContentAsJson(httpEntity));
|
||||
}
|
||||
catch (JSONException ex) {
|
||||
throw new ReportableException("Invalid content received from server (" + ex.getMessage() + ")", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateResponse(CloseableHttpResponse httpResponse, String serviceUrl) {
|
||||
if (httpResponse.getEntity() == null) {
|
||||
throw new ReportableException("No content received from server '" + serviceUrl + "'");
|
||||
}
|
||||
if (httpResponse.getStatusLine().getStatusCode() != 200) {
|
||||
throw createException(serviceUrl, httpResponse);
|
||||
}
|
||||
}
|
||||
|
||||
private ProjectGenerationResponse createResponse(CloseableHttpResponse httpResponse, HttpEntity httpEntity)
|
||||
throws IOException {
|
||||
ProjectGenerationResponse response = new ProjectGenerationResponse(ContentType.getOrDefault(httpEntity));
|
||||
response.setContent(FileCopyUtils.copyToByteArray(httpEntity.getContent()));
|
||||
String fileName = extractFileName(httpResponse.getFirstHeader("Content-Disposition"));
|
||||
if (fileName != null) {
|
||||
response.setFileName(fileName);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the creation of the project using the specified URL.
|
||||
* @param url the URL
|
||||
* @return the response
|
||||
*/
|
||||
private CloseableHttpResponse executeProjectGenerationRequest(URI url) {
|
||||
return execute(new HttpGet(url), url, "generate project");
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the meta-data of the service at the specified URL.
|
||||
* @param url the URL
|
||||
* @return the response
|
||||
*/
|
||||
private CloseableHttpResponse executeInitializrMetadataRetrieval(String url) {
|
||||
HttpGet request = new HttpGet(url);
|
||||
request.setHeader(new BasicHeader(HttpHeaders.ACCEPT, ACCEPT_META_DATA));
|
||||
return execute(request, url, "retrieve metadata");
|
||||
}
|
||||
|
||||
private CloseableHttpResponse execute(HttpUriRequest request, Object url, String description) {
|
||||
try {
|
||||
request.addHeader("User-Agent", "SpringBootCli/" + getClass().getPackage().getImplementationVersion());
|
||||
return getHttp().execute(request);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new ReportableException(
|
||||
"Failed to " + description + " from service at '" + url + "' (" + ex.getMessage() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
private ReportableException createException(String url, CloseableHttpResponse httpResponse) {
|
||||
String message = "Initializr service call failed using '" + url + "' - service returned "
|
||||
+ httpResponse.getStatusLine().getReasonPhrase();
|
||||
String error = extractMessage(httpResponse.getEntity());
|
||||
if (StringUtils.hasText(error)) {
|
||||
message += ": '" + error + "'";
|
||||
}
|
||||
else {
|
||||
int statusCode = httpResponse.getStatusLine().getStatusCode();
|
||||
message += " (unexpected " + statusCode + " error)";
|
||||
}
|
||||
throw new ReportableException(message);
|
||||
}
|
||||
|
||||
private String extractMessage(HttpEntity entity) {
|
||||
if (entity != null) {
|
||||
try {
|
||||
JSONObject error = getContentAsJson(entity);
|
||||
if (error.has("message")) {
|
||||
return error.getString("message");
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private JSONObject getContentAsJson(HttpEntity entity) throws IOException, JSONException {
|
||||
return new JSONObject(getContent(entity));
|
||||
}
|
||||
|
||||
private String getContent(HttpEntity entity) throws IOException {
|
||||
ContentType contentType = ContentType.getOrDefault(entity);
|
||||
Charset charset = contentType.getCharset();
|
||||
charset = (charset != null) ? charset : StandardCharsets.UTF_8;
|
||||
byte[] content = FileCopyUtils.copyToByteArray(entity.getContent());
|
||||
return new String(content, charset);
|
||||
}
|
||||
|
||||
private String extractFileName(Header header) {
|
||||
if (header != null) {
|
||||
String value = header.getValue();
|
||||
int start = value.indexOf(FILENAME_HEADER_PREFIX);
|
||||
if (start != -1) {
|
||||
value = value.substring(start + FILENAME_HEADER_PREFIX.length());
|
||||
int end = value.indexOf('\"');
|
||||
if (end != -1) {
|
||||
return value.substring(0, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
/*
|
||||
* Copyright 2012-2022 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
|
||||
*
|
||||
* https://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.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* Define the metadata available for a particular service instance.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class InitializrServiceMetadata {
|
||||
|
||||
private static final String DEPENDENCIES_EL = "dependencies";
|
||||
|
||||
private static final String TYPE_EL = "type";
|
||||
|
||||
private static final String VALUES_EL = "values";
|
||||
|
||||
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<String, Dependency> dependencies;
|
||||
|
||||
private final MetadataHolder<String, ProjectType> projectTypes;
|
||||
|
||||
private final Map<String, String> defaults;
|
||||
|
||||
/**
|
||||
* Creates a new instance using the specified root {@link JSONObject}.
|
||||
* @param root the root JSONObject
|
||||
* @throws JSONException on JSON parsing failure
|
||||
*/
|
||||
InitializrServiceMetadata(JSONObject root) throws JSONException {
|
||||
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.
|
||||
* @return the supported dependencies
|
||||
*/
|
||||
Collection<Dependency> getDependencies() {
|
||||
return this.dependencies.values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the dependency with the specified id or {@code null} if no such dependency
|
||||
* exists.
|
||||
* @param id the id
|
||||
* @return the dependency or {@code null}
|
||||
*/
|
||||
Dependency getDependency(String id) {
|
||||
return this.dependencies.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the project types supported by the service.
|
||||
* @return the supported project types
|
||||
*/
|
||||
Map<String, ProjectType> getProjectTypes() {
|
||||
return this.projectTypes.getContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the default type to use or {@code null} if the metadata does not define any
|
||||
* default.
|
||||
* @return the default project type or {@code null}
|
||||
*/
|
||||
ProjectType getDefaultType() {
|
||||
if (this.projectTypes.getDefaultItem() != null) {
|
||||
return this.projectTypes.getDefaultItem();
|
||||
}
|
||||
String defaultTypeId = getDefaults().get("type");
|
||||
if (defaultTypeId != null) {
|
||||
return this.projectTypes.getContent().get(defaultTypeId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the defaults applicable to the service.
|
||||
* @return the defaults of the service
|
||||
*/
|
||||
Map<String, String> getDefaults() {
|
||||
return this.defaults;
|
||||
}
|
||||
|
||||
private Map<String, Dependency> parseDependencies(JSONObject root) throws JSONException {
|
||||
Map<String, Dependency> result = new HashMap<>();
|
||||
if (!root.has(DEPENDENCIES_EL)) {
|
||||
return result;
|
||||
}
|
||||
JSONObject dependencies = root.getJSONObject(DEPENDENCIES_EL);
|
||||
JSONArray array = dependencies.getJSONArray(VALUES_EL);
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
JSONObject group = array.getJSONObject(i);
|
||||
parseGroup(group, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private MetadataHolder<String, ProjectType> parseProjectTypes(JSONObject root) throws JSONException {
|
||||
MetadataHolder<String, ProjectType> result = new MetadataHolder<>();
|
||||
if (!root.has(TYPE_EL)) {
|
||||
return result;
|
||||
}
|
||||
JSONObject type = root.getJSONObject(TYPE_EL);
|
||||
JSONArray array = type.getJSONArray(VALUES_EL);
|
||||
String defaultType = (type.has(DEFAULT_ATTRIBUTE) ? type.getString(DEFAULT_ATTRIBUTE) : null);
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
JSONObject typeJson = array.getJSONObject(i);
|
||||
ProjectType projectType = parseType(typeJson, defaultType);
|
||||
result.getContent().put(projectType.getId(), projectType);
|
||||
if (projectType.isDefaultType()) {
|
||||
result.setDefaultItem(projectType);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, String> parseDefaults(JSONObject root) throws JSONException {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
Iterator<?> keys = root.keys();
|
||||
while (keys.hasNext()) {
|
||||
String key = (String) keys.next();
|
||||
Object o = root.get(key);
|
||||
if (o instanceof JSONObject child) {
|
||||
if (child.has(DEFAULT_ATTRIBUTE)) {
|
||||
result.put(key, child.getString(DEFAULT_ATTRIBUTE));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void parseGroup(JSONObject group, Map<String, Dependency> dependencies) throws JSONException {
|
||||
if (group.has(VALUES_EL)) {
|
||||
JSONArray content = group.getJSONArray(VALUES_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) throws JSONException {
|
||||
String id = getStringValue(object, ID_ATTRIBUTE, null);
|
||||
String name = getStringValue(object, NAME_ATTRIBUTE, null);
|
||||
String description = getStringValue(object, DESCRIPTION_ATTRIBUTE, null);
|
||||
return new Dependency(id, name, description);
|
||||
}
|
||||
|
||||
private ProjectType parseType(JSONObject object, String defaultId) throws JSONException {
|
||||
String id = getStringValue(object, ID_ATTRIBUTE, null);
|
||||
String name = getStringValue(object, NAME_ATTRIBUTE, null);
|
||||
String action = getStringValue(object, ACTION_ATTRIBUTE, null);
|
||||
boolean defaultType = id.equals(defaultId);
|
||||
Map<String, String> 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) throws JSONException {
|
||||
return object.has(name) ? object.getString(name) : defaultValue;
|
||||
}
|
||||
|
||||
private Map<String, String> parseStringItems(JSONObject json) throws JSONException {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
for (Iterator<?> iterator = json.keys(); iterator.hasNext();) {
|
||||
String key = (String) iterator.next();
|
||||
Object value = json.get(key);
|
||||
if (value instanceof String string) {
|
||||
result.put(key, string);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static final class MetadataHolder<K, T> {
|
||||
|
||||
private final Map<K, T> content;
|
||||
|
||||
private T defaultItem;
|
||||
|
||||
private MetadataHolder() {
|
||||
this.content = new HashMap<>();
|
||||
}
|
||||
|
||||
Map<K, T> getContent() {
|
||||
return this.content;
|
||||
}
|
||||
|
||||
T getDefaultItem() {
|
||||
return this.defaultItem;
|
||||
}
|
||||
|
||||
void setDefaultItem(T defaultItem) {
|
||||
this.defaultItem = defaultItem;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
/*
|
||||
* Copyright 2012-2022 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
|
||||
*
|
||||
* https://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.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.http.client.utils.URIBuilder;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Represent the settings to apply to generating the project.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Eddú Meléndez
|
||||
*/
|
||||
class ProjectGenerationRequest {
|
||||
|
||||
public static final String DEFAULT_SERVICE_URL = "https://start.spring.io";
|
||||
|
||||
private String serviceUrl = DEFAULT_SERVICE_URL;
|
||||
|
||||
private String output;
|
||||
|
||||
private boolean extract;
|
||||
|
||||
private String groupId;
|
||||
|
||||
private String artifactId;
|
||||
|
||||
private String version;
|
||||
|
||||
private String name;
|
||||
|
||||
private String description;
|
||||
|
||||
private String packageName;
|
||||
|
||||
private String type;
|
||||
|
||||
private String packaging;
|
||||
|
||||
private String build;
|
||||
|
||||
private String format;
|
||||
|
||||
private boolean detectType;
|
||||
|
||||
private String javaVersion;
|
||||
|
||||
private String language;
|
||||
|
||||
private String bootVersion;
|
||||
|
||||
private final List<String> dependencies = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* The URL of the service to use.
|
||||
* @return the service URL
|
||||
* @see #DEFAULT_SERVICE_URL
|
||||
*/
|
||||
String getServiceUrl() {
|
||||
return this.serviceUrl;
|
||||
}
|
||||
|
||||
void setServiceUrl(String serviceUrl) {
|
||||
this.serviceUrl = serviceUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* The location of the generated project.
|
||||
* @return the location of the generated project
|
||||
*/
|
||||
String getOutput() {
|
||||
return this.output;
|
||||
}
|
||||
|
||||
void setOutput(String output) {
|
||||
if (output != null && output.endsWith("/")) {
|
||||
this.output = output.substring(0, output.length() - 1);
|
||||
this.extract = true;
|
||||
}
|
||||
else {
|
||||
this.output = output;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the project archive should be extracted in the output location. If the
|
||||
* {@link #getOutput() output} ends with "/", the project is extracted automatically.
|
||||
* @return {@code true} if the archive should be extracted, otherwise {@code false}
|
||||
*/
|
||||
boolean isExtract() {
|
||||
return this.extract;
|
||||
}
|
||||
|
||||
void setExtract(boolean extract) {
|
||||
this.extract = extract;
|
||||
}
|
||||
|
||||
/**
|
||||
* The groupId to use or {@code null} if it should not be customized.
|
||||
* @return the groupId or {@code null}
|
||||
*/
|
||||
String getGroupId() {
|
||||
return this.groupId;
|
||||
}
|
||||
|
||||
void setGroupId(String groupId) {
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
/**
|
||||
* The artifactId to use or {@code null} if it should not be customized.
|
||||
* @return the artifactId or {@code null}
|
||||
*/
|
||||
String getArtifactId() {
|
||||
return this.artifactId;
|
||||
}
|
||||
|
||||
void setArtifactId(String artifactId) {
|
||||
this.artifactId = artifactId;
|
||||
}
|
||||
|
||||
/**
|
||||
* The artifact version to use or {@code null} if it should not be customized.
|
||||
* @return the artifact version or {@code null}
|
||||
*/
|
||||
String getVersion() {
|
||||
return this.version;
|
||||
}
|
||||
|
||||
void setVersion(String version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name to use or {@code null} if it should not be customized.
|
||||
* @return the name or {@code null}
|
||||
*/
|
||||
String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* The description to use or {@code null} if it should not be customized.
|
||||
* @return the description or {@code null}
|
||||
*/
|
||||
String getDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the package name or {@code null} if it should not be customized.
|
||||
* @return the package name or {@code null}
|
||||
*/
|
||||
String getPackageName() {
|
||||
return this.packageName;
|
||||
}
|
||||
|
||||
void setPackageName(String packageName) {
|
||||
this.packageName = packageName;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of project to generate. Should match one of the advertised type that the
|
||||
* service supports. If not set, the default is retrieved from the service metadata.
|
||||
* @return the project type
|
||||
*/
|
||||
String getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* The packaging type or {@code null} if it should not be customized.
|
||||
* @return the packaging type or {@code null}
|
||||
*/
|
||||
String getPackaging() {
|
||||
return this.packaging;
|
||||
}
|
||||
|
||||
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.
|
||||
* @return the build type
|
||||
*/
|
||||
String getBuild() {
|
||||
return this.build;
|
||||
}
|
||||
|
||||
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.
|
||||
* @return the project format
|
||||
*/
|
||||
String getFormat() {
|
||||
return this.format;
|
||||
}
|
||||
|
||||
void setFormat(String format) {
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the type should be detected based on the build and format value.
|
||||
* @return {@code true} if type detection will be performed, otherwise {@code false}
|
||||
*/
|
||||
boolean isDetectType() {
|
||||
return this.detectType;
|
||||
}
|
||||
|
||||
void setDetectType(boolean detectType) {
|
||||
this.detectType = detectType;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Java version to use or {@code null} if it should not be customized.
|
||||
* @return the Java version or {@code null}
|
||||
*/
|
||||
String getJavaVersion() {
|
||||
return this.javaVersion;
|
||||
}
|
||||
|
||||
void setJavaVersion(String javaVersion) {
|
||||
this.javaVersion = javaVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* The programming language to use or {@code null} if it should not be customized.
|
||||
* @return the programming language or {@code null}
|
||||
*/
|
||||
String getLanguage() {
|
||||
return this.language;
|
||||
}
|
||||
|
||||
void setLanguage(String language) {
|
||||
this.language = language;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Spring Boot version to use or {@code null} if it should not be customized.
|
||||
* @return the Spring Boot version or {@code null}
|
||||
*/
|
||||
String getBootVersion() {
|
||||
return this.bootVersion;
|
||||
}
|
||||
|
||||
void setBootVersion(String bootVersion) {
|
||||
this.bootVersion = bootVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* The identifiers of the dependencies to include in the project.
|
||||
* @return the dependency identifiers
|
||||
*/
|
||||
List<String> getDependencies() {
|
||||
return this.dependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the URI to use to generate a project represented by this request.
|
||||
* @param metadata the metadata that describes the service
|
||||
* @return the project generation URI
|
||||
*/
|
||||
URI generateUrl(InitializrServiceMetadata metadata) {
|
||||
try {
|
||||
URIBuilder builder = new URIBuilder(this.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.dependencies.isEmpty()) {
|
||||
builder.setParameter("dependencies", StringUtils.collectionToCommaDelimitedString(this.dependencies));
|
||||
}
|
||||
|
||||
if (this.groupId != null) {
|
||||
builder.setParameter("groupId", this.groupId);
|
||||
}
|
||||
String resolvedArtifactId = resolveArtifactId();
|
||||
if (resolvedArtifactId != null) {
|
||||
builder.setParameter("artifactId", resolvedArtifactId);
|
||||
}
|
||||
if (this.version != null) {
|
||||
builder.setParameter("version", this.version);
|
||||
}
|
||||
if (this.name != null) {
|
||||
builder.setParameter("name", this.name);
|
||||
}
|
||||
if (this.description != null) {
|
||||
builder.setParameter("description", this.description);
|
||||
}
|
||||
if (this.packageName != null) {
|
||||
builder.setParameter("packageName", this.packageName);
|
||||
}
|
||||
if (this.type != null) {
|
||||
builder.setParameter("type", projectType.getId());
|
||||
}
|
||||
if (this.packaging != null) {
|
||||
builder.setParameter("packaging", this.packaging);
|
||||
}
|
||||
if (this.javaVersion != null) {
|
||||
builder.setParameter("javaVersion", this.javaVersion);
|
||||
}
|
||||
if (this.language != null) {
|
||||
builder.setParameter("language", this.language);
|
||||
}
|
||||
if (this.bootVersion != null) {
|
||||
builder.setParameter("bootVersion", this.bootVersion);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
catch (URISyntaxException ex) {
|
||||
throw new ReportableException("Invalid service URL (" + ex.getMessage() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
protected ProjectType determineProjectType(InitializrServiceMetadata metadata) {
|
||||
if (this.type != null) {
|
||||
ProjectType result = metadata.getProjectTypes().get(this.type);
|
||||
if (result == null) {
|
||||
throw new ReportableException(
|
||||
("No project type with id '" + this.type + "' - check the service capabilities (--list)"));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
else if (isDetectType()) {
|
||||
Map<String, ProjectType> 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.isEmpty()) {
|
||||
throw new ReportableException("No type found with build '" + this.build + "' and format '" + this.format
|
||||
+ "' check the service capabilities (--list)");
|
||||
}
|
||||
else {
|
||||
throw new ReportableException("Multiple types found with build '" + this.build + "' and format '"
|
||||
+ this.format + "' use --type with a more specific value " + types.keySet());
|
||||
}
|
||||
}
|
||||
else {
|
||||
ProjectType defaultType = metadata.getDefaultType();
|
||||
if (defaultType == null) {
|
||||
throw new ReportableException(("No project type is set and no default is defined. "
|
||||
+ "Check the service capabilities (--list)"));
|
||||
}
|
||||
return defaultType;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the artifactId to use or {@code null} if it should not be customized.
|
||||
* @return the artifactId
|
||||
*/
|
||||
protected String resolveArtifactId() {
|
||||
if (this.artifactId != null) {
|
||||
return this.artifactId;
|
||||
}
|
||||
if (this.output != null) {
|
||||
int i = this.output.lastIndexOf('.');
|
||||
return (i != -1) ? this.output.substring(0, i) : this.output;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void filter(Map<String, ProjectType> projects, String tag, String tagValue) {
|
||||
projects.entrySet().removeIf((entry) -> !tagValue.equals(entry.getValue().getTags().get(tag)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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
|
||||
*/
|
||||
class ProjectGenerationResponse {
|
||||
|
||||
private final ContentType contentType;
|
||||
|
||||
private byte[] content;
|
||||
|
||||
private String fileName;
|
||||
|
||||
ProjectGenerationResponse(ContentType contentType) {
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link ContentType} of this instance.
|
||||
* @return the content type
|
||||
*/
|
||||
ContentType getContentType() {
|
||||
return this.contentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* The generated project archive or file.
|
||||
* @return the content
|
||||
*/
|
||||
byte[] getContent() {
|
||||
return this.content;
|
||||
}
|
||||
|
||||
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.
|
||||
* @return the file name, or {@code null}
|
||||
*/
|
||||
String getFileName() {
|
||||
return this.fileName;
|
||||
}
|
||||
|
||||
void setFileName(String fileName) {
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright 2012-2020 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
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cli.command.init;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import org.springframework.boot.cli.util.Log;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
/**
|
||||
* Helper class used to generate the project.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class ProjectGenerator {
|
||||
|
||||
private static final String ZIP_MIME_TYPE = "application/zip";
|
||||
|
||||
private final InitializrService initializrService;
|
||||
|
||||
ProjectGenerator(InitializrService initializrService) {
|
||||
this.initializrService = initializrService;
|
||||
}
|
||||
|
||||
void generateProject(ProjectGenerationRequest request, boolean force) throws IOException {
|
||||
ProjectGenerationResponse response = this.initializrService.generate(request);
|
||||
String fileName = (request.getOutput() != null) ? request.getOutput() : response.getFileName();
|
||||
if (shouldExtract(request, response)) {
|
||||
if (isZipArchive(response)) {
|
||||
extractProject(response, request.getOutput(), force);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
Log.info("Could not extract '" + response.getContentType() + "'");
|
||||
// Use value from the server since we can't extract it
|
||||
fileName = response.getFileName();
|
||||
}
|
||||
}
|
||||
if (fileName == null) {
|
||||
throw new ReportableException("Could not save the project, the server did not set a preferred "
|
||||
+ "file name and no location was set. Specify the output location for the project.");
|
||||
}
|
||||
writeProject(response, fileName, force);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if the project should be extracted.
|
||||
* @param request the generation request
|
||||
* @param response the generation response
|
||||
* @return if the project should be extracted
|
||||
*/
|
||||
private boolean shouldExtract(ProjectGenerationRequest request, ProjectGenerationResponse response) {
|
||||
if (request.isExtract()) {
|
||||
return true;
|
||||
}
|
||||
// explicit name hasn't been provided for an archive and there is no extension
|
||||
return isZipArchive(response) && request.getOutput() != null && !request.getOutput().contains(".");
|
||||
}
|
||||
|
||||
private boolean isZipArchive(ProjectGenerationResponse entity) {
|
||||
if (entity.getContentType() != null) {
|
||||
try {
|
||||
return ZIP_MIME_TYPE.equals(entity.getContentType().getMimeType());
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void extractProject(ProjectGenerationResponse entity, String output, boolean overwrite) throws IOException {
|
||||
File outputDirectory = (output != null) ? new File(output) : new File(System.getProperty("user.dir"));
|
||||
if (!outputDirectory.exists()) {
|
||||
outputDirectory.mkdirs();
|
||||
}
|
||||
try (ZipInputStream zipStream = new ZipInputStream(new ByteArrayInputStream(entity.getContent()))) {
|
||||
extractFromStream(zipStream, overwrite, outputDirectory);
|
||||
fixExecutableFlag(outputDirectory, "mvnw");
|
||||
fixExecutableFlag(outputDirectory, "gradlew");
|
||||
Log.info("Project extracted to '" + outputDirectory.getAbsolutePath() + "'");
|
||||
}
|
||||
}
|
||||
|
||||
private void extractFromStream(ZipInputStream zipStream, boolean overwrite, File outputDirectory)
|
||||
throws IOException {
|
||||
ZipEntry entry = zipStream.getNextEntry();
|
||||
String canonicalOutputPath = outputDirectory.getCanonicalPath() + File.separator;
|
||||
while (entry != null) {
|
||||
File file = new File(outputDirectory, entry.getName());
|
||||
String canonicalEntryPath = file.getCanonicalPath();
|
||||
if (!canonicalEntryPath.startsWith(canonicalOutputPath)) {
|
||||
throw new ReportableException("Entry '" + entry.getName() + "' would be written to '"
|
||||
+ canonicalEntryPath + "'. This is outside the output location of '" + canonicalOutputPath
|
||||
+ "'. Verify your target server configuration.");
|
||||
}
|
||||
if (file.exists() && !overwrite) {
|
||||
throw new ReportableException((file.isDirectory() ? "Directory" : "File") + " '" + file.getName()
|
||||
+ "' already exists. Use --force if you want to overwrite or "
|
||||
+ "specify an alternate location.");
|
||||
}
|
||||
if (!entry.isDirectory()) {
|
||||
FileCopyUtils.copy(StreamUtils.nonClosing(zipStream), new FileOutputStream(file));
|
||||
}
|
||||
else {
|
||||
file.mkdir();
|
||||
}
|
||||
zipStream.closeEntry();
|
||||
entry = zipStream.getNextEntry();
|
||||
}
|
||||
}
|
||||
|
||||
private void writeProject(ProjectGenerationResponse entity, String output, boolean overwrite) throws IOException {
|
||||
File outputFile = new File(output);
|
||||
if (outputFile.exists()) {
|
||||
if (!overwrite) {
|
||||
throw new ReportableException(
|
||||
"File '" + outputFile.getName() + "' already exists. Use --force if you want to "
|
||||
+ "overwrite or specify an alternate location.");
|
||||
}
|
||||
if (!outputFile.delete()) {
|
||||
throw new ReportableException("Failed to delete existing file " + outputFile.getPath());
|
||||
}
|
||||
}
|
||||
FileCopyUtils.copy(entity.getContent(), outputFile);
|
||||
Log.info("Content saved to '" + output + "'");
|
||||
}
|
||||
|
||||
private void fixExecutableFlag(File dir, String fileName) {
|
||||
File f = new File(dir, fileName);
|
||||
if (f.exists()) {
|
||||
f.setExecutable(true, false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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
|
||||
*/
|
||||
class ProjectType {
|
||||
|
||||
private final String id;
|
||||
|
||||
private final String name;
|
||||
|
||||
private final String action;
|
||||
|
||||
private final boolean defaultType;
|
||||
|
||||
private final Map<String, String> tags = new HashMap<>();
|
||||
|
||||
ProjectType(String id, String name, String action, boolean defaultType, Map<String, String> tags) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.action = action;
|
||||
this.defaultType = defaultType;
|
||||
if (tags != null) {
|
||||
this.tags.putAll(tags);
|
||||
}
|
||||
}
|
||||
|
||||
String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
String getAction() {
|
||||
return this.action;
|
||||
}
|
||||
|
||||
boolean isDefaultType() {
|
||||
return this.defaultType;
|
||||
}
|
||||
|
||||
Map<String, String> getTags() {
|
||||
return Collections.unmodifiableMap(this.tags);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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;
|
||||
|
||||
/**
|
||||
* Exception with a message that can be reported to the user.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 1.2.0
|
||||
*/
|
||||
public class ReportableException extends RuntimeException {
|
||||
|
||||
public ReportableException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ReportableException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright 2012-2022 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
|
||||
*
|
||||
* https://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 java.util.Map.Entry;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* A helper class generating a report from the meta-data of a particular service.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class ServiceCapabilitiesReportGenerator {
|
||||
|
||||
private static final String NEW_LINE = System.lineSeparator();
|
||||
|
||||
private final InitializrService initializrService;
|
||||
|
||||
/**
|
||||
* Creates an instance using the specified {@link InitializrService}.
|
||||
* @param initializrService the initializr service
|
||||
*/
|
||||
ServiceCapabilitiesReportGenerator(InitializrService initializrService) {
|
||||
this.initializrService = initializrService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a report for the specified service. The report contains the available
|
||||
* capabilities as advertised by the root endpoint.
|
||||
* @param url the url of the service
|
||||
* @return the report that describes the service
|
||||
* @throws IOException if the report cannot be generated
|
||||
*/
|
||||
String generate(String url) throws IOException {
|
||||
Object content = this.initializrService.loadServiceCapabilities(url);
|
||||
if (content instanceof InitializrServiceMetadata metadata) {
|
||||
return generateHelp(url, metadata);
|
||||
}
|
||||
return content.toString();
|
||||
}
|
||||
|
||||
private String generateHelp(String url, InitializrServiceMetadata metadata) {
|
||||
String header = "Capabilities of " + url;
|
||||
StringBuilder report = new StringBuilder();
|
||||
report.append("=".repeat(header.length())).append(NEW_LINE);
|
||||
report.append(header).append(NEW_LINE);
|
||||
report.append("=".repeat(header.length())).append(NEW_LINE);
|
||||
report.append(NEW_LINE);
|
||||
reportAvailableDependencies(metadata, report);
|
||||
report.append(NEW_LINE);
|
||||
reportAvailableProjectTypes(metadata, report);
|
||||
report.append(NEW_LINE);
|
||||
reportDefaults(report, metadata);
|
||||
return report.toString();
|
||||
}
|
||||
|
||||
private void reportAvailableDependencies(InitializrServiceMetadata metadata, StringBuilder report) {
|
||||
report.append("Available dependencies:").append(NEW_LINE);
|
||||
report.append("-----------------------").append(NEW_LINE);
|
||||
List<Dependency> dependencies = getSortedDependencies(metadata);
|
||||
for (Dependency dependency : dependencies) {
|
||||
report.append(dependency.getId()).append(" - ").append(dependency.getName());
|
||||
if (dependency.getDescription() != null) {
|
||||
report.append(": ").append(dependency.getDescription());
|
||||
}
|
||||
report.append(NEW_LINE);
|
||||
}
|
||||
}
|
||||
|
||||
private List<Dependency> getSortedDependencies(InitializrServiceMetadata metadata) {
|
||||
List<Dependency> dependencies = new ArrayList<>(metadata.getDependencies());
|
||||
dependencies.sort(Comparator.comparing(Dependency::getId));
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
private void reportAvailableProjectTypes(InitializrServiceMetadata metadata, StringBuilder report) {
|
||||
report.append("Available project types:").append(NEW_LINE);
|
||||
report.append("------------------------").append(NEW_LINE);
|
||||
SortedSet<Entry<String, ProjectType>> entries = new TreeSet<>(Entry.comparingByKey());
|
||||
entries.addAll(metadata.getProjectTypes().entrySet());
|
||||
for (Entry<String, ProjectType> entry : entries) {
|
||||
ProjectType type = entry.getValue();
|
||||
report.append(entry.getKey()).append(" - ").append(type.getName());
|
||||
if (!type.getTags().isEmpty()) {
|
||||
reportTags(report, type);
|
||||
}
|
||||
if (type.isDefaultType()) {
|
||||
report.append(" (default)");
|
||||
}
|
||||
report.append(NEW_LINE);
|
||||
}
|
||||
}
|
||||
|
||||
private void reportTags(StringBuilder report, ProjectType type) {
|
||||
Map<String, String> tags = type.getTags();
|
||||
Iterator<Map.Entry<String, String>> iterator = tags.entrySet().iterator();
|
||||
report.append(" [");
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<String, String> entry = iterator.next();
|
||||
report.append(entry.getKey()).append(":").append(entry.getValue());
|
||||
if (iterator.hasNext()) {
|
||||
report.append(", ");
|
||||
}
|
||||
}
|
||||
report.append("]");
|
||||
}
|
||||
|
||||
private void reportDefaults(StringBuilder report, InitializrServiceMetadata metadata) {
|
||||
report.append("Defaults:").append(NEW_LINE);
|
||||
report.append("---------").append(NEW_LINE);
|
||||
List<String> defaultsKeys = new ArrayList<>(metadata.getDefaults().keySet());
|
||||
Collections.sort(defaultsKeys);
|
||||
for (String defaultsKey : defaultsKeys) {
|
||||
String defaultsValue = metadata.getDefaults().get(defaultsKey);
|
||||
report.append(defaultsKey).append(": ").append(defaultsValue).append(NEW_LINE);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* CLI command for initializing a new application using Spring Initializr.
|
||||
*/
|
||||
package org.springframework.boot.cli.command.init;
|
||||
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* Copyright 2012-2021 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
|
||||
*
|
||||
* https://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.options;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.function.Function;
|
||||
|
||||
import joptsimple.BuiltinHelpFormatter;
|
||||
import joptsimple.HelpFormatter;
|
||||
import joptsimple.OptionDescriptor;
|
||||
import joptsimple.OptionParser;
|
||||
import joptsimple.OptionSet;
|
||||
import joptsimple.OptionSpecBuilder;
|
||||
|
||||
import org.springframework.boot.cli.command.OptionParsingCommand;
|
||||
import org.springframework.boot.cli.command.status.ExitStatus;
|
||||
|
||||
/**
|
||||
* Delegate used by {@link OptionParsingCommand} to parse options and run the command.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @since 1.0.0
|
||||
* @see OptionParsingCommand
|
||||
* @see #run(OptionSet)
|
||||
*/
|
||||
public class OptionHandler {
|
||||
|
||||
private final Function<String, String> argumentProcessor;
|
||||
|
||||
private OptionParser parser;
|
||||
|
||||
private String help;
|
||||
|
||||
private Collection<OptionHelp> optionHelp;
|
||||
|
||||
/**
|
||||
* Create a new {@link OptionHandler} instance.
|
||||
*/
|
||||
public OptionHandler() {
|
||||
this(Function.identity());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link OptionHandler} instance with an argument processor.
|
||||
* @param argumentProcessor strategy that can be used to manipulate arguments before
|
||||
* they are used.
|
||||
*/
|
||||
public OptionHandler(Function<String, String> argumentProcessor) {
|
||||
this.argumentProcessor = argumentProcessor;
|
||||
}
|
||||
|
||||
public OptionSpecBuilder option(String name, String description) {
|
||||
return getParser().accepts(name, description);
|
||||
}
|
||||
|
||||
public OptionSpecBuilder option(List<String> aliases, String description) {
|
||||
return getParser().acceptsAll(aliases, description);
|
||||
}
|
||||
|
||||
public OptionParser getParser() {
|
||||
if (this.parser == null) {
|
||||
this.parser = new OptionParser();
|
||||
options();
|
||||
}
|
||||
return this.parser;
|
||||
}
|
||||
|
||||
protected void options() {
|
||||
}
|
||||
|
||||
public final ExitStatus run(String... args) throws Exception {
|
||||
String[] argsToUse = args.clone();
|
||||
for (int i = 0; i < argsToUse.length; i++) {
|
||||
if ("-cp".equals(argsToUse[i])) {
|
||||
argsToUse[i] = "--cp";
|
||||
}
|
||||
argsToUse[i] = this.argumentProcessor.apply(argsToUse[i]);
|
||||
}
|
||||
OptionSet options = getParser().parse(argsToUse);
|
||||
return run(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the command using the specified parsed {@link OptionSet}.
|
||||
* @param options the parsed option set
|
||||
* @return an ExitStatus
|
||||
* @throws Exception in case of errors
|
||||
*/
|
||||
protected ExitStatus run(OptionSet options) throws Exception {
|
||||
return ExitStatus.OK;
|
||||
}
|
||||
|
||||
public String getHelp() {
|
||||
if (this.help == null) {
|
||||
getParser().formatHelpWith(new BuiltinHelpFormatter(80, 2));
|
||||
OutputStream out = new ByteArrayOutputStream();
|
||||
try {
|
||||
getParser().printHelpOn(out);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
return "Help not available";
|
||||
}
|
||||
this.help = out.toString().replace(" --cp ", " -cp ");
|
||||
}
|
||||
return this.help;
|
||||
}
|
||||
|
||||
public Collection<OptionHelp> getOptionsHelp() {
|
||||
if (this.optionHelp == null) {
|
||||
OptionHelpFormatter formatter = new OptionHelpFormatter();
|
||||
getParser().formatHelpWith(formatter);
|
||||
try {
|
||||
getParser().printHelpOn(new ByteArrayOutputStream());
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// Ignore and provide no hints
|
||||
}
|
||||
this.optionHelp = formatter.getOptionHelp();
|
||||
}
|
||||
return this.optionHelp;
|
||||
}
|
||||
|
||||
private static class OptionHelpFormatter implements HelpFormatter {
|
||||
|
||||
private final List<OptionHelp> help = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public String format(Map<String, ? extends OptionDescriptor> options) {
|
||||
Comparator<OptionDescriptor> comparator = Comparator
|
||||
.comparing((optionDescriptor) -> optionDescriptor.options().iterator().next());
|
||||
Set<OptionDescriptor> sorted = new TreeSet<>(comparator);
|
||||
sorted.addAll(options.values());
|
||||
for (OptionDescriptor descriptor : sorted) {
|
||||
if (!descriptor.representsNonOptions()) {
|
||||
this.help.add(new OptionHelpAdapter(descriptor));
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
Collection<OptionHelp> getOptionHelp() {
|
||||
return Collections.unmodifiableList(this.help);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class OptionHelpAdapter implements OptionHelp {
|
||||
|
||||
private final Set<String> options;
|
||||
|
||||
private final String description;
|
||||
|
||||
OptionHelpAdapter(OptionDescriptor descriptor) {
|
||||
this.options = new LinkedHashSet<>();
|
||||
for (String option : descriptor.options()) {
|
||||
String prefix = (option.length() != 1) ? "--" : "-";
|
||||
this.options.add(prefix + option);
|
||||
}
|
||||
if (this.options.contains("--cp")) {
|
||||
this.options.remove("--cp");
|
||||
this.options.add("-cp");
|
||||
}
|
||||
this.description = descriptor.description();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getOptions() {
|
||||
return this.options;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageHelp() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.options;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Help for a specific option.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public interface OptionHelp {
|
||||
|
||||
/**
|
||||
* Returns the set of options that are mutually synonymous.
|
||||
* @return the options
|
||||
*/
|
||||
Set<String> getOptions();
|
||||
|
||||
/**
|
||||
* Returns usage help for the option.
|
||||
* @return the usage help
|
||||
*/
|
||||
String getUsageHelp();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Support classes for handling command line options.
|
||||
*/
|
||||
package org.springframework.boot.cli.command.options;
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Command infrastructure for the CLI.
|
||||
*/
|
||||
package org.springframework.boot.cli.command;
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.shell;
|
||||
|
||||
import jline.Terminal;
|
||||
import org.fusesource.jansi.Ansi;
|
||||
import org.fusesource.jansi.AnsiRenderer.Code;
|
||||
|
||||
/**
|
||||
* Simple utility class to build an ANSI string when supported by the {@link Terminal}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class AnsiString {
|
||||
|
||||
private final Terminal terminal;
|
||||
|
||||
private final StringBuilder value = new StringBuilder();
|
||||
|
||||
/**
|
||||
* Create a new {@link AnsiString} for the given {@link Terminal}.
|
||||
* @param terminal the terminal used to test if {@link Terminal#isAnsiSupported() ANSI
|
||||
* is supported}.
|
||||
*/
|
||||
AnsiString(Terminal terminal) {
|
||||
this.terminal = terminal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append text with the given ANSI codes.
|
||||
* @param text the text to append
|
||||
* @param codes the ANSI codes
|
||||
* @return this string
|
||||
*/
|
||||
AnsiString append(String text, Code... codes) {
|
||||
if (codes.length == 0 || !isAnsiSupported()) {
|
||||
this.value.append(text);
|
||||
return this;
|
||||
}
|
||||
Ansi ansi = Ansi.ansi();
|
||||
for (Code code : codes) {
|
||||
ansi = applyCode(ansi, code);
|
||||
}
|
||||
this.value.append(ansi.a(text).reset().toString());
|
||||
return this;
|
||||
}
|
||||
|
||||
private Ansi applyCode(Ansi ansi, Code code) {
|
||||
if (code.isColor()) {
|
||||
if (code.isBackground()) {
|
||||
return ansi.bg(code.getColor());
|
||||
}
|
||||
return ansi.fg(code.getColor());
|
||||
}
|
||||
return ansi.a(code.getAttribute());
|
||||
}
|
||||
|
||||
private boolean isAnsiSupported() {
|
||||
return this.terminal.isAnsiSupported();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.value.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.shell;
|
||||
|
||||
import jline.console.ConsoleReader;
|
||||
|
||||
import org.springframework.boot.cli.command.AbstractCommand;
|
||||
import org.springframework.boot.cli.command.status.ExitStatus;
|
||||
|
||||
/**
|
||||
* Clear the {@link Shell} screen.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ClearCommand extends AbstractCommand {
|
||||
|
||||
private final ConsoleReader consoleReader;
|
||||
|
||||
ClearCommand(ConsoleReader consoleReader) {
|
||||
super("clear", "Clear the screen");
|
||||
this.consoleReader = consoleReader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExitStatus run(String... args) throws Exception {
|
||||
this.consoleReader.setPrompt("");
|
||||
this.consoleReader.clearScreen();
|
||||
return ExitStatus.OK;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright 2012-2020 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
|
||||
*
|
||||
* https://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.shell;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import jline.console.ConsoleReader;
|
||||
import jline.console.completer.AggregateCompleter;
|
||||
import jline.console.completer.ArgumentCompleter;
|
||||
import jline.console.completer.ArgumentCompleter.ArgumentDelimiter;
|
||||
import jline.console.completer.Completer;
|
||||
import jline.console.completer.FileNameCompleter;
|
||||
import jline.console.completer.StringsCompleter;
|
||||
|
||||
import org.springframework.boot.cli.command.Command;
|
||||
import org.springframework.boot.cli.command.options.OptionHelp;
|
||||
import org.springframework.boot.cli.util.Log;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* JLine {@link Completer} for Spring Boot {@link Command}s.
|
||||
*
|
||||
* @author Jon Brisbin
|
||||
* @author Phillip Webb
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class CommandCompleter extends StringsCompleter {
|
||||
|
||||
private final Map<String, Completer> commandCompleters = new HashMap<>();
|
||||
|
||||
private final List<Command> commands = new ArrayList<>();
|
||||
|
||||
private final ConsoleReader console;
|
||||
|
||||
public CommandCompleter(ConsoleReader consoleReader, ArgumentDelimiter argumentDelimiter,
|
||||
Iterable<Command> commands) {
|
||||
this.console = consoleReader;
|
||||
List<String> names = new ArrayList<>();
|
||||
for (Command command : commands) {
|
||||
this.commands.add(command);
|
||||
names.add(command.getName());
|
||||
List<String> options = new ArrayList<>();
|
||||
for (OptionHelp optionHelp : command.getOptionsHelp()) {
|
||||
options.addAll(optionHelp.getOptions());
|
||||
}
|
||||
AggregateCompleter argumentCompleters = new AggregateCompleter(new StringsCompleter(options),
|
||||
new FileNameCompleter());
|
||||
ArgumentCompleter argumentCompleter = new ArgumentCompleter(argumentDelimiter, argumentCompleters);
|
||||
argumentCompleter.setStrict(false);
|
||||
this.commandCompleters.put(command.getName(), argumentCompleter);
|
||||
}
|
||||
getStrings().addAll(names);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int complete(String buffer, int cursor, List<CharSequence> candidates) {
|
||||
int completionIndex = super.complete(buffer, cursor, candidates);
|
||||
int spaceIndex = buffer.indexOf(' ');
|
||||
String commandName = ((spaceIndex != -1) ? buffer.substring(0, spaceIndex) : "");
|
||||
if (StringUtils.hasText(commandName)) {
|
||||
for (Command command : this.commands) {
|
||||
if (command.getName().equals(commandName)) {
|
||||
if (cursor == buffer.length() && buffer.endsWith(" ")) {
|
||||
printUsage(command);
|
||||
break;
|
||||
}
|
||||
Completer completer = this.commandCompleters.get(command.getName());
|
||||
if (completer != null) {
|
||||
completionIndex = completer.complete(buffer, cursor, candidates);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return completionIndex;
|
||||
}
|
||||
|
||||
private void printUsage(Command command) {
|
||||
try {
|
||||
int maxOptionsLength = 0;
|
||||
List<OptionHelpLine> optionHelpLines = new ArrayList<>();
|
||||
for (OptionHelp optionHelp : command.getOptionsHelp()) {
|
||||
OptionHelpLine optionHelpLine = new OptionHelpLine(optionHelp);
|
||||
optionHelpLines.add(optionHelpLine);
|
||||
maxOptionsLength = Math.max(maxOptionsLength, optionHelpLine.getOptions().length());
|
||||
}
|
||||
|
||||
this.console.println();
|
||||
this.console.println("Usage:");
|
||||
this.console.println(command.getName() + " " + command.getUsageHelp());
|
||||
for (OptionHelpLine optionHelpLine : optionHelpLines) {
|
||||
this.console.println(String.format("\t%" + maxOptionsLength + "s: %s", optionHelpLine.getOptions(),
|
||||
optionHelpLine.getUsage()));
|
||||
}
|
||||
this.console.drawLine();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
Log.error(ex.getMessage() + " (" + ex.getClass().getName() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulated options and usage help.
|
||||
*/
|
||||
private static class OptionHelpLine {
|
||||
|
||||
private final String options;
|
||||
|
||||
private final String usage;
|
||||
|
||||
OptionHelpLine(OptionHelp optionHelp) {
|
||||
this.options = String.join(", ", optionHelp.getOptions());
|
||||
this.usage = optionHelp.getUsageHelp();
|
||||
}
|
||||
|
||||
String getOptions() {
|
||||
return this.options;
|
||||
}
|
||||
|
||||
String getUsage() {
|
||||
return this.usage;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2012-2020 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
|
||||
*
|
||||
* https://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.shell;
|
||||
|
||||
import jline.console.completer.ArgumentCompleter.ArgumentList;
|
||||
import jline.console.completer.ArgumentCompleter.WhitespaceArgumentDelimiter;
|
||||
|
||||
/**
|
||||
* Escape aware variant of {@link WhitespaceArgumentDelimiter}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class EscapeAwareWhiteSpaceArgumentDelimiter extends WhitespaceArgumentDelimiter {
|
||||
|
||||
@Override
|
||||
public boolean isEscaped(CharSequence buffer, int pos) {
|
||||
return (isEscapeChar(buffer, pos - 1));
|
||||
}
|
||||
|
||||
private boolean isEscapeChar(CharSequence buffer, int pos) {
|
||||
if (pos >= 0) {
|
||||
for (char c : getEscapeChars()) {
|
||||
if (buffer.charAt(pos) == c) {
|
||||
return !isEscapeChar(buffer, pos - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isQuoted(CharSequence buffer, int pos) {
|
||||
int closingQuote = searchBackwards(buffer, pos - 1, getQuoteChars());
|
||||
if (closingQuote == -1) {
|
||||
return false;
|
||||
}
|
||||
int openingQuote = searchBackwards(buffer, closingQuote - 1, buffer.charAt(closingQuote));
|
||||
if (openingQuote == -1) {
|
||||
return true;
|
||||
}
|
||||
return isQuoted(buffer, openingQuote - 1);
|
||||
}
|
||||
|
||||
private int searchBackwards(CharSequence buffer, int pos, char... chars) {
|
||||
while (pos >= 0) {
|
||||
for (char c : chars) {
|
||||
if (buffer.charAt(pos) == c && !isEscaped(buffer, pos)) {
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
pos--;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
String[] parseArguments(String line) {
|
||||
ArgumentList delimit = delimit(line, 0);
|
||||
return cleanArguments(delimit.getArguments());
|
||||
}
|
||||
|
||||
private String[] cleanArguments(String[] arguments) {
|
||||
String[] cleanArguments = new String[arguments.length];
|
||||
for (int i = 0; i < arguments.length; i++) {
|
||||
cleanArguments[i] = cleanArgument(arguments[i]);
|
||||
}
|
||||
return cleanArguments;
|
||||
}
|
||||
|
||||
private String cleanArgument(String argument) {
|
||||
for (char c : getQuoteChars()) {
|
||||
String quote = String.valueOf(c);
|
||||
if (argument.startsWith(quote) && argument.endsWith(quote)) {
|
||||
return replaceEscapes(argument.substring(1, argument.length() - 1));
|
||||
}
|
||||
}
|
||||
return replaceEscapes(argument);
|
||||
}
|
||||
|
||||
private String replaceEscapes(String string) {
|
||||
string = string.replace("\\ ", " ");
|
||||
string = string.replace("\\\\", "\\");
|
||||
string = string.replace("\\t", "\t");
|
||||
string = string.replace("\\\"", "\"");
|
||||
string = string.replace("\\'", "'");
|
||||
return string;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.shell;
|
||||
|
||||
import org.springframework.boot.cli.command.AbstractCommand;
|
||||
import org.springframework.boot.cli.command.Command;
|
||||
import org.springframework.boot.cli.command.status.ExitStatus;
|
||||
|
||||
/**
|
||||
* {@link Command} to quit the {@link Shell}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ExitCommand extends AbstractCommand {
|
||||
|
||||
ExitCommand() {
|
||||
super("exit", "Quit the embedded shell");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExitStatus run(String... args) throws Exception {
|
||||
throw new ShellExitException();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.shell;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.cli.command.Command;
|
||||
import org.springframework.boot.cli.command.options.OptionHelp;
|
||||
import org.springframework.boot.cli.command.status.ExitStatus;
|
||||
import org.springframework.boot.loader.tools.JavaExecutable;
|
||||
|
||||
/**
|
||||
* Decorate an existing command to run it by forking the current java process.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ForkProcessCommand extends RunProcessCommand {
|
||||
|
||||
private static final String MAIN_CLASS = "org.springframework.boot.loader.JarLauncher";
|
||||
|
||||
private final Command command;
|
||||
|
||||
ForkProcessCommand(Command command) {
|
||||
super(new JavaExecutable().toString());
|
||||
this.command = command;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.command.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return this.command.getDescription();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageHelp() {
|
||||
return this.command.getUsageHelp();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelp() {
|
||||
return this.command.getHelp();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<OptionHelp> getOptionsHelp() {
|
||||
return this.command.getOptionsHelp();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExitStatus run(String... args) throws Exception {
|
||||
List<String> fullArgs = new ArrayList<>();
|
||||
fullArgs.add("-cp");
|
||||
fullArgs.add(System.getProperty("java.class.path"));
|
||||
fullArgs.add(MAIN_CLASS);
|
||||
fullArgs.add(this.command.getName());
|
||||
fullArgs.addAll(Arrays.asList(args));
|
||||
run(fullArgs);
|
||||
return ExitStatus.OK;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.shell;
|
||||
|
||||
import org.springframework.boot.cli.command.AbstractCommand;
|
||||
import org.springframework.boot.cli.command.Command;
|
||||
import org.springframework.boot.cli.command.status.ExitStatus;
|
||||
|
||||
/**
|
||||
* {@link Command} to change the {@link Shell} prompt.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class PromptCommand extends AbstractCommand {
|
||||
|
||||
private final ShellPrompts prompts;
|
||||
|
||||
public PromptCommand(ShellPrompts shellPrompts) {
|
||||
super("prompt", "Change the prompt used with the current 'shell' command. "
|
||||
+ "Execute with no arguments to return to the previous value.");
|
||||
this.prompts = shellPrompts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExitStatus run(String... strings) throws Exception {
|
||||
if (strings.length > 0) {
|
||||
for (String string : strings) {
|
||||
this.prompts.pushPrompt(string + " ");
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.prompts.popPrompt();
|
||||
}
|
||||
return ExitStatus.OK;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.shell;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.boot.cli.command.AbstractCommand;
|
||||
import org.springframework.boot.cli.command.Command;
|
||||
import org.springframework.boot.cli.command.status.ExitStatus;
|
||||
import org.springframework.boot.loader.tools.RunProcess;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Special {@link Command} used to run a process from the shell. NOTE: this command is not
|
||||
* directly installed into the shell.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class RunProcessCommand extends AbstractCommand {
|
||||
|
||||
private final String[] command;
|
||||
|
||||
private volatile RunProcess process;
|
||||
|
||||
RunProcessCommand(String... command) {
|
||||
super(null, null);
|
||||
this.command = command;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExitStatus run(String... args) throws Exception {
|
||||
return run(Arrays.asList(args));
|
||||
}
|
||||
|
||||
protected ExitStatus run(Collection<String> args) throws IOException {
|
||||
this.process = new RunProcess(this.command);
|
||||
int code = this.process.run(true, StringUtils.toStringArray(args));
|
||||
if (code == 0) {
|
||||
return ExitStatus.OK;
|
||||
}
|
||||
else {
|
||||
return new ExitStatus(code, "EXTERNAL_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
boolean handleSigInt() {
|
||||
return this.process.handleSigInt();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* Copyright 2012-2022 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
|
||||
*
|
||||
* https://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.shell;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.ServiceLoader;
|
||||
import java.util.Set;
|
||||
|
||||
import jline.console.ConsoleReader;
|
||||
import jline.console.completer.CandidateListCompletionHandler;
|
||||
import org.fusesource.jansi.AnsiRenderer.Code;
|
||||
|
||||
import org.springframework.boot.cli.command.Command;
|
||||
import org.springframework.boot.cli.command.CommandFactory;
|
||||
import org.springframework.boot.cli.command.CommandRunner;
|
||||
import org.springframework.boot.cli.command.core.HelpCommand;
|
||||
import org.springframework.boot.cli.command.core.VersionCommand;
|
||||
import org.springframework.boot.loader.tools.SignalUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A shell for Spring Boot. Drops the user into an event loop (REPL) where command line
|
||||
* completion and history are available without relying on OS shell features.
|
||||
*
|
||||
* @author Jon Brisbin
|
||||
* @author Dave Syer
|
||||
* @author Phillip Webb
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class Shell {
|
||||
|
||||
private static final Set<Class<?>> NON_FORKED_COMMANDS;
|
||||
|
||||
static {
|
||||
Set<Class<?>> nonForked = new HashSet<>();
|
||||
nonForked.add(VersionCommand.class);
|
||||
NON_FORKED_COMMANDS = Collections.unmodifiableSet(nonForked);
|
||||
}
|
||||
|
||||
private final ShellCommandRunner commandRunner;
|
||||
|
||||
private final ConsoleReader consoleReader;
|
||||
|
||||
private final EscapeAwareWhiteSpaceArgumentDelimiter argumentDelimiter = new EscapeAwareWhiteSpaceArgumentDelimiter();
|
||||
|
||||
private final ShellPrompts prompts = new ShellPrompts();
|
||||
|
||||
/**
|
||||
* Create a new {@link Shell} instance.
|
||||
* @throws IOException in case of I/O errors
|
||||
*/
|
||||
Shell() throws IOException {
|
||||
attachSignalHandler();
|
||||
this.consoleReader = new ConsoleReader();
|
||||
this.commandRunner = createCommandRunner();
|
||||
initializeConsoleReader();
|
||||
}
|
||||
|
||||
private ShellCommandRunner createCommandRunner() {
|
||||
ShellCommandRunner runner = new ShellCommandRunner();
|
||||
runner.addCommand(new HelpCommand(runner));
|
||||
runner.addCommands(getCommands());
|
||||
runner.addAliases("exit", "quit");
|
||||
runner.addAliases("help", "?");
|
||||
runner.addAliases("clear", "cls");
|
||||
return runner;
|
||||
}
|
||||
|
||||
private Iterable<Command> getCommands() {
|
||||
List<Command> commands = new ArrayList<>();
|
||||
ServiceLoader<CommandFactory> factories = ServiceLoader.load(CommandFactory.class, getClass().getClassLoader());
|
||||
for (CommandFactory factory : factories) {
|
||||
for (Command command : factory.getCommands()) {
|
||||
commands.add(convertToForkCommand(command));
|
||||
}
|
||||
}
|
||||
commands.add(new PromptCommand(this.prompts));
|
||||
commands.add(new ClearCommand(this.consoleReader));
|
||||
commands.add(new ExitCommand());
|
||||
return commands;
|
||||
}
|
||||
|
||||
private Command convertToForkCommand(Command command) {
|
||||
for (Class<?> nonForked : NON_FORKED_COMMANDS) {
|
||||
if (nonForked.isInstance(command)) {
|
||||
return command;
|
||||
}
|
||||
}
|
||||
return new ForkProcessCommand(command);
|
||||
}
|
||||
|
||||
private void initializeConsoleReader() {
|
||||
this.consoleReader.setHistoryEnabled(true);
|
||||
this.consoleReader.setBellEnabled(false);
|
||||
this.consoleReader.setExpandEvents(false);
|
||||
this.consoleReader
|
||||
.addCompleter(new CommandCompleter(this.consoleReader, this.argumentDelimiter, this.commandRunner));
|
||||
this.consoleReader.setCompletionHandler(new CandidateListCompletionHandler());
|
||||
}
|
||||
|
||||
private void attachSignalHandler() {
|
||||
SignalUtils.attachSignalHandler(this::handleSigInt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the shell until the user exists.
|
||||
* @throws Exception on error
|
||||
*/
|
||||
public void run() throws Exception {
|
||||
printBanner();
|
||||
try {
|
||||
runInputLoop();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
if (!(ex instanceof ShellExitException)) {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void printBanner() {
|
||||
String version = getClass().getPackage().getImplementationVersion();
|
||||
version = (version != null) ? " (v" + version + ")" : "";
|
||||
System.out.println(ansi("Spring Boot", Code.BOLD).append(version, Code.FAINT));
|
||||
System.out.println(ansi("Hit TAB to complete. Type 'help' and hit RETURN for help, and 'exit' to quit."));
|
||||
}
|
||||
|
||||
private void runInputLoop() throws Exception {
|
||||
String line;
|
||||
while ((line = this.consoleReader.readLine(getPrompt())) != null) {
|
||||
while (line.endsWith("\\")) {
|
||||
line = line.substring(0, line.length() - 1);
|
||||
line += this.consoleReader.readLine("> ");
|
||||
}
|
||||
if (StringUtils.hasLength(line)) {
|
||||
String[] args = this.argumentDelimiter.parseArguments(line);
|
||||
this.commandRunner.runAndHandleErrors(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getPrompt() {
|
||||
String prompt = this.prompts.getPrompt();
|
||||
return ansi(prompt, Code.FG_BLUE).toString();
|
||||
}
|
||||
|
||||
private AnsiString ansi(String text, Code... codes) {
|
||||
return new AnsiString(this.consoleReader.getTerminal()).append(text, codes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Final handle an interrupt signal (CTRL-C).
|
||||
*/
|
||||
protected void handleSigInt() {
|
||||
if (this.commandRunner.handleSigInt()) {
|
||||
return;
|
||||
}
|
||||
System.out.println(String.format("%nThanks for using Spring Boot"));
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension of {@link CommandRunner} to deal with {@link RunProcessCommand}s and
|
||||
* aliases.
|
||||
*/
|
||||
private class ShellCommandRunner extends CommandRunner {
|
||||
|
||||
private volatile Command lastCommand;
|
||||
|
||||
private final Map<String, String> aliases = new HashMap<>();
|
||||
|
||||
ShellCommandRunner() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
void addAliases(String command, String... aliases) {
|
||||
for (String alias : aliases) {
|
||||
this.aliases.put(alias, command);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Command findCommand(String name) {
|
||||
if (name.startsWith("!")) {
|
||||
return new RunProcessCommand(name.substring(1));
|
||||
}
|
||||
if (this.aliases.containsKey(name)) {
|
||||
name = this.aliases.get(name);
|
||||
}
|
||||
return super.findCommand(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void beforeRun(Command command) {
|
||||
this.lastCommand = command;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterRun(Command command) {
|
||||
}
|
||||
|
||||
boolean handleSigInt() {
|
||||
Command command = this.lastCommand;
|
||||
if (command instanceof RunProcessCommand runProcessCommand) {
|
||||
return runProcessCommand.handleSigInt();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.shell;
|
||||
|
||||
import org.springframework.boot.cli.command.AbstractCommand;
|
||||
import org.springframework.boot.cli.command.Command;
|
||||
import org.springframework.boot.cli.command.status.ExitStatus;
|
||||
|
||||
/**
|
||||
* {@link Command} to start a nested REPL shell.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.0.0
|
||||
* @see Shell
|
||||
*/
|
||||
public class ShellCommand extends AbstractCommand {
|
||||
|
||||
public ShellCommand() {
|
||||
super("shell", "Start a nested shell");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExitStatus run(String... args) throws Exception {
|
||||
new Shell().run();
|
||||
return ExitStatus.OK;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.shell;
|
||||
|
||||
import org.springframework.boot.cli.command.CommandException;
|
||||
|
||||
/**
|
||||
* Exception used to stop the {@link Shell}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class ShellExitException extends CommandException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public ShellExitException() {
|
||||
super(Option.RETHROW);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.shell;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
|
||||
/**
|
||||
* Abstraction to manage a stack of prompts.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class ShellPrompts {
|
||||
|
||||
private static final String DEFAULT_PROMPT = "$ ";
|
||||
|
||||
private final Deque<String> prompts = new ArrayDeque<>();
|
||||
|
||||
/**
|
||||
* Push a new prompt to be used by the shell.
|
||||
* @param prompt the prompt
|
||||
* @see #popPrompt()
|
||||
*/
|
||||
public void pushPrompt(String prompt) {
|
||||
this.prompts.push(prompt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pop a previously pushed prompt, returning to the previous value.
|
||||
* @see #pushPrompt(String)
|
||||
*/
|
||||
public void popPrompt() {
|
||||
if (!this.prompts.isEmpty()) {
|
||||
this.prompts.pop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current prompt.
|
||||
* @return the current prompt
|
||||
*/
|
||||
public String getPrompt() {
|
||||
return this.prompts.isEmpty() ? DEFAULT_PROMPT : this.prompts.peek();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Classes for running a nested shell in the CLI.
|
||||
*/
|
||||
package org.springframework.boot.cli.command.shell;
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.status;
|
||||
|
||||
/**
|
||||
* Encapsulation of the outcome of a command.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @since 1.0.0
|
||||
* @see ExitStatus#OK
|
||||
* @see ExitStatus#ERROR
|
||||
*/
|
||||
public final class ExitStatus {
|
||||
|
||||
/**
|
||||
* Generic "OK" exit status with zero exit code and {@literal hangup=false}.
|
||||
*/
|
||||
public static final ExitStatus OK = new ExitStatus(0, "OK");
|
||||
|
||||
/**
|
||||
* Generic "not OK" exit status with non-zero exit code and {@literal hangup=true}.
|
||||
*/
|
||||
public static final ExitStatus ERROR = new ExitStatus(-1, "ERROR", true);
|
||||
|
||||
private final int code;
|
||||
|
||||
private final String name;
|
||||
|
||||
private final boolean hangup;
|
||||
|
||||
/**
|
||||
* Create a new {@link ExitStatus} instance.
|
||||
* @param code the exit code
|
||||
* @param name the name
|
||||
*/
|
||||
public ExitStatus(int code, String name) {
|
||||
this(code, name, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ExitStatus} instance.
|
||||
* @param code the exit code
|
||||
* @param name the name
|
||||
* @param hangup true if it is OK for the caller to hangup
|
||||
*/
|
||||
public ExitStatus(int code, String name, boolean hangup) {
|
||||
this.code = code;
|
||||
this.name = name;
|
||||
this.hangup = hangup;
|
||||
}
|
||||
|
||||
/**
|
||||
* An exit code appropriate for use in {@code System.exit()}.
|
||||
* @return an exit code
|
||||
*/
|
||||
public int getCode() {
|
||||
return this.code;
|
||||
}
|
||||
|
||||
/**
|
||||
* A name describing the outcome.
|
||||
* @return a name
|
||||
*/
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flag to signal that the caller can (or should) hangup. A server process with
|
||||
* non-daemon threads should set this to false.
|
||||
* @return the flag
|
||||
*/
|
||||
public boolean isHangup() {
|
||||
return this.hangup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the existing code to a hangup.
|
||||
* @return a new ExitStatus with hangup=true
|
||||
*/
|
||||
public ExitStatus hangup() {
|
||||
return new ExitStatus(this.code, this.name, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getName() + ":" + getCode();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* CLI command status.
|
||||
*/
|
||||
package org.springframework.boot.cli.command.status;
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Main entry point of the Spring Boot CLI.
|
||||
*/
|
||||
package org.springframework.boot.cli;
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.util;
|
||||
|
||||
/**
|
||||
* Simple logger used by the CLI.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public abstract class Log {
|
||||
|
||||
private static LogListener listener;
|
||||
|
||||
public static void info(String message) {
|
||||
System.out.println(message);
|
||||
if (listener != null) {
|
||||
listener.info(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static void infoPrint(String message) {
|
||||
System.out.print(message);
|
||||
if (listener != null) {
|
||||
listener.infoPrint(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static void error(String message) {
|
||||
System.err.println(message);
|
||||
if (listener != null) {
|
||||
listener.error(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static void error(Exception ex) {
|
||||
ex.printStackTrace(System.err);
|
||||
if (listener != null) {
|
||||
listener.error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
static void setListener(LogListener listener) {
|
||||
Log.listener = listener;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.util;
|
||||
|
||||
/**
|
||||
* Listener that can be attached to the {@link Log} to capture calls.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
interface LogListener {
|
||||
|
||||
void info(String message);
|
||||
|
||||
void infoPrint(String message);
|
||||
|
||||
void error(String message);
|
||||
|
||||
void error(Exception ex);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Utility classes for the CLI.
|
||||
*/
|
||||
package org.springframework.boot.cli.util;
|
||||
@@ -0,0 +1 @@
|
||||
org.springframework.boot.cli.DefaultCommandFactory
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"homepage": "https://projects.spring.io/spring-boot/",
|
||||
"version": "${scoopVersion}",
|
||||
"license": "Apache 2.0",
|
||||
"hash": "${hash}",
|
||||
"url": "https://repo.spring.io/${repo}/org/springframework/boot/spring-boot-cli/${project.version}/spring-boot-cli-${project.version}-bin.zip",
|
||||
"extract_dir": "spring-${project.version}",
|
||||
"bin": "bin\\\\spring.bat",
|
||||
"suggest": {
|
||||
"JDK": [
|
||||
"java/oraclejdk",
|
||||
"java/openjdk"
|
||||
]
|
||||
},
|
||||
"checkver": {
|
||||
"github": "https://github.com/spring-projects/spring-boot",
|
||||
"re": "/releases/tag/(?:v)?(2[\\d.]+)\\\\.RELEASE"
|
||||
},
|
||||
"autoupdate": {
|
||||
"url": "https://repo.spring.io/release/org/springframework/boot/spring-boot-cli/\$version.RELEASE/spring-boot-cli-\$version.RELEASE-bin.zip",
|
||||
"extract_dir": "spring-\$version.RELEASE",
|
||||
"hash": {
|
||||
"url": "\$url.sha256"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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 cli.command;
|
||||
|
||||
import org.springframework.boot.cli.command.AbstractCommand;
|
||||
import org.springframework.boot.cli.command.status.ExitStatus;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*/
|
||||
public class CustomCommand extends AbstractCommand {
|
||||
|
||||
public CustomCommand() {
|
||||
super("custom", "Custom command added in tests");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExitStatus run(String... args) throws Exception {
|
||||
System.err.println("Custom Command Hello");
|
||||
return ExitStatus.OK;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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 cli.command;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.springframework.boot.cli.command.Command;
|
||||
import org.springframework.boot.cli.command.CommandFactory;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*/
|
||||
public class CustomCommandFactory implements CommandFactory {
|
||||
|
||||
@Override
|
||||
public Collection<Command> getCommands() {
|
||||
return Collections.singleton(new CustomCommand());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.cli.command.status.ExitStatus;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link CommandRunner}.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class CommandRunnerIntegrationTests {
|
||||
|
||||
@BeforeEach
|
||||
void clearDebug() {
|
||||
System.clearProperty("debug");
|
||||
}
|
||||
|
||||
@Test
|
||||
void debugEnabledAndArgumentRemovedWhenNotAnApplicationArgument() {
|
||||
CommandRunner runner = new CommandRunner("spring");
|
||||
ArgHandlingCommand command = new ArgHandlingCommand();
|
||||
runner.addCommand(command);
|
||||
runner.runAndHandleErrors("args", "samples/app.groovy", "--debug");
|
||||
assertThat(command.args).containsExactly("samples/app.groovy");
|
||||
assertThat(System.getProperty("debug")).isEqualTo("true");
|
||||
}
|
||||
|
||||
@Test
|
||||
void debugNotEnabledAndArgumentRetainedWhenAnApplicationArgument() {
|
||||
CommandRunner runner = new CommandRunner("spring");
|
||||
ArgHandlingCommand command = new ArgHandlingCommand();
|
||||
runner.addCommand(command);
|
||||
runner.runAndHandleErrors("args", "samples/app.groovy", "--", "--debug");
|
||||
assertThat(command.args).containsExactly("samples/app.groovy", "--", "--debug");
|
||||
assertThat(System.getProperty("debug")).isNull();
|
||||
}
|
||||
|
||||
static class ArgHandlingCommand extends AbstractCommand {
|
||||
|
||||
private String[] args;
|
||||
|
||||
ArgHandlingCommand() {
|
||||
super("args", "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExitStatus run(String... args) throws Exception {
|
||||
this.args = args;
|
||||
return ExitStatus.OK;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* Copyright 2012-2022 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
|
||||
*
|
||||
* https://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;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.boot.cli.command.core.HelpCommand;
|
||||
import org.springframework.boot.cli.command.core.HintCommand;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.BDDMockito.willThrow;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
|
||||
/**
|
||||
* Tests for {@link CommandRunner}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Dave Syer
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CommandRunnerTests {
|
||||
|
||||
private CommandRunner commandRunner;
|
||||
|
||||
@Mock
|
||||
private Command regularCommand;
|
||||
|
||||
@Mock
|
||||
private Command anotherCommand;
|
||||
|
||||
private final Set<Call> calls = EnumSet.noneOf(Call.class);
|
||||
|
||||
private ClassLoader loader;
|
||||
|
||||
@AfterEach
|
||||
void close() {
|
||||
Thread.currentThread().setContextClassLoader(this.loader);
|
||||
System.clearProperty("debug");
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.loader = Thread.currentThread().getContextClassLoader();
|
||||
this.commandRunner = new CommandRunner("spring") {
|
||||
|
||||
@Override
|
||||
protected void showUsage() {
|
||||
CommandRunnerTests.this.calls.add(Call.SHOW_USAGE);
|
||||
super.showUsage();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean errorMessage(String message) {
|
||||
CommandRunnerTests.this.calls.add(Call.ERROR_MESSAGE);
|
||||
return super.errorMessage(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void printStackTrace(Exception ex) {
|
||||
CommandRunnerTests.this.calls.add(Call.PRINT_STACK_TRACE);
|
||||
super.printStackTrace(ex);
|
||||
}
|
||||
};
|
||||
lenient().doReturn("another").when(this.anotherCommand).getName();
|
||||
lenient().doReturn("command").when(this.regularCommand).getName();
|
||||
lenient().doReturn("A regular command").when(this.regularCommand).getDescription();
|
||||
this.commandRunner.addCommand(this.regularCommand);
|
||||
this.commandRunner.addCommand(new HelpCommand(this.commandRunner));
|
||||
this.commandRunner.addCommand(new HintCommand(this.commandRunner));
|
||||
}
|
||||
|
||||
@Test
|
||||
void runWithoutArguments() {
|
||||
assertThatExceptionOfType(NoArgumentsException.class).isThrownBy(this.commandRunner::run);
|
||||
}
|
||||
|
||||
@Test
|
||||
void runCommand() throws Exception {
|
||||
this.commandRunner.run("command", "--arg1", "arg2");
|
||||
then(this.regularCommand).should().run("--arg1", "arg2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingCommand() {
|
||||
assertThatExceptionOfType(NoSuchCommandException.class).isThrownBy(() -> this.commandRunner.run("missing"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void appArguments() throws Exception {
|
||||
this.commandRunner.runAndHandleErrors("command", "--", "--debug", "bar");
|
||||
then(this.regularCommand).should().run("--", "--debug", "bar");
|
||||
// When handled by the command itself it shouldn't cause the system property to be
|
||||
// set
|
||||
assertThat(System.getProperty("debug")).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlesSuccess() {
|
||||
int status = this.commandRunner.runAndHandleErrors("command");
|
||||
assertThat(status).isEqualTo(0);
|
||||
assertThat(this.calls).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlesNoSuchCommand() {
|
||||
int status = this.commandRunner.runAndHandleErrors("missing");
|
||||
assertThat(status).isEqualTo(1);
|
||||
assertThat(this.calls).containsOnly(Call.ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlesRegularExceptionWithMessage() throws Exception {
|
||||
willThrow(new RuntimeException("With Message")).given(this.regularCommand).run();
|
||||
int status = this.commandRunner.runAndHandleErrors("command");
|
||||
assertThat(status).isEqualTo(1);
|
||||
assertThat(this.calls).containsOnly(Call.ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlesRegularExceptionWithoutMessage() throws Exception {
|
||||
willThrow(new RuntimeException()).given(this.regularCommand).run();
|
||||
int status = this.commandRunner.runAndHandleErrors("command");
|
||||
assertThat(status).isEqualTo(1);
|
||||
assertThat(this.calls).containsOnly(Call.ERROR_MESSAGE, Call.PRINT_STACK_TRACE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlesExceptionWithDashDashDebug() throws Exception {
|
||||
willThrow(new RuntimeException()).given(this.regularCommand).run();
|
||||
int status = this.commandRunner.runAndHandleErrors("command", "--debug");
|
||||
assertThat(System.getProperty("debug")).isEqualTo("true");
|
||||
assertThat(status).isEqualTo(1);
|
||||
assertThat(this.calls).containsOnly(Call.ERROR_MESSAGE, Call.PRINT_STACK_TRACE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void exceptionMessages() {
|
||||
assertThat(new NoSuchCommandException("name").getMessage())
|
||||
.isEqualTo("'name' is not a valid command. See 'help'.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void help() throws Exception {
|
||||
this.commandRunner.run("help", "command");
|
||||
then(this.regularCommand).should().getHelp();
|
||||
}
|
||||
|
||||
@Test
|
||||
void helpNoCommand() {
|
||||
assertThatExceptionOfType(NoHelpCommandArgumentsException.class)
|
||||
.isThrownBy(() -> this.commandRunner.run("help"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void helpUnknownCommand() {
|
||||
assertThatExceptionOfType(NoSuchCommandException.class)
|
||||
.isThrownBy(() -> this.commandRunner.run("help", "missing"));
|
||||
}
|
||||
|
||||
private enum Call {
|
||||
|
||||
SHOW_USAGE, ERROR_MESSAGE, PRINT_STACK_TRACE
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.cli.command.options.OptionHandler;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link OptionParsingCommand}.
|
||||
*
|
||||
* @author Dave Syer
|
||||
*/
|
||||
class OptionParsingCommandTests {
|
||||
|
||||
@Test
|
||||
void optionHelp() {
|
||||
OptionHandler handler = new OptionHandler();
|
||||
handler.option("bar", "Bar");
|
||||
OptionParsingCommand command = new TestOptionParsingCommand("foo", "Foo", handler);
|
||||
assertThat(command.getHelp()).contains("--bar");
|
||||
}
|
||||
|
||||
static class TestOptionParsingCommand extends OptionParsingCommand {
|
||||
|
||||
TestOptionParsingCommand(String name, String description, OptionHandler handler) {
|
||||
super(name, description, handler);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2012-2022 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
|
||||
*
|
||||
* https://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.encodepassword;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.boot.cli.command.status.ExitStatus;
|
||||
import org.springframework.boot.cli.util.MockLog;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
||||
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
|
||||
/**
|
||||
* Tests for {@link EncodePasswordCommand}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class EncodePasswordCommandTests {
|
||||
|
||||
private MockLog log;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<String> message;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.log = MockLog.attach();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
MockLog.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
void encodeWithNoAlgorithmShouldUseBcrypt() throws Exception {
|
||||
EncodePasswordCommand command = new EncodePasswordCommand();
|
||||
ExitStatus status = command.run("boot");
|
||||
then(this.log).should().info(this.message.capture());
|
||||
assertThat(this.message.getValue()).startsWith("{bcrypt}");
|
||||
assertThat(PasswordEncoderFactories.createDelegatingPasswordEncoder().matches("boot", this.message.getValue()))
|
||||
.isTrue();
|
||||
assertThat(status).isEqualTo(ExitStatus.OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
void encodeWithBCryptShouldUseBCrypt() throws Exception {
|
||||
EncodePasswordCommand command = new EncodePasswordCommand();
|
||||
ExitStatus status = command.run("-a", "bcrypt", "boot");
|
||||
then(this.log).should().info(this.message.capture());
|
||||
assertThat(this.message.getValue()).doesNotStartWith("{");
|
||||
assertThat(new BCryptPasswordEncoder().matches("boot", this.message.getValue())).isTrue();
|
||||
assertThat(status).isEqualTo(ExitStatus.OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
void encodeWithPbkdf2ShouldUsePbkdf2() throws Exception {
|
||||
EncodePasswordCommand command = new EncodePasswordCommand();
|
||||
ExitStatus status = command.run("-a", "pbkdf2", "boot");
|
||||
then(this.log).should().info(this.message.capture());
|
||||
assertThat(this.message.getValue()).doesNotStartWith("{");
|
||||
assertThat(new Pbkdf2PasswordEncoder().matches("boot", this.message.getValue())).isTrue();
|
||||
assertThat(status).isEqualTo(ExitStatus.OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
void encodeWithUnknownAlgorithmShouldExitWithError() throws Exception {
|
||||
EncodePasswordCommand command = new EncodePasswordCommand();
|
||||
ExitStatus status = command.run("--algorithm", "bad", "boot");
|
||||
then(this.log).should().error("Unknown algorithm, valid options are: default,bcrypt,pbkdf2");
|
||||
assertThat(status).isEqualTo(ExitStatus.ERROR);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.json.JSONException;
|
||||
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.ArgumentMatchers.argThat;
|
||||
import static org.mockito.ArgumentMatchers.isA;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Abstract base class for tests that use a mock {@link CloseableHttpClient}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
public abstract class AbstractHttpClientMockTests {
|
||||
|
||||
protected final CloseableHttpClient http = mock(CloseableHttpClient.class);
|
||||
|
||||
protected void mockSuccessfulMetadataTextGet() throws IOException {
|
||||
mockSuccessfulMetadataGet("metadata/service-metadata-2.1.0.txt", "text/plain", true);
|
||||
}
|
||||
|
||||
protected void mockSuccessfulMetadataGet(boolean serviceCapabilities) throws IOException {
|
||||
mockSuccessfulMetadataGet("metadata/service-metadata-2.1.0.json", "application/vnd.initializr.v2.1+json",
|
||||
serviceCapabilities);
|
||||
}
|
||||
|
||||
protected void mockSuccessfulMetadataGetV2(boolean serviceCapabilities) throws IOException {
|
||||
mockSuccessfulMetadataGet("metadata/service-metadata-2.0.0.json", "application/vnd.initializr.v2+json",
|
||||
serviceCapabilities);
|
||||
}
|
||||
|
||||
protected void mockSuccessfulMetadataGet(String contentPath, String contentType, boolean serviceCapabilities)
|
||||
throws IOException {
|
||||
CloseableHttpResponse response = mock(CloseableHttpResponse.class);
|
||||
byte[] content = readClasspathResource(contentPath);
|
||||
mockHttpEntity(response, content, contentType);
|
||||
mockStatus(response, 200);
|
||||
given(this.http.execute(argThat(getForMetadata(serviceCapabilities)))).willReturn(response);
|
||||
}
|
||||
|
||||
protected byte[] readClasspathResource(String contentPath) throws IOException {
|
||||
Resource resource = new ClassPathResource(contentPath);
|
||||
return StreamUtils.copyToByteArray(resource.getInputStream());
|
||||
}
|
||||
|
||||
protected void mockSuccessfulProjectGeneration(MockHttpProjectGenerationRequest request) throws IOException {
|
||||
// Required for project generation as the metadata is read first
|
||||
mockSuccessfulMetadataGet(false);
|
||||
CloseableHttpResponse response = mock(CloseableHttpResponse.class);
|
||||
mockHttpEntity(response, request.content, request.contentType);
|
||||
mockStatus(response, 200);
|
||||
String header = (request.fileName != null) ? contentDispositionValue(request.fileName) : null;
|
||||
mockHttpHeader(response, "Content-Disposition", header);
|
||||
given(this.http.execute(argThat(getForNonMetadata()))).willReturn(response);
|
||||
}
|
||||
|
||||
protected void mockProjectGenerationError(int status, String message) throws IOException, JSONException {
|
||||
// Required for project generation as the metadata is read first
|
||||
mockSuccessfulMetadataGet(false);
|
||||
CloseableHttpResponse response = mock(CloseableHttpResponse.class);
|
||||
mockHttpEntity(response, createJsonError(status, message).getBytes(), "application/json");
|
||||
mockStatus(response, status);
|
||||
given(this.http.execute(isA(HttpGet.class))).willReturn(response);
|
||||
}
|
||||
|
||||
protected void mockMetadataGetError(int status, String message) throws IOException, JSONException {
|
||||
CloseableHttpResponse response = mock(CloseableHttpResponse.class);
|
||||
mockHttpEntity(response, createJsonError(status, message).getBytes(), "application/json");
|
||||
mockStatus(response, status);
|
||||
given(this.http.execute(isA(HttpGet.class))).willReturn(response);
|
||||
}
|
||||
|
||||
protected HttpEntity mockHttpEntity(CloseableHttpResponse response, byte[] content, String contentType) {
|
||||
try {
|
||||
HttpEntity entity = mock(HttpEntity.class);
|
||||
given(entity.getContent()).willReturn(new ByteArrayInputStream(content));
|
||||
Header contentTypeHeader = (contentType != null) ? new BasicHeader("Content-Type", contentType) : null;
|
||||
given(entity.getContentType()).willReturn(contentTypeHeader);
|
||||
given(response.getEntity()).willReturn(entity);
|
||||
return entity;
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException("Should not happen", ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected void mockStatus(CloseableHttpResponse response, int status) {
|
||||
StatusLine statusLine = mock(StatusLine.class);
|
||||
given(statusLine.getStatusCode()).willReturn(status);
|
||||
given(response.getStatusLine()).willReturn(statusLine);
|
||||
}
|
||||
|
||||
protected void mockHttpHeader(CloseableHttpResponse response, String headerName, String value) {
|
||||
Header header = (value != null) ? new BasicHeader(headerName, value) : null;
|
||||
given(response.getFirstHeader(headerName)).willReturn(header);
|
||||
}
|
||||
|
||||
private ArgumentMatcher<HttpGet> getForMetadata(boolean serviceCapabilities) {
|
||||
if (!serviceCapabilities) {
|
||||
return new HasAcceptHeader(InitializrService.ACCEPT_META_DATA, true);
|
||||
}
|
||||
return new HasAcceptHeader(InitializrService.ACCEPT_SERVICE_CAPABILITIES, true);
|
||||
}
|
||||
|
||||
private ArgumentMatcher<HttpGet> getForNonMetadata() {
|
||||
return new HasAcceptHeader(InitializrService.ACCEPT_META_DATA, false);
|
||||
}
|
||||
|
||||
private String contentDispositionValue(String fileName) {
|
||||
return "attachment; filename=\"" + fileName + "\"";
|
||||
}
|
||||
|
||||
private String createJsonError(int status, String message) throws JSONException {
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("status", status);
|
||||
if (message != null) {
|
||||
json.put("message", message);
|
||||
}
|
||||
return json.toString();
|
||||
}
|
||||
|
||||
static class MockHttpProjectGenerationRequest {
|
||||
|
||||
String contentType;
|
||||
|
||||
String fileName;
|
||||
|
||||
byte[] content = new byte[] { 0, 0, 0, 0 };
|
||||
|
||||
MockHttpProjectGenerationRequest(String contentType, String fileName) {
|
||||
this(contentType, fileName, new byte[] { 0, 0, 0, 0 });
|
||||
}
|
||||
|
||||
MockHttpProjectGenerationRequest(String contentType, String fileName, byte[] content) {
|
||||
this.contentType = contentType;
|
||||
this.fileName = fileName;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class HasAcceptHeader implements ArgumentMatcher<HttpGet> {
|
||||
|
||||
private final String value;
|
||||
|
||||
private final boolean shouldMatch;
|
||||
|
||||
HasAcceptHeader(String value, boolean shouldMatch) {
|
||||
this.value = value;
|
||||
this.shouldMatch = shouldMatch;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(HttpGet get) {
|
||||
if (get == null) {
|
||||
return false;
|
||||
}
|
||||
Header acceptHeader = get.getFirstHeader(HttpHeaders.ACCEPT);
|
||||
if (this.shouldMatch) {
|
||||
return acceptHeader != null && this.value.equals(acceptHeader.getValue());
|
||||
}
|
||||
return acceptHeader == null || !this.value.equals(acceptHeader.getValue());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
/*
|
||||
* Copyright 2012-2022 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
|
||||
*
|
||||
* https://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.Header;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.boot.cli.command.status.ExitStatus;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
|
||||
/**
|
||||
* Tests for {@link InitCommand}
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Eddú Meléndez
|
||||
* @author Vignesh Thangavel Ilangovan
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class InitCommandTests extends AbstractHttpClientMockTests {
|
||||
|
||||
private final TestableInitCommandOptionHandler handler;
|
||||
|
||||
private final InitCommand command;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<HttpUriRequest> requestCaptor;
|
||||
|
||||
InitCommandTests() {
|
||||
InitializrService initializrService = new InitializrService(this.http);
|
||||
this.handler = new TestableInitCommandOptionHandler(initializrService);
|
||||
this.command = new InitCommand(this.handler);
|
||||
}
|
||||
|
||||
@Test
|
||||
void listServiceCapabilitiesText() throws Exception {
|
||||
mockSuccessfulMetadataTextGet();
|
||||
this.command.run("--list", "--target=https://fake-service");
|
||||
}
|
||||
|
||||
@Test
|
||||
void listServiceCapabilities() throws Exception {
|
||||
mockSuccessfulMetadataGet(true);
|
||||
this.command.run("--list", "--target=https://fake-service");
|
||||
}
|
||||
|
||||
@Test
|
||||
void listServiceCapabilitiesV2() throws Exception {
|
||||
mockSuccessfulMetadataGetV2(true);
|
||||
this.command.run("--list", "--target=https://fake-service");
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateProject() throws Exception {
|
||||
String fileName = UUID.randomUUID().toString() + ".zip";
|
||||
File file = new File(fileName);
|
||||
assertThat(file.exists()).as("file should not exist").isFalse();
|
||||
MockHttpProjectGenerationRequest request = new MockHttpProjectGenerationRequest("application/zip", fileName);
|
||||
mockSuccessfulProjectGeneration(request);
|
||||
try {
|
||||
assertThat(this.command.run()).isEqualTo(ExitStatus.OK);
|
||||
assertThat(file.exists()).as("file should have been created").isTrue();
|
||||
}
|
||||
finally {
|
||||
assertThat(file.delete()).as("failed to delete test file").isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateProjectNoFileNameAvailable() throws Exception {
|
||||
MockHttpProjectGenerationRequest request = new MockHttpProjectGenerationRequest("application/zip", null);
|
||||
mockSuccessfulProjectGeneration(request);
|
||||
assertThat(this.command.run()).isEqualTo(ExitStatus.ERROR);
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateProjectAndExtract(@TempDir File tempDir) throws Exception {
|
||||
byte[] archive = createFakeZipArchive("test.txt", "Fake content");
|
||||
MockHttpProjectGenerationRequest request = new MockHttpProjectGenerationRequest("application/zip", "demo.zip",
|
||||
archive);
|
||||
mockSuccessfulProjectGeneration(request);
|
||||
assertThat(this.command.run("--extract", tempDir.getAbsolutePath())).isEqualTo(ExitStatus.OK);
|
||||
File archiveFile = new File(tempDir, "test.txt");
|
||||
assertThat(archiveFile).exists();
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateProjectAndExtractWillNotWriteEntriesOutsideOutputLocation(@TempDir File tempDir) throws Exception {
|
||||
byte[] archive = createFakeZipArchive("../outside.txt", "Fake content");
|
||||
MockHttpProjectGenerationRequest request = new MockHttpProjectGenerationRequest("application/zip", "demo.zip",
|
||||
archive);
|
||||
mockSuccessfulProjectGeneration(request);
|
||||
assertThat(this.command.run("--extract", tempDir.getAbsolutePath())).isEqualTo(ExitStatus.ERROR);
|
||||
File archiveFile = new File(tempDir.getParentFile(), "outside.txt");
|
||||
assertThat(archiveFile).doesNotExist();
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateProjectAndExtractWithConvention(@TempDir File tempDir) throws Exception {
|
||||
byte[] archive = createFakeZipArchive("test.txt", "Fake content");
|
||||
MockHttpProjectGenerationRequest request = new MockHttpProjectGenerationRequest("application/zip", "demo.zip",
|
||||
archive);
|
||||
mockSuccessfulProjectGeneration(request);
|
||||
assertThat(this.command.run(tempDir.getAbsolutePath() + "/")).isEqualTo(ExitStatus.OK);
|
||||
File archiveFile = new File(tempDir, "test.txt");
|
||||
assertThat(archiveFile).exists();
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateProjectArchiveExtractedByDefault() throws Exception {
|
||||
String fileName = UUID.randomUUID().toString();
|
||||
assertThat(fileName.contains(".")).as("No dot in filename").isFalse();
|
||||
byte[] archive = createFakeZipArchive("test.txt", "Fake content");
|
||||
MockHttpProjectGenerationRequest request = new MockHttpProjectGenerationRequest("application/zip", "demo.zip",
|
||||
archive);
|
||||
mockSuccessfulProjectGeneration(request);
|
||||
File file = new File(fileName);
|
||||
File archiveFile = new File(file, "test.txt");
|
||||
try {
|
||||
assertThat(this.command.run(fileName)).isEqualTo(ExitStatus.OK);
|
||||
assertThat(archiveFile).exists();
|
||||
}
|
||||
finally {
|
||||
archiveFile.delete();
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateProjectFileSavedAsFileByDefault() throws Exception {
|
||||
String fileName = UUID.randomUUID().toString();
|
||||
String content = "Fake Content";
|
||||
byte[] archive = content.getBytes();
|
||||
MockHttpProjectGenerationRequest request = new MockHttpProjectGenerationRequest("application/octet-stream",
|
||||
"pom.xml", archive);
|
||||
mockSuccessfulProjectGeneration(request);
|
||||
File file = new File(fileName);
|
||||
try {
|
||||
assertThat(this.command.run(fileName)).isEqualTo(ExitStatus.OK);
|
||||
assertThat(file.exists()).as("File not saved properly").isTrue();
|
||||
assertThat(file.isFile()).as("Should not be a directory").isTrue();
|
||||
}
|
||||
finally {
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateProjectAndExtractUnsupportedArchive(@TempDir File tempDir) throws Exception {
|
||||
String fileName = UUID.randomUUID().toString() + ".zip";
|
||||
File file = new File(fileName);
|
||||
assertThat(file.exists()).as("file should not exist").isFalse();
|
||||
try {
|
||||
byte[] archive = createFakeZipArchive("test.txt", "Fake content");
|
||||
MockHttpProjectGenerationRequest request = new MockHttpProjectGenerationRequest("application/foobar",
|
||||
fileName, archive);
|
||||
mockSuccessfulProjectGeneration(request);
|
||||
assertThat(this.command.run("--extract", tempDir.getAbsolutePath())).isEqualTo(ExitStatus.OK);
|
||||
assertThat(file.exists()).as("file should have been saved instead").isTrue();
|
||||
}
|
||||
finally {
|
||||
assertThat(file.delete()).as("failed to delete test file").isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateProjectAndExtractUnknownContentType(@TempDir File tempDir) throws Exception {
|
||||
String fileName = UUID.randomUUID().toString() + ".zip";
|
||||
File file = new File(fileName);
|
||||
assertThat(file.exists()).as("file should not exist").isFalse();
|
||||
try {
|
||||
byte[] archive = createFakeZipArchive("test.txt", "Fake content");
|
||||
MockHttpProjectGenerationRequest request = new MockHttpProjectGenerationRequest(null, fileName, archive);
|
||||
mockSuccessfulProjectGeneration(request);
|
||||
assertThat(this.command.run("--extract", tempDir.getAbsolutePath())).isEqualTo(ExitStatus.OK);
|
||||
assertThat(file.exists()).as("file should have been saved instead").isTrue();
|
||||
}
|
||||
finally {
|
||||
assertThat(file.delete()).as("failed to delete test file").isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void fileNotOverwrittenByDefault(@TempDir File tempDir) throws Exception {
|
||||
File file = new File(tempDir, "test.file");
|
||||
file.createNewFile();
|
||||
long fileLength = file.length();
|
||||
MockHttpProjectGenerationRequest request = new MockHttpProjectGenerationRequest("application/zip",
|
||||
file.getAbsolutePath());
|
||||
mockSuccessfulProjectGeneration(request);
|
||||
assertThat(this.command.run()).as("Should have failed").isEqualTo(ExitStatus.ERROR);
|
||||
assertThat(file.length()).as("File should not have changed").isEqualTo(fileLength);
|
||||
}
|
||||
|
||||
@Test
|
||||
void overwriteFile(@TempDir File tempDir) throws Exception {
|
||||
File file = new File(tempDir, "test.file");
|
||||
file.createNewFile();
|
||||
long fileLength = file.length();
|
||||
MockHttpProjectGenerationRequest request = new MockHttpProjectGenerationRequest("application/zip",
|
||||
file.getAbsolutePath());
|
||||
mockSuccessfulProjectGeneration(request);
|
||||
assertThat(this.command.run("--force")).isEqualTo(ExitStatus.OK);
|
||||
assertThat(fileLength != file.length()).as("File should have changed").isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void fileInArchiveNotOverwrittenByDefault(@TempDir File tempDir) throws Exception {
|
||||
File conflict = new File(tempDir, "test.txt");
|
||||
assertThat(conflict.createNewFile()).as("Should have been able to create file").isTrue();
|
||||
long fileLength = conflict.length();
|
||||
// also contains test.txt
|
||||
byte[] archive = createFakeZipArchive("test.txt", "Fake content");
|
||||
MockHttpProjectGenerationRequest request = new MockHttpProjectGenerationRequest("application/zip", "demo.zip",
|
||||
archive);
|
||||
mockSuccessfulProjectGeneration(request);
|
||||
assertThat(this.command.run("--extract", tempDir.getAbsolutePath())).isEqualTo(ExitStatus.ERROR);
|
||||
assertThat(conflict.length()).as("File should not have changed").isEqualTo(fileLength);
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseProjectOptions() throws Exception {
|
||||
this.handler.disableProjectGeneration();
|
||||
this.command.run("-g=org.demo", "-a=acme", "-v=1.2.3-SNAPSHOT", "-n=acme-sample",
|
||||
"--description=Acme sample project", "--package-name=demo.foo", "-t=ant-project", "--build=grunt",
|
||||
"--format=web", "-p=war", "-j=1.9", "-l=groovy", "-b=1.2.0.RELEASE", "-d=web,data-jpa");
|
||||
assertThat(this.handler.lastRequest.getGroupId()).isEqualTo("org.demo");
|
||||
assertThat(this.handler.lastRequest.getArtifactId()).isEqualTo("acme");
|
||||
assertThat(this.handler.lastRequest.getVersion()).isEqualTo("1.2.3-SNAPSHOT");
|
||||
assertThat(this.handler.lastRequest.getName()).isEqualTo("acme-sample");
|
||||
assertThat(this.handler.lastRequest.getDescription()).isEqualTo("Acme sample project");
|
||||
assertThat(this.handler.lastRequest.getPackageName()).isEqualTo("demo.foo");
|
||||
assertThat(this.handler.lastRequest.getType()).isEqualTo("ant-project");
|
||||
assertThat(this.handler.lastRequest.getBuild()).isEqualTo("grunt");
|
||||
assertThat(this.handler.lastRequest.getFormat()).isEqualTo("web");
|
||||
assertThat(this.handler.lastRequest.getPackaging()).isEqualTo("war");
|
||||
assertThat(this.handler.lastRequest.getJavaVersion()).isEqualTo("1.9");
|
||||
assertThat(this.handler.lastRequest.getLanguage()).isEqualTo("groovy");
|
||||
assertThat(this.handler.lastRequest.getBootVersion()).isEqualTo("1.2.0.RELEASE");
|
||||
List<String> dependencies = this.handler.lastRequest.getDependencies();
|
||||
assertThat(dependencies).hasSize(2);
|
||||
assertThat(dependencies.contains("web")).isTrue();
|
||||
assertThat(dependencies.contains("data-jpa")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseProjectWithCamelCaseOptions() throws Exception {
|
||||
this.handler.disableProjectGeneration();
|
||||
this.command.run("--groupId=org.demo", "--artifactId=acme", "--version=1.2.3-SNAPSHOT", "--name=acme-sample",
|
||||
"--description=Acme sample project", "--packageName=demo.foo", "--type=ant-project", "--build=grunt",
|
||||
"--format=web", "--packaging=war", "--javaVersion=1.9", "--language=groovy",
|
||||
"--bootVersion=1.2.0.RELEASE", "--dependencies=web,data-jpa");
|
||||
assertThat(this.handler.lastRequest.getGroupId()).isEqualTo("org.demo");
|
||||
assertThat(this.handler.lastRequest.getArtifactId()).isEqualTo("acme");
|
||||
assertThat(this.handler.lastRequest.getVersion()).isEqualTo("1.2.3-SNAPSHOT");
|
||||
assertThat(this.handler.lastRequest.getName()).isEqualTo("acme-sample");
|
||||
assertThat(this.handler.lastRequest.getDescription()).isEqualTo("Acme sample project");
|
||||
assertThat(this.handler.lastRequest.getPackageName()).isEqualTo("demo.foo");
|
||||
assertThat(this.handler.lastRequest.getType()).isEqualTo("ant-project");
|
||||
assertThat(this.handler.lastRequest.getBuild()).isEqualTo("grunt");
|
||||
assertThat(this.handler.lastRequest.getFormat()).isEqualTo("web");
|
||||
assertThat(this.handler.lastRequest.getPackaging()).isEqualTo("war");
|
||||
assertThat(this.handler.lastRequest.getJavaVersion()).isEqualTo("1.9");
|
||||
assertThat(this.handler.lastRequest.getLanguage()).isEqualTo("groovy");
|
||||
assertThat(this.handler.lastRequest.getBootVersion()).isEqualTo("1.2.0.RELEASE");
|
||||
List<String> dependencies = this.handler.lastRequest.getDependencies();
|
||||
assertThat(dependencies).hasSize(2);
|
||||
assertThat(dependencies.contains("web")).isTrue();
|
||||
assertThat(dependencies.contains("data-jpa")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseProjectWithKebabCaseOptions() throws Exception {
|
||||
this.handler.disableProjectGeneration();
|
||||
this.command.run("--group-id=org.demo", "--artifact-id=acme", "--version=1.2.3-SNAPSHOT", "--name=acme-sample",
|
||||
"--description=Acme sample project", "--package-name=demo.foo", "--type=ant-project", "--build=grunt",
|
||||
"--format=web", "--packaging=war", "--java-version=1.9", "--language=groovy",
|
||||
"--boot-version=1.2.0.RELEASE", "--dependencies=web,data-jpa");
|
||||
assertThat(this.handler.lastRequest.getGroupId()).isEqualTo("org.demo");
|
||||
assertThat(this.handler.lastRequest.getArtifactId()).isEqualTo("acme");
|
||||
assertThat(this.handler.lastRequest.getVersion()).isEqualTo("1.2.3-SNAPSHOT");
|
||||
assertThat(this.handler.lastRequest.getName()).isEqualTo("acme-sample");
|
||||
assertThat(this.handler.lastRequest.getDescription()).isEqualTo("Acme sample project");
|
||||
assertThat(this.handler.lastRequest.getPackageName()).isEqualTo("demo.foo");
|
||||
assertThat(this.handler.lastRequest.getType()).isEqualTo("ant-project");
|
||||
assertThat(this.handler.lastRequest.getBuild()).isEqualTo("grunt");
|
||||
assertThat(this.handler.lastRequest.getFormat()).isEqualTo("web");
|
||||
assertThat(this.handler.lastRequest.getPackaging()).isEqualTo("war");
|
||||
assertThat(this.handler.lastRequest.getJavaVersion()).isEqualTo("1.9");
|
||||
assertThat(this.handler.lastRequest.getLanguage()).isEqualTo("groovy");
|
||||
assertThat(this.handler.lastRequest.getBootVersion()).isEqualTo("1.2.0.RELEASE");
|
||||
List<String> dependencies = this.handler.lastRequest.getDependencies();
|
||||
assertThat(dependencies).hasSize(2);
|
||||
assertThat(dependencies.contains("web")).isTrue();
|
||||
assertThat(dependencies.contains("data-jpa")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void overwriteFileInArchive(@TempDir File tempDir) throws Exception {
|
||||
File conflict = new File(tempDir, "test.txt");
|
||||
assertThat(conflict.createNewFile()).as("Should have been able to create file").isTrue();
|
||||
long fileLength = conflict.length();
|
||||
// also contains test.txt
|
||||
byte[] archive = createFakeZipArchive("test.txt", "Fake content");
|
||||
MockHttpProjectGenerationRequest request = new MockHttpProjectGenerationRequest("application/zip", "demo.zip",
|
||||
archive);
|
||||
mockSuccessfulProjectGeneration(request);
|
||||
assertThat(this.command.run("--force", "--extract", tempDir.getAbsolutePath())).isEqualTo(ExitStatus.OK);
|
||||
assertThat(fileLength != conflict.length()).as("File should have changed").isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseTypeOnly() throws Exception {
|
||||
this.handler.disableProjectGeneration();
|
||||
this.command.run("-t=ant-project");
|
||||
assertThat(this.handler.lastRequest.getBuild()).isEqualTo("maven");
|
||||
assertThat(this.handler.lastRequest.getFormat()).isEqualTo("project");
|
||||
assertThat(this.handler.lastRequest.isDetectType()).isFalse();
|
||||
assertThat(this.handler.lastRequest.getType()).isEqualTo("ant-project");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseBuildOnly() throws Exception {
|
||||
this.handler.disableProjectGeneration();
|
||||
this.command.run("--build=ant");
|
||||
assertThat(this.handler.lastRequest.getBuild()).isEqualTo("ant");
|
||||
assertThat(this.handler.lastRequest.getFormat()).isEqualTo("project");
|
||||
assertThat(this.handler.lastRequest.isDetectType()).isTrue();
|
||||
assertThat(this.handler.lastRequest.getType()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseFormatOnly() throws Exception {
|
||||
this.handler.disableProjectGeneration();
|
||||
this.command.run("--format=web");
|
||||
assertThat(this.handler.lastRequest.getBuild()).isEqualTo("maven");
|
||||
assertThat(this.handler.lastRequest.getFormat()).isEqualTo("web");
|
||||
assertThat(this.handler.lastRequest.isDetectType()).isTrue();
|
||||
assertThat(this.handler.lastRequest.getType()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseLocation() throws Exception {
|
||||
this.handler.disableProjectGeneration();
|
||||
this.command.run("foobar.zip");
|
||||
assertThat(this.handler.lastRequest.getOutput()).isEqualTo("foobar.zip");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseLocationWithSlash() throws Exception {
|
||||
this.handler.disableProjectGeneration();
|
||||
this.command.run("foobar/");
|
||||
assertThat(this.handler.lastRequest.getOutput()).isEqualTo("foobar");
|
||||
assertThat(this.handler.lastRequest.isExtract()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseMoreThanOneArg() throws Exception {
|
||||
this.handler.disableProjectGeneration();
|
||||
assertThat(this.command.run("foobar", "barfoo")).isEqualTo(ExitStatus.ERROR);
|
||||
}
|
||||
|
||||
@Test
|
||||
void userAgent() throws Exception {
|
||||
this.command.run("--list", "--target=https://fake-service");
|
||||
then(this.http).should().execute(this.requestCaptor.capture());
|
||||
Header agent = this.requestCaptor.getValue().getHeaders("User-Agent")[0];
|
||||
assertThat(agent.getValue()).startsWith("SpringBootCli/");
|
||||
}
|
||||
|
||||
private byte[] createFakeZipArchive(String fileName, String content) throws IOException {
|
||||
try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
|
||||
try (ZipOutputStream zos = new ZipOutputStream(bos)) {
|
||||
ZipEntry entry = new ZipEntry(fileName);
|
||||
zos.putNextEntry(entry);
|
||||
zos.write(content.getBytes());
|
||||
zos.closeEntry();
|
||||
return bos.toByteArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class TestableInitCommandOptionHandler extends InitCommand.InitOptionHandler {
|
||||
|
||||
private boolean disableProjectGeneration;
|
||||
|
||||
private ProjectGenerationRequest lastRequest;
|
||||
|
||||
TestableInitCommandOptionHandler(InitializrService initializrService) {
|
||||
super(initializrService);
|
||||
}
|
||||
|
||||
void disableProjectGeneration() {
|
||||
this.disableProjectGeneration = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void generateProject(OptionSet options) throws IOException {
|
||||
this.lastRequest = createProjectGenerationRequest(options);
|
||||
if (!this.disableProjectGeneration) {
|
||||
super.generateProject(options);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.StandardCharsets;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link InitializrServiceMetadata}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class InitializrServiceMetadataTests {
|
||||
|
||||
@Test
|
||||
void parseDefaults() throws Exception {
|
||||
InitializrServiceMetadata metadata = createInstance("2.0.0");
|
||||
assertThat(metadata.getDefaults().get("bootVersion")).isEqualTo("1.1.8.RELEASE");
|
||||
assertThat(metadata.getDefaults().get("javaVersion")).isEqualTo("1.7");
|
||||
assertThat(metadata.getDefaults().get("groupId")).isEqualTo("org.test");
|
||||
assertThat(metadata.getDefaults().get("name")).isEqualTo("demo");
|
||||
assertThat(metadata.getDefaults().get("description")).isEqualTo("Demo project for Spring Boot");
|
||||
assertThat(metadata.getDefaults().get("packaging")).isEqualTo("jar");
|
||||
assertThat(metadata.getDefaults().get("language")).isEqualTo("java");
|
||||
assertThat(metadata.getDefaults().get("artifactId")).isEqualTo("demo");
|
||||
assertThat(metadata.getDefaults().get("packageName")).isEqualTo("demo");
|
||||
assertThat(metadata.getDefaults().get("type")).isEqualTo("maven-project");
|
||||
assertThat(metadata.getDefaults().get("version")).isEqualTo("0.0.1-SNAPSHOT");
|
||||
assertThat(metadata.getDefaults()).as("Wrong number of defaults").hasSize(11);
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseDependencies() throws Exception {
|
||||
InitializrServiceMetadata metadata = createInstance("2.0.0");
|
||||
assertThat(metadata.getDependencies()).hasSize(5);
|
||||
|
||||
// Security description
|
||||
assertThat(metadata.getDependency("aop").getName()).isEqualTo("AOP");
|
||||
assertThat(metadata.getDependency("security").getName()).isEqualTo("Security");
|
||||
assertThat(metadata.getDependency("security").getDescription()).isEqualTo("Security description");
|
||||
assertThat(metadata.getDependency("jdbc").getName()).isEqualTo("JDBC");
|
||||
assertThat(metadata.getDependency("data-jpa").getName()).isEqualTo("JPA");
|
||||
assertThat(metadata.getDependency("data-mongodb").getName()).isEqualTo("MongoDB");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseTypes() throws Exception {
|
||||
InitializrServiceMetadata metadata = createInstance("2.0.0");
|
||||
ProjectType projectType = metadata.getProjectTypes().get("maven-project");
|
||||
assertThat(projectType).isNotNull();
|
||||
assertThat(projectType.getTags().get("build")).isEqualTo("maven");
|
||||
assertThat(projectType.getTags().get("format")).isEqualTo("project");
|
||||
}
|
||||
|
||||
private static InitializrServiceMetadata createInstance(String version) throws JSONException {
|
||||
try {
|
||||
return new InitializrServiceMetadata(readJson(version));
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException("Failed to read json", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static JSONObject readJson(String version) throws IOException, JSONException {
|
||||
Resource resource = new ClassPathResource("metadata/service-metadata-" + version + ".json");
|
||||
try (InputStream stream = resource.getInputStream()) {
|
||||
return new JSONObject(StreamUtils.copyToString(stream, StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.ArgumentMatchers.isA;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link InitializrService}
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class InitializrServiceTests extends AbstractHttpClientMockTests {
|
||||
|
||||
private final InitializrService invoker = new InitializrService(this.http);
|
||||
|
||||
@Test
|
||||
void loadMetadata() throws Exception {
|
||||
mockSuccessfulMetadataGet(false);
|
||||
InitializrServiceMetadata metadata = this.invoker.loadMetadata("https://foo/bar");
|
||||
assertThat(metadata).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateSimpleProject() throws Exception {
|
||||
ProjectGenerationRequest request = new ProjectGenerationRequest();
|
||||
MockHttpProjectGenerationRequest mockHttpRequest = new MockHttpProjectGenerationRequest("application/xml",
|
||||
"foo.zip");
|
||||
ProjectGenerationResponse entity = generateProject(request, mockHttpRequest);
|
||||
assertProjectEntity(entity, mockHttpRequest.contentType, mockHttpRequest.fileName);
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateProjectCustomTargetFilename() throws Exception {
|
||||
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
|
||||
void generateProjectNoDefaultFileName() throws Exception {
|
||||
ProjectGenerationRequest request = new ProjectGenerationRequest();
|
||||
MockHttpProjectGenerationRequest mockHttpRequest = new MockHttpProjectGenerationRequest("application/xml",
|
||||
null);
|
||||
ProjectGenerationResponse entity = generateProject(request, mockHttpRequest);
|
||||
assertProjectEntity(entity, mockHttpRequest.contentType, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateProjectBadRequest() throws Exception {
|
||||
String jsonMessage = "Unknown dependency foo:bar";
|
||||
mockProjectGenerationError(400, jsonMessage);
|
||||
ProjectGenerationRequest request = new ProjectGenerationRequest();
|
||||
request.getDependencies().add("foo:bar");
|
||||
assertThatExceptionOfType(ReportableException.class).isThrownBy(() -> this.invoker.generate(request))
|
||||
.withMessageContaining(jsonMessage);
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateProjectBadRequestNoExtraMessage() throws Exception {
|
||||
mockProjectGenerationError(400, null);
|
||||
ProjectGenerationRequest request = new ProjectGenerationRequest();
|
||||
assertThatExceptionOfType(ReportableException.class).isThrownBy(() -> this.invoker.generate(request))
|
||||
.withMessageContaining("unexpected 400 error");
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateProjectNoContent() throws Exception {
|
||||
mockSuccessfulMetadataGet(false);
|
||||
CloseableHttpResponse response = mock(CloseableHttpResponse.class);
|
||||
mockStatus(response, 500);
|
||||
given(this.http.execute(isA(HttpGet.class))).willReturn(response);
|
||||
ProjectGenerationRequest request = new ProjectGenerationRequest();
|
||||
assertThatExceptionOfType(ReportableException.class).isThrownBy(() -> this.invoker.generate(request))
|
||||
.withMessageContaining("No content received from server");
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadMetadataBadRequest() throws Exception {
|
||||
String jsonMessage = "whatever error on the server";
|
||||
mockMetadataGetError(500, jsonMessage);
|
||||
ProjectGenerationRequest request = new ProjectGenerationRequest();
|
||||
assertThatExceptionOfType(ReportableException.class).isThrownBy(() -> this.invoker.generate(request))
|
||||
.withMessageContaining(jsonMessage);
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadMetadataInvalidJson() throws Exception {
|
||||
CloseableHttpResponse response = mock(CloseableHttpResponse.class);
|
||||
mockHttpEntity(response, "Foo-Bar-Not-JSON".getBytes(), "application/json");
|
||||
mockStatus(response, 200);
|
||||
given(this.http.execute(isA(HttpGet.class))).willReturn(response);
|
||||
ProjectGenerationRequest request = new ProjectGenerationRequest();
|
||||
assertThatExceptionOfType(ReportableException.class).isThrownBy(() -> this.invoker.generate(request))
|
||||
.withMessageContaining("Invalid content received from server");
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadMetadataNoContent() throws Exception {
|
||||
CloseableHttpResponse response = mock(CloseableHttpResponse.class);
|
||||
mockStatus(response, 500);
|
||||
given(this.http.execute(isA(HttpGet.class))).willReturn(response);
|
||||
ProjectGenerationRequest request = new ProjectGenerationRequest();
|
||||
assertThatExceptionOfType(ReportableException.class).isThrownBy(() -> this.invoker.generate(request))
|
||||
.withMessageContaining("No content received from server");
|
||||
}
|
||||
|
||||
private ProjectGenerationResponse generateProject(ProjectGenerationRequest request,
|
||||
MockHttpProjectGenerationRequest mockRequest) throws Exception {
|
||||
mockSuccessfulProjectGeneration(mockRequest);
|
||||
ProjectGenerationResponse entity = this.invoker.generate(request);
|
||||
assertThat(entity.getContent()).as("wrong body content").isEqualTo(mockRequest.content);
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static void assertProjectEntity(ProjectGenerationResponse entity, String mimeType, String fileName) {
|
||||
if (mimeType == null) {
|
||||
assertThat(entity.getContentType()).isNull();
|
||||
}
|
||||
else {
|
||||
assertThat(entity.getContentType().getMimeType()).isEqualTo(mimeType);
|
||||
}
|
||||
assertThat(entity.getFileName()).isEqualTo(fileName);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* Copyright 2012-2021 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
|
||||
*
|
||||
* https://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.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Tests for {@link ProjectGenerationRequest}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Eddú Meléndez
|
||||
*/
|
||||
class ProjectGenerationRequestTests {
|
||||
|
||||
public static final Map<String, String> EMPTY_TAGS = Collections.emptyMap();
|
||||
|
||||
private final ProjectGenerationRequest request = new ProjectGenerationRequest();
|
||||
|
||||
@Test
|
||||
void defaultSettings() {
|
||||
assertThat(this.request.generateUrl(createDefaultMetadata())).isEqualTo(createDefaultUrl("?type=test-type"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void customServer() throws URISyntaxException {
|
||||
String customServerUrl = "https://foo:8080/initializr";
|
||||
this.request.setServiceUrl(customServerUrl);
|
||||
this.request.getDependencies().add("security");
|
||||
assertThat(this.request.generateUrl(createDefaultMetadata()))
|
||||
.isEqualTo(new URI(customServerUrl + "/starter.zip?dependencies=security&type=test-type"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void customBootVersion() {
|
||||
this.request.setBootVersion("1.2.0.RELEASE");
|
||||
assertThat(this.request.generateUrl(createDefaultMetadata()))
|
||||
.isEqualTo(createDefaultUrl("?type=test-type&bootVersion=1.2.0.RELEASE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void singleDependency() {
|
||||
this.request.getDependencies().add("web");
|
||||
assertThat(this.request.generateUrl(createDefaultMetadata()))
|
||||
.isEqualTo(createDefaultUrl("?dependencies=web&type=test-type"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleDependencies() {
|
||||
this.request.getDependencies().add("web");
|
||||
this.request.getDependencies().add("data-jpa");
|
||||
assertThat(this.request.generateUrl(createDefaultMetadata()))
|
||||
.isEqualTo(createDefaultUrl("?dependencies=web%2Cdata-jpa&type=test-type"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void customJavaVersion() {
|
||||
this.request.setJavaVersion("1.8");
|
||||
assertThat(this.request.generateUrl(createDefaultMetadata()))
|
||||
.isEqualTo(createDefaultUrl("?type=test-type&javaVersion=1.8"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void customPackageName() {
|
||||
this.request.setPackageName("demo.foo");
|
||||
assertThat(this.request.generateUrl(createDefaultMetadata()))
|
||||
.isEqualTo(createDefaultUrl("?packageName=demo.foo&type=test-type"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void customType() throws URISyntaxException {
|
||||
ProjectType projectType = new ProjectType("custom", "Custom Type", "/foo", true, EMPTY_TAGS);
|
||||
InitializrServiceMetadata metadata = new InitializrServiceMetadata(projectType);
|
||||
this.request.setType("custom");
|
||||
this.request.getDependencies().add("data-rest");
|
||||
assertThat(this.request.generateUrl(metadata)).isEqualTo(
|
||||
new URI(ProjectGenerationRequest.DEFAULT_SERVICE_URL + "/foo?dependencies=data-rest&type=custom"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void customPackaging() {
|
||||
this.request.setPackaging("war");
|
||||
assertThat(this.request.generateUrl(createDefaultMetadata()))
|
||||
.isEqualTo(createDefaultUrl("?type=test-type&packaging=war"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void customLanguage() {
|
||||
this.request.setLanguage("groovy");
|
||||
assertThat(this.request.generateUrl(createDefaultMetadata()))
|
||||
.isEqualTo(createDefaultUrl("?type=test-type&language=groovy"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void customProjectInfo() {
|
||||
this.request.setGroupId("org.acme");
|
||||
this.request.setArtifactId("sample");
|
||||
this.request.setVersion("1.0.1-SNAPSHOT");
|
||||
this.request.setDescription("Spring Boot Test");
|
||||
assertThat(this.request.generateUrl(createDefaultMetadata()))
|
||||
.isEqualTo(createDefaultUrl("?groupId=org.acme&artifactId=sample&version=1.0.1-SNAPSHOT"
|
||||
+ "&description=Spring+Boot+Test&type=test-type"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void outputCustomizeArtifactId() {
|
||||
this.request.setOutput("my-project");
|
||||
assertThat(this.request.generateUrl(createDefaultMetadata()))
|
||||
.isEqualTo(createDefaultUrl("?artifactId=my-project&type=test-type"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void outputArchiveCustomizeArtifactId() {
|
||||
this.request.setOutput("my-project.zip");
|
||||
assertThat(this.request.generateUrl(createDefaultMetadata()))
|
||||
.isEqualTo(createDefaultUrl("?artifactId=my-project&type=test-type"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void outputArchiveWithDotsCustomizeArtifactId() {
|
||||
this.request.setOutput("my.nice.project.zip");
|
||||
assertThat(this.request.generateUrl(createDefaultMetadata()))
|
||||
.isEqualTo(createDefaultUrl("?artifactId=my.nice.project&type=test-type"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void outputDoesNotOverrideCustomArtifactId() {
|
||||
this.request.setOutput("my-project");
|
||||
this.request.setArtifactId("my-id");
|
||||
assertThat(this.request.generateUrl(createDefaultMetadata()))
|
||||
.isEqualTo(createDefaultUrl("?artifactId=my-id&type=test-type"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildNoMatch() throws Exception {
|
||||
InitializrServiceMetadata metadata = readMetadata();
|
||||
setBuildAndFormat("does-not-exist", null);
|
||||
assertThatExceptionOfType(ReportableException.class).isThrownBy(() -> this.request.generateUrl(metadata))
|
||||
.withMessageContaining("does-not-exist");
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildMultipleMatch() throws Exception {
|
||||
InitializrServiceMetadata metadata = readMetadata("types-conflict");
|
||||
setBuildAndFormat("gradle", null);
|
||||
assertThatExceptionOfType(ReportableException.class).isThrownBy(() -> this.request.generateUrl(metadata))
|
||||
.withMessageContaining("gradle-project").withMessageContaining("gradle-project-2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildOneMatch() throws Exception {
|
||||
InitializrServiceMetadata metadata = readMetadata();
|
||||
setBuildAndFormat("gradle", null);
|
||||
assertThat(this.request.generateUrl(metadata)).isEqualTo(createDefaultUrl("?type=gradle-project"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void typeAndBuildAndFormat() throws Exception {
|
||||
InitializrServiceMetadata metadata = readMetadata();
|
||||
setBuildAndFormat("gradle", "project");
|
||||
this.request.setType("maven-build");
|
||||
assertThat(this.request.generateUrl(metadata)).isEqualTo(createUrl("/pom.xml?type=maven-build"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidType() {
|
||||
this.request.setType("does-not-exist");
|
||||
assertThatExceptionOfType(ReportableException.class)
|
||||
.isThrownBy(() -> this.request.generateUrl(createDefaultMetadata()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void noTypeAndNoDefault() {
|
||||
assertThatExceptionOfType(ReportableException.class)
|
||||
.isThrownBy(() -> this.request.generateUrl(readMetadata("types-conflict")))
|
||||
.withMessageContaining("no default is defined");
|
||||
}
|
||||
|
||||
private static URI createUrl(String actionAndParam) {
|
||||
try {
|
||||
return new URI(ProjectGenerationRequest.DEFAULT_SERVICE_URL + actionAndParam);
|
||||
}
|
||||
catch (URISyntaxException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static URI createDefaultUrl(String param) {
|
||||
return createUrl("/starter.zip" + param);
|
||||
}
|
||||
|
||||
void setBuildAndFormat(String build, String format) {
|
||||
this.request.setBuild((build != null) ? build : "maven");
|
||||
this.request.setFormat((format != null) ? format : "project");
|
||||
this.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() throws JSONException {
|
||||
return readMetadata("2.0.0");
|
||||
}
|
||||
|
||||
private static InitializrServiceMetadata readMetadata(String version) throws JSONException {
|
||||
try {
|
||||
Resource resource = new ClassPathResource("metadata/service-metadata-" + version + ".json");
|
||||
String content = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
|
||||
JSONObject json = new JSONObject(content);
|
||||
return new InitializrServiceMetadata(json);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException("Failed to read metadata", ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link ServiceCapabilitiesReportGenerator}
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class ServiceCapabilitiesReportGeneratorTests extends AbstractHttpClientMockTests {
|
||||
|
||||
private final ServiceCapabilitiesReportGenerator command = new ServiceCapabilitiesReportGenerator(
|
||||
new InitializrService(this.http));
|
||||
|
||||
@Test
|
||||
void listMetadataFromServer() throws IOException {
|
||||
mockSuccessfulMetadataTextGet();
|
||||
String expected = new String(readClasspathResource("metadata/service-metadata-2.1.0.txt"));
|
||||
String content = this.command.generate("http://localhost");
|
||||
assertThat(content).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void listMetadata() throws IOException {
|
||||
mockSuccessfulMetadataGet(true);
|
||||
doTestGenerateCapabilitiesFromJson();
|
||||
}
|
||||
|
||||
@Test
|
||||
void listMetadataV2() throws IOException {
|
||||
mockSuccessfulMetadataGetV2(true);
|
||||
doTestGenerateCapabilitiesFromJson();
|
||||
}
|
||||
|
||||
private void doTestGenerateCapabilitiesFromJson() throws IOException {
|
||||
String content = this.command.generate("http://localhost");
|
||||
assertThat(content).contains("aop - AOP");
|
||||
assertThat(content).contains("security - Security: Security description");
|
||||
assertThat(content).contains("type: maven-project");
|
||||
assertThat(content).contains("packaging: jar");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.shell;
|
||||
|
||||
import jline.console.completer.ArgumentCompleter.ArgumentList;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link EscapeAwareWhiteSpaceArgumentDelimiter}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class EscapeAwareWhiteSpaceArgumentDelimiterTests {
|
||||
|
||||
private final EscapeAwareWhiteSpaceArgumentDelimiter delimiter = new EscapeAwareWhiteSpaceArgumentDelimiter();
|
||||
|
||||
@Test
|
||||
void simple() {
|
||||
String s = "one two";
|
||||
assertThat(this.delimiter.delimit(s, 0).getArguments()).containsExactly("one", "two");
|
||||
assertThat(this.delimiter.parseArguments(s)).containsExactly("one", "two");
|
||||
assertThat(this.delimiter.isDelimiter(s, 2)).isFalse();
|
||||
assertThat(this.delimiter.isDelimiter(s, 3)).isTrue();
|
||||
assertThat(this.delimiter.isDelimiter(s, 4)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void escaped() {
|
||||
String s = "o\\ ne two";
|
||||
assertThat(this.delimiter.delimit(s, 0).getArguments()).containsExactly("o\\ ne", "two");
|
||||
assertThat(this.delimiter.parseArguments(s)).containsExactly("o ne", "two");
|
||||
assertThat(this.delimiter.isDelimiter(s, 2)).isFalse();
|
||||
assertThat(this.delimiter.isDelimiter(s, 3)).isFalse();
|
||||
assertThat(this.delimiter.isDelimiter(s, 4)).isFalse();
|
||||
assertThat(this.delimiter.isDelimiter(s, 5)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void quoted() {
|
||||
String s = "'o ne' 't w o'";
|
||||
assertThat(this.delimiter.delimit(s, 0).getArguments()).containsExactly("'o ne'", "'t w o'");
|
||||
assertThat(this.delimiter.parseArguments(s)).containsExactly("o ne", "t w o");
|
||||
}
|
||||
|
||||
@Test
|
||||
void doubleQuoted() {
|
||||
String s = "\"o ne\" \"t w o\"";
|
||||
assertThat(this.delimiter.delimit(s, 0).getArguments()).containsExactly("\"o ne\"", "\"t w o\"");
|
||||
assertThat(this.delimiter.parseArguments(s)).containsExactly("o ne", "t w o");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nestedQuotes() {
|
||||
String s = "\"o 'n''e\" 't \"w o'";
|
||||
assertThat(this.delimiter.delimit(s, 0).getArguments()).containsExactly("\"o 'n''e\"", "'t \"w o'");
|
||||
assertThat(this.delimiter.parseArguments(s)).containsExactly("o 'n''e", "t \"w o");
|
||||
}
|
||||
|
||||
@Test
|
||||
void escapedQuotes() {
|
||||
String s = "\\'a b";
|
||||
ArgumentList argumentList = this.delimiter.delimit(s, 0);
|
||||
assertThat(argumentList.getArguments()).isEqualTo(new String[] { "\\'a", "b" });
|
||||
assertThat(this.delimiter.parseArguments(s)).containsExactly("'a", "b");
|
||||
}
|
||||
|
||||
@Test
|
||||
void escapes() {
|
||||
String s = "\\ \\\\.\\\\\\t";
|
||||
assertThat(this.delimiter.parseArguments(s)).containsExactly(" \\.\\\t");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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
|
||||
*
|
||||
* https://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.util;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Mock log for testing message output.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public interface MockLog extends LogListener {
|
||||
|
||||
static MockLog attach() {
|
||||
MockLog log = mock(MockLog.class);
|
||||
Log.setListener(log);
|
||||
return log;
|
||||
}
|
||||
|
||||
static void clear() {
|
||||
Log.setListener(null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
cli.command.CustomCommandFactory
|
||||
Binary file not shown.
@@ -0,0 +1,30 @@
|
||||
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
|
||||
https://maven.apache.org/xsd/settings-1.0.0.xsd">
|
||||
<localRepository>build/local-m2-repository</localRepository>
|
||||
<mirrors>
|
||||
<mirror>
|
||||
<id>central-mirror</id>
|
||||
<url>https://central-mirror.example.com/maven2</url>
|
||||
<mirrorOf>central</mirrorOf>
|
||||
</mirror>
|
||||
</mirrors>
|
||||
<servers>
|
||||
<server>
|
||||
<id>central-mirror</id>
|
||||
<username>user</username>
|
||||
<password>password</password>
|
||||
</server>
|
||||
</servers>
|
||||
<proxies>
|
||||
<proxy>
|
||||
<active>true</active>
|
||||
<protocol>http</protocol>
|
||||
<host>proxy.example.com</host>
|
||||
<port>3128</port>
|
||||
<username>user</username>
|
||||
<password>password</password>
|
||||
</proxy>
|
||||
</proxies>
|
||||
</settings>
|
||||
@@ -0,0 +1,13 @@
|
||||
import org.springframework.util.*
|
||||
|
||||
@Component
|
||||
public class Test implements CommandLineRunner {
|
||||
|
||||
public void run(String... args) throws Exception {
|
||||
println "HasClasses-" + ClassUtils.isPresent("missing", null) + "-" +
|
||||
ClassUtils.isPresent("org.springframework.boot.SpringApplication", null) + "-" +
|
||||
ClassUtils.isPresent(args[0], null)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<settings>
|
||||
<localRepository>build/local-m2-repository</localRepository>
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>cli-test-repo</id>
|
||||
<activation>
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>local.central</id>
|
||||
<url>file:build/test-repository</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>spring-snapshot</id>
|
||||
<url>https://repo.spring.io/snapshot</url>
|
||||
<releases>
|
||||
<enabled>false</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>spring-milestone</id>
|
||||
<url>https://repo.spring.io/milestone</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
</profile>
|
||||
</profiles>
|
||||
</settings>
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2012-2013 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
|
||||
*
|
||||
* https://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.
|
||||
*/
|
||||
|
||||
command("foo") { args ->
|
||||
println "Hello Foo"
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2012-2013 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
|
||||
*
|
||||
* https://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.test.command
|
||||
|
||||
class TestCommand implements Command {
|
||||
|
||||
String name = "foo"
|
||||
|
||||
String description = "My script command"
|
||||
|
||||
String help = "No options"
|
||||
|
||||
String usageHelp = "Not very useful"
|
||||
|
||||
Collection<String> optionsHelp = ["No options"]
|
||||
|
||||
boolean optionCommand = false
|
||||
|
||||
void run(String... args) {
|
||||
println "Hello ${args[0]}"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2012-2013 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
|
||||
*
|
||||
* https://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.test.command
|
||||
|
||||
@Grab("org.eclipse.jgit:org.eclipse.jgit:2.3.1.201302201838-r")
|
||||
import org.eclipse.jgit.api.Git
|
||||
|
||||
|
||||
class TestCommand extends OptionHandler {
|
||||
|
||||
String name = "foo"
|
||||
|
||||
void options() {
|
||||
option "foo", "Foo set"
|
||||
}
|
||||
|
||||
void run(OptionSet options) {
|
||||
// Demonstrate use of Grape.grab to load dependencies before running
|
||||
println "Clean: " + Git.open(".." as File).status().call().isClean()
|
||||
println "Hello ${options.nonOptionArguments()}: ${options.has('foo')}"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2012-2013 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
|
||||
*
|
||||
* https://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.
|
||||
*/
|
||||
|
||||
def foo() {
|
||||
"Foo"
|
||||
}
|
||||
|
||||
command("foo") {
|
||||
|
||||
options {
|
||||
option "foo", "A foo of type String"
|
||||
option "bar", "Bar has a value" withOptionalArg() ofType Integer
|
||||
}
|
||||
|
||||
run { options ->
|
||||
println "Hello ${foo()}: bar=${options.valueOf('bar')}"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.test
|
||||
|
||||
@Component
|
||||
class Example implements CommandLineRunner {
|
||||
|
||||
@Autowired
|
||||
private MyService myService
|
||||
|
||||
void run(String... args) {
|
||||
println "Hello ${this.myService.sayWorld()} From ${getClass().getClassLoader().getResource('samples/app.groovy')}"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Service
|
||||
class MyService {
|
||||
|
||||
String sayWorld() {
|
||||
return "World!"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="
|
||||
http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>foo</groupId>
|
||||
<artifactId>foo</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<version>1.0</version>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,5 @@
|
||||
@DependencyManagementBom('test:child:1.0.0')
|
||||
@Grab('ejb-api')
|
||||
class CustomDependencyManagement {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
@DependencyManagementBom("foo:bar:1.0")
|
||||
@DependencyManagementBom("alpha:bravo:2.0")
|
||||
class DuplicateDependencyManagement {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@Grab('jackson-core')
|
||||
class GrabTest {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<project>
|
||||
<parent>
|
||||
<groupId>test</groupId>
|
||||
<artifactId>parent</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>child</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
</project>
|
||||
@@ -0,0 +1,18 @@
|
||||
<project>
|
||||
<groupId>test</groupId>
|
||||
<artifactId>parent</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>javax.ejb</groupId>
|
||||
<artifactId>ejb-api</artifactId>
|
||||
<version>3.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
</project>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user