Enterprise RIA With Spring 3, Flex 4 and GraniteDS
With the client+server RIA platform concept of GraniteDS, applications can be written faster with less code, have a clean architecture, and separated layers.
Join the DZone community and get the full member experience.
Join For FreeAdobe Flex is one of the most widely used client technologies for building rich applications and Spring 3 is one of the most popular Java application frameworks. These two technologies make a great combination for building enterprise applications with a modern-looking and rich user interface.
There are various options to integrate them, and each has its pros and cons, such as Web/REST services and the Spring-Flex project promoted by Adobe and SpringSource. There are lots of articles and resources about them. Here, I will focus on an alternative approach using the open-source project GraniteDS.
GraniteDS is based on a cleanroom implementation of the AMF3 remoting and messaging protocols and has been historically the first open-source implementation of the AMF3 protocol in Java. It has been providing out-of-the-box integration with Spring very early and this integration has continually been improved with each new version of GraniteDS, following a few core principles:
Provide a fully integrated Spring/Flex/GraniteDS RIA platform that makes configuration and integration code mostly inexistent. The platform includes in particular the tools and Flex client libraries necessary to easily use all features of Spring and its associated technologies (persistence, security, etc.).
Promote type-safety as much as possible in both Java and AS3 applications, ensuring that most integration issues can be detected early at compile/build time.
These core design choices make GraniteDS very different from for example Adobe BlazeDS which has only a server-side part. In this article, I will show this concept of the RIA platform at work by building a simple application using the following features:
Flex AMF remoting to Spring services.
Support for Hibernate/JPA detached entities directly in the Flex application: bye-bye DTOs and lazy initialization exceptions
Support for the Bean Validation API (JSR-303) with the corresponding Flex validators
Support for Spring Security 3 and Flex components that integrate with server-side authorization
Support for "real-time" data push
As a side note, GraniteDS still supports the classic Flex RemoteObject API and thus a close drop-in replacement for BlazeDS with some useful enhancements, but provides an alternative Flex API called Tide that is easier to use and brings the full power of the platform.
Project Setup
We have to start somewhere, and the first step is to create the Spring application. This is no big deal. We could just start with a standard Spring MVC application, and just add a few GraniteDS elements in the Spring application context. To make things easier, I'm going to use a Maven archetype (Maven 3 recommended):
mvn archetype:generate -DarchetypeGroupId=org.graniteds.archetypes -DarchetypeArtifactId=graniteds-tide-spring-jpa-hibernate -DgroupId=org.example -DartifactId=gdsspringflex -Dversion=1.0-SNAPSHOT
This creates a basic project skeleton that includes persistence, security and real-time data push features. As a starting point you can simply build the project and run it with the Maven jetty
plugin:
cd gdsspringflexmvn installcd webappmvn jetty:run-war
Then browse http://localhost:8080/gdsspringflex/gdsspringflex.swf, and log in with admin/admin or user/user.
The structure of the project is a classic multi-module Maven project with a Flex module, a Java module, and a Web application module. It uses the very nice flexmojos
plugin to build the Flex application with Maven.
<b>gdsspringflex</b>|- pom.xml|- flex |- pom.xml |- src/main/flex |- Main.mxml |- Login.mxml |- Home.mxml|- java |- pom.xml |- src/main/java |- org/example/entities |- AbstractEntity.java |- Welcome.java |- org/example/services |- ObserveAllPublishAll.java |- WelcomeService.java |- WelcomeServiceImpl.java|- webapp |- pom.xml |- src/main/webapp |- WEB-INF |- web.xml |- dispatcher-servlet.xml |- spring |- app-config.xml |- app-jpa-config.xml |- app-security-config.xml
If we forget about the default generated application sources in the Flex and Java modules, and focus only on configuration, the most interesting files are web.xml and the app\*config.xml
Spring configuration files.
The web.xml file basically includes Spring 3 listeners, a Spring MVC dispatcher servlet mapped on /graniteamf/*
that will handle AMF requests, and a Gravity servlet for Jetty mapped on /gravityamf/*
(Gravity is the name of the GraniteDS Comet-based messaging implementation).
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> <display-name>GraniteDS Tide/Spring</display-name> <description>GraniteDS Tide/Spring Archetype Application</description> <context-param> <param-name>contextConfigLocation</param-name> <param-value> /WEB-INF/spring/app-config.xml, /WEB-INF/spring/app-*-config.xml </param-value> </context-param> <!-- Spring listeners --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <listener> <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class> </listener> <!-- Spring MVC dispatcher servlet that handles incoming AMF requests on the /graniteamf endpoint --><servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup></servlet><servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/graniteamf/*</url-pattern></servlet-mapping> <!-- Gravity servlet that handles AMF asynchronous messaging request on the /gravityamf endpoint --> <servlet> <servlet-name>GravityServlet</servlet-name> <servlet-class>org.granite.gravity.jetty.GravityJettyServlet</servlet-class> <!--servlet-class>org.granite.gravity.tomcat.GravityTomcatServlet</servlet-class--> <!--servlet-class>org.granite.gravity.jbossweb.GravityJBossWebServlet</servlet-class--> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>GravityServlet</servlet-name> <url-pattern>/gravityamf/*</url-pattern> </servlet-mapping></span> <welcome-file-list> <welcome-file>index.html</welcome-file> </welcome-file-list></web-app>
The reason why there is a specific servlet for Gravity is that it is optimized to use the specific asynchronous capabilities of the underlying servlet container to get better scalability. This can not be achieved with the default Spring MVC dispatcher servlet. That's also why this is necessary to configure different servlet implementations depending on the target container (Tomcat, JBossWeb, etc.).
Next is the main Spring 3 configuration that is mostly basic Spring MVC stuff:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:graniteds="http://www.graniteds.org/config" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.graniteds.org/config http://www.graniteds.org/public/dtd/2.1.0/granite-config-2.1.xsd"> <!-- Annotation scan --> <context:component-scan base-package="org.example"/> <!-- Spring MVC configuration --> <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"/> <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"/> <!-- Configuration of GraniteDS --> <graniteds:flex-filter url-pattern="/*" tide="true"/> <!-- Simple messaging destination for data push --> <graniteds:messaging-destination id="welcomeTopic" no-local="true" session-selector="true"/> </beans>
The main thing concerning GraniteDS is the flex-filter declaration. There is also an example messaging topic that is used by the default Hello World application. app-jpa-config.xml contains the JPA configuration and does not include anything about GraniteDS. Lastly, Spring Security:
<beansxmlns="http://www.springframework.org/schema/beans"xmlns:security="http://www.springframework.org/schema/security"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:aop="http://www.springframework.org/schema/aop"xmlns:tx="http://www.springframework.org/schema/tx"xmlns:context="http://www.springframework.org/schema/context" xmlns:graniteds="http://www.graniteds.org/config"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd http://www.graniteds.org/config http://www.graniteds.org/public/dtd/2.1.0/granite-config-2.1.xsd" default-autowire="byName" default-lazy-init="true"> <security:authentication-manager alias="authenticationManager"> <security:authentication-provider> <security:user-service> <security:user name="admin" password="admin" authorities="ROLE_USER,ROLE_ADMIN" /> <security:user name="user" password="user" authorities="ROLE_USER" /> </security:user-service> </security:authentication-provider> </security:authentication-manager> <security:global-method-security secured-annotations="enabled" jsr250-annotations="enabled"/> <!-- Configuration for Tide/Spring authorization --> <graniteds:tide-identity/> <!-- Uncomment when there are more than one authentication-manager : <graniteds:security-service authentication-manager="authenticationManager"/> --></beans>
Once again mostly Spring stuff; we just find here the tide-identity
bean that is used to integrate Spring Security with the Tide Identity Flex component.
We're done with the server-side setup. GraniteDS detects automatically most of the Spring configuration at startup and configures itself accordingly, so these 10 lines of XML are generally enough for most projects. If you have a look at the various Maven POMs, you will find the dependencies on the server-side GraniteDS jars and client-side GraniteDS swcs. You can also have a look at the Flex mxml code of the example application generated by the archetype, but for now, I will start from scratch.
Remoting To Spring Services
First is the traditional Hello World and its incarnation as a Spring 3 service:
@RemoteDestinationpublic interface HelloService {public String hello(String name);}@Service("helloService")public class HelloServiceImpl implements HelloService {public String hello(String name) {return "Hello " + name;}}
You have probably noticed the @RemoteDestination
annotation on the interface, meaning that the service is allowed to be called remotely from Flex. Now the Flex application:
<s:Applicationxmlns:fx="http://ns.adobe.com/mxml/2009"xmlns:s="library://ns.adobe.com/flex/spark"xmlns:mx="library://ns.adobe.com/flex/mx" xmlns="*" preinitialize="init()"> <fx:Script> <![CDATA[ import org.granite.tide.Component; import org.granite.tide.spring.Spring; import org.granite.tide.events.TideResultEvent; import org.granite.tide.service.DefaultServiceInitializer; private function init():void { Spring.getInstance().initApplication(); Spring.getInstance().getSpringContext().serviceInitializer = new DefaultServiceInitializer('/gdsspringflex'); } [In] public var helloService:Component; private function hello(name:String):void { helloService.hello(name, function(event:TideResultEvent):void { message.text = "Message: " + (event.result as String); } ); } ]]> </fx:Script> <s:VGroup width="100%"> <s:Label text="Name"/> <s:TextInput id="tiName"/> <s:Button label="Hello" click="hello(tiName.text)"/> <s:Label id="message"/></s:VGroup> </s:Application>
You can rebuild and restart the project with:
mvn clean installcd webappmvn jetty:run-war
Well, that's not exactly the shortest Hello World application, but let's see the interesting bits:
The
init()
method is called in the preinitialize handler. It does two things: initializes the Tide framework with Spring support, and declares a service initializer with the context root of our application. The task of the service initializer is to set up all remoting/messaging stuff, such as server endpoint uris, channels, etc. Basically, it replaces the traditional Flex static services-config.xml file. Other implementations can easily be built for example to retrieve the channel's configuration dynamically from a remote file (useful with an AIR application for example).A client proxy for the
helloService
Spring bean is injected in the mxml by using the annotation[In]
. By default, the variable name should match the Spring service name; otherwise, we would have to specify the service name in the[In("helloService")]
annotation. This may seem like a "magic" injection, but as we asked for an instance of Component, the framework knows for sure that you want a client proxy for a remote bean.The
hello
function demonstrates the basic Tide remoting API. It simply calls a remote method on the client proxy with the required arguments and provides callbacks for result and fault events, much like jQuery, so you don't have to deal manually with event listeners, asynchronous tokens, responders, and all the joys ofRemoteObject
.
Now that we got the basics working, we can improve this a little. The Flex project POM is configured to automatically generate (with the GraniteDS Gas3 generator embedded in flexmojos) typesafe AS3 proxies for all Java interfaces named *Service
and annotated with @RemoteDestination
. That means we could also simply write this:
import org.example.services.HelloService;[In]public var helloService:HelloService;
This looks like a minor cosmetic change, but now you benefit from code completion in your IDE and from better error checking by the Flex compiler. Going even further, the whole injection can be made completely typesafe and not rely on the service name any further by using the annotation [Inject]
instead of [In]
(note that we can now give our injected variable any name):
[Inject]public var myService:HelloService;private function hello(name:String):void { myService.hello(name, function(event:TideResultEvent):void { message.text = "Message: " + (event.result as String); } );}
And as the injection in the client now uses the interface name, we don't have to give a name to the Spring service anymore:
@Servicepublic class HelloServiceImpl implements HelloService {public String hello(String name) {return "Hello " + name;}}
Now you can do any refactoring you want on the Java side, like changing method signatures. Gas3 will then regenerate the AS3 proxies and the Flex compiler will immediately tell you what's wrong. Another interesting thing is that the Flex mxml now looks like a Spring bean, making it very easy for Spring developers to get started with Flex.
Persistence and Integration With Hibernate/JPA
Let's go a bit further and see how to do simple CRUD with a couple of JPA entities:
@Entitypublic class Author extends AbstractEntity { @Basic private String name; @OneToMany(cascade=CascadeType.ALL, fetch=FetchType.LAZY, mappedBy="author", orphanRemoval=true) private Set books = new HashSet(); // Getters/setters ...}@Entitypublic class Book extends AbstractEntity { @Basic private String title; @ManyToOne(optional=false) private Author author; // Getters/setters ...}
Both entities extend AbstractEntity
, but it is not mandatory at all. It's just a helper class provided by the Maven archetype.
Now we create a simple Spring service to handle basic CRUD for these entities (obviously a politically correct Spring service should use DAOs, but DAOs are awful and won't change anything here):
@RemoteDestinationpublic interface AuthorService { public List<author> findAllAuthors(); public Author createAuthor(Author author); public Author updateAuthor(Author author); public void deleteAuthor(Long id);}@Servicepublic class AuthorServiceImpl implements AuthorService { @PersistenceContext private EntityManager entityManager; @Transactional(readOnly=true) public List<author> findAllAuthors() { return entityManager.createQuery("select a from Author a order by a.name").getResultList(); } @Transactional public Author createAuthor(Author author) { entityManager.persist(author); entityManager.refresh(author); return author; } @Transactional public Author updateAuthor(Author author) { return entityManager.merge(author); } @Transactional public void deleteAuthor(Long id) { Author author = entityManager.find(Author.class, id); entityManager.remove(author); }}</author></author>
And the Flex application:
<s:Applicationxmlns:fx="http://ns.adobe.com/mxml/2009"xmlns:s="library://ns.adobe.com/flex/spark"xmlns:mx="library://ns.adobe.com/flex/mx" xmlns="*" preinitialize="init()"> <fx:Script> <![CDATA[ import mx.controls.Alert; import mx.collections.ArrayCollection; import mx.data.utils.Managed; import org.granite.tide.spring.Spring; import org.granite.tide.service.DefaultServiceInitializer; import org.granite.tide.events.TideResultEvent; import org.granite.tide.events.TideFaultEvent; import org.granite.tide.TideResponder; import org.example.entities.Author; import org.example.services.AuthorService; private function init():void { Spring.getInstance().initApplication(); Spring.getInstance().getSpringContext().serviceInitializer = new DefaultServiceInitializer('/gdsspringflex'); } [Inject] public var authorService:AuthorService; [Bindable] public var authors:ArrayCollection; private function findAllAuthors():void { authorService.findAllAuthors( function(event:TideResultEvent):void { authors = ArrayCollection(event.result); } ); } [Bindable] private var author:Author = new Author(); private function createAuthor():void { authorService.createAuthor(author, function(event:TideResultEvent):void { authors.addItem(author); author = new Author(); }, function(event:TideFaultEvent):void { Alert.show(event.fault.toString()); } ); } private function editAuthor():void { currentState = 'edit'; author = Author(lAuthors.selectedItem); } private function updateAuthor():void { authorService.updateAuthor(lAuthors.selectedItem, function(event:TideResultEvent):void { lAuthors.selectedItem = null; author = new Author(); currentState = 'create'; }, function(event:TideFaultEvent):void { Alert.show(event.fault.toString()); } ); } private function cancelAuthor():void { lAuthors.selectedItem = null; author = new Author(); currentState = 'create'; } private function deleteAuthor():void { authorService.deleteAuthor(lAuthors.selectedItem.id, function(event:TideResultEvent):void { var idx:int = authors.getItemIndex(lAuthors.selectedItem); authors.removeItemAt(idx); lAuthors.selectedItem = null; author = new Author(); currentState = 'create'; }, function(event:TideFaultEvent):void { Alert.show(event.fault.toString()); } ); } ]]> </fx:Script> <s:states> <s:State name="create"/> <s:State name="edit"/> </s:states> <s:Group width="800"> <s:layout> <s:VerticalLayout paddingLeft="10" paddingRight="10" paddingTop="10" paddingBottom="10"/> </s:layout> <mx:Form id="fAuthor"> <mx:FormHeading label.create="New author" label.edit="Edit author"/> <mx:FormItem label="Name"> <s:TextInput id="iName" text="@{author.name}"/> </mx:FormItem> <mx:FormItem> <s:HGroup> <s:Button id="bSave" label.create="Create" label.edit="Update" click.create="createAuthor()" click.edit="updateAuthor()"/> <s:Button id="bDelete" label="Delete" visible.create="false" visible.edit="true" click.edit="deleteAuthor()"/> <s:Button id="bCancel" label="Cancel" visible.create="false" visible.edit="true" click.edit="cancelAuthor()"/> </s:HGroup> </mx:FormItem> </mx:Form> <s:Label fontWeight="bold" text="Authors List"/> <s:List id="lAuthors" dataProvider="{authors}" labelField="name" width="100%" change="editAuthor()" creationComplete="findAllAuthors()"/> <s:Button label="Refresh" click="findAllAuthors()"/> </s:Group> </s:Application>
This is a simple CRUD application using some very convenient Flex 4 features such as states and bidirectional data binding. Note how all boilerplate code to connect the client and the server has literally disappeared while still keeping a clean separation between the two layers. In the real world, we would probably want to apply some MVC pattern instead of a monolithic mxml, but this would not change much. The important thing is that the Flex and Java parts do not contain useless or redundant code and are thus a lot easier to maintain. Even using model-driven code generators to build the Flex application automatically would be easier because there is basically much less code to generate.
Now rebuild and run the application on Jetty, and check that you can create, update and delete authors. There are two minor issues with this example that have not much interest in themselves but that I will use to show two interesting features of Tide.
First issue: when you start updating the name of an author, the change is propagated to the list by bidirectional binding, but the previous value is not restored when you click on "Cancel," leading to an inconsistent display. This is mainly a problem of Flex 4 bidirectional binding because it propagates all changes immediately but it is not able to roll back these changes. We would have three options to fix it: save the entity state somewhere before editing and restore it upon cancellation, copy the original data and bind the input fields on the copy (but then the list would not be updated by binding), or avoid using bidirectional binding. None of these options is really appealing. Fortunately, Tide provides a very simple feature to deal with this and makes bidirectional binding really usable:
private function cancelAuthor():void { Managed.resetEntity(author); lAuthors.selectedItem = null; author = new Author(); currentState = 'create'; }
The Managed.resetEntity()
function simply rolls back all changes done on the Flex side and restores the last stable state received from the server.
Second issue: clicking on refresh loses the current selected item. This is because we replace the dataProvider
of the list each time we receive a new collection. This can easily be fixed by using the data merge functionality of Tide:
public var authors:ArrayCollection = new ArrayCollection();private function findAllAuthors():void { authorService.findAllAuthors(new TideResponder(null, null, null, authors));}
This relatively ugly remote call using a TideResponder
indicates to Tide that it should merge the call result with the provided variable instead of completely replacing the collection with authors = event.result
in the result handler. Note that we don't even need a result handler anymore, once again saving a few lines of codes.
This issue with item selection illustrates why keeping the same collection and entity instances across remote calls is very important if you want to set up data-driven visual effects or animations (remember, the R of RIA). All visual features of Flex highly depend on the object instance that drives the effect and the events that it dispatches. This is where the Tide entity cache and merge help a lot by ensuring that each entity instance will exist only once and dispatch only the necessary events when it is updated from the server.
I'll finish this part by showing how to display and update the collection of books. For now, you have maybe noticed that the existence of this collection did not cause any problem at all, though it is marked lazy on the JPA entity. No LazyInitializationException
, and no particular issue when merging the entity modified in Flex in the JPA persistence context. GraniteDS has transparently serialized and deserialized the Hibernate internal state of the collection back and forth, thus making the data coming from Flex appear exactly as if it came from a Java client.
So let's try to implement the editing of the list of books in the update form. We don't have to change anything in the service. Hibernate will take care of persisting the collection because of the cascading option we selected. On the Flex side, we can use an editable List (note that there is no built-in editable Spark List in Flex 4, so we use a custom ItemRenderer inspired by this blog post, see the full sources attached), and add this:
<mx:FormItem label="Books" includeIn="edit"> <s:HGroup> <s:List id="lBooks" dataProvider="{author.books}" labelField="title" width="300" itemRenderer="BookItemRenderer"/> <s:VGroup> <s:Button label="Add" click="addBook()"/> <s:Button label="Remove" enabled="{Boolean(lBooks.selectedItem)}" click="removeBook()"/> </s:VGroup> </s:HGroup></mx:FormItem>
And the corresponding script actions:
private function addBook():void { var book:Book = new Book(); book.author = author; author.books.addItem(book); lBooks.selectedIndex = author.books.length-1;}private function removeBook():void { author.books.removeItemAt(lBooks.selectedIndex);}
The Tide framework automatically takes care of initializing the collection when needed, you just have to bind it to any Flex data component such as List. Once again all the usual boilerplate code necessary to deal with data and collections has completely disappeared.
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:
@Entitypublic 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 ...}@Entitypublic 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 behavior 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 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:Applicationxmlns: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:
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 remember Spring security configuration, it's the client counterpart of the tide-identity component we declared there.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 anymore:
<s:VGroupxmlns: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 anymore.
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 a 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 as 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 :
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 toauthorTopic
.Add the
DataPublishListener
entity listener to our entitiesAuthor
andBook
. In our example, they already extend the AbstractEntity class provided by the archetype so it's already the case.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 to 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);}
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. From there, 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.
Conclusion
That was a long article, but at least I hope it gave you a good idea about the client+server RIA platform concept of GraniteDS. Applications can be written faster with a lot less code, and still have a clean architecture with clearly separated layers.
Opinions expressed by DZone contributors are their own.
Comments