William has posted 6 posts at DZone. View Full User Profile

Enterprise RIA with Spring 3, Flex 4 and GraniteDS

01.25.2011
| 47819 views |
  • submit to reddit

Integration with Bean Validation

Our application is still missing a critical piece : data validation. As a first step, we can leverage the Hibernate integration with Bean Validation and simply annotate our entities to let the server handle validation :

@Entity
public class Author extends AbstractEntity {

@Basic
@Size(min=2, max=25)
private String name;

@OneToMany(cascade=CascadeType.ALL, fetch=FetchType.LAZY, mappedBy="author", orphanRemoval=true)
@Valid
private Set books = new HashSet();

// Getters/setters
...
}

@Entity
public class Author extends AbstractEntity {

@Basic
@Size(min=2, max=100)
private String title;

...
}

 

Once you change this and redeploy, creating an invalid author has now become impossible but there is error message is a mess that cannot be understood by a real user. We could simply add a particular behaviour in the fault handler :

function(event:TideFaultEvent):void {
if (event.fault.faultCode == 'Validation.Failed') {
// Do something interesting, for example show the first error message
Alert.show(event.fault.extendedData.invalidValues[0].message);
}
else
Alert.show(event.fault.toString());
}

 

We now get the standard Flex validation error popup on the correct input field. It's definitely nicer, but it would be even better if we didn't have to call the server at all to check for text size. Of course we could manually add Flex validators to each field, but it would be very tedious and we would have to maintain consistency between the client-side and server-side validator rules.

Fortunately with GraniteDS 2.2 it can be a lot easier. If you have a look at the generated ActionScript 3 entity for Author (in fact its parent class AuthorBase.as found in flex/target/generated-sources), you will notice that annotations have been generated corresponding to the Java Bean Validation annotations.

The FormValidator component is able to use these annotations and automatically handle validation on the client side, but we first have to instruct the Flex compiler that it should keep these annotations in the compiled classes, which is not the case by default. In the Flex module pom.xml, you can find a section , just add the validation annotations that we are using here :

<keepAs3Metadata>NotNull</keepAs3Metadata>
<keepAs3Metadata>Size</keepAs3Metadata>
<keepAs3Metadata>Valid</keepAs3Metadata>

 

After a new clean build and restart, you can see that the GraniteDS validation engine now enforces the constraints on the client, which gives the user a much better feedback about its actions. We can also prevent any call to the server when something is wrong :

private function updateAuthor():void {
if (!fvAuthor.validateEntity())
return;
authorService.updateAuthor(...);
}

 

Integration with Spring Security

All is good, but anyone can modify anything on our ultra critical book database, so it's time to add a bit of security. The Maven archetype includes a simple Spring Security 3 setup with two users admin/admin and user/user that is obviously not suitable for any real application but that we can just use as an example. The first step is to add authentication, and we can for example reuse the simple login form Login.mxml provided by the archetype. We just need the logic to switch between the login form and the application, so we create a new main mxml by renaming the existing Main.mxml to Home.mxml and creating a new Main.mxml :

<s:Application
xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx"
xmlns="*"
controlBarVisible="{identity.loggedIn}"
preinitialize="Spring.getInstance().initApplication()"
currentState="identity.loggedIn ? 'loggedIn' : ''"
creationComplete="init()">

<fx:Script>
<![CDATA[
import org.granite.tide.spring.Spring;
import org.granite.tide.spring.Identity;
import org.granite.tide.service.DefaultServiceInitializer;

[Bindable] [Inject]
public var identity:Identity;

private function init():void {
// Define service endpoint resolver
Spring.getInstance().getSpringContext().serviceInitializer = new DefaultServiceInitializer('/gdsspringflex');

// Check current authentication state
identity.isLoggedIn();
}
]]>
</fx:Script>

<s:states>
<s:State name=""/>
<s:State name="loggedIn"/>
</s:states>

<s:controlBarContent>
<s:Label text="Spring Flex GraniteDS example" fontSize="18" fontWeight="bold" width="100%"/>
<s:Button label="Logout" click="identity.logout();"/>
</s:controlBarContent>

<Login id="loginView" excludeFrom="loggedIn"/>
<Home id="homeView" includeIn="loggedIn"/>

</s:Application>

 

As you can see we have moved the Tide initialization to this new mxml, and added two main blocks to handle authentication :

  1. Once again we use the handy Flex 4 states to display the login form or the application, and bind the current state to the Tide Identity component loggedIn property that represents the current authentication state. If you remind of the Spring security configuration, it's the client counterpart of the tide-identity component we declared there.
  2. We call identity.isLoggedIn() at application startup to detect if the user is already authenticated, so for example a browser refresh will not redisplay the login form. It can also be useful when the authentication is done through a simple Web page and you just want to retrieve the authentication state instead of displaying a Flex login form.

