Fast forward existing prototype work

This commit is contained in:
Dave Syer
2013-04-24 10:02:07 +01:00
parent 80b151e2b3
commit fb6b224470
294 changed files with 23494 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
<assembly>
<id>dist</id>
<formats>
<format>zip</format>
<format>dir</format>
</formats>
<baseDirectory>spring-${project.version}</baseDirectory>
<includeBaseDirectory>true</includeBaseDirectory>
<fileSets>
<fileSet>
<directory>src/main/scripts</directory>
<outputDirectory>bin</outputDirectory>
<useDefaultExcludes>true</useDefaultExcludes>
</fileSet>
<fileSet>
<directory>src/main/resources</directory>
<outputDirectory>bin</outputDirectory>
<useDefaultExcludes>true</useDefaultExcludes>
<filtered>true</filtered>
</fileSet>
</fileSets>
<dependencySets>
<dependencySet>
<includes>
<include>org.springframework.bootstrap:spring-bootstrap-cli:jar:*</include>
</includes>
<outputDirectory>lib</outputDirectory>
</dependencySet>
</dependencySets>
</assembly>

View File

@@ -0,0 +1,81 @@
/*
* 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.bootstrap.cli;
import java.io.IOException;
import java.io.PrintStream;
/**
* Abstract {@link Command} implementation.
*
* @author Phillip Webb
*/
public abstract class AbstractCommand implements Command {
private String name;
private boolean optionCommand;
private String description;
/**
* Create a new {@link AbstractCommand} instance.
* @param name the name of the command
* @param description the command description
*/
public AbstractCommand(String name, String description) {
this(name, description, false);
}
/**
* Create a new {@link AbstractCommand} instance.
* @param name the name of the command
* @param description the command description
* @param optionCommand if this command is an option command (see
* {@link Command#isOptionCommand()}
*/
public AbstractCommand(String name, String description, boolean optionCommand) {
this.name = name;
this.description = description;
this.optionCommand = optionCommand;
}
@Override
public String getName() {
return this.name;
}
@Override
public String getDescription() {
return this.description;
}
@Override
public boolean isOptionCommand() {
return this.optionCommand;
}
@Override
public String getUsageHelp() {
return null;
}
@Override
public void printHelp(PrintStream out) throws IOException {
}
}

View File

@@ -0,0 +1,96 @@
/*
* 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.bootstrap.cli;
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 SpringBootstrapCli}.
*
* @author Phillip Webb
*/
public class BootstrapCliException extends RuntimeException {
private static final long serialVersionUID = 0L;
private final EnumSet<Option> options;
/**
* Create a new {@link BootstrapCliException} with the specified options.
* @param options the exception options
*/
public BootstrapCliException(Option... options) {
this.options = asEnumSet(options);
}
/**
* Create a new {@link BootstrapCliException} with the specified options.
* @param message the exception message to display to the user
* @param options the exception options
*/
public BootstrapCliException(String message, Option... options) {
super(message);
this.options = asEnumSet(options);
}
/**
* Create a new {@link BootstrapCliException} 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 BootstrapCliException(String message, Throwable cause, Option... options) {
super(message, 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 options a set of options that are understood by the
* {@link SpringBootstrapCli}.
*/
public Set<Option> getOptions() {
return Collections.unmodifiableSet(this.options);
}
/**
* Specific options understood by the {@link SpringBootstrapCli}.
*/
public static enum Option {
/**
* Print basic CLI usage information.
*/
SHOW_USAGE,
/**
* Print the stack-trace of the exception.
*/
STACK_TRACE
}
}

View File

@@ -0,0 +1,68 @@
/*
* 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.bootstrap.cli;
import java.io.IOException;
import java.io.PrintStream;
/**
* A single command that can be run from the CLI.
*
* @author Phillip Webb
* @see #run(String...)
*/
public interface Command {
/**
* Returns the name of the command.
*/
String getName();
/**
* Returns {@code true} if this is an 'option command'. An option command is a special
* type of command that usually makes more sense to present as if it is an option. For
* example '--help'.
*/
boolean isOptionCommand();
/**
* Returns a description of the command.
*/
String getDescription();
/**
* Returns usage help for the command. This should be a simple one-line string
* describing basic usage. eg. '[options] &lt;file&gt;'. Do not include the name of
* the command in this string.
*/
String getUsageHelp();
/**
* Prints help for the command.
* @param out the output writer to display help
* @throws IOException
*/
void printHelp(PrintStream out) throws IOException;
/**
* Run the command.
* @param args command arguments (this will not include the command itself)
* @throws Exception
*/
void run(String... args) throws Exception;
}

View File

@@ -0,0 +1,54 @@
/*
* 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.bootstrap.cli;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import static java.util.Arrays.*;
/**
* {@link Command} to 'create' a new spring groovy script.
*
* @author Phillip Webb
*/
public class CreateCommand extends OptionParsingCommand {
public CreateCommand() {
super("create", "Create an new spring groovy script");
}
@Override
public String getUsageHelp() {
return "[options] <file>";
}
@Override
protected OptionParser createOptionParser() {
OptionParser parser = new OptionParser();
parser.acceptsAll(asList("overwite", "f"), "Overwrite any existing file");
parser.accepts("type", "Create a specific application type").withOptionalArg()
.ofType(String.class).describedAs("web, batch, integration");
return parser;
}
@Override
protected void run(OptionSet options) {
throw new IllegalStateException("Not implemented"); // FIXME
}
}

