Skip to content

Commit ac34f9c

Browse files
author
Dave Syer
committed
First proper draft of DSL for Groovy Commands
Users can declare or Command, OptionHandler classes in an init script or they can use a DSL, e.g. command("foo") { args -> println "Do stuff with ${args} array" } or command("foo") { options { option "bar", "Help text for bar option" ithOptionArg() ofType Integer } run { options -> println "Do stuff with ${options.valueOf('bar')}" } }
1 parent 1e75c0a commit ac34f9c

File tree

14 files changed

+330
-128
lines changed

14 files changed

+330
-128
lines changed

spring-boot-cli/src/main/java/org/springframework/boot/cli/command/InitCommand.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,22 @@ else if (CommandFactory.class.isAssignableFrom(type)) {
117117
}
118118
}
119119
else if (Commands.class.isAssignableFrom(type)) {
120-
Map<String, Closure<?>> commands = ((Commands) type.newInstance())
121-
.getCommands();
120+
Commands instance = (Commands) type.newInstance();
121+
Map<String, Closure<?>> commands = instance.getCommands();
122+
Map<String, OptionHandler> handlers = instance.getOptions();
122123
for (String command : commands.keySet()) {
123-
this.cli.register(new ScriptCommand(command, commands
124-
.get(command)));
124+
if (handlers.containsKey(command)) {
125+
// An OptionHandler is available
126+
OptionHandler handler = handlers.get(command);
127+
handler.setClosure(commands.get(command));
128+
this.cli.register(new ScriptCommand(command, handler));
129+
}
130+
else {
131+
// Otherwise just a plain Closure
132+
this.cli.register(new ScriptCommand(command, commands
133+
.get(command)));
134+
135+
}
125136
}
126137
}
127138
else if (Script.class.isAssignableFrom(type)) {
@@ -161,6 +172,8 @@ public GroovyCompilerScope getScope() {
161172

162173
public static interface Commands {
163174
Map<String, Closure<?>> getCommands();
175+
176+
Map<String, OptionHandler> getOptions();
164177
}
165178

166179
}

spring-boot-cli/src/main/java/org/springframework/boot/cli/command/OptionHandler.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public class OptionHandler {
5151

5252
private OptionParser parser;
5353

54-
private Closure<Void> closure;
54+
private Closure<?> closure;
5555

5656
private String help;
5757

@@ -74,12 +74,9 @@ public OptionParser getParser() {
7474
}
7575

7676
protected void options() {
77-
if (this.closure != null) {
78-
this.closure.call();
79-
}
8077
}
8178

82-
public void setOptions(Closure<Void> closure) {
79+
public void setClosure(Closure<?> closure) {
8380
this.closure = closure;
8481
}
8582

@@ -100,6 +97,9 @@ public final void run(String... args) throws Exception {
10097
* @throws Exception
10198
*/
10299
protected void run(OptionSet options) throws Exception {
100+
if (this.closure != null) {
101+
this.closure.call(options);
102+
}
103103
}
104104

105105
public String getHelp() {

spring-boot-cli/src/main/java/org/springframework/boot/cli/command/ScriptCompilationCustomizer.java

Lines changed: 158 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -27,30 +27,30 @@
2727
import org.codehaus.groovy.ast.ClassCodeVisitorSupport;
2828
import org.codehaus.groovy.ast.ClassHelper;
2929
import org.codehaus.groovy.ast.ClassNode;
30-
import org.codehaus.groovy.ast.MethodNode;
30+
import org.codehaus.groovy.ast.InnerClassNode;
3131
import org.codehaus.groovy.ast.Parameter;
3232
import org.codehaus.groovy.ast.PropertyNode;
3333
import org.codehaus.groovy.ast.expr.ArgumentListExpression;
3434
import org.codehaus.groovy.ast.expr.ClosureExpression;
3535
import org.codehaus.groovy.ast.expr.ConstantExpression;
36+
import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
3637
import org.codehaus.groovy.ast.expr.Expression;
3738
import org.codehaus.groovy.ast.expr.MapExpression;
3839
import org.codehaus.groovy.ast.expr.MethodCallExpression;
3940
import org.codehaus.groovy.ast.stmt.BlockStatement;
41+
import org.codehaus.groovy.ast.stmt.EmptyStatement;
4042
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
41-
import org.codehaus.groovy.ast.stmt.Statement;
4243
import org.codehaus.groovy.classgen.GeneratorContext;
4344
import org.codehaus.groovy.control.CompilationFailedException;
4445
import org.codehaus.groovy.control.CompilePhase;
4546
import org.codehaus.groovy.control.SourceUnit;
4647
import org.codehaus.groovy.control.customizers.CompilationCustomizer;
4748
import org.codehaus.groovy.control.customizers.ImportCustomizer;
48-
import org.springframework.asm.Opcodes;
4949
import org.springframework.boot.cli.Command;
5050
import org.springframework.boot.cli.command.InitCommand.Commands;
5151

5252
/**
53-
* Customizer for the compilation of CLI commands.
53+
* Customizer for the compilation of CLI script commands.
5454
*
5555
* @author Dave Syer
5656
*/
@@ -64,29 +64,92 @@ public ScriptCompilationCustomizer() {
6464
public void call(SourceUnit source, GeneratorContext context, ClassNode classNode)
6565
throws CompilationFailedException {
6666
findCommands(source, classNode);
67-
overrideOptionsMethod(source, classNode);
6867
addImports(source, context, classNode);
6968
}
7069

70+
/**
71+
* If the script defines a block in this form:
72+
*
73+
* <pre>
74+
* command("foo") { args ->
75+
* println "Command foo called with args: ${args}"
76+
* }
77+
* </pre>
78+
*
79+
* Then the block is taken and used to create a Command named "foo" that runs the
80+
* closure when it is executed.
81+
*
82+
* If you want to declare options (and provide help text), use this form:
83+
*
84+
* <pre>
85+
* command("foo") {
86+
*
87+
* options {
88+
* option "foo", "My Foo option"
89+
* option "bar", "Bar has a value" withOptionalArg() ofType Integer
90+
* }
91+
*
92+
* run { options ->
93+
* println "Command foo called with bar=${options.valueOf('bar')}"
94+
* }
95+
*
96+
* }
97+
* </pre>
98+
*
99+
* In this case the "options" block is taken and used to override the
100+
* {@link OptionHandler#options()} method. Each "option" is a call to
101+
* {@link OptionHandler#option(String, String)}, and hence returns an
102+
* {@link OptionSpecBuilder}. Makes a nice readable DSL for adding options.
103+
*
104+
* @param source the source node
105+
* @param classNode the class node to manipulate
106+
*/
71107
private void findCommands(SourceUnit source, ClassNode classNode) {
72-
CommandVisitor visitor = new CommandVisitor(source);
108+
CommandVisitor visitor = new CommandVisitor(source, classNode);
73109
classNode.visitContents(visitor);
74110
visitor.addFactory(classNode);
75111
}
76112

113+
/**
114+
* Add imports to the class node to make writing simple commands easier. No need to
115+
* import {@link OptionParser}, {@link OptionSet}, {@link Command} or
116+
* {@link OptionHandler}.
117+
*
118+
* @param source the source node
119+
* @param context the current context
120+
* @param classNode the class node to manipulate
121+
*/
122+
private void addImports(SourceUnit source, GeneratorContext context,
123+
ClassNode classNode) {
124+
ImportCustomizer importCustomizer = new ImportCustomizer();
125+
importCustomizer.addImports("joptsimple.OptionParser", "joptsimple.OptionSet",
126+
OptionParsingCommand.class.getCanonicalName(),
127+
Command.class.getCanonicalName(), OptionHandler.class.getCanonicalName());
128+
importCustomizer.call(source, context, classNode);
129+
}
130+
131+
/**
132+
* Helper to extract a Commands instance (adding that interface to the current class
133+
* node) so individual commands can be registered with the CLI.
134+
*
135+
* @author Dave Syer
136+
*/
77137
private static class CommandVisitor extends ClassCodeVisitorSupport {
78138

79139
private SourceUnit source;
80-
private MapExpression map = new MapExpression();
140+
private MapExpression closures = new MapExpression();
141+
private MapExpression options = new MapExpression();
81142
private List<ExpressionStatement> statements = new ArrayList<ExpressionStatement>();
82143
private ExpressionStatement statement;
144+
private ClassNode classNode;
83145

84-
public CommandVisitor(SourceUnit source) {
146+
public CommandVisitor(SourceUnit source, ClassNode classNode) {
85147
this.source = source;
148+
this.classNode = classNode;
86149
}
87150

88151
private boolean hasCommands() {
89-
return !this.map.getMapEntryExpressions().isEmpty();
152+
return !this.closures.getMapEntryExpressions().isEmpty();
90153
}
91154

92155
private void addFactory(ClassNode classNode) {
@@ -96,7 +159,10 @@ private void addFactory(ClassNode classNode) {
96159
classNode.addInterface(ClassHelper.make(Commands.class));
97160
classNode.addProperty(new PropertyNode("commands", Modifier.PUBLIC
98161
| Modifier.FINAL, ClassHelper.MAP_TYPE.getPlainNodeReference(),
99-
classNode, this.map, null, null));
162+
classNode, this.closures, null, null));
163+
classNode.addProperty(new PropertyNode("options", Modifier.PUBLIC
164+
| Modifier.FINAL, ClassHelper.MAP_TYPE.getPlainNodeReference(),
165+
classNode, this.options, null, null));
100166
}
101167

102168
@Override
@@ -128,91 +194,105 @@ public void visitMethodCallExpression(MethodCallExpression call) {
128194
this.statements.add(this.statement);
129195
ConstantExpression name = (ConstantExpression) arguments
130196
.getExpression(0);
131-
ClosureExpression closure = (ClosureExpression) arguments
132-
.getExpression(1);
133-
this.map.addMapEntryExpression(name, closure);
197+
Expression expression = arguments.getExpression(1);
198+
if (expression instanceof ClosureExpression) {
199+
ClosureExpression closure = (ClosureExpression) expression;
200+
ActionExtractorVisitor action = new ActionExtractorVisitor(
201+
this.source, this.classNode, name.getText());
202+
closure.getCode().visit(action);
203+
if (action.hasOptions()) {
204+
this.options.addMapEntryExpression(name, action.getOptions());
205+
expression = action.getAction();
206+
}
207+
else {
208+
expression = new ClosureExpression(
209+
new Parameter[] { new Parameter(
210+
ClassHelper.make(String[].class), "args") },
211+
closure.getCode());
212+
}
213+
this.closures.addMapEntryExpression(name, expression);
214+
}
134215
}
135216
}
136217
}
137218

138219
}
139220

140221
/**
141-
* Add imports to the class node to make writing simple commands easier. No need to
142-
* import {@link OptionParser}, {@link OptionSet}, {@link Command} or
143-
* {@link OptionHandler}.
222+
* Helper to pull out options and action closures from a command declaration (if they
223+
* are there).
144224
*
145-
* @param source the source node
146-
* @param context the current context
147-
* @param classNode the class node to manipulate
225+
* @author Dave Syer
148226
*/
149-
private void addImports(SourceUnit source, GeneratorContext context,
150-
ClassNode classNode) {
151-
ImportCustomizer importCustomizer = new ImportCustomizer();
152-
importCustomizer.addImports("joptsimple.OptionParser", "joptsimple.OptionSet",
153-
OptionParsingCommand.class.getCanonicalName(),
154-
Command.class.getCanonicalName(), OptionHandler.class.getCanonicalName());
155-
importCustomizer.call(source, context, classNode);
156-
}
227+
private static class ActionExtractorVisitor extends ClassCodeVisitorSupport {
157228

158-
/**
159-
* If the script defines a block in this form:
160-
*
161-
* <pre>
162-
* options {
163-
* option "foo", "My Foo option"
164-
* option "bar", "Bar has a value" withOptionalArg() ofType Integer
165-
* }
166-
* </pre>
167-
*
168-
* Then the block is taken and used to override the {@link OptionHandler#options()}
169-
* method. In the example "option" is a call to
170-
* {@link OptionHandler#option(String, String)}, and hence returns an
171-
* {@link OptionSpecBuilder}. Makes a nice readable DSL for adding options.
172-
*
173-
* @param source the source node
174-
* @param classNode the class node to manipulate
175-
*/
176-
private void overrideOptionsMethod(SourceUnit source, ClassNode classNode) {
177-
178-
ClosureExpression closure = options(source, classNode);
179-
if (closure != null) {
180-
classNode.addMethod(new MethodNode("options", Opcodes.ACC_PROTECTED,
181-
ClassHelper.VOID_TYPE, new Parameter[0], new ClassNode[0], closure
182-
.getCode()));
183-
classNode.setSuperClass(ClassHelper.make(OptionHandler.class));
229+
private static final Parameter[] OPTIONS_PARAMETERS = new Parameter[] { new Parameter(
230+
ClassHelper.make(OptionSet.class), "options") };
231+
private SourceUnit source;
232+
private ClassNode classNode;
233+
private Expression options;
234+
private ClosureExpression action;
235+
private String name;
236+
237+
public ActionExtractorVisitor(SourceUnit source, ClassNode classNode, String name) {
238+
this.source = source;
239+
this.classNode = classNode;
240+
this.name = name;
184241
}
185242

186-
}
243+
@Override
244+
protected SourceUnit getSourceUnit() {
245+
return this.source;
246+
}
187247

188-
private ClosureExpression options(SourceUnit source, ClassNode classNode) {
189-
190-
BlockStatement block = source.getAST().getStatementBlock();
191-
List<Statement> statements = block.getStatements();
192-
193-
for (Statement statement : new ArrayList<Statement>(statements)) {
194-
if (statement instanceof ExpressionStatement) {
195-
ExpressionStatement expr = (ExpressionStatement) statement;
196-
Expression expression = expr.getExpression();
197-
if (expression instanceof MethodCallExpression) {
198-
MethodCallExpression method = (MethodCallExpression) expression;
199-
if (method.getMethod().getText().equals("options")) {
200-
statements.remove(statement);
201-
expression = method.getArguments();
202-
if (expression instanceof ArgumentListExpression) {
203-
ArgumentListExpression arguments = (ArgumentListExpression) expression;
204-
expression = arguments.getExpression(0);
205-
if (expression instanceof ClosureExpression) {
206-
return (ClosureExpression) expression;
207-
}
208-
}
248+
public boolean hasOptions() {
249+
return this.options != null;
250+
}
251+
252+
public Expression getOptions() {
253+
return this.options;
254+
}
255+
256+
public ClosureExpression getAction() {
257+
return this.action != null ? this.action : new ClosureExpression(
258+
OPTIONS_PARAMETERS, new EmptyStatement());
259+
}
260+
261+
@Override
262+
public void visitMethodCallExpression(MethodCallExpression call) {
263+
Expression methodCall = call.getMethod();
264+
if (methodCall instanceof ConstantExpression) {
265+
ConstantExpression method = (ConstantExpression) methodCall;
266+
if ("options".equals(method.getValue())) {
267+
ArgumentListExpression arguments = (ArgumentListExpression) call
268+
.getArguments();
269+
Expression expression = arguments.getExpression(0);
270+
if (expression instanceof ClosureExpression) {
271+
ClosureExpression closure = (ClosureExpression) expression;
272+
InnerClassNode type = new InnerClassNode(this.classNode,
273+
this.classNode.getName() + "$" + this.name
274+
+ "OptionHandler", Modifier.PUBLIC,
275+
ClassHelper.make(OptionHandler.class));
276+
type.addMethod("options", Modifier.PROTECTED,
277+
ClassHelper.VOID_TYPE, Parameter.EMPTY_ARRAY,
278+
ClassNode.EMPTY_ARRAY, closure.getCode());
279+
this.classNode.getModule().addClass(type);
280+
this.options = new ConstructorCallExpression(type,
281+
ArgumentListExpression.EMPTY_ARGUMENTS);
282+
}
283+
}
284+
else if ("run".equals(method.getValue())) {
285+
ArgumentListExpression arguments = (ArgumentListExpression) call
286+
.getArguments();
287+
Expression expression = arguments.getExpression(0);
288+
if (expression instanceof ClosureExpression) {
289+
ClosureExpression closure = (ClosureExpression) expression;
290+
this.action = new ClosureExpression(OPTIONS_PARAMETERS,
291+
closure.getCode());
209292
}
210293
}
211294
}
212295
}
213-
214-
return null;
215-
216296
}
217297

218298
}

0 commit comments

Comments
 (0)