Tuesday, January 29, 2008

BlazeDS Test Drive sample in Grails



In my previous blog entry I introduced the Grails Flex Plugin. I got some reactions on this post and one of them was from Alexander Negoda (aka greendog), who had some problems getting BlazeDS Test Drive Sample 5: Updating data to work on Grails.

A good reason for me to implement the sample myself, and write a small tutorial!


  1. I assume you have Grails already installed and know the basics. This plugin requires requires the latest and greatest Grails 1.0-final development build. It is not compatible with Grails 1.0-RC4.

  2. Create a new Grails application by executing grails create-app product from the Grails command line.

  3. From within the product application folder, install the Flex plugin by executing: grails install-plugin flex
    All Flex libraries and configuration files will be copied into your application's web-app folder.

  4. Create the Product domain class by executing: grails create-domain-class product

  5. Open Product.groovy and add properties so your domain class looks like:

    class Product {
    String name
    String description
    String image
    String category
    Double price
    Integer qtyInStock
    }

  6. Create the Product service by executing: grails create-service product

  7. Open ProductService.groovy and change the class so it looks like:

    class ProductService {

    static expose = ['flex-remoting']

    def getProducts() {
    return Product.list();
    }

    def update(Product product) {
    def p = Product.get(product.id)
    if (p) {
    p.properties = product.properties
    p.save()
    }
    }

    }

    Notice that we expose the service as a flex-remoting service.

  8. Now we are finished with the domain and service layer and we can focus on the Flex front-end. Within the application's web-app folder create a file called main.mxml and add the following content:

    <?xml version="1.0" encoding="utf-8"?>
    <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" xmlns="*" layout="horizontal"
    creationComplete="srv.getProducts()" viewSourceURL="srcview/index.html">

    <mx:RemoteObject id="srv" destination="productService"/>

    <mx:Panel title="Catalog" width="100%" height="100%">
    <mx:DataGrid id="list" dataProvider="{srv.getProducts.lastResult}" width="100%" height="100%"/>
    </mx:Panel>

    <ProductForm product="{Product(list.selectedItem)}"/>

    </mx:Application>

  9. Within the application's web-app folder create a file called ProductForm.mxml and add the following content:

    <?xml version="1.0" encoding="utf-8"?>
    <mx:Panel xmlns:mx="http://www.adobe.com/2006/mxml" xmlns="*"
    title="Details" width="100%" height="100%">

    <Product id="product"
    name="{productName.text}"
    category="{category.text}"
    price="{Number(price.text)}"
    image="{image.text}"
    description="{description.text}"/>

    <mx:RemoteObject id="srv" destination="productService"/>

    <mx:Form width="100%">

    <mx:FormItem label="Name">
    <mx:TextInput id="productName" text="{product.name}"/>
    </mx:FormItem>

    <mx:FormItem label="Category">
    <mx:TextInput id="category" text="{product.category}"/>
    </mx:FormItem>

    <mx:FormItem label="Image">
    <mx:TextInput id="image" text="{product.image}"/>
    </mx:FormItem>

    <mx:FormItem label="Price">
    <mx:TextInput id="price" text="{product.price}"/>
    </mx:FormItem>

    <mx:FormItem label="Description" width="100%">
    <mx:TextArea id="description" text="{product.description}" width="100%" height="100"/>
    </mx:FormItem>

    </mx:Form>

    <mx:ControlBar>
    <mx:Button label="Update" click="srv.update(product);"/>
    </mx:ControlBar>

    </mx:Panel>

  10. Within the application's web-app folder create a file called Product.as and add the following content:

    package
    {
    [Bindable]
    [RemoteClass(alias="Product")]
    public class Product
    {
    public function Product()
    {
    }

    public var id:int;

    public var name:String;

    public var description:String;

    public var image:String;

    public var category:String;

    public var price:Number;

    public var qtyInStock:int;

    }
    }

    Note that the main.mxml, ProductForm.mxml and Product.as files are for 99% a copy from the BlazeDS Test Drive sample. In main.mxml and ProductForm.mxml only the destination id of the RemoteObject was changed. And in Product.as the alias of the RemoteClass and the name of the productId column was changed.

  11. Before we run the example, let's add some data to the embedded HSQLDB database. In your application's BootStrap.groovy class create some records as shown below:

    class BootStrap {

    def init = { servletContext ->
    new Product(name: 'Nokia 6010', category: '6000', image: 'Nokia_6010.gif', price: 99.00, description: 'Easy to use without sacrificing style, the Nokia 6010 phone offers functional voice communication supported by text messaging, multimedia messaging, mobile internet, games and more.', qtyInStock: 21).save()
    new Product(name: 'Nokia 3100 Blue', category: '9000', image: 'Nokia_3100_blue.gif', price: 109.00, description: 'Light up the night with a glow-in-the-dark cover - when it is charged with light you can easily find your phone in the dark. When you get a call, the Nokia 3100 phone flashes in tune with your ringing tone. And when you snap on a Nokia Xpress-on gaming cover, you will get luminescent light effects in time to the gaming action.', qtyInStock: 99).save()
    new Product(name: 'Nokia 3100 Pink', category: '3000', image: 'Nokia_3100_pink.gif', price: 139.00, description: 'Light up the night with a glow-in-the-dark cover - when it is charged with light you can easily find your phone in the dark. When you get a call, the Nokia 3100 phone flashes in tune with your ringing tone. And when you snap on a Nokia Xpress-on gaming cover, you will get luminescent light effects in time to the gaming action.', qtyInStock: 30).save()
    new Product(name: 'Nokia 3120', category: '3000', image: 'Nokia_3120.gif', price: 159.99, description: 'Designed for both business and pleasure, the elegant Nokia 3120 phone offers a pleasing mix of features. Enclosed within its chic, compact body, you will discover the benefits of tri-band compatibility, a color screen, MMS, XHTML browsing, cheerful screensavers, and much more.', qtyInStock: 10).save()
    new Product(name: 'Nokia 3220', category: '3000', image: 'Nokia_3220.gif', price: 199.00, description: 'The Nokia 3220 phone is a fresh new cut on some familiar ideas - animate your MMS messages with cute characters, see the music with lights that flash in time with your ringing tone, download wallpapers and screensavers with matching color schemes for the interface.', qtyInStock: 20).save()
    new Product(name: 'Nokia 3650', category: '3000', image: 'Nokia_3650.gif', price: 200.00, description: 'Messaging is more personal, versatile and fun with the Nokia 3650 camera phone. Capture experiences as soon as you see them and send the photos you take to you friends and family.', qtyInStock: 11).save()
    new Product(name: 'Nokia 6820', category: '6000', image: 'Nokia_6820.gif', price: 299.99, description: 'Messaging just got a whole lot smarter. The Nokia 6820 messaging device puts the tools you need for rich communication - full messaging keyboard, digital camera, mobile email, MMS, SMS, and Instant Messaging - right at your fingertips, in a small, sleek device.', qtyInStock: 8).save()
    new Product(name: 'Nokia 6670', category: '6000', image: 'Nokia_6670.gif', price: 319.99, description: 'Classic business tools meet your creative streak in the Nokia 6670 imaging smartphone. It has a Netfront Web browser with PDF support, document viewer applications for email attachments, a direct printing application, and a megapixel still camera that also shoots up to 10 minutes of video.', qtyInStock: 2).save()
    new Product(name: 'Nokia 6620', category: '6000', image: 'Nokia_6620.gif', price: 329.99, description: 'Shoot a basket. Shoot a movie. Video phones from Nokia... the perfect way to save and share life\u2019s playful moments. Feel connected.', qtyInStock: 10).save()
    new Product(name: 'Nokia 3230 Silver', category: '3000', image: 'Nokia_3230_black.gif', price: 500.00, description: 'Get creative with the Nokia 3230 smartphone. Create your own ringing tones, print your mobile images, play multiplayer games over a wireless Bluetooth connection, and browse HTML and xHTML Web pages. ', qtyInStock: 10).save()
    new Product(name: 'Nokia 6680', category: '6000', image: 'Nokia_6680.gif', price: 222.00, description: 'The Nokia 6680 is an imaging smartphone that', qtyInStock: 36).save()
    new Product(name: 'Nokia 6630', category: '6000', image: 'Nokia_6630.gif', price: 379.00, description: 'The Nokia 6630 imaging smartphone is a 1.3 megapixel digital imaging device (1.3 megapixel camera sensor, effective resolution 1.23 megapixels for image capture, image size 1280 x 960 pixels).', qtyInStock: 8).save()
    new Product(name: 'Nokia 7610 Black', category: '7000', image: 'Nokia_7610_black.gif', price: 450.00, description: 'The Nokia 7610 imaging phone with its sleek, compact design stands out in any crowd. Cut a cleaner profile with a megapixel camera and 4x digital zoom. Quality prints are all the proof you need of your cutting edge savvy.', qtyInStock: 20).save()
    new Product(name: 'Nokia 7610 White', category: '7000', image: 'Nokia_7610_white.gif', price: 399.99, description: 'The Nokia 7610 imaging phone with its sleek, compact design stands out in any crowd. Cut a cleaner profile with a megapixel camera and 4x digital zoom. Quality prints are all the proof you need of your cutting edge savvy.', qtyInStock: 7).save()
    new Product(name: 'Nokia 6680', category: '6000', image: 'Nokia_6680.gif', price: 219.00, description: 'The Nokia 6680 is an imaging smartphone.', qtyInStock: 15).save()
    new Product(name: 'Nokia 9300', category: '9000', image: 'Nokia_9300_close.gif', price: 599.00, description: 'The Nokia 9300 combines popular voice communication features with important productivity applications in one well-appointed device. Now the tools you need to stay in touch and on top of schedules, email, news, and messages are conveniently at your fingertips.', qtyInStock: 26).save()
    new Product(name: 'Nokia 9500', category: '9000', image: 'Nokia_9500_close.gif', price: 799.99, description: 'Fast data connectivity with Wireless LAN. Browse the Internet in full color, on a wide, easy-to-view screen. Work with office documents not just email with attachments and memos, but presentations and databases too.', qtyInStock: 54).save()
    new Product(name: 'Nokia N90', category: '9000', image: 'Nokia_N90.gif', price: 499.00, description: 'Twist and shoot. It is a pro-photo taker. A personal video-maker. Complete with Carl Zeiss Optics for crisp, bright images you can view, edit, print and share. Meet the Nokia N90.', qtyInStock: 12).save()
    }
    def destroy = {
    }
    }

  12. Now run the application by executing grails run-app and when started open a browser and navigate to http://localhost:8080/product/main.mxml





