mardi 1 décembre 2009

Advanced data features of the Tide framework

Two powerful features of the Tide framework are demonstrated in the example projects but very poorly documented (or not documented at all) : collaborative data updates and concurrent updates conflicts detection/resolution.

Collaborative data updates is the ability to transparently push updates made on an entity by a user to all other interested users. Here is how it works :

  • First tracked entities have to be listened by the DataPublishListener entity listener. If all entities need to be tracked, just put the listener on some AbstractEntity mapped superclass :
    @Entity
    @EntityListeners({DataPublishListener.class})
    public class MyEntity {
    ...
    }

  • Define a Gravity topic to push the updates :
    For example with Seam or Spring, just add this in your configuration file :
    <graniteds:messaging-destination id="myTopic" no-local="true" session-selector="true"/>

    The important thing is to specify the session selector parameter that will allow Tide to store the Gravity/JMS selector in the user client session. It's also useful to set no-local so a client does not receive its own updates. In the classic services-config.xml configuration file, just use :
    <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="myTopic">
    <properties>
    <no-local>true</no-local>
    <session-selector>true</session-selector>
    </properties>
    <channels>
    <channel ref="my-gravityamf"/>
    </channels>
    <adapter ref="simple"/>
    </destination>
    </service>
    If transactions or high scalabilty are needed it can work also with a JMS or ActiveMQ topic.

  • All server components annotated with @DataEnabled are intercepted by Tide and the annotation parameters are used to determine the push configuration (topic and selection of pushed clients).
    @DataEnabled(topic="myTopic", params=MyParams.class, publish=PublishMode.ON_SUCCESS)
    public class MyComponent {
    ...
    }

  • PublishMode.ON_SUCCESS indicates that Tide will automatically push the updates for all successful remote calls. PublishMode.ON_COMMIT is not available for now but will be implemented later to get transactional behaviour in JEE environments. The default is PublishMode.MANUAL that requires to call the push method manually with :
    DataContext.publish();

  • The params class defines which clients will receive which updates. It internally uses JMS-like selectors to route messages. Let's see the simplest possible one :
    public class ObserveAllPublishAll implements DataTopicParams {
    @Override
    public void observes(DataObserveParams params) {
    }

    @Override
    public void publishes(DataPublishParams params, Object entity) {
    }
    }
    The observes method indicates which criteria will be used to determine what the client will receive. Here we set no criteria so by default all clients will receive everything. The publishes method indicates the value of the criteria for the update of the specified entity. Here again we set nothing so every update will be pushed to everyone.
    Here is a more interesting example :
    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__");
    }
    }
    The observes method indicates that we are interested in getting all updates marked public or made by the current user (all values are appended with a OR operator to the client selector). The publishes method defines the user parameter when the entity is marked as restricted. All this means that a client will receive only public updates or updates made by himself on its own private entities. It's possible to use as many criteria parameters as you want, just ensure that observes and publishes are consistent.

  • Once all this server-side configuration is done, there is still to define a client-side data observer to receive the updates :
    Tide.getInstance().addComponent("myTopic", DataObserver);
    Tide.getInstance().addEventObserver("org.granite.tide.login", "myTopic", "subscribe");
    Tide.getInstance().addEventObserver("org.granite.tide.logout", "myTopic", "unsubscribe");
    This just registers a DataObserver component with the same name as the topic. The events are used to automatically subscribe/unsubcribe the observer on user login/logout.


  • It's a relatively complex setup but once done you don't have anything else to do to automatically get all clients transparently updated when any user modifies something.


    Now that you have this working, you will probably try to update the same entity from two different browsers and see that the result in the database is unpredictable. GraniteDS 2.1 comes with a conflict handling mechanism that should help with these cases. The main requirement is to enable optimistic locking on your JPA entities with a numeric @Version field.

    Then you just have to define a conflict handler on the global context with :
    private function init():void {
    Tide.getInstance().getContext().addEventListener(TideDataConflictsEvent.DATA_CONFLICTS, conflictsHandler);
    }

    private var _conflicts:Conflicts;

    private function conflictsHandler(event:TideDataConflictsEvent):void {
    _conflicts = event.conflicts;
    Alert.show("Someone else has modified the entity you are currently working on.\nDo you want to keep your modifications ?",
    "Modification conflict", Alert.YES | Alert.NO, null, conflictsCloseHandler);
    }

    private function conflictsCloseHandler(event:CloseEvent):void {
    if (event.detail == Alert.YES)
    _conflicts.acceptAllClient();
    else
    _conflicts.acceptAllServer();
    }


    This is relatively straighforward: a conflict event is dispatched whenever conflicts are detected during merge of the pushed updates. Then it's possible either to keep local changes with acceptAllClient, or to get latest server data with acceptAllServer.

    If you don't use data push, it's still possible to use this conflict handling by detecting server-side OptimisticLockException. All Tide service factory install by default a PersistenceExceptionConverter that understands this exception and translates it to something that can be used on the client.
    On the client, just register the corresponding exception handler with :
    Tide.getInstance().addExceptionHandler(OptimisticLockExceptionHandler);
    Then all optimistic lock errors will be considered as local conflicts and will use the previously described conflict handling mechanism. That will allow the end user to keep its change or get the last data from the server.

    Note that this API is not necessarily final so your feedback would be very welcome to improve it or add missing features.