Okapi Java Persistence API
Contents
Introduction
This article describes basic concepts behind the Okapi Persistence Beans API (OPB API).
OPB provides an API for storing objects in various serialization formats like JSON, XML, YAML, binary streams, relational or object databases, and many others.
The serialized objects are deserialized incrementally from the storage.
The API guarantees that references between the objects are preserved upon serialization, and restored after deserialization.
OPB is optimized for speed and memory usage.
In this description we use Okapi core classes to illustrate the concepts, but OPB is not dependent on the core of Okapi Framework, and can be used with any other framework or set of classes, providing them with a persistence layer.
API Parts
The OPB consists of common modules, which provide common functionality, and Okapi-specific modules.
Common Modules
The persistence framework classes are located in the okapi-lib-persistence
project of the Okapi Maven build. The project contains the following packages:
net.sf.okapi.lib.persistence
net.sf.okapi.lib.persistence.beans
net.sf.okapi.persistence.json.jackson
net.sf.okapi.persistence.xml.java.beans
net.sf.okapi.persistence.xml.java.xstream
Okapi-Specific Modules
Okapi-specific bean classes are located in the okapi-lib-beans
project of the Okapi Maven build. The project contains the following packages:
net.sf.okapi.lib.beans.sessions
net.sf.okapi.lib.beans.v0
net.sf.okapi.lib.beans.v1
Usage examples
The persistence beans are used in the okapi-step-xliffkit
project of the Okapi Maven build in the following packages:
net.sf.okapi.steps.xliffkit.writer.XLIFFKitWriterTest
net.sf.okapi.steps.xliffkit.reader.XLIFFKitReaderTest
The Concepts
The conceptual parts of OPB will be described below:
- bean
- session
- reference
- frame
- anti-bean
- factory bean
- proxy bean
- version
- beans mapper
Beans
A bean in general is a Java class with a no-arguments (nullary) constructor, private properties, and public getters and setters for accessing the fields. An example of a bean:
public class TestBean { private int data; private boolean ready; public TestBean() { // No-arguments constructor super(); data = -1; ready = false; } public int getData() { // Getter for the private data field return data; } public void setData(int data) { // Setter for the private data field this.data = data; } public boolean isReady() { // Getter for the private ready field return ready; } public void setReady(boolean ready) { // Setter for the private ready field this.ready = ready; } }
Beans are in the core of OPB, the bridge between a persistence framework and non-bean core classes. The only action required of a developer who wants to persist his/her classes with OPB is to create OPB-compliant beans and register them with OPB.
The API provides mechanisms for creation of beans and registration of them within persistence sessions. Also the API controls versions of the beans, providing backwards compatibility with older beans.
External serialization/persistence frameworks, like JSON Jackson or Hibernate, access the beans, and the OPB handles synchronization of the beans and their corresponding core classes.
The beans constitute an intermediate layer between core classes and persistence frameworks, imposing no constraints on the core classes' design.
The OPB classes access the beans by getters and setters; Java reflection is not used.
Why Beans?
- Beans are known to most widely-used persistence frameworks.
- Beans are supported by Java IDEs, which create getters and setters for the developer.
- Beans are persistence-framework-independent; the same bean can be accessed by different frameworks.
- Beans allow adding persistence to existing, well-tested, non-bean classes without the need to modify them to comply with a framework's expectations for method signatures, etc.
- Beans allow adding persistence to Java core classes, and to third-party libraries whose code is not easily modified.
- Beans allow controlling which parts of the core class’s state should be persisted, and assigning proper names quickly.
- Beans are faster than reflection-based alternatives.
Bean, Step-by-Step
All beans in OPB are subclasses of the net.sf.okapi.persistence.PersistenceBean
class.
PersistenceBean
declares 3 abstract methods a bean-writer is expected to implement. Actually those methods bind the bean with the related core class.
Let's create a bean for the TestClass
core class:
public class TestClass { private int data; public boolean ready; public TestClass(int data) { this.data = data; ready = true; } public int getData() { return data; } }
- In Eclipse choose New/Class. Type in the bean's name. It's a good practice to take the core class name and add the
Bean
suffix to it. For theTestClass
the bean name would beTestClassBean
:
- To choose the superclass, press the Browse button next to the Superclass box, start typing "
PersistenceBean
", choosenet.sf.okapi.persistence.PersistenceBean
, hit OK, then Finish.
Eclipse will create a new class:
package net.sf.okapi.persistence.wiki; import net.sf.okapi.persistence.PersistenceBean; public class TestClassBean extends PersistenceBean<PutCoreClassHere> { }
Some text is underlined with a red wave: TestClassBean
and PutCoreClassHere
.
- Select
PutCoreClassHere
and replace it with the name of the core class:TestClass
.
- Point your mouse at
TestClassBean
and choose *Add unimplemented methods*.
Eclipse will create stubs for the 3 abstract methods:
public class TestClassBean extends PersistenceBean<TestClass> { @Override protected TestClass createObject(IPersistenceSession session) { // TODO Auto-generated method stub return null; } @Override protected void fromObject(TestClass obj, IPersistenceSession session) { // TODO Auto-generated method stub } @Override protected void setObject(TestClass obj, IPersistenceSession session) { // TODO Auto-generated method stub } }
- Add the fields to be persisted. You can copy them from the core class and give them different names if so needed. The fields should be private:
public class TestClassBean extends PersistenceBean<TestClass> { private int data; private boolean ready; @Override protected TestClass createObject(IPersistenceSession session) { ...
- Add the following code to bind the bean with the core class:
public class TestClassBean extends PersistenceBean<TestClass> { private int data; private boolean ready; @Override protected TestClass createObject(IPersistenceSession session) { return new TestClass(data); } @Override protected void fromObject(TestClass obj, IPersistenceSession session) { data = obj.getData(); ready = obj.ready; } @Override protected void setObject(TestClass obj, IPersistenceSession session) { obj.ready = ready; } }
- Right-click inside the class and choose Source/Generate Getters and Setters...:
- Press Select All, then Ok.
public class TestClassBean extends PersistenceBean<TestClass> { private int data; private boolean ready; @Override protected TestClass createObject(IPersistenceSession session) { return new TestClass(data); } @Override protected void fromObject(TestClass obj, IPersistenceSession session) { data = obj.getData(); ready = obj.ready; } @Override protected void setObject(TestClass obj, IPersistenceSession session) { obj.ready = ready; } public int getData() { return data; } public void setData(int data) { this.data = data; } public boolean isReady() { return ready; } public void setReady(boolean ready) { this.ready = ready; } }
Complex Beans
Very often core objects contain object fields. The developer is expected to provide beans for all internal classes as well as for the core classes that contain them.
An OPB bean can contain ONLY the fields that are:
- simple types (int, String, boolean, enum, ...)
- other OPB beans (subclasses of
PersistenceBean
) - container types (arrays, maps, lists, sets, ...) which elements are of a simple type or OPB beans
A good example would be the AltTranslationBean
bean:
public class AltTranslationBean extends PersistenceBean<AltTranslation> { private String srcLocId; private String trgLocId; private TextUnitBean tu = new TextUnitBean(); private MatchType type; private int score; private String origin; ...
It handles persistence of the AltTranslation
class:
public class AltTranslation implements Comparable<AltTranslation> { LocaleId srcLocId; LocaleId trgLocId; ITextUnit tu; MatchType type; int score; String origin; ...
As you can see, origin
, score
, and the enum type
are the same in the bean and the core class. But srcLocId
and trgLocId
are stored as String, and not LocaleId
. The bean code handles conversion between LocaleId
and String. (Please note that there is also the LocaleIdBean
that can be alternatively used for serialization of LocaleId
.)
The ITextUnit
type requires a bean. It is a good practice to instantiate a bean after its declaration.
private TextUnitBean tu = new TextUnitBean(); private List<TextPartBean> parts = new ArrayList<TextPartBean>();
Wildcard Beans
The core classes might contain fields whose actual type is unknown at compile-time. Consider IResource resource
in the Event
class. At run-time it's initialized with an actual object implementing IResource
, and definitely we want to store all fields of that object.
OPB defines the 2 "wildcard" beans in net.sf.okapi.persistence.beans
:
TypeInfoBean
- stores the class name of the actual object, and during deserialization tries to instantiate that object, presuming the constructor is no-arguments (nullary). OPB falls back to this bean if it finds no registered bean for an actual object.
When there's no mapping registered for a core class, OPB tries for every registered bean to find the closest superclass mapped to the bean, all the way up to the Java Object
base class, which is mapped to the TypeInfoBean
.
FactoryBean
- also stores the class name of the actual object, but then finds a bean class for the actual object, creates the bean, initializes it with the actual object, and stores the created bean in itscontent
field, so theFactoryBean
is a wrapper for the actual object's newly created bean.
In some cases FactoryBean
doesn't store the object itself, but only a reference to it. See the References section for details.
In other words, a FactoryBean
field in a bean accepts a reference to any any other bean. If a field in a core class can be assigned with another core object, use a FactoryBean
field in its bean. FactoryBean
should always be used for object references.
For a core class, if an object field is not always assigned with an object instantiated inside that class, but possibly somewhere outside, use a FactoryBean
field in the bean.
Sessions
The beans are written and read with sessions. Every serialization format requires a separate session that supports that given format.
Furthermore, every set of core classes requires a separate session derived from the base session that handles its serialization format.
The session takes care of versions, name spaces, and mappings of beans to core classes. A session can be in one of 3 states: reading, writing, and idle.
There's a base session class net.sf.okapi.persistence.PersistenceSession
, and all serialization-format-specific sessions should be derived from that class. For instance, JSON Jackson session (net.sf.okapi.persistence.json.jackson.JSONPersistenceSession
) is a subclass of PersistenceSession
. It employs persistence features of the Jackson framework to write and read a JSON stream. But the Okapi framework defines a session derived from the base net.sf.okapi.persistence.json.jackson.JSONPersistenceSession
in net.sf.okapi.steps.xliffkit.common.persistence.sessions.OkapiJsonSession
that handles beans for Okapi core classes.
PersistenceSession
declares abstract methods that a developer who wants to introduce a new serialization format is expected to implement. Those methods are:
Method | Method description | Parameters | Parameters description |
startWriting |
initializes the session for writing | OutputStream outStream |
the output stream to write to |
writeBean |
writes a given bean to the output stream | IPersistenceBean bean, String name |
the bean is labeled with the name (if the current format supports labels) |
endWriting |
finishing writing | OutputStream outStream |
the stream writing was performed to |
startReading |
initializes the session for reading | InputStream inStream |
the input stream to read from |
readBean |
reads the next bean from the input stream | Class beanClass, String name |
the expected class of the bean and expected label |
endReading |
finishes reading | InputStream inStream |
the stream reading was performed from |
getMimeType |
returns the MIME type of the session format (if applicable) | ||
convert |
converts a given object to the expected class | Object obj, Class expectedClass |
the object to be converted to the expected class |
Session, Step-by-Step
All sessions in OPB are subclasses of the net.sf.okapi.persistence.PersistenceSession
class.
If you want to implement a new serialization format for your classes, create a new session class. You would also require a new session class if you want to use a different persistence framework for the same serialization format. For instance, JSON persistence with Jackson and flexjson will require 2 separate session classes.
- In Eclipse choose New/Class and type in a name for the new session class, say XMLPersistenceSession.
- To choose the superclass, press the Browse button next to the Superclass box, start typing "
PersistenceSession
", choosenet.sf.okapi.persistence.PersistenceSession
, hit OK, then Finish.
Eclipse will create a new class:
public class XMLPersistenceSession extends PersistenceSession { @Override protected void endReading(InputStream inStream) { // TODO Auto-generated method stub } @Override protected void endWriting(OutputStream outStream) { // TODO Auto-generated method stub } @Override protected Class<?> getDefItemClass() { // TODO Auto-generated method stub return null; } @Override protected String getDefItemLabel() { // TODO Auto-generated method stub return null; } @Override protected String getDefVersionId() { // TODO Auto-generated method stub return null; } @Override protected <T extends IPersistenceBean<?>> T readBean(Class<T> beanClass, String name) { // TODO Auto-generated method stub return null; } @Override protected void startReading(InputStream inStream) { // TODO Auto-generated method stub } @Override protected void startWriting(OutputStream outStream) { // TODO Auto-generated method stub } @Override protected void writeBean(IPersistenceBean<?> bean, String name) { // TODO Auto-generated method stub } @Override public <T extends IPersistenceBean<?>> T convert(Object obj, Class<T> expectedClass) { // TODO Auto-generated method stub return null; } @Override public String getMimeType() { // TODO Auto-generated method stub return null; } @Override public void registerVersions() { // TODO Auto-generated method stub } }
If you are planning to subclass the session later, remove some of the methods and make the class abstract. We will remove the following method stubs:
Method | Description |
getDefItemClass |
returns the core object class that will be persisted with the session |
getDefItemLabel |
returns a prefix for the label given to written objects (item1, item2, ... by default) |
getDefVersionId |
returns a default version Id (described below) |
registerVersions |
callback method of the version control (described below) |
We will end up with this code:
public abstract class XMLPersistenceSession extends PersistenceSession { @Override protected void endReading(InputStream inStream) { // TODO Auto-generated method stub } @Override protected void endWriting(OutputStream outStream) { // TODO Auto-generated method stub } @Override protected <T extends IPersistenceBean<?>> T readBean(Class<T> beanClass, String name) { // TODO Auto-generated method stub return null; } @Override protected void startReading(InputStream inStream) { // TODO Auto-generated method stub } @Override protected void startWriting(OutputStream outStream) { // TODO Auto-generated method stub } @Override protected void writeBean(IPersistenceBean<?> bean, String name) { // TODO Auto-generated method stub } @Override public <T extends IPersistenceBean<?>> T convert(Object obj, Class<T> expectedClass) { // TODO Auto-generated method stub return null; } @Override public String getMimeType() { // TODO Auto-generated method stub return null; } }
JSON Example
Let's take a look at how the JSON persistence session is implemented.
net.sf.okapi.lib.persistence.json.jackson.JSONPersistenceSession
is an abstract class derived from net.sf.okapi.lib.persistence.PersistenceSession
.
It declares private fields that store references to Jackson framework objects like ObjectMapper, JsonFactory, JsonParser, JsonGenerator
, and initializes those objects in the session constructor.
- The
convert
method invokes the Jacksonmapper.convertValue()
to convert a given object to an expected type. readBean
reads an object by means of a Jackson parser controlling the object label.writeBean
writes an object with Jackson.getMimeType
returns "application/json".readFieldValue
is a private helper.startReading
configures the Jackson framework for reading, and reads the header from the input JSON stream.endReading
finalizes the Jackson parsing task.startWriting
instantiates a Jackson generator, and creates a temporary stream for the JSON output body part.endWriting
finalizes the Jackson generator to flush the output buffers to the temporary body stream, and then copies the header and the body parts to the output JSON stream.
The implementation is so cumbersome because the header, containing reference frames (described below), is built in the process of serialization.
The net.sf.okapi.lib.beans.sessions.OkapiJsonSession
class binds Okapi core classes with JSONPersistenceSession
. Here we implement the methods not implemented in the abstract superclass.
registerVersions
registers versions of Okapi-specific beans.getDefItemClass
returns the Okapi Event class as the default serialization class. The information about the item class is stored in the header, and doesn't prevent you from storing instances of different classes within the same session. You just specify the default class the session is handling. In Okapi the session stores events.getDefItemLabel
returns a prefix for the labels that OPB will automatically assign to stored objects. If this method returns an empty string or null, the API uses the default "item" prefix.getDefVersionId
returns Id of the version that should be used in serialization. Deserialization controls versions based on the info read from the JSON header, or in whatever else way applicable for a given serialization format.
To summarize, we have the base class PersistenceSession
for any serialization format, then we have its subclass JSONPersistenceSession
that handles JSON Jackson format, and we have its next level subclass OkapiJsonSession
that handles Okapi serialization to JSON.
An example of a parsed JSON file created with the OkapiJsonSession
:
The same as raw JSON text:
Versions
The set of core classes might change with the time. New core classes can be added to the core framework, or some classes may be deleted in later versions. The developer might want to create new beans for those new classes, or change or remove existing beans. All these situations would make old serialized beans unreadable.
OPB provides version control that allows one to read older beans and instantiate new core classes from older beans, thus providing backwards compatibility. Also if you have several versions, you can choose with which of them you want to serialize your objects.
A version is a set of beans plus a version driver.
It is advisable to place all beans in a separate package, and not in the packages of core classes.
As a general rule, don't change the interface of the beans that have been released to the user. You should change their implementation if you change core classes, but if you need to add/remove getters or setters, change arguments, add new beans, or remove some of the beans, you should create a new version with the modified beans. Again, this rule applies only to released versions. Once a version is released, its set of beans, their public methods, and the method signatures should remain intact.
Though you are absolutely free to change insides of the beans, when you change your core classes your beans will most likely start producing compile errors, because they have been written for old core classes. Update the beans' code (normally just slight changes) in the packages of all versions to reflect the change in core classes. If you do this, older versions will be read correctly, and all beans will be backwards compatible.
Version, Step-by-Step
- Create a new package for the new version. As of M23, Okapi provides 2 registered beans versions in the packages of okapi-lib-beans project:
net.sf.okapi.lib.beans.v0
-- Version 0.0 (demo purposes), and net.sf.okapi.lib.beans.v1
-- Version 1.0 of JSON persistence.
- Create your bean classes in the new package. If you want to modify beans from previous versions, copy them to the new package.
- Create a version driver. The version driver is a class that defines a unique version Id, registers beans of the version, and does other things to provide backwards compatibility.
In Eclipse choose New/Class. Type in a good name for the version driver, for instance, "OkapiBeansVersion1
".
- Press the Add button next to the Interfaces box, select the
IVersionDriver
:
- Hit OK, then Finish. Eclipse will create a version driver stub:
public class OkapiBeansVersion1 implements IVersionDriver { @Override public String getVersionId() { // TODO Auto-generated method stub return null; } @Override public void registerBeans(BeanMapper beanMapper) { // TODO Auto-generated method stub } }
- Add a public static Id for the version. It shouldn't be just "1.0", but something unique. It is a good idea to include the framework name in the version Id, like in "OKAPI 1.0":
public class OkapiBeansVersion1 implements IVersionDriver { public static final String VERSION = "OKAPI 1.0"; @Override public String getVersionId() { // TODO Auto-generated method stub return null; } @Override public void registerBeans(BeanMapper beanMapper) { // TODO Auto-generated method stub } }
- Implement
getVersionId
. Simply
@Override public String getVersionId() { return VERSION; }
- Register beans of this version, and beans of previous versions if used in the new version too:
@Override public void registerBeans(BeanMapper beanMapper) { beanMapper.registerBean(Event.class, EventBean.class); beanMapper.registerBean(TextUnit.class, TextUnitBean.class); beanMapper.registerBean(RawDocument.class, RawDocumentBean.class); beanMapper.registerBean(Property.class, PropertyBean.class); }
You register your beans by calling registerBean
of the bean mapper passed to the method with its arguments. Actual parameters will be the core class and the bean class. If you have something like this,
beanMapper.registerBean(Event.class, net.sf.okapi.lib.beans.v1.EventBean.class); beanMapper.registerBean(Event.class, net.sf.okapi.lib.beans.v2.EventBean.class); beanMapper.registerBean(Event.class, net.sf.okapi.lib.beans.v3.EventBean.class);
then net.sf.okapi.lib.beans.v3.EventBean.class
will be mapped to Event.class
, and all previous mappings will be overridden. You can subclass a version driver to register all beans of a previous version with a single call of super.registerBeans(beanMapper)
, and then add your version-specific registerBean
calls for the beans unique for the new version.
Every bean should be registered with the session that will use it. The session establishes an association between a core object and its bean. Registration is necessary for the API to be able to automatically store a given object with the right bean.
The API provides the BeanMapper
class, which a session instantiates internally to handle bean mapping. The session calls of registerBean
are delegated to the internal BeanMapper
.
- Optionally (least likely for new versions), you can assign mapping of outdated version Ids and class names with the two helper static classes:
VersionMapper
andNamespaceMapper
. The first one is used to map old version Ids to new ones. The second one is helpful to resolve changed class names:
VersionMapper.mapVersionId("1.0", VERSION); NamespaceMapper.mapName("net.sf.okapi.steps.xliffkit.common.persistence.versioning.TestEvent", net.sf.okapi.lib.beans.v0.TestEvent.class);
Version Registration
The session is responsible for registration of all versions it will support. You won't need to write a new session class for new versions of beans. You just implement the abstract method PersistenceSession.registerVersions
.
In net.sf.okapi.lib.beans.sessions.OkapiJsonSession
the version drivers for 2 versions of Okapi beans are called PersistenceMapper
and OkapiBeans
:
@Override public void registerVersions() { VersionMapper.registerVersion(PersistenceMapper.class); // v0 VersionMapper.registerVersion(OkapiBeans.class); // v1 }
References
OPB provides persistence of references inside an object, and between objects. The reference persistence mechanism employs the following concepts:
- reference Id
- root object
- internal reference
- external reference
- frame
- anti-bean
- proxy bean
First we'll define them, then explain how they do the job.
Reference Id
Every bean subclassed from PersistenceBean
has a unique long refId
field, and the reference
field of FactoryBean
uses those refIds to link beans.
Root Object
The root object is the object for which the method session#serialize()
is called.
In OkapiJsonSession
the root object is an event. OPB finds a bean for the class Event, and stores all its internal object fields (like IResource resource
) in internal beans of the EventBean
representing the root object.
Internal Reference
Internal references are references inside a root object (i.e. references between the root object's object fields and object fields of those internal objects). Some object fields can contain references to the root object itself.
External reference
External references are references from object fields of one root object to object fields of another root object.
Frame
Frame is a set of root objects linked with references. The frames are deserialized as a whole for OPB to be able to restore references between the objects in the frame.
Frames are stored by the session as a set of refIds of root objects. JSONPersistenceSession
stores the information about frames in the header. You can see the frames in the screenshots in the JSON example session.
Frames, internal and external references are detected automatically by OPB, there's no need for the developer to specify frames.
If an object contains only internal references, no frame is created for it. In other words, frames always contain refIds of two or more root objects.
Anti-Bean
Anti-bean is a "virtual" bean representing a "physical" bean stored as a bean field of another bean.
To put it differently, an anti-bean is a reference to a bean, not the bean itself.
Anti-bean has a negative refId, hence the strange name. The anti-bean’s refId is the negated refId of the bean it references.
Anti-beans are created for root objects only, when the root object was referenced from another bean, and that bean has already been serialized. Anti-beans are introduced so as not to store the already-stored bean several times, thus braking references.
Proxy Bean
Proxy beans are used during deserialization to resolve object references. OPB creates one proxy bean for every registered bean class and keeps them in a map.
A proxy bean is asked to create a core object when there's an external reference to an object handled by another bean, and that other bean has not yet created its object.
The proxy bean knows how to instantiate that object's class; it creates the object, sets the reference to it, and puts the created referenced object in an object cache. When that second bean is ready to create its object and set its fields, it finds the already-created object in the cache, and just sets the fields of the found object—i.e. the one the first object has created and now refers to.
Deprecation of Reference Bean
Version 1 of OPB contained a ReferenceBean
class, which has been deprecated after M23, and is not intended for use in later versions. The reason for deprecation is the fact that the functionality of the bean is already provided by the FactoryBean
class.
Self Bean
Sometimes you will need to create a new core class that contains no object fields, only simple type fields:
- simple types (int, String, boolean, enum, ...)
- container types (arrays, maps, lists, sets, ...) whose elements are of a simple type
A good example of such a class would be a new annotation that contains various meta-data you would like to serialize along with Okapi resources.
There is no need to create both the new core class and a bean for it, and register that with the version driver, etc. Instead you can subclass your core class from SelfBean
, provide getters and setters for internal fields, and OPB will be able to serialize and deserialize those classes, freeing you from the unnecessary bean-related overhead.
Global Annotations
Normally beans create their original core classes right after they are deserialized (or all beans of their frame are deserialized). Sometimes there is a need to have some annotation objects available in the beginning of deserialization, before other objects.
An example could be ScopingReportAnnotation
from the okapi-step-scopingreport
project (package net.sf.okapi.steps.scopingreport
). That annotation contains fields that are formed at the end of document processing (like the document's word count). Sometimes it is desirable to place a header with a scoping report in the beginning of a translatable document before its text. Normally the scoping report annotation is attached to the EndBatchItem
resource, and would be deserialized only with that resource somewhere close to the end of the deserialization session, but OPB provides a handy means for getting that annotation before the whole document is deserialized. The mechanism is called global annotations.
Global annotations are not bound to Okapi resources, and are automatically placed in the annotations
section in the header of a JSON document.
To make a class a global annotation, you need to meet 2 requirements:
- The class should implement the
IAnnotation
interface. - The class should implement the marker interface
Serializable
:
public class ScopingReportAnnotation extends SelfBean implements IAnnotation, Serializable { private static final long serialVersionUID = -1566108918173044555L; private Map<String, String> fields = new HashMap<String, String>(); . . .
The side effect of placing Serializable
on the implements
clause is you are asked to provide a serialVersionUID
for the class. Have your Java IDE create it for you; the value is not used by OPB.
The OPB deserializes the global annotations before other objects and attaches them to the Okapi StartDocument
event. Through the mechanism of global annotations you can have document-level meta-data available before the document is deserialized.
Serialization of custom beans
Sometimes you might want to create custom classes and their corresponing beans. If those classes have a sophisticated structure, and subclassing from SelfBean
does not seem reasonable, then you will need to create pairs of core classes and beans for such complex cases.
Or you want to create a proprietary extension for the Okapi framework containing a number of classes and persistence beans.
You have two possibilities to register such extensions with OPB which are described step-by-step in the following sections.
Serialization with version ID mapping
1. Decide which version of Okapi beans you will use in your implementation. For instance, you decided to use net.sf.okapi.lib.beans.v1.OkapiBeans
.
2. Subclass net.sf.okapi.lib.beans.v1.OkapiBeans
(an implementation of IVersionDriver included in Okapi).
For example, you call the version driver for your private beans TranslationBeans
:
public class TranslationBeans extends OkapiBeans {
3. Create a unique version ID for your driver:
public static final String VERSION = "EXTRL_V1";
4. In registerBeans()
call the superclass's (OkapiBean's) registerBeans()
so that base Okapi beans are available to your version:
@Override public void registerBeans(BeanMapper beanMapper) { super.registerBeans(beanMapper); // All Okapi beans . . .
5. Add registration of your private beans to registerBeans()
as follows:
package com.example.translation.persistence.beans.v1; import net.sf.okapi.lib.beans.v1.OkapiBeans; import net.sf.okapi.lib.persistence.BeanMapper; import net.sf.okapi.steps.scopingreport.ScopingReportAnnotation; import com.example.translation.common.annotations.ParametersAnnotation; import com.example.translation.steps.common.rsagrouper.RsaGroupsAnnotation; public class TranslationBeans extends OkapiBeans { public static final String VERSION = "EXTRL_V1"; @Override public String getVersionId() { return VERSION; } @Override public void registerBeans(BeanMapper beanMapper) { super.registerBeans(beanMapper); // All Okapi beans beanMapper.registerBean(ParametersAnnotation.class, ParametersAnnotationBean.class); beanMapper.registerBean(ScopingReportAnnotation.class, ScopingReportAnnotationBean.class); beanMapper.registerBean(RsaGroupsAnnotation.class, RsaGroupsAnnotationBean.class); } }
6. Somewhere in your code add registration of your version driver, and set the version ID mapping:
public Task06_Tkit_Translation(ConverterModel model) { super(model); VersionMapper.registerVersion(TranslationBeans.class); // Your version driver VersionMapper.mapVersionId(OkapiBeans.VERSION, TranslationBeans.VERSION); // Have OPB use your ID }
These 2 lines should be called from both serialization and deserialization code before a persistence session is created.
Serialization by subclassing of steps
XLIFFKitReaderStep
and XLIFFKitWriterStep
classes in the okapi-step-xliffkit
project provide protected methods getSession()
. You can subclass those classes (both of them), and register your private beans with these calls:
super.getSession().registerBean(ParametersAnnotation.class, ParametersAnnotationBean.class); super.getSession().registerBean(ScopingReportAnnotation.class, ScopingReportAnnotationBean.class); super.getSession().registerBean(RsaGroupsAnnotation.class, RsaGroupsAnnotationBean.class);