/**
* The contents of this file are subject to the OpenMRS Public License
* Version 1.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
* http://license.openmrs.org
*
* Software distributed under the License is distributed on an "AS IS"
* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
* License for the specific language governing rights and limitations
* under the License.
*
* Copyright (C) OpenMRS, LLC. All Rights Reserved.
*/
package org.openmrs.module.sync.api.db.hibernate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.CallbackException;
import org.hibernate.EmptyInterceptor;
import org.hibernate.EntityMode;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.action.BeforeTransactionCompletionProcess;
import org.hibernate.collection.AbstractPersistentCollection;
import org.hibernate.collection.PersistentList;
import org.hibernate.collection.PersistentMap;
import org.hibernate.collection.PersistentSet;
import org.hibernate.criterion.Expression;
import org.hibernate.criterion.Projections;
import org.hibernate.engine.ForeignKeys;
import org.hibernate.engine.SessionImplementor;
import org.hibernate.event.EventSource;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.metadata.CollectionMetadata;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.type.EmbeddedComponentType;
import org.hibernate.type.Type;
import org.openmrs.Cohort;
import org.openmrs.Obs;
import org.openmrs.OpenmrsObject;
import org.openmrs.Patient;
import org.openmrs.PersonAttribute;
import org.openmrs.PersonAttributeType;
import org.openmrs.User;
import org.openmrs.api.context.Context;
import org.openmrs.module.sync.SyncException;
import org.openmrs.module.sync.SyncItem;
import org.openmrs.module.sync.SyncItemKey;
import org.openmrs.module.sync.SyncItemState;
import org.openmrs.module.sync.SyncRecord;
import org.openmrs.module.sync.SyncRecordState;
import org.openmrs.module.sync.SyncSubclassStub;
import org.openmrs.module.sync.SyncUtil;
import org.openmrs.module.sync.api.SyncService;
import org.openmrs.module.sync.serialization.Item;
import org.openmrs.module.sync.serialization.Normalizer;
import org.openmrs.module.sync.serialization.Package;
import org.openmrs.module.sync.serialization.Record;
import org.openmrs.util.OpenmrsConstants;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.StringUtils;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
/**
* Implements 'change interception' for data synchronization feature using Hibernate interceptor
* mechanism. Intercepted changes are recorded into the synchronization journal table in DB.
* @see org.hibernate.EmptyInterceptor
*/
public class HibernateSyncInterceptor extends EmptyInterceptor implements ApplicationContextAware, Serializable {
private static final long serialVersionUID = -4905755656754047400L;
protected final Log log = LogFactory.getLog(getClass());
private static HibernateSyncInterceptor instance;
private ApplicationContext context;
private static ThreadLocal<SyncRecord> syncRecordHolder = new ThreadLocal<SyncRecord>();
private HibernateSyncInterceptor() {
log.info("Initializing the synchronization interceptor");
}
/**
* @return a new HibernateSyncInterceptor instance, or the existing one to ensure it is a singleton
*/
public static HibernateSyncInterceptor getInstance() {
if (instance == null) {
instance = new HibernateSyncInterceptor();
}
return instance;
}
/**
* No operation, logging only
* @see EmptyInterceptor#afterTransactionBegin(Transaction)
*/
@Override
public void afterTransactionBegin(Transaction tx) {
if (log.isDebugEnabled()) {
log.debug("Transaction Started");
}
}
/**
* Packages up deletes and sets the item state to DELETED.
* @see EmptyInterceptor#onDelete(Object, java.io.Serializable, Object[], String[], org.hibernate.type.Type[])
*/
@Override
public void onDelete(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
log.debug("Delete intercepted");
if (shouldSynchronize(entity)) {
log.debug("Packaging: " + SyncUtil.formatObject(entity));
packageObject((OpenmrsObject) entity, state, propertyNames, types, id, SyncItemState.DELETED);
}
else {
log.debug("Entity configured not to sync: " + SyncUtil.formatObject(entity));
}
}
/**
* Packages up inserts and sets the item state to NEW
* @see EmptyInterceptor#onSave(Object, java.io.Serializable, Object[], String[], org.hibernate.type.Type[])
*/
@Override
public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
log.debug("Insert intercepted");
if (shouldSynchronize(entity)) {
log.debug("Packaging: " + SyncUtil.formatObject(entity));
packageObject((OpenmrsObject) entity, state, propertyNames, types, id, SyncItemState.NEW);
}
else {
log.debug("Entity configured not to sync: " + SyncUtil.formatObject(entity));
}
return false; // This means that we did not modify the passed in entity
}
/**
* Packages up updates and sets the item state to NEW
* @see EmptyInterceptor#onFlushDirty(Object, java.io.Serializable, Object[], Object[], String[], org.hibernate.type.Type[])
*/
@Override
public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) {
log.debug("Update intercepted");
if (shouldSynchronize(entity)) {
log.debug("Packaging: " + SyncUtil.formatObject(entity));
packageObject((OpenmrsObject) entity, currentState, propertyNames, types, id, SyncItemState.UPDATED);
}
else {
log.debug("Entity configured not to sync: " + SyncUtil.formatObject(entity));
}
return false; // This means that we did not modify the passed in entity
}
/**
* Handles collection remove event. As can be seen in org.hibernate.engine.Collections,
* hibernate only calls remove when it is about to recreate a collection.
*/
@Override
public void onCollectionRemove(Object collection, Serializable key) throws CallbackException {
if (log.isDebugEnabled()) {
log.debug("onCollectionRemove key: " + key);
}
// TODO: This has never done anything. We should investigate if we need to process a delete?
return;
}
/**
* Handles collection recreate. Recreate is triggered by hibernate when collection object is
* replaced by new/different instance.
* <p>
* remarks: See hibernate AbstractFlushingEventListener and org.hibernate.engine.Collections
* implementation to understand how collection updates are hooked up in hibernate, specifically
* see Collections.prepareCollectionForUpdate().
*
* @see org.hibernate.engine.Collections
* @see org.hibernate.event.def.AbstractFlushingEventListener
*/
@Override
public void onCollectionRecreate(Object collection, Serializable key) throws CallbackException {
if (log.isDebugEnabled()) {
log.debug("onCollectionRecreate key: " + key);
}
if (collection instanceof AbstractPersistentCollection) {
processHibernateCollection((AbstractPersistentCollection) collection, key, "recreate");
}
else {
// TODO: MS - We should look at whether changing this to an exception will cause issues
log.warn("Unsupported collection type; collection must derive from AbstractPersistentCollection," + " collection type was:" + collection.getClass().getName());
}
}
/**
* Handles updates of a collection (i.e. added/removed entries).
* <p>
* remarks: See hibernate AbstractFlushingEventListener implementation to understand how
* collection updates are hooked up in hibernate.
*
* @see org.hibernate.engine.Collections
* @see org.hibernate.event.def.AbstractFlushingEventListener
*/
@Override
public void onCollectionUpdate(Object collection, Serializable key) throws CallbackException {
if (log.isDebugEnabled()) {
log.debug("onCollectionUpdate key: " + key);
}
if (collection instanceof AbstractPersistentCollection) {
processHibernateCollection((AbstractPersistentCollection) collection, key, "update");
}
else {
// TODO: MS - We should look at whether changing this to an exception will cause issues.
log.warn("Unsupported collection type; collection must derive from AbstractPersistentCollection," + " collection type was:" + collection.getClass().getName());
}
}
/**
* Intercept prepared statements for logging purposes only.
* <p>
* NOTE: At this point, we are ignoring any prepared statements. This method gets called on any
* prepared stmt; meaning selects also which makes handling this reliably difficult.
* Fundamentally, short of replaying sql as is on parent, it is difficult to imagine safe and
* complete implementation.
* <p>
* Preferred approach is to weed out all dynamic SQL from openMRS DB layer and if absolutely
* necessary, create a hook for DB layer code to Explicitly specify what SQL should be passed to
* the parent during synchronization.
*
* @see EmptyInterceptor#onPrepareStatement(String)
*/
@Override
public String onPrepareStatement(String sql) {
if (log.isTraceEnabled()) {
log.trace("Prepare Statement: " + sql);
}
return sql;
}
/**
* We register our beforeTransactionCompletion process here to ensure it gets run for this transaction
* @see EmptyInterceptor#preFlush(Iterator)
*/
@Override
public void preFlush(Iterator entities) {
if (log.isDebugEnabled()) {
log.debug("preFlush intercepted: " + SyncUtil.formatEntities(entities));
}
registerBeforeTransactionCompletionProcess();
}
/**
* No operation, logging only
* @see EmptyInterceptor#postFlush(Iterator)
*/
@Override
public void postFlush(Iterator entities) {
if (log.isDebugEnabled()) {
log.debug("postFlush intercepted: " + SyncUtil.formatEntities(entities));
}
}
/**
* No operation, logging only. The processing of sync records happens in a BeforeTransactionCompletionProcess
* (see below), due to the fact that any exceptions thrown by beforeTransactionCompletion are swallowed, so the
* user would have no idea that their saving operation failed to save sync records. In contrast,
* beforeTransactionCompletionProcesses are run outside of that try/catch block and result in exceptions
* bubbling back up
* @see org.hibernate.impl.SessionImpl#beforeTransactionCompletion(org.hibernate.Transaction)
* @see EmptyInterceptor#beforeTransactionCompletion(org.hibernate.Transaction)
*/
@Override
public void beforeTransactionCompletion(Transaction tx) {
if (log.isDebugEnabled()) {
log.debug("About to Complete Transaction: " + SyncUtil.formatTransactionStatus(tx));
}
}
/**
* Registers a {@link BeforeTransactionCompletionProcess} if none has been registered with the
* current session, it uses ThreadLocal variable to check if it is already set since sessions
* are thread bound
*/
private void registerBeforeTransactionCompletionProcess() {
log.debug("Registering SyncBeforeTransactionCompletionProcess with the current session");
EventSource eventSource = (EventSource) getSessionFactory().getCurrentSession();
eventSource.getActionQueue().registerProcess(new BeforeTransactionCompletionProcess() {
public void doBeforeTransactionCompletion(SessionImplementor sessionImpl) {
log.trace("doBeforeTransactionCompletion process: checking for SyncRecord to save");
try {
SyncRecord record = getSyncRecord();
syncRecordHolder.remove();
// Does this transaction contain any serialized changes?
if (record != null && record.hasItems()) {
// Grab user if we have one, and use the UUID of the user as creator of this SyncRecord
User user = Context.getAuthenticatedUser();
if (user != null) {
record.setCreator(user.getUuid());
}
// Grab database version
record.setDatabaseVersion(OpenmrsConstants.OPENMRS_VERSION_SHORT);
// Complete the record
record.setUuid(SyncUtil.generateUuid());
if (record.getOriginalUuid() == null) {
log.debug("OriginalUuid is null, so assigning a new UUID: " + record.getUuid());
record.setOriginalUuid(record.getUuid());
}
else {
log.debug("OriginalUuid is: " + record.getOriginalUuid());
}
record.setState(SyncRecordState.NEW);
record.setTimestamp(new Date());
record.setRetryCount(0);
log.info("Saving SyncRecord " + record.getOriginalUuid() + ": " + record.getItems().size() + " items");
// Save SyncRecord
getSyncService().createSyncRecord(record, record.getOriginalUuid());
}
else {
// note: this will happen all the time with read-only transactions
if (log.isTraceEnabled()) {
log.trace("No SyncItems in SyncRecord, save discarded (note: maybe a read-only transaction)!");
}
}
}
catch (Exception e) {
log.error("A error occurred while trying to save a sync record in the interceptor", e);
throw new SyncException("Error in interceptor, see log messages and callstack.", e);
}
}
});
log.debug("Successfully registered SyncBeforeTransactionCompletionProcess with the current session");
}
/**
* No operation, logging only
* @see EmptyInterceptor#afterTransactionCompletion(Transaction)
*/
@Override
public void afterTransactionCompletion(Transaction tx) {
if (log.isDebugEnabled()) {
log.debug("Transaction Completed: " + SyncUtil.formatTransactionStatus(tx));
}
// Because the beforeTransactionCompletion method is not called on rollback, we need to ensure any syncRecords still on the thread are removed after the tx is completed
syncRecordHolder.remove();
}
/**
* Serializes and packages an intercepted change in object state.
* <p>
* IMPORTANT serialization notes:
* <p>
* Transient Properties. Transients are not serialized/journalled. Marking an object property as
* transient is the supported way of designating it as something not to be recorded into the
* journal.
* <p/>
* Hibernate Identity property. A property designated in Hibernate as identity (i.e. primary
* key) *is* not serialized. This is because sync does not enforce global uniqueness of database
* primary keys. Instead, custom uuid property is used. This allows us to continue to use native
* types for 'traditional' entity relationships.
*
* @param entity The object changed.
* @param currentState Array containing data for each field in the object as they will be saved.
* @param propertyNames Array containing name for each field in the object, corresponding to
* currentState.
* @param types Array containing Type of the field in the object, corresponding to currentState.
* @param state SyncItemState, e.g. NEW, UPDATED, DELETED
* @param id Value of the identifier for this entity
*/
protected void packageObject(OpenmrsObject entity, Object[] currentState, String[] propertyNames, Type[] types,
Serializable id, SyncItemState state) throws SyncException {
String objectUuid = null;
String originalRecordUuid = null;
Set<String> transientProps = null;
String infoMsg = null;
ClassMetadata data = null;
String idPropertyName = null;
org.hibernate.tuple.IdentifierProperty idPropertyObj = null;
// The container of values to be serialized:
// Holds tuples of <property-name> -> {<property-type-name>,
// <property-value as string>}
HashMap<String, PropertyClassValue> values = new HashMap<String, PropertyClassValue>();
try {
objectUuid = entity.getUuid();
// pull-out sync-network wide change id for the sync *record* (not the entity itself),
// if one was already assigned (i.e. this change is coming from some other server)
originalRecordUuid = getSyncRecord().getOriginalUuid();
if (log.isDebugEnabled()) {
// build up a starting msg for all logging:
StringBuilder sb = new StringBuilder();
sb.append("In PackageObject, entity type:");
sb.append(entity.getClass().getName());
sb.append(", entity uuid:");
sb.append(objectUuid);
sb.append(", originalUuid uuid:");
sb.append(originalRecordUuid);
log.debug(sb.toString());
}
// Transient properties are not serialized.
transientProps = new HashSet<String>();
for (Field f : entity.getClass().getDeclaredFields()) {
if (Modifier.isTransient(f.getModifiers())) {
transientProps.add(f.getName());
if (log.isDebugEnabled())
log.debug("The field " + f.getName() + " is transient - so we won't serialize it");
}
}
/*
* Retrieve metadata for this type; we need to determine what is the
* PK field for this type. We need to know this since PK values are
* *not* journalled; values of primary keys are assigned where
* physical DB records are created. This is so to avoid issues with
* id collisions.
*
* In case of <generator class="assigned" />, the Identifier
* property is already assigned value and needs to be journalled.
* Also, the prop will *not* be part of currentState,thus we need to
* pull it out with reflection/metadata.
*/
data = getSessionFactory().getClassMetadata(entity.getClass());
if (data.hasIdentifierProperty()) {
idPropertyName = data.getIdentifierPropertyName();
idPropertyObj = ((org.hibernate.persister.entity.AbstractEntityPersister) data).getEntityMetamodel()
.getIdentifierProperty();
if (id != null && idPropertyObj.getIdentifierGenerator() != null
&& (idPropertyObj.getIdentifierGenerator() instanceof org.hibernate.id.Assigned
// || idPropertyObj.getIdentifierGenerator() instanceof org.openmrs.api.db.hibernate.NativeIfNotAssignedIdentityGenerator
)) {
// serialize value as string
values.put(idPropertyName, new PropertyClassValue(id.getClass().getName(), id.toString()));
}
} else if (data.getIdentifierType() instanceof EmbeddedComponentType) {
// if we have a component identifier type (like AlertRecipient),
// make
// sure we include those properties
EmbeddedComponentType type = (EmbeddedComponentType) data.getIdentifierType();
for (int i = 0; i < type.getPropertyNames().length; i++) {
String propertyName = type.getPropertyNames()[i];
Object propertyValue = type.getPropertyValue(entity, i, org.hibernate.EntityMode.POJO);
addProperty(values, entity, type.getSubtypes()[i], propertyName, propertyValue, infoMsg);
}
}
/*
* Loop through all the properties/values and put in a hash for
* duplicate removal
*/
for (int i = 0; i < types.length; i++) {
String typeName = types[i].getName();
if (log.isDebugEnabled())
log.debug("Processing, type: " + typeName + " Field: " + propertyNames[i]);
if (propertyNames[i].equals(idPropertyName) && log.isInfoEnabled())
log.debug(infoMsg + ", Id for this class: " + idPropertyName + " , value:" + currentState[i]);
if (currentState[i] != null) {
// is this the primary key or transient? if so, we don't
// want to serialize
if (propertyNames[i].equals(idPropertyName)
|| ("personId".equals(idPropertyName) && "patientId".equals(propertyNames[i]))
//|| ("personId".equals(idPropertyName) && "userId".equals(propertyNames[i]))
|| transientProps.contains(propertyNames[i])) {
// if (log.isInfoEnabled())
log.debug("Skipping property (" + propertyNames[i]
+ ") because it's either the primary key or it's transient.");
} else {
addProperty(values, entity, types[i], propertyNames[i], currentState[i], infoMsg);
}
} else {
// current state null -- skip
if (log.isDebugEnabled())
log.debug("Field Type: " + typeName + " Field Name: " + propertyNames[i] + " is null, skipped");
}
}
/*
* Now serialize the data identified and put in the value-map
*/
// Setup the serialization data structures to hold the state
Package pkg = new Package();
String className = entity.getClass().getName();
Record xml = pkg.createRecordForWrite(className);
Item entityItem = xml.getRootItem();
// loop through the map of the properties that need to be serialized
for (Map.Entry<String, PropertyClassValue> me : values.entrySet()) {
String property = me.getKey();
// if we are processing onDelete event all we need is uuid
if ((state == SyncItemState.DELETED) && (!"uuid".equals(property))) {
continue;
}
try {
PropertyClassValue pcv = me.getValue();
appendRecord(xml, entity, entityItem, property, pcv.getClazz(), pcv.getValue());
}
catch (Exception e) {
String msg = "Could not append attribute. Error while processing property: " + property + " - "
+ e.getMessage();
throw (new SyncException(msg, e));
}
}
values.clear(); // Be nice to GC
if (objectUuid == null)
throw new SyncException("uuid is null for: " + className + " with id: " + id);
/*
* Create SyncItem and store change in SyncRecord kept in
* ThreadLocal.
*/
SyncItem syncItem = new SyncItem();
syncItem.setKey(new SyncItemKey<String>(objectUuid, String.class));
syncItem.setState(state);
syncItem.setContent(xml.toStringAsDocumentFragement());
syncItem.setContainedType(entity.getClass());
if (log.isDebugEnabled())
log.debug("Adding SyncItem to SyncRecord");
getSyncRecord().addItem(syncItem);
getSyncRecord().addContainedClass(entity.getClass().getName());
// set the originating uuid for the record: do this once per Tx;
// else we may end up with empty string
if (getSyncRecord().getOriginalUuid() == null || "".equals(getSyncRecord().getOriginalUuid())) {
getSyncRecord().setOriginalUuid(originalRecordUuid);
}
}
catch (SyncException ex) {
log.error("Journal error\n", ex);
throw (ex);
}
catch (Exception e) {
log.error("Journal error\n", e);
throw (new SyncException("Error in interceptor, see log messages and callstack.", e));
}
return;
}
/**
* Convenience method to add a property to the given list of values to turn into xml
*
* @param values
* @param entity
* @param propertyType
* @param propertyName
* @param propertyValue
* @param infoMsg
* @throws Exception
*/
private void addProperty(HashMap<String, PropertyClassValue> values, OpenmrsObject entity, Type propertyType,
String propertyName, Object propertyValue, String infoMsg) throws Exception {
Normalizer n;
String propertyTypeName = propertyType.getName();
if ((n = SyncUtil.getNormalizer(propertyTypeName)) != null) {
// Handle safe types like
// boolean/String/integer/timestamp via Normalizers
values.put(propertyName, new PropertyClassValue(propertyTypeName, n.toString(propertyValue)));
} else if ((n = SyncUtil.getNormalizer(propertyValue.getClass())) != null) {
values.put(propertyName, new PropertyClassValue(propertyValue.getClass().getName(), n.toString(propertyValue)));
} else if (propertyType.isCollectionType() && (n = isCollectionOfSafeTypes(entity, propertyName)) != null) {
// if the property is a list/set/collection AND the members of that
// collection are a "safe type",
// then we put the values into the xml
values.put(propertyName, new PropertyClassValue(propertyTypeName, n.toString(propertyValue)));
}
/*
* Not a safe type, check if the object implements the OpenmrsObject interface
*/
else if (propertyValue instanceof OpenmrsObject) {
OpenmrsObject childObject = (OpenmrsObject) propertyValue;
String childUuid = fetchUuid(childObject);
if (childUuid != null) {
values.put(propertyName, new PropertyClassValue(propertyTypeName, childUuid));
}
else {
String msg = infoMsg + ", Field value should be synchronized, but uuid is null. Field Type: " + propertyType + " Field Name: " + propertyName;
log.error(msg + ". Turn on debug logging for more details.");
throw (new SyncException(msg));
}
}
else {
// state != null but it is not safetype or
// implements OpenmrsObject: do not package and log
// as info
if (log.isDebugEnabled())
log.debug(infoMsg + ", Field Type: " + propertyType + " Field Name: " + propertyName
+ " is not safe or OpenmrsObject, skipped!");
}
}
/**
* Checks the collection to see if it is a collection of supported types. If so, then it returns
* appropriate normalizer. Note, this handles maps too.
*
* @param object
* @param propertyName
* @return a Normalizer for the given type or null if not a safe type
* @throws NoSuchFieldException
* @throws SecurityException
*/
private Normalizer isCollectionOfSafeTypes(OpenmrsObject object, String propertyName) throws SecurityException,
NoSuchFieldException {
try {
java.lang.reflect.ParameterizedType collectionType = ((java.lang.reflect.ParameterizedType) object.getClass()
.getDeclaredField(propertyName).getGenericType());
if (Map.class.isAssignableFrom((Class) collectionType.getRawType())) {
//this is a map; Map<K,V>: verify that K and V are of types we know how to process
java.lang.reflect.Type keyType = collectionType.getActualTypeArguments()[0];
java.lang.reflect.Type valueType = collectionType.getActualTypeArguments()[1];
Normalizer keyNormalizer = SyncUtil.getNormalizer((Class) keyType);
Normalizer valueNormalizer = SyncUtil.getNormalizer((Class) valueType);
if (keyNormalizer != null && valueNormalizer != null) {
return SyncUtil.getNormalizer((Class) collectionType.getRawType());
} else {
return null;
}
} else {
//this is some other collection, so just get a normalizer for its
return SyncUtil.getNormalizer((Class) (collectionType.getActualTypeArguments()[0]));
}
}
catch (Throwable t) {
// might get here if the property is on a superclass to the object
log.trace("Unable to get collection field: " + propertyName + " from object " + object.getClass()
+ " for some reason", t);
}
// on errors just return null
return null;
}
/**
* Adds a property value to the existing serialization record as a string.
* <p>
* If data is null it will be skipped, no empty serialization items are written. In case of xml
* serialization, the data will be serialized as: <property
* type='classname'>data</property>
*
* @param xml record node to append to
* @param entity the object holding the given property
* @param parent the pointer to the root parent node
* @param property new item name (in case of xml serialization this will be child element name)
* @param classname type of the property, will be recorded as attribute named 'type' on the
* child item
* @param data String content, in case of xml serialized as text node (i.e. not CDATA)
* @throws Exception
*/
protected void appendRecord(Record xml, OpenmrsObject entity, Item parent, String property, String classname, String data)
throws Exception {
// if (data != null && data.length() > 0) {
// this will break if we don't allow data.length==0 - some string values
// are required NOT NULL, but can be blank
if (data != null) {
Item item = xml.createItem(parent, property);
item.setAttribute("type", classname);
data = transformItemForSyncRecord(item, entity, property, data);
xml.createText(item, data);
}
}
/**
* Called while saving a SyncRecord to allow for manipulating what is stored. The impl of this
* method transforms the {@link PersonAttribute#getValue()} and {@link Obs#getVoidReason()}
* methods to not reference primary keys. (Instead the uuid is referenced and then dereferenced
* before being saved). If no transformation is to take place, the data is returned as given.
*
* @param item the serialized sync item associated with this record
* @param entity the OpenmrsObject containing the property
* @param property the property name
* @param data the current value for the
* @return the transformed (or unchanged) data to save in the SyncRecord
*/
public String transformItemForSyncRecord(Item item, OpenmrsObject entity, String property, String data) {
// data will not be null here, so NPE checks are not needed
if (entity instanceof PersonAttribute && "value".equals(property)) {
PersonAttribute attr = (PersonAttribute) entity;
// use PersonAttributeType.format to get the uuid
if (attr.getAttributeType() == null)
throw new SyncException("Unable to find person attr type on attr with uuid: " + entity.getUuid());
String className = attr.getAttributeType().getFormat();
try {
Class c = Context.loadClass(className);
item.setAttribute("type", className);
// An empty string represents an empty value. Return it as the UUID does not exist.
if ((data.trim()).isEmpty())
return data;
// only convert to uuid if this is an OpenMrs object
// otherwise, we are just storing a simple String or Integer
// value
if (OpenmrsObject.class.isAssignableFrom(c)) {
String valueObjectUuid = fetchUuid(c, Integer.valueOf(data));
return valueObjectUuid;
}
}
catch (Throwable t) {
log.warn("Unable to get class of type: " + className + " for sync'ing attribute.value column", t);
}
} else if (entity instanceof PersonAttributeType && "foreignKey".equals(property)) {
if (StringUtils.hasLength(data)) {
PersonAttributeType attrType = (PersonAttributeType) entity;
String className = attrType.getFormat();
try {
Class c = Context.loadClass(className);
String foreignKeyObjectUuid = fetchUuid(c, Integer.valueOf(data));
// set the class name on this to be the uuid-ized type
// instead of java.lang.Integer.
// the SyncUtil.valForField method will handle changing this
// back to an integer
item.setAttribute("type", className);
return foreignKeyObjectUuid;
}
catch (Throwable t) {
log.warn("Unable to get class of type: " + className + " for sync'ing foreignKey column", t);
}
}
} else if (entity instanceof Obs && "voidReason".equals(property)) {
if (data.contains("(new obsId: ")) {
// rip out the obs id and replace it with a uuid
String voidReason = String.copyValueOf(data.toCharArray()); // copy
// the
// string
// so
// that
// we're
// operating
// on
// a
// new
// object
int start = voidReason.lastIndexOf(" ") + 1;
int end = voidReason.length() - 1;
String obsId = voidReason.substring(start, end);
try {
String newObsUuid = fetchUuid(Obs.class, Integer.valueOf(obsId));
return data.substring(0, data.lastIndexOf(" ")) + " " + newObsUuid + ")";
}
catch (Exception e) {
log.trace("unable to get uuid from obs pk: " + obsId, e);
}
}
} else if (entity instanceof Cohort && "memberIds".equals(property)) {
// convert integer patient ids to uuids
try {
item.setAttribute("type", "java.util.Set<org.openmrs.Patient>");
StringBuilder sb = new StringBuilder();
data = data.replaceFirst("\\[", "").replaceFirst("\\]", "");
sb.append("[");
String[] fieldVals = data.split(",");
for (int x = 0; x < fieldVals.length; x++) {
if (x >= 1)
sb.append(", ");
String eachFieldVal = fieldVals[x].trim(); // take out whitespace
String uuid = fetchUuid(Patient.class, Integer.valueOf(eachFieldVal));
sb.append(uuid);
}
sb.append("]");
return sb.toString();
}
catch (Throwable t) {
log.warn("Unable to get Patient for sync'ing cohort.memberIds property", t);
}
}
return data;
}
/**
* Determines if entity is to be 'synchronized', eg. implements OpenmrsObject interface.
*
* @param entity Object to examine.
* @return true if entity should be synchronized, else false.
*/
protected boolean shouldSynchronize(Object entity) {
Boolean ret = true;
// check if this object is to be sync-ed: compare against the configured classes
// for time being, suspend any flushing -- we are in the middle of hibernate stack
org.hibernate.FlushMode flushMode = getSessionFactory().getCurrentSession().getFlushMode();
getSessionFactory().getCurrentSession().setFlushMode(org.hibernate.FlushMode.MANUAL);
try {
ret = getSyncService().shouldSynchronize(entity);
}
catch (Exception ex) {
log.warn("Journal error\n", ex);
//log error info as warning but continue on
}
finally {
if (getSessionFactory() != null) {
getSessionFactory().getCurrentSession().setFlushMode(flushMode);
}
}
return ret;
}
/**
* Retrieves uuid of OpenmrsObject instance from the storage based on identity value (i.e. PK).
* <p>
* Remarks: It is important for the implementation to avoid loading obj into session while
* trying to determine its uuid. As a result, the implementation uses the combination of
* reflection to determine the object's identifier value and Hibernate criteria in order to
* build select statement for getting the uuid. The reason to avoid fetching the obj is because
* doing it causes an error in hibernate when processing disconnected proxies. Specifically,
* during obs edit, several properties are are disconnected as the form controller uses Criteria
* object to construct select queury session.clear() and then session.merge(). Finally,
* implementation suspends any state flushing to avoid any weird auto-flush events being
* triggered while select is being executed.
*
* @param obj Instance of OpenmrsObject for which to retrieve uuid for.
* @return uuid from storage if obj identity value is set, else null.
* @see ForeignKeys
*/
protected String fetchUuid(OpenmrsObject obj) {
if (obj == null) {
return null;
}
if (log.isDebugEnabled()) {
log.debug("Attempting to fetch uuid for from OpenmrsObject");
}
try {
return obj.getUuid();
}
catch (Exception e) {
log.debug("Unable to get uuid from OpenmrsObject directly", e);
}
try {
if (obj instanceof HibernateProxy) {
log.debug("Attempting to retrieve via the Hibernate Proxy class and identifier");
HibernateProxy proxy = (HibernateProxy) obj;
Class persistentClass = proxy.getHibernateLazyInitializer().getPersistentClass();
Object identifier = proxy.getHibernateLazyInitializer().getIdentifier();
String uuid = fetchUuid(persistentClass, identifier);
log.debug("Successfully retrieved uuid " + uuid);
return uuid;
}
}
catch (Exception e) {
log.debug("Unable to fetch uuid from Hibernate Proxy: ", e);
}
try {
log.debug("Attempting to load from the database given class and id");
String uuid = fetchUuid(obj.getClass(), obj.getId());
log.debug("Successfully retrieved uuid " + uuid);
return uuid;
}
catch (Exception e) {
log.debug("Unable to fetch uuid from class and id", e);
}
try {
log.debug("Attempting to load from the database given class only, using hibernate mapping to determine identifier");
ClassMetadata data = getSessionFactory().getClassMetadata(obj.getClass());
if (data != null) {
String idPropertyName = data.getIdentifierPropertyName();
if (idPropertyName != null) {
Method m = SyncUtil.getGetterMethod(obj.getClass(), idPropertyName);
if (m != null) {
Object idPropertyValue = m.invoke(obj);
String uuid = fetchUuid(obj.getClass(), idPropertyValue);
log.debug("Successfully retrieved uuid " + uuid);
return uuid;
}
}
}
}
catch (Exception e) {
log.debug("Unable to fetch uuid from reflection via hibernate metadata", e);
}
log.warn("*** All attempts failed to fetch the uuid for an OpenmrsObject ***");
return null;
}
/**
* See {@link #fetchUuid(OpenmrsObject)}
*
* @param objTrueType
* @param idPropertyValue
* @return
*/
protected String fetchUuid(Class objTrueType, Object idPropertyValue) {
String uuid = null;
// for time being, suspend any flushing
org.hibernate.FlushMode flushMode = getSessionFactory().getCurrentSession().getFlushMode();
getSessionFactory().getCurrentSession().setFlushMode(org.hibernate.FlushMode.MANUAL);
try {
// try to fetch the instance and get its uuid
if (idPropertyValue != null) {
// build sql to fetch uuid - avoid loading obj into session
org.hibernate.Criteria criteria = getSessionFactory().getCurrentSession().createCriteria(objTrueType);
criteria.add(Expression.idEq(idPropertyValue));
criteria.setProjection(Projections.property("uuid"));
uuid = (String) criteria.uniqueResult();
if (uuid == null)
log.warn("Unable to find obj of type: " + objTrueType + " with primary key: " + idPropertyValue);
return uuid;
}
}
finally {
if (getSessionFactory() != null) {
getSessionFactory().getCurrentSession().setFlushMode(flushMode);
}
}
return null;
}
/**
* Processes changes to hibernate collections. At the moment, only persistent sets are
* supported.
* <p>
* Remarks: Note that simple lists and maps of primitive types are supported also by default via
* normalizers and do not require explicit handling as shown here for sets of any reference
* types.
* <p>
*
* @param collection Instance of Hibernate AbstractPersistentCollection to process.
* @param key key of owner for the collection.
* @param action hibernate 'action' being performed: update, recreate. note, deletes are handled
* via re-create
*/
protected void processHibernateCollection(AbstractPersistentCollection collection, Serializable key, String action) {
if (!(collection instanceof PersistentSet || collection instanceof PersistentMap || collection instanceof PersistentList)) {
log.debug("Unsupported collection type, collection type was:" + collection.getClass().getName());
return;
}
OpenmrsObject owner = null;
String originalRecordUuid = null;
LinkedHashMap<String, OpenmrsObject> entriesHolder = null;
// we only process recreate and update
if (!"update".equals(action) && !"recreate".equals(action)) {
log.error("Unexpected 'action' supplied, valid values: recreate, update. value provided: " + action);
throw new CallbackException("Unexpected 'action' supplied while processing a persistent set.");
}
// retrieve owner and original uuid if there is one
if (collection.getOwner() instanceof OpenmrsObject) {
owner = (OpenmrsObject) collection.getOwner();
if (!this.shouldSynchronize(owner)) {
if (log.isDebugEnabled())
log.debug("Determined entity not to be journaled, exiting onDelete.");
return;
}
originalRecordUuid = getSyncRecord().getOriginalUuid();
} else {
log.debug("Cannot process collection where owner is not OpenmrsObject.");
return;
}
/*
* determine if this set needs to be processed. Process if: 1. it is
* recreate or 2. is dirty && current state does not equal stored
* snapshot
*/
boolean process = false;
if ("recreate".equals(action)) {
process = true;
} else {
if (collection.isDirty()) {
org.hibernate.persister.collection.CollectionPersister persister = ((org.hibernate.engine.SessionFactoryImplementor) getSessionFactory())
.getCollectionPersister(collection.getRole());
Object ss = null;
try { // code around hibernate bug:
// http://opensource.atlassian.com/projects/hibernate/browse/HHH-2937
ss = collection.getSnapshot(persister);
}
catch (NullPointerException ex) {}
if (ss == null) {
log.debug("snapshot is null");
if (collection.empty())
process = false;
else
process = true;
} else if (!collection.equalsSnapshot(persister)) {
process = true;
}
;
}
if (!process) {
log.debug("set processing, no update needed: not dirty or current state and snapshots are same");
}
}
if (!process)
return;
// pull out the property name on owner that corresponds to the collection
ClassMetadata data = getSessionFactory().getClassMetadata(owner.getClass());
String[] propNames = data.getPropertyNames();
// this is the name of the property on owner object that contains the set
String ownerPropertyName = null;
for (String propName : propNames) {
Object propertyVal = data.getPropertyValue(owner, propName, org.hibernate.EntityMode.POJO);
// note: test both with equals() and == because
// PersistentSet.equals()
// actually does not handle equality of two persistent sets well
if (collection == propertyVal || collection.equals(propertyVal)) {
ownerPropertyName = propName;
break;
}
}
if (ownerPropertyName == null) {
log.error("Could not find the property on owner object that corresponds to the collection being processed.");
log.error("owner info: \ntype: " + owner.getClass().getName() + ", \nuuid: " + owner.getUuid()
+ ",\n property name for collection: " + ownerPropertyName);
throw new CallbackException(
"Could not find the property on owner object that corresponds to the collection being processed.");
}
//now we know this needs to be processed. Proceed accordingly:
if (collection instanceof PersistentSet || collection instanceof PersistentList
|| collection instanceof PersistentMap) {
processPersistentCollection(collection, key, action, originalRecordUuid, owner, ownerPropertyName);
}
return;
}
/**
* Processes changes to persistent collection that contains instances of OpenmrsObject objects.
* <p>
* Remarks:
* <p>
* Xml 'schema' for the sync item content for the persisted collection follows. Note that for persisted
* collections syncItemKey is a composite of owner object uuid and the property name that contains the
* collection. <br/>
* <persistent-collection> element: wrapper element <br/>
* <owner uuid='' propertyName='' type='' action='recreate|update' > element: this
* captures the information about the object that holds reference to the collection being
* processed <br/>
* -uuid: owner object uuid <br/>
* -properyName: names of the property on owner object that holds this collection <br/>
* -type: owner class name <br/>
* -action: recreate, update -- these are collection events defined by hibernate interceptor <br/>
* <entry action='update|delete' uuid='' type='' > element: this captures info about
* individual collection entries: <br/>
* -action: what is being done to this item of the collection: delete (item was removed from the
* collection) or update (item was added to the collection) <br/>
* -uuid: entry's uuid <br/>
* -type: class name
*
* @param collection Instance of Hibernate AbstractPersistentCollection to process.
* @param key key of owner for the collection.
* @param action action being performed on the collection: update, recreate
*/
private void processPersistentCollection(AbstractPersistentCollection collection, Serializable key, String action, String originalRecordUuid,
OpenmrsObject owner, String ownerPropertyName) {
LinkedHashMap<String, OpenmrsObject> entriesHolder = null;
// Setup the serialization data structures to hold the state
Package pkg = new Package();
entriesHolder = new LinkedHashMap<String, OpenmrsObject>();
try {
CollectionMetadata collMD = getCollectionMetadata(owner.getClass(), ownerPropertyName, getSessionFactory());
if (collMD == null) {
throw new SyncException("Can't find a collection with " + ownerPropertyName + " in class "
+ owner.getClass());
}
Class<?> elementClass = collMD.getElementType().getReturnedClass();
//If this is a simple type like Integer, serialization of the collection will be as below:
//<org.openmrs.Cohort>
// <memberIds type="java.util.Set(org.openmrs.Cohort)">[2, 3]</memberIds>
// ............. and more
//This should work just fine as long as there is a Normalizer registered for it
if (!OpenmrsObject.class.isAssignableFrom(elementClass) && SyncUtil.getNormalizer(elementClass) != null) {
//Check if there is already a NEW/UPDATE sync item for the owner
SyncItem syncItem = new SyncItem();
syncItem.setKey(new SyncItemKey<String>(owner.getUuid(), String.class));
syncItem.setContainedType(owner.getClass());
syncItem.setState(SyncItemState.UPDATED);
boolean ownerHasSyncItem = getSyncRecord().hasSyncItem(syncItem);
syncItem.setState(SyncItemState.NEW);
if (!ownerHasSyncItem)
ownerHasSyncItem = getSyncRecord().hasSyncItem(syncItem);
if (!ownerHasSyncItem) {
ClassMetadata cmd = getSessionFactory().getClassMetadata(owner.getClass());
//create an UPDATE sync item for the owner so that the collection changes get recorded along
Serializable primaryKeyValue = cmd.getIdentifier(owner, (SessionImplementor)getSessionFactory().getCurrentSession());
packageObject(owner, cmd.getPropertyValues(owner, EntityMode.POJO), cmd.getPropertyNames(),
cmd.getPropertyTypes(), primaryKeyValue, SyncItemState.UPDATED);
} else {
//There is already an UPDATE OR NEW SyncItem for the owner containing the above updates
}
return;
}
// find out what entries need to be serialized
for (Object entry : (Iterable)collection) {
if (entry instanceof OpenmrsObject) {
OpenmrsObject obj = (OpenmrsObject) entry;
// attempt to retrieve entry uuid
String entryUuid = obj.getUuid();
if (entryUuid == null) {
entryUuid = fetchUuid(obj);
if (log.isDebugEnabled()) {
log.debug("Entry uuid was null, attempted to fetch uuid with the following results");
log.debug("Entry type:" + obj.getClass().getName() + ",uuid:" + entryUuid);
}
}
// well, this is messed up: have an instance of
// OpenmrsObject but has no uuid
if (entryUuid == null) {
log.error("Cannot handle collection entries where uuid is null.");
throw new CallbackException("Cannot handle collection entries where uuid is null.");
}
// add it to the holder to avoid possible duplicates: key =
// uuid + action
entriesHolder.put(entryUuid + "|update", obj);
} else {
log.warn("Cannot handle collections where entries are not OpenmrsObject and have no Normalizers. Type was "
+ entry.getClass() + " in property " + ownerPropertyName + " in class " + owner.getClass());
// skip out early because we don't want to write any xml for it
// it was handled by the normal property writer hopefully
return;
}
}
// add on deletes
if (!"recreate".equals(action) && collection.getRole() != null) {
org.hibernate.persister.collection.CollectionPersister persister = ((org.hibernate.engine.SessionFactoryImplementor) getSessionFactory())
.getCollectionPersister(collection.getRole());
Iterator it = collection.getDeletes(persister, false);
if (it != null) {
while (it.hasNext()) {
Object entryDelete = it.next();
if (entryDelete instanceof OpenmrsObject) {
OpenmrsObject objDelete = (OpenmrsObject) entryDelete;
// attempt to retrieve entry uuid
String entryDeleteUuid = objDelete.getUuid();
if (entryDeleteUuid == null) {
entryDeleteUuid = fetchUuid(objDelete);
if (log.isDebugEnabled()) {
log.debug("Entry uuid was null, attempted to fetch uuid with the following results");
log.debug("Entry type:" + entryDeleteUuid.getClass().getName() + ",uuid:"
+ entryDeleteUuid);
}
}
// well, this is messed up: have an instance of
// OpenmrsObject but has no uuid
if (entryDeleteUuid == null) {
log.error("Cannot handle collection delete entries where uuid is null.");
throw new CallbackException("Cannot handle collection delete entries where uuid is null.");
}
// add it to the holder to avoid possible
// duplicates: key = uuid + action
// also, only add if there is no update action for the same object: see SYNC-280
if (!entriesHolder.containsKey(entryDeleteUuid + "|update")) {
entriesHolder.put(entryDeleteUuid + "|delete", objDelete);
}
} else {
// TODO: more debug info
log.warn("Cannot handle collections where entries are not OpenmrsObject and have no Normalizers!");
// skip out early because we don't want to write any
// xml for it. it
// was handled by the normal property writer
// hopefully
return;
}
}
}
}
/*
* Create SyncItem and store change in SyncRecord kept in
* ThreadLocal. note: when making SyncItemKey, make it a composite
* string of uuid + prop. name to avoid collisions with updates to
* parent object or updates to more than one collection on same
* owner
*/
// Setup the serialization data structures to hold the state
Record xml = pkg.createRecordForWrite(collection.getClass().getName());
Item entityItem = xml.getRootItem();
// serialize owner info: we will need type, prop name where collection
// goes, and owner uuid
Item item = xml.createItem(entityItem, "owner");
item.setAttribute("type", this.getType(owner));
item.setAttribute("properyName", ownerPropertyName);
item.setAttribute("action", action);
item.setAttribute("uuid", owner.getUuid());
// build out the xml for the item content
Boolean hasNoAutomaticPrimaryKey = null;
String type = null;
for (String entryKey : entriesHolder.keySet()) {
OpenmrsObject entryObject = entriesHolder.get(entryKey);
if (type == null) {
type = this.getType(entryObject);
hasNoAutomaticPrimaryKey = SyncUtil.hasNoAutomaticPrimaryKey(type);
}
Item temp = xml.createItem(entityItem, "entry");
temp.setAttribute("type", type);
temp.setAttribute("action", entryKey.substring(entryKey.indexOf('|') + 1));
temp.setAttribute("uuid", entryObject.getUuid());
if (hasNoAutomaticPrimaryKey) {
temp.setAttribute("primaryKey", getSyncService().getPrimaryKey(entryObject));
}
}
SyncItem syncItem = new SyncItem();
syncItem.setKey(new SyncItemKey<String>(owner.getUuid() + "|" + ownerPropertyName, String.class));
syncItem.setState(SyncItemState.UPDATED);
syncItem.setContainedType(collection.getClass());
syncItem.setContent(xml.toStringAsDocumentFragement());
getSyncRecord().addOrRemoveAndAddItem(syncItem);
getSyncRecord().addContainedClass(owner.getClass().getName());
// do the original uuid dance, same as in packageObject
if (getSyncRecord().getOriginalUuid() == null || "".equals(getSyncRecord().getOriginalUuid())) {
getSyncRecord().setOriginalUuid(originalRecordUuid);
}
}
catch (Exception ex) {
log.error("Error processing Persistent collection, see callstack and inner expection", ex);
throw new CallbackException("Error processing Persistent collection, see callstack and inner expection.", ex);
}
}
/**
* Returns string representation of type for given object. The main idea is to strip off the
* hibernate proxy info, if it happens to be present.
*
* @param obj object
* @return
*/
private String getType(Object obj) {
// be defensive about it
if (obj == null) {
throw new CallbackException("Error trying to determine type for object; object is null.");
}
Object concreteObj = obj;
if (obj instanceof org.hibernate.proxy.HibernateProxy) {
concreteObj = ((HibernateProxy) obj).getHibernateLazyInitializer().getImplementation();
}
return concreteObj.getClass().getName();
}
/**
* Sets the originating uuid for the sync record. This is done once per Tx; else we may end up
* with an empty string. NOTE: This code is needed because we need for entity to know if it is
* genuine local change or it is coming from the ingest code. This is what original_uuid field
* in sync_journal is used for. The way sync record uuids from ingest code are passed to the
* interceptor is by calling this method: the *1st* thing ingest code will do when processing
* changes is to issue this call to let interceptor know we are about to process ingest changes.
* This is done so that the intercetor can pull out the 'original' record uuid associated with
* the change to the entity that is being processed. Since ingest and interceptor do not have
* direct reference to each other, there is no simple way to pass this info directly. Note that
* this technique *relies* on the fact that syncRecordHolder is ThreadLocal; in other words, the
* uuid is passed and stored on the 'stack' by using thread local storage to ensure that
* multiple calling worker threads that maybe ingesting records concurrently are storring their
* respective record state locally. More: read javadoc on SyncRecord to see what
* SyncRecord.OriginalUuid is and how it is used.
*
* @param originalRecordUuid String representing value of orig record uuid
*/
public static void setOriginalRecordUuid(String originalRecordUuid) {
String currentValue = getSyncRecord().getOriginalUuid();
if (currentValue == null || "".equals(currentValue)) {
getSyncRecord().setOriginalUuid(originalRecordUuid);
}
else {
if (!currentValue.equals(originalRecordUuid)) {
throw new SyncException("originalRecordUuid is already set to a different value than expected");
}
}
}
/**
* Adds syncItem to pending sync record for the patient stub necessary to handle new patient
* from the existing user scenario. See {@link SyncSubclassStub} class comments for detailed description
* of how this works.
*
* @see SyncSubclassStub
*/
public static void addSyncItemForSubclassStub(SyncSubclassStub stub) {
try {
// Setup the serialization data structures to hold the state
Package pkg = new Package();
String className = stub.getClass().getName();
Record xml = pkg.createRecordForWrite(className);
Item parentItem = xml.getRootItem();
Item item = null;
//uuid
item = xml.createItem(parentItem, "uuid");
item.setAttribute("type", stub.getUuid().getClass().getName());
xml.createText(item, stub.getUuid());
//requiredColumnNames
item = xml.createItem(parentItem, "requiredColumnNames");
item.setAttribute("type", "java.util.List<java.lang.String>");
String value = "[";
for (int x=0; x < stub.getRequiredColumnNames().size(); x++) {
if (x != 0)
value += ",";
//value += "\"" + stub.getRequiredColumnNames().get(x) + "\"";
value += stub.getRequiredColumnNames().get(x);
}
value += "]";
xml.createText(item, value);
//requiredColumnValues
item = xml.createItem(parentItem, "requiredColumnValues");
item.setAttribute("type", "java.util.List<java.lang.String>");
value = "[";
for (int x=0; x < stub.getRequiredColumnValues().size(); x++) {
String columnvalue = stub.getRequiredColumnValues().get(x);
if (x != 0)
value += ",";
value += columnvalue;
}
value += "]";
xml.createText(item, value);
//requiredColumnClasses
item = xml.createItem(parentItem, "requiredColumnClasses");
item.setAttribute("type", "java.util.List<java.lang.String>");
value = "[";
for (int x=0; x < stub.getRequiredColumnClasses().size(); x++) {
String columnvalue = stub.getRequiredColumnClasses().get(x);
if (x != 0)
value += ",";
//value += "\"" + columnvalue + "\"";
value += columnvalue;
}
value += "]";
xml.createText(item, value);
//parentTable
item = xml.createItem(parentItem, "parentTable");
item.setAttribute("type", stub.getParentTable().getClass().getName());
xml.createText(item, stub.getParentTable());
//parentTableId
item = xml.createItem(parentItem, "parentTableId");
item.setAttribute("type", stub.getParentTableId().getClass().getName());
xml.createText(item, stub.getParentTableId());
//subclassTable
item = xml.createItem(parentItem, "subclassTable");
item.setAttribute("type", stub.getSubclassTable().getClass().getName());
xml.createText(item, stub.getSubclassTable());
//subclassTableId
item = xml.createItem(parentItem, "subclassTableId");
item.setAttribute("type", stub.getSubclassTableId().getClass().getName());
xml.createText(item, stub.getSubclassTableId());
SyncItem syncItem = new SyncItem();
syncItem.setKey(new SyncItemKey<String>(stub.getUuid(), String.class));
syncItem.setState(SyncItemState.NEW);
syncItem.setContent(xml.toStringAsDocumentFragement());
syncItem.setContainedType(stub.getClass());
getSyncRecord().addItem(syncItem);
getSyncRecord().addContainedClass(stub.getClass().getName());
}
catch (SyncException syncEx) {
//just rethrow it
throw (syncEx);
}
catch (Exception e) {
throw (new SyncException("Unknow error while creating patient stub for patient uuid: " + stub.getUuid(), e));
}
return;
}
/**
* Utility method that recursively fetches the CollectionMetadata for the specified collection
* property and class from given SessionFactory object
*
* @param clazz the class in which the collection is defined
* @param collPropertyName the collection's property name
* @param sf SessionFactory object
* @return the CollectionMetadata if any
*/
private CollectionMetadata getCollectionMetadata(Class<?> clazz, String collPropertyName, SessionFactory sf) {
CollectionMetadata cmd = sf.getCollectionMetadata(clazz.getName() + "." + collPropertyName);
//Recursively check if there is collection metadata for the superclass
if (cmd == null && clazz.getSuperclass() != null && !Object.class.equals(clazz.getSuperclass())) {
return getCollectionMetadata(clazz.getSuperclass(), collPropertyName, sf);
}
return cmd;
}
/**
* @return the existing sync record for the current thread, creating a new sync record for the thread if none yet exists,
*/
private static SyncRecord getSyncRecord() {
SyncRecord syncRecord = syncRecordHolder.get();
if (syncRecord == null) {
syncRecord = new SyncRecord();
syncRecordHolder.set(syncRecord);
}
return syncRecord;
}
/**
* @return the syncService
*/
private SyncService getSyncService() {
return Context.getService(SyncService.class);
}
/**
* @return the sessionFactory
*/
private SessionFactory getSessionFactory() {
return (SessionFactory) context.getBean("sessionFactory");
}
/**
* From Spring docs: There might be a single instance of Interceptor for a SessionFactory, or a
* new instance might be specified for each Session. Whichever approach is used, the interceptor
* must be serializable if the Session is to be serializable. This means that
* SessionFactory-scoped interceptors should implement readResolve().
*/
private Object readResolve() throws ObjectStreamException {
return getInstance();
}
/**
* @see ApplicationContextAware#setApplicationContext(ApplicationContext)
*/
public void setApplicationContext(ApplicationContext context) throws BeansException {
this.context = context;
}
/**
* Helper container class to store type/value tuple for a given object property. Utilized during
* serialization of intercepted entity changes.
*
* @see HibernateSyncInterceptor#packageObject(org.openmrs.OpenmrsObject, Object[], String[], org.hibernate.type.Type[], java.io.Serializable, org.openmrs.module.sync.SyncItemState)
*/
protected class PropertyClassValue {
String clazz;
String value;
public PropertyClassValue(String clazz, String value) {
this.clazz = clazz;
this.value = value;
}
public String getClazz() {
return clazz;
}
public String getValue() {
return value;
}
}
}