Now enjoy and look at the list of products retrieved during startup of the application. Navigate trough the product list and change the properties on the right side. Pushing the Update button will update the record in the database.

Note that updating causes a long stacktrace in the console window, but the update succeeds anyway. It's a StackOverflowError within HSQLDB; I tested the code on a MySQL database and then there is no error thrown.

32 comments:

greendog said...

Perfectly, Marcel!
Prompt please how to make, that at start of a server the database was not updated?

greendog said...

Has forgotten to write.
At me your example at once has not earned. There was an error - a long field "description:".
I have corrected. All worked.

greendog said...

Strange... I cannot edit data through phpMyadmin. Error:
"Method Not Implemented
GET to /phpMyAdmin/tbl_change.php not supported."
Other databases work normally.

And more. At attempt to update a product, returns such error:

[RPC Fault faultString="groovy.lang.ReadOnlyPropertyException : Cannot set readonly property: properties for class: Product" faultCode="Server.Processing" faultDetail="null"]
at mx.rpc::AbstractInvoker/http://www.adobe.com/2006/flex/mx/internal::faultHandler()[E:\dev\flex_3_beta3\sdk\frameworks\projects\rpc\src\mx\rpc\AbstractInvoker.as:223]
at mx.rpc::Responder/fault()[E:\dev\flex_3_beta3\sdk\frameworks\projects\rpc\src\mx\rpc\Responder.as:56]
at mx.rpc::AsyncRequest/fault()[E:\dev\flex_3_beta3\sdk\frameworks\projects\rpc\src\mx\rpc\AsyncRequest.as:110]
at NetConnectionMessageResponder/statusHandler()[E:\dev\flex_3_beta3\sdk\frameworks\projects\rpc\src\mx\messaging\channels\NetConnectionChannel.as:531]
at mx.messaging::MessageResponder/status()[E:\dev\flex_3_beta3\sdk\frameworks\projects\rpc\src\mx\messaging\MessageResponder.as:229]

