Relocate projects to spring-boot-project

Move projects to better reflect the way that Spring Boot is released.

The following projects are under `spring-boot-project`:

  - `spring-boot`
  - `spring-boot-autoconfigure`
  - `spring-boot-tools`
  - `spring-boot-starters`
  - `spring-boot-actuator`
  - `spring-boot-actuator-autoconfigure`
  - `spring-boot-test`
  - `spring-boot-test-autoconfigure`
  - `spring-boot-devtools`
  - `spring-boot-cli`
  - `spring-boot-docs`

See gh-9316
This commit is contained in:
Phillip Webb
2017-09-19 14:29:46 -07:00
parent 0419d42b7c
commit 0ba4830b4f
4023 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli;
import java.io.IOException;
import org.junit.Test;
import org.springframework.boot.cli.infrastructure.CommandLineInvoker;
import org.springframework.boot.cli.infrastructure.CommandLineInvoker.Invocation;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertThat;
/**
* Integration Tests for the command line application.
*
* @author Andy Wilkinson
* @author Phillip Webb
*/
public class CommandLineIT {
private final CommandLineInvoker cli = new CommandLineInvoker();
@Test
public void hintProducesListOfValidCommands()
throws IOException, InterruptedException {
Invocation cli = this.cli.invoke("hint");
assertThat(cli.await(), equalTo(0));
assertThat("Unexpected error: \n" + cli.getErrorOutput(),
cli.getErrorOutput().length(), equalTo(0));
assertThat(cli.getStandardOutputLines().size(), equalTo(10));
}
@Test
public void invokingWithNoArgumentsDisplaysHelp()
throws IOException, InterruptedException {
Invocation cli = this.cli.invoke();
assertThat(cli.await(), equalTo(1));
assertThat(cli.getErrorOutput().length(), equalTo(0));
assertThat(cli.getStandardOutput(), startsWith("usage:"));
}
@Test
public void unrecognizedCommandsAreHandledGracefully()
throws IOException, InterruptedException {
Invocation cli = this.cli.invoke("not-a-real-command");
assertThat(cli.await(), equalTo(1));
assertThat(cli.getErrorOutput(),
containsString("'not-a-real-command' is not a valid command"));
assertThat(cli.getStandardOutput().length(), equalTo(0));
}
@Test
public void version() throws IOException, InterruptedException {
Invocation cli = this.cli.invoke("version");
assertThat(cli.await(), equalTo(0));
assertThat(cli.getErrorOutput().length(), equalTo(0));
assertThat(cli.getStandardOutput(), startsWith("Spring CLI v"));
}
@Test
public void help() throws IOException, InterruptedException {
Invocation cli = this.cli.invoke("help");
assertThat(cli.await(), equalTo(1));
assertThat(cli.getErrorOutput().length(), equalTo(0));
assertThat(cli.getStandardOutput(), startsWith("usage:"));
}
}

View File

