/
RestGriffonPlugin.groovy
429 lines (342 loc) · 15.8 KB
/
RestGriffonPlugin.groovy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
/*
* Copyright 2009-2012 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.
*/
/**
* @author Andres Almiray
*/
class RestGriffonPlugin {
// the plugin version
String version = '1.0.0'
// the version or versions of Griffon the plugin is designed for
String griffonVersion = '1.2.0 > *'
// the other plugins this plugin depends on
Map dependsOn = [lombok: '0.4']
// resources that are included in plugin packaging
List pluginIncludes = []
// the plugin license
String license = 'Apache Software License 2.0'
// Toolkit compatibility. No value means compatible with all
// Valid values are: swing, javafx, swt, pivot, gtk
List toolkits = []
// Platform compatibility. No value means compatible with all
// Valid values are:
// linux, linux64, windows, windows64, macosx, macosx64, solaris
List platforms = []
// URL where documentation can be found
String documentation = ''
// URL where source can be found
String source = 'https://github.com/griffon/griffon-rest-plugin'
List authors = [
[
name: 'Andres Almiray',
email: 'aalmiray@yahoo.com'
]
]
String title = 'Lightweight HTTP/REST/SOAP client support'
String description = '''
The Rest plugin enables the usage of [groovy-rest][1] on a Griffon application.
Usage
-----
The plugin will inject the following dynamic methods:
* `<R> R withRest(Map<String, Object> params, Closure<R> stmts)` - executes stmts using a RESTClient
* `<R> R withHttp(Map<String, Object> params, Closure<R> stmts)` - executes stmts using an HTTPBuilder
* `<R> R withAsyncHttp(Map<String, Object> params, Closure<R> stmts)` - executes stmts using a AsyncHTTPBuilder
* `<R> R withRest(Map<String, Object> params, CallableWithArgs<R> stmts)` - executes stmts using a RESTClient
* `<R> R withHttp(Map<String, Object> params, CallableWithArgs<R> stmts)` - executes stmts using an HTTPBuilder
* `<R> R withAsyncHttp(Map<String, Object> params, CallableWithArgs<R> stmts)` - executes stmts using a AsyncHTTPBuilder
Where params may contain
| Property | Type | Required |
| ------------ | --------------- | -------- |
| uri | String | yes |
| contentType | String | |
| id | String | |
All dynamic methods will create a new client when invoked unless you define an
`id:` attribute. When this attribute is supplied the client will be stored in a
cache managed by the `RestProvider` that handled the call. You may specify
parameters for configuring an HTTP proxy, for example
| Property | Type | Required | Default |
| ------------ | --------------- | -------- | ------- |
| scheme | String | | http |
| port | int | | 80 |
| host | String | yes | |
| username | String | | |
| password | String | | |
Configuring a proxy host for `http://acme.com:81` can be done in this way
withRest(uri: 'http://foo.com/', proxy: [host: 'acme.com', port: 81]) {
...
}
The method `withAsyncHttp` accepts the following additional properties
| Property | Type |
| ---------- | --------------- |
| threadPool | ExecutorService |
| poolSize | int |
| timeout | int |
These methods are also accessible to any component through the singleton
`griffon.plugins.rest.RestEnhancer`. You can inject these methods to
non-artifacts via metaclasses. Simply grab hold of a particular metaclass and
call `RestEnhancer.enhance(metaClassInstance)`.
Configuration
-------------
### RestAware AST Transformation
The preferred way to mark a class for method injection is by annotating it with
`@griffon.plugins.rest.RestAware`. This transformation injects the
`griffon.plugins.rest.RestContributionHandler` interface and default behavior
that fulfills the contract.
### Dynamic Method Injection
Dynamic methods will be added to controllers by default. You can
change this setting by adding a configuration flag in `griffon-app/conf/Config.groovy`
griffon.rest.injectInto = ['controller', 'service']
Dynamic method injection wil skipped for classes implementing
`griffon.plugins.rest.RestContributionHandler`.
### Example
This example relies on [Grails][2] as the service provider. Follow these steps
to configure the service on the Grails side:
1. Download a copy of [Grails][3] and install it.
2. Create a new Grails application. We'll pick 'exporter' as the application name.
grails create-app exporter
3. Create a controller named `Calculator`
grails create-controller calculator
4. Paste the following code in `grails-app/controllers/exporter/CalculatorController.groovy`
package exporter
import grails.converters.JSON
class CalculatorController {
def add() {
double a = params.a.toDouble()
double b = params.b.toDouble()
render([result: (a + b)] as JSON)
}
}
5. Run the application
grails run-app
Now we're ready to build the Griffon application
1. Create a new Griffon application. We'll pick `calculator` as the application
name
griffon create-app calculator
2. Install the rest plugin
griffon install-plugin rest
3. Fix the view script to look like this
package calculator
application(title: 'Rest Plugin Example',
pack: true,
locationByPlatform: true,
iconImage: imageIcon('/griffon-icon-48x48.png').image,
iconImages: [imageIcon('/griffon-icon-48x48.png').image,
imageIcon('/griffon-icon-32x32.png').image,
imageIcon('/griffon-icon-16x16.png').image]) {
gridLayout(cols: 2, rows: 4)
label('Num1:')
textField(columns: 20, text: bind(target: model, targetProperty: 'num1'))
label('Num2:')
textField(columns: 20, text: bind(target: model, targetProperty: 'num2'))
label('Result:')
label(text: bind{model.result})
button(calculateAction, enabled: bind{model.enabled})
}
4. Let's add required properties to the model
package calculator
@Bindable
class CalculatorModel {
String num1
String num2
String result
boolean enabled = true
}
5. Now for the controller code. Notice that there is minimal error handling in
place. If the user types something that is not a number the client will
surely break, but the code is sufficient for now.
package calculator
import rest.rest.ContentType
@griffon.plugins.rest.RestAware
class CalculatorController {
def model
def calculate = { evt = null ->
String a = model.num1
String b = model.num2
execInsideUIAsync { model.enabled = false }
try {
def result = withRest(url: 'http://localhost:8080/exporter/calculator/', id: 'client') {
def response = get(path: 'add', query: [a: a, b: b], accept: ContentType.JSON)
response.json.result
}
execInsideUIAsync { model.result = result }
} finally {
execInsideUIAsync { model.enabled = true }
}
}
}
6. Run the application
griffon run-app
The plugin exposes a Java friendly API to make the exact same calls from Java,
or any other JVM language for that matter. Here's for example the previous code
rewritten in Java. Note the usage of @RestWare on a Java class
package calculator;
import static griffon.util.CollectionUtils.newMap;
import griffon.util.CallableWithArgs;
import griffon.util.CollectionUtils;
import groovyx.net.http.HttpResponseDecorator;
import groovyx.net.http.RESTClient;
import java.awt.event.ActionEvent;
import java.util.Map;
import net.sf.json.JSONObject;
import org.codehaus.griffon.runtime.core.AbstractGriffonController;
@griffon.plugins.rest.RestAware
public class CalculatorController extends AbstractGriffonController {
private CalculatorModel model;
public void setModel(CalculatorModel model) {
this.model = model;
}
public void calculate(ActionEvent event) {
final String a = model.getNum1();
final String b = model.getNum2();
enableModel(false);
try {
Map<String, Object> params = CollectionUtils.<String, Object> map()
.e("uri", "http://localhost:8080/exporter/calculator/")
.e("id", "client");
final String result = withRest(params,
new CallableWithArgs<String>() {
public String call(Object[] args) {
RESTClient client = (RESTClient) args[0];
try {
HttpResponseDecorator response = (HttpResponseDecorator) client.get(
newMap(
"path", "add",
"query", newMap("a", a, "b", b)));
JSONObject json = (JSONObject) response.getData();
return json.getString("result");
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
});
execInsideUIAsync(new Runnable() {
public void run() {
model.setResult(result);
}
});
} finally {
enableModel(true);
}
}
private void enableModel(final boolean enabled) {
execInsideUIAsync(new Runnable() {
public void run() {
model.setEnabled(enabled);
}
});
}
}
Testing
-------
Dynamic methods will not be automatically injected during unit testing, because
addons are simply not initialized for this kind of tests. However you can use
`RestEnhancer.enhance(metaClassInstance, restProviderInstance)` where
`restProviderInstance` is of type `griffon.plugins.rest.RestProvider`.
The contract for this interface looks like this
public interface RestProvider {
<R> R withAsyncHttp(Map<String, Object> params, Closure<R> closure);
<R> R withHttp(Map<String, Object> params, Closure<R> closure);
<R> R withRest(Map<String, Object> params, Closure<R> closure);
<R> R withAsyncHttp(Map<String, Object> params, CallableWithArgs<R> callable);
<R> R withHttp(Map<String, Object> params, CallableWithArgs<R> callable);
<R> R withRest(Map<String, Object> params, CallableWithArgs<R> callable);
}
It's up to you define how these methods need to be implemented for your tests.
For example, here's an implementation that never fails regardless of the
arguments it receives
class MyRestProvider implements RestProvider {
public <R> R withAsyncHttp(Map<String, Object> params, Closure<R> closure) { null }
public <R> R withHttp(Map<String, Object> params, Closure<R> closure) { null }
public <R> R withRest(Map<String, Object> params, Closure<R> closure) { null }
public <R> R withAsyncHttp(Map<String, Object> params, CallableWithArgs<R> callable) { null }
public <R> R withHttp(Map<String, Object> params, CallableWithArgs<R> callable) { null }
public <R> R withRest(Map<String, Object> params, CallableWithArgs<R> callable) { null }
}
This implementation may be used in the following way
class MyServiceTests extends GriffonUnitTestCase {
void testSmokeAndMirrors() {
MyService service = new MyService()
RestEnhancer.enhance(service.metaClass, new MyRestProvider())
// exercise service methods
}
}
On the other hand, if the service is annotated with `@RestAware` then usage
of `RestEnhancer` should be avoided at all costs. Simply set
`restProviderInstance` on the service instance directly, like so, first the
service definition
@griffon.plugins.rest.RestAware
class MyService {
def serviceMethod() { ... }
}
Next is the test
class MyServiceTests extends GriffonUnitTestCase {
void testSmokeAndMirrors() {
MyService service = new MyService()
service.restProvider = new MyRestProvider()
// exercise service methods
}
}
Tool Support
------------
### DSL Descriptors
This plugin provides DSL descriptors for Intellij IDEA and Eclipse (provided
you have the Groovy Eclipse plugin installed). These descriptors are found
inside the `griffon-rest-compile-x.y.z.jar`, with locations
* dsdl/rest.dsld
* gdsl/rest.gdsl
### Lombok Support
Rewriting Java AST in a similar fashion to Groovy AST transformations is
posisble thanks to the [lombok][4] plugin.
#### JavaC
Support for this compiler is provided out-of-the-box by the command line tools.
There's no additional configuration required.
#### Eclipse
Follow the steps found in the [Lombok][4] plugin for setting up Eclipse up to
number 5.
6. Go to the path where the `lombok.jar` was copied. This path is either found
inside the Eclipse insatllation directory or in your local settings. Copy
the following file from the project's working directory
$ cp $USER_HOME/.griffon/<version>/projects/<project>/plugins/rest-<version>/dist/griffon-rest-compile-<version>.jar .
6. Edit the launch script for Eclipse and tweak the boothclasspath entry so
that includes the file you just copied
-Xbootclasspath/a:lombok.jar:lombok-pg-<version>.jar:\
griffon-lombok-compile-<version>.jargriffon-rest-compile-<version>.jar
7. Launch Eclipse once more. Eclipse should be able to provide content assist
for Java classes annotated with `@RestAware`.
#### NetBeans
Follow the instructions found in [Annotation Processors Support in the NetBeans
IDE, Part I: Using Project Lombok][6]. You may need to specify
`lombok.core.AnnotationProcessor` in the list of Annotation Processors.
NetBeans should be able to provide code suggestions on Java classes annotated
with `@RestAware`.
#### Intellij IDEA
Follow the steps found in the [Lombok][4] plugin for setting up Intellij IDEA
up to number 5.
6. Copy `griffon-rest-compile-<version>.jar` to the `lib` directory
$ pwd
$USER_HOME/Library/Application Support/IntelliJIdea11/lombok-plugin
$ cp $USER_HOME/.griffon/<version>/projects/<project>/plugins/rest-<version>/dist/griffon-rest-compile-<version>.jar lib
7. Launch IntelliJ IDEA once more. Code completion should work now for Java
classes annotated with `@RestAware`.
[1]: http://groovy.codehaus.org/modules/http-builder
[2]: http://grails.org
[3]: http://grails.org/Download
[4]: /plugin/lombok
[5]: /plugin/eclipse-support
[6]: http://netbeans.org/kb/docs/java/annotations-lombok.html
[7]: http://code.google.com/p/lombok-intellij-plugin
'''
}