Except removing the Tide initialization, we also need to do a small change to Home.mxml as it is not the main mxml any more :

<s:VGroup
xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx"
xmlns="*">

<fx:Metadata>[Name]</fx:Metadata>

<fx:Script>
<![CDATA[
import mx.controls.Alert;
import mx.collections.ArrayCollection;
import org.granite.tide.events.TideResultEvent;
import org.granite.tide.events.TideFaultEvent;

import org.example.entities.Author;
import org.example.services.AuthorService;

[Inject]
public var authorService:AuthorService;
]]>
...
</s:VGroup>
The metadata [Name] indicates that this mxml has to be managed by Tide (i.e. injection, observers...). Without it, nothing will work any more.

 

Now we would like to prevent non-administrator users from deleting authors. As with did with validation, we can in a first step rely on server-side security and simply annotate the service method :

@Transactional
@Secured("ROLE_ADMIN")
public void deleteAuthor(Long id) {
Author author = entityManager.find(Author.class, id);
entityManager.remove(author);
}

 

You can check that you cannot delete an author when logged in as user. The error message is handled by our fault handler and displayed as an alert. It's a bit tedious to handle this in each and every fault handler, so you can define a custom exception handler that will globally intercept such security errors that always have a faultCode 'Server.Security.AccessDenied' :

public class AccessDeniedExceptionHandler implements IExceptionHandler {

public function accepts(emsg:ErrorMessage):Boolean {
return emsg.faultCode == 'Server.Security.AccessDenied';
}

public function handle(context:BaseContext, emsg:ErrorMessage):void {
// Do whatever you want here, for example a simple alert
Alert.show(emsg.faultString);
}
}
And register this handler in the main mxml with :
Spring.getInstance().addExceptionHandler(AccessDeniedExceptionHandler);
Now authorization errors will be properly handled and displayed for all remote calls.

 

That would be even better if we didn't even display the 'Delete' button to our user if he's not allowed to use it. It's very easy to hide or disable parts of the UI depending on user access rights by using the Identity component that has a few methods similar to the Spring Security jsp tags :

<s:Button label="Delete" visible="{identity.ifAllGranted('ROLE_ADMIN')}" includeInLayout="{identity.ifAllGranted('ROLE_ADMIN')}" click="..."/>

 