@@ -0,0 +1,169 @@
/*
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli;
import java.io.File;
import org.junit.Test;
import org.springframework.boot.cli.command.archive.JarCommand;
import org.springframework.boot.cli.infrastructure.CommandLineInvoker;
import org.springframework.boot.cli.infrastructure.CommandLineInvoker.Invocation;
import org.springframework.boot.loader.tools.JavaExecutable;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
/**
* Integration test for {@link JarCommand}.
*
* @author Andy Wilkinson
* @author Stephane Nicoll
*/
public class JarCommandIT {
private static final boolean JAVA_9_OR_LATER = isClassPresent(
"java.security.cert.URICertStoreParameters");
private final CommandLineInvoker cli = new CommandLineInvoker(
new File("src/it/resources/jar-command"));
@Test
public void noArguments() throws Exception {
Invocation invocation = this.cli.invoke("jar");
invocation.await();
assertThat(invocation.getStandardOutput(), equalTo(""));
assertThat(invocation.getErrorOutput(), containsString("The name of the "
+ "resulting jar and at least one source file must be specified"));
}
@Test
public void noSources() throws Exception {
Invocation invocation = this.cli.invoke("jar", "test-app.jar");
invocation.await();
assertThat(invocation.getStandardOutput(), equalTo(""));
assertThat(invocation.getErrorOutput(), containsString("The name of the "
+ "resulting jar and at least one source file must be specified"));
}
@Test
public void jarCreationWithGrabResolver() throws Exception {
File jar = new File("target/test-app.jar");
Invocation invocation = this.cli.invoke("run", jar.getAbsolutePath(),
"bad.groovy");
invocation.await();
if (!JAVA_9_OR_LATER) {
assertThat(invocation.getErrorOutput(), equalTo(""));
}
invocation = this.cli.invoke("jar", jar.getAbsolutePath(), "bad.groovy");
invocation.await();
if (!JAVA_9_OR_LATER) {
assertEquals(invocation.getErrorOutput(), 0,
invocation.getErrorOutput().length());
}
assertTrue(jar.exists());
Process process = new JavaExecutable()
.processBuilder("-jar", jar.getAbsolutePath()).start();
invocation = new Invocation(process);
invocation.await();
if (!JAVA_9_OR_LATER) {
assertThat(invocation.getErrorOutput(), equalTo(""));
}
}
@Test
public void jarCreation() throws Exception {
File jar = new File("target/test-app.jar");
Invocation invocation = this.cli.invoke("jar", jar.getAbsolutePath(),
"jar.groovy");
invocation.await();
if (!JAVA_9_OR_LATER) {
assertEquals(invocation.getErrorOutput(), 0,
invocation.getErrorOutput().length());
}
assertTrue(jar.exists());
Process process = new JavaExecutable()
.processBuilder("-jar", jar.getAbsolutePath()).start();
invocation = new Invocation(process);
invocation.await();
if (!JAVA_9_OR_LATER) {
assertThat(invocation.getErrorOutput(), equalTo(""));
}
assertThat(invocation.getStandardOutput(), containsString("Hello World!"));
assertThat(invocation.getStandardOutput(),
containsString("/BOOT-INF/classes!/public/public.txt"));
assertThat(invocation.getStandardOutput(),
containsString("/BOOT-INF/classes!/resources/resource.txt"));
assertThat(invocation.getStandardOutput(),
containsString("/BOOT-INF/classes!/static/static.txt"));
assertThat(invocation.getStandardOutput(),
containsString("/BOOT-INF/classes!/templates/template.txt"));
assertThat(invocation.getStandardOutput(),
containsString("/BOOT-INF/classes!/root.properties"));
assertThat(invocation.getStandardOutput(), containsString("Goodbye Mama"));
}
@Test
public void jarCreationWithIncludes() throws Exception {
File jar = new File("target/test-app.jar");
Invocation invocation = this.cli.invoke("jar", jar.getAbsolutePath(), "--include",
"-public/**,-resources/**", "jar.groovy");
invocation.await();
if (!JAVA_9_OR_LATER) {
assertEquals(invocation.getErrorOutput(), 0,
invocation.getErrorOutput().length());
}
assertTrue(jar.exists());
Process process = new JavaExecutable()
.processBuilder("-jar", jar.getAbsolutePath()).start();
invocation = new Invocation(process);
invocation.await();
if (!JAVA_9_OR_LATER) {
assertThat(invocation.getErrorOutput(), equalTo(""));
}
assertThat(invocation.getStandardOutput(), containsString("Hello World!"));
assertThat(invocation.getStandardOutput(),
not(containsString("/public/public.txt")));
assertThat(invocation.getStandardOutput(),
not(containsString("/resources/resource.txt")));
assertThat(invocation.getStandardOutput(), containsString("/static/static.txt"));
assertThat(invocation.getStandardOutput(),
containsString("/templates/template.txt"));
assertThat(invocation.getStandardOutput(), containsString("Goodbye Mama"));
}
private static boolean isClassPresent(String name) {
try {
Class.forName(name);
return true;
}
catch (Exception ex) {
return false;
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli;
import java.io.File;
import org.junit.Test;
import org.springframework.boot.cli.command.archive.WarCommand;
import org.springframework.boot.cli.infrastructure.CommandLineInvoker;
import org.springframework.boot.cli.infrastructure.CommandLineInvoker.Invocation;
import org.springframework.boot.loader.tools.JavaExecutable;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration test for {@link WarCommand}.
*
* @author Andrey Stolyarov
* @author Henri Kerola
*/
public class WarCommandIT {
private final CommandLineInvoker cli = new CommandLineInvoker(
new File("src/it/resources/war-command"));
@Test
public void warCreation() throws Exception {
File war = new File("target/test-app.war");
Invocation invocation = this.cli.invoke("war", war.getAbsolutePath(),
"war.groovy");
invocation.await();
assertThat(war.exists()).isTrue();
Process process = new JavaExecutable()
.processBuilder("-jar", war.getAbsolutePath(), "--server.port=0").start();
invocation = new Invocation(process);
invocation.await();
assertThat(invocation.getOutput()).contains("onStart error");
assertThat(invocation.getOutput()).contains("Tomcat started");
assertThat(invocation.getOutput())
.contains("/WEB-INF/lib-provided/tomcat-embed-core");
assertThat(invocation.getOutput()).contains("WEB-INF/classes!/root.properties");
process.destroy();
}
}

View File

@@ -0,0 +1,218 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.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.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
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;
public CommandLineInvoker() {
this(new File("."));
}
public CommandLineInvoker(File workingDirectory) {
this.workingDirectory = workingDirectory;
}
public Invocation invoke(String... args) throws IOException {
return new Invocation(runCliProcess(args));
}
private Process runCliProcess(String... args) throws IOException {
List<String> command = new ArrayList<>();
command.add(findLaunchScript().getAbsolutePath());
command.addAll(Arrays.asList(args));
ProcessBuilder processBuilder = new ProcessBuilder(command)
.directory(this.workingDirectory);
processBuilder.environment().remove("JAVA_OPTS");
return processBuilder.start();
}
private File findLaunchScript() throws IOException {
File unpacked = new File("target/unpacked-cli");
if (!unpacked.isDirectory()) {
File zip = new File("target")
.listFiles((pathname) -> pathname.getName().endsWith("-bin.zip"))[0];
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()));
String line;
List<String> lines = new ArrayList<>();
try {
while ((line = reader.readLine()) != null) {
if (!line.startsWith("Picked up ")) {
lines.add(line);
}
}
}
catch (IOException ex) {
throw new RuntimeException("Failed to read output");
}
return lines;
}
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,6 @@
@GrabResolver(name='clojars.org', root='http://clojars.org/repo')
@Grab('redis.embedded:embedded-redis:0.2')
@Component
class EmbeddedRedis {
}

View File

@@ -0,0 +1,27 @@
package org.test
@EnableGroovyTemplates
@Component
class Example implements CommandLineRunner {
@Autowired
private MyService myService
void run(String... args) {
println "Hello ${this.myService.sayWorld()}"
println getClass().getResource('/public/public.txt')
println getClass().getResource('/resources/resource.txt')
println getClass().getResource('/static/static.txt')
println getClass().getResource('/templates/template.txt')
println getClass().getResource('/root.properties')
println template('template.txt', [world:'Mama'])
}
}
@Service
class MyService {
String sayWorld() {
return 'World!'
}
}

View File

@@ -0,0 +1 @@
Goodbye ${world}

View File

@@ -0,0 +1,10 @@
package org.test
@Component
class Example implements CommandLineRunner {
void run(String... args) {
print "Ssshh"
}
}

View File

@@ -0,0 +1,17 @@
package org.test
@RestController
class WarExample implements CommandLineRunner {
@RequestMapping("/")
public String hello() {
return "Hello"
}
void run(String... args) {
println getClass().getResource('/org/apache/tomcat/InstanceManager.class')
println getClass().getResource('/root.properties')
throw new RuntimeException("onStart error")
}
}

View File

@@ -0,0 +1,46 @@
<assembly>
<id>bin</id>
<formats>
<format>zip</format>
<format>tar.gz</format>
</formats>
<baseDirectory>spring-${project.version}</baseDirectory>
<includeBaseDirectory>true</includeBaseDirectory>
<fileSets>
<fileSet>
<directory>src/main/content</directory>
<outputDirectory></outputDirectory>
<useDefaultExcludes>true</useDefaultExcludes>
<fileMode>644</fileMode>
<directoryMode>755</directoryMode>
<filtered>true</filtered>
<includes>
<include>INSTALL.txt</include>
</includes>
</fileSet>
<fileSet>
<directory>src/main/content</directory>
<outputDirectory></outputDirectory>
<useDefaultExcludes>true</useDefaultExcludes>
<fileMode>644</fileMode>
<directoryMode>755</directoryMode>
<excludes>
<exclude>INSTALL.txt</exclude>
</excludes>
</fileSet>
<fileSet>
<directory>src/main/executablecontent</directory>
<outputDirectory></outputDirectory>
<useDefaultExcludes>true</useDefaultExcludes>
<fileMode>755</fileMode>
<directoryMode>755</directoryMode>
</fileSet>
</fileSets>
<files>
<file>
<source>${project.build.directory}/${project.artifactId}-${project.version}-full.jar</source>
<outputDirectory>lib</outputDirectory>
<destName>${project.build.finalName}.jar</destName>
</file>
</files>
</assembly>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<assembly
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
<id>full</id>
<formats>
<format>jar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<dependencySets>
<dependencySet>
<useProjectArtifact />
<includes>
<include>${project.groupId}:${project.artifactId}</include>
</includes>
<unpack>true</unpack>
<outputDirectory>BOOT-INF/classes/</outputDirectory>
</dependencySet>
</dependencySets>
<fileSets>
<fileSet>
<directory>${project.build.directory}/assembly</directory>
<outputDirectory></outputDirectory>
</fileSet>
</fileSets>
</assembly>

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 an 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 install 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
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

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
http://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.codehaus.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.codehaus.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
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License
>>> CGLIB 3.0 (cglib:cglib:3.0):
Per the LICENSE file in the CGLIB JAR distribution downloaded from
http://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 http://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 http://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,95 @@
#!/usr/bin/env bash
# OS specific support (must be 'true' or 'false').
cygwin=false;
darwin=false;
case "`uname`" in
CYGWIN*)
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
[ -z "$JAVA_HOME" -a -f "/usr/libexec/java_home" ] && export JAVA_HOME=`/usr/libexec/java_home`
[ -z "$JAVA_HOME" -a -d "/Library/Java/Home" ] && export JAVA_HOME="/Library/Java/Home"
[ -z "$JAVA_HOME" -a -d "/System/Library/Frameworks/JavaVM.framework/Home" ] && export JAVA_HOME="/System/Library/Frameworks/JavaVM.framework/Home"
else
javaExecutable="`which javac`"
[ -z "$javaExecutable" -o "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ] && echo "JAVA_HOME not set and cannot find javac to deduce location, please set JAVA_HOME." && exit 1
# readlink(1) is not available as standard on Solaris 10.
readLink=`which 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
echo ""
echo "======================================================================================================"
echo " Please ensure that your JAVA_HOME points to a valid Java SDK."
echo " You are currently pointing to:"
echo ""
echo " ${JAVA_HOME}"
echo ""
echo " This does not seem to be valid. Please rectify and restart."
echo "======================================================================================================"
echo ""
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\"`/../" >&-
export SPRING_HOME="`pwd -P`"
cd "$SAVED" >&-
fi
if [ ! -d "${SPRING_HOME}" ]; then
echo "Not a directory: SPRING_HOME=${SPRING_HOME}"
echo "Please rectify and restart."
exit 2
fi
CLASSPATH=.:${SPRING_HOME}/bin
if [ -d ${SPRING_HOME}/ext ]; then
CLASSPATH=$CLASSPATH:${SPRING_HOME}/ext
fi
for f in ${SPRING_HOME}/lib/*; do
CLASSPATH=$CLASSPATH:$f
done
if $cygwin; then
SPRING_HOME=`cygpath --path --mixed "$SPRING_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
fi
"${JAVA_HOME}/bin/java" ${JAVA_OPTS} -cp "$CLASSPATH" org.springframework.boot.loader.JarLauncher "$@"

View File

@@ -0,0 +1,27 @@
require 'formula'
class Springboot < Formula
homepage 'http://projects.spring.io/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 '${checksum}'
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,51 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli;
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.CommandFactory;
import org.springframework.boot.cli.command.archive.JarCommand;
import org.springframework.boot.cli.command.archive.WarCommand;
import org.springframework.boot.cli.command.core.VersionCommand;
import org.springframework.boot.cli.command.grab.GrabCommand;
import org.springframework.boot.cli.command.init.InitCommand;
import org.springframework.boot.cli.command.install.InstallCommand;
import org.springframework.boot.cli.command.install.UninstallCommand;
import org.springframework.boot.cli.command.run.RunCommand;
/**
* Default implementation of {@link CommandFactory}.
*
* @author Dave Syer
*/
public class DefaultCommandFactory implements CommandFactory {
private static final List<Command> defaultCommands = Arrays.<Command>asList(
new VersionCommand(), new RunCommand(), new GrabCommand(), new JarCommand(),
new WarCommand(), new InstallCommand(), new UninstallCommand(),
new InitCommand());
@Override
public Collection<Command> getCommands() {
return defaultCommands;
}
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli;
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
* @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[urls.size()]);
}
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.app;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
/**
* A launcher for {@code SpringApplication} or a {@code SpringApplication} subclass. The
* class that is used can be configured using the System property
* {@code spring.application.class.name} or the {@code SPRING_APPLICATION_CLASS_NAME}
* environment variable. Uses reflection to allow the launching code to exist in a
* separate ClassLoader from the application code.
*
* @author Andy Wilkinson
* @since 1.2.0
* @see System#getProperty(String)
* @see System#getenv(String)
*/
public class SpringApplicationLauncher {
private static final String DEFAULT_SPRING_APPLICATION_CLASS = "org.springframework.boot.SpringApplication";
private final ClassLoader classLoader;
/**
* Creates a new launcher that will use the given {@code classLoader} to load the
* configured {@code SpringApplication} class.
* @param classLoader the {@code ClassLoader} to use
*/
public SpringApplicationLauncher(ClassLoader classLoader) {
this.classLoader = classLoader;
}
/**
* Launches the application created using the given {@code sources}. The application
* is launched with the given {@code args}.
* @param sources The sources for the application
* @param args The args for the application
* @return The application's {@code ApplicationContext}
* @throws Exception if the launch fails
*/
public Object launch(Class<?>[] sources, String[] args) throws Exception {
Map<String, Object> defaultProperties = new HashMap<>();
defaultProperties.put("spring.groovy.template.check-template-location", "false");
Class<?> applicationClass = this.classLoader
.loadClass(getSpringApplicationClassName());
Constructor<?> constructor = applicationClass.getConstructor(Class[].class);
Object application = constructor.newInstance((Object) sources);
applicationClass.getMethod("setDefaultProperties", Map.class).invoke(application,
defaultProperties);
Method method = applicationClass.getMethod("run", String[].class);
return method.invoke(application, (Object) args);
}
private String getSpringApplicationClassName() {
String className = System.getProperty("spring.application.class.name");
if (className == null) {
className = getEnvironmentVariable("SPRING_APPLICATION_CLASS_NAME");
}
if (className == null) {
className = DEFAULT_SPRING_APPLICATION_CLASS;
}
return className;
}
protected String getEnvironmentVariable(String name) {
return System.getenv(name);
}
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.app;
import java.io.IOException;
import java.io.InputStream;
import java.util.jar.Manifest;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
/**
* {@link SpringBootServletInitializer} for CLI packaged WAR files.
*
* @author Phillip Webb
* @since 1.3.0
*/
public class SpringApplicationWebApplicationInitializer
extends SpringBootServletInitializer {
/**
* The entry containing the source class.
*/
public static final String SOURCE_ENTRY = "Spring-Application-Source-Classes";
private String[] sources;
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
try {
this.sources = getSources(servletContext);
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
super.onStartup(servletContext);
}
private String[] getSources(ServletContext servletContext) throws IOException {
Manifest manifest = getManifest(servletContext);
if (manifest == null) {
throw new IllegalStateException("Unable to read manifest");
}
String sources = manifest.getMainAttributes().getValue(SOURCE_ENTRY);
return sources.split(",");
}
private Manifest getManifest(ServletContext servletContext) throws IOException {
InputStream stream = servletContext.getResourceAsStream("/META-INF/MANIFEST.MF");
Manifest manifest = (stream == null ? null : new Manifest(stream));
return manifest;
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
try {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Class<?>[] sourceClasses = new Class<?>[this.sources.length];
for (int i = 0; i < this.sources.length; i++) {
sourceClasses[i] = classLoader.loadClass(this.sources[i]);
}
return builder.sources(sourceClasses)
.properties("spring.groovy.template.check-template-location=false");
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.archive;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Enumeration;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import org.springframework.boot.cli.app.SpringApplicationLauncher;
/**
* A launcher for a CLI application that has been compiled and packaged as a jar file.
*
* @author Andy Wilkinson
* @author Phillip Webb
*/
public final class PackagedSpringApplicationLauncher {
/**
* The entry containing the source class.
*/
public static final String SOURCE_ENTRY = "Spring-Application-Source-Classes";
/**
* The entry containing the start class.
*/
public static final String START_CLASS_ENTRY = "Start-Class";
private PackagedSpringApplicationLauncher() {
}
private void run(String[] args) throws Exception {
URLClassLoader classLoader = (URLClassLoader) Thread.currentThread()
.getContextClassLoader();
new SpringApplicationLauncher(classLoader).launch(getSources(classLoader), args);
}
private Class<?>[] getSources(URLClassLoader classLoader) throws Exception {
Enumeration<URL> urls = classLoader.getResources("META-INF/MANIFEST.MF");
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
Manifest manifest = new Manifest(url.openStream());
if (isCliPackaged(manifest)) {
String sources = manifest.getMainAttributes().getValue(SOURCE_ENTRY);
return loadClasses(classLoader, sources.split(","));
}
}
throw new IllegalStateException(
"Cannot locate " + SOURCE_ENTRY + " in MANIFEST.MF");
}
private boolean isCliPackaged(Manifest manifest) {
Attributes attributes = manifest.getMainAttributes();
String startClass = attributes.getValue(START_CLASS_ENTRY);
return getClass().getName().equals(startClass);
}
private Class<?>[] loadClasses(ClassLoader classLoader, String[] names)
throws ClassNotFoundException {
Class<?>[] classes = new Class<?>[names.length];
for (int i = 0; i < names.length; i++) {
classes[i] = classLoader.loadClass(names[i]);
}
return classes;
}
public static void main(String[] args) throws Exception {
new PackagedSpringApplicationLauncher().run(args);
}
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Class that are packaged as part of CLI generated JARs.
* @see org.springframework.boot.cli.command.archive.JarCommand
*/
package org.springframework.boot.cli.archive;

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command;
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
*/
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,81 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command;
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
* @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,117 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command;
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
*/
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,37 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command;
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
*/
@FunctionalInterface
public interface CommandFactory {
/**
* Returns the CLI {@link Command}s.
* @return The commands
*/
Collection<Command> getCommands();
}

View File

@@ -0,0 +1,304 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command;
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
* @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 -d argument
appArgsDetected |= "--".equals(arg);
if (("-d".equals(arg) || "--debug".equals(arg)) && !appArgsDetected) {
continue;
}
rtn.add(arg);
}
return rtn.toArray(new String[rtn.size()]);
}
/**
* 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) {
options = ((CommandException) ex).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 ? "Unexpected error" : message);
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", "-d, --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-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command;
/**
* 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-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command;
/**
* 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,32 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command;
/**
* Exception used to when the help command is called without arguments.
*
* @author Phillip Webb
*/
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,32 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command;
/**
* Exception used when a command is not found.
*
* @author Phillip Webb
*/
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-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command;
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
* @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,333 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.archive;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.jar.Manifest;
import groovy.lang.Grab;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.transform.ASTTransformation;
import org.springframework.boot.cli.app.SpringApplicationLauncher;
import org.springframework.boot.cli.archive.PackagedSpringApplicationLauncher;
import org.springframework.boot.cli.command.Command;
import org.springframework.boot.cli.command.OptionParsingCommand;
import org.springframework.boot.cli.command.archive.ResourceMatcher.MatchedResource;
import org.springframework.boot.cli.command.options.CompilerOptionHandler;
import org.springframework.boot.cli.command.options.OptionHandler;
import org.springframework.boot.cli.command.options.OptionSetGroovyCompilerConfiguration;
import org.springframework.boot.cli.command.options.SourceOptions;
import org.springframework.boot.cli.command.status.ExitStatus;
import org.springframework.boot.cli.compiler.GroovyCompiler;
import org.springframework.boot.cli.compiler.GroovyCompilerConfiguration;
import org.springframework.boot.cli.compiler.RepositoryConfigurationFactory;
import org.springframework.boot.cli.compiler.grape.RepositoryConfiguration;
import org.springframework.boot.loader.tools.JarWriter;
import org.springframework.boot.loader.tools.Layout;
import org.springframework.boot.loader.tools.Library;
import org.springframework.boot.loader.tools.LibraryScope;
import org.springframework.boot.loader.tools.Repackager;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.util.Assert;
/**
* Abstract {@link Command} to create a self-contained executable archive file from a CLI
* application.
*
* @author Andy Wilkinson
* @author Phillip Webb
* @author Andrey Stolyarov
* @author Henri Kerola
*/
abstract class ArchiveCommand extends OptionParsingCommand {
protected ArchiveCommand(String name, String description,
OptionHandler optionHandler) {
super(name, description, optionHandler);
}
@Override
public String getUsageHelp() {
return "[options] <" + getName() + "-name> <files>";
}
/**
* Abstract base {@link CompilerOptionHandler} for archive commands.
*/
protected abstract static class ArchiveOptionHandler extends CompilerOptionHandler {
private final String type;
private final Layout layout;
private OptionSpec<String> includeOption;
private OptionSpec<String> excludeOption;
public ArchiveOptionHandler(String type, Layout layout) {
this.type = type;
this.layout = layout;
}
protected Layout getLayout() {
return this.layout;
}
@Override
protected void doOptions() {
this.includeOption = option("include",
"Pattern applied to directories on the classpath to find files to "
+ "include in the resulting ").withRequiredArg()
.withValuesSeparatedBy(",").defaultsTo("");
this.excludeOption = option("exclude",
"Pattern applied to directories on the classpath to find files to "
+ "exclude from the resulting " + this.type).withRequiredArg()
.withValuesSeparatedBy(",").defaultsTo("");
}
@Override
protected ExitStatus run(OptionSet options) throws Exception {
List<?> nonOptionArguments = new ArrayList<Object>(
options.nonOptionArguments());
Assert.isTrue(nonOptionArguments.size() >= 2, "The name of the resulting "
+ this.type + " and at least one source file must be specified");
File output = new File((String) nonOptionArguments.remove(0));
Assert.isTrue(output.getName().toLowerCase().endsWith("." + this.type),
"The output '" + output + "' is not a " + this.type.toUpperCase()
+ " file.");
deleteIfExists(output);
GroovyCompiler compiler = createCompiler(options);
List<URL> classpath = getClassPathUrls(compiler);
List<MatchedResource> classpathEntries = findMatchingClasspathEntries(
classpath, options);
String[] sources = new SourceOptions(nonOptionArguments).getSourcesArray();
Class<?>[] compiledClasses = compiler.compile(sources);
List<URL> dependencies = getClassPathUrls(compiler);
dependencies.removeAll(classpath);
writeJar(output, compiledClasses, classpathEntries, dependencies);
return ExitStatus.OK;
}
private void deleteIfExists(File file) {
if (file.exists() && !file.delete()) {
throw new IllegalStateException(
"Failed to delete existing file " + file.getPath());
}
}
private GroovyCompiler createCompiler(OptionSet options) {
List<RepositoryConfiguration> repositoryConfiguration = RepositoryConfigurationFactory
.createDefaultRepositoryConfiguration();
GroovyCompilerConfiguration configuration = new OptionSetGroovyCompilerConfiguration(
options, this, repositoryConfiguration);
GroovyCompiler groovyCompiler = new GroovyCompiler(configuration);
groovyCompiler.getAstTransformations().add(0, new GrabAnnotationTransform());
return groovyCompiler;
}
private List<URL> getClassPathUrls(GroovyCompiler compiler) {
return new ArrayList<>(Arrays.asList(compiler.getLoader().getURLs()));
}
private List<MatchedResource> findMatchingClasspathEntries(List<URL> classpath,
OptionSet options) throws IOException {
ResourceMatcher matcher = new ResourceMatcher(
options.valuesOf(this.includeOption),
options.valuesOf(this.excludeOption));
List<File> roots = new ArrayList<>();
for (URL classpathEntry : classpath) {
roots.add(new File(URI.create(classpathEntry.toString())));
}
return matcher.find(roots);
}
private void writeJar(File file, Class<?>[] compiledClasses,
List<MatchedResource> classpathEntries, List<URL> dependencies)
throws FileNotFoundException, IOException, URISyntaxException {
final List<Library> libraries;
try (JarWriter writer = new JarWriter(file)) {
addManifest(writer, compiledClasses);
addCliClasses(writer);
for (Class<?> compiledClass : compiledClasses) {
addClass(writer, compiledClass);
}
libraries = addClasspathEntries(writer, classpathEntries);
}
libraries.addAll(createLibraries(dependencies));
Repackager repackager = new Repackager(file);
repackager.setMainClass(PackagedSpringApplicationLauncher.class.getName());
repackager.repackage((callback) -> {
for (Library library : libraries) {
callback.library(library);
}
});
}
private List<Library> createLibraries(List<URL> dependencies)
throws URISyntaxException {
List<Library> libraries = new ArrayList<>();
for (URL dependency : dependencies) {
File file = new File(dependency.toURI());
libraries.add(new Library(file, getLibraryScope(file)));
}
return libraries;
}
private void addManifest(JarWriter writer, Class<?>[] compiledClasses)
throws IOException {
Manifest manifest = new Manifest();
manifest.getMainAttributes().putValue("Manifest-Version", "1.0");
manifest.getMainAttributes().putValue(
PackagedSpringApplicationLauncher.SOURCE_ENTRY,
commaDelimitedClassNames(compiledClasses));
writer.writeManifest(manifest);
}
private String commaDelimitedClassNames(Class<?>[] classes) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < classes.length; i++) {
builder.append(i == 0 ? "" : ",");
builder.append(classes[i].getName());
}
return builder.toString();
}
protected void addCliClasses(JarWriter writer) throws IOException {
addClass(writer, PackagedSpringApplicationLauncher.class);
addClass(writer, SpringApplicationLauncher.class);
Resource[] resources = new PathMatchingResourcePatternResolver()
.getResources("org/springframework/boot/groovy/**");
for (Resource resource : resources) {
String url = resource.getURL().toString();
addResource(writer, resource,
url.substring(url.indexOf("org/springframework/boot/groovy/")));
}
}
protected final void addClass(JarWriter writer, Class<?> sourceClass)
throws IOException {
addClass(writer, sourceClass.getClassLoader(), sourceClass.getName());
}
protected final void addClass(JarWriter writer, ClassLoader classLoader,
String sourceClass) throws IOException {
if (classLoader == null) {
classLoader = Thread.currentThread().getContextClassLoader();
}
String name = sourceClass.replace('.', '/') + ".class";
InputStream stream = classLoader.getResourceAsStream(name);
writer.writeEntry(this.layout.getClassesLocation() + name, stream);
}
private void addResource(JarWriter writer, Resource resource, String name)
throws IOException {
InputStream stream = resource.getInputStream();
writer.writeEntry(name, stream);
}
private List<Library> addClasspathEntries(JarWriter writer,
List<MatchedResource> entries) throws IOException {
List<Library> libraries = new ArrayList<>();
for (MatchedResource entry : entries) {
if (entry.isRoot()) {
libraries.add(new Library(entry.getFile(), LibraryScope.COMPILE));
}
else {
writeClasspathEntry(writer, entry);
}
}
return libraries;
}
protected void writeClasspathEntry(JarWriter writer, MatchedResource entry)
throws IOException {
writer.writeEntry(entry.getName(), new FileInputStream(entry.getFile()));
}
protected abstract LibraryScope getLibraryScope(File file);
}
/**
* {@link ASTTransformation} to change {@code @Grab} annotation values.
*/
private static class GrabAnnotationTransform implements ASTTransformation {
@Override
public void visit(ASTNode[] nodes, SourceUnit source) {
for (ASTNode node : nodes) {
if (node instanceof ModuleNode) {
visitModule((ModuleNode) node);
}
}
}
private void visitModule(ModuleNode module) {
for (ClassNode classNode : module.getClasses()) {
AnnotationNode annotation = new AnnotationNode(new ClassNode(Grab.class));
annotation.addMember("value", new ConstantExpression("groovy"));
classNode.addAnnotation(annotation);
// We only need to do it at most once
break;
}
// Disable the addition of a static initializer that calls Grape.addResolver
// because all the dependencies are local now
disableGrabResolvers(module.getClasses());
disableGrabResolvers(module.getImports());
}
private void disableGrabResolvers(List<? extends AnnotatedNode> nodes) {
for (AnnotatedNode classNode : nodes) {
List<AnnotationNode> annotations = classNode.getAnnotations();
for (AnnotationNode node : new ArrayList<>(annotations)) {
if (node.getClassNode().getNameWithoutPackage()
.equals("GrabResolver")) {
node.setMember("initClass", new ConstantExpression(false));
}
}
}
}
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.archive;
import java.io.File;
import org.springframework.boot.cli.command.Command;
import org.springframework.boot.loader.tools.Layouts;
import org.springframework.boot.loader.tools.LibraryScope;
/**
* {@link Command} to create a self-contained executable jar file from a CLI application.
*
* @author Andy Wilkinson
* @author Phillip Webb
*/
public class JarCommand extends ArchiveCommand {
public JarCommand() {
super("jar", "Create a self-contained executable jar "
+ "file from a Spring Groovy script", new JarOptionHandler());
}
private static final class JarOptionHandler extends ArchiveOptionHandler {
JarOptionHandler() {
super("jar", new Layouts.Jar());
}
@Override
protected LibraryScope getLibraryScope(File file) {
return LibraryScope.COMPILE;
}
}
}

View File

@@ -0,0 +1,221 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.archive;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
/**
* Used to match resources for inclusion in a CLI application's jar file.
*
* @author Andy Wilkinson
*/
class ResourceMatcher {
private static final String[] DEFAULT_INCLUDES = { "public/**", "resources/**",
"static/**", "templates/**", "META-INF/**", "*" };
private static final String[] DEFAULT_EXCLUDES = { ".*", "repository/**", "build/**",
"target/**", "**/*.jar", "**/*.groovy" };
private final AntPathMatcher pathMatcher = new AntPathMatcher();
private final List<String> includes;
private final List<String> excludes;
ResourceMatcher(List<String> includes, List<String> excludes) {
this.includes = getOptions(includes, DEFAULT_INCLUDES);
this.excludes = getOptions(excludes, DEFAULT_EXCLUDES);
}
public List<MatchedResource> find(List<File> roots) throws IOException {
List<MatchedResource> matchedResources = new ArrayList<>();
for (File root : roots) {
if (root.isFile()) {
matchedResources.add(new MatchedResource(root));
}
else {
matchedResources.addAll(findInFolder(root));
}
}
return matchedResources;
}
private List<MatchedResource> findInFolder(File folder) throws IOException {
List<MatchedResource> matchedResources = new ArrayList<>();
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(
new FolderResourceLoader(folder));
for (String include : this.includes) {
for (Resource candidate : resolver.getResources(include)) {
File file = candidate.getFile();
if (file.isFile()) {
MatchedResource matchedResource = new MatchedResource(folder, file);
if (!isExcluded(matchedResource)) {
matchedResources.add(matchedResource);
}
}
}
}
return matchedResources;
}
private boolean isExcluded(MatchedResource matchedResource) {
for (String exclude : this.excludes) {
if (this.pathMatcher.match(exclude, matchedResource.getName())) {
return true;
}
}
return false;
}
private List<String> getOptions(List<String> values, String[] defaults) {
Set<String> result = new LinkedHashSet<>();
Set<String> minus = new LinkedHashSet<>();
boolean deltasFound = false;
for (String value : values) {
if (value.startsWith("+")) {
deltasFound = true;
value = value.substring(1);
result.add(value);
}
else if (value.startsWith("-")) {
deltasFound = true;
value = value.substring(1);
minus.add(value);
}
else if (!value.trim().isEmpty()) {
result.add(value);
}
}
for (String value : defaults) {
if (!minus.contains(value) || !deltasFound) {
result.add(value);
}
}
return new ArrayList<>(result);
}
/**
* {@link ResourceLoader} to get load resource from a folder.
*/
private static class FolderResourceLoader extends DefaultResourceLoader {
private final File rootFolder;
FolderResourceLoader(File root) throws MalformedURLException {
super(new FolderClassLoader(root));
this.rootFolder = root;
}
@Override
protected Resource getResourceByPath(String path) {
return new FileSystemResource(new File(this.rootFolder, path));
}
}
/**
* {@link ClassLoader} backed by a folder.
*/
private static class FolderClassLoader extends URLClassLoader {
FolderClassLoader(File rootFolder) throws MalformedURLException {
super(new URL[] { rootFolder.toURI().toURL() });
}
@Override
public Enumeration<URL> getResources(String name) throws IOException {
return findResources(name);
}
@Override
public URL getResource(String name) {
return findResource(name);
}
}
/**
* A single matched resource.
*/
public static final class MatchedResource {
private final File file;
private final String name;
private final boolean root;
private MatchedResource(File file) {
this.name = file.getName();
this.file = file;
this.root = this.name.endsWith(".jar");
}
private MatchedResource(File rootFolder, File file) {
this.name = StringUtils.cleanPath(file.getAbsolutePath()
.substring(rootFolder.getAbsolutePath().length() + 1));
this.file = file;
this.root = false;
}
private MatchedResource(File resourceFile, String path, boolean root) {
this.file = resourceFile;
this.name = path;
this.root = root;
}
public String getName() {
return this.name;
}
public File getFile() {
return this.file;
}
public boolean isRoot() {
return this.root;
}
@Override
public String toString() {
return this.file.getAbsolutePath();
}
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.archive;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import org.springframework.boot.cli.command.Command;
import org.springframework.boot.loader.tools.JarWriter;
import org.springframework.boot.loader.tools.Layouts;
import org.springframework.boot.loader.tools.LibraryScope;
/**
* {@link Command} to create a self-contained executable jar file from a CLI application.
*
* @author Andrey Stolyarov
* @author Phillip Webb
* @author Henri Kerola
* @since 1.3.0
*/
public class WarCommand extends ArchiveCommand {
public WarCommand() {
super("war", "Create a self-contained executable war "
+ "file from a Spring Groovy script", new WarOptionHandler());
}
private static final class WarOptionHandler extends ArchiveOptionHandler {
WarOptionHandler() {
super("war", new Layouts.War());
}
@Override
protected LibraryScope getLibraryScope(File file) {
String fileName = file.getName();
if (fileName.contains("tomcat-embed")
|| fileName.contains("spring-boot-starter-tomcat")) {
return LibraryScope.PROVIDED;
}
return LibraryScope.COMPILE;
}
@Override
protected void addCliClasses(JarWriter writer) throws IOException {
addClass(writer, null, "org.springframework.boot."
+ "cli.app.SpringApplicationWebApplicationInitializer");
super.addCliClasses(writer);
}
@Override
protected void writeClasspathEntry(JarWriter writer,
ResourceMatcher.MatchedResource entry) throws IOException {
writer.writeEntry(getLayout().getClassesLocation() + entry.getName(),
new FileInputStream(entry.getFile()));
}
}
}

View File

@@ -0,0 +1,125 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.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
*/
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 (final 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 ? "example:" : "examples:");
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,115 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.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
*/
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 ? 0 : Integer.valueOf(args[0]) - 1);
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,41 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.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
*/
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,69 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.grab;
import java.util.List;
import joptsimple.OptionSet;
import org.springframework.boot.cli.command.Command;
import org.springframework.boot.cli.command.OptionParsingCommand;
import org.springframework.boot.cli.command.options.CompilerOptionHandler;
import org.springframework.boot.cli.command.options.OptionSetGroovyCompilerConfiguration;
import org.springframework.boot.cli.command.options.SourceOptions;
import org.springframework.boot.cli.command.status.ExitStatus;
import org.springframework.boot.cli.compiler.GroovyCompiler;
import org.springframework.boot.cli.compiler.GroovyCompilerConfiguration;
import org.springframework.boot.cli.compiler.RepositoryConfigurationFactory;
import org.springframework.boot.cli.compiler.grape.RepositoryConfiguration;
/**
* {@link Command} to grab the dependencies of one or more Groovy scripts.
*
* @author Andy Wilkinson
*/
public class GrabCommand extends OptionParsingCommand {
public GrabCommand() {
super("grab", "Download a spring groovy script's dependencies to ./repository",
new GrabOptionHandler());
}
private static final class GrabOptionHandler extends CompilerOptionHandler {
@Override
protected ExitStatus run(OptionSet options) throws Exception {
SourceOptions sourceOptions = new SourceOptions(options);
List<RepositoryConfiguration> repositoryConfiguration = RepositoryConfigurationFactory
.createDefaultRepositoryConfiguration();
GroovyCompilerConfiguration configuration = new OptionSetGroovyCompilerConfiguration(
options, this, repositoryConfiguration);
if (System.getProperty("grape.root") == null) {
System.setProperty("grape.root", ".");
}
GroovyCompiler groovyCompiler = new GroovyCompiler(configuration);
groovyCompiler.compile(sourceOptions.getSourcesArray());
return ExitStatus.OK;
}
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.init;
/**
* Provide some basic information about a dependency.
*
* @author Stephane Nicoll
* @since 1.2.0
*/
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;
}
public String getId() {
return this.id;
}
public String getName() {
return this.name;
}
public String getDescription() {
return this.description;
}
}

View File

@@ -0,0 +1,274 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.init;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
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
* @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 {
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) {
this.serviceCapabilitiesReport = new ServiceCapabilitiesReportGenerator(
initializrService);
this.projectGenerator = new ProjectGenerator(initializrService);
}
@Override
protected void options() {
this.target = option(Arrays.asList("target"), "URL of the service to use")
.withRequiredArg()
.defaultsTo(ProjectGenerationRequest.DEFAULT_SERVICE_URL);
this.listCapabilities = option(Arrays.asList("list", "l"),
"List the capabilities of the service. Use it to discover the "
+ "dependencies and the types that are available");
projectGenerationOptions();
otherOptions();
}
private void projectGenerationOptions() {
this.groupId = option(Arrays.asList("groupId", "g"),
"Project coordinates (for example 'org.test')").withRequiredArg();
this.artifactId = option(Arrays.asList("artifactId", "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("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;
}
}
}

View File

@@ -0,0 +1,264 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.init;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.Charset;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHeaders;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.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
* @since 1.2.0
*/
class InitializrService {
private static final String FILENAME_HEADER_PREFIX = "filename=\"";
private static final Charset UTF_8 = Charset.forName("UTF-8");
/**
* 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
*/
public ProjectGenerationResponse generate(ProjectGenerationRequest request)
throws IOException {
Log.info("Using service at " + request.getServiceUrl());
InitializrServiceMetadata metadata = loadMetadata(request.getServiceUrl());
URI url = request.generateUrl(metadata);
CloseableHttpResponse httpResponse = executeProjectGenerationRequest(url);
HttpEntity httpEntity = httpResponse.getEntity();
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
*/
public 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
*/
public 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 : 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,254 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.init;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.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
* @since 1.2.0
*/
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
*/
public 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}
*/
public Dependency getDependency(String id) {
return this.dependencies.get(id);
}
/**
* Return the project types supported by the service.
* @return the supported project types
*/
public 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}
*/
public 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
*/
public 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) {
JSONObject child = (JSONObject) o;
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) {
result.put(key, (String) value);
}
}
return result;
}
private final static class MetadataHolder<K, T> {
private final Map<K, T> content;
private T defaultItem;
private MetadataHolder() {
this.content = new HashMap<>();
}
public Map<K, T> getContent() {
return this.content;
}
public T getDefaultItem() {
return this.defaultItem;
}
public void setDefaultItem(T defaultItem) {
this.defaultItem = defaultItem;
}
}
}

