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 :
@Entity
@EntityListeners({DataPublishListener.class})
public class MyEntity {
...
}
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"If transactions or high scalabilty are needed it can work also with a JMS or ActiveMQ topic.
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>
@DataEnabled(topic="myTopic", params=MyParams.class, publish=PublishMode.ON_SUCCESS)
public class MyComponent {
...
}
DataContext.publish();
public class ObserveAllPublishAll implements DataTopicParams {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.
@Override
public void observes(DataObserveParams params) {
}
@Override
public void publishes(DataPublishParams params, Object entity) {
}
}
Here is a more interesting example :
public class AddressBookParams implements DataTopicParams {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.
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__");
}
}
Tide.getInstance().addComponent("myTopic", DataObserver);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.
Tide.getInstance().addEventObserver("org.granite.tide.login", "myTopic", "subscribe");
Tide.getInstance().addEventObserver("org.granite.tide.logout", "myTopic", "unsubscribe");
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.
2 commentaires:
This looks very nice, this is the high level functionality we are looking for when developing applications and it's great that granite ds will offer it out of the box.
Something I can think of right away for the api when looking at the Conflict.as source, it will be able to show all entities that differ and then you can atomically take the one from the server or client. But would it be possible to get more fine grained "Property"-conflicts. So we can actually display to the user prop a & b are the same on client and server, but for prop c we have client.value and server.value, which one would you like to take.
You can in fact already get all conflicting entities and accept client or server changes individually. Conflicts simply contains an array of Conflict objects for each entity.
Property-level conflict detection is definitely interesting but quite more complex and we won't be able to do this in 2.1, more likely 2.2.
Enregistrer un commentaire