I've been working on a system that will be using remote calls to communicate between a client (browser, mobile phone, possitbly a GWT client) and the server. The client sends a request, and a grails controller returns a grails domain object encoded using JSON. Relatively straight-forward stuff, but I hit a few snags. I was thankful when I discovered http://blog.lourish.com/ which goes into some details into how to make it happen. Detailed post are here, here, and here.
I debated using the ObjectMarshaller to restrict the data sent (afterall, the client doesn't need to know the class name of my objects), but in the end, I decided to use Data Transfer Objects. I can see a future development where these objects will be used as commands, for example.
The problem that's been keeping me awake tonight, tho, is in the translation from domain object to DTO. Based on my reading, it looked like I could transform any kind of object into any other kind of object, as long as the initial object knew what to do.
class User { // grails will contribute fields for id and version String lastName String firstName Address workAddress // Address homeAddress // The client does not need that info and SHOULD NOT ever see it static hasMany [roles: Role, groups: Groups] // etc doThis() { //.. } doThat() { //... } } class UserDTO { String lastName String firstName }
How do you take a User object and make a UserDTO out of it? Well, you should certainly have a look at Peter Ledbrook's DTO plugin. But for my needs, I thought I'd stick with something simpler. Just use the groovy "as" operator.
All you need to do something like
def dto = User as DTO
is to have User implement asType(Class clazz) and to handle (by hand) the case where clazz is DTO:
class User { // same fields as before, etc Object asType(Class clazz) { if (clazz.isAssignableFrom(UserDTO)) { return new UserDTO(lastName: lastName, firstName:firstName) } else { return super.asType(clazz) } } }
All works well. Unit tests confirm, there's nothing to it.
void testUserAsUserDTO() { String lastName = 'Lovelace' String firstName = 'Ada' User u = new User(lastName: lastName, firstName: firstName); UserDTO dto = u as UserDTO; assertEquals(UserDTO.class, dto.class) assertEquals(lastName, dto.lastName); assertEquals(firstName, dto.firstName); }
Integration test. I want to make sure my controller sends the right data
The controller:
def whoAmI = { def me = authenticateService.userDomain() // acegi plugin; this returns a User if (me) { def dto = me as UserDTO render dto as JSON } else { render [error: "You are not logged in"] as JSON } }
The test:
class RpcWhoAmITest extends ControllerUnitTestCase { void testWhoAmI() { String lastName = 'Lovelace'; String firstName = 'Ada'; User u = new User(lastName: lastName, firstName: firstName) mockDomain(User.class,[u]) mockLoginAs(u) controller.whoAmI() def returnedUser = JSON.parse(controller.response.contentAsString) assertNotNull(returnedUser) assertEquals(lastName, returnedUser.lastName) assertEquals(firstName, returnedUser.firstName) } }
And that... fails! The message is
org.codehaus.groovy.runtime.typehandling.GroovyCastException: Cannot cast object 'my.package.User : 1' with class 'my.package.User' to class 'my.package.rpc.UserDTO' at org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation.castToType(DefaultTypeTransformation.java:348) at ...
What went wrong? The call to mock my domain object is what went wrong. It replaces my asType(Class clazz) with its own. Fortunately, that's relatively easy to fix. I needed to override the method addConverters in grails.test.GrailsUnitTestCase to replace asType(Class) only if it didn't already exist (in my test class):
@Override protected void addConverters(Class clazz) { registerMetaClass(clazz) if (!clazz.metaClass.asType) { clazz.metaClass.asType = {Class asClass -> if (ConverterUtil.isConverterClass(asClass)) { return ConverterUtil.createConverter(asClass, delegate, applicationContext) } else { return ConverterUtil.invokeOriginalAsTypeMethod(delegate, asClass) } } } }
Sadly, after all this work, I deploy, launch, and still get GroovyCastExceptions. It turns out that the instrumentation of domain class objects essentially throws out my "asType()" method. In the end, I switched to the DTO plugin (which post-instruments the domain object to do it's own stuff, something I considered doing, but at some point, the "quick, home-made solution" just isn't.
No comments:
Post a Comment