graniteds.orgCommunity Documentation
Data serialization between a client application and a Java EE server may use different kinds of transfer encodings, including XML, JSON, Java serialization, or various other serialization frameworks. GraniteDS provides an implementation of the Adobe AMF3 (ActionScript Message Format) binary encoding which is very compact, fast and efficient. Other formats may be added later but AMF3 is a really easy-to-use and performant format.
The AMF3 format allows for serialization of strongly typed objects. GraniteDS adds the concept of externalization to transform the serialized objects before and after they are serialized. This allows for example to serialize JPA entities without triggering initialization of all lazy properties.
When building a JavaFX client, you can then easily deserialize these entities to a properly JavaFX-bindable bean having the same properties. This way the client and server parts of the application are cleanly separated, the JavaFX bean does not have any dependency (even internal runtime) on the JPA provider and the JPA entity having no dependency on the JavaFX binding API.
AMF3 is a very compact binary format for data serialization/deserialization and remote method invocation. A key feature of this format is that it preserves the entire graph of your data without duplicating identical objects (contrary to JSON for example). For example, if A1 and A2 contain a reference to the same B1, the serialization of A1 and A2 does not duplicate B1. The target client VM will contain exactly the same data graph with only one B1 referenced by one A1 and one A2. Furthermore, there is no risk of infinite recursion if the data graph contains circular references. For example, if B1 contains the set of A# that references B1. AMF3 messages are sent as a part of a AMF0 envelope and body. GraniteDS implements an AMF3 serializer/deserializer and relies on some code borrowed from the OpenAMF project for AMF0 serialization/deserialization. The AMF0 and AMF3 specifications are now public. You may download them here. You will need a Macromedia or Adobe account.
The Tide remoting API is an alternative to the low-level RemoteService API that simplifies the handling of asynchronicity and brings much more features
that will be described in the next chapters.
This section describes the usage of the Tide API with a standard AMF provider. When the Tide API is used in conjunction with GraniteDS and Tide-enabled server framework adapters, there are some specificities that are described in the chapters concerning each framework integration (EJB3, Spring, CDI).
Let's see the same hello example with Tide. Note the usage of the Tide context object which reprensents the client application container.
public class HelloExample {
public static void main(String[] args) {
ContextManager contextManager = new SimpleContextManager(new DefaultPlatform());
Context context = contextManager.getContext();
ServerSession serverSession = new ServerSession("spring", "/myapp", "localhost", 8080);
context.set(serverSession);
serverSession.start();
Component helloService = new ComponentImpl(serverSession);
context.set("helloService", helloService);
// Asynchronous call using handlers
helloService.call("sayHello", "Barack", new TideResponder<String>() {
@Override
public void result(TideResultEvent<String> result) {
System.out.println("Async result: " + result.getResult());
}
@Override
public void fault(TideFaultEvent fault) {
System.err.println("Fault: " + fault.getFault());
}
};
// Synchronous wait of Future result
Future<String> futureResult = helloService.call("sayHello", "Barack");
String result = futureResult.get();
System.out.println("Sync result: " + result);
}
This is a bit different than the RemoteService API. It looks like a mostly cosmetic changes, but there are many internal things that differ.
The core of the Tide framework is the context which contains the various elements of the application. Here we create a simple ContextManager
which implements a very minimalistic built-in application container. For more demanding environment, we recommend using the SpringContextManager
which will use a Spring application container.
The Platform SPI is a simple interface that allows to integrate the Tide context with the client UI framework. For example, JavaFX requires that
all graphic operations are executed in the main UI thread. The JavaFX platform implementation will ensure that the asynchronous result handlers of remote calls
will be executed in the UI thread so you can do whatever UI operation you need using the received data.
The ServerSession encapsulates all communication between the client application and the remote services for a particular server endpoint. Note that
here it has to be "attached" manually to the Tide context with context.set(). In a Spring environment, it would just have to be declared as a Spring bean.
Finally the Component instance represents a client proxy to the actual remote service. The method call executes the remote call
and returns a Future object which can be used to get the result. It is also possible to provide a last argument to the method call
which can implement TideResponder and result, fault.
Here we use an untyped ComponentImpl implementation but it's also possible to generate typesafe client proxies from the service interfaces.
The previous example was a bit simplistic, and in more realistic applications you might want to use the client proxies from some controller class instead of the main application (!). For a more 'enterprisy' usage, we might configure a Spring container on the client application.
package com.myapp.client;
@Configuration
public class Config {
@Bean
public SpringEventBus eventBus() {
return new SpringEventBus();
}
@Bean
public SpringContextManager contextManager(SpringEventBus eventBus) {
return new SpringContextManager(new JavaFXPlatform(eventBus));
}
@Bean(initMethod="start", destroyMethod="stop")
public ServerSession serverSession() throws Exception {
return new ServerSession("spring", "/test", "localhost", 8080);
}
@Bean
public Component helloService(ServerSession serverSession) {
return new ComponentImpl(serverSession);
}
@Bean(initMethod="start")
public App app() {
return new App();
}
}
package com.myapp.client;
public class App {
public static void main(String[] args) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.scan("com.myapp.client");
applicationContext.refresh();
applicationContext.registerShutdownHook();
applicationContext.start();
}
@Inject @Qualifier("helloService")
private Component helloService;
public void start() {
helloService.call("sayHello", "Barack", new TideResponder<String>() {
@Override
public void result(TideResultEvent<String> result) {
System.out.println("Async result: " + result.getResult());
}
@Override
public void fault(TideFaultEvent fault) {
System.err.println("Fault: " + fault.getFault());
}
};
}
}
Here we use the Spring 3.1 Java configuration mechanism, but you could also do all this in XML or any other Spring configuration style.
The important things here are that we declared two components of types EventBus and ContextManager,
the ServerSession and a Component as Spring beans.
Once everything is properly wired together, you can simply inject the client proxies in whatever bean you want to execute the remote calls.
In some cases, you may need to pass some value to the result/fault handler to be able to distinguish different calls on the same method.
You can then override the default TideResponder implementation and store a token object:
public static class HelloResponder implements TideResponder {
private final String token;
public HelloResponder(String token) {
this.token = token;
}
@Override
public void result(TideResultEvent event) {
System.out.println("Result for " + token + ": " + event.getResult());
}
@Override
public void fault(TideFaultEvent event) {
System.err.println("Fault for " + token + ": " + event.getFault());
}
}
public void call() {
helloService.call("sayHello", "Barack", new HelloResponder("firstCall"));
helloService.call("sayHello", "Barack", new HelloResponder("secondCall"));
}
In this case, there will be two outputs for each token. Note that as everything is asynchronous, the order of the results is undefined.
The TideMergeResponder interface is an extension of TideResponder that makes possible to provide a return object that will be merged
with the server result. It helps working with the asynchronous nature of remoting by limiting the need for result handlers.
private List<Product> products = new ArrayList<Product>();
public function call():void {
productService.findAllProducts(new TideMergeResponder<List<Product>>() {
@Override
public void result(TideResultEvent<List<Product>> event) {
System.out.println("Result was merged: " + (event.getResult() == products));
}
@Override
public void fault(TideFaultEvent event) {
System.err.println("Fault for " + token + ": " + event.getFault());
}
@Override
public List<Product> getMergeResultWith() {
return products;
}
});
}
This may not seem very useful in this case, but when combined with a data binding mechanism such as the one in JavaFX, that means that you don't have to handle
the actual result. By using a JavaFX ObservableList The binding would transparently propagate all incoming remote data to the UI.
Note that this kind of merge will work correcly only with mutable objects (so no String, Number, ...).
It is generally the most useful with collections.
The server exceptions can be handled on the client-side by defining a fault callback on each remote call. It works fine on a case by case basis but it is very tedious and you can always forget a case, in which case the error will be either ignored or result in a global error popup that is not very elegant.
To help dealing with server exceptions, it is possible to define common handlers for particular fault codes on the client-side, and exception converters on the server-side, to convert server exceptions to common fault codes.
On the server, you have to define an ExceptionConverter class. For example we could write a converter to handle the JPA
EntityNotFoundException (in fact there is already a built-in converter for all JPA exceptions):
public class EntityNotFoundExceptionConverter implements ExceptionConverter {
public static final String ENTITY_NOT_FOUND = "Persistence.EntityNotFound";
public boolean accepts(Throwable t, Throwable finalException) {
return t.getClass().equals(javax.persistence.EntityNotFoundException.class);
}
public ServiceException convert(
Throwable t, String detail, Map<String, Object> extendedData) {
ServiceException se = new ServiceException(
ENTITY_NOT_FOUND, t.getMessage(), detail, t
);
se.getExtendedData().putAll(extendedData);
return se;
}
}
This class will intercept all EntityNotFound exceptions on the server-side, and convert it to a
proper ENTITY_NOT_FOUND fault event.
The argument finalException contains the deepest throwable in the error and can be used to check if some higher level
exception converter should be used to handle the exception. For example, the HibernateExceptionConverter checks
if the exception is wrapped in a PersistenceException, in which case it lets the JPA
PersistenceExceptionConverter accept the exception.
This exception converter has to be declared on the GDS server config :
When using scan="true" in granite-config.xml, ensure that there is a
META-INF/granite-config.properties file (even empty) in the jar containing the exception converter class
(same principle than the seam.properties file to specify which jars need to be scanned in JBoss Seam 2.x).
When not using automatic scan, you can add this in granite-config.xml :
<exception-converters> <exception-converter type="com.myapp.custom.MyExceptionConverter"/> </exception-converters>
On the client side, you then have to define an exception handler class:
public class EntityNotFoundExceptionHandler implements ExceptionHandler {
public boolean accepts(FaultMessage emsg) {
return "Persistence.EntityNotFound".equals(emsg.getCode());
}
public void handle(Context context, FaultMessage emsg, TideFaultEvent faultEvent) {
System.err.println("Entity not found: " + emsg.getMessage());
}
}
... and register it as an exception handler in the Tide context. That is simply declare it as a managed bean with context.set(new EntityNotFoundExceptionHandler())
or as a Spring bean when using Spring.
The server data objects are usually defined as JPA entities. Using them directly on the client is possible but requires having a runtime dependency
on the JPA provider on the client, which may not be practical or suitable at all. This is for example what would happen by using standard Java serialization.
Additionally, using a JPA entity on a JavaFX client (for example) means that your data beans will not benefit from all the data binding machinery of JavaFX which requires
the use of special properties implementations (javafx.beans.property.Property).
You could probably build a 'dual' Java class which is both a JPA entity and a bindable JavaFX bean but that would imply a very tight coupling between the client and the
server (and a dependency of the server application on JavaFX !!) and might at last not work at all (in particular for collection properties).
Having two different classes for the same data object on the client and the server is thus a cleaner approach and simply requires some tooling to automatically generate one from the other. GraniteDS provides a JPA/JavaBean to JavaFX class generator which handles exactly this task.
Let's say we have a basic entity bean that represents a person. The following code shows its implementation using JPA annotations:
package com.myapp.entity;
import java.io.Serializable;
import javax.persistence.Basic;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Version;
@Entity
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
@Id @GeneratedValue
private Integer id;
@Version
private Integer version;
@Basic
private String firstName;
@Basic
private String lastName;
public Integer getId() {
return id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
With GraniteDS automated externalization and without any modification made to our bean, we may serialize all properties of the Person JPA entity,
and convert them to a Person JavaFX bean. Furthermore, thanks to the Gfx code generator, we do not even have to write the JavaFX bean by ourselves.
Here is a sample generated bean implementation:
@JavaFXObject
public class PersonBase implements Identifiable, Lazyable, DataNotifier {
private boolean __initialized = true;
@SuppressWarnings("unused")
private String __detachedState = null;
private final BooleanProperty __dirty = new SimpleBooleanProperty(this, "dirty", false);
private EventHandlerManager __handlerManager = new EventHandlerManager(this);
@Override
public EventDispatchChain buildEventDispatchChain(EventDispatchChain tail) {
return tail.prepend(__handlerManager);
}
public <T extends Event> void addEventHandler(EventType<T> type, EventHandler<? super T> handler) {
__handlerManager.addEventHandler(type, handler);
}
public <T extends Event> void removeEventHandler(EventType<T> type, EventHandler<? super T> handler) {
__handlerManager.removeEventHandler(type, handler);
}
public boolean isInitialized() {
return __initialized;
}
@IgnoredMethod
public BooleanProperty dirtyProperty() {
return __dirty;
}
public boolean isDirty() {
return __dirty.get();
}
private ObjectProperty<Long> id = new SimpleObjectProperty<Long>(this, "id");
private StringProperty uid = new SimpleStringProperty(this, "uid");
private ObjectProperty<Integer> version = new SimpleObjectProperty<Integer>(this, "version");
private StringProperty firstName = new SimpleStringProperty(this, "firstName");
private StringProperty lastName = new SimpleStringProperty(this, "lastName");
public ObjectProperty<Long> idProperty() {
return id;
}
@Id
public Long getId() {
return id.get();
}
public StringProperty uidProperty() {
return uid;
}
public void setUid(String value) {
uid.set(value);
}
public String getUid() {
return uid.get();
}
public ObjectProperty<Integer> versionProperty() {
return version;
}
@Version
public Integer getVersion() {
return version.get();
}
public StringProperty firstNameProperty() {
return firstName;
}
public void setFirstName(String value) {
firstName.set(value);
}
public String getFirstName() {
return firstName();
}
public StringProperty lastNameProperty() {
return lastName;
}
public void setLastName(String value) {
lastName.set(value);
}
public String getLastName() {
return lastName.get();
}
}
This JavaFX bean reproduces all properties found in the JPA entity, public and private and even includes some extra properties and features,
(__initialized and __detachedState), that correspond the the JPA internal state for lazy loading. Note that these
two fields are present because the Gfx generator has detected that our class is a JPA entity annotated with @Entity.
For simple Java beans, these two fields would not be present, but this shows that the pluggable externalizer mechanism in GraniteDS allows to do a lot
more than simply serializing public data and value objects.
You may also notice a few more additions in the generated bean that are useful with more advanced features of the framework.
DataNotifier is a interface for bean that can dispatch events related to their internal state, that is used by the form validation framework.
dirtyProperty is a bindable property updated by the data management framework that indicates whether the bean has been modified since
its last server update.
With the externalizer mechanism in GraniteDS, serializing data between a client and a server is almost as powerful as pure Java serialization and additionally allows to maintain a clean decoupling between the client and server applications, whatever framework is used on both sides.
In order to externalize the Person.java entity bean, we must tell GraniteDS which classes we want to externalize
with a special rule in the granite-config.xml file:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE granite-config PUBLIC
"-//Granite Data Services//DTD granite-config internal//EN"
"http://www.graniteds.org/public/dtd/3.0.0/granite-config.dtd">
<granite-config>
<class-getter type="org.granite.hibernate.HibernateClassGetter"/>
<externalizers>
<externalizer type="org.granite.hibernate.HibernateExternalizer">
<include type="com.myapp.entity.Person"/>
</externalizer>
</externalizers>
</granite-config>
This instructs GraniteDS to externalize all classes named com.myapp.entity.Person by using the org.granite.hibernate.HibernateExternalizer.
Note that the HibernateClassGetter configuration is necessary to detect Hibernate proxies (lazy-initialized beans). See more about this feature in the JPA and lazy initialization section.
If you use an abstract entity bean as a parent to all your entity beans you could use this declaration, but note that type in the example above
is replaced by instance-of:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE granite-config PUBLIC
"-//Granite Data Services//DTD granite-config internal//EN"
"http://www.graniteds.org/public/dtd/3.0.0/granite-config.dtd">
<granite-config>
<class-getter type="org.granite.hibernate.HibernateClassGetter"/>
<externalizers>
<externalizer type="org.granite.hibernate.HibernateExternalizer">
<include instance-of="com.myapp.entity.AbstractEntity"/>
</externalizer>
</externalizers>
</granite-config>
This will avoid the need of writing externalization instructions for all your beans, and all instances of AbstractEntity
will be automatically externalized.
You may also use an annotated-with attribute as follows:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE granite-config PUBLIC
"-//Granite Data Services//DTD granite-config internal//EN"
"http://www.graniteds.org/public/dtd/3.0.0/granite-config.dtd">
<granite-config>
<class-getter type="org.granite.hibernate.HibernateClassGetter"/>
<externalizers>
<externalizer type="org.granite.hibernate.HibernateExternalizer">
<include annotated-with="javax.persistence.Entity"/>
<include annotated-with="javax.persistence.MappedSuperclass"/>
<include annotated-with="javax.persistence.Embeddable"/>
</externalizer>
</externalizers>
</granite-config>
Of course, you may mix these different attributes as you want.
Note, however, that there are precedence rules for these three configuration options: type has precedence over annotated-with
and annotated-with has precedence over instance-of.
Playing with rule precedence provides a way to override general rules with more specific rules for particular classes.
Instead of configuring externalizers with the above method, you may use the autoscan feature:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE granite-config PUBLIC
"-//Granite Data Services//DTD granite-config internal//EN"
"http://www.graniteds.org/public/dtd/3.0.0/granite-config.dtd">
<granite-config scan="true"/>
With this very short configuration, GraniteDS will scan at startup all classes available in the classpath,
actually all classes found in the classloader of the GraniteConfig class, and discover all externalizers
(classes that implements the GDS Externalizer interface). The matching rule are defined implicitly by each externalizer,
for example the Hibernate externalizer is defined to match all classes annotated with @Entity.
GraniteDS comes with a set of built-in externalizers for the most usual kinds of Java classes:
org.granite.messaging.amf.io.util.externalizer.DefaultExternalizer: this externalizer may be used with any POJO bean.
org.granite.messaging.amf.io.util.externalizer.EnumExternalizer: this externalizer may be used with Java enum types. When autoscan is enabled, it will be automatically used for all enum types.
org.granite.hibernate.HibernateExternalizer: This externalizer may be used with all JPA/Hibernate entities (i.e., all classes annotated with @Entity, @MappedSuperclass or @Embeddable annotations). Include granite-hibernate.jar in your classpath in order to use this feature.
org.granite.toplink.TopLinkExternalizer: this externalizer may be used with all JPA/TopLink entities (i.e., all classes annotated with @Entity, @MappedSuperclass or @Embeddable annotations). Include granite-toplink.jar in your classpath in order to use this feature.
org.granite.eclipselink.EclipseLinkExternalizer: this externalizer will be used with the new version of TopLink (renamed EclipseLink). Include granite-eclipselink.jar in your classpath in order to use this feature.
org.granite.openjpa.OpenJpaExternalizer: this externalizer may be used with all JPA/OpenJPA (formerly WebLogic Kodo) entities (mainly in WebLogic environments). Include granite-openjpa.jar in your classpath in order to use this feature.
org.granite.datanucleus.DataNucleusExternalizer: this externalizer may be used with all JPA/DataNucleus entities. Include granite-datanucleus.jar in your classpath in order to use this feature.
org.granite.tide.cdi.TideEventExternalizer: this externalizer externalizes classes annotated with the TideEvent annotation.
org.granite.messaging.amf.io.util.externalizer.LongExternalizer: externalizes Java long or Long values.
org.granite.messaging.amf.io.util.externalizer.BigIntegerExternalizer: externalizes Java BigInteger values.
org.granite.messaging.amf.io.util.externalizer.BigDecimalExternalizer: externalizes Java BigDecimal values.
GraniteDS provides support for JavaFX beans with the org.granite.client.javafx.JavaFXExternalizer externalizer.
It will unpack JavaFX properties and serialize them to a normalized network form.
It is easy to write your own externalizer, you have to implement the org.granite.messaging.amf.io.util.externalizer.Externalizer interface,
or extend the DefaultExternalizer class. There is no particular use case for this extension; it mostly depends on your specific needs
and you should look at the standard externalizer implementations to figure out how to write your custom code.
If you use autoscan configuration, make sure your class is packaged in a jar accessible via the GraniteConfig class loader
(granite.jar classpath), put a META-INF/granite-config.properties in your jar, even empty,
and put relevant code in the accept method to define which classes your externalizer should process:
public int accept(Class<?> clazz) {
return clazz.isAnnotationPresent(MySpecialAnnotation.class) ? 1 : -1;
}
You may, of course, use any kind of conditional expression, based on annotations, inheritance, etc.
The returned value is a numeric weight used when GDS tries to figure out what externalizer it should use when it encounters a
Java bean at serialization time: -1 means "do not use this externalizer", 0 or more means "use this externalizer if there is no
other externalizer that returns a superior weight for this bean". DefaultExternalizer has a weight of 0,
EnumExternalizer and the built-in JPA externalizers a weight of 1.
If your class would normally be externalized by the HibernateExternalizer, you may, for example,
use a weight of 2 when you want to replace the default serialization for some particular entities.
Creating your own externalizer generally means that you also need to write a corresponding template for the Gas3 generator with matching
implementations of readExternal and writeExternal.
Two standard annotations are available that give you more control over the externalization process:
@ExternalizedBean: This class annotation may be used to instruct GDS to externalize the annotated bean
with the DefaultExternalizer or any other externalizer specified in the type attribute.
For example, you could annotate a Java class with:
@ExternalizedBean(type=path.to.MyExternalizer.class)
public class MyExternalizedBean {
...
}
@ExternalizedProperty: This method annotation may be used on a public getter when you want to externalize a property
with no corresponding field (i.e., a computed property). For example:
public class MyBean {
private int value;
...
@ExternalizedProperty
public int getSquare() {
return value * value;
}
}
Of course, this annotation will only be used if the MyBean class is configured for externalization.
Note that externalized properties are always read only: a setSquare(...) will never be used in the client to server serialization.
Note also that Gfx uses this annotation when it generates JavaFX bean so you'll find an extra square member field
in your generated MyBean.java.
A problem with the default AMF3 serialization is to get the true class name of an object in special cases. For example, a simple
myObject.getClass().getName() with a proxied entity bean would return org.hibernate.proxy.HibernateProxy instead of
the underlying entity bean class name. In order to get through this kind of problem, you must configure a class getter.
Other methods of ClassGetter are also used by Tide to determine some internal properties of the managed objects, such as their
JPA initialization state.
Class getters are generally used in conjunction with externalizers. For example, the full configuration for an application using Hibernate entities would be (without autoscan):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE granite-config PUBLIC
"-//Granite Data Services//DTD granite-config internal//EN"
"http://www.graniteds.org/public/dtd/3.0.0/granite-config.dtd">
<granite-config>
<class-getter type="org.granite.hibernate.HibernateClassGetter"/>
<externalizers>
<externalizer type="org.granite.hibernate.HibernateExternalizer">
<include instance-of="test.granite.ejb3.entity.AbstractEntity"/>
</externalizer>
</externalizers>
</granite-config>
The org.granite.hibernate.HibernateClassGetter class is used in order to retreive the correct entity class name from a proxy.
You may write and plug your own class getter in a similar way.
At deserialization time, from client to server, GraniteDS must instantiate and populate new JavaBeans with serialized data. The population issue (strictly private field), as we have seen before, is addressed by externalizers. But there is still a problem with classes that do not declare a default constructor. How do we instantiate those classes with meaningful parameters at deserialization time?
When GraniteDS encounters classes without a default constructor, it tries to instantiate them by using the Sun JVM sun.reflect.ReflectionFactory
class that bypasses this limitation. Then, if it can successfully instantiate this kind of class, fields deserialization follows the standard process
with or without externalization. This solution has three serious limitations however: it only works with a Sun JVM, it does not take care of complex
initialization you may have put in your custom contructor, and it cannot simply work with classes that should be created via a static method, such as singletons.
With GraniteDS instantiators, you may control the instantiation process, delaying the actual instantiation of the class after all its serialized data has been read.
Two instantiators come with GDS:
org.granite.messaging.amf.io.util.instantiator.EnumInstantiator: This instantiator is used in order to get an Enum constant
value from an Enum class and value (the String representation of the constant), by means of the
java.lang.Enum.valueOf(Class<? extends Enum> enumType, String name) method.
org.granite.hibernate.HibernateProxyInstantiator: It is used when GDS needs to recreate an HibernateProxy.
See source code for details.
Note that those instantiators do not require an entry in granite-config.xml, they are respectively used by the
EnumExternalizer, HibernateExternalizer, and TopLinkExternalizer.
Let's say you have a JavaBean like this one:
package org.test;
import java.util.Map;
import java.util.HashMap;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
public class MyBean {
private final static Map<String, MyBean> beans = new HashMap<String, MyBean>();
private final String name;
private final String encodedName;
protected MyBean(String name) {
this.name = name;
try {
this.encodedName = URLEncoder.encode(name, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
public static MyBean getInstance(String name) {
MyBean bean = null;
synchronized (beans) {
bean = beans.get(name);
if (bean == null) {
bean = new MyBean(name);
beans.put(name, bean);
}
}
return bean;
}
public String getName() {
return name;
}
public String getEncodedName() {
return encodedName;
}
}
With this kind of Java class, even with the help of the GDS DefaultExternalizer and the Sun ReflectionFactory facility,
you will not be able to get the cached instance of your bean and the encodedName field will not be correctly initialized.
Instead, a new instance of MyBean would be created with a simulated default constructor and the name field would be assigned with
serialized data.
The solution is to write a custom instantiator that will be used at deserialization time:
package org.test;
import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
import org.granite.messaging.amf.io.util.instantiator.AbstractInstanciator;
public class MyBeanInstanciator extends AbstractInstanciator<MyBean> {
private static final long serialVersionUID = -1L;
private static final List<String> orderedFields;
static {
List<String> of = new ArrayList<String>(1);
of.add("name");
orderedFields = Collections.unmodifiableList(of);
}
@Override
public List<String> getOrderedFieldNames() {
return orderedFields;
}
@Override
public MyBean newInstance() {
return MyBean.getInstance((String)get("name"));
}
}
You should finally use a granite-config.xml file as follows in order to use your instantiator:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE granite-config PUBLIC
"-//Granite Data Services//DTD granite-config internal//EN"
"http://www.graniteds.org/public/dtd/3.0.0/granite-config.dtd">
<granite-config>
<externalizers>
<externalizer type="org.granite.messaging.amf.io.util.externalizer.DefaultExternalizer">
<include type="org.test.MyBean"/>
</externalizer>
</externalizers>
<instanciators>
<instanciator type="org.test.MyBean">org.test.MyBeanInstanciator</instanciator>
</instanciators>
</granite-config>
In many Java EE applications, persistence is done by using a JPA provider (such as Hibernate). The application directly persists and fetch Java entities, so this could seem natural to transfer these same objects to the client layer instead of adding a extra conversion layer with data transfer objects. However this is not as simple as it seems, in particular when using the lazy loading feature of JPA (and most applications using JPA should use lazy loading).
Usual serialization providers (AMF or not) will either throw exceptions during serialization (because the lazy loaded associations are not available at this time), or load the complete object graph and thus limit the applicability of lazy loading (when using patterns such as Open Session in View).
GraniteDS on the other hand is able to reliably serialize JPA entities with its externalizer mechanism (even detached objects outside of a JPA session)
and supports both kinds of associations: proxy (single-valued associations) and collections
(such as List, Set, Bag and Map).
As described in the previous section, it provides built-in support for Hibernate, TopLink/EclipseLink, OpenJPA and DataNucleus.
It is important to note that as a JPA detached entity can be reliably serialized between the Java client and the Java EE service, it's perfectly possible (and even recommended) to directly persist or merge entities sent from the client application without any intermediate DTO layer.
In your JPA entity bean, you may have a single-valued association like this:
@Entity
public class MyEntity {
@Id @GeneratedValue
private Integer id;
@OneToOne(fetch=FetchType.LAZY)
private MyOtherEntity other;
// Skipped code...
}
@Entity
public class MyOtherEntity {
@Id @GeneratedValue
private Integer id;
// Skipped code...
}
If you load a large collection of MyEntity and do not need other references, this kind of declaration prevents
unnecessary performance and memory usage (please refer to Hibernate documention in order to actually fetch these references when you need them).
With GDS, you can keep those uninitialized references as is.
When the client deserializes your collection of MyEntity's with lazy loaded MyOtherEntity references,
it reads a initialized flag set to true when it encounters a MyEntity instance
since MyEntity's are all initialized; so it reads all MyEntity fields including the _other one.
When it deserializes a MyOtherEntity instance referenced by a MyEntity,
it reads a initialized flag set to false since MyOtherEntity is lazy loaded,
so it only reads the MyOtherEntity id.
Informations put in __initialized, __detachedState and _id are sufficient
to restore a correct HibernateProxy instances when you give back MyEntity objects to the server for update.
GDS also provides a way to keep uninitialized collections as is. When the externalizer encounters an uninitialized collection, it does not try to serialize its content and marks it as uninitialized. This information is kept in client beans and when this bean is sent back to the server (e.g., for an update), the externalizer restores a lazy initialized collection in Java. This gives you a good control over serialization depth, as you do not face the risk of serializing the entire graph of your data, and prevents faulty updates (i.e., an empty collection is saved and deletes database data while it was only uninitialized).
For example, in this persistent set:
package com.myapp.entity;
import java.util.HashSet;
import java.util.Set;
...
import javax.persistence.CascadeType;
import javax.persistence.FetchType;
import javax.persistence.OneToMany;
@Entity
public class Person extends AbstractEntity {
...
@OneToMany(cascade=CascadeType.ALL, fetch=FetchType.LAZY, mappedBy="person")
private Set<Contact> contacts = new HashSet<Contact>();
...
public Set<Contact> getContacts() {
return contacts;
}
public void setContacts(Set<Contact> contacts) {
this.contacts = contacts;
}
}
// code for Contact skipped...
package com.myapp.entity;
...
import javafx.collections.ObservableList;
@JavaFXObject
@RemoteClass("test.granite.ejb3.entity.Person")
public class Person implements Identifiable, Lazyable, DataNotifier {
...
private ObservableList<Contact> contacts = new PersistentSet<Contact>();
...
public void setContacts(ObservableList<Contact> contacts) {
this.contacts = value;
}
public ObservableList<Contact> getContacts() {
return this.contacts;
}
// code for Contact skipped...
The actual, persistence aware, ObservableList implementation is part of a GDS JavaFX client library
(granite-javafx-client.jar) that contains all you need in order to use the lazy loaded collections feature.
If GDS encounters an uninitialized Set, it is serialized as a org.granite.messaging.persistence.ExternalizablePersistentSet
that contains some extra data indicating its intitialization state.
Other persistent collections, such as List, Bag, and Map, are handled in a similar manner.
GDS/JPA uses the interface Identifiable that requires a readable property uid for all entity beans.
See a long Hibernate discussion here about equals/hashCode/collection problems and the use of UUIDs. This is only an implementation choice and you are free
to code whatever you want, for example generate the uid from a natural identifier or from the database key.
With standard configuration (scan set to false), you must use the appropriate class getter together with the persistence externalizer
(eg. org.granite.openjpa.OpenJpaClassGetter with org.granite.openjpa.OpenJpaExternalizer).
With all persistence externalizers, provided that you have added the relevant jar to your application classpath, you may use the auto scan feature: <granite-config scan="true"> without anything else (no class getter or externalizer configuration): your entities will be automatically externalized according to the underlying JPA engine.
If you put many persistence externalizers libraries in the same application, only the one corresponding to your JPA provider will be active.
This can be useful to build portable application between Java EE servers. If you bundle both granite-hibernate.jar and granite-eclipselink.jar,
the application should work under both JBoss (which bundles Hibernate) and GlassFish 3 (which bundles EclipseLink).
Security in a Java client cannot simply rely on standard web-app security-constraints configured in web.xml.
Generally, you have only one channel-definition, equivalent to a url-pattern in web.xml,
and multiple destinations. So, the security must be destination-based rather than URL-pattern based, and Java EE standard configuration in web.xml
does not provide anything like that.
With a configured SecurityService, you will be able to use Channel's setCredentials and logout methods.
Another important feature in security is to be able to create and expose a java.security.Principal to, for example,
an EJB3 session bean backend so role-based security can be used.
At this time, GraniteDS provides security service implementations for Tomcat5+, Jetty6+, GlassFish V2+ and V3 and WebLogic 10+ servers. Because JBoss comes with Tomcat by default but may be configured to use Jetty instead, Tomcat or Jetty security services may work as well with JBoss.
When you are using Java Enterprise frameworks such as Seam or Spring together with GraniteDS, you may use specific Seam Security or Spring Security implementations instead of the previous container-based services: please refer to Seam Services or Spring Services for more information.
To enable security, you simply put this kind of declaration in your granite-config.xml file:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE granite-config PUBLIC
"-//Granite Data Services//DTD granite-config internal//EN"
"http://www.graniteds.org/public/dtd/3.0.0/granite-config.dtd">
<granite-config>
...
<security type="org.granite.messaging.service.security.TomcatSecurityService"/>
<!--
Alternatively for Tomcat 7.x
<security type="org.granite.messaging.service.security.Tomcat7SecurityService"/>
Alternatively for Jetty 6.x
<security type="org.granite.messaging.service.security.Jetty6SecurityService"/>
For Jetty 7.x/8.x (available at eclipse.org)
<security type="org.granite.messaging.service.security.Jetty7SecurityService"/>
For GlassFish 2.x
<security type="org.granite.messaging.service.security.GlassFishSecurityService"/>
For GlassFish 3.x
<security type="org.granite.messaging.service.security.GlassFishV3SecurityService"/>
For WebLogic
<security type="org.granite.messaging.service.security.WebLogicSecurityService"/>
-->
</granite-config>
Some of these implementations (currently only TomcatSecurityService) accept an optional parameter. In the case of the
Tomcat service, it's the name of the service that will be used to execute the authentication in case you have many services defined in your
server.xml.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE granite-config PUBLIC
"-//Granite Data Services//DTD granite-config internal//EN"
"http://www.graniteds.org/public/dtd/3.0.0/granite-config.dtd">
<granite-config>
...
<security type="org.granite.messaging.service.security.TomcatSecurityService">
<param name="service" value="your-tomcat-service-name-here"/>
</security>
</granite-config>
You may now use role-based security on destination in your services-config.xml file:
<?xml version="1.0" encoding="UTF-8"?>
<services-config>
<services>
<service id="granite-service"
class="flex.messaging.services.RemotingService"
messageTypes="flex.messaging.messages.RemotingMessage">
<destination id="person">
<channels>
<channel ref="my-graniteamf"/>
</channels>
<properties>
<scope>session</scope>
<source>com.myapp.PersonService</source>
</properties>
<security>
<security-constraint>
<auth-method>Custom</auth-method>
<roles>
<role>user</role>
<role>admin</role>
</roles>
</security-constraint>
</security>
</destination>
<destination id="restrictedPerson">
<channels>
<channel ref="my-graniteamf"/>
</channels>
<properties>
<scope>session</scope>
<source>com.myapp.RestrictedPersonService</source>
</properties>
<security>
<security-constraint>
<auth-method>Custom</auth-method>
<roles>
<role>admin</role>
</roles>
</security-constraint>
</security>
</destination>
</service>
</services>
...
</services-config>
Here, the person destination can be used by authenticated users with user or admin roles,
while the restrictedPerson destination can only be used by authenticated users with the admin role.
Please refer to Tomcat and JBoss documentation for setting up your users/roles configuration.
You may write and configure a specific RemoteDestinationSecurizer in order to add fine grained security checks for specific actions.
public interface RemotingDestinationSecurizer extends DestinationSecurizer {
public void canExecute(ServiceInvocationContext context)
throws SecurityServiceException;
}
You then have to tell GraniteDS where to use your securizer:
<services-config>
<services>
<service ...>
<destination id="restrictedDestination">
...
<properties>
<securizer>path.to.MyDestinationSecurizer</securizer>
</properties>
</destination>
</service>
</services>
...
</services-config>
Note that securizers, if any, are always called before the standard SecurityService.authorize() method.
At Java side, AMF deserialization instantiates classes that are referenced in the binary-encoded request coming from the client. Thus, a malicious AMF3 request can be crafted in order to instantiate an arbitrary Java class (and execute its constructor and setters) that has nothing to do with the expected data exchanged between the client application and the server application.
GraniteDS’ fix for this security issue relies on a new configurable option that you can put in your granite-config.xml file.
If you don’t configure anything, you will always see this warning at the startup of the application:
WARN [GraniteConfig] You should configure a deserializer securizer in your granite-config.xml file in order to prevent potential security exploits!
In order to secure your application, you are strongly encouraged to configure a securizer as follows:
<!DOCTYPE granite-config PUBLIC
"-//Granite Data Services//DTD granite-config internal//EN"
"http://www.graniteds.org/public/dtd/3.0.0/granite-config.dtd">
<granite-config scan="true">
<amf3-deserializer-securizer param="
org\.granite\..* |
flex\.messaging\..* |
com\.myapp\.entity\..*
"/>
...
</granite-config>
By default, the securizer uses the org.granite.messaging.amf.io.RegexAMF3DeserializerSecurizer class that,
uses a regular expression parameter. Only classes whose name match one of theses patterns are allowed to be instantiated.
Of course, all standard Java types are allowed by default and you don’t have to explicitely add their package names expressions.
If this default regex-based implementation doesn’t fit your needs, you may write your own securizer implementation.
It only has to implement the org.granite.messaging.amf.io.AMF3DeserializerSecurizer interface and can be specified in
granite-config.xml:
<!DOCTYPE granite-config PUBLIC
"-//Granite Data Services//DTD granite-config internal//EN"
"http://www.graniteds.org/public/dtd/3.0.0/granite-config.dtd">
<granite-config scan="true">
<amf3-deserializer-securizer type="com.myapp.MySecurizer"/>
...
</granite-config>