View File

@@ -0,0 +1,28 @@
/*
* 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.bootstrap.cli;
/**
* Exception thrown when no CLI options are specified.
*
* @author Phillip Webb
*/
class NoArgumentsException extends BootstrapCliException {
private static final long serialVersionUID = 1L;
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2012-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* 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.bootstrap.cli;
/**
* Exception thrown when the 'help' command is issued without any arguments.
*
* @author Phillip Webb
*/
class NoHelpCommandArgumentsException extends BootstrapCliException {
private static final long serialVersionUID = 1L;
public NoHelpCommandArgumentsException() {
super(Option.SHOW_USAGE);
}
}

View File

@@ -0,0 +1,33 @@
/*
* 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.bootstrap.cli;
/**
* Exception thrown when an unknown command is specified.
*
* @author Phillip Webb
*/
class NoSuchCommandException extends BootstrapCliException {
private static final long serialVersionUID = 1L;
public NoSuchCommandException(String name) {
super(String.format("%1$s: '%2$s' is not a valid command. See '%1$s --help'.",
SpringBootstrapCli.CLI_APP, name));
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2012-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* 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.bootstrap.cli;
/**
* Exception thrown when an unknown root option is specified. This only applies to
* {@link Command#isOptionCommand() option command}.
*
* @author Phillip Webb
*/
class NoSuchOptionException extends BootstrapCliException {
private static final long serialVersionUID = 1L;
public NoSuchOptionException(String name) {
super("Unknown option: --" + name, Option.SHOW_USAGE);
}
}

View File

@@ -0,0 +1,54 @@
/*
* 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.bootstrap.cli;
import java.io.IOException;
import java.io.PrintStream;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
/**
* Base class for any {@link Command}s that use an {@link OptionParser}.
*
* @author Phillip Webb
*/
public abstract class OptionParsingCommand extends AbstractCommand {
private OptionParser parser;
public OptionParsingCommand(String name, String description) {
super(name, description);
this.parser = createOptionParser();
}
protected abstract OptionParser createOptionParser();
@Override
public void printHelp(PrintStream out) throws IOException {
this.parser.printHelpOn(out);
}
@Override
public final void run(String... args) throws Exception {
OptionSet options = parser.parse(args);
run(options);
}
protected abstract void run(OptionSet options) throws Exception;
}

View File

@@ -0,0 +1,146 @@
/*
* 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.bootstrap.cli;
import java.awt.Desktop;
import java.io.File;
import java.util.List;
import java.util.logging.Level;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.springframework.bootstrap.cli.runner.BootstrapRunner;
import org.springframework.bootstrap.cli.runner.BootstrapRunnerConfiguration;
import static java.util.Arrays.asList;
/**
* {@link Command} to 'run' a spring groovy script.
*
* @author Phillip Webb
* @see BootstrapRunner
*/
public class RunCommand extends OptionParsingCommand {
private OptionSpec<Void> noWatchOption; // FIXME
private OptionSpec<Void> editOption;
private OptionSpec<Void> noGuessImportsOption;
private OptionSpec<Void> noGuessDependenciesOption;
private OptionSpec<Void> verboseOption;
private OptionSpec<Void> quiteOption;
public RunCommand() {
super("run", "Run a spring groovy script");
}
@Override
public String getUsageHelp() {
return "[options] <file>";
}
@Override
protected OptionParser createOptionParser() {
OptionParser parser = new OptionParser();
this.noWatchOption = parser.accepts("no-watch",
"Do not watch the specified file for changes");
this.editOption = parser.acceptsAll(asList("edit", "e"),
"Open the file with the default system editor");
this.noGuessImportsOption = parser.accepts("no-guess-imports",
"Do not attempt to guess imports");
this.noGuessDependenciesOption = parser.accepts("no-guess-dependencies",
"Do not attempt to guess dependencies");
this.verboseOption = parser.acceptsAll(asList("verbose", "v"), "Verbose logging");
this.quiteOption = parser.acceptsAll(asList("quiet", "q"), "Quiet logging");
return parser;
}
@Override
protected void run(OptionSet options) throws Exception {
List<String> nonOptionArguments = options.nonOptionArguments();
File file = getFileArgument(nonOptionArguments);
List<String> args = nonOptionArguments.subList(1, nonOptionArguments.size());
if (options.has(this.editOption)) {
Desktop.getDesktop().edit(file);
}
BootstrapRunnerConfiguration configuration = new BootstrapRunnerConfigurationAdapter(
options);
new BootstrapRunner(configuration, file, args.toArray(new String[args.size()]))
.compileAndRun();
}
private File getFileArgument(List<String> nonOptionArguments) {
if (nonOptionArguments.size() == 0) {
throw new RuntimeException("Please specify a file to run");
}
String filename = nonOptionArguments.get(0);
File file = new File(filename);
if (!file.isFile() || !file.canRead()) {
throw new RuntimeException("Unable to read '" + filename + "'");
}
return file;
}
/**
* Simple adapter class to present the {@link OptionSet} as a
* {@link BootstrapRunnerConfiguration}.
*/
private class BootstrapRunnerConfigurationAdapter implements
BootstrapRunnerConfiguration {
private OptionSet options;
public BootstrapRunnerConfigurationAdapter(OptionSet options) {
this.options = options;
}
@Override
public boolean isWatchForFileChanges() {
return !this.options.has(RunCommand.this.noWatchOption);
}
@Override
public boolean isGuessImports() {
return !this.options.has(RunCommand.this.noGuessImportsOption);
}
@Override
public boolean isGuessDependencies() {
return !this.options.has(RunCommand.this.noGuessDependenciesOption);
}
@Override
public Level getLogLevel() {
if (this.options.has(RunCommand.this.verboseOption)) {
return Level.FINEST;
}
if (this.options.has(RunCommand.this.quiteOption)) {
return Level.OFF;
}
return Level.INFO;
}
}
}

View File

@@ -0,0 +1,214 @@
/*
* 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.bootstrap.cli;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
/**
* Spring Bootstrap Command Line Interface. This is the main entry-point for the spring
* bootstrap command line application. This class will parse input arguments and delegate
* to a suitable {@link Command} implementation based on the first argument.
*
* <p>
* The '-d' and '--debug' switches are handled by this class, however, most argument
* parsing is left to the {@link Command} implementation. The {@link OptionParsingCommand}
* class provides a convenient base for command that need to parse arguments.
*
* @author Phillip Webb
* @see #main(String...)
* @see BootstrapCliException
* @see Command
* @see OptionParsingCommand
*/
public class SpringBootstrapCli {
public static final String CLI_APP = "spr";
private static final Set<BootstrapCliException.Option> NO_EXCEPTION_OPTIONS = EnumSet
.noneOf(BootstrapCliException.Option.class);
private List<Command> commands;
/**
* Create a new {@link SpringBootstrapCli} implementation with the default set of
* commands.
*/
public SpringBootstrapCli() {
setCommands(Arrays.asList(new VersionCommand(), new RunCommand(),
new CreateCommand()));
}
/**
* Set the command available to the CLI. Primarily used to support testing. NOTE: The
* 'help' command will be automatically provided in addition to this list.
* @param commands the commands to add
*/
protected void setCommands(List<? extends Command> commands) {
this.commands = new ArrayList<Command>(commands);
this.commands.add(0, new HelpCommand());
}
/**
* Run the CLI and handle and errors.
* @param args the input arguments
* @return a return status code (non zero is used to indicate an error)
*/
public int runAndHandleErrors(String... args) {
String[] argsWithoutDebugFlags = removeDebugFlags(args);
boolean debug = argsWithoutDebugFlags.length != args.length;
try {
run(argsWithoutDebugFlags);
return 0;
} catch (NoArgumentsException ex) {
showUsage();
return 1;
} catch (Exception ex) {
Set<BootstrapCliException.Option> options = NO_EXCEPTION_OPTIONS;
if (ex instanceof BootstrapCliException) {
options = ((BootstrapCliException) ex).getOptions();
}
errorMessage(ex.getMessage());
if (options.contains(BootstrapCliException.Option.SHOW_USAGE)) {
showUsage();
}
if (debug || options.contains(BootstrapCliException.Option.STACK_TRACE)) {
printStackTrace(ex);
}
return 1;
}
}
/**
* Parse the arguments and run a suitable command.
* @param args the arguments
* @throws Exception
*/
protected void run(String... args) throws Exception {
if (args.length == 0) {
throw new NoArgumentsException();
}
String commandName = args[0];
String[] commandArguments = Arrays.copyOfRange(args, 1, args.length);
find(commandName).run(commandArguments);
}
private Command find(String name) {
boolean isOption = name.startsWith("--");
if (isOption) {
name = name.substring(2);
}
for (Command candidate : this.commands) {
if ((isOption && candidate.isOptionCommand() || !isOption)
&& candidate.getName().equals(name)) {
return candidate;
}
}
throw (isOption ? new NoSuchOptionException(name) : new NoSuchCommandException(
name));
}
protected void showUsage() {
System.out.print("usage: " + CLI_APP + " ");
for (Command command : this.commands) {
if (command.isOptionCommand()) {
System.out.print("[--" + command.getName() + "] ");
}
}
System.out.println("");
System.out.println(" <command> [<args>]");
System.out.println("");
System.out.println("Available commands are:");
for (Command command : this.commands) {
if (!command.isOptionCommand()) {
System.out.println(String.format(" %1$-15s %2$s", command.getName(),
command.getDescription()));
}
}
System.out.println("");
System.out
.println("See 'spr help <command>' for more information on a specific command.");
}
protected void errorMessage(String message) {
System.err.println(message == null ? "Unexpected error" : message);
}
protected void printStackTrace(Exception ex) {
System.err.println("");
ex.printStackTrace(System.err);
System.err.println("");
}
private String[] removeDebugFlags(String[] args) {
List<String> rtn = new ArrayList<String>(args.length);
for (String arg : args) {
if (!("-d".equals(arg) || "--debug".equals(arg))) {
rtn.add(arg);
}
}
return rtn.toArray(new String[rtn.size()]);
}
/**
* Internal {@link Command} used for 'help' and '--help' requests.
*/
private class HelpCommand extends AbstractCommand {
public HelpCommand() {
super("help", "Show command help", true);
}
@Override
public void run(String... args) throws Exception {
if (args.length == 0) {
throw new NoHelpCommandArgumentsException();
}
String commandName = args[0];
for (Command command : SpringBootstrapCli.this.commands) {
if (!command.isOptionCommand() && command.getName().equals(commandName)) {
System.out.println(CLI_APP + " " + command.getName() + " - "
+ command.getDescription());
System.out.println();
if (command.getUsageHelp() != null) {
System.out.println("usage: " + CLI_APP + " " + command.getName()
+ " " + command.getUsageHelp());
System.out.println();
}
command.printHelp(System.out);
return;
}
}
throw new NoSuchCommandException(commandName);
}
}
/**
* The main CLI entry-point.
* @param args CLI arguments
*/
public static void main(String... args) {
int exitCode = new SpringBootstrapCli().runAndHandleErrors(args);
if (exitCode != 0) {
System.exit(exitCode);
}
}
}

View File

@@ -0,0 +1,35 @@
/*
* 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.bootstrap.cli;
/**
* {@link Command} to displat the 'version' number.
*
* @author Phillip Webb
*/
public class VersionCommand extends AbstractCommand {
public VersionCommand() {
super("version", "Show the version", true);
}
@Override
public void run(String... args) {
throw new IllegalStateException("Not implemented"); // FIXME
}
}

View File

@@ -0,0 +1,45 @@
/*
* 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.bootstrap.cli.compiler;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.AnnotationNode;
/**
* General purpose AST utilities.
*
* @author Phillip Webb
*/
public abstract class AstUtils {
/**
* Determine if an {@link AnnotatedNode} has one or more of the specified annotations.
*/
public static boolean hasLeastOneAnnotation(AnnotatedNode node, String... annotations) {
for (AnnotationNode annotationNode : node.getAnnotations()) {
for (String annotation : annotations) {
if (annotation.equals(annotationNode.getClassNode().getName())) {
return true;
}
}
}
return false;
}
}

View File

@@ -0,0 +1,83 @@
/*
* 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.bootstrap.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
*/
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
*/
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.
*/
public void applyToMainClass(GroovyClassLoader loader,
GroovyCompilerConfiguration configuration, GeneratorContext generatorContext,
SourceUnit source, ClassNode classNode) throws CompilationFailedException {
}
/**
* Apply any additional configuration.
*/
public void apply(GroovyClassLoader loader,
GroovyCompilerConfiguration configuration, GeneratorContext generatorContext,
SourceUnit source, ClassNode classNode) throws CompilationFailedException {
}
}

View File

@@ -0,0 +1,130 @@
/*
* 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.bootstrap.cli.compiler;
import groovy.grape.Grape;
import groovy.lang.Grapes;
import groovy.lang.GroovyClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Customizer that allows dependencies to be added during compilation. Delegates to Groovy
* {@link Grapes} to actually resolve dependencies. This class provides a fluent API for
* conditionally adding dependencies. For example:
* {@code dependencies.ifMissing("com.corp.SomeClass").add(group, module, version)}.
*
* @author Phillip Webb
*/
public class DependencyCustomizer {
private final GroovyClassLoader loader;
private final List<Map<String, Object>> dependencies;
/**
* Create a new {@link DependencyCustomizer} instance. The {@link #call()} method must
* be used to actually resolve dependencies.
* @param loader
*/
public DependencyCustomizer(GroovyClassLoader loader) {
this.loader = loader;
this.dependencies = new ArrayList<Map<String, Object>>();
}
/**
* Create a new nested {@link DependencyCustomizer}.
* @param parent
*/
protected DependencyCustomizer(DependencyCustomizer parent) {
this.loader = parent.loader;
this.dependencies = parent.dependencies;
}
/**
* Create a nested {@link DependencyCustomizer} that only applies if the specified
* class names are not on the class path.
* @param classNames the class names to test
* @return a nested {@link DependencyCustomizer}
*/
public DependencyCustomizer ifMissingClasses(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 e) {
}
}
return DependencyCustomizer.this.canAdd();
}
};
}
/**
* Add a single dependencies.
* @param group the group ID
* @param module the module ID
* @param version the version
* @return this {@link DependencyCustomizer} for continued use
*/
@SuppressWarnings("unchecked")
public DependencyCustomizer add(String group, String module, String version) {
if (canAdd()) {
Map<String, Object> dependency = new HashMap<String, Object>();
dependency.put("group", group);
dependency.put("module", module);
dependency.put("version", version);
dependency.put("transitive", true);
return add(dependency);
}
return this;
}
/**
* Add a dependencies.
* @param dependencies a map of the dependencies to add.
* @return this {@link DependencyCustomizer} for continued use
*/
public DependencyCustomizer add(Map<String, Object>... dependencies) {
this.dependencies.addAll(Arrays.asList(dependencies));
return this;
}
/**
* Strategy called to test if dependencies can be added. Subclasses override as
* requred.
*/
protected boolean canAdd() {
return true;
}
/**
* Apply the dependencies.
*/
void call() {
HashMap<String, Object> args = new HashMap<String, Object>();
args.put("classLoader", this.loader);
Grape.grab(args, this.dependencies.toArray(new Map[this.dependencies.size()]));
}
}

