27
27
import org .codehaus .groovy .ast .ClassCodeVisitorSupport ;
28
28
import org .codehaus .groovy .ast .ClassHelper ;
29
29
import org .codehaus .groovy .ast .ClassNode ;
30
- import org .codehaus .groovy .ast .MethodNode ;
30
+ import org .codehaus .groovy .ast .InnerClassNode ;
31
31
import org .codehaus .groovy .ast .Parameter ;
32
32
import org .codehaus .groovy .ast .PropertyNode ;
33
33
import org .codehaus .groovy .ast .expr .ArgumentListExpression ;
34
34
import org .codehaus .groovy .ast .expr .ClosureExpression ;
35
35
import org .codehaus .groovy .ast .expr .ConstantExpression ;
36
+ import org .codehaus .groovy .ast .expr .ConstructorCallExpression ;
36
37
import org .codehaus .groovy .ast .expr .Expression ;
37
38
import org .codehaus .groovy .ast .expr .MapExpression ;
38
39
import org .codehaus .groovy .ast .expr .MethodCallExpression ;
39
40
import org .codehaus .groovy .ast .stmt .BlockStatement ;
41
+ import org .codehaus .groovy .ast .stmt .EmptyStatement ;
40
42
import org .codehaus .groovy .ast .stmt .ExpressionStatement ;
41
- import org .codehaus .groovy .ast .stmt .Statement ;
42
43
import org .codehaus .groovy .classgen .GeneratorContext ;
43
44
import org .codehaus .groovy .control .CompilationFailedException ;
44
45
import org .codehaus .groovy .control .CompilePhase ;
45
46
import org .codehaus .groovy .control .SourceUnit ;
46
47
import org .codehaus .groovy .control .customizers .CompilationCustomizer ;
47
48
import org .codehaus .groovy .control .customizers .ImportCustomizer ;
48
- import org .springframework .asm .Opcodes ;
49
49
import org .springframework .boot .cli .Command ;
50
50
import org .springframework .boot .cli .command .InitCommand .Commands ;
51
51
52
52
/**
53
- * Customizer for the compilation of CLI commands.
53
+ * Customizer for the compilation of CLI script commands.
54
54
*
55
55
* @author Dave Syer
56
56
*/
@@ -64,29 +64,92 @@ public ScriptCompilationCustomizer() {
64
64
public void call (SourceUnit source , GeneratorContext context , ClassNode classNode )
65
65
throws CompilationFailedException {
66
66
findCommands (source , classNode );
67
- overrideOptionsMethod (source , classNode );
68
67
addImports (source , context , classNode );
69
68
}
70
69
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
+ */
71
107
private void findCommands (SourceUnit source , ClassNode classNode ) {
72
- CommandVisitor visitor = new CommandVisitor (source );
108
+ CommandVisitor visitor = new CommandVisitor (source , classNode );
73
109
classNode .visitContents (visitor );
74
110
visitor .addFactory (classNode );
75
111
}
76
112
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
+ */
77
137
private static class CommandVisitor extends ClassCodeVisitorSupport {
78
138
79
139
private SourceUnit source ;
80
- private MapExpression map = new MapExpression ();
140
+ private MapExpression closures = new MapExpression ();
141
+ private MapExpression options = new MapExpression ();
81
142
private List <ExpressionStatement > statements = new ArrayList <ExpressionStatement >();
82
143
private ExpressionStatement statement ;
144
+ private ClassNode classNode ;
83
145
84
- public CommandVisitor (SourceUnit source ) {
146
+ public CommandVisitor (SourceUnit source , ClassNode classNode ) {
85
147
this .source = source ;
148
+ this .classNode = classNode ;
86
149
}
87
150
88
151
private boolean hasCommands () {
89
- return !this .map .getMapEntryExpressions ().isEmpty ();
152
+ return !this .closures .getMapEntryExpressions ().isEmpty ();
90
153
}
91
154
92
155
private void addFactory (ClassNode classNode ) {
@@ -96,7 +159,10 @@ private void addFactory(ClassNode classNode) {
96
159
classNode .addInterface (ClassHelper .make (Commands .class ));
97
160
classNode .addProperty (new PropertyNode ("commands" , Modifier .PUBLIC
98
161
| 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 ));
100
166
}
101
167
102
168
@ Override
@@ -128,91 +194,105 @@ public void visitMethodCallExpression(MethodCallExpression call) {
128
194
this .statements .add (this .statement );
129
195
ConstantExpression name = (ConstantExpression ) arguments
130
196
.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
+ }
134
215
}
135
216
}
136
217
}
137
218
138
219
}
139
220
140
221
/**
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).
144
224
*
145
- * @param source the source node
146
- * @param context the current context
147
- * @param classNode the class node to manipulate
225
+ * @author Dave Syer
148
226
*/
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 {
157
228
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 ;
184
241
}
185
242
186
- }
243
+ @ Override
244
+ protected SourceUnit getSourceUnit () {
245
+ return this .source ;
246
+ }
187
247
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 ());
209
292
}
210
293
}
211
294
}
212
295
}
213
-
214
- return null ;
215
-
216
296
}
217
297
218
298
}
0 commit comments