GraniteDS provides various features that simplify the handling of data between Flex and Java EE, in particular when using JPA or Hibernate as a persistence mechanism.
From the Flex documentation of IManaged
:
private var _property:String; public function get property():String { return Manager.getProperty(this, "property", _property); } public function set property(value:String):void { Manager.setProperty(this, "property", _property, _property = value); }
@MappedSuperclass
@EntityListeners({AbstractEntity.AbstractEntityListener.class})
public abstract class AbstractEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id @GeneratedValue
private Long id;
/* "UUID" and "UID" are Oracle reserved keywords -> "ENTITY_UID" */
@Column(name="ENTITY_UID", unique=true, nullable=false, updatable=false, length=36)
private String uid;
@Version
private Integer version;
public Long getId() {
return id;
}
public Integer getVersion() {
return version;
}
@Override
public boolean equals(Object o) {
return (o == this || (o instanceof AbstractEntity && uid().equals(((AbstractEntity)o).uid())));
}
@Override
public int hashCode() {
return uid().hashCode();
}
public static class AbstractEntityListener {
@PrePersist
public void onPrePersist(AbstractEntity abstractEntity) {
abstractEntity.uid();
}
}
private String uid() {
if (uid == null)
uid = UUID.randomUUID().toString();
return uid;
}
}
<gas3 outputdir="as3" tide="true">
<classpath>
<pathelement location="classes"/>
</classpath>
<fileset dir="classes">
<include name="com/myapp/entity/**/*.class"/>
</fileset>
</gas3>
In a typical Flex/app server/database application, an entity lives in three layers:
Object(myEntity.myCollection).withInitialized(function(collection:IList):void { // Do something with the content of the list var obj:Object = collection.getItemAt(0); });
public function savePerson():void {
person.lastName = "Test";
personService.save(person); // This will send all loaded collections associated to the Person object
}
var uperson:Person = new EntityGraphUnintializer(tideContext).uninitializeEntityGraph(person) as Person; personService.save(uperson); // This will send only the Person object without any loaded collection
person.contacts.getItemAt(0).email = '[email protected]'; var uperson:Person = new EntityGraphUnintializer(tideContext).uninitializeEntityGraph(person) as Person; personService.save(uperson); // This will send the Person object with only the contacts collection loaded
Tide uses the client data tracking (the same used for dirty checking, see below) to determine which parts of the graph need to be sent.
If you need to uninitialize more than one argument for a remote call, you have to use the same EntityGraphUninitializer
. It is important
because the uninitializer copies the entities in a temporary context before uninitializing their associations so the normal context keeps unchanged, and
all processed entities have to share this same temporary context.
var egu:EntityGraphUnintializer = new EntityGraphUninitialize(tideContext); uperson1 = egu.uninitializeEntityGraph(person1); uperson2 = egu.uninitializeEntityGraph(person2); personService.save(uperson1, uperson2);
Calling the EntityGraphUninitializer
manually is a bit tedious and ugly, so there is a cleaner possibility when you are using
generated typesafe service proxies. You can annotate your service method arguments with @org.granite.tide.data.Lazy
:
public void save(@Lazy Person person) {
}
Gas3 will then generate a [Lazy]
annotation on your service methods (so take care that you have added the [Lazy] annotation to your Flex
metadata compilation configuration). Next in the Flex application, register the UninitializeArgumentPreprocessor
component in Tide as follows :
Tide.getInstance().addComponents([UninitializeArgumentPreprocessor]);
Once you have done this, all calls to PersonService.save()
will automatically use a properly uninitialized version of the person
argument.
This new feature cannot be yet applied to local context variables (mostly used with Seam or CDI stateful components). Only method arguments can be processed, but this should cover be the vast majority of use cases. Support for context variables will come with GDS 3.0.
A particular entity instance can be in two states :
The current state of an entity can be accessed with :
entity.meta_dirty
<mx:Button label="Save" click="entityService.save()"
enabled="{tideContext.meta_dirty}"/>
In a typical client/server interaction, here is what happens :
private function restore():void { Managed.resetEntity(entity); }
You can also reset all entities in the context to their last stable state with :
private function restoreAll():void { tideContext.meta_resetAllEntities(); }
There are two ways of managing this: either you just rely on optimistic locking and intercept the corresponding server exceptions to display a message to the user, or you use data push (see section Data Push) so all Flex clients are updated in near real-time. Note however that even with data push, there can still be conflicts between changes made by a user and updates received from the server.
With normal optimistic locking, the remote service call at step 3 will trigger a OptimisticLockException
. Tide provides a built-in
exception handler to handle this case: it will extract the entity
argument of the exception, compare its state with the client state
and dispatch a conflict event TideDataConflictEvent
on the Tide context when it's not identical. The exception handler can be enabled with :
Tide.getInstance().addExceptionHandler(OptimisticLockExceptionHandler);
When data push is used, an entity instance can be updated with data received from the server at any time. If the current user was working on this instance,
it is obviously not desirable that his work is overwritten without notice. Similarly to the previous case, Tide will determine that an incoming data from
another user session is in conflict with the local data and dispatch a TideDataConflictEvent
on the Tide context.
What can you do with this event ? Basically there are two possibilities : accept the server-side state or keep the client state. Here is an example of a conflict handler defined in a Flex application, generally in the main mxml :
<mx:Application ...
preinitialize="Tide.getInstance().initApplication()"
creationComplete="init();">
<mx:Script>
private function init():void {
Tide.getInstance().getEjbContext().addEventListener(
TideDataConflictsEvent.DATA_CONFLICTS, conflictsHandler);
}
private var _conflicts:Conflicts;
private function conflictsHandler(event:TideDataConflictsEvent):void {
_conflicts = event.conflicts;
Alert.show("Keep local state ?", "Data conflict",
Alert.YES|Alert.NO, null, conflictsCloseHandler);
}
private function conflictsCloseHandler(event:CloseEvent):void {
if (event.detail == Alert.YES)
_conflicts.acceptAllClient();
else
_conflicts.acceptAllServer();
}
</mx:Script>
...
</mx:Application>
If you look at the ASDoc for Conflicts
, there are a few properties that give more details about the conflicts and make possible
to present a better alert message to the user.
When using the Hibernate native API (Session
), the optimistick lock exception StaleObjectStateException
is unfortunately missing a critical information to allow for correct conflict handling (that is present in the JPA OptimistickLockException
).
In this case, you should use the provided Hibernate event wrappers that add the missing data to the Hibernate exception. Here is what is will look like when
configuring the SessionFactory
with Spring :
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.HSQLDialect</prop>
<prop key="hibernate.show_sql">false</prop>
<prop key="hibernate.hbm2ddl.auto">update</prop>
</props>
</property>
<property name="eventListeners">
<map>
<entry key="merge"><bean class="org.granite.tide.hibernate.HibernateMergeListener"/></entry>
<entry key="create"><bean class="org.granite.tide.hibernate.HibernatePersistListener"/></entry>
<entry key="create-onflush"><bean class="org.granite.tide.hibernate.HibernatePersistOnFlushListener"/></entry>
<entry key="delete"><bean class="org.granite.tide.hibernate.HibernateDeleteListener"/></entry>
<entry key="update"><bean class="org.granite.tide.hibernate.HibernateSaveOrUpdateListener"/></entry>
<entry key="save-update"><bean class="org.granite.tide.hibernate.HibernateSaveOrUpdateListener"/></entry>
<entry key="save"><bean class="org.granite.tide.hibernate.HibernateSaveOrUpdateListener"/></entry>
<entry key="lock"><bean class="org.granite.tide.hibernate.HibernateLockListener"/></entry>
<entry key="flush"><bean class="org.granite.tide.hibernate.HibernateFlushListener"/></entry>
<entry key="auto-flush"><bean class="org.granite.tide.hibernate.HibernateAutoFlushListener"/></entry>
</map>
</property>
...
</bean>
When using client conversations, the situation becomes a bit more complex as each conversation has its own entity cache. That means that when a user modifies some data in a conversation context, the change is not immediately visible in others even if they contain the same entity instance.
The Tide context object provides a few methods to deal with such situations :
tideContext.meta_mergeInParentContext()
This will merge all stable state of the current conversation context in its parent context and all its children. In the common case where you don't use nested conversations, that just means that the changes are merged in the global context as well as all other conversations.
If you use nested conversations, the merge will be done only in the direct parent context and all its children, but not in the global context.
tideContext.meta_mergeInGlobalContext()
This will merge all stable state of the current conversation context in its parent context, in the global context and in all child contexts recursively.
Starting with GraniteDS 2.2, however, the preferred way to execute validation is a Flex-side, JSR-303 like, validation framework (see Bean Validation (JSR-303) for details).
The server support for Hibernate Validator 3 is available in granite-hibernate.jar
, and the support for Bean Validation is available
in granite-beanvalidation.jar
. You will have to add one of these jars in your application lib
folder.
The validator integration is based on the GraniteDS exception handling framework. A server exception converter is registered to handle the
InvalidStateException
, and a client exception handler can be registered with:
Seam.getInstance().addExceptionHandler(ValidatorExceptionHandler);
This exception handler intercepts all server validation faults and dispatches a validation event on the context. To complete the integration,
a TideEntityValidator
Flex component can be attached to any UI component:
<mx:Application
...
xmlns:tv="org.granite.tide.validators.*">
<tv:TideEntityValidator id="teval" entity="{tideContext.booking}"
property="creditCardName" listener="{creditCardNameInput}"/>
<mx:TextInput id="creditCardNameInput" text="{tideContext.booking.creditCardName}"/>
...
</mx:Application>
You can have more control on the validation behaviour by directly listening to validation events on the context:
public function setup():void { tideContext.addEventListener(TideValidatorEvent.INVALID, validationHandler); } private function validationHandler(event:TideValidatorEvent):void { var invalidValues:Array = event.invalidValues; for each (var iv:Object in invalidValues) { var invalidValue:InvalidValue = iv as InvalidValue; Alert.show( "Invalid property: " + invalidValue.path + " on object " + invalidValue.bean ); } }
Another possibility is to use an input validator that calls the server when the validation is triggered.
With Seam on the server, it then uses the Validators
component to get a proper ClassValidator
, and thus just works
with Hibernate Validator 3.x for now. With other server technologies, it uses a built-in validator handler which supports both HV3 and Bean Validation
implementations.
The use of this component is quite simple and very similar to any other Flex validator, with additional parameters to define the entity to validate.
<tv:TideInputValidator id="tival"
source="{creditCardNameInput}" property="text"
entity="{tideContext.booking}" entityProperty="creditCardName"
entityManager="{tideContext}"/>
<mx:TextInput id="creditCardNameInput" text="{tideContext.booking.creditCardName}"/>
The property entityManager
of the validator may be optional if the entity is maintained in the context (i.e., ctx.myEntity
)
and has a proper entityManager
defined (with meta_getEntityManager
). This is the case for all entities retrieved from
the server.
On the client-side, you first need to register the client component with:
<mx:Script>
import org.granite.tide.collections.PagedQuery;
Tide.getInstance().addComponent("people", PagedQuery);
</mx:Script>
<mx:Script>
import org.granite.tide.collections.PagedQuery;
Tide.getInstance().addComponentWithFactory("people", PagedQuery, { maxResults: 36 });
</mx:Script>
<mx:DataGrid id="people" dataProvider="{ctx.people}" width="100%" height="100%"
liveScrolling="false">
<mx:columns>
<mx:DataGridColumn dataField="firstName" headerText="First name"/>
<mx:DataGridColumn dataField="lastName" headerText="Last name"/>
</mx:columns>
</mx:DataGrid>
<s:List>
<mx:AsyncListView list="{people}"/>
</s:List>
See for more details in the Flex 4 documentation.
Just like the paged collections of LCDS, everything works only if there is a correct uid
property on the entities.
It is thus recommended to use the Tide templates for the Gas3 generator, which provide a default implementation of uid
if there is none.
The default AsyncListView
does not support automatic propagation of the UI control sorting to its data provider.
Tide provides an alternative SortableAsyncListView
that works in all cases.
<s:VGroup ...
xmlns:c="org.granite.tide.collections.*">
<s:DataGrid ...>
<c:SortableAsyncListView list="{people}"/>
</s:DataGrid>
The PagedQuery
components expects that the corresponding server component implements a specific method to fetch elements.
There are two ways of handling filtering, either with an untyped map or with a typesafe filter object:
For untyped filters, the server component shoud implement the following method:
public Map find(Map filter, int first, int max, String order, boolean desc);
first
, max
, order
and desc
are straightforward.
filter
is a map containing the parameter values of the query. These values can be set on the client by:
tideContext.pagedQuery.filter.<parameter1> = <value1>; tideContext.pagedQuery.filter.<parameter2> = <value2>; ...
The return object must be a map containing four properties:
firstResult
: Should be exactly the same as the argument passed in (int first).
maxResults
: Should be exactly the same as the argument passed in (int max), except when its value is 0, meaning that the client
component is initializing and needs a max value. In this case, you have to set the page value, which must absolutely be greater than the maximum
expected number of elements displayed simultaneously in a table.
resultCount
: Count of results.
resultList
: List of results.
The following code snippet is a quick and dirty implementation and can be used as a base for other implementations (here this is a Spring service but the equivalent implementations for EJB3 or CDI would be extremely similar):
@Service("people")
@Transactional(readOnly=true)
public class PeopleServiceImpl implements PeopleService {
@PersistenceContext
protected EntityManager manager;
public Map find(Map filter, int first, int max, String order, boolean desc) {
Map result = new HashMap(4);
String from = "from Person e ";
String where = "where lower(e.lastName) like '%' || lower(:lastName) || '%' ";
String orderBy = (
order != null ? "order by e." + order + (desc ? " desc" : "") : ""
);
String lastName = (
filter.containsKey("lastName") ? (String)filter.get("lastName") : ""
);
Query qc = manager.createQuery("select count(e) " + from + where);
qc.setParameter("lastName", lastName);
long resultCount = (Long)qc.getSingleResult();
if (max == 0)
max = 36;
Query ql = manager.createQuery("select e " + from + where + orderBy);
ql.setFirstResult(first);
ql.setMaxResults(max);
ql.setParameter("lastName", lastName);
List resultList = ql.getResultList();
result.put("firstResult", first);
result.put("maxResults", max);
result.put("resultCount", resultCount);
result.put("resultList", resultList);
return result;
}
}
It is also possible to define on the Flex size an alternative remote component name and method name that will implement the querying :
<mx:Script>
Spring.getInstance().addComponentWithFactory("people", PagedQuery,
{ maxResults: 36, remoteComponentName: "peopleService", methodName: "list" });
</mx:Script>
In this case, the previous component would be :
@Service("peopleService")
@Transactional(readOnly=true)
public class PeopleServiceImpl implements PeopleService {
@PersistenceContext
protected EntityManager manager;
public Map list(Map filter, int first, int max, String order, boolean desc) {
Map result = new HashMap();
String from = "from Person e ";
String where = "where lower(e.lastName) like '%' || lower(:lastName) || '%' ";
String orderBy = (
order != null ? "order by e." + order + (desc ? " desc" : "") : ""
);
String lastName = (
filter.containsKey("lastName") ? (String)filter.get("lastName") : ""
);
Query qc = manager.createQuery("select count(e) " + from + where);
qc.setParameter("lastName", lastName);
long resultCount = (Long)qc.getSingleResult();
if (max == 0)
max = 36;
Query ql = manager.createQuery("select e " + from + where + orderBy);
ql.setFirstResult(first);
ql.setMaxResults(max);
ql.setParameter("lastName", lastName);
List resultList = ql.getResultList();
result.put("resultCount", resultCount);
result.put("resultList", resultList);
return result;
}
}
Note that this time we did not have to return firstResult
and maxResults
because the page size is defined on the client.
It is finally possible to use a typesafe filter object instead of the Map
. The server implementation will then be something like :
@Service("peopleService")
@Transactional(readOnly=true)
public class PeopleServiceImpl implements PeopleService {
@PersistenceContext
protected EntityManager manager;
public Map list(Person examplePerson, int first, int max, String order, boolean desc) {
Map result = new HashMap();
String from = "from Person e ";
String where = "where lower(e.lastName) like '%' || lower(:lastName) || '%' ";
String orderBy = (
order != null ? "order by e." + order + (desc ? " desc" : "") : ""
);
String lastName = (
examplePerson.getLastName() != null ? examplePerson.getLastName() : ""
);
Query qc = manager.createQuery("select count(e) " + from + where);
qc.setParameter("lastName", lastName);
long resultCount = (Long)qc.getSingleResult();
if (max == 0)
max = 36;
Query ql = manager.createQuery("select e " + from + where + orderBy);
ql.setFirstResult(first);
ql.setMaxResults(max);
ql.setParameter("lastName", lastName);
List resultList = ql.getResultList();
result.put("resultCount", resultCount);
result.put("resultList", resultList);
return result;
}
}
To use this, you also have to define the filter class on the client component :
<mx:Script>
import org.granite.tide.collections.PagedQuery;
Tide.getInstance().addComponentWithFactory("people", PagedQuery, { maxResults: 36, filterClass: Person });
</mx:Script>
If the filter class is bindable, then people.filter
will be an instance of the provided filter class.
If not, the PagedQuery
will create an ObjectProxy
that wraps the filter instance to track changes on it.
You can also directly provide your own instance of the filter instead of letting the component instantiate the class itself:
<mx:Script>
import org.granite.tide.collections.PagedQuery;
Tide.getInstance().addComponentWithFactory("people", PagedQuery, { maxResults: 36, filter: new Person() });
</mx:Script>
This requires a bit of configuration :
Let's see all this in details :
Define a Gravity topic: in the standard case, it can be done in services-config.xml
:
<service id="gravity-service"
class="flex.messaging.services.MessagingService"
messageTypes="flex.messaging.messages.AsyncMessage">
<adapters>
<adapter-definition id="simple"
class="org.granite.gravity.adapters.SimpleServiceAdapter"/>
</adapters>
<destination id="dataTopic">
<properties>
<no-local>true</no-local>
<session-selector>true</session-selector>
</properties>
<channels>
<channel ref="gravityamf"/>
</channels>
<adapter ref="simple"/>
</destination>
</service>
...
<channel-definition id="gravityamf" class="org.granite.gravity.channels.GravityChannel">
<endpoint
uri="http://{server.name}:{server.port}/{context.root}/gravityamf/amf"
class="flex.messaging.endpoints.AMFEndpoint"/>
</channel-definition>
<graniteds:messaging-destination id="dataTopic" no-local="true" session-selector="true"/>
<graniteds:messaging-destination name="dataTopic" no-local="true" session-selector="true"/>
The two important parameters for the topic definition are :
no-local
should be set to true
. That means that the client that triggers the modification will not receive
the result of the update twice : first by the remoting call, then by the messaging update.
session-selector
must be set to true
. Tide uses JMS-style selectors to determine which data will be sent
to which clients and thus needs to store the current messaging selector state in the user session.
Add the Tide JPA publishing listener on the entities that should be tracked:
@Entity
@EntityListeners({DataPublishListener.class})
public abstract class MyEntity {
...
}
Add the Tide data annotation on all services, example here with a Spring service:
@DataEnabled(topic="dataTopic", publish=PublishMode.ON_SUCCESS)
public interface MyService {
...
}
The attributes of this annotations are :
topic
: the name of the messaging topic that will be used to dispatch updates. Obviously this is the one we declared just before in
services-config.xml
.
publish
: the publishing mode. PublishMode.MANUAL
means that you will have to trigger the dispatch manually,
for example in an interceptor. PublishMode.ON_SUCCESS
means that Tide will automatically dispatch the updates on any successful call.
PublishMode.ON_COMMIT
is not yet implemented.
params
: a filter class that will define to which updates are sent to which clients. By default there is no filtering,
otherwise see below for a more detailed explanation.
It is possible to tell the Tide engine how it should dispatch each update (i.e. to which clients).
public class AddressBookParams implements DataTopicParams {
public void observes(DataObserveParams params) {
params.addValue("user", Identity.instance().getCredentials().getUsername());
params.addValue("user", "__public__");
}
public void publishes(DataPublishParams params, Object entity) {
if (((AbstractEntity)entity).isRestricted())
params.setValue("user", ((AbstractEntity)entity).getCreatedBy());
else
params.setValue("user", "__public__");
}
}
public void observes(DataObserveParams params) {
params.addValue("user", Identity.instance().getCredentials().getUsername());
params.addValue("user", "__public__");
params.addValue("group", "admin");
params.addValue("group", "superadmin");
}
Will generate the following selector :
(user = 'username' OR user = '__public__') AND (group = 'admin' OR group = 'superadmin')
There are three publishing modes :
PublishMode.ON_SUCCESS
is the easiest to use, and dispatch updates after each successful remote call, regardless of the actual
result of the transaction (if there is one).
PublishMode.ON_COMMIT
allows for a transactional behaviour, and does the dispatch only on transaction commit.
PublishMode.MANUAL
lets you do the dispatch manually in your services when you want, giving the most control.
@DataEnabled(topic="dataTopic", publishMode=PublishMode.ON_COMMIT, useInterceptor=true)
public class MyService {
}
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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.graniteds.org/config http://www.graniteds.org/public/dtd/2.3.0/granite-config-2.3.xsd">
...
<graniteds:tide-data-publishing-advice/>
For CDI, enable the interceptor in beans.xml
:
<beans
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
<interceptors>
<class>org.granite.tide.cdi.TideDataPublishingInterceptor</class>
</interceptors>
</beans>
For EJB 3, you can define a global interceptor in ejb-jar.xml
:
<assembly-descriptor>
<interceptor-binding>
<ejb-name>*</ejb-name>
<interceptor-class>org.granite.tide.ejb.TideDataPublishingInterceptor</interceptor-class>
</interceptor-binding>
...
</assembly-descriptor>
Or alternatively configure the interceptor on each EJB 3 :
@Stateless
@Local(MyService.class)
@Interceptors(TideDataPublishingInterceptor.class)
@DataEnabled(topic="myTopic", publish=PublishMode.ON_COMMIT, useInterceptor=true)
public class MyServiceBean {
...
}
@DataEnabled(topic="dataTopic", params=DefaultDataTopicParams.class, publishMode=PublishMode.MANUAL, useInterceptor=true)
public class MyService {
@Inject
private Gravity gravity;
public void doSomething() {
DataContext.init(gravity, "dataTopic", DefaultDataTopicParams.class, PublishMode.MANUAL);
try {
Object result = invocation.proceed();
DataContext.publish(PublishMode.MANUAL);
return result;
}
finally {
DataContext.remove();
}
}
}
@Interceptor
public class CustomPublishInterceptor {
@Inject
private Gravity gravity;
@AroundInvoke
public Object aroundInvoke(InvocationContext invocation) throws Exception {
DataContext.init(gravity, "dataTopic", DefaultDataTopicParams.class, PublishMode.MANUAL);
try {
Object result = invocation.proceed();
DataContext.publish(PublishMode.MANUAL);
return result;
}
finally {
DataContext.remove();
}
}
}
<destination id="dataTopic">
<properties>
<jms>
<destination-type>Topic</destination-type>
<connection-factory>ConnectionFactory</connection-factory>
<destination-jndi-name>topic/dataTopic</destination-jndi-name>
<destination-name>dataTopic</destination-name>
<acknowledge-mode>AUTO_ACKNOWLEDGE</acknowledge-mode>
<transacted-sessions>true</transacted-sessions>
<no-local>true</no-local>
</jms>
<no-local>true</no-local>
<session-selector>true</session-selector>
</properties>
<channels>
<channel ref="gravityamf"/>
</channels>
<adapter ref="jms"/>
</destination>