domix said...

Great plugin¡¡ Congrats..
May be in futures releases the plugin generate automatically the AS3 Class for the domain model.

Marcel Overdijk said...

@domic
Yes, generating the AS3 class for the domain model and even scaffolding is on the roadmap (see http://jira.codehaus.org/browse/GRAILSPLUGINS-225). But I don't know when as I'm working on this (and other things ;-) in my spare time.

@greendog
What's exactly the problem? I don't see what phpMyAdmin has to do with the example? Did you follow the exact steps from the tutorial?

Cheers,
Marcel

Armand Janssen said...

Real nice!

BlazeDS is currently still beta. But since it is a direct copy from lifecycle server, it should be stable code.

It is expected that the first official release will be next month together with the release of Flex 3!

Marcel Overdijk said...

I had some email conversation today with greendog. We found out that a bug in Grails 1.0-RC4 caused his problems and it runs only on the latest Grails 1.0-final development build.

I updated the tutorial for future readers. So make sure you use the latest development build!

Anonymous said...

Hi Marcel,
Can you elaborate a bit more regarding the transaction commit for each service? For instance, if the update method is more complex, like updating other database object how the commit is guaranteed for both database objects?

Marcel Overdijk said...

@paulo jorge dias

All Grails services have transaction demarcation enabled by default. See http://grails.org/doc/1.0.x/guide/8.%20The%20Service%20Layer.html#8.1 Declarative Transactions for more information.

Jeremy Anderson said...

Very nice...this makes it very easy to spike out a quick Flex app.

Jeremy Anderson said...

Very nice indeed. I just finished working through this, and there's only one thing that I would like to add to the wish list. Storing the .mxml files under /web-app make sense to me, but I'd like to see a folder under the /src directory for the .as files so they don't end up cluttering up the /web-app directory.

Keep up the good work.

Unknown said...

Hi, I just tried this but Flex compiler fails at this point:

1 Error found.

Error /ProductForm.mxml:5
Type was not found or was not a compile-time constant: Product.

4:
5: Product id="product"
6: name="{productName.text}"

Anonymous said...

I get an error with Groovy v1.0 and the flex plugin 0.1

02/12 10:39:11 INFO Loading configuration file C:\projects\product\web-app\WEB-I
NF\flex\flex-webtier-config.xml
02/12 10:39:11 INFO Loading configuration file C:\projects\product\web-app\WEB-I
NF\flex\flex-config.xml
2008-02-12 10:39:11.995:/product:INFO: FlexMxmlServlet: Starting Adobe Flex Web
Tier Compiler
2008-02-12 10:39:11.011:/product:INFO: FlexMxmlServlet: Adobe Flex Web Tier Com
piler Build: 190739
2008-02-12 10:39:12.386::WARN: failed MessageBrokerServlet
java.lang.NoClassDefFoundError: flex/management/BaseControl
at flex.messaging.config.MessagingConfiguration.createBroker(MessagingCo
nfiguration.java:104)
at flex.messaging.MessageBrokerServlet.init(MessageBrokerServlet.java:10
7)
at org.mortbay.jetty.servlet.ServletHolder.initServlet(ServletHolder.jav
a:433)
at org.mortbay.jetty.servlet.ServletHolder.doStart(ServletHolder.java:25
6)
at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:
40)