Finally if you manage to configure Spring Security ACL (I won't even try to show this here, it would require a complete article), you could use domain object security and secure each author instance separately (8 is the Spring Security ACL bit mask for 'delete') :

<s:Button label="Delete" visible="{identity.hasPermission(author, '8')}" includeInLayout="{identity.hasPermission(author, '8')}" click="..."/>

 

As with validation, you can see that most of the value of GraniteDS resides in the Flex libraries that it provides. This level of integration cannot be achieved with a server-only framework.

Data push

The last thing I will demonstrate is the ability to dispatch updates on an entity to all connected clients. This can be useful with frequently updated data, so every user has an up-to-date view without having to click on some 'Refresh' button. Enabling this involves a few steps :

  1. Define a GraniteDS messaging topic in the Spring configuration. The archetype already defines a topic named welcomeTopic, we can just reuse it and for example rename it to authorTopic.
  2. Add the DataPublishListener entity listener to our entities Author and Book. In our example they already extend the AbstractEntity class provided by the archetype so it's already the case.
  3. Configure a client DataObserver for the topic in the main mxml, and bind its subscription/unsubscription to the login/logout events so publishing can depend on security :
    Spring.getInstance().addComponent("authorTopic", DataObserver);
    Spring.getInstance().addEventObserver("org.granite.tide.login", "authorTopic", "subscribe");
    Spring.getInstance().addEventObserver("org.granite.tide.logout", "authorTopic", "unsubscribe");
  4. Annotate all service interfaces (or all implementations) with @DataEnabled, even if they are read-only :
    @RemoteDestination
    @DataEnabled(topic="authorTopic", params=ObserveAllPublishAll.class, publishMode=PublishMode.ON_SUCCESS)
    public interface AuthorService {
    ...
    }

 

The default ObserveAllPublishAll class comes from the archetype and defines a publishing policy where everyone receives everything. Alternative dispatching strategies can be defined if it's necessary to restrict the set of recipients depending on the data itself for security, functional or performance reasons.

Now you can rebuild and restart and connect from two different machines or two different browsers, and check that changes made on an author in one browser are propagated to the other.

New and deleted authors are not propagated automatically, we have to handle these two cases manually. This is not very hard, we just have to observe some built-in events dispatched by Tide :

[Observer("org.granite.tide.data.persist.Author")]
public function persistAuthorHandler(author:Author):void {
authors.addItem(author);
}

[Observer("org.granite.tide.data.remove.Author")]
public function removeAuthorHandler(author:Author):void {
var idx:int = authors.getItemIndex(author);
if (idx >= 0)
authors.removeItemAt(idx);
}

 

Fine, the list is now correctly refreshed with new and deleted authors, but you will notice that new authors are added twice on the current user application. Indeed we add it from both the data observer and the result handler. We can safely keep only the global observer since it will always be called and remove the addItem from the handler. Such observers can be put in many views at the same time and all of them will be updated.

Published at DZone with permission of its author, William Draï.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Nuttapong Maneenate replied on Sat, 2011/02/12 - 9:59am

Why step mvn install it error ? [ERROR] Failed to execute goal org.sonatype.flexmojos:flexmojos-maven-plugin:3.8 :compile-swf (default-compile-swf) on project gdsspringflex-flex: Error compilin g! -> [Help 1]

Lucas Marino replied on Tue, 2011/02/22 - 11:05pm

Thank you for your work is excellent. I am interested in your articles and I would ask permission to translate into Spanish and post mentioning the source of course. If you agree email me. My email is marinoluck@gmail.com or answer me in this article. Thank you very much. Lucas Marino

Arthur Vernon replied on Wed, 2011/02/23 - 5:22pm

With the Data Push part of the tutorial, what facilities exist within the framework to identify the source of an update to avoid adding a new author twice to the list? As it currently stands, when I take this code "as is" and add a new author, the code 1. Adds the author to the authors list at creation time. 2. Adds it a second time as a result of the observer. I suppose the trusting solution is to maintain the list only via the observer.

William Draï replied on Thu, 2011/02/24 - 3:55pm in response to: Nuttapong Maneenate

@Nuttapong : Not sure what happens here, maybe try to reset you maven repo.

@Arthur : Right, the recommended solution is to keep only the observer. Unfortunately there is currently no way of knowing where the update comes from (local, remoting or push).

Chris Jansen replied on Fri, 2011/02/25 - 11:24am

I had to specify a version for maven-jetty-plugin to get it to run inside of Spring STS IDE.  Great tutorial too - thank for writing it up!

Richard Van Der Laan replied on Tue, 2011/03/08 - 6:09am

Hi there,

I am unable to extract the extended data from the validation exception, as described in the example:

function(event:TideFaultEvent):void {

    if (event.fault.faultCode == 'Validation.Failed') {        // Do something interesting, for example show the first error message-->        Alert.show(event.fault.extendedData.invalidValues[0].message);    }}
 The problem is that fault (type mx.rpc.Fault) has no extendedData property and the property cannot dynamically be accessed. Is this due to an API change? Or am I doing something wrong?

Deepak Srivastav replied on Fri, 2011/03/11 - 8:01am

my entity attached with a form is not updated in the database ie the effect of updating entity in flex side does not update the entity the method of service is called each time but the entity remained same .I will greatly appreciate any kind of help

iain starks replied on Fri, 2011/04/01 - 5:43am

I failed to do jetty:run-war after creating the archetype, getting a java.lang.NoSuchMethodError: javax.persistence.spi.PersistenceUnitInfo.getValidationMode()Ljavax/persistence/ValidationMode; The version ins persistence-api 1.0 doesn't have this method, but it was being pulled in as well from the hibernate-jpa-2.0-api-1.0.0.Final.jar So I removed the dependency: javax.persistence persistence-api 1.0 From the java pom and re-build and that seemed to get me going.

Eric Be replied on Mon, 2011/05/02 - 11:12pm

Hi, Great article! I was wondering if there was a way to access your completed exmaple/application to see the entire sources of everything. I can't find a d/l link anywhere in the tutorial and the archetype project doesn't include any of your more detailed classes/interfaces/etc. Thanks! Eric

Lou Leal replied on Mon, 2011/07/18 - 3:18pm in response to: Richard Van Der Laan

According to this http://www.graniteds.org/jira/browse/GDS-846 this error was fixed in 2.2.-_SP2 but I updated my pom to this version of granite and the same error occurs. I guess it didn't get fixed...

Dmitry Kv replied on Tue, 2011/08/09 - 11:03am in response to: Lou Leal

should be
(event.extendedData.invalidValues[0].message);

Khent Johnson replied on Fri, 2011/09/02 - 2:41pm

Hello! Just got in here. I used the most recent release of GraniteDS and same with Eric I'd like to access your complete example and if possible could anyone share the links of guide related to the topic. I'll be glad and thankful to any response. Cheers! GAR Labs

Sanjay Patel replied on Wed, 2012/08/29 - 7:53am

Thanks. This is very nice article to get started. 

Can you please add or guide how to get flexmojos wrapper goal working? 

Gary Huitson replied on Wed, 2012/09/12 - 10:03am in response to: Richard Van Der Laan

I think theres a mistake in the example code

The line should read...

Alert.show(event.extendedData.invalidValues[0].message);  

The extendedData is a property of event not event.fault 

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.