Move spring-boot-cli into spring-boot-tools

Closes gh-32492
This commit is contained in:
Andy Wilkinson
2022-09-23 16:30:46 +01:00
parent 0c5d0ac717
commit f67db3d9ad
136 changed files with 3 additions and 3 deletions

View File

@@ -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:");
}
}

View File

@@ -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
}
}
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 "$@"

View File

@@ -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 "$@"

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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]);
}
}

View File

@@ -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;
}
}

View File

@@ -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] &lt;file&gt;'. 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;
}

View File

@@ -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
}
}

View File

@@ -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();
}

View File

@@ -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("");
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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)));
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

@@ -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;
}
}
}

View File

@@ -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();
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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"
}
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}
}

View File

@@ -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
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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));
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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");
}
}

View File

@@ -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");
}
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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)
}
}

View File

@@ -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>

View File

@@ -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"
}

View File

@@ -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]}"
}
}

View File

@@ -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')}"
}
}

View File

@@ -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')}"
}
}

View File

@@ -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!"
}
}

View File

@@ -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>

View File

@@ -0,0 +1,5 @@
@DependencyManagementBom('test:child:1.0.0')
@Grab('ejb-api')
class CustomDependencyManagement {
}

View File

@@ -0,0 +1,5 @@
@DependencyManagementBom("foo:bar:1.0")
@DependencyManagementBom("alpha:bravo:2.0")
class DuplicateDependencyManagement {
}

View File

@@ -0,0 +1,4 @@
@Grab('jackson-core')
class GrabTest {
}

View File

@@ -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>

View File

@@ -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