Anonymous said...

Sorry, I meant Grails v1.0

Anonymous said...

Great stuff, one question. When getting a list from a domain that has a hasMany relationship, I get the following error message:

failed to lazily initialize a collection of role

Only if I enable fetchMode to "eager" does it work, but I don't want to fetch the entire model. Any suggestions?

Thanks Kirk

Marcel Overdijk said...

@anonymous

The lazy loading exception is very common when using Hibernate together with remoting. The problem is that the session is already closed. There is no out-of-the-box solution as far as I know.

Easiest way to solve is it to create a custom DTO class and set the properties and collections of data you need. Then send back this object to your Flex client.

Cheers,
Marcel

Anonymous said...

I found one solution that seems to be working by modifying the web.xml files (2,3,4) and adding the following:

<filter>
<filter-name>sessionView</filter-name>
<filter-class>org.springframework.orm.hibernate3.support.OpenSessionInViewFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>sessionView</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

Marcel Overdijk said...

Grails automatically configures an GrailsOpenSessionInViewInterceptor which should take of this... but apparently it's not.

On a related I think passing the 'Hibernate' objects directly can give some problem. The BlazeDS JavaAdapter receives your 'Hibernate' object and converts it to the AS3 object to be used the Flex client. In this process it will read all properties and collections etc. I think this could cause a lot of unexpected database queries to be executed. But this should be investigated.