View File

@@ -0,0 +1,436 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.init;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.http.client.utils.URIBuilder;
import org.springframework.util.StringUtils;
/**
* Represent the settings to apply to generating the project.
*
* @author Stephane Nicoll
* @author Eddú Meléndez
* @since 1.2.0
*/
class ProjectGenerationRequest {
public static final String DEFAULT_SERVICE_URL = "https://start.spring.io";
private String serviceUrl = DEFAULT_SERVICE_URL;
private String output;
private 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 List<String> dependencies = new ArrayList<>();
/**
* The URL of the service to use.
* @return the service URL
* @see #DEFAULT_SERVICE_URL
*/
public String getServiceUrl() {
return this.serviceUrl;
}
public void setServiceUrl(String serviceUrl) {
this.serviceUrl = serviceUrl;
}
/**
* The location of the generated project.
* @return the location of the generated project
*/
public String getOutput() {
return this.output;
}
public 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 or not 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}
*/
public boolean isExtract() {
return this.extract;
}
public 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}
*/
public String getGroupId() {
return this.groupId;
}
public 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}
*/
public String getArtifactId() {
return this.artifactId;
}
public 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}
*/
public String getVersion() {
return this.version;
}
public 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}
*/
public String getName() {
return this.name;
}
public 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}
*/
public String getDescription() {
return this.description;
}
public 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}
*/
public String getPackageName() {
return this.packageName;
}
public void setPackageName(String packageName) {
this.packageName = packageName;
}
/**
* The type of project to generate. Should match one of the advertized type that the
* service supports. If not set, the default is retrieved from the service metadata.
* @return the project type
*/
public String getType() {
return this.type;
}
public 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}
*/
public String getPackaging() {
return this.packaging;
}
public void setPackaging(String packaging) {
this.packaging = packaging;
}
/**
* The build type to use. Ignored if a type is set. Can be used alongside the
* {@link #getFormat() format} to identify the type to use.
* @return the build type
*/
public String getBuild() {
return this.build;
}
public void setBuild(String build) {
this.build = build;
}
/**
* The project format to use. Ignored if a type is set. Can be used alongside the
* {@link #getBuild() build} to identify the type to use.
* @return the project format
*/
public String getFormat() {
return this.format;
}
public void setFormat(String format) {
this.format = format;
}
/**
* Whether or not the type should be detected based on the build and format value.
* @return {@code true} if type detection will be performed, otherwise {@code false}
*/
public boolean isDetectType() {
return this.detectType;
}
public 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}
*/
public String getJavaVersion() {
return this.javaVersion;
}
public 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}
*/
public String getLanguage() {
return this.language;
}
public 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}
*/
public String getBootVersion() {
return this.bootVersion;
}
public void setBootVersion(String bootVersion) {
this.bootVersion = bootVersion;
}
/**
* The identifiers of the dependencies to include in the project.
* @return the dependency identifiers
*/
public 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 e) {
throw new ReportableException("Invalid service URL (" + e.getMessage() + ")");
}
}
protected ProjectType determineProjectType(InitializrServiceMetadata metadata) {
if (this.type != null) {
ProjectType result = metadata.getProjectTypes().get(this.type);
if (result == null) {
throw new 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 : this.output.substring(0, i));
}
return null;
}
private static void filter(Map<String, ProjectType> projects, String tag,
String tagValue) {
for (Iterator<Map.Entry<String, ProjectType>> it = projects.entrySet()
.iterator(); it.hasNext();) {
Map.Entry<String, ProjectType> entry = it.next();
String value = entry.getValue().getTags().get(tag);
if (!tagValue.equals(value)) {
it.remove();
}
}
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.init;
import org.apache.http.entity.ContentType;
/**
* Represent the response of a {@link ProjectGenerationRequest}.
*
* @author Stephane Nicoll
* @since 1.2.0
*/
class ProjectGenerationResponse {
private 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
*/
public ContentType getContentType() {
return this.contentType;
}
/**
* The generated project archive or file.
* @return the content
*/
public byte[] getContent() {
return this.content;
}
public void setContent(byte[] content) {
this.content = content;
}
/**
* The preferred file name to use to store the entity on disk or {@code null} if no
* preferred value has been set.
* @return the file name, or {@code null}
*/
public String getFileName() {
return this.fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
}

View File

@@ -0,0 +1,165 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.init;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.springframework.boot.cli.util.Log;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StreamUtils;
/**
* 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;
}
public 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
if (isZipArchive(response) && request.getOutput() != null
&& !request.getOutput().contains(".")) {
return true;
}
return false;
}
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 outputFolder = (output != null ? new File(output)
: new File(System.getProperty("user.dir")));
if (!outputFolder.exists()) {
outputFolder.mkdirs();
}
try (ZipInputStream zipStream = new ZipInputStream(
new ByteArrayInputStream(entity.getContent()))) {
extractFromStream(zipStream, overwrite, outputFolder);
fixExecutableFlag(outputFolder, "mvnw");
fixExecutableFlag(outputFolder, "gradlew");
Log.info("Project extracted to '" + outputFolder.getAbsolutePath() + "'");
}
}
private void extractFromStream(ZipInputStream zipStream, boolean overwrite,
File outputFolder) throws IOException {
ZipEntry entry = zipStream.getNextEntry();
while (entry != null) {
File file = new File(outputFolder, entry.getName());
if (file.exists() && !overwrite) {
throw new ReportableException((file.isDirectory() ? "Directory" : "File")
+ " '" + file.getName()
+ "' already exists. Use --force if you want to overwrite or "
+ "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,72 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.init;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* Represent a project type that is supported by a service.
*
* @author Stephane Nicoll
* @since 1.2.0
*/
class ProjectType {
private final String id;
private final String name;
private final String action;
private final boolean defaultType;
private final Map<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);
}
}
public String getId() {
return this.id;
}
public String getName() {
return this.name;
}
public String getAction() {
return this.action;
}
public boolean isDefaultType() {
return this.defaultType;
}
public Map<String, String> getTags() {
return Collections.unmodifiableMap(this.tags);
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.init;
/**
* 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,155 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.init;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import 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
* @since 1.2.0
*/
class ServiceCapabilitiesReportGenerator {
private static final String NEW_LINE = System.getProperty("line.separator");
private final InitializrService initializrService;
/**
* Creates an instance using the specified {@link 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
*/
public String generate(String url) throws IOException {
Object content = this.initializrService.loadServiceCapabilities(url);
if (content instanceof InitializrServiceMetadata) {
return generateHelp(url, (InitializrServiceMetadata) content);
}
return content.toString();
}
private String generateHelp(String url, InitializrServiceMetadata metadata) {
String header = "Capabilities of " + url;
StringBuilder report = new StringBuilder();
report.append(repeat("=", header.length()) + NEW_LINE);
report.append(header + NEW_LINE);
report.append(repeat("=", header.length()) + 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:" + NEW_LINE);
report.append("-----------------------" + NEW_LINE);
List<Dependency> dependencies = getSortedDependencies(metadata);
for (Dependency dependency : dependencies) {
report.append(dependency.getId() + " - " + dependency.getName());
if (dependency.getDescription() != null) {
report.append(": " + dependency.getDescription());
}
report.append(NEW_LINE);
}
}
private List<Dependency> getSortedDependencies(InitializrServiceMetadata metadata) {
ArrayList<Dependency> dependencies = new ArrayList<>(metadata.getDependencies());
dependencies.sort(Comparator.comparing(Dependency::getId));
return dependencies;
}
private void reportAvailableProjectTypes(InitializrServiceMetadata metadata,
StringBuilder report) {
report.append("Available project types:" + NEW_LINE);
report.append("------------------------" + NEW_LINE);
SortedSet<Entry<String, ProjectType>> entries = new TreeSet<>(
Comparator.comparing(Entry::getKey));
entries.addAll(metadata.getProjectTypes().entrySet());
for (Entry<String, ProjectType> entry : entries) {
ProjectType type = entry.getValue();
report.append(entry.getKey() + " - " + type.getName());
if (!type.getTags().isEmpty()) {
reportTags(report, type);
}
if (type.isDefaultType()) {
report.append(" (default)");
}
report.append(NEW_LINE);
}
}
private void reportTags(StringBuilder report, ProjectType type) {
Map<String, String> tags = type.getTags();
Iterator<Map.Entry<String, String>> iterator = tags.entrySet().iterator();
report.append(" [");
while (iterator.hasNext()) {
Map.Entry<String, String> entry = iterator.next();
report.append(entry.getKey() + ":" + entry.getValue());
if (iterator.hasNext()) {
report.append(", ");
}
}
report.append("]");
}
private void reportDefaults(StringBuilder report,
InitializrServiceMetadata metadata) {
report.append("Defaults:" + NEW_LINE);
report.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 + ": " + defaultsValue + NEW_LINE);
}
}
private static String repeat(String s, int count) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < count; i++) {
sb.append(s);
}
return sb.toString();
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.install;
import java.io.File;
import java.util.List;
/**
* Resolve artifact identifiers (typically in the form {@literal group:artifact:version})
* to {@link File}s.
*
* @author Andy Wilkinson
* @since 1.2.0
*/
@FunctionalInterface
interface DependencyResolver {
/**
* Resolves the given {@code artifactIdentifiers}, typically in the form
* "group:artifact:version", and their dependencies.
* @param artifactIdentifiers The artifacts to resolve
* @return The {@code File}s for the resolved artifacts
* @throws Exception if dependency resolution fails
*/
List<File> resolve(List<String> artifactIdentifiers) throws Exception;
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.install;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.codehaus.groovy.control.CompilationFailedException;
import org.springframework.boot.cli.compiler.GroovyCompiler;
import org.springframework.boot.cli.compiler.GroovyCompilerConfiguration;
/**
* A {@code DependencyResolver} implemented using Groovy's {@code @Grab}.
*
* @author Dave Syer
* @author Andy Wilkinson
*/
class GroovyGrabDependencyResolver implements DependencyResolver {
private final GroovyCompilerConfiguration configuration;
GroovyGrabDependencyResolver(GroovyCompilerConfiguration configuration) {
this.configuration = configuration;
}
@Override
public List<File> resolve(List<String> artifactIdentifiers)
throws CompilationFailedException, IOException {
GroovyCompiler groovyCompiler = new GroovyCompiler(this.configuration);
List<File> artifactFiles = new ArrayList<>();
if (!artifactIdentifiers.isEmpty()) {
List<URL> initialUrls = getClassPathUrls(groovyCompiler);
groovyCompiler.compile(createSources(artifactIdentifiers));
List<URL> artifactUrls = getClassPathUrls(groovyCompiler);
artifactUrls.removeAll(initialUrls);
for (URL artifactUrl : artifactUrls) {
artifactFiles.add(toFile(artifactUrl));
}
}
return artifactFiles;
}
private List<URL> getClassPathUrls(GroovyCompiler compiler) {
return new ArrayList<>(Arrays.asList(compiler.getLoader().getURLs()));
}
private String createSources(List<String> artifactIdentifiers) throws IOException {
File file = File.createTempFile("SpringCLIDependency", ".groovy");
file.deleteOnExit();
try (OutputStreamWriter stream = new OutputStreamWriter(
new FileOutputStream(file), "UTF-8")) {
for (String artifactIdentifier : artifactIdentifiers) {
stream.write("@Grab('" + artifactIdentifier + "')");
}
// Dummy class to force compiler to do grab
stream.write("class Installer {}");
}
// Windows paths get tricky unless you work with URI
return file.getAbsoluteFile().toURI().toString();
}
private File toFile(URL url) {
try {
return new File(url.toURI());
}
catch (URISyntaxException ex) {
return new File(url.getPath());
}
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.install;
import java.util.List;
import joptsimple.OptionSet;
import org.springframework.boot.cli.command.Command;
import org.springframework.boot.cli.command.OptionParsingCommand;
import org.springframework.boot.cli.command.options.CompilerOptionHandler;
import org.springframework.boot.cli.command.status.ExitStatus;
import org.springframework.boot.cli.util.Log;
import org.springframework.util.Assert;
/**
* {@link Command} to install additional dependencies into the CLI.
*
* @author Dave Syer
* @author Andy Wilkinson
* @since 1.2.0
*/
public class InstallCommand extends OptionParsingCommand {
public InstallCommand() {
super("install", "Install dependencies to the lib/ext directory",
new InstallOptionHandler());
}
@Override
public String getUsageHelp() {
return "[options] <coordinates>";
}
private static final class InstallOptionHandler extends CompilerOptionHandler {
@Override
@SuppressWarnings("unchecked")
protected ExitStatus run(OptionSet options) throws Exception {
List<String> args = (List<String>) options.nonOptionArguments();
Assert.notEmpty(args, "Please specify at least one "
+ "dependency, in the form group:artifact:version, to install");
try {
new Installer(options, this).install(args);
}
catch (Exception ex) {
String message = ex.getMessage();
Log.error(message != null ? message : ex.getClass().toString());
}
return ExitStatus.OK;
}
}
}

View File

@@ -0,0 +1,163 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.install;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.List;
import java.util.Properties;
import joptsimple.OptionSet;
import org.springframework.boot.cli.command.options.CompilerOptionHandler;
import org.springframework.boot.cli.command.options.OptionSetGroovyCompilerConfiguration;
import org.springframework.boot.cli.compiler.GroovyCompilerConfiguration;
import org.springframework.boot.cli.compiler.RepositoryConfigurationFactory;
import org.springframework.boot.cli.compiler.grape.RepositoryConfiguration;
import org.springframework.boot.cli.util.Log;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.SystemPropertyUtils;
/**
* Shared logic for the {@link InstallCommand} and {@link UninstallCommand}.
*
* @author Andy Wilkinson
*/
class Installer {
private final DependencyResolver dependencyResolver;
private final Properties installCounts;
Installer(OptionSet options, CompilerOptionHandler compilerOptionHandler)
throws IOException {
this(new GroovyGrabDependencyResolver(
createCompilerConfiguration(options, compilerOptionHandler)));
}
Installer(DependencyResolver resolver) throws IOException {
this.dependencyResolver = resolver;
this.installCounts = loadInstallCounts();
}
private static GroovyCompilerConfiguration createCompilerConfiguration(
OptionSet options, CompilerOptionHandler compilerOptionHandler) {
List<RepositoryConfiguration> repositoryConfiguration = RepositoryConfigurationFactory
.createDefaultRepositoryConfiguration();
return new OptionSetGroovyCompilerConfiguration(options, compilerOptionHandler,
repositoryConfiguration) {
@Override
public boolean isAutoconfigure() {
return false;
}
};
}
private Properties loadInstallCounts() throws IOException {
Properties properties = new Properties();
File installed = getInstalled();
if (installed.exists()) {
FileReader reader = new FileReader(installed);
properties.load(reader);
reader.close();
}
return properties;
}
private void saveInstallCounts() throws IOException {
try (FileWriter writer = new FileWriter(getInstalled())) {
this.installCounts.store(writer, null);
}
}
public void install(List<String> artifactIdentifiers) throws Exception {
File extDirectory = getDefaultExtDirectory();
extDirectory.mkdirs();
Log.info("Installing into: " + extDirectory);
List<File> artifactFiles = this.dependencyResolver.resolve(artifactIdentifiers);
for (File artifactFile : artifactFiles) {
int installCount = getInstallCount(artifactFile);
if (installCount == 0) {
FileCopyUtils.copy(artifactFile,
new File(extDirectory, artifactFile.getName()));
}
setInstallCount(artifactFile, installCount + 1);
}
saveInstallCounts();
}
private int getInstallCount(File file) {
String countString = this.installCounts.getProperty(file.getName());
if (countString == null) {
return 0;
}
return Integer.valueOf(countString);
}
private void setInstallCount(File file, int count) {
if (count == 0) {
this.installCounts.remove(file.getName());
}
else {
this.installCounts.setProperty(file.getName(), Integer.toString(count));
}
}
public void uninstall(List<String> artifactIdentifiers) throws Exception {
File extDirectory = getDefaultExtDirectory();
Log.info("Uninstalling from: " + extDirectory);
List<File> artifactFiles = this.dependencyResolver.resolve(artifactIdentifiers);
for (File artifactFile : artifactFiles) {
int installCount = getInstallCount(artifactFile);
if (installCount <= 1) {
new File(extDirectory, artifactFile.getName()).delete();
}
setInstallCount(artifactFile, installCount - 1);
}
saveInstallCounts();
}
public void uninstallAll() throws Exception {
File extDirectory = getDefaultExtDirectory();
Log.info("Uninstalling from: " + extDirectory);
for (String name : this.installCounts.stringPropertyNames()) {
new File(extDirectory, name).delete();
}
this.installCounts.clear();
saveInstallCounts();
}
private File getDefaultExtDirectory() {
String home = SystemPropertyUtils
.resolvePlaceholders("${spring.home:${SPRING_HOME:.}}");
File extDirectory = new File(new File(home, "lib"), "ext");
if (!extDirectory.isDirectory()) {
if (!extDirectory.mkdirs()) {
throw new IllegalStateException(
"Failed to create ext directory " + extDirectory);
}
}
return extDirectory;
}
private File getInstalled() {
return new File(getDefaultExtDirectory(), ".installed");
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.install;
import java.util.List;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.springframework.boot.cli.command.Command;
import org.springframework.boot.cli.command.OptionParsingCommand;
import org.springframework.boot.cli.command.options.CompilerOptionHandler;
import org.springframework.boot.cli.command.status.ExitStatus;
import org.springframework.boot.cli.util.Log;
/**
* {@link Command} to uninstall dependencies from the CLI's lib/ext directory.
*
* @author Dave Syer
* @author Andy Wilkinson
* @since 1.2.0
*/
public class UninstallCommand extends OptionParsingCommand {
public UninstallCommand() {
super("uninstall", "Uninstall dependencies from the lib/ext directory",
new UninstallOptionHandler());
}
@Override
public String getUsageHelp() {
return "[options] <coordinates>";
}
private static class UninstallOptionHandler extends CompilerOptionHandler {
private OptionSpec<Void> allOption;
@Override
protected void doOptions() {
this.allOption = option("all", "Uninstall all");
}
@Override
@SuppressWarnings("unchecked")
protected ExitStatus run(OptionSet options) throws Exception {
List<String> args = (List<String>) options.nonOptionArguments();
try {
if (options.has(this.allOption)) {
if (!args.isEmpty()) {
throw new IllegalArgumentException(
"Please use --all without specifying any dependencies");
}
new Installer(options, this).uninstallAll();
}
if (args.isEmpty()) {
throw new IllegalArgumentException(
"Please specify at least one dependency, in the form group:artifact:version, to uninstall");
}
new Installer(options, this).uninstall(args);
}
catch (Exception ex) {
String message = ex.getMessage();
Log.error(message != null ? message : ex.getClass().toString());
}
return ExitStatus.OK;
}
}
}

View File

@@ -0,0 +1,73 @@
/*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.options;
import java.util.Arrays;
import joptsimple.OptionSpec;
/**
* An {@link OptionHandler} for commands that result in the compilation of one or more
* Groovy scripts.
*
* @author Andy Wilkinson
* @author Dave Syer
*/
public class CompilerOptionHandler extends OptionHandler {
private OptionSpec<Void> noGuessImportsOption;
private OptionSpec<Void> noGuessDependenciesOption;
private OptionSpec<Boolean> autoconfigureOption;
private OptionSpec<String> classpathOption;
@Override
protected final void options() {
this.noGuessImportsOption = option("no-guess-imports",
"Do not attempt to guess imports");
this.noGuessDependenciesOption = option("no-guess-dependencies",
"Do not attempt to guess dependencies");
this.autoconfigureOption = option("autoconfigure",
"Add autoconfigure compiler transformations").withOptionalArg()
.ofType(Boolean.class).defaultsTo(true);
this.classpathOption = option(Arrays.asList("classpath", "cp"),
"Additional classpath entries").withRequiredArg();
doOptions();
}
protected void doOptions() {
}
public OptionSpec<Void> getNoGuessImportsOption() {
return this.noGuessImportsOption;
}
public OptionSpec<Void> getNoGuessDependenciesOption() {
return this.noGuessDependenciesOption;
}
public OptionSpec<String> getClasspathOption() {
return this.classpathOption;
}
public OptionSpec<Boolean> getAutoconfigureOption() {
return this.autoconfigureOption;
}
}

View File

@@ -0,0 +1,181 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.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 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
* @see OptionParsingCommand
* @see #run(OptionSet)
*/
public class OptionHandler {
private OptionParser parser;
private String help;
private Collection<OptionHelp> optionHelp;
public OptionSpecBuilder option(String name, String description) {
return getParser().accepts(name, description);
}
public OptionSpecBuilder option(Collection<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";
}
}
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 "";
}
public Collection<OptionHelp> getOptionHelp() {
return Collections.unmodifiableList(this.help);
}
}
private static class OptionHelpAdapter implements OptionHelp {
private final LinkedHashSet<String> options;
private final String description;
OptionHelpAdapter(OptionDescriptor descriptor) {
this.options = new LinkedHashSet<>();
for (String option : descriptor.options()) {
this.options.add((option.length() == 1 ? "-" : "--") + 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,40 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.options;
import java.util.Set;
/**
* Help for a specific option.
*
* @author Phillip Webb
*/
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,100 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.options;
import java.util.List;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.springframework.boot.cli.compiler.GroovyCompilerConfiguration;
import org.springframework.boot.cli.compiler.GroovyCompilerScope;
import org.springframework.boot.cli.compiler.RepositoryConfigurationFactory;
import org.springframework.boot.cli.compiler.grape.RepositoryConfiguration;
/**
* Simple adapter class to present an {@link OptionSet} as a
* {@link GroovyCompilerConfiguration}.
*
* @author Andy Wilkinson
*/
public class OptionSetGroovyCompilerConfiguration implements GroovyCompilerConfiguration {
private final OptionSet options;
private final CompilerOptionHandler optionHandler;
private final List<RepositoryConfiguration> repositoryConfiguration;
protected OptionSetGroovyCompilerConfiguration(OptionSet optionSet,
CompilerOptionHandler compilerOptionHandler) {
this(optionSet, compilerOptionHandler,
RepositoryConfigurationFactory.createDefaultRepositoryConfiguration());
}
public OptionSetGroovyCompilerConfiguration(OptionSet optionSet,
CompilerOptionHandler compilerOptionHandler,
List<RepositoryConfiguration> repositoryConfiguration) {
this.options = optionSet;
this.optionHandler = compilerOptionHandler;
this.repositoryConfiguration = repositoryConfiguration;
}
protected OptionSet getOptions() {
return this.options;
}
@Override
public GroovyCompilerScope getScope() {
return GroovyCompilerScope.DEFAULT;
}
@Override
public boolean isGuessImports() {
return !this.options.has(this.optionHandler.getNoGuessImportsOption());
}
@Override
public boolean isGuessDependencies() {
return !this.options.has(this.optionHandler.getNoGuessDependenciesOption());
}
@Override
public boolean isAutoconfigure() {
return this.optionHandler.getAutoconfigureOption().value(this.options);
}
@Override
public String[] getClasspath() {
OptionSpec<String> classpathOption = this.optionHandler.getClasspathOption();
if (this.options.has(classpathOption)) {
return this.options.valueOf(classpathOption).split(":");
}
return DEFAULT_CLASSPATH;
}
@Override
public List<RepositoryConfiguration> getRepositoryConfiguration() {
return this.repositoryConfiguration;
}
@Override
public boolean isQuiet() {
return false;
}
}

View File

@@ -0,0 +1,139 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.options;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import joptsimple.OptionSet;
import org.springframework.boot.cli.util.ResourceUtils;
import org.springframework.util.Assert;
/**
* Extract source file options (anything following '--' in an {@link OptionSet}).
*
* @author Phillip Webb
* @author Dave Syer
* @author Greg Turnquist
* @author Andy Wilkinson
*/
public class SourceOptions {
private final List<String> sources;
private final List<?> args;
/**
* Create a new {@link SourceOptions} instance.
* @param options the source option set
*/
public SourceOptions(OptionSet options) {
this(options, null);
}
/**
* Create a new {@link SourceOptions} instance.
* @param arguments the source arguments
*/
public SourceOptions(List<?> arguments) {
this(arguments, null);
}
/**
* Create a new {@link SourceOptions} instance. If it is an error to pass options that
* specify non-existent sources, but the default paths are allowed not to exist (the
* paths are tested before use). If default paths are provided and the option set
* contains no source file arguments it is not an error even if none of the default
* paths exist).
* @param optionSet the source option set
* @param classLoader an optional classloader used to try and load files that are not
* found in the local filesystem
*/
public SourceOptions(OptionSet optionSet, ClassLoader classLoader) {
this(optionSet.nonOptionArguments(), classLoader);
}
private SourceOptions(List<?> nonOptionArguments, ClassLoader classLoader) {
List<String> sources = new ArrayList<>();
int sourceArgCount = 0;
for (Object option : nonOptionArguments) {
if (option instanceof String) {
String filename = (String) option;
if ("--".equals(filename)) {
break;
}
List<String> urls = new ArrayList<>();
File fileCandidate = new File(filename);
if (fileCandidate.isFile()) {
urls.add(fileCandidate.getAbsoluteFile().toURI().toString());
}
else if (!isAbsoluteWindowsFile(fileCandidate)) {
urls.addAll(ResourceUtils.getUrls(filename, classLoader));
}
for (String url : urls) {
if (isSource(url)) {
sources.add(url);
}
}
if (isSource(filename)) {
if (urls.isEmpty()) {
throw new IllegalArgumentException("Can't find " + filename);
}
else {
sourceArgCount++;
}
}
}
}
this.args = Collections.unmodifiableList(
nonOptionArguments.subList(sourceArgCount, nonOptionArguments.size()));
Assert.isTrue(!sources.isEmpty(), "Please specify at least one file");
this.sources = Collections.unmodifiableList(sources);
}
private boolean isAbsoluteWindowsFile(File file) {
return isWindows() && file.isAbsolute();
}
private boolean isWindows() {
return File.separatorChar == '\\';
}
private boolean isSource(String name) {
return name.endsWith(".java") || name.endsWith(".groovy");
}
public List<?> getArgs() {
return this.args;
}
public String[] getArgsArray() {
return this.args.toArray(new String[this.args.size()]);
}
public List<String> getSources() {
return this.sources;
}
public String[] getSourcesArray() {
return this.sources.toArray(new String[this.sources.size()]);
}
}

View File

@@ -0,0 +1,161 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.run;
import java.io.File;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.springframework.boot.cli.command.Command;
import org.springframework.boot.cli.command.OptionParsingCommand;
import org.springframework.boot.cli.command.options.CompilerOptionHandler;
import org.springframework.boot.cli.command.options.OptionSetGroovyCompilerConfiguration;
import org.springframework.boot.cli.command.options.SourceOptions;
import org.springframework.boot.cli.command.status.ExitStatus;
import org.springframework.boot.cli.compiler.GroovyCompilerScope;
import org.springframework.boot.cli.compiler.RepositoryConfigurationFactory;
import org.springframework.boot.cli.compiler.grape.RepositoryConfiguration;
/**
* {@link Command} to 'run' a groovy script or scripts.
*
* @author Phillip Webb
* @author Dave Syer
* @author Andy Wilkinson
* @see SpringApplicationRunner
*/
public class RunCommand extends OptionParsingCommand {
public RunCommand() {
super("run", "Run a spring groovy script", new RunOptionHandler());
}
@Override
public String getUsageHelp() {
return "[options] <files> [--] [args]";
}
public void stop() {
if (this.getHandler() != null) {
((RunOptionHandler) this.getHandler()).stop();
}
}
private static class RunOptionHandler extends CompilerOptionHandler {
private final Object monitor = new Object();
private OptionSpec<Void> watchOption;
private OptionSpec<Void> verboseOption;
private OptionSpec<Void> quietOption;
private SpringApplicationRunner runner;
@Override
protected void doOptions() {
this.watchOption = option("watch", "Watch the specified file for changes");
this.verboseOption = option(Arrays.asList("verbose", "v"),
"Verbose logging of dependency resolution");
this.quietOption = option(Arrays.asList("quiet", "q"), "Quiet logging");
}
public void stop() {
synchronized (this.monitor) {
if (this.runner != null) {
this.runner.stop();
}
this.runner = null;
}
}
@Override
protected synchronized ExitStatus run(OptionSet options) throws Exception {
synchronized (this.monitor) {
if (this.runner != null) {
throw new RuntimeException(
"Already running. Please stop the current application before running another (use the 'stop' command).");
}
SourceOptions sourceOptions = new SourceOptions(options);
List<RepositoryConfiguration> repositoryConfiguration = RepositoryConfigurationFactory
.createDefaultRepositoryConfiguration();
repositoryConfiguration.add(0, new RepositoryConfiguration("local",
new File("repository").toURI(), true));
SpringApplicationRunnerConfiguration configuration = new SpringApplicationRunnerConfigurationAdapter(
options, this, repositoryConfiguration);
this.runner = new SpringApplicationRunner(configuration,
sourceOptions.getSourcesArray(), sourceOptions.getArgsArray());
this.runner.compileAndRun();
return ExitStatus.OK;
}
}
/**
* Simple adapter class to present the {@link OptionSet} as a
* {@link SpringApplicationRunnerConfiguration}.
*/
private class SpringApplicationRunnerConfigurationAdapter
extends OptionSetGroovyCompilerConfiguration
implements SpringApplicationRunnerConfiguration {
SpringApplicationRunnerConfigurationAdapter(OptionSet options,
CompilerOptionHandler optionHandler,
List<RepositoryConfiguration> repositoryConfiguration) {
super(options, optionHandler, repositoryConfiguration);
}
@Override
public GroovyCompilerScope getScope() {
return GroovyCompilerScope.DEFAULT;
}
@Override
public boolean isWatchForFileChanges() {
return getOptions().has(RunOptionHandler.this.watchOption);
}
@Override
public Level getLogLevel() {
if (isQuiet()) {
return Level.OFF;
}
if (getOptions().has(RunOptionHandler.this.verboseOption)) {
return Level.FINEST;
}
return Level.INFO;
}
@Override
public boolean isQuiet() {
return getOptions().has(RunOptionHandler.this.quietOption);
}
}
}
}

View File

@@ -0,0 +1,278 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.run;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import org.springframework.boot.cli.app.SpringApplicationLauncher;
import org.springframework.boot.cli.compiler.GroovyCompiler;
import org.springframework.boot.cli.util.ResourceUtils;
/**
* Compiles Groovy code running the resulting classes using a {@code SpringApplication}.
* Takes care of threading and class-loading issues and can optionally monitor sources for
* changes.
*
* @author Phillip Webb
* @author Dave Syer
*/
public class SpringApplicationRunner {
private static int watcherCounter = 0;
private static int runnerCounter = 0;
private final Object monitor = new Object();
private final SpringApplicationRunnerConfiguration configuration;
private final String[] sources;
private final String[] args;
private final GroovyCompiler compiler;
private RunThread runThread;
private FileWatchThread fileWatchThread;
/**
* Create a new {@link SpringApplicationRunner} instance.
* @param configuration the configuration
* @param sources the files to compile/watch
* @param args input arguments
*/
SpringApplicationRunner(final SpringApplicationRunnerConfiguration configuration,
String[] sources, String... args) {
this.configuration = configuration;
this.sources = sources.clone();
this.args = args.clone();
this.compiler = new GroovyCompiler(configuration);
int level = configuration.getLogLevel().intValue();
if (level <= Level.FINER.intValue()) {
System.setProperty(
"org.springframework.boot.cli.compiler.grape.ProgressReporter",
"detail");
System.setProperty("trace", "true");
}
else if (level <= Level.FINE.intValue()) {
System.setProperty("debug", "true");
}
else if (level == Level.OFF.intValue()) {
System.setProperty("spring.main.banner-mode", "OFF");
System.setProperty("logging.level.ROOT", "OFF");
System.setProperty(
"org.springframework.boot.cli.compiler.grape.ProgressReporter",
"none");
}
}
/**
* Compile and run the application.
* @throws Exception on error
*/
public void compileAndRun() throws Exception {
synchronized (this.monitor) {
try {
stop();
Class<?>[] compiledSources = compile();
monitorForChanges();
// Run in new thread to ensure that the context classloader is setup
this.runThread = new RunThread(compiledSources);
this.runThread.start();
this.runThread.join();
}
catch (Exception ex) {
if (this.fileWatchThread == null) {
throw ex;
}
else {
ex.printStackTrace();
}
}
}
}
public void stop() {
synchronized (this.monitor) {
if (this.runThread != null) {
this.runThread.shutdown();
this.runThread = null;
}
}
}
private Class<?>[] compile() throws IOException {
Class<?>[] compiledSources = this.compiler.compile(this.sources);
if (compiledSources.length == 0) {
throw new RuntimeException(
"No classes found in '" + Arrays.toString(this.sources) + "'");
}
return compiledSources;
}
private void monitorForChanges() {
if (this.fileWatchThread == null && this.configuration.isWatchForFileChanges()) {
this.fileWatchThread = new FileWatchThread();
this.fileWatchThread.start();
}
}
/**
* Thread used to launch the Spring Application with the correct context classloader.
*/
private class RunThread extends Thread {
private final Object monitor = new Object();
private final Class<?>[] compiledSources;
private Object applicationContext;
/**
* Create a new {@link RunThread} instance.
* @param compiledSources the sources to launch
*/
RunThread(Class<?>... compiledSources) {
super("runner-" + (runnerCounter++));
this.compiledSources = compiledSources;
if (compiledSources.length != 0) {
setContextClassLoader(compiledSources[0].getClassLoader());
}
setDaemon(true);
}
@Override
public void run() {
synchronized (this.monitor) {
try {
this.applicationContext = new SpringApplicationLauncher(
getContextClassLoader()).launch(this.compiledSources,
SpringApplicationRunner.this.args);
}
catch (Exception ex) {
ex.printStackTrace();
}
}
}
/**
* Shutdown the thread, closing any previously opened application context.
*/
public void shutdown() {
synchronized (this.monitor) {
if (this.applicationContext != null) {
try {
Method method = this.applicationContext.getClass()
.getMethod("close");
method.invoke(this.applicationContext);
}
catch (NoSuchMethodException ex) {
// Not an application context that we can close
}
catch (Exception ex) {
ex.printStackTrace();
}
finally {
this.applicationContext = null;
}
}
}
}
}
/**
* Thread to watch for file changes and trigger recompile/reload.
*/
private class FileWatchThread extends Thread {
private long previous;
private List<File> sources;
FileWatchThread() {
super("filewatcher-" + (watcherCounter++));
this.previous = 0;
this.sources = getSourceFiles();
for (File file : this.sources) {
if (file.exists()) {
long current = file.lastModified();
if (current > this.previous) {
this.previous = current;
}
}
}
setDaemon(false);
}
private List<File> getSourceFiles() {
List<File> sources = new ArrayList<>();
for (String source : SpringApplicationRunner.this.sources) {
List<String> paths = ResourceUtils.getUrls(source,
SpringApplicationRunner.this.compiler.getLoader());
for (String path : paths) {
try {
URL url = new URL(path);
if ("file".equals(url.getProtocol())) {
sources.add(new File(url.getFile()));
}
}
catch (MalformedURLException ex) {
// Ignore
}
}
}
return sources;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(TimeUnit.SECONDS.toMillis(1));
for (File file : this.sources) {
if (file.exists()) {
long current = file.lastModified();
if (this.previous < current) {
this.previous = current;
compileAndRun();
}
}
}
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
catch (Exception ex) {
// Swallow, will be reported by compileAndRun
}
}
}
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.run;
import java.util.logging.Level;
import org.springframework.boot.cli.compiler.GroovyCompilerConfiguration;
/**
* Configuration for the {@link SpringApplicationRunner}.
*
* @author Phillip Webb
*/
public interface SpringApplicationRunnerConfiguration
extends GroovyCompilerConfiguration {
/**
* Returns {@code true} if the source file should be monitored for changes and
* automatically recompiled.
* @return {@code true} if file watching should be performed, otherwise {@code false}
*/
boolean isWatchForFileChanges();
/**
* Returns the logging level to use.
* @return the logging level
*/
Level getLogLevel();
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.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-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.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,149 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.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;
/**
* JLine {@link Completer} for Spring Boot {@link Command}s.
*
* @author Jon Brisbin
* @author Phillip Webb
*/
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 (!"".equals(commandName.trim())) {
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) {
StringBuilder options = new StringBuilder();
for (String option : optionHelp.getOptions()) {
options.append(options.length() == 0 ? "" : ", ");
options.append(option);
}
this.options = options.toString();
this.usage = optionHelp.getUsageHelp();
}
public String getOptions() {
return this.options;
}
public String getUsage() {
return this.usage;
}
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.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;
}
public 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-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.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-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.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,51 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.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
*/
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,65 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.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;
/**
* 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, args.toArray(new String[args.size()]));
if (code == 0) {
return ExitStatus.OK;
}
else {
return new ExitStatus(code, "EXTERNAL_ERROR");
}
}
public boolean handleSigInt() {
return this.process.handleSigInt();
}
}

View File

@@ -0,0 +1,233 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.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
*/
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);
}
public 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) {
}
public boolean handleSigInt() {
Command command = this.lastCommand;
if (command != null && command instanceof RunProcessCommand) {
return ((RunProcessCommand) command).handleSigInt();
}
return false;
}
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.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
* @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,34 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.shell;
import org.springframework.boot.cli.command.CommandException;
/**
* Exception used to stop the {@link Shell}.
*
* @author Phillip Webb
*/
public class ShellExitException extends CommandException {
private static final long serialVersionUID = 1L;
public ShellExitException() {
super(Option.RETHROW);
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.shell;
import java.util.Stack;
/**
* Abstraction to manage a stack of prompts.
*
* @author Phillip Webb
*/
public class ShellPrompts {
private static final String DEFAULT_PROMPT = "$ ";
private final Stack<String> prompts = new Stack<>();
/**
* 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,104 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.command.status;
/**
* Encapsulation of the outcome of a command.
*
* @author Dave Syer
* @see ExitStatus#OK
* @see ExitStatus#ERROR
*
*/
public final class ExitStatus {
/**
* Generic "OK" exit status with zero exit code and {@literal hangup=false}.
*/
public static ExitStatus OK = new ExitStatus(0, "OK");
/**
* Generic "not OK" exit status with non-zero exit code and {@literal hangup=true}.
*/
public static 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,135 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.compiler;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassCodeVisitorSupport;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ImportNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.transform.ASTTransformation;
/**
* A base class for {@link ASTTransformation AST transformations} that are solely
* interested in {@link AnnotatedNode AnnotatedNodes}.
*
* @author Andy Wilkinson
* @since 1.1.0
*/
public abstract class AnnotatedNodeASTTransformation implements ASTTransformation {
private final Set<String> interestingAnnotationNames;
private final boolean removeAnnotations;
private SourceUnit sourceUnit;
protected AnnotatedNodeASTTransformation(Set<String> interestingAnnotationNames,
boolean removeAnnotations) {
this.interestingAnnotationNames = interestingAnnotationNames;
this.removeAnnotations = removeAnnotations;
}
@Override
public void visit(ASTNode[] nodes, SourceUnit source) {
this.sourceUnit = source;
List<AnnotationNode> annotationNodes = new ArrayList<>();
ClassVisitor classVisitor = new ClassVisitor(source, annotationNodes);
for (ASTNode node : nodes) {
if (node instanceof ModuleNode) {
ModuleNode module = (ModuleNode) node;
visitAnnotatedNode(module.getPackage(), annotationNodes);
for (ImportNode importNode : module.getImports()) {
visitAnnotatedNode(importNode, annotationNodes);
}
for (ImportNode importNode : module.getStarImports()) {
visitAnnotatedNode(importNode, annotationNodes);
}
for (Map.Entry<String, ImportNode> entry : module.getStaticImports()
.entrySet()) {
visitAnnotatedNode(entry.getValue(), annotationNodes);
}
for (Map.Entry<String, ImportNode> entry : module.getStaticStarImports()
.entrySet()) {
visitAnnotatedNode(entry.getValue(), annotationNodes);
}
for (ClassNode classNode : module.getClasses()) {
visitAnnotatedNode(classNode, annotationNodes);
classNode.visitContents(classVisitor);
}
}
}
processAnnotationNodes(annotationNodes);
}
protected SourceUnit getSourceUnit() {
return this.sourceUnit;
}
protected abstract void processAnnotationNodes(List<AnnotationNode> annotationNodes);
private void visitAnnotatedNode(AnnotatedNode annotatedNode,
List<AnnotationNode> annotatedNodes) {
if (annotatedNode != null) {
Iterator<AnnotationNode> annotationNodes = annotatedNode.getAnnotations()
.iterator();
while (annotationNodes.hasNext()) {
AnnotationNode annotationNode = annotationNodes.next();
if (this.interestingAnnotationNames
.contains(annotationNode.getClassNode().getName())) {
annotatedNodes.add(annotationNode);
if (this.removeAnnotations) {
annotationNodes.remove();
}
}
}
}
}
private class ClassVisitor extends ClassCodeVisitorSupport {
private final SourceUnit source;
private List<AnnotationNode> annotationNodes;
ClassVisitor(SourceUnit source, List<AnnotationNode> annotationNodes) {
this.source = source;
this.annotationNodes = annotationNodes;
}
@Override
protected SourceUnit getSourceUnit() {
return this.source;
}
@Override
public void visitAnnotations(AnnotatedNode node) {
visitAnnotatedNode(node, this.annotationNodes);
}
}
}

View File

@@ -0,0 +1,195 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.compiler;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.expr.ArgumentListExpression;
import org.codehaus.groovy.ast.expr.ClosureExpression;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.springframework.util.PatternMatchUtils;
/**
* General purpose AST utilities.
*
* @author Phillip Webb
* @author Dave Syer
* @author Greg Turnquist
*/
public abstract class AstUtils {
/**
* Determine if a {@link ClassNode} has one or more of the specified annotations on
* the class or any of its methods. N.B. the type names are not normally fully
* qualified.
* @param node the class to examine
* @param annotations the annotations to look for
* @return {@code true} if at least one of the annotations is found, otherwise
* {@code false}
*/
public static boolean hasAtLeastOneAnnotation(ClassNode node, String... annotations) {
if (hasAtLeastOneAnnotation((AnnotatedNode) node, annotations)) {
return true;
}
for (MethodNode method : node.getMethods()) {
if (hasAtLeastOneAnnotation(method, annotations)) {
return true;
}
}
return false;
}
/**
* Determine if an {@link AnnotatedNode} has one or more of the specified annotations.
* N.B. the annotation type names are not normally fully qualified.
* @param node the node to examine
* @param annotations the annotations to look for
* @return {@code true} if at least one of the annotations is found, otherwise
* {@code false}
*/
public static boolean hasAtLeastOneAnnotation(AnnotatedNode node,
String... annotations) {
for (AnnotationNode annotationNode : node.getAnnotations()) {
for (String annotation : annotations) {
if (PatternMatchUtils.simpleMatch(annotation,
annotationNode.getClassNode().getName())) {
return true;
}
}
}
return false;
}
/**
* Determine if a {@link ClassNode} has one or more fields of the specified types or
* method returning one or more of the specified types. N.B. the type names are not
* normally fully qualified.
* @param node the class to examine
* @param types the types to look for
* @return {@code true} if at least one of the types is found, otherwise {@code false}
*/
public static boolean hasAtLeastOneFieldOrMethod(ClassNode node, String... types) {
Set<String> typesSet = new HashSet<>(Arrays.asList(types));
for (FieldNode field : node.getFields()) {
if (typesSet.contains(field.getType().getName())) {
return true;
}
}
for (MethodNode method : node.getMethods()) {
if (typesSet.contains(method.getReturnType().getName())) {
return true;
}
}
return false;
}
/**
* Determine if a {@link ClassNode} subclasses any of the specified types N.B. the
* type names are not normally fully qualified.
* @param node the class to examine
* @param types the types that may have been sub-classed
* @return {@code true} if the class subclasses any of the specified types, otherwise
* {@code false}
*/
public static boolean subclasses(ClassNode node, String... types) {
for (String type : types) {
if (node.getSuperClass().getName().equals(type)) {
return true;
}
}
return false;
}
public static boolean hasAtLeastOneInterface(ClassNode classNode, String... types) {
Set<String> typesSet = new HashSet<>(Arrays.asList(types));
for (ClassNode inter : classNode.getInterfaces()) {
if (typesSet.contains(inter.getName())) {
return true;
}
}
return false;
}
/**
* Extract a top-level {@code name} closure from inside this block if there is one,
* optionally removing it from the block at the same time.
* @param block a block statement (class definition)
* @param name the name to look for
* @param remove whether or not the extracted closure should be removed
* @return a beans Closure if one can be found, null otherwise
*/
public static ClosureExpression getClosure(BlockStatement block, String name,
boolean remove) {
for (ExpressionStatement statement : getExpressionStatements(block)) {
Expression expression = statement.getExpression();
if (expression instanceof MethodCallExpression) {
ClosureExpression closure = getClosure(name,
(MethodCallExpression) expression);
if (closure != null) {
if (remove) {
block.getStatements().remove(statement);
}
return closure;
}
}
}
return null;
}
private static List<ExpressionStatement> getExpressionStatements(
BlockStatement block) {
ArrayList<ExpressionStatement> statements = new ArrayList<>();
for (Statement statement : block.getStatements()) {
if (statement instanceof ExpressionStatement) {
statements.add((ExpressionStatement) statement);
}
}
return statements;
}
private static ClosureExpression getClosure(String name,
MethodCallExpression expression) {
Expression method = expression.getMethod();
if (method instanceof ConstantExpression) {
if (name.equals(((ConstantExpression) method).getValue())) {
return (ClosureExpression) ((ArgumentListExpression) expression
.getArguments()).getExpression(0);
}
}
return null;
}
public static ClosureExpression getClosure(BlockStatement block, String name) {
return getClosure(block, name, false);
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.compiler;
import groovy.lang.GroovyClassLoader;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.classgen.GeneratorContext;
import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.customizers.ImportCustomizer;
/**
* Strategy that can be used to apply some auto-configuration during the
* {@link CompilePhase#CONVERSION} Groovy compile phase.
*
* @author Phillip Webb
*/
public abstract class CompilerAutoConfiguration {
/**
* Strategy method used to determine when compiler auto-configuration should be
* applied. Defaults to always.
* @param classNode the class node
* @return {@code true} if the compiler should be auto configured using this class. If
* this method returns {@code false} no other strategy methods will be called.
*/
public boolean matches(ClassNode classNode) {
return true;
}
/**
* Apply any dependency customizations. This method will only be called if
* {@link #matches} returns {@code true}.
* @param dependencies dependency customizer
* @throws CompilationFailedException if the dependencies cannot be applied
*/
public void applyDependencies(DependencyCustomizer dependencies)
throws CompilationFailedException {
}
/**
* Apply any import customizations. This method will only be called if
* {@link #matches} returns {@code true}.
* @param imports import customizer
* @throws CompilationFailedException if the imports cannot be applied
*/
public void applyImports(ImportCustomizer imports) throws CompilationFailedException {
}
/**
* Apply any customizations to the main class. This method will only be called if
* {@link #matches} returns {@code true}. This method is useful when a groovy file
* defines more than one class but customization only applies to the first class.
* @param loader the class loader being used during compilation
* @param configuration the compiler configuration
* @param generatorContext the current context
* @param source the source unit
* @param classNode the main class
* @throws CompilationFailedException if the customizations cannot be applied
*/
public void applyToMainClass(GroovyClassLoader loader,
GroovyCompilerConfiguration configuration, GeneratorContext generatorContext,
SourceUnit source, ClassNode classNode) throws CompilationFailedException {
}
/**
* Apply any additional configuration.
* @param loader the class loader being used during compilation
* @param configuration the compiler configuration
* @param generatorContext the current context
* @param source the source unit
* @param classNode the class
* @throws CompilationFailedException if the configuration cannot be applied
*/
public void apply(GroovyClassLoader loader, GroovyCompilerConfiguration configuration,
GeneratorContext generatorContext, SourceUnit source, ClassNode classNode)
throws CompilationFailedException {
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.compiler;
import groovy.lang.GroovyClassLoader;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.transform.ASTTransformation;
import org.springframework.boot.cli.compiler.grape.DependencyResolutionContext;
import org.springframework.core.annotation.Order;
/**
* {@link ASTTransformation} to apply
* {@link CompilerAutoConfiguration#applyDependencies(DependencyCustomizer) dependency
* auto-configuration}.
*
* @author Phillip Webb
* @author Dave Syer
* @author Andy Wilkinson
*/
@Order(DependencyAutoConfigurationTransformation.ORDER)
public class DependencyAutoConfigurationTransformation implements ASTTransformation {
/**
* The order of the transformation.
*/
public static final int ORDER = DependencyManagementBomTransformation.ORDER + 100;
private final GroovyClassLoader loader;
private final DependencyResolutionContext dependencyResolutionContext;
private final Iterable<CompilerAutoConfiguration> compilerAutoConfigurations;
public DependencyAutoConfigurationTransformation(GroovyClassLoader loader,
DependencyResolutionContext dependencyResolutionContext,
Iterable<CompilerAutoConfiguration> compilerAutoConfigurations) {
this.loader = loader;
this.dependencyResolutionContext = dependencyResolutionContext;
this.compilerAutoConfigurations = compilerAutoConfigurations;
}
@Override
public void visit(ASTNode[] nodes, SourceUnit source) {
for (ASTNode astNode : nodes) {
if (astNode instanceof ModuleNode) {
visitModule((ModuleNode) astNode);
}
}
}
private void visitModule(ModuleNode module) {
DependencyCustomizer dependencies = new DependencyCustomizer(this.loader, module,
this.dependencyResolutionContext);
for (ClassNode classNode : module.getClasses()) {
for (CompilerAutoConfiguration autoConfiguration : this.compilerAutoConfigurations) {
if (autoConfiguration.matches(classNode)) {
autoConfiguration.applyDependencies(dependencies);
}
}
}
}
}

View File

@@ -0,0 +1,270 @@
/*
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.compiler;
import groovy.lang.Grab;
import groovy.lang.GroovyClassLoader;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.springframework.boot.cli.compiler.dependencies.ArtifactCoordinatesResolver;
import org.springframework.boot.cli.compiler.grape.DependencyResolutionContext;
/**
* Customizer that allows dependencies to be added during compilation. Adding a dependency
* results in a {@link Grab @Grab} annotation being added to the primary {@link ClassNode
* class} is the {@link ModuleNode module} that's being customized.
* <p>
* This class provides a fluent API for conditionally adding dependencies. For example:
* {@code dependencies.ifMissing("com.corp.SomeClass").add(module)}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
public class DependencyCustomizer {
private final GroovyClassLoader loader;
private final ClassNode classNode;
private final DependencyResolutionContext dependencyResolutionContext;
/**
* Create a new {@link DependencyCustomizer} instance.
* @param loader the current classloader
* @param moduleNode the current module
* @param dependencyResolutionContext the context for dependency resolution
*/
public DependencyCustomizer(GroovyClassLoader loader, ModuleNode moduleNode,
DependencyResolutionContext dependencyResolutionContext) {
this.loader = loader;
this.classNode = moduleNode.getClasses().get(0);
this.dependencyResolutionContext = dependencyResolutionContext;
}
/**
* Create a new nested {@link DependencyCustomizer}.
* @param parent the parent customizer
*/
protected DependencyCustomizer(DependencyCustomizer parent) {
this.loader = parent.loader;
this.classNode = parent.classNode;
this.dependencyResolutionContext = parent.dependencyResolutionContext;
}
public String getVersion(String artifactId) {
return getVersion(artifactId, "");
}
public String getVersion(String artifactId, String defaultVersion) {
String version = this.dependencyResolutionContext.getArtifactCoordinatesResolver()
.getVersion(artifactId);
if (version == null) {
version = defaultVersion;
}
return version;
}
/**
* Create a nested {@link DependencyCustomizer} that only applies if any of the
* specified class names are not on the class path.
* @param classNames the class names to test
* @return a nested {@link DependencyCustomizer}
*/
public DependencyCustomizer ifAnyMissingClasses(final String... classNames) {
return new DependencyCustomizer(this) {
@Override
protected boolean canAdd() {
for (String className : classNames) {
try {
DependencyCustomizer.this.loader.loadClass(className);
}
catch (Exception ex) {
return true;
}
}
return false;
}
};
}
/**
* Create a nested {@link DependencyCustomizer} that only applies if all of the
* specified class names are not on the class path.
* @param classNames the class names to test
* @return a nested {@link DependencyCustomizer}
*/
public DependencyCustomizer ifAllMissingClasses(final String... classNames) {
return new DependencyCustomizer(this) {
@Override
protected boolean canAdd() {
for (String className : classNames) {
try {
DependencyCustomizer.this.loader.loadClass(className);
return false;
}
catch (Exception ex) {
// swallow exception and continue
}
}
return DependencyCustomizer.this.canAdd();
}
};
}
/**
* Create a nested {@link DependencyCustomizer} that only applies if the specified
* paths are on the class path.
* @param paths the paths to test
* @return a nested {@link DependencyCustomizer}
*/
public DependencyCustomizer ifAllResourcesPresent(final String... paths) {
return new DependencyCustomizer(this) {
@Override
protected boolean canAdd() {
for (String path : paths) {
try {
if (DependencyCustomizer.this.loader.getResource(path) == null) {
return false;
}
return true;
}
catch (Exception ex) {
// swallow exception and continue
}
}
return DependencyCustomizer.this.canAdd();
}
};
}
/**
* Create a nested {@link DependencyCustomizer} that only applies at least one of the
* specified paths is on the class path.
* @param paths the paths to test
* @return a nested {@link DependencyCustomizer}
*/
public DependencyCustomizer ifAnyResourcesPresent(final String... paths) {
return new DependencyCustomizer(this) {
@Override
protected boolean canAdd() {
for (String path : paths) {
try {
if (DependencyCustomizer.this.loader.getResource(path) != null) {
return true;
}
return false;
}
catch (Exception ex) {
// swallow exception and continue
}
}
return DependencyCustomizer.this.canAdd();
}
};
}
/**
* Add dependencies and all of their dependencies. The group ID and version of the
* dependencies are resolved from the modules using the customizer's
* {@link ArtifactCoordinatesResolver}.
* @param modules The module IDs
* @return this {@link DependencyCustomizer} for continued use
*/
public DependencyCustomizer add(String... modules) {
for (String module : modules) {
add(module, null, null, true);
}
return this;
}
/**
* Add a single dependency and, optionally, all of its dependencies. The group ID and
* version of the dependency are resolved from the module using the customizer's
* {@link ArtifactCoordinatesResolver}.
* @param module The module ID
* @param transitive {@code true} if the transitive dependencies should also be added,
* otherwise {@code false}.
* @return this {@link DependencyCustomizer} for continued use
*/
public DependencyCustomizer add(String module, boolean transitive) {
return add(module, null, null, transitive);
}
/**
* Add a single dependency with the specified classifier and type and, optionally, all
* of its dependencies. The group ID and version of the dependency are resolved from
* the module by using the customizer's {@link ArtifactCoordinatesResolver}.
* @param module The module ID
* @param classifier The classifier, may be {@code null}
* @param type The type, may be {@code null}
* @param transitive {@code true} if the transitive dependencies should also be added,
* otherwise {@code false}.
* @return this {@link DependencyCustomizer} for continued use
*/
public DependencyCustomizer add(String module, String classifier, String type,
boolean transitive) {
if (canAdd()) {
ArtifactCoordinatesResolver artifactCoordinatesResolver = this.dependencyResolutionContext
.getArtifactCoordinatesResolver();
this.classNode.addAnnotation(
createGrabAnnotation(artifactCoordinatesResolver.getGroupId(module),
artifactCoordinatesResolver.getArtifactId(module),
artifactCoordinatesResolver.getVersion(module), classifier,
type, transitive));
}
return this;
}
private AnnotationNode createGrabAnnotation(String group, String module,
String version, String classifier, String type, boolean transitive) {
AnnotationNode annotationNode = new AnnotationNode(new ClassNode(Grab.class));
annotationNode.addMember("group", new ConstantExpression(group));
annotationNode.addMember("module", new ConstantExpression(module));
annotationNode.addMember("version", new ConstantExpression(version));
if (classifier != null) {
annotationNode.addMember("classifier", new ConstantExpression(classifier));
}
if (type != null) {
annotationNode.addMember("type", new ConstantExpression(type));
}
annotationNode.addMember("transitive", new ConstantExpression(transitive));
annotationNode.addMember("initClass", new ConstantExpression(false));
return annotationNode;
}
/**
* Strategy called to test if dependencies can be added. Subclasses override as
* required. Returns {@code true} by default.
* @return {@code true} if dependencies can be added, otherwise {@code false}
*/
protected boolean canAdd() {
return true;
}
/**
* Returns the {@link DependencyResolutionContext}.
* @return the dependency resolution context
*/
public DependencyResolutionContext getDependencyResolutionContext() {
return this.dependencyResolutionContext;
}
}

View File

@@ -0,0 +1,243 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.compiler;
import java.net.MalformedURLException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import groovy.grape.Grape;
import org.apache.maven.model.Model;
import org.apache.maven.model.Repository;
import org.apache.maven.model.building.DefaultModelBuilder;
import org.apache.maven.model.building.DefaultModelBuilderFactory;
import org.apache.maven.model.building.DefaultModelBuildingRequest;
import org.apache.maven.model.building.ModelSource;
import org.apache.maven.model.building.UrlModelSource;
import org.apache.maven.model.resolution.InvalidRepositoryException;
import org.apache.maven.model.resolution.ModelResolver;
import org.apache.maven.model.resolution.UnresolvableModelException;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.ListExpression;
import org.codehaus.groovy.control.messages.Message;
import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
import org.codehaus.groovy.syntax.SyntaxException;
import org.codehaus.groovy.transform.ASTTransformation;
import org.springframework.boot.cli.compiler.dependencies.MavenModelDependencyManagement;
import org.springframework.boot.cli.compiler.grape.DependencyResolutionContext;
import org.springframework.boot.groovy.DependencyManagementBom;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
/**
* {@link ASTTransformation} for processing {@link DependencyManagementBom} annotations.
*
* @author Andy Wilkinson
* @since 1.3.0
*/
@Order(DependencyManagementBomTransformation.ORDER)
public class DependencyManagementBomTransformation
extends AnnotatedNodeASTTransformation {
/**
* The order of the transformation.
*/
public static final int ORDER = Ordered.HIGHEST_PRECEDENCE + 100;
private static final Set<String> DEPENDENCY_MANAGEMENT_BOM_ANNOTATION_NAMES = Collections
.unmodifiableSet(
new HashSet<>(Arrays.asList(DependencyManagementBom.class.getName(),
DependencyManagementBom.class.getSimpleName())));
private final DependencyResolutionContext resolutionContext;
public DependencyManagementBomTransformation(
DependencyResolutionContext resolutionContext) {
super(DEPENDENCY_MANAGEMENT_BOM_ANNOTATION_NAMES, true);
this.resolutionContext = resolutionContext;
}
@Override
protected void processAnnotationNodes(List<AnnotationNode> annotationNodes) {
if (!annotationNodes.isEmpty()) {
if (annotationNodes.size() > 1) {
for (AnnotationNode annotationNode : annotationNodes) {
handleDuplicateDependencyManagementBomAnnotation(annotationNode);
}
}
else {
processDependencyManagementBomAnnotation(annotationNodes.get(0));
}
}
}
private void processDependencyManagementBomAnnotation(AnnotationNode annotationNode) {
Expression valueExpression = annotationNode.getMember("value");
List<Map<String, String>> bomDependencies = createDependencyMaps(valueExpression);
updateDependencyResolutionContext(bomDependencies);
}
private List<Map<String, String>> createDependencyMaps(Expression valueExpression) {
Map<String, String> dependency = null;
List<ConstantExpression> constantExpressions = getConstantExpressions(
valueExpression);
List<Map<String, String>> dependencies = new ArrayList<>(
constantExpressions.size());
for (ConstantExpression expression : constantExpressions) {
Object value = expression.getValue();
if (value instanceof String) {
String[] components = ((String) expression.getValue()).split(":");
if (components.length == 3) {
dependency = new HashMap<>();
dependency.put("group", components[0]);
dependency.put("module", components[1]);
dependency.put("version", components[2]);
dependency.put("type", "pom");
dependencies.add(dependency);
}
else {
handleMalformedDependency(expression);
}
}
}
return dependencies;
}
private List<ConstantExpression> getConstantExpressions(Expression valueExpression) {
if (valueExpression instanceof ListExpression) {
return getConstantExpressions((ListExpression) valueExpression);
}
if (valueExpression instanceof ConstantExpression
&& ((ConstantExpression) valueExpression).getValue() instanceof String) {
return Arrays.asList((ConstantExpression) valueExpression);
}
reportError("@DependencyManagementBom requires an inline constant that is a "
+ "string or a string array", valueExpression);
return Collections.emptyList();
}
private List<ConstantExpression> getConstantExpressions(
ListExpression valueExpression) {
List<ConstantExpression> expressions = new ArrayList<>();
for (Expression expression : valueExpression.getExpressions()) {
if (expression instanceof ConstantExpression
&& ((ConstantExpression) expression).getValue() instanceof String) {
expressions.add((ConstantExpression) expression);
}
else {
reportError(
"Each entry in the array must be an " + "inline string constant",
expression);
}
}
return expressions;
}
private void handleMalformedDependency(Expression expression) {
Message message = createSyntaxErrorMessage(
String.format(
"The string must be of the form \"group:module:version\"%n"),
expression);
getSourceUnit().getErrorCollector().addErrorAndContinue(message);
}
private void updateDependencyResolutionContext(
List<Map<String, String>> bomDependencies) {
URI[] uris = Grape.getInstance().resolve(null,
bomDependencies.toArray(new Map[bomDependencies.size()]));
DefaultModelBuilder modelBuilder = new DefaultModelBuilderFactory().newInstance();
for (URI uri : uris) {
try {
DefaultModelBuildingRequest request = new DefaultModelBuildingRequest();
request.setModelResolver(new GrapeModelResolver());
request.setModelSource(new UrlModelSource(uri.toURL()));
request.setSystemProperties(System.getProperties());
Model model = modelBuilder.build(request).getEffectiveModel();
this.resolutionContext.addDependencyManagement(
new MavenModelDependencyManagement(model));
}
catch (Exception ex) {
throw new IllegalStateException("Failed to build model for '" + uri
+ "'. Is it a valid Maven bom?", ex);
}
}
}
private void handleDuplicateDependencyManagementBomAnnotation(
AnnotationNode annotationNode) {
Message message = createSyntaxErrorMessage(
"Duplicate @DependencyManagementBom annotation. It must be declared at most once.",
annotationNode);
getSourceUnit().getErrorCollector().addErrorAndContinue(message);
}
private void reportError(String message, ASTNode node) {
getSourceUnit().getErrorCollector()
.addErrorAndContinue(createSyntaxErrorMessage(message, node));
}
private Message createSyntaxErrorMessage(String message, ASTNode node) {
return new SyntaxErrorMessage(
new SyntaxException(message, node.getLineNumber(), node.getColumnNumber(),
node.getLastLineNumber(), node.getLastColumnNumber()),
getSourceUnit());
}
private static class GrapeModelResolver implements ModelResolver {
@Override
public ModelSource resolveModel(String groupId, String artifactId, String version)
throws UnresolvableModelException {
Map<String, String> dependency = new HashMap<>();
dependency.put("group", groupId);
dependency.put("module", artifactId);
dependency.put("version", version);
dependency.put("type", "pom");
try {
return new UrlModelSource(
Grape.getInstance().resolve(null, dependency)[0].toURL());
}
catch (MalformedURLException e) {
throw new UnresolvableModelException(e.getMessage(), groupId, artifactId,
version);
}
}
@Override
public void addRepository(Repository repository)
throws InvalidRepositoryException {
}
@Override
public ModelResolver newCopy() {
return this;
}
}
}

View File

@@ -0,0 +1,244 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.compiler;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import groovy.lang.GroovyClassLoader;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.SourceUnit;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
/**
* Extension of the {@link GroovyClassLoader} with support for obtaining '.class' files as
* resources.
*
* @author Phillip Webb
* @author Dave Syer
*/
public class ExtendedGroovyClassLoader extends GroovyClassLoader {
private static final String SHARED_PACKAGE = "org.springframework.boot.groovy";
private static final URL[] NO_URLS = new URL[] {};
private final Map<String, byte[]> classResources = new HashMap<>();
private final GroovyCompilerScope scope;
private final CompilerConfiguration configuration;
public ExtendedGroovyClassLoader(GroovyCompilerScope scope) {
this(scope, createParentClassLoader(scope), new CompilerConfiguration());
}
private static ClassLoader createParentClassLoader(GroovyCompilerScope scope) {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if (scope == GroovyCompilerScope.DEFAULT) {
classLoader = new DefaultScopeParentClassLoader(classLoader);
}
return classLoader;
}
private ExtendedGroovyClassLoader(GroovyCompilerScope scope, ClassLoader parent,
CompilerConfiguration configuration) {
super(parent, configuration);
this.configuration = configuration;
this.scope = scope;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
return super.findClass(name);
}
catch (ClassNotFoundException ex) {
if (this.scope == GroovyCompilerScope.DEFAULT
&& name.startsWith(SHARED_PACKAGE)) {
Class<?> sharedClass = findSharedClass(name);
if (sharedClass != null) {
return sharedClass;
}
}
throw ex;
}
}
private Class<?> findSharedClass(String name) {
try {
String path = name.replace('.', '/').concat(".class");
try (InputStream inputStream = getParent().getResourceAsStream(path)) {
if (inputStream != null) {
return defineClass(name, FileCopyUtils.copyToByteArray(inputStream));
}
}
return null;
}
catch (Exception ex) {
return null;
}
}
@Override
public InputStream getResourceAsStream(String name) {
InputStream resourceStream = super.getResourceAsStream(name);
if (resourceStream == null) {
byte[] bytes = this.classResources.get(name);
resourceStream = bytes == null ? null : new ByteArrayInputStream(bytes);
}
return resourceStream;
}
@Override
public ClassCollector createCollector(CompilationUnit unit, SourceUnit su) {
InnerLoader loader = AccessController.doPrivileged(getInnerLoader());
return new ExtendedClassCollector(loader, unit, su);
}
private PrivilegedAction<InnerLoader> getInnerLoader() {
return () -> new InnerLoader(ExtendedGroovyClassLoader.this) {
// Don't return URLs from the inner loader so that Tomcat only
// searches the parent. Fixes 'TLD skipped' issues
@Override
public URL[] getURLs() {
return NO_URLS;
}
};
}
public CompilerConfiguration getConfiguration() {
return this.configuration;
}
/**
* Inner collector class used to track as classes are added.
*/
protected class ExtendedClassCollector extends ClassCollector {
protected ExtendedClassCollector(InnerLoader loader, CompilationUnit unit,
SourceUnit su) {
super(loader, unit, su);
}
@Override
protected Class<?> createClass(byte[] code, ClassNode classNode) {
Class<?> createdClass = super.createClass(code, classNode);
ExtendedGroovyClassLoader.this.classResources
.put(classNode.getName().replace('.', '/') + ".class", code);
return createdClass;
}
}
/**
* ClassLoader used for a parent that filters so that only classes from groovy-all.jar
* are exposed.
*/
private static class DefaultScopeParentClassLoader extends ClassLoader {
private static final String[] GROOVY_JARS_PREFIXES = { "groovy", "antlr", "asm" };
private final URLClassLoader groovyOnlyClassLoader;
DefaultScopeParentClassLoader(ClassLoader parent) {
super(parent);
this.groovyOnlyClassLoader = new URLClassLoader(getGroovyJars(parent),
parent.getParent());
}
private URL[] getGroovyJars(final ClassLoader parent) {
Set<URL> urls = new HashSet<>();
findGroovyJarsDirectly(parent, urls);
if (urls.isEmpty()) {
findGroovyJarsFromClassPath(parent, urls);
}
Assert.state(!urls.isEmpty(), "Unable to find groovy JAR");
return new ArrayList<>(urls).toArray(new URL[urls.size()]);
}
private void findGroovyJarsDirectly(ClassLoader classLoader, Set<URL> urls) {
while (classLoader != null) {
if (classLoader instanceof URLClassLoader) {
for (URL url : ((URLClassLoader) classLoader).getURLs()) {
if (isGroovyJar(url.toString())) {
urls.add(url);
}
}
}
classLoader = classLoader.getParent();
}
}
private void findGroovyJarsFromClassPath(ClassLoader parent, Set<URL> urls) {
String classpath = System.getProperty("java.class.path");
String[] entries = classpath.split(System.getProperty("path.separator"));
for (String entry : entries) {
if (isGroovyJar(entry)) {
File file = new File(entry);
if (file.canRead()) {
try {
urls.add(file.toURI().toURL());
}
catch (MalformedURLException ex) {
// Swallow and continue
}
}
}
}
}
private boolean isGroovyJar(String entry) {
entry = StringUtils.cleanPath(entry);
for (String jarPrefix : GROOVY_JARS_PREFIXES) {
if (entry.contains("/" + jarPrefix + "-")) {
return true;
}
}
return false;
}
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
if (!name.startsWith("java.")) {
this.groovyOnlyClassLoader.loadClass(name);
}
return super.loadClass(name, resolve);
}
}
}

View File

@@ -0,0 +1,135 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.compiler;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.ast.PackageNode;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.ListExpression;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.transform.GroovyASTTransformation;
import org.springframework.boot.groovy.DependencyManagementBom;
import org.springframework.core.Ordered;
/**
* A base class that lets plugin authors easily add additional BOMs to all apps. All the
* dependencies in the BOM (and its transitives) will be added to the dependency
* management lookup, so an app can use just the artifact id (e.g. "spring-jdbc") in a
* {@code @Grab}. To install, implement the missing methods and list the class in
* {@code META-INF/services/org.springframework.boot.cli.compiler.SpringBootAstTransformation}
* . The {@link #getOrder()} value needs to be before
* {@link DependencyManagementBomTransformation#ORDER}.
*
* @author Dave Syer
* @since 1.3.0
*/
@GroovyASTTransformation(phase = CompilePhase.CONVERSION)
public abstract class GenericBomAstTransformation
implements SpringBootAstTransformation, Ordered {
private static ClassNode BOM = ClassHelper.make(DependencyManagementBom.class);
@Override
public void visit(ASTNode[] nodes, SourceUnit source) {
for (ASTNode astNode : nodes) {
if (astNode instanceof ModuleNode) {
visitModule((ModuleNode) astNode, getBomModule());
}
}
}
/**
* The bom to be added to dependency management in compact form:
* <code>"&lt;groupId&gt;:&lt;artifactId&gt;:&lt;version&gt;"</code> (like in a
* {@code @Grab}).
* @return the maven co-ordinates of the BOM to add
*/
protected abstract String getBomModule();
private void visitModule(ModuleNode node, String module) {
addDependencyManagementBom(node, module);
}
private void addDependencyManagementBom(ModuleNode node, String module) {
AnnotatedNode annotated = getAnnotatedNode(node);
if (annotated != null) {
AnnotationNode bom = getAnnotation(annotated);
List<Expression> expressions = new ArrayList<>(
getConstantExpressions(bom.getMember("value")));
expressions.add(new ConstantExpression(module));
bom.setMember("value", new ListExpression(expressions));
}
}
private AnnotationNode getAnnotation(AnnotatedNode annotated) {
List<AnnotationNode> annotations = annotated.getAnnotations(BOM);
if (!annotations.isEmpty()) {
return annotations.get(0);
}
AnnotationNode annotation = new AnnotationNode(BOM);
annotated.addAnnotation(annotation);
return annotation;
}
private AnnotatedNode getAnnotatedNode(ModuleNode node) {
PackageNode packageNode = node.getPackage();
if (packageNode != null && !packageNode.getAnnotations(BOM).isEmpty()) {
return packageNode;
}
if (!node.getClasses().isEmpty()) {
return node.getClasses().get(0);
}
return packageNode;
}
private List<ConstantExpression> getConstantExpressions(Expression valueExpression) {
if (valueExpression instanceof ListExpression) {
return getConstantExpressions((ListExpression) valueExpression);
}
if (valueExpression instanceof ConstantExpression
&& ((ConstantExpression) valueExpression).getValue() instanceof String) {
return Arrays.asList((ConstantExpression) valueExpression);
}
return Collections.emptyList();
}
private List<ConstantExpression> getConstantExpressions(
ListExpression valueExpression) {
List<ConstantExpression> expressions = new ArrayList<>();
for (Expression expression : valueExpression.getExpressions()) {
if (expression instanceof ConstantExpression
&& ((ConstantExpression) expression).getValue() instanceof String) {
expressions.add((ConstantExpression) expression);
}
}
return expressions;
}
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.compiler;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ClassCodeVisitorSupport;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.ast.PropertyNode;
import org.codehaus.groovy.ast.expr.ClosureExpression;
import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.transform.ASTTransformation;
import org.springframework.core.annotation.Order;
/**
* {@link ASTTransformation} to resolve beans declarations inside application source
* files. Users only need to define a <code>beans{}</code> DSL element, and this
* transformation will remove it and make it accessible to the Spring application via an
* interface.
*
* @author Dave Syer
*/
@Order(GroovyBeansTransformation.ORDER)
public class GroovyBeansTransformation implements ASTTransformation {
/**
* The order of the transformation.
*/
public static final int ORDER = DependencyManagementBomTransformation.ORDER + 200;
@Override
public void visit(ASTNode[] nodes, SourceUnit source) {
for (ASTNode node : nodes) {
if (node instanceof ModuleNode) {
ModuleNode module = (ModuleNode) node;
for (ClassNode classNode : new ArrayList<>(module.getClasses())) {
if (classNode.isScript()) {
classNode.visitContents(new ClassVisitor(source, classNode));
}
}
}
}
}
private class ClassVisitor extends ClassCodeVisitorSupport {
private static final String SOURCE_INTERFACE = "org.springframework.boot.BeanDefinitionLoader.GroovyBeanDefinitionSource";
private static final String BEANS = "beans";
private final SourceUnit source;
private final ClassNode classNode;
private boolean xformed = false;
ClassVisitor(SourceUnit source, ClassNode classNode) {
this.source = source;
this.classNode = classNode;
}
@Override
protected SourceUnit getSourceUnit() {
return this.source;
}
@Override
public void visitBlockStatement(BlockStatement block) {
if (block.isEmpty() || this.xformed) {
return;
}
ClosureExpression closure = beans(block);
if (closure != null) {
// Add a marker interface to the current script
this.classNode.addInterface(ClassHelper.make(SOURCE_INTERFACE));
// Implement the interface by adding a public read-only property with the
// same name as the method in the interface (getBeans). Make it return the
// closure.
this.classNode.addProperty(
new PropertyNode(BEANS, Modifier.PUBLIC | Modifier.FINAL,
ClassHelper.CLOSURE_TYPE.getPlainNodeReference(),
this.classNode, closure, null, null));
// Only do this once per class
this.xformed = true;
}
}
/**
* Extract a top-level <code>beans{}</code> closure from inside this block if
* there is one. Removes it from the block at the same time.
* @param block a block statement (class definition)
* @return a beans Closure if one can be found, null otherwise
*/
private ClosureExpression beans(BlockStatement block) {
return AstUtils.getClosure(block, BEANS, true);
}
}
}

View File

@@ -0,0 +1,331 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.compiler;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.ServiceLoader;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyClassLoader.ClassCollector;
import groovy.lang.GroovyCodeSource;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.classgen.GeneratorContext;
import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.Phases;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.customizers.CompilationCustomizer;
import org.codehaus.groovy.control.customizers.ImportCustomizer;
import org.codehaus.groovy.transform.ASTTransformation;
import org.codehaus.groovy.transform.ASTTransformationVisitor;
import org.springframework.boot.cli.compiler.dependencies.SpringBootDependenciesDependencyManagement;
import org.springframework.boot.cli.compiler.grape.AetherGrapeEngine;
import org.springframework.boot.cli.compiler.grape.AetherGrapeEngineFactory;
import org.springframework.boot.cli.compiler.grape.DependencyResolutionContext;
import org.springframework.boot.cli.compiler.grape.GrapeEngineInstaller;
import org.springframework.boot.cli.util.ResourceUtils;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
/**
* Compiler for Groovy sources. Primarily a simple Facade for
* {@link GroovyClassLoader#parseClass(GroovyCodeSource)} with the following additional
* features:
* <ul>
* <li>{@link CompilerAutoConfiguration} strategies will be read from
* {@code META-INF/services/org.springframework.boot.cli.compiler.CompilerAutoConfiguration}
* (per the standard java {@link ServiceLoader} contract) and applied during compilation
* </li>
*
* <li>Multiple classes can be returned if the Groovy source defines more than one Class
* </li>
*
* <li>Generated class files can also be loaded using
* {@link ClassLoader#getResource(String)}</li>
* </ul>
*
* @author Phillip Webb
* @author Dave Syer
* @author Andy Wilkinson
*/
public class GroovyCompiler {
private final GroovyCompilerConfiguration configuration;
private final ExtendedGroovyClassLoader loader;
private final Iterable<CompilerAutoConfiguration> compilerAutoConfigurations;
private final List<ASTTransformation> transformations;
/**
* Create a new {@link GroovyCompiler} instance.
* @param configuration the compiler configuration
*/
public GroovyCompiler(final GroovyCompilerConfiguration configuration) {
this.configuration = configuration;
this.loader = createLoader(configuration);
DependencyResolutionContext resolutionContext = new DependencyResolutionContext();
resolutionContext.addDependencyManagement(
new SpringBootDependenciesDependencyManagement());
AetherGrapeEngine grapeEngine = AetherGrapeEngineFactory.create(this.loader,
configuration.getRepositoryConfiguration(), resolutionContext,
configuration.isQuiet());
GrapeEngineInstaller.install(grapeEngine);
this.loader.getConfiguration()
.addCompilationCustomizers(new CompilerAutoConfigureCustomizer());
if (configuration.isAutoconfigure()) {
this.compilerAutoConfigurations = ServiceLoader
.load(CompilerAutoConfiguration.class);
}
else {
this.compilerAutoConfigurations = Collections.emptySet();
}
this.transformations = new ArrayList<>();
this.transformations
.add(new DependencyManagementBomTransformation(resolutionContext));
this.transformations.add(new DependencyAutoConfigurationTransformation(
this.loader, resolutionContext, this.compilerAutoConfigurations));
this.transformations.add(new GroovyBeansTransformation());
if (this.configuration.isGuessDependencies()) {
this.transformations.add(
new ResolveDependencyCoordinatesTransformation(resolutionContext));
}
for (ASTTransformation transformation : ServiceLoader
.load(SpringBootAstTransformation.class)) {
this.transformations.add(transformation);
}
this.transformations.sort(AnnotationAwareOrderComparator.INSTANCE);
}
/**
* Return a mutable list of the {@link ASTTransformation}s to be applied during
* {@link #compile(String...)}.
* @return the AST transformations to apply
*/
public List<ASTTransformation> getAstTransformations() {
return this.transformations;
}
public ExtendedGroovyClassLoader getLoader() {
return this.loader;
}
private ExtendedGroovyClassLoader createLoader(
GroovyCompilerConfiguration configuration) {
ExtendedGroovyClassLoader loader = new ExtendedGroovyClassLoader(
configuration.getScope());
for (URL url : getExistingUrls()) {
loader.addURL(url);
}
for (String classpath : configuration.getClasspath()) {
loader.addClasspath(classpath);
}
return loader;
}
private URL[] getExistingUrls() {
ClassLoader tccl = Thread.currentThread().getContextClassLoader();
if (tccl instanceof ExtendedGroovyClassLoader) {
return ((ExtendedGroovyClassLoader) tccl).getURLs();
}
else {
return new URL[0];
}
}
public void addCompilationCustomizers(CompilationCustomizer... customizers) {
this.loader.getConfiguration().addCompilationCustomizers(customizers);
}
/**
* Compile the specified Groovy sources, applying any
* {@link CompilerAutoConfiguration}s. All classes defined in the sources will be
* returned from this method.
* @param sources the sources to compile
* @return compiled classes
* @throws CompilationFailedException in case of compilation failures
* @throws IOException in case of I/O errors
* @throws CompilationFailedException in case of compilation errors
*/
public Class<?>[] compile(String... sources)
throws CompilationFailedException, IOException {
this.loader.clearCache();
List<Class<?>> classes = new ArrayList<>();
CompilerConfiguration configuration = this.loader.getConfiguration();
CompilationUnit compilationUnit = new CompilationUnit(configuration, null,
this.loader);
ClassCollector collector = this.loader.createCollector(compilationUnit, null);
compilationUnit.setClassgenCallback(collector);
for (String source : sources) {
List<String> paths = ResourceUtils.getUrls(source, this.loader);
for (String path : paths) {
compilationUnit.addSource(new URL(path));
}
}
addAstTransformations(compilationUnit);
compilationUnit.compile(Phases.CLASS_GENERATION);
for (Object loadedClass : collector.getLoadedClasses()) {
classes.add((Class<?>) loadedClass);
}
ClassNode mainClassNode = MainClass.get(compilationUnit);
Class<?> mainClass = null;
for (Class<?> loadedClass : classes) {
if (mainClassNode.getName().equals(loadedClass.getName())) {
mainClass = loadedClass;
}
}
if (mainClass != null) {
classes.remove(mainClass);
classes.add(0, mainClass);
}
return classes.toArray(new Class<?>[classes.size()]);
}
@SuppressWarnings("rawtypes")
private void addAstTransformations(CompilationUnit compilationUnit) {
LinkedList[] phaseOperations = getPhaseOperations(compilationUnit);
processConversionOperations(phaseOperations[Phases.CONVERSION]);
}
@SuppressWarnings("rawtypes")
private LinkedList[] getPhaseOperations(CompilationUnit compilationUnit) {
try {
Field field = CompilationUnit.class.getDeclaredField("phaseOperations");
field.setAccessible(true);
LinkedList[] phaseOperations = (LinkedList[]) field.get(compilationUnit);
return phaseOperations;
}
catch (Exception ex) {
throw new IllegalStateException(
"Phase operations not available from compilation unit");
}
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void processConversionOperations(LinkedList conversionOperations) {
int index = getIndexOfASTTransformationVisitor(conversionOperations);
conversionOperations.add(index, new CompilationUnit.SourceUnitOperation() {
@Override
public void call(SourceUnit source) throws CompilationFailedException {
ASTNode[] nodes = new ASTNode[] { source.getAST() };
for (ASTTransformation transformation : GroovyCompiler.this.transformations) {
transformation.visit(nodes, source);
}
}
});
}
private int getIndexOfASTTransformationVisitor(LinkedList<?> conversionOperations) {
for (int index = 0; index < conversionOperations.size(); index++) {
if (conversionOperations.get(index).getClass().getName()
.startsWith(ASTTransformationVisitor.class.getName())) {
return index;
}
}
return conversionOperations.size();
}
/**
* {@link CompilationCustomizer} to call {@link CompilerAutoConfiguration}s.
*/
private class CompilerAutoConfigureCustomizer extends CompilationCustomizer {
CompilerAutoConfigureCustomizer() {
super(CompilePhase.CONVERSION);
}
@Override
public void call(SourceUnit source, GeneratorContext context, ClassNode classNode)
throws CompilationFailedException {
ImportCustomizer importCustomizer = new SmartImportCustomizer(source, context,
classNode);
ClassNode mainClassNode = MainClass.get(source.getAST().getClasses());
// Additional auto configuration
for (CompilerAutoConfiguration autoConfiguration : GroovyCompiler.this.compilerAutoConfigurations) {
if (autoConfiguration.matches(classNode)) {
if (GroovyCompiler.this.configuration.isGuessImports()) {
autoConfiguration.applyImports(importCustomizer);
importCustomizer.call(source, context, classNode);
}
if (classNode.equals(mainClassNode)) {
autoConfiguration.applyToMainClass(GroovyCompiler.this.loader,
GroovyCompiler.this.configuration, context, source,
classNode);
}
autoConfiguration.apply(GroovyCompiler.this.loader,
GroovyCompiler.this.configuration, context, source,
classNode);
}
}
importCustomizer.call(source, context, classNode);
}
}
private static class MainClass {
@SuppressWarnings("unchecked")
public static ClassNode get(CompilationUnit source) {
return get(source.getAST().getClasses());
}
public static ClassNode get(List<ClassNode> classes) {
for (ClassNode node : classes) {
if (AstUtils.hasAtLeastOneAnnotation(node, "Enable*AutoConfiguration")) {
return null; // No need to enhance this
}
if (AstUtils.hasAtLeastOneAnnotation(node, "*Controller", "Configuration",
"Component", "*Service", "Repository", "Enable*")) {
return node;
}
}
return (classes.isEmpty() ? null : classes.get(0));
}
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.compiler;
import java.util.List;
import org.springframework.boot.cli.compiler.grape.RepositoryConfiguration;
/**
* Configuration for the {@link GroovyCompiler}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
public interface GroovyCompilerConfiguration {
/**
* Constant to be used when there is no {@link #getClasspath() classpath}.
*/
String[] DEFAULT_CLASSPATH = { "." };
/**
* Returns the scope in which the compiler operates.
* @return the scope of the compiler
*/
GroovyCompilerScope getScope();
/**
* Returns if import declarations should be guessed.
* @return {@code true} if imports should be guessed, otherwise {@code false}
*/
boolean isGuessImports();
/**
* Returns if jar dependencies should be guessed.
* @return {@code true} if dependencies should be guessed, otherwise {@code false}
*/
boolean isGuessDependencies();
/**
* Returns true if auto-configuration transformations should be applied.
* @return {@code true} if auto-configuration transformations should be applied,
* otherwise {@code false}
*/
boolean isAutoconfigure();
/**
* Returns the classpath for local resources.
* @return a path for local resources
*/
String[] getClasspath();
/**
* Returns the configuration for the repositories that will be used by the compiler to
* resolve dependencies.
* @return the repository configurations
*/
List<RepositoryConfiguration> getRepositoryConfiguration();
/**
* Returns if running in quiet mode.
* @return {@code true} if running in quiet mode
*/
boolean isQuiet();
}

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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.compiler;
/**
* The scope in which a groovy compiler operates.
*
* @author Phillip Webb
*/
public enum GroovyCompilerScope {
/**
* Default scope, exposes groovy.jar (loaded from the parent) and the shared cli
* package (loaded via groovy classloader).
*/
DEFAULT,
/**
* Extension scope, allows full access to internal CLI classes.
*/
EXTENSION
}

View File

@@ -0,0 +1,131 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.compiler;
import java.io.File;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import org.apache.maven.settings.Profile;
import org.apache.maven.settings.Repository;
import org.codehaus.plexus.interpolation.InterpolationException;
import org.codehaus.plexus.interpolation.Interpolator;
import org.codehaus.plexus.interpolation.PropertiesBasedValueSource;
import org.codehaus.plexus.interpolation.RegexBasedInterpolator;
import org.springframework.boot.cli.compiler.grape.RepositoryConfiguration;
import org.springframework.boot.cli.compiler.maven.MavenSettings;
import org.springframework.boot.cli.compiler.maven.MavenSettingsReader;
import org.springframework.util.StringUtils;
/**
* Factory used to create {@link RepositoryConfiguration}s.
*
* @author Andy Wilkinson
* @author Dave Syer
*/
public final class RepositoryConfigurationFactory {
private static final RepositoryConfiguration MAVEN_CENTRAL = new RepositoryConfiguration(
"central", URI.create("http://repo1.maven.org/maven2/"), false);
private static final RepositoryConfiguration SPRING_MILESTONE = new RepositoryConfiguration(
"spring-milestone", URI.create("http://repo.spring.io/milestone"), false);
private static final RepositoryConfiguration SPRING_SNAPSHOT = new RepositoryConfiguration(
"spring-snapshot", URI.create("http://repo.spring.io/snapshot"), true);
private RepositoryConfigurationFactory() {
}
/**
* Create a new default repository configuration.
* @return the newly-created default repository configuration
*/
public static List<RepositoryConfiguration> createDefaultRepositoryConfiguration() {
MavenSettings mavenSettings = new MavenSettingsReader().readSettings();
List<RepositoryConfiguration> repositoryConfiguration = new ArrayList<>();
repositoryConfiguration.add(MAVEN_CENTRAL);
if (!Boolean.getBoolean("disableSpringSnapshotRepos")) {
repositoryConfiguration.add(SPRING_MILESTONE);
repositoryConfiguration.add(SPRING_SNAPSHOT);
}
addDefaultCacheAsRepository(mavenSettings.getLocalRepository(),
repositoryConfiguration);
addActiveProfileRepositories(mavenSettings.getActiveProfiles(),
repositoryConfiguration);
return repositoryConfiguration;
}
private static void addDefaultCacheAsRepository(String localRepository,
List<RepositoryConfiguration> repositoryConfiguration) {
RepositoryConfiguration repository = new RepositoryConfiguration("local",
getLocalRepositoryDirectory(localRepository).toURI(), true);
if (!repositoryConfiguration.contains(repository)) {
repositoryConfiguration.add(0, repository);
}
}
private static void addActiveProfileRepositories(List<Profile> activeProfiles,
List<RepositoryConfiguration> configurations) {
for (Profile activeProfile : activeProfiles) {
Interpolator interpolator = new RegexBasedInterpolator();
interpolator.addValueSource(
new PropertiesBasedValueSource(activeProfile.getProperties()));
for (Repository repository : activeProfile.getRepositories()) {
configurations.add(getRepositoryConfiguration(interpolator, repository));
}
}
}
private static RepositoryConfiguration getRepositoryConfiguration(
Interpolator interpolator, Repository repository) {
String name = interpolate(interpolator, repository.getId());
String url = interpolate(interpolator, repository.getUrl());
boolean snapshotsEnabled = false;
if (repository.getSnapshots() != null) {
snapshotsEnabled = repository.getSnapshots().isEnabled();
}
return new RepositoryConfiguration(name, URI.create(url), snapshotsEnabled);
}
private static String interpolate(Interpolator interpolator, String value) {
try {
return interpolator.interpolate(value);
}
catch (InterpolationException ex) {
return value;
}
}
private static File getLocalRepositoryDirectory(String localRepository) {
if (StringUtils.hasText(localRepository)) {
return new File(localRepository);
}
return new File(getM2HomeDirectory(), "repository");
}
private static File getM2HomeDirectory() {
String mavenRoot = System.getProperty("maven.home");
if (StringUtils.hasLength(mavenRoot)) {
return new File(mavenRoot);
}
return new File(System.getProperty("user.home"), ".m2");
}
}

View File

@@ -0,0 +1,112 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.compiler;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import groovy.lang.Grab;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.transform.ASTTransformation;
import org.springframework.boot.cli.compiler.grape.DependencyResolutionContext;
import org.springframework.core.annotation.Order;
/**
* {@link ASTTransformation} to resolve {@link Grab} artifact coordinates.
*
* @author Andy Wilkinson
* @author Phillip Webb
*/
@Order(ResolveDependencyCoordinatesTransformation.ORDER)
public class ResolveDependencyCoordinatesTransformation
extends AnnotatedNodeASTTransformation {
/**
* The order of the transformation.
*/
public static final int ORDER = DependencyManagementBomTransformation.ORDER + 300;
private static final Set<String> GRAB_ANNOTATION_NAMES = Collections
.unmodifiableSet(new HashSet<>(
Arrays.asList(Grab.class.getName(), Grab.class.getSimpleName())));
private final DependencyResolutionContext resolutionContext;
public ResolveDependencyCoordinatesTransformation(
DependencyResolutionContext resolutionContext) {
super(GRAB_ANNOTATION_NAMES, false);
this.resolutionContext = resolutionContext;
}
@Override
protected void processAnnotationNodes(List<AnnotationNode> annotationNodes) {
for (AnnotationNode annotationNode : annotationNodes) {
transformGrabAnnotation(annotationNode);
}
}
private void transformGrabAnnotation(AnnotationNode grabAnnotation) {
grabAnnotation.setMember("initClass", new ConstantExpression(false));
String value = getValue(grabAnnotation);
if (value != null && !isConvenienceForm(value)) {
applyGroupAndVersion(grabAnnotation, value);
}
}
private String getValue(AnnotationNode annotation) {
Expression expression = annotation.getMember("value");
if (expression instanceof ConstantExpression) {
Object value = ((ConstantExpression) expression).getValue();
return (value instanceof String ? (String) value : null);
}
return null;
}
private boolean isConvenienceForm(String value) {
return value.contains(":") || value.contains("#");
}
private void applyGroupAndVersion(AnnotationNode annotation, String module) {
if (module != null) {
setMember(annotation, "module", module);
}
else {
Expression expression = annotation.getMembers().get("module");
module = (String) ((ConstantExpression) expression).getValue();
}
if (annotation.getMember("group") == null) {
setMember(annotation, "group", this.resolutionContext
.getArtifactCoordinatesResolver().getGroupId(module));
}
if (annotation.getMember("version") == null) {
setMember(annotation, "version", this.resolutionContext
.getArtifactCoordinatesResolver().getVersion(module));
}
}
private void setMember(AnnotationNode annotation, String name, String value) {
ConstantExpression expression = new ConstantExpression(value);
annotation.setMember(name, expression);
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.compiler;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.classgen.GeneratorContext;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.customizers.ImportCustomizer;
/**
* Smart extension of {@link ImportCustomizer} that will only add a specific import if a
* class with the same name is not already explicitly imported.
*
* @author Dave Syer
* @since 1.1
*/
class SmartImportCustomizer extends ImportCustomizer {
private SourceUnit source;
SmartImportCustomizer(SourceUnit source, GeneratorContext context,
ClassNode classNode) {
this.source = source;
}
@Override
public ImportCustomizer addImport(String alias, String className) {
if (this.source.getAST()
.getImport(ClassHelper.make(className).getNameWithoutPackage()) == null) {
super.addImport(alias, className);
}
return this;
}
@Override
public ImportCustomizer addImports(String... imports) {
for (String alias : imports) {
if (this.source.getAST()
.getImport(ClassHelper.make(alias).getNameWithoutPackage()) == null) {
super.addImports(alias);
}
}
return this;
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.compiler;
import org.codehaus.groovy.transform.ASTTransformation;
/**
* Marker interface for AST transformations that should be installed automatically from
* {@code META-INF/services}.
*
* @author Dave Syer
*/
@FunctionalInterface
public interface SpringBootAstTransformation extends ASTTransformation {
}

Some files were not shown because too many files have changed in this diff Show More