View File

@@ -0,0 +1,88 @@
/*
* 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.bootstrap.cli.compiler;
import groovy.lang.GroovyClassLoader;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.HashMap;
import java.util.Map;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.SourceUnit;
/**
* Extension of the {@link GroovyClassLoader} that support for obtaining '.class' files as
* resources.
*
* @author Phillip Webb
*/
class ExtendedGroovyClassLoader extends GroovyClassLoader {
private Map<String, byte[]> classResources = new HashMap<String, byte[]>();
public ExtendedGroovyClassLoader(ClassLoader loader, CompilerConfiguration config) {
super(loader, config);
}
@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
protected ClassCollector createCollector(CompilationUnit unit, SourceUnit su) {
InnerLoader loader = AccessController
.doPrivileged(new PrivilegedAction<InnerLoader>() {
@Override
public InnerLoader run() {
return new InnerLoader(ExtendedGroovyClassLoader.this);
}
});
return new ExtendedClassCollector(loader, unit, su);
}
/**
* 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;
}
}
}

View File

@@ -0,0 +1,148 @@
/*
* 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.bootstrap.cli.compiler;
import groovy.lang.GroovyClassLoader;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
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.CompilerConfiguration;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.customizers.CompilationCustomizer;
import org.codehaus.groovy.control.customizers.ImportCustomizer;
import org.springframework.bootstrap.cli.compiler.autoconfigure.SpringBootstrapCompilerAutoConfiguration;
import org.springframework.bootstrap.cli.compiler.autoconfigure.SpringMvcCompilerAutoConfiguration;
/**
* Compiler for Groovy source files. Primarily a simple Facade for
* {@link GroovyClassLoader#parseClass(File)} with the following additional features:
* <ul>
* <li>{@link CompilerAutoConfiguration} strategies will de 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
*/
public class GroovyCompiler {
// FIXME could be a strategy
private static final CompilerAutoConfiguration[] COMPILER_AUTO_CONFIGURATIONS = {
new SpringBootstrapCompilerAutoConfiguration(),
new SpringMvcCompilerAutoConfiguration(),
new SpringBootstrapCompilerAutoConfiguration() };
private GroovyCompilerConfiguration configuration;
private ExtendedGroovyClassLoader loader;
/**
* Create a new {@link GroovyCompiler} instance.
* @param configuration the compiler configuration
*/
public GroovyCompiler(final GroovyCompilerConfiguration configuration) {
this.configuration = configuration;
CompilerConfiguration compilerConfiguration = new CompilerConfiguration();
this.loader = new ExtendedGroovyClassLoader(getClass().getClassLoader(),
compilerConfiguration);
compilerConfiguration
.addCompilationCustomizers(new CompilerAutoConfigureCustomizer());
}
/**
* Compile the specified Groovy source files, applying any
* {@link CompilerAutoConfiguration}s. All classes defined in the file will be
* returned from this method with the first item being the primary class (defined at
* the top of the file).
* @param file the file to compile
* @return compiled classes
* @throws CompilationFailedException
* @throws IOException
*/
public Class<?>[] compile(File file) throws CompilationFailedException, IOException {
this.loader.clearCache();
List<Class<?>> classes = new ArrayList<Class<?>>();
Class<?> mainClass = this.loader.parseClass(file);
for (Class<?> loadedClass : this.loader.getLoadedClasses()) {
classes.add(loadedClass);
}
classes.remove(mainClass);
classes.add(0, mainClass);
return classes.toArray(new Class<?>[classes.size()]);
}
/**
* {@link CompilationCustomizer} to call {@link CompilerAutoConfiguration}s.
*/
private class CompilerAutoConfigureCustomizer extends CompilationCustomizer {
public CompilerAutoConfigureCustomizer() {
super(CompilePhase.CONVERSION);
}
@Override
public void call(SourceUnit source, GeneratorContext context, ClassNode classNode)
throws CompilationFailedException {
ImportCustomizer importCustomizer = new ImportCustomizer();
// Early sweep to get dependencies
DependencyCustomizer dependencyCustomizer = new DependencyCustomizer(
GroovyCompiler.this.loader);
for (CompilerAutoConfiguration autoConfiguration : COMPILER_AUTO_CONFIGURATIONS) {
if (autoConfiguration.matches(classNode)) {
if (GroovyCompiler.this.configuration.isGuessDependencies()) {
autoConfiguration.applyDependencies(dependencyCustomizer);
}
}
}
dependencyCustomizer.call();
// Additional auto configuration
for (CompilerAutoConfiguration autoConfiguration : COMPILER_AUTO_CONFIGURATIONS) {
if (autoConfiguration.matches(classNode)) {
if (GroovyCompiler.this.configuration.isGuessImports()) {
autoConfiguration.applyImports(importCustomizer);
importCustomizer.call(source, context, classNode);
}
if (source.getAST().getClasses().size() > 0
&& classNode.equals(source.getAST().getClasses().get(0))) {
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);
}
}
}

View File

@@ -0,0 +1,36 @@
/*
* 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.bootstrap.cli.compiler;
/**
* Configuration for the {@link GroovyCompiler}.
*
* @author Phillip Webb
*/
public interface GroovyCompilerConfiguration {
/**
* Returns if import declarations should be guessed.
*/
boolean isGuessImports();
/**
* Returns if jar dependencies should be guessed.
*/
boolean isGuessDependencies();
}

View File

@@ -0,0 +1,64 @@
/*
* 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.bootstrap.cli.compiler.autoconfigure;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.control.customizers.ImportCustomizer;
import org.springframework.bootstrap.cli.compiler.AstUtils;
import org.springframework.bootstrap.cli.compiler.CompilerAutoConfiguration;
import org.springframework.bootstrap.cli.compiler.DependencyCustomizer;
/**
* {@link CompilerAutoConfiguration} for Spring Batch.
*
* @author Dave Syer
* @author Phillip Webb
*/
public class SpringBatchCompilerAutoConfiguration extends CompilerAutoConfiguration {
@Override
public boolean matches(ClassNode classNode) {
return AstUtils.hasLeastOneAnnotation(classNode, "EnableBatchProcessing");
}
@Override
public void applyDependencies(DependencyCustomizer dependencies) {
dependencies.ifMissingClasses("org.springframework.batch.core.Job").add(
"org.springframework.batch", "spring-batch-core", "2.1.9.RELEASE");
}
@Override
public void applyImports(ImportCustomizer imports) {
imports.addImports(
"org.springframework.batch.repeat.RepeatStatus",
"org.springframework.batch.core.scope.context.ChunkContext",
"org.springframework.batch.core.step.tasklet.Tasklet",
"org.springframework.batch.core.configuration.annotation.StepScope",
"org.springframework.batch.core.configuration.annotation.JobBuilderFactory",
"org.springframework.batch.core.configuration.annotation.StepBuilderFactory",
"org.springframework.batch.core.configuration.annotation.EnableBatchProcessing",
"org.springframework.batch.core.Step",
"org.springframework.batch.core.StepExecution",
"org.springframework.batch.core.StepContribution",
"org.springframework.batch.core.Job",
"org.springframework.batch.core.JobExecution",
"org.springframework.batch.core.JobParameter",
"org.springframework.batch.core.JobParameters",
"org.springframework.batch.core.launch.JobLauncher",
"org.springframework.batch.core.converter.DefaultJobParametersConverter");
}
}

View File

@@ -0,0 +1,97 @@
/*
* 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.bootstrap.cli.compiler.autoconfigure;
import groovy.lang.GroovyClassLoader;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.classgen.GeneratorContext;
import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.customizers.ImportCustomizer;
import org.springframework.bootstrap.cli.compiler.CompilerAutoConfiguration;
import org.springframework.bootstrap.cli.compiler.DependencyCustomizer;
import org.springframework.bootstrap.cli.compiler.GroovyCompilerConfiguration;
/**
* {@link CompilerAutoConfiguration} for Spring Bootstrap.
*
* @author Dave Syer
* @author Phillip Webb
*/
public class SpringBootstrapCompilerAutoConfiguration extends CompilerAutoConfiguration {
@Override
public void applyDependencies(DependencyCustomizer dependencies) {
dependencies.ifMissingClasses("org.springframework.bootstrap.SpringApplication")
.add("org.springframework.bootstrap", "spring-bootstrap-application",
"0.0.1-SNAPSHOT");
// FIXME get the version
}
@Override
public void applyImports(ImportCustomizer imports) {
imports.addImports("javax.sql.DataSource",
"org.springframework.beans.factory.annotation.Autowired",
"org.springframework.beans.factory.annotation.Value",
"org.springframework.context.annotation.Import",
"org.springframework.context.annotation.ImportResource",
"org.springframework.context.annotation.Profile",
"org.springframework.context.annotation.Scope",
"org.springframework.context.annotation.Configuration",
"org.springframework.context.annotation.Bean",
"org.springframework.bootstrap.context.annotation.EnableAutoConfiguration");
imports.addStarImports("org.springframework.stereotype");
}
@Override
public void applyToMainClass(GroovyClassLoader loader,
GroovyCompilerConfiguration configuration, GeneratorContext generatorContext,
SourceUnit source, ClassNode classNode) throws CompilationFailedException {
if (true) {
addEnableAutoConfigurationAnnotation(source, classNode);
}
}
private void addEnableAutoConfigurationAnnotation(SourceUnit source,
ClassNode classNode) {
if (!hasEnableAutoConfigureAnnotation(classNode)) {
try {
Class<?> annotationClass = source
.getClassLoader()
.loadClass(
"org.springframework.bootstrap.context.annotation.EnableAutoConfiguration");
AnnotationNode annotationNode = new AnnotationNode(new ClassNode(
annotationClass));
classNode.addAnnotation(annotationNode);
} catch (ClassNotFoundException e) {
throw new IllegalStateException(e);
}
}
}
private boolean hasEnableAutoConfigureAnnotation(ClassNode classNode) {
for (AnnotationNode node : classNode.getAnnotations()) {
if ("EnableAutoConfiguration".equals(node.getClassNode()
.getNameWithoutPackage())) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,60 @@
/*
* 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.bootstrap.cli.compiler.autoconfigure;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.control.customizers.ImportCustomizer;
import org.springframework.bootstrap.cli.compiler.AstUtils;
import org.springframework.bootstrap.cli.compiler.CompilerAutoConfiguration;
import org.springframework.bootstrap.cli.compiler.DependencyCustomizer;
/**
* {@link CompilerAutoConfiguration} for Spring MVC.
*
* @author Dave Syer
* @author Phillip Webb
*/
public class SpringMvcCompilerAutoConfiguration extends CompilerAutoConfiguration {
@Override
public void applyDependencies(DependencyCustomizer dependencies) {
dependencies.ifMissingClasses("org.springframework.web.servlet.mvc.Controller")
.add("org.springframework", "spring-webmvc", "4.0.0.BOOTSTRAP-SNAPSHOT");
dependencies.ifMissingClasses("org.apache.catalina.startup.Tomcat",
"org.eclipse.jetty.server.Server").add("org.eclipse.jetty",
"jetty-webapp", "8.1.10.v20130312");
// FIXME restore Tomcat when we can get reload to work
// dependencies.ifMissingClasses("org.apache.catalina.startup.Tomcat",
// "org.eclipse.jetty.server.Server")
// .add("org.apache.tomcat.embed", "tomcat-embed-core", "7.0.37")
// .add("org.apache.tomcat.embed", "tomcat-embed-logging-juli", "7.0.37");
}
@Override
public boolean matches(ClassNode classNode) {
return AstUtils.hasLeastOneAnnotation(classNode, "Controller", "EnableWebMvc");
}
@Override
public void applyImports(ImportCustomizer imports) {
imports.addStarImports("org.springframework.web.bind.annotation",
"org.springframework.web.servlet.config.annotation");
}
}

View File

@@ -0,0 +1,191 @@
/*
* 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.bootstrap.cli.runner;
import java.io.File;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import org.springframework.bootstrap.cli.compiler.GroovyCompiler;
/**
* Compiles Groovy code running the resulting classes using a {@code SpringApplication}.
* Takes care of threading and class-loading issues and can optionally monitor files for
* changes.
*
* @author Phillip Webb
*/
public class BootstrapRunner {
// FIXME logging
private BootstrapRunnerConfiguration configuration;
private final File file;
private final String[] args;
private final GroovyCompiler compiler;
private RunThread runThread;
private FileWatchThread fileWatchThread;
/**
* Create a new {@link BootstrapRunner} instance.
* @param configuration the configuration
* @param file the file to compile/watch
* @param args input arguments
*/
public BootstrapRunner(final BootstrapRunnerConfiguration configuration, File file,
String... args) {
this.configuration = configuration;
this.file = file;
this.args = args;
this.compiler = new GroovyCompiler(configuration);
if (configuration.getLogLevel().intValue() <= Level.FINE.intValue()) {
System.setProperty("groovy.grape.report.downloads", "true");
}
}
/**
* Compile and run the application. This method is synchronized as it can be called by
* file monitoring threads.
* @throws Exception
*/
public synchronized void compileAndRun() throws Exception {
try {
// Shutdown gracefully any running container
if (this.runThread != null) {
this.runThread.shutdown();
this.runThread = null;
}
// Compile
Class<?>[] classes = this.compiler.compile(this.file);
if (classes.length == 0) {
throw new RuntimeException("No classes found in '" + this.file + "'");
}
// Run in new thread to ensure that the context classloader is setup
this.runThread = new RunThread(classes);
this.runThread.start();
this.runThread.join();
// Start monitoring for changes
if (this.fileWatchThread == null
&& this.configuration.isWatchForFileChanges()) {
this.fileWatchThread = new FileWatchThread();
this.fileWatchThread.start();
}
} catch (Exception ex) {
if (this.fileWatchThread == null) {
throw ex;
} else {
ex.printStackTrace();
}
}
}
/**
* Thread used to launch the Spring Application with the correct context classloader.
*/
private class RunThread extends Thread {
private final Class<?>[] classes;
private Object applicationContext;
/**
* Create a new {@link RunThread} instance.
* @param classes the classes to launch
*/
public RunThread(Class<?>... classes) {
this.classes = classes;
if (classes.length != 0) {
setContextClassLoader(classes[0].getClassLoader());
}
}
@Override
public void run() {
try {
// User reflection to load and call Spring
Class<?> application = getContextClassLoader().loadClass(
"org.springframework.bootstrap.SpringApplication");
Method method = application.getMethod("run", Object[].class,
String[].class);
this.applicationContext = method.invoke(null, this.classes,
BootstrapRunner.this.args);
} catch (Exception ex) {
ex.printStackTrace();
}
}
/**
* Shutdown the thread, closing any previously opened appplication context.
*/
public synchronized void shutdown() {
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;
public FileWatchThread() {
this.previous = BootstrapRunner.this.file.lastModified();
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(TimeUnit.SECONDS.toMillis(1));
long current = BootstrapRunner.this.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,41 @@
/*
* 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.bootstrap.cli.runner;
import java.util.logging.Level;
import org.springframework.bootstrap.cli.compiler.GroovyCompilerConfiguration;
/**
* Configuration for the {@link BootstrapRunner}.
*
* @author Phillip Webb
*/
public interface BootstrapRunnerConfiguration extends GroovyCompilerConfiguration {
/**
* Returns {@code true} if the source file should be monitored for changes and
* automatically recompiled.
*/
boolean isWatchForFileChanges();
/**
* Returns the logging level to use.
*/
Level getLogLevel();
}

View File

View File

@@ -0,0 +1,186 @@
package org.springframework.bootstrap.cli;
import java.io.PrintStream;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Set;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willThrow;
import static org.mockito.Matchers.anyObject;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link SpringBootstrapCli}.
*
* @author Phillip Webb
*/
public class SpringBootstrapCliTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
private SpringBootstrapCli cli;
@Mock
private Command regularCommand;
@Mock
private Command optionCommand;
private Set<Call> calls = EnumSet.noneOf(Call.class);
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
this.cli = new SpringBootstrapCli() {
@Override
protected void showUsage() {
SpringBootstrapCliTests.this.calls.add(Call.SHOW_USAGE);
super.showUsage();
};
@Override
protected void errorMessage(String message) {
SpringBootstrapCliTests.this.calls.add(Call.ERROR_MESSAGE);
super.errorMessage(message);
}
@Override
protected void printStackTrace(Exception ex) {
SpringBootstrapCliTests.this.calls.add(Call.PRINT_STACK_TRACE);
super.printStackTrace(ex);
}
};
given(this.regularCommand.getName()).willReturn("command");
given(this.regularCommand.getDescription()).willReturn("A regular command");
given(this.optionCommand.getName()).willReturn("option");
given(this.optionCommand.getDescription()).willReturn("An optional command");
given(this.optionCommand.isOptionCommand()).willReturn(true);
this.cli.setCommands(Arrays.asList(this.regularCommand, this.optionCommand));
}
@Test
public void runWithoutArguments() throws Exception {
this.thrown.expect(NoArgumentsException.class);
this.cli.run();
}
@Test
public void runCommand() throws Exception {
this.cli.run("command", "--arg1", "arg2");
verify(this.regularCommand).run("--arg1", "arg2");
}
@Test
public void runOptionCommand() throws Exception {
this.cli.run("--option", "--arg1", "arg2");
verify(this.optionCommand).run("--arg1", "arg2");
}
@Test
public void runOptionCommandWithoutOption() throws Exception {
this.cli.run("option", "--arg1", "arg2");
verify(this.optionCommand).run("--arg1", "arg2");
}
@Test
public void runOptionOnNonOptionCommand() throws Exception {
this.thrown.expect(NoSuchOptionException.class);
this.cli.run("--command", "--arg1", "arg2");
}
@Test
public void missingCommand() throws Exception {
this.thrown.expect(NoSuchCommandException.class);
this.cli.run("missing");
}
@Test
public void handlesSuccess() throws Exception {
int status = this.cli.runAndHandleErrors("--option");
assertThat(status, equalTo(0));
assertThat(this.calls, equalTo((Set<Call>) EnumSet.noneOf(Call.class)));
}
@Test
public void handlesNoArgumentsException() throws Exception {
int status = this.cli.runAndHandleErrors();
assertThat(status, equalTo(1));
assertThat(this.calls, equalTo((Set<Call>) EnumSet.of(Call.SHOW_USAGE)));
}
@Test
public void handlesNoSuchOptionException() throws Exception {
int status = this.cli.runAndHandleErrors("--missing");
assertThat(status, equalTo(1));
assertThat(this.calls,
equalTo((Set<Call>) EnumSet.of(Call.ERROR_MESSAGE, Call.SHOW_USAGE)));
}
@Test
public void handlesRegularException() throws Exception {
willThrow(new RuntimeException()).given(this.regularCommand).run();
int status = this.cli.runAndHandleErrors("command");
assertThat(status, equalTo(1));
assertThat(this.calls, equalTo((Set<Call>) EnumSet.of(Call.ERROR_MESSAGE)));
}
@Test
public void handlesExceptionWithDashD() throws Exception {
willThrow(new RuntimeException()).given(this.regularCommand).run();
int status = this.cli.runAndHandleErrors("command", "-d");
assertThat(status, equalTo(1));
assertThat(this.calls, equalTo((Set<Call>) EnumSet.of(Call.ERROR_MESSAGE,
Call.PRINT_STACK_TRACE)));
}
@Test
public void handlesExceptionWithDashDashDebug() throws Exception {
willThrow(new RuntimeException()).given(this.regularCommand).run();
int status = this.cli.runAndHandleErrors("command", "--debug");
assertThat(status, equalTo(1));
assertThat(this.calls, equalTo((Set<Call>) EnumSet.of(Call.ERROR_MESSAGE,
Call.PRINT_STACK_TRACE)));
}
@Test
public void exceptionMessages() throws Exception {
assertThat(new NoSuchOptionException("name").getMessage(),
equalTo("Unknown option: --name"));
assertThat(new NoSuchCommandException("name").getMessage(),
equalTo("spr: 'name' is not a valid command. See 'spr --help'."));
}
@Test
public void help() throws Exception {
this.cli.run("help", "command");
verify(this.regularCommand).printHelp((PrintStream) anyObject());
}
@Test
public void helpNoCommand() throws Exception {
this.thrown.expect(NoHelpCommandArgumentsException.class);
this.cli.run("help");
}
@Test
public void helpUnknownCommand() throws Exception {
this.thrown.expect(NoSuchCommandException.class);
this.cli.run("help", "missing");
}
private static enum Call {
SHOW_USAGE, ERROR_MESSAGE, PRINT_STACK_TRACE
}
}