Okapi Java Persistence API

From Okapi Framework
Jump to: navigation, search

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 the TestClass the bean name would be TestClassBean:

File:OJPA a1.png

  • To choose the superclass, press the Browse button next to the Superclass box, start typing "PersistenceBean", choose net.sf.okapi.persistence.PersistenceBean, hit OK, then Finish.

OJPA a2.png

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.

OJPA a3.png

  • Select PutCoreClassHere and replace it with the name of the core class: TestClass.
  • Point your mouse at TestClassBean and choose *Add unimplemented methods*.

OJPA a4.png

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...:

OJPA a5.png

  • 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 its content field, so the FactoryBean 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.

OJPA a6.png

  • To choose the superclass, press the Browse button next to the Superclass box, start typing "PersistenceSession", choose net.sf.okapi.persistence.PersistenceSession, hit OK, then Finish.

OJPA a7.png

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 Jackson mapper.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:

OJPA a8.png

The same as raw JSON text:

OJPA a9.png

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".

File:OJPA a10.png

  • Press the Add button next to the Interfaces box, select the IVersionDriver:

OJPA a11.png

  • 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 and NamespaceMapper. 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);