NOTE: this article covers jBPM that uses persistence as without persistence process variables are kept in memory only.
jBPM puts single requirement on the objects that are used as process variables:
- object must be serializable (simply must implement java.io.Serializable interface)
- JPA entities of another system
- documents stored in document/content management system
- etc
- serialization based, mentioned above that actually works on all object types as long as they are serializable (org.drools.core.marshalling.impl.SerializablePlaceholderResolverStrategy)
- JPA based that works on objects that are entities (org.drools.persistence.jpa.marshaller.JPAPlaceholderResolverStrategy)
// create entity manager factory
EntityManagerFactory emf = Persistence.createEntityManagerFactory("org.jbpm.sample");
RuntimeEnvironment environment =
RuntimeEnvironmentBuilder.Factory.get().newDefaultBuilder()
.entityManagerFactory(emf)
.addEnvironmentEntry(EnvironmentName.OBJECT_MARSHALLING_STRATEGIES,
new ObjectMarshallingStrategy[]{
// set the entity manager factory for jpa strategy so it
// know how to store and read entities
new JPAPlaceholderResolverStrategy(emf),
// set the serialization based strategy as last one to
// deal with non entities classes
new SerializablePlaceholderResolverStrategy(
ClassObjectMarshallingStrategyAcceptor.DEFAULT )
})
.addAsset(ResourceFactory.newClassPathResource("cmis-store.bpmn"),
ResourceType.BPMN2)
.get();
// create the runtime manager and start using entities as part of your process RuntimeManager manager =
RuntimeManagerFactory.Factory.get().newSingletonRuntimeManager(environment);
Note: make sure that you add your entity classes into persistence.xml that will be used by the jpa strategy
JPA will accept only classes that declares a field with @Id annotation (javax.persistence.Id) that allows us to ensure we will have an unique id to be used when retrieving the variable.
Serialization based one simply accepts all variables by default and thus it should be the last strategy inline. Although this default behavior can be altered by providing other acceptor implementation.
Once the strategy accepts the variable it performs marshaling operation to store the variable and unmarshaling to retrieve the variable from the back end store (of the type it supports).
In case of JPA, marshaling will check if entity is already stored entity – has id set – and:
- if not, it will persist the entity using entity manager factory that was assigned to it
- if yes, it will merge it with the persistence context to make sure up to date information is stored
With that, we quickly covered the default (serialization based) strategy and JPA based strategy. But the title of this article says we can store variables anywhere, so how’s that possible?
It’s possible because of the nature of variable persistence strategies – they are pluggable. We can create our own and simply add it to the environment and process variables that meets the acceptance criteria of the strategy will be persisted by that given strategy. To not leave you with empty hands let’s look at another implementation I created for purpose of this article (although when working on it I believe it will become more than just example for this article).
Implementing variable persistence strategy is actually very simple, it’s a matter of implementing single interface: org.kie.api.marshalling.ObjectMarshallingStrategy
public interface ObjectMarshallingStrategy {
public boolean accept(Object object);
public void write(ObjectOutputStream os,
Object object) throws IOException;
public Object read(ObjectInputStream os) throws IOException, ClassNotFoundException;
public byte[] marshal( Context context,
ObjectOutputStream os,
Object object ) throws IOException;
public Object unmarshal( Context context,
ObjectInputStream is,
byte[] object,
ClassLoader classloader ) throws IOException, ClassNotFoundException;
public Context createContext();
}
the most important methods for us are:
- accept – decides if this strategy will be responsible for persistence of given object
- marshal – performs operation to store process variable
- unmarshal – performs operation to retrieve process variable
So first bit of requirements:
- process variables must be of certain type to be stored in the content repository
- documents (process variables stored in cms) can be:
- created
- updated (with versioning)
- read
- process variables must be kept up to date
- when marshaling
- create new documents if it does not have object id assigned yet
- update document if it has already object id assigned
- by overriding existing content
- by creating new major version of the document
- by creating new minor version of the document
- when unmarshaling
- load the content of the document based on given object id
- creating new documents from the process based on custom content
- update existing documents with custom content
- load existing documents into process variable based on object id only
public byte[] marshal(Context context, ObjectOutputStream os, Object object) throws IOException {
Document document = (Document) object;
// connect to repository
Session session = getRepositorySession(user, password, url, repository);
try {
if (document.getDocumentContent() != null) {
// no object id yet, let's create the document
if (document.getObjectId() == null) {
Folder parent = ... // find folder by path
if (parent == null) {
parent = .. // create folder
}
// now we are ready to create the document in CMS
} else {
// object id exists so time to update
}
}
// now nee need to store some info as part of the process instance
// so we can later on look up, in this case is the object id and class
// that we use as process variable so we can recreate the instance on read
ByteArrayOutputStream buff = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream( buff );
oos.writeUTF(document.getObjectId());
oos.writeUTF(object.getClass().getCanonicalName());
oos.close();
return buff.toByteArray();
} finally {
// let's clear the session in the end
session.clear();
}
}
Then let’s look at the unmarshal method:
public Object unmarshal(Context context, ObjectInputStream ois, byte[] object, ClassLoader classloader) throws IOException, ClassNotFoundException {
DroolsObjectInputStream is = new DroolsObjectInputStream( new ByteArrayInputStream( object ), classloader );
// first we read out the object id and class name we stored during marshaling
String objectId = is.readUTF();
String canonicalName = is.readUTF();
// connect to repository
Session session = getRepositorySession(user, password, url, repository);
try {
// get the document from repository and create new instance ot the variable class
CmisObject doc = .....
Document document = (Document) Class.forName(canonicalName).newInstance();
// populate process variable with meta data and content
document.setObjectId(objectId);
document.setDocumentName(doc.getName());
document.setFolderName(getFolderName(doc.getParents()));
document.setFolderPath(getPathAsString(doc.getPaths()));
if (doc.getContentStream() != null) {
ContentStream stream = doc.getContentStream();
document.setDocumentContent(IOUtils.toByteArray(stream.getStream()));
document.setUpdated(false);
document.setDocumentType(stream.getMimeType());
}
return document;
} catch(Exception e) {
throw new RuntimeException("Cannot read document from CMIS", e);
} finally {
// do some clean up...
is.close();
session.clear();
}
}
nothing more that the logic to get ids and class name so the instance can be recreated and load the document from cms repository and we’re done 🙂
Last but not least, the accept method.
public boolean accept(Object object) {
if (object instanceof Document) {
return true;
}
return false;
}
Complete source code with some tests showing complete usage case from process can be found here. Enjoy and feel free to provide feedback, maybe it’s worth to start producing repository of such strategies so we can have rather rich set of strategies to be used…