Anonymous said...

Humm, this does cause problems. Coming from ROR, where you could just use :include -> childcollection, I was hoping to do something similar in Grails. Looks like creating DTO's is the only solution for now.

Thanks Kirk

Marcel Overdijk said...

It's typically related to Hibernate and using it in a remoting environment.

For example when you use Hibernate together with GWT remoting you face the same problem. There is some project which claims to solve this problem for GWT: http://hibernate4gwt.sourceforge.net/
I don't how tight this is coupled with GWT, but maybe you check it out.

Good luck,
Marcel

Anonymous said...

Hi Marcel,

Very great job.

I want change location of .as and mxml views, how ?

I´ll try this. If you have any suggestion for this change, thanks.

PS: sorry poor english!

Marcel Overdijk said...

@Elissandro

I think you can put the .as and .mxml files in any subfolder of web-app.

Anonymous said...

Hi Marcel,

I want like Jeremy Anderson say, .as and mxml in other folder than WEB.

Ok, I change flex-config.xml element source-path puting absolute path to folder in path-element

This works only with absolute path.

If anyone know how to work with realtive path, thanks.

cheers

Anonymous said...

Nice plugin !

I played around nicely with it but is there a way to link the client session with the remoting service in order to implement some kind of security as we can't trust the client side ?

Thanks again for this plugin

Anonymous said...

Hi Marcel

I have tried out your plug-in and it worked fine for your example. I then tried to use it with one of my services that returns an Order. An Order has many Products and I get the following error...

"failed to lazily initialize a collection of role: Order.products"


Do you have any idea how I can tell BlaseDS or you plug-in not to worry about serialising "products"?


Cheers,
Stephen

Anonymous said...

Sorry. I see you've already answered my question above.

Anonymous said...

Hi

I tried the solution above adding OpenSessionInViewFilter.

It worked great for flex but I can't delete anything in grails. I get the following error...

Write operations are not allowed in read-only mode (FlushMode.NEVER/MANUAL): Turn your Session into FlushMode.COMMIT/AUTO or remove 'readOnly' marker from transaction definition.

Can anyone tell me how I can set the FlushMode or remove the 'readOnly' marker?

Cheers,
Stephen

Anonymous said...

Hi

I worked it out. I just had to extend the OpenSessionInViewFilter with my own class, set the flushmode in the constructor and use this class in my web.xml instead.


public class OpenSessionInViewFilter extends org.springframework.orm.hibernate3.support.OpenSessionInViewFilter {

/**
* User a different FlushMode
*/
protected Session getSession(SessionFactory sessionFactory) throws DataAccessResourceFailureException {
Session session = SessionFactoryUtils.getSession(sessionFactory, true);
session.setFlushMode(FlushMode.AUTO);
return session;
}

/**
* we do an explicit flush here just in case we do not have an automated flush
*/
protected void closeSession(Session session, SessionFactory factory) {
session.flush();
super.closeSession(session, factory);
}
}

nicolasdij said...

Hi Marcel! Excellent plugin the one you developed! I was wondering if i could use it for a service that saves an object and its children.
I tried it at home but didn't work as the hasMany relationship side is not converted to my DTO's instance

Unknown said...

Is it possible to access the sources of this plugin somewhere? Have you added anything to it since 0.2? Because I have a few features I would like to add.

Anonymous said...

good post

testando said...

What about the grails metaclasses? How BlazeDS and the flex plugin deal with that?
I'm developing an applicantion using grails and blazeDS, when I get the result of a get service an error like this is shown: ReferenceError: Error #1056: It's not possible to create the property metaClass br.com.maid.ct.model.Domain.Product.