FreezeDry makes it easy to persist and serialize objects. You don't need binding files. In most cases you don't need to pollute your code with annotations. You don't need no-arg constructors. And you can persist top-level generic classes. FreezeDry supports JSON, XML, and flattened key-value pairs.
As an added bonus, FreezeDry has an object difference calculator for comparing objects and reporting back field level differences.
FreezeDry aims to be non-intrusive, flexible, and extensible. You can write and register you own classes for converting objects into a persisted form, and you get the translation to JSON, XML, and flattened key-value pairs for free.
FreezDry has set of convenient classes that makes it easy to persist objects and reconstitute them from their persisted form. These classes, derived from the Persistence interface, remove the need for you to deal directly with the PersistenceEngine. The Persistence classes allow a fair amount of flexibility, but if they don't meet your needs, you can still use the PersistenceEngine directly as before.
The current FreezeDry version provides the ability to convert between Java objects and XML, Java objects and JSON, and Java objects and flattened key-value pairs. In its most basic form, converting a Java object into a persisted form is as simple as:
final JsonPersistence persistence = new JsonPersistence();
persistence.write( division, "division.json" );
And to reconstitute the division object from the "division.json" file, all you need to do is:
final JsonPersistence persistence = new JsonPersistence();
Division redivision = persistence.read( Division.class, "division.json" );
In some cases you might find that direct use of the PersistenceEngine, although more complicated, provides more control over the transformation of objects and persisted forms into and out of the semantic model. The code below demonstrates how to persist a Java object to an XML file by directly using the PersistenceEngine and an XmlWriter:
// create the persistence engine
final PersistenceEngine engine = new PersistenceEngine();
// create the semantic model that represents the object "division"
final InfoNode rootNode = engine.createSemanticModel( division );
// write XML to the file "division.xml"
try( PrintWriter printWriter = new PrintWriter( new FileWriter( "division.xml" ) ) )
{
final XmlWriter writer = new XmlWriter();
writer.setDisplayTypeInfo( false );
writer.write( rootNode, printWriter );
}
catch( IOException e )
{
// deal with any IO exceptions
....
}
This produces an XML file like the one below:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<Division>
<people>
<Person>
<givenName>Johnny</givenName>
<familyName>Hernandez</familyName>
<age>13</age>
<birthDate>1963-04-22</birthDate>
<friends>
<MapEntry>
<Key>Polly</Key>
<Value>bird</Value>
</MapEntry>
...
</friends>
</Person>
<Person>
<givenName>Julie</givenName>
<familyName>Prosky</familyName>
<age>15</age>
</Person>
...
</people>
</Division>
Don't like the way the XML, JSON or key-value pairs are mapped to your object? Well, there are ways to alter the mapping--either through annotations, by implementing a custom node builder, or by implementing a custom reader and writer.
In the next sections we will describe FreezeDrys design.
At a high level, FreezeDry has is composed of:
FreezeDry is set up to work right out of the box for most needs. And it mostly does that. In cases where you need to customize its behavior, or where FreezeDry gets stuck, it will need your help. It really tries hard to figure out what you're trying to do, but it isn't perfect. It turns out to be easier to convert a Java object into a persisted state, such as an XML or JSON file, than converting a persisted state into a Java object. Largely this is do to the lossy nature of the persistence. For example, in JSON there isn't an elegant way to add type information to the key. And in XML, you could add an attribute type to each element. But that isn't usually considered good form. And so in both of these cases, type information is likely lost. And mostly, it is when reconstituting complex objects from persistence where FreezeDry may need help.
FreezeDry provides an easy way to persist Java objects to XML, JSON, and lists of key-value pairs. And to be useful, it also provides an easy way to reconstitute objects from their persisted form. As shown above, you can write an object to its persisted form in one simple line of code:
new XmlPersistence().write( division, "person.xml" );
and you can also reconstitute the division object from the "person.xml" file in one simple, but more complex, line of code:
Division redivision = new XmlPersistence().read( Division.class, "person.xml" );
There are currently three Persistence classes, all of which derive from the Persistence interface. There is an AbstractPersistence class which manages the PersistenceEngine, and then there is an AbstractFileBasedPersistence class which defines and implements a read and write method for file-based actions.
Class | Description |
---|---|
Persistence | The interface defining what a Persistence class must provide |
AbstractPersistence | Manages the PersistenceEngine |
AbstractFileBasedPersistence | Provides basic file-base read and write methods |
XmlPersistence | Writes Java objects to XML, and reads XML back into Java objects |
JsonPersistence | Writes Java objects to JSON, and reads JSON back into Java objects |
KeyValuePersistence | Writes Java objects to list of key-value pairs, and reads lists of key-value pairs back into Java objects |
Each Persistence class provides access to the PersistenceEngine through which you can perform modifications in the same way as previously (see Behavior Modification). In addition, each of the concrete Persistence implementation provide customization that would typically be needed for dealing with that specified persistence form.
Class | Customization |
---|---|
XmlPersistence | setDisplayTypeInfo(boolean) allows user to specify that the type info should be added to the XML elements as attributes. |
JsonPersistence | none |
KeyValuePersistence | setKeySeparator(...) and setKeyValueSeparator(...) allows the user to set the separator between the elements of the key, and between the key and the values. |
FreezeDry provides an easy way to persist Java objects to XML, JSON, and lists of key-value pairs. And to be
FreezeDry can also be used to serialize/deserialize objects as XML, JSON, or Java's basic object serialization. As a simple example, we can serialize an object to a FileOutputStream:
new JsonPersistenceSerializer().serialize( person, new FileOutputStream( filename ) );
which effectively persists the object to a file as JSON. But you could replace the FileOutputStream with an ObjectOutputStream as well. And then to deserialize an object:
new JsonPersistenceSerializer().deserialize( new FileInputStream( filename ), Person.class );
which loads the object from the file. But, the object could be deserialized from anyInputStream.
FreezeDry currently support serializion of objects in four formats:
Class | Description |
---|---|
XmlPersistenceSerializer | Serializes/Deserializes as XML |
JsonPersistenceSerializer | Serializes/Deserializes as JSON |
KeyValuePersistenceSerializer | Serializes/Deserializes as key-value pairs |
ObjectSerializer | Java's basic serialization (added for convenience) |
All of FreezeDry's serializers implement the Serializer interface by extending the PersistenceSerializer class. The PersistenceSerializer contains the common methods and implements pretty much all of the serialization/deserialization capabilities.
FreezeDry 0.2.7 added an object difference calculator. The difference calcualtor compares two objects of the same class and returns the fields that differ between the two objects. The following code snippet shows how to calculate the difference between two objects of the Division class, division1 and division2:
private Division division1;
private Division division2;
.
.
.
new ObjectDifferenceCalculator().calculateDifference( division1, division2 );
which returns a map containing the field names that differ, associated with the details of the difference.
{
Division.people[0].Person.age => Object: 13; Reference Object: 37,
Division.people[0].Person.birthDate => Object: 1963-04-22; Reference Object: 2013-12-05
}
In this case, the age and birth date of the first person in the list of people differed between the two divisions.
FreezeDry's behavior can be modified in a number ways. The four typical approaches to modifying its behavior are:
When using the FreezeDry persistence framework, you'll mostly be dealing with three classes. The PersistenceEngine, a Reader, and a Writer. The PersistenceEngine is responsible for taking a Java object, and constructing the a tree-like representation of that object called the Semantic Model. It is also responsible for converting the tree-like Semantic Model back into a Java object.
The PersistenceEngine is responsible for taking objects and their fields into and out of a Semantic Model, but to do so it needs help. Really, the PersistenceEngine doesn't, and shouldn't, know the details of how to convert a particular field into an InfoNode (and back). Rather, it should just know that it can. And the reason it can, is because it manages a set of NodeBuilder objects that do know the details. The PersistenceEngine only knows how a field (based on its class) is mapped to a specified NodeBuilder.
To create a semantic model from an object, you use the PersistenceEngine method:
final InfoNode createSemanticModel( final Object object )
to which you pass the object you wish to persist. Upon such a call, the PersistenceEngine will recurse through the object, and for each field construct an InfoNode that holds information about that field. To create the InfoNode, the PersistenceEngine selects a NodeBuilder based on the mapping between the *class* of the objects and their associated NodeBuilder. For example, suppose that during processing, the PersistenceEngine ran across a field was a List< String >. In the default case, the PersistenceEngine would look up the List in the mapping, and find that there is an entry for Collection, and recognize that a List is a Collection, and return the CollectionNodeBuilder. Then, when the PersistenceEngine ran across the String element, it would use a StringNodeBuilder.
To find out what class is mapped to what NodeBuilder object, you can use the PersistenceEngine method
final NodeBuilder getNodeBuilder( final Class< ? > clazz )
And to adjust the mapping you can use one of the following methods
NodeBuilder addNodeBuilder( final Class< ? > clazz, final NodeBuilder builder )
NodeBuilder removeNodeBuilder( final Class< ? > clazz )
To create an object from the semantic model, you use the PersistenceEngine method
Object parseSemanticModel( final Class< ? > clazz, final InfoNode rootNode )
Naturally, you may ask where the rootNode comes from. Good question. It comes from the Reader method
InfoNode read( final Class< ? > clazz, final InputStream input )
but more on that later. The key point to remember is that the by handing the PersistenceEngine a valid Semantic Model as represented by the root node of the InfoNode tree, and you also give the PersistenceEngine the *class* that the semantic model represents, you get back an object of that class type. Its as simple as that.
FreezeDry's semantic model is a tree structure that is built from InfoNode objects. So if you love recursion, then you'll love the semantic model because it's all about recursion. Powerfully simple and stunningly complex. It just depends whether you're skimming the code or trying to debug it.
The InfoNode tree is a representation of the object with information about the object and all of its data, which may be other objects. When converting from an object to the semantic model, the InfoNode object will by rich with information. The Java object has it all. However, when going from a persisted form to a semantic model to an object, the InfoNode objects may only hold the persisted name of the field, and in some cases the value. In other words, in this case, the InfoNode objects may hold only a sparse amount information.
Let's take a look at what information an InfoNode holds, aside from knowing about its parent and its children.
Field | Type | Description |
---|---|---|
nodeType | NodeType | enum: node is a ROOT, COMPOUND, or LEAF |
fieldName | String | the name of the object's field this node represents |
value | Object | the value of the field (must be a leaf node) |
persistName | String | the persisted name of the field |
clazz | Class< ? > | the class type of the field |
genericParameterTypes | List< Type > | the generic type information of the field |
As a note, when creating a semantic model from a persisted form, in many cases leaf nodes may only contain the persistName and the value. Furthermore, in many cases, the root node and compound nodes may only contain the persistName. The PersistenceEngine, in conjunction with the class, using a set of default class types for instantiation, and/or field annotations, will figure out the types that it needs to instantiate.
NodeBuilders are responsible for creating an InfoNode from an object or field, and for creating an object or setting a field within an object when given an InfoNode.
Recall that the PersistenceEngine creates the semantic model which is composed of InfoNode objects arranged in a tree-like structure. It is the NodeBuilder objects that are responsible for creating InfoNodes from an object and its fields. The PersistenceEngine holds a mapping between the class types and the NodeBuilder to use for creating an InfoNode for that class type.
Recall that the PersistenceEngine is also responsible for creating an object from a semantic model. When creating an object from the semantic model, the PersistenceEngine also uses the NodeBuilder associated with the type represented by the InfoNode, and in cases when the InfoNode doesn't hold the type information, it finds it from the class itself.
The NodeBuilder interface has
InfoNode createInfoNode( Class< ? > containingClass, Object object, String fieldName )
Object createObject( final Class< ? > clazz, final InfoNode node )
void setPersistenceEngine( final PersistenceEngine engine )
The first two methods are the methods we just mentioned. The first one creates an InfoNode from an object, providing the builder with information about the containing class and the name of the field the object represents within the containing class. The containing class information turns out to be important when dealing with structures such as
Map< String, Map< String, List< Double > >
List< Person >
The second method creates an object from an InfoNode using the class type information specified in the method. In cases where the class type is also specified in the InfoNode the default NodeBuilder implementations will use the most specified of the two specified classes. For example, it would use ArrayList if both ArrayList and List where specified.
The third method hands the NodeBuilder a PersistenceEngine. Although this may seem odd at first glance, recall that the construction of the semantic model from an object, and the construction of an object from the semantic model are done recursively. The NodeBuilder and the PersistenceEngine work hand in hand. The PersistenceEngine calls upon the NodeBuilder to create an InfoNode or an object during its recursive descent down the object structure or the semantic model. And the NodeBuilder calls back to the PersistenceEngine to create InfoNodes or objects for compound nodes. For example, suppose that during the persistence of an object, the PesistenceEngine comes across a field that is a List< Person >. In this case, it will call the CollectionNodeBuilder to create the an InfoNode representing the List. However, the CollectionNodeBuilder needs to create sub-nodes for each Person element in that list, which it doesn't know how to do. So it calls back to the PersistenceEngine to create an InfoNode for each Person it comes across. And the PersistenceEngine calls the appropriate NodeBuilder to create the InfoNode for Person (or uses default behavior to create a compound InfoNode).
The AbstractNodeBuilder implements manages the reference to the PersistenceEngine but also provides a mapping for instantiating objects for which the type is an interface or abstract. For example, suppose that your class looks like this
public class Example {
private List< Double > numbers;
...
}
When attempting to instantiate the field *numbers*, unless the InfoNode has type information that specifies that *numbers* is an ArrayList, the NodeBuilder must rely on some default mechanism to determine what class type to instantiate. The AbstractNodeBuilder provides four methods to deal with this
/**
* Adds and interface to class mapping used when constructing a collection.
* @param interfaceClass The interface
* @param concreteClass The concrete Class to use to instantiate the specified interface
*/
void addInterfaceToClassMapping( Class< ? > interfaceClass, Class< ? > concreteClass )
/**
* @return The mapping between the interfaces and the concrete classes that will be used.
*/
Map< String, String > getInterfaceToClassMapping()
/**
* Returns the concrete class for the specified interface. Or null if it doesn't exist
* @param clazz The interface for which to return the concrete class
* @return the concrete class for the specified interface. Or null if it doesn't exist
*/
Class< ? > getClassForInterface( final Class< ? > clazz )
/**
* Returns true if the interface-to-class mapping contains the specified interface;
* false otherwise
* @param clazz The interface to check
* @return true if the interface-to-class mapping contains the specified interface;
* false otherwise
*/
boolean containsInterface( final Class< ? > clazz )
FreezeDry comes with a set of NodeBuilder objects that deal with many of the basic types.
The PersistenceReader and PersistenceWriter interfaces each define one method.
public interface PersistenceWriter {
void write( final InfoNode rootNode, final java.io.Writer output );
}
The PersistenceWriter accepts the root InfoNode of the semantic model and a java.io.Writer to which to send the persisted form (such as an XML or JSON file).
public interface PersistenceReader {
InfoNode read( final Class< ? > clazz, final java.io.Reader reader );
}
The PersistenceReader accepts an java.io.Reader representing the persisted form (such as an XML or JSON file or stream), a class type that represents the class of the object to create from the persisted form, and returns the semantic model as the root InfoNode of the tree-like structure. Recall that is exactly the semantic model that the PersistenceEngine needs to parse the semantic model into an object.
FreezeDry currently provides three PersistenceReaders and three PersistenceWriters.
Name | Persisted Form | Description |
---|---|---|
XmlReader | XML | reads from XML sources and produces a semantic model |
XmlWriter | XML | writes an object to an XML persisted form |
JsonReader | JSON | reads from JSON sources and produces a semantic model |
JsonWriter | JSON | writes an object to a JSON persisted form |
KeyValueReader | Key-Values | reads lists of key-value pairs produces a semantic model |
KeyValueWriter | Key-Values | writes an object to a persisted form that is a list of key-value pairs |
Because XML and JSON are well-documented standards, I won't discuss them here. But the Key-Value readers and writers require a description. Effectively, the lists of key-value pairs are flattened versions of a Java object, and the readers and writers can handle quite complex objects straight out of the box. For example, the output below shows what the Division object (standard FreezeDry example) looks like when persisted in a key-value form.
Division.people[0].Person.givenName = "Johnny"
Division.people[0].Person.familyName = "Hernandez"
Division.people[0].Person.age = 13
Division.people[0].Person.birthDate = 1963-04-22
Division.people[0].Person.Mood[0] = 0.000
Division.people[0].Person.Mood[1] = 0.707
...
Division.people[0].Person.Mood[9] = 0.707
Division.people[0].Person.friends{"Polly"} = "bird"
Division.people[0].Person.friends{"Sparky"} = "dog"
Division.people[0].Person.groups{"numbers"}{"one"} = "ONE"
Division.people[0].Person.groups{"numbers"}{"two"} = "TWO"
Division.people[0].Person.groups{"numbers"}{"three"} = "THREE"
Division.people[0].Person.groups{"letters"}{"a"} = "AY"
Division.people[0].Person.groups{"letters"}{"b"} = "BEE"
...
Division.people[3].Person.givenName = "Booda"
Division.people[3].Person.familyName = "Ghad"
Division.people[3].Person.age = 17
Division.months{"January"}[0] = 1
Division.months{"January"}[1] = 2
Division.months{"January"}[2] = 3
Division.months{"January"}[3] = 31
...
Division.months{"March"}[0] = 1
Division.months{"March"}[1] = 2
Division.months{"March"}[2] = 3
Division.months{"March"}[3] = 31
Division.carNames[0] = "civic"
Division.carNames[1] = "tsx"
Division.carNames[2] = "accord"
Division.collectionMatrix[0][0] = 11
Division.collectionMatrix[0][1] = 12
...
Division.collectionMatrix[2][2] = 33
Division.personMap{"funny"}.givenName = "Pryor"
Division.personMap{"funny"}.familyName = "Richard"
Division.personMap{"funny"}.age = 63
Division.personMap{"sad"}.givenName = "Jones"
Division.personMap{"sad"}.familyName = "Jenny"
Division.personMap{"sad"}.age = 45
Division.personMap{"pretty"}.givenName = "Mendez"
Division.personMap{"pretty"}.familyName = "Ginder"
Division.personMap{"pretty"}.age = 23
Notice that each line has a key, followed by a separator ("="), and then followed by a value. The values in this example are simple. The keys may appear a bit complex. Notice that each key is composed of elements, which are separated by a ".".
Separator Name | Description |
---|---|
Key Separator | Separates the elements of the key |
Key-Value Separator | Separates the key from the value |
Both of these separators can be specified through code.
The elements of the key are determined by how they appear in the class. For example, notice the Division.carNames near the bottom of the key-value pair list. The Division is the class that is being persisted. And carNames is a field in that class, which happens to be a List< String >. The "[0]", "[1]", and "[2]" are the indexes of the element in the list, and are generated by a PersistenceRenderer that uses a Decorator. Similarly, at the bottom of the list of key-value pairs, you may notice Division.personMap. As the name suggests, personMap is a Map< String, Person >.
Renderers are responsible for expressing the objects they render as key-value pairs. And, at the same time, they are responsible for parsing the key-value pairs that adhere to their format, back into an object. For example, in the above text, near the bottom, notice the three key-value pairs that start with Division.carNames. The CollectionRenderer knows how to take a collection (List, Set, Queue, etc) and renderer into key-value pairs (and how to parse the key-values back into a collection). As another example, the keys beginning Division.personMap found at the bottom of the list of key-value pairs, represent a Map< String, Person > and therefore use a MapRenderer to create these key-value pairs.
Each PersistenceRenderer also provides a method isRenderer(...) that returns true if the key-string passed to it matches the format of something that it renderers. FreezeDry provides the following renderers:
Name | Description |
---|---|
PersistenceRenderer | Interface defining what a persistence renderer must have |
AbstractPersistenceRenderer | Manages the *KeyValueBuilder* and the mapping between the types and the *Decorator*s. Also provides some come utilities needed by the renderers. |
LeafNodeRenderer | Renders simple leaf nodes |
MapRenderer | Used to render Maps |
CollectionRenderer | Used for rendering Collections |
FlatteningCollectionRenderer | Renders simple Collections as a single line value, and uses the CollectionRenderer to deal with Collections of non-leaf type object (i.e. things that aren't strings or numbers) |
You may also notice that strings are surrounded by quotes, integers don't have decimals, and that doubles only show 3 decimal places. These decorations and formatting are achieved by Decorators. Decorators also provide an isDecorated(...) method that returns true if the string it is passed is decorated (matches its pattern) by that decorator. And if the string is decorated by that decorator, it can "undecorate" it as well.
FreezeDry provides the following out of the box:
Name | Description |
---|---|
Decorator | Interface defining what a Decorator is. |
StringDecorator | By default surrounds a string with quotes. The opening string and closing string can be specified. For example, an index has an opening string "[" and a closing string of "]". |
IntegerDecorator | Formats and parses integers |
DoubleDecorator | Formats and parse floating point numbers. By default has 3 digits to the right of the decimal point. The format can be adjusted. |
BooleanDecorator | Formats and parses boolean. |
In some cases you may want to change the way an object or field are persisted. For example, you may want to persist a field using a specified name instead of its field name. The code below shows a snippet of the Person class. Notice the annotation above the familyName field. This annotation tells the PersistenceEngine two things. First, when creating the semantic model, set the persistName as the specified persistence name. Depending on the implementation of the Writer interface, the familyName field would then be persisted as LastName. Second, when creating an object from the semantic model, if the semantic model has a *persistName* specified, it will find the field with the associated annotation and set that field. In this case, if the InfoNode has a persistence name of LastName, the familyName field will be set.
public class Person {
...
@Persist( persistenceName = "LastName" )
private String familyName;
...
}
"But", you may wonder, "didn't you say that often when reading a persisted form into the semantic model, only the persistence name and value are specified?" And, you may question further, "Doesn't that mean that I always have to annotate the class I want to persist?" Now you're starting to get angry, "That sucks!". Well, slow down. If the InfoNode doesn't have a fieldName specified, then it will look for a field with the specified persistence name. In this way, the default behavior is to use the persistence name as the field name. But, now, you can easily override its name.
Let me back up a bit and explain the design related to annotations. There are two types of annotations used in FreezeDry. The first type is the built-in annotation Persist. And the second type are custom annotations that are specific to a NodeBuilder.
This is used by the persistence engine, and is meant to represent common types of customizations that one would want to perform across all objects. Such as the specifying a name for the persisted field. The other customization is the class type to use to instantiate the field. As you recall, if we have a field that is of type Map< String, String >, then we may want to override the common behavior of FreezeDry that would instantiate this field as a LinkedHashMap< String, String >, and instead instantiate the field as a more simple HashMap< String, String >.
public class Person {
...
@Persist( instantiateAs = HashMap.class, persistenceName = "BuddyList" )
private Map< String, String > friends;
...
}
The Persist annotation is shown below.
@Retention( RetentionPolicy.RUNTIME )
@Target( { ElementType.TYPE, ElementType.FIELD } )
public @interface Persist {
String persistenceName() default "";
Class< ? > instantiateAs() default Null.class;
public static class Null { }
}
Notice that the retention policy states that the annotation is available at run-time, and that the targets are types and fields.
Custom annotations, the "second type", are intended to work hand-in-hand with a specific NodeBuilder. The idea is that when a NodeBuilder is creating an InfoNode from an object or field, or when the NodeBuilder is creating an object or setting a field based on an InfoNode, the annotation for that type is customizes its behavior. So the rule is that for each NodeBuilder you implement, you should create an annotation if you want to customize its behavior from the class file of the object you wish to persist or reconstitute.
The following custom annotations are already available within FreezeDry.
Annotation | NodeBuilder | Customizations |
---|---|---|
PeristCollection | CollectionNodeBuilder | elementPersistName, elementType |
PersistMap | MapNodeBuilder | entryPersistName, keyPersistName, keyType, valuePersistName, valueType |
PersistArray | ArrayNodeBuilder | elementPersistName, elementType |
PersistDate | DateNodeBuilder | value (represents the format) |
This section describes the larger changes. More detailed changes can be found in the README file in the file download area. Most updates will contain bug fixes and minor code refactoring.
Updates to the build and the use of FreezeDry, and added an object difference utility.
<dependency>
<groupId>com.closure-sys</groupId>
<artifactId>freezedry</artifactId>
<version>0.2.7</version>
</dependency>
Mainly improved the use of FreezeDry for serialization of objects that have an associated NodeBuilder.
Mainly bug fix related to inheritance, completed TODO for setting the persist names of map elements, added ability to specify that a field is ignored (i.e. not persisted), and added test cases.
Mainly provided fixes that allow FreezeDry to be use more effectively as a serialization engine. Mostly this work was done for the Diffusive project.