org.apache.juneau.transform.package.html Maven / Gradle / Ivy
Transform API
Table of Contents
-
1 - Transforms
By default, the Juneau framework can serialize and parse a wide variety of POJOs out-of-the-box.
However, two special classes are provided tailor how certain Java objects are handled by the framework.
These classes are:
-
{@link org.apache.juneau.transform.BeanFilter} - Transforms that alter the way beans are handled.
-
{@link org.apache.juneau.transform.PojoSwap} - Transforms that swap non-serializable POJOs with
serializable POJOs during serialization (and optionally vis-versa during parsing).
- {@link org.apache.juneau.transform.StringSwap} - Convenience subclass for swaps that convert
objects to strings.
- {@link org.apache.juneau.transform.MapSwap} - Convenience subclass for swaps that convert
objects to maps.
Transforms are added to serializers and parsers in a variety of ways:
-
{@link org.apache.juneau.serializer.SerializerBuilder#beanFilters(Class[])} / {@link org.apache.juneau.serializer.SerializerBuilder#pojoSwaps(Class[])} - On serializers.
-
{@link org.apache.juneau.serializer.SerializerGroupBuilder#beanFilters(Class[])} / {@link org.apache.juneau.serializer.SerializerGroupBuilder#pojoSwaps(Class[])} - On groups of serializers.
-
{@link org.apache.juneau.parser.ParserBuilder#beanFilters(Class[])} / {@link org.apache.juneau.parser.ParserBuilder#pojoSwaps(Class[])} - On parsers.
-
{@link org.apache.juneau.parser.ParserGroupBuilder#beanFilters(Class[])} / {@link org.apache.juneau.parser.ParserGroupBuilder#pojoSwaps(Class[])} - On groups of parsers.
-
{@link org.apache.juneau.rest.client.RestClientBuilder#beanFilters(Class[])} / {@link org.apache.juneau.rest.client.RestClientBuilder#pojoSwaps(Class[])} - On the serializer and parser registered on a REST client.
-
{@link org.apache.juneau.rest.annotation.RestResource#beanFilters() @RestResource.beanFilters()} / {@link org.apache.juneau.rest.annotation.RestResource#pojoSwaps() @RestResource.pojoSwaps()}- On all serializers and parsers defined on a REST servlet.
-
{@link org.apache.juneau.rest.annotation.RestMethod#beanFilters() @RestMethod.beanFilters()} / {@link org.apache.juneau.rest.annotation.RestMethod#pojoSwaps() @RestMethod.pojoSwaps()} - On all serializers and parsers defined on a method in a REST servlet.
-
{@link org.apache.juneau.rest.jaxrs.JuneauProvider#beanFilters() @JuneauProvider.beanFilters()} / {@link org.apache.juneau.rest.jaxrs.JuneauProvider#pojoSwaps() @JuneauProvider.pojoSwaps()} - On all serializers and parsers defined on a JAX-RS provider.
Swaps can also be associated with classes through the {@link org.apache.juneau.annotation.Swap @Swap} annotation.
1.1 - BeanFilter Class
Bean filters are used to tailor how Juneau handles bean classes.
They can be used for the following purposes:
-
Include or exclude which properties are exposed in beans, or the order those properties are serialized.
-
Define property-namers for customizing bean property names.
-
Define bean subtypes.
-
Define bean interface classes.
It should be noted that the {@link org.apache.juneau.annotation.Bean @Bean} annotation provides equivalent
functionality through annotations.
However, the BeanFilter
class allows you to provide the same features when you do not have
access to the source code.
Explicitly specify which properties are visible on a bean class
// Define bean filter that orders properties by "age" then "name"
public class MyBeanFilter extends BeanFilter<Person> {
public MyBeanFilter() {
setProperties("age" ,"name" );
}
}
WriterSerializer s = new JsonSerializerBuilder().beanFilters(MyBeanFilter.class ).build();
Person p = getPerson();
String json = s.serialize(p); // Prints "{age:45,name:'John Smith'}"
Note that this is equivalent to specifying the following annotation on the bean class:
@Bean (properties="age,name")
public class Person {
...
}
Exclude which properties are visible on a bean class
// Define bean filter that excludes "name"
public class MyBeanFilter extends BeanFilter<Person> {
public MyBeanFilter() {
setExcludeProperties("name" );
}
}
WriterSerializer s = new JsonSerializerBuilder().beanFilters(MyBeanFilter.class ).build();
Person p = getPerson();
String json = s.serialize(p); // Prints "{age:45}"
Note that this is equivalent to specifying the following annotation on the bean class:
@Bean (excludeProperties={"name" })
public class Person {
...
}
Define specialized property namers
// Define bean filter with our own property namer.
public class MyBeanFilter extends BeanFilter<Person> {
public MyBeanFilter() {
setPropertyNamer(UpperCasePropertyNamer.class );
}
}
// Define property namer that upper-cases the property names
public class UpperCasePropertyNamer implements PropertyNamer {
@Override
public String getPropertyName(String name) {
return name.toUpperCase();
}
}
// Serialize to JSON
WriterSerializer s = new JsonSerializerBuilder().beanFilters(MyBeanFilter.class ).build();
Person person = getPerson();
String json = s.serialize(p); // Prints "{AGE:45,NAME:'John Smith'}"
// Parse back into bean
ReaderParser p = new JsonParserBuilder().beanFilters(MyBeanFilter.class ).build();
person = p.parse(json, Person.class); // Read back into original object
Note that this is equivalent to specifying the following annotation on the bean class:
@Bean (propertyNamer=UpperCasePropertyNamer.class )
public class Person {
...
}
Limiting bean properties to parent bean classes
Occasionally, you may want to limit bean properties to some parent interface.
For example, in the RequestEchoResource
class in the sample war file, we serialize instances of
HttpServletRequest
and HttpServletResponse
.
However, we really only want to serialize the properties defined on those specific APIs, not
vendor-specific methods on the instances of those classes.
This can be done through the interfaceClass
property of a bean filter.
For example, let's define the following parent class and subclass:
// Abstract parent class
public abstract class MyClass {
public String foo ="foo" ;
}
// Subclass 1
public class MyClassBar extends MyClass {
public String bar ="bar" ;
}
Suppose we only want to render the properties defined on MyClass
, not those defined on child
classes.
To do so, we can define the following bean filter:
// Define bean filter that limits properties to only those defined on MyClass
public class MyBeanFilter extends BeanFilter<MyClass> {
public MyBeanFilter() {
setInterfaceClass(MyClass.class );
}
}
When serialized, the serialized bean will only include properties defined on the parent class.
// Serialize to JSON
WriterSerializer s = new JsonSerializerBuilder().beanFilters(MyBeanFilter.class ).build();
MyClass c = new MyClassBar();
String json = s.serialize(p); // Prints "{foo:'foo'}"
The equivalent can be done through an annotation on the parent class, which applies to all child classes:
@Bean (interfaceClass=MyClass.class )
public abstract class MyClass {
public String foo ="foo" ;
}
The annotation can also be applied on the individual child classes, like so...
@Bean (interfaceClass=MyClass.class )
public class MyClassBar extends MyClass {
public String bar ="bar" ;
}
Also, the *BeanFilters(...)
methods will automatically interpret any
non-BeanFilter
classes passed in as meaning interface classes.
So in the previous example, the BeanFilter
class could have been avoided altogether by just
passing in MyClass.class
to the serializer, like so:
// Serialize to JSON
WriterSerializer s = new JsonSerializerBuilder().beanFilters(MyClass.class ).build();
In fact, this is the shortcut used in the RequestEchoResource
sample class:
@RestResource (
beanFilters={
// Interpret these as their parent classes, not subclasses
HttpServletRequest.class , HttpSession.class , ServletContext.class
}
)
public class RequestEchoResource extends RestServletDefault {
Allowing non-public bean classes/methods/fields to be used by the framework
By default, only public classes are interpreted as beans. Non-public classes are treated as 'other' POJOs
that are typically just serialized to strings using the toString()
method.
Likewise, by default, only public fields/methods are interpreted as bean properties.
The following bean context properties can be used to allow non-public classes/methods/fields to be
used by the framework:
- {@link org.apache.juneau.BeanContext#BEAN_beanClassVisibility}
- {@link org.apache.juneau.BeanContext#BEAN_beanConstructorVisibility}
- {@link org.apache.juneau.BeanContext#BEAN_methodVisibility}
- {@link org.apache.juneau.BeanContext#BEAN_beanFieldVisibility}
Also, specifying a {@link org.apache.juneau.annotation.BeanProperty @BeanProperty} annotation on non-public
getters/setters/fields will also allow them to be detected by the framework.
public class MyBean {
// A bean property
public int f1;
// Not a bean property
@BeanIgnore
public int f2;
// A bean property
@BeanProperty
protected int f3;
// A bean property
@BeanProperty
private int getF3() {...}
}
1.2 - PojoSwap Class
{@link org.apache.juneau.transform.PojoSwap PojoSwaps} are a critical component of the Juneau architecture.
They allow the Juneau serializers and parsers to be extended to handle virtually any kind of Java object.
As explained in the overview, Juneau has built-in support for serializing and parsing specific kinds of
objects, like primitive objects, bean, maps, collections, and arrays.
Other kinds of POJOs, such as {@code Date} objects, cannot be serialized properly, since they are not true
beans.
This is where PojoSwaps
come into play.
The purpose of an PojoSwap
is to convert a non-serializable object to a serializable surrogate
form during serialization, and to optionally convert that surrogate form back into the original object
during parsing.
For example, the following swap can be used to convert {@link java.util.Date} objects to ISO8601 strings
during serialization, and {@link java.util.Date} objects from ISO8601 string during parsing:
// Sample swap for converting Dates to ISO8601 strings.
public class MyDateSwap extends StringSwap<Date> {
// ISO8601 formatter.
private DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ" );
/** Converts a Date object to an ISO8601 string. */
@Override
public String swap(BeanSession session, Date o) {
return format .format(o);
}
/** Converts an ISO8601 string to a Date object. */
@Override
public Date unswap(BeanSession session, String o, ClassMeta hint) throws ParseException {
try {
return format .parse(o);
} catch (java.text.ParseException e) {
throw new ParseException(e);
}
}
}
The swap above can then be associated with serializers and parsers as the following example shows:
// Sample bean with a Date field.
public class MyBean {
public Date date = new Date(112, 2, 3, 4, 5, 6);
}
// Create a new JSON serializer, associate our date swap with it, and serialize a sample bean.
Serializer serializer = new JsonSerializerBuilder().pojoSwaps(MyDateSwap.class ).build();
String json = serializer.serialize(new MyBean()); // == "{date:'2012-03-03T04:05:06-0500'}"
// Create a JSON parser, associate our date swap with it, and reconstruct our bean (including the date).
ReaderParser parser = new JsonParserBuilder().pojoSwaps(MyDateSwap.class ).build();
MyBean bean = parser.parse(json, MyBean.class );
int day = bean.date .getDay(); // == 3
In addition, the {@link org.apache.juneau.BeanMap#get(Object)} and
{@link org.apache.juneau.BeanMap#put(String,Object)} methods will automatically convert to swapped values
as the following example shows:
// Create a new bean context and add our swap.
BeanContext beanContext = new BeanContext().pojoSwaps(MyDateSwap.class );
// Create a new bean.
MyBean myBean = new MyBean();
// Wrap it in a bean map.
BeanMap<Bean> beanMap = beanContext.forBean(myBean);
// Use the get() method to get the date field as an ISO8601 string.
String date = (String)beanMap.get("date" ); // == "2012-03-03T04:05:06-0500"
// Use the put() method to set the date field to an ISO8601 string.
beanMap.put("date" , "2013-01-01T12:30:00-0500" ); // Set it to a new value.
// Verify that the date changed on the original bean.
int year = myBean.date .getYear(); // == 113
Another example of a PojoSwap
is one that converts byte []
arrays to BASE64-encoded strings:
public class ByteArrayBase64Swap extends StringSwap<byte []> {
@Override
public String swap(byte [] b) throws SerializeException {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
OutputStream b64os = MimeUtility.encode(baos, "base64" );
b64os.write(b);
b64os.close();
return new String(baos.toByteArray());
} catch (Exception e) {
throw new SerializeException(e);
}
}
@Override
public byte [] unswap(String s, ClassMeta<?> hint) throws ParseException {
try {
byte [] b = s.getBytes();
ByteArrayInputStream bais = new ByteArrayInputStream(b);
InputStream b64is = MimeUtility.decode(bais, "base64" );
byte [] tmp = new byte [b.length];
int n = b64is.read(tmp);
byte [] res = new byte [n];
System.arraycopy (tmp, 0, res, 0, n);
return res;
} catch (Exception e) {
throw new ParseException(e);
}
}
}
The following example shows the BASE64 swap in use:
// Create a JSON serializer and register the BASE64 encoding swap with it.
Serializer serializer = new JsonSerializerBuilder().pojoSwaps(ByteArrayBase64Swap.class ).build();
ReaderParser parser = new JsonParserBuilder().pojoSwaps(ByteArrayBase64Swap.class ).build();
byte [] a1 = {1,2,3};
String s1 = serializer.serialize(a1); // Produces "'AQID'"
a1 = parser.parse(s1, byte [].class ); // Reproduces {1,2,3}
byte [][] a2 = {{1,2,3},{4,5,6},null };
String s2 = serializer.serialize(a2); // Produces "['AQID','BAUG',null]"
a2 = parser.parse(s2, byte [][].class ); // Reproduces {{1,2,3},{4,5,6},null}
It should be noted that the sample swaps shown above have already been implemented in the
org.apache.juneau.transforms package.
The following are a list of out-of-the-box swaps:
- {@link org.apache.juneau.transforms.ByteArrayBase64Swap}
- Converts byte arrays to BASE64 encoded strings.
- {@link org.apache.juneau.transforms.CalendarSwap}
- Swaps for converting
Calendar
objects to various date format strings.
- {@link org.apache.juneau.transforms.DateSwap}
- Swaps for converting
Date
objects to various date format strings.
- {@link org.apache.juneau.transforms.EnumerationSwap}
- Swaps for converting
Enumeration
objects to arrays.
- {@link org.apache.juneau.transforms.IteratorSwap}
- Swaps for converting
Iterator
objects to arrays.
- {@link org.apache.juneau.transforms.ReaderSwap}
- Swaps for converting
Readers
to objects before serialization.
- {@link org.apache.juneau.transforms.XMLGregorianCalendarSwap}
- Swaps for converting
XMLGregorianCalendar
objects to ISO8601 strings.
Valid swapped class types
The swapped class type can be any serializable class type as defined in the
POJO categories table.
1.3 - One-Way PojoSwaps
In the previous section, we defined two-way swaps, meaning swaps where the original objects could be
reconstructing during parsing.
However, there are certain kinds of POJOs that we may want to support for serializing, but that are not
possible to reconstruct during parsing.
For these, we can use one-way object swaps.
A one-way POJO swap is simply an object transform that only implements the {@code swap()} method.
The {@code unswap()} method is simply left unimplemented.
An example of a one-way swaps would be one that allows {@code Iterators} to be serialized as JSON arrays.
It can make sense to be able to render {@code Iterators} as arrays, but in general it's not possible to
reconstruct an {@code Iterator} during parsing.
public class IteratorSwap extends PojoSwap<Iterator,List> {
@Override
public List swap(Iterator o) {
List l = new LinkedList();
while (o.hasNext())
l.add(o.next());
return l;
}
}
Here is an example of our one-way swap being used.
Note that trying to parse the original object will cause a {@link org.apache.juneau.parser.ParseException}
to be thrown.
// Create a JSON serializer that can serialize Iterators.
Serializer serializer = new JsonSerializerBuilder().pojoSwaps(IteratorSwap.class ).build();
// Construct an iterator we want to serialize.
Iterator iterator = new ObjectList(1,2,3).iterator();
// Serialize our Iterator
String s = serializer.serialize(iterator); // Produces "[1,2,3]"
// Try to parse it.
ReaderParser parser = new JsonParserBuilder().pojoSwaps(IteratorSwap.class ).build();
iterator = parser.parse(s, Iterator.class ); // Throws ParseException!!!
1.4 - Stop Classes
Occasionally, you may want to limit bean properties to only those defined on a parent class or interface.
There are a couple of ways of doing this.
For example, let's define the following parent class and subclass:
// Abstract parent class
public abstract class MyClass {
public String foo ="foo" ;
}
// Subclass 1
public class MyClassBar extends MyClass {
public String bar ="bar" ;
}
Suppose we only want to render the properties defined on MyClass
, not those defined on
child classes.
To do so, we can define the following bean filter:
// Define transform that limits properties to only those defined on MyClass
public class MyBeanFilter extends BeanFilter<MyClass> {
public MyBeanFilter() {
setInterfaceClass(MyClass.class );
}
}
When serialized, the serialized bean will only include properties defined on the parent class.
// Serialize to JSON
WriterSerializer s = new JsonSerializerBuilder().beanFilters(MyBeanFilter.class ).build();
MyClass c = new MyClassBar();
String json = s.serialize(p); // Prints "{foo:'foo'}"
The equivalent can be done through an annotation on the parent class, which applies to all child classes:
@Bean (interfaceClass=MyClass.class )
public abstract class MyClass {
public String foo ="foo" ;
}
The annotation can also be applied on the individual child classes, like so...
@Bean (interfaceClass=MyClass.class )
public class MyClassBar extends MyClass {
public String bar ="bar" ;
}
Also, the beanFilters()
methods will automatically interpret any non-BeanFilter
classes passed in as meaning interface classes.
So in the previous example, the BeanFilter
class could have been avoided altogether by just
passing in MyClass.class
to the serializer, like so:
// Serialize to JSON
WriterSerializer s = new JsonSerializerBuilder().beanFilters(MyClass.class ).build();
1.5 - Surrogate Classes
Surrogate classes are very similar in concept to one-way PojoSwaps
except they represent a
simpler syntax.
For example, let's say we want to be able to serialize the following class, but it's not serializable for
some reason (for example, there are no properties exposed):
public class MyNonSerializableClass {
protected String foo ;
}
This could be solved with the following PojoSwap
.
public class MySerializableSurrogate {
public String foo ;
}
public class MySwap extends PojoSwap<MyNonSerializableClass,MySerializableSurrogate> {
@Override
public MySerializableSurrogate swap(MyNonSerializableClass o) {
MySerializableSurrogate s = new MySerializableSurrogate();
s.foo = o.foo ;
return s;
}
}
However, the same can be accomplished by using a surrogate class that simply contains a constructor with
the non-serializable class as an argument:
public class MySerializableSurrogate {
public String foo ;
public MySerializableSurrogate(MyNonSerializableClass c) {
this .foo = c.foo ;
}
}
The surrogate class is registered in the same way as a PojoSwap
:
// Create a JSON serializer that can serialize Iterators.
Serializer serializer = new JsonSerializerBuilder().pojoSwaps(MySerializableSurrogate.class ).build();
When the serializer encounters the non-serializable class, it will serialize an instance of the surrogate
instead.
1.6 - Serializing to ObjectMaps
A shortcut method for transforming is provided that can often be simpler than defining transforms.
In this case, we add methods to our class to serialize to {@link org.apache.juneau.ObjectMap ObjectMaps}
public class MyClass {
private String f1 ;
// Constructor that takes in an ObjectMap
public MyClass(ObjectMap m) {
f1 = m.getString("f1" );
}
// Method that converts object to an ObjectMap
public ObjectMap toObjectMap() {
return new ObjectMap().append("f1" , f1 );
}
The toObjectMap()
method will automatically be used during serialization, and
the constructor will automatically be used during parsing.
This will work for all serializers and parsers.