org.openmrs.module.sync.api.db.hibernate.HibernateSyncDAO.java Source code

Java tutorial

Introduction

Here is the source code for org.openmrs.module.sync.api.db.hibernate.HibernateSyncDAO.java

Source

/**
 * 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 java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.Reader;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.Blob;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Criteria;
import org.hibernate.FlushMode;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Expression;
import org.hibernate.criterion.MatchMode;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.hibernate.engine.SessionFactoryImplementor;
import org.hibernate.exception.ConstraintViolationException;
import org.hibernate.impl.CriteriaImpl;
import org.hibernate.impl.SessionImpl;
import org.hibernate.loader.OuterJoinLoader;
import org.hibernate.loader.criteria.CriteriaLoader;
import org.hibernate.persister.entity.OuterJoinLoadable;
import org.openmrs.GlobalProperty;
import org.openmrs.OpenmrsObject;
import org.openmrs.api.context.Context;
import org.openmrs.api.db.DAOException;
import org.openmrs.module.sync.SyncClass;
import org.openmrs.module.sync.SyncConstants;
import org.openmrs.module.sync.SyncSubclassStub;
import org.openmrs.module.sync.SyncRecord;
import org.openmrs.module.sync.SyncRecordState;
import org.openmrs.module.sync.SyncStatistic;
import org.openmrs.module.sync.SyncUtil;
import org.openmrs.module.sync.api.db.SyncDAO;
import org.openmrs.module.sync.ingest.SyncImportRecord;
import org.openmrs.module.sync.ingest.SyncIngestException;
import org.openmrs.module.sync.server.RemoteServer;
import org.openmrs.module.sync.server.RemoteServerType;
import org.openmrs.module.sync.server.SyncServerRecord;
import org.openmrs.util.OpenmrsConstants;
import org.openmrs.util.OpenmrsUtil;
import org.springframework.util.StringUtils;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

public class HibernateSyncDAO implements SyncDAO {

    protected final Log log = LogFactory.getLog(getClass());

    /**
     * Hibernate session factory
     */
    private SessionFactory sessionFactory;

    public HibernateSyncDAO() {
    }

    /**
     * Set session Factory interceptor
     * 
     * @param sessionFactory
     */
    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#createSyncRecord(org.openmrs.module.sync.SyncRecord)
     */
    public void createSyncRecord(SyncRecord record) throws DAOException {
        if (record.getUuid() == null) {
            //TODO: Create Uuid if missing?
            throw new DAOException("SyncRecord must have a UUID");
        }

        Session session = sessionFactory.getCurrentSession();
        try {
            session.save(record);
        } catch (ConstraintViolationException e) {
            sessionFactory.getCurrentSession().clear();
            SyncRecord existingRecord = getSyncRecord(record.getUuid());
            if (existingRecord != null) {
                //compare the contents
                if (existingRecord.equals(record)) {
                    //this was an identical record getting re-saved, ignore the exception
                    return;
                }
            }
            throw e;
        }
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#updateSyncRecord(org.openmrs.module.sync.SyncRecord)
     */
    public void updateSyncRecord(SyncRecord record) throws DAOException {
        Session session = sessionFactory.getCurrentSession();
        session.saveOrUpdate(record);
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#deleteSyncRecord(org.openmrs.module.sync.SyncRecord)
     */
    public void deleteSyncRecord(SyncRecord record) throws DAOException {
        Session session = sessionFactory.getCurrentSession();
        session.delete(record);
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#createSyncImportRecord(org.openmrs.module.sync.engine.SyncImportRecord)
     */
    public void createSyncImportRecord(SyncImportRecord record) throws DAOException {
        if (record.getUuid() == null) {
            //TODO: Create Uuid if missing?
            throw new DAOException("SyncImportRecord must have a UUID");
        }
        Session session = sessionFactory.getCurrentSession();
        session.save(record);
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#updateSyncImportRecord(org.openmrs.module.sync.engine.SyncImportRecord)
     */
    public void updateSyncImportRecord(SyncImportRecord record) throws DAOException {
        Session session = sessionFactory.getCurrentSession();
        session.merge(record);
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#deleteSyncImportRecord(org.openmrs.module.sync.engine.SyncImportRecord)
     */
    public void deleteSyncImportRecord(SyncImportRecord record) throws DAOException {
        Session session = sessionFactory.getCurrentSession();
        session.delete(record);
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#deleteSyncImportRecordsByServer(java.lang.Integer)
     */
    public void deleteSyncImportRecordsByServer(Integer serverId) throws DAOException {
        Session session = sessionFactory.getCurrentSession();
        session.createSQLQuery("delete from sync_import where source_server_id =:serverId")
                .setInteger("serverId", serverId).executeUpdate();
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#getNextSyncRecord()
     */
    @SuppressWarnings("unchecked")
    public SyncRecord getFirstSyncRecordInQueue() throws DAOException {
        List<SyncRecord> result = sessionFactory.getCurrentSession().createCriteria(SyncRecord.class)
                .add(Restrictions.in("state",
                        new SyncRecordState[] { SyncRecordState.NEW, SyncRecordState.PENDING_SEND }))
                .addOrder(Order.asc("timestamp")).addOrder(Order.asc("recordId")).setFetchSize(1).list();

        if (result.size() < 1) {
            return null;
        } else {
            return result.get(0);
        }
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#getNextSyncRecord()
     */
    @SuppressWarnings("unchecked")
    public SyncRecord getLatestRecord() throws DAOException {
        List<Integer> result = sessionFactory.getCurrentSession().createCriteria(SyncRecord.class)
                .setProjection(Projections.max("recordId")).list();

        if (result.size() < 1) {
            return null;
        } else {
            Integer maxRecordId = result.get(0);
            return getSyncRecord(maxRecordId);
        }
    }

    @SuppressWarnings("unchecked")
    public SyncRecord getEarliestRecord(Date afterDate) throws DAOException {
        Criteria criteria = sessionFactory.getCurrentSession().createCriteria(SyncRecord.class)
                .setProjection(Projections.min("recordId"));

        if (afterDate != null)
            criteria.add(Restrictions.ge("timestamp", afterDate));

        List<Integer> result = criteria.list();

        if (result.size() < 1) {
            return null;
        } else {
            Integer minRecordId = result.get(0);
            return getSyncRecord(minRecordId);
        }
    }

    /**
     * @see org.openmrs.module.sync.api.SyncService#getNextRecord(SyncRecord)
     */
    public SyncRecord getNextRecord(SyncRecord record) {
        Criteria criteria = sessionFactory.getCurrentSession().createCriteria(SyncRecord.class);
        criteria.setProjection(Projections.min("recordId"));
        criteria.add(Restrictions.gt("recordId", record.getRecordId()));
        Object nextRecordId = criteria.uniqueResult();
        if (nextRecordId == null) {
            return null;
        }
        return getSyncRecord((Integer) nextRecordId);
    }

    /**
     * @see org.openmrs.module.sync.api.SyncService#getPreviousRecord(SyncRecord)
     */
    public SyncRecord getPreviousRecord(SyncRecord record) {
        Criteria criteria = sessionFactory.getCurrentSession().createCriteria(SyncRecord.class);
        criteria.setProjection(Projections.max("recordId"));
        criteria.add(Restrictions.lt("recordId", record.getRecordId()));
        Object prevRecordId = criteria.uniqueResult();
        if (prevRecordId == null) {
            return null;
        }
        return getSyncRecord((Integer) prevRecordId);
    }

    @SuppressWarnings("unchecked")
    public List<SyncRecord> getSyncRecords(String query) throws DAOException {
        return sessionFactory.getCurrentSession().createCriteria(SyncRecord.class)
                .add(Expression.or(Restrictions.like("items", query, MatchMode.ANYWHERE),
                        Restrictions.eq("originalUuid", query)))
                .addOrder(Order.desc("timestamp")).setMaxResults(250) // max number of records returned
                .list();
    }

    public SyncRecord getSyncRecord(Integer recordId) throws DAOException {
        return (SyncRecord) sessionFactory.getCurrentSession().createCriteria(SyncRecord.class)
                .add(Restrictions.eq("recordId", recordId)).uniqueResult();
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#getSyncRecord(java.lang.String)
     */
    public SyncRecord getSyncRecord(String uuid) throws DAOException {
        return (SyncRecord) sessionFactory.getCurrentSession().createCriteria(SyncRecord.class)
                .add(Restrictions.eq("uuid", uuid)).uniqueResult();
    }

    public SyncRecord getSyncRecordByOriginalUuid(String originalUuid) throws DAOException {
        return (SyncRecord) sessionFactory.getCurrentSession().createCriteria(SyncRecord.class)
                .add(Restrictions.eq("originalUuid", originalUuid)).uniqueResult();
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#getSyncImportRecord(java.lang.String)
     */
    public SyncImportRecord getSyncImportRecord(String uuid) throws DAOException {
        return (SyncImportRecord) sessionFactory.getCurrentSession().createCriteria(SyncImportRecord.class)
                .add(Restrictions.eq("uuid", uuid)).uniqueResult();
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#getSyncImportRecords(org.openmrs.module.sync.engine.SyncRecordState)
     */
    @SuppressWarnings("unchecked")
    public List<SyncImportRecord> getSyncImportRecords(SyncRecordState... state) throws DAOException {
        return sessionFactory.getCurrentSession().createCriteria(SyncImportRecord.class)
                .add(Restrictions.in("state", state)).addOrder(Order.asc("timestamp"))
                .addOrder(Order.asc("importId")).list();
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#getSyncRecords()
     */
    @SuppressWarnings("unchecked")
    public List<SyncRecord> getSyncRecords() throws DAOException {
        return sessionFactory.getCurrentSession().createCriteria(SyncRecord.class).addOrder(Order.asc("timestamp"))
                .addOrder(Order.asc("recordId")).list();
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#getSyncRecords(org.openmrs.module.sync.engine.SyncRecordState)
     */
    @SuppressWarnings("unchecked")
    public List<SyncRecord> getSyncRecords(SyncRecordState state) throws DAOException {
        return sessionFactory.getCurrentSession().createCriteria(SyncRecord.class)
                .add(Restrictions.eq("state", state)).addOrder(Order.asc("timestamp"))
                .addOrder(Order.asc("recordId")).list();
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#getSyncRecords(org.openmrs.module.sync.SyncRecordState[],
     *      boolean, java.lang.Integer, org.openmrs.module.sync.server.RemoteServer, java.lang.Integer)
     */
    @SuppressWarnings("unchecked")
    public List<SyncRecord> getSyncRecords(SyncRecordState[] states, boolean inverse, Integer maxSyncRecords,
            RemoteServer server, Integer firstRecordId) throws DAOException {
        if (maxSyncRecords == null) {
            maxSyncRecords = Integer.parseInt(SyncConstants.PROPERTY_NAME_MAX_RECORDS_DEFAULT);
        }

        Criteria criteria = sessionFactory.getCurrentSession().createCriteria(SyncRecord.class, "s");

        String column = "s.state";

        if (server != null) {
            criteria = criteria.createCriteria("serverRecords", "sr");
            criteria.add(Restrictions.eq("sr.syncServer", server));
            column = "sr.state";
        }

        if (inverse)
            criteria.add(Restrictions.not(Restrictions.in(column, states)));
        else
            criteria.add(Restrictions.in(column, states));

        if (firstRecordId != null)
            criteria.add(Restrictions.ge("s.recordId", firstRecordId));

        criteria.addOrder(Order.asc("s.timestamp"));
        criteria.addOrder(Order.asc("s.recordId"));

        // if the user sets -1 as the max records, don't restrict the number of records downloaded/transferred
        if (maxSyncRecords > 0)
            criteria.setMaxResults(maxSyncRecords);

        return criteria.list();
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#deleteSyncRecords(org.openmrs.module.sync.SyncRecordState[],
     *      java.util.Date)
     */
    public Integer deleteSyncRecords(SyncRecordState[] states, Date to) throws DAOException {
        List<String> stateStrings = new ArrayList<String>();
        for (SyncRecordState s : states) {
            stateStrings.add(s.name());
        }

        // no matter what kind of server this current server is (parent or child), the sync server records
        // must be either committed or not syncing in order to delete them
        String[] syncServerStates = new String[] { SyncRecordState.NOT_SUPPOSED_TO_SYNC.name(),
                SyncRecordState.COMMITTED.name() };

        // delete all rows in sync_server_id that are of the right state and are old
        Query deleteSSRQuery = sessionFactory.getCurrentSession().createSQLQuery(
                "delete from sync_server_record where state in (:states) and (select timestamp from sync_record sr where sr.record_id = sync_server_record.record_id) < :to");
        deleteSSRQuery.setParameterList("states", syncServerStates);
        deleteSSRQuery.setDate("to", to);
        Integer quantityDeleted = deleteSSRQuery.executeUpdate(); // this quantity isn't really used

        // if a sync_record now has zero sync_record_server rows, then that means all
        // the rows were deleted in the previous query and so the sync_record can also be deleted
        Query deleteQuery = sessionFactory.getCurrentSession().createSQLQuery(
                "delete from sync_record where (select count(*) from sync_server_record ssr where ssr.record_id = sync_record.record_id) = 0 and sync_record.timestamp <= :to and sync_record.state in (:states)");
        deleteQuery.setDate("to", to);
        deleteQuery.setParameterList("states", stateStrings);
        quantityDeleted = deleteQuery.executeUpdate();

        return quantityDeleted;
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#getSyncRecords(java.util.Date, java.util.Date,
     *      Integer, Integer)
     */
    @SuppressWarnings("unchecked")
    public List<SyncRecord> getSyncRecords(Date from, Date to, Integer firstRecordId, Integer numberToReturn,
            boolean oldestToNewest) throws DAOException {
        Criteria criteria = sessionFactory.getCurrentSession().createCriteria(SyncRecord.class);

        if (from != null)
            criteria.add(Restrictions.gt("timestamp", from)); // greater than

        if (to != null)
            criteria.add(Restrictions.le("timestamp", to)); // less-than or equal

        if (numberToReturn != null)
            criteria.setMaxResults(numberToReturn);

        if (oldestToNewest) {
            if (firstRecordId != null)
                criteria.add(Restrictions.ge("recordId", firstRecordId));

            criteria.addOrder(Order.asc("timestamp"));
            criteria.addOrder(Order.asc("recordId"));
        } else {
            if (firstRecordId != null)
                criteria.add(Restrictions.le("recordId", firstRecordId));

            criteria.addOrder(Order.desc("timestamp"));
            criteria.addOrder(Order.desc("recordId"));
        }

        return criteria.list();
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#getGlobalProperty(String propertyName)
     */
    public String getGlobalProperty(String propertyName) throws DAOException {

        if (propertyName == null)
            throw new DAOException("Cannot retrieve property with null property name.");

        GlobalProperty gp = (GlobalProperty) sessionFactory.getCurrentSession().get(GlobalProperty.class,
                propertyName);

        if (gp == null)
            return null;

        return gp.getPropertyValue();

    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#setGlobalProperty(String propertyName, String
     *      propertyValue)
     */
    public void setGlobalProperty(String propertyName, String propertyValue) throws DAOException {

        if (propertyName == null)
            throw new DAOException("Cannot set property with null property name.");

        Session session = sessionFactory.getCurrentSession();

        // try to look up the global property first so we use the same uuid for the gp
        GlobalProperty gp = (GlobalProperty) session.get(GlobalProperty.class, propertyName);
        if (gp == null) {
            // the gp doesn't exist, create a new one with a new uuid now
            gp = new GlobalProperty(propertyName, propertyValue);
            gp.setUuid(SyncUtil.generateUuid());
        } else {
            gp.setPropertyValue(propertyValue);
        }

        session.merge(gp);
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#saveRemoteServer(org.openmrs.module.sync.engine.RemoteServer)
     */
    public RemoteServer saveRemoteServer(RemoteServer server) throws DAOException {
        Session session = sessionFactory.getCurrentSession();
        return (RemoteServer) session.merge(server);
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#deleteRemoteServer(org.openmrs.module.sync.engine.RemoteServer)
     */
    public void deleteRemoteServer(RemoteServer server) throws DAOException {
        Session session = sessionFactory.getCurrentSession();
        deleteSyncImportRecordsByServer(server.getServerId());
        // trying to speed up the deletion process...
        session.createSQLQuery("delete from sync_server_record where server_id =:serverId")
                .setInteger("serverId", server.getServerId()).executeUpdate();
        session.delete(server);
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#getGlobalProperty(String propertyName)
     */
    public RemoteServer getRemoteServer(Integer serverId) throws DAOException {
        return (RemoteServer) sessionFactory.getCurrentSession().get(RemoteServer.class, serverId);
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#getGlobalProperty(String propertyName)
     */
    public RemoteServer getRemoteServer(String uuid) throws DAOException {
        return (RemoteServer) sessionFactory.getCurrentSession().createCriteria(RemoteServer.class)
                .add(Restrictions.eq("uuid", uuid)).uniqueResult();
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#getGlobalProperty(String propertyName)
     */
    public RemoteServer getRemoteServerByUsername(String username) throws DAOException {
        return (RemoteServer) sessionFactory.getCurrentSession().createCriteria(RemoteServer.class)
                .add(Restrictions.eq("childUsername", username)).uniqueResult();
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#getGlobalProperty(String propertyName)
     */
    @SuppressWarnings("unchecked")
    public List<RemoteServer> getRemoteServers() throws DAOException {
        return (List<RemoteServer>) sessionFactory.getCurrentSession().createCriteria(RemoteServer.class).list();
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#getGlobalProperty(String propertyName)
     */
    public RemoteServer getParentServer() throws DAOException {
        return (RemoteServer) sessionFactory.getCurrentSession().createCriteria(RemoteServer.class)
                .add(Restrictions.eq("serverType", RemoteServerType.PARENT)).uniqueResult();
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#saveSyncClass(org.openmrs.module.sync.SyncClass)
     */
    public void saveSyncClass(SyncClass syncClass) throws DAOException {
        Session session = sessionFactory.getCurrentSession();
        session.saveOrUpdate(syncClass);
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#deleteSyncClass(org.openmrs.module.sync.SyncClass)
     */
    public void deleteSyncClass(SyncClass syncClass) throws DAOException {
        Session session = sessionFactory.getCurrentSession();
        session.delete(syncClass);
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#getSyncClass(Integer)
     */
    public SyncClass getSyncClass(Integer syncClassId) throws DAOException {
        return (SyncClass) sessionFactory.getCurrentSession().get(SyncClass.class, syncClassId);
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#getSyncClasses()
     */
    @SuppressWarnings("unchecked")
    public List<SyncClass> getSyncClasses() throws DAOException {

        List<SyncClass> classes = (List<SyncClass>) sessionFactory.getCurrentSession()
                .createCriteria(SyncClass.class).addOrder(Order.asc("name")).list();

        if (classes == null && log.isWarnEnabled())
            log.warn("getSyncClasses is null.");

        return classes;
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#getSyncClassByName(String)
     */
    public SyncClass getSyncClassByName(String className) throws DAOException {
        Criteria crit = sessionFactory.getCurrentSession().createCriteria(SyncClass.class)
                .add(Expression.eq("name", className));

        return (SyncClass) crit.uniqueResult();
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#deleteOpenmrsObject(org.openmrs.synchronization.OpenmrsObject)
     */
    public void deleteOpenmrsObject(OpenmrsObject o) throws DAOException {
        sessionFactory.getCurrentSession().delete(o);
    }

    /**
     * Sets hibernate flush mode to org.hibernate.FlushMode.MANUAL.
     * 
     * @return true if the flush mode was set already manual
     * @see #isFlushModeManual()
     * @see org.hibernate.FlushMode
     * @see org.openmrs.module.sync.api.db.SyncDAO#setFlushModeManual()
     */
    public boolean setFlushModeManual() throws DAOException {
        boolean isManualAlready = isFlushModeManual();

        // don't need to set it if its already manual
        if (!isManualAlready)
            sessionFactory.getCurrentSession().setFlushMode(org.hibernate.FlushMode.MANUAL);

        return isManualAlready;
    }

    /**
     * Sets hibernate flush mode to org.hibernate.FlushMode.AUTO.
     * 
     * @see org.hibernate.FlushMode
     * @see org.openmrs.module.sync.api.db.SyncDAO#setFlushModeAutomatic()
     */
    public void setFlushModeAutomatic() throws DAOException {
        sessionFactory.getCurrentSession().setFlushMode(org.hibernate.FlushMode.AUTO);
    }

    /**
     * Returns true if the flush mode is currently set to manual
     * 
     * @return true if the flush mode is manual
     * @throws DAOException
     */
    public boolean isFlushModeManual() throws DAOException {
        return FlushMode.isManualFlushMode(sessionFactory.getCurrentSession().getFlushMode());
    }

    /**
     * Executes hibernate flush.
     * 
     * @see org.hibernate.Session#flush()
     * @see org.openmrs.module.sync.api.db.SyncDAO#flushSession()
     */
    public void flushSession() throws DAOException {
        sessionFactory.getCurrentSession().flush();
    }

    /**
     * Performs generic save of openmrs object using Hibernate session.saveorupdate.
     * 
     * @throws DAOException
     */
    public void saveOrUpdate(Object object) throws DAOException {
        sessionFactory.getCurrentSession().saveOrUpdate(object);
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#getSyncStatistics(java.util.Date, java.util.Date)
     */
    @SuppressWarnings("unchecked")
    public Map<RemoteServer, LinkedHashSet<SyncStatistic>> getSyncStatistics(Date fromDate, Date toDate)
            throws DAOException {

        //first get the list of remote servers and make map out of it
        List<RemoteServer> servers = this.getRemoteServers();

        Map<RemoteServer, LinkedHashSet<SyncStatistic>> map = new HashMap<RemoteServer, LinkedHashSet<SyncStatistic>>();

        String hqlChild = "select rs.nickname, ssr.state, count(*) "
                + "from RemoteServer rs join rs.serverRecords as ssr "
                + "where rs.serverId = :server_id and ssr.state  <> '"
                + SyncRecordState.NOT_SUPPOSED_TO_SYNC.toString() + "' " + "group by rs.nickname, ssr.state "
                + "order by nickname, state";

        String hqlParent = "select count(*) from SyncRecord where originalUuid = uuid and state <> '"
                + SyncRecordState.COMMITTED.toString() + "' and state <> '"
                + SyncRecordState.NOT_SUPPOSED_TO_SYNC.toString() + "'";

        //for each server configured, get its stats
        for (RemoteServer r : servers) {
            if (r.getServerType() == RemoteServerType.CHILD) {
                Query q = sessionFactory.getCurrentSession().createQuery(hqlChild);
                q.setParameter("server_id", r.getServerId());
                List<Object[]> rows = q.list();
                LinkedHashSet<SyncStatistic> props = new LinkedHashSet<SyncStatistic>();
                for (Object[] row : rows) {
                    SyncStatistic stat = new SyncStatistic(SyncStatistic.Type.SYNC_RECORD_COUNT_BY_STATE,
                            row[1].toString(), row[2]); //state/count
                    props.add(stat);
                }
                map.put(r, props);
            } else {
                //for parent servers, get the number of records in sync record
                Query q = sessionFactory.getCurrentSession().createQuery(hqlParent);
                Long count = (Long) q.uniqueResult();
                LinkedHashSet<SyncStatistic> props = new LinkedHashSet<SyncStatistic>();
                if (count != null) {
                    props.add(new SyncStatistic(SyncStatistic.Type.SYNC_RECORD_COUNT_BY_STATE, "AWAITING", count)); //count
                }
                map.put(r, props);
            }
        }

        return map;
    }

    /*
     * called at Openmrs sync parent server: exports the openmrs database to a
     * DDL output stream for sending it back to a new child node being created
     */
    public void exportChildDB(String uuidForChild, OutputStream os) throws DAOException {
        PrintStream out = new PrintStream(os);
        Set<String> tablesToSkip = new HashSet<String>();
        {
            tablesToSkip.add("hl7_in_archive");
            tablesToSkip.add("hl7_in_queue");
            tablesToSkip.add("hl7_in_error");
            tablesToSkip.add("formentry_archive");
            tablesToSkip.add("formentry_queue");
            tablesToSkip.add("formentry_error");
            tablesToSkip.add("sync_class");
            tablesToSkip.add("sync_import");
            tablesToSkip.add("sync_record");
            tablesToSkip.add("sync_server");
            tablesToSkip.add("sync_server_class");
            tablesToSkip.add("sync_server_record");
            // TODO: figure out which other tables to skip
            // tablesToSkip.add("obs");
            // tablesToSkip.add("concept");
            // tablesToSkip.add("patient");
        }
        List<String> tablesToDump = new ArrayList<String>();
        Session session = sessionFactory.getCurrentSession();
        String schema = (String) session.createSQLQuery("SELECT schema()").uniqueResult();
        log.warn("schema: " + schema);
        // Get all tables that we'll need to dump
        {
            Query query = session.createSQLQuery(
                    "SELECT tabs.table_name FROM INFORMATION_SCHEMA.TABLES tabs WHERE tabs.table_schema = '"
                            + schema + "'");
            for (Object tn : query.list()) {
                String tableName = (String) tn;
                if (!tablesToSkip.contains(tableName.toLowerCase()))
                    tablesToDump.add(tableName);
            }
        }
        log.warn("tables to dump: " + tablesToDump);

        String thisServerGuid = getGlobalProperty(SyncConstants.PROPERTY_SERVER_UUID);

        // Write the DDL Header as mysqldump does
        {
            out.println("-- ------------------------------------------------------");
            out.println("-- Database dump to create an openmrs child server");
            out.println("-- Schema: " + schema);
            out.println("-- Parent GUID: " + thisServerGuid);
            out.println("-- Parent version: " + OpenmrsConstants.OPENMRS_VERSION);
            out.println("-- ------------------------------------------------------");
            out.println("");
            out.println("/*!40101 SET CHARACTER_SET_CLIENT=utf8 */;");
            out.println("/*!40101 SET NAMES utf8 */;");
            out.println("/*!40103 SET TIME_ZONE='+00:00' */;");
            out.println("/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;");
            out.println("/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;");
            out.println("/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;");
            out.println("/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;");
            out.println("/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;");
            out.println("/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;");
            out.println("/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;");
            out.println("/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;");
            out.println("");
        }
        try {
            // JDBC way of doing this
            // Connection conn =
            // DriverManager.getConnection("jdbc:mysql://localhost/" + schema,
            // "test", "test");
            Connection conn = sessionFactory.getCurrentSession().connection();
            try {
                Statement st = conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY);

                // Get the create database statement
                ResultSet rs = st.executeQuery("SHOW CREATE DATABASE " + schema);
                for (String tableName : tablesToDump) {
                    out.println();
                    out.println("--");
                    out.println("-- Table structure for table `" + tableName + "`");
                    out.println("--");
                    out.println("DROP TABLE IF EXISTS `" + tableName + "`;");
                    out.println("SET @saved_cs_client     = @@character_set_client;");
                    out.println("SET character_set_client = utf8;");
                    rs = st.executeQuery("SHOW CREATE TABLE " + tableName);
                    while (rs.next()) {
                        out.println(rs.getString("Create Table") + ";");
                    }
                    out.println("SET character_set_client = @saved_cs_client;");
                    out.println();

                    {
                        out.println("-- Dumping data for table `" + tableName + "`");
                        out.println("LOCK TABLES `" + tableName + "` WRITE;");
                        out.println("/*!40000 ALTER TABLE `" + tableName + "` DISABLE KEYS */;");
                        boolean first = true;

                        rs = st.executeQuery("select * from " + tableName);
                        ResultSetMetaData md = rs.getMetaData();
                        int numColumns = md.getColumnCount();
                        int rowNum = 0;
                        boolean insert = false;

                        while (rs.next()) {
                            if (rowNum == 0) {
                                insert = true;
                                out.print("INSERT INTO `" + tableName + "` VALUES ");
                            }
                            ++rowNum;
                            if (first) {
                                first = false;
                            } else {
                                out.print(", ");
                            }
                            if (rowNum % 20 == 0) {
                                out.println();
                            }
                            out.print("(");
                            for (int i = 1; i <= numColumns; ++i) {
                                if (i != 1) {
                                    out.print(",");
                                }
                                if (rs.getObject(i) == null) {
                                    out.print("NULL");
                                } else {
                                    switch (md.getColumnType(i)) {
                                    case Types.VARCHAR:
                                    case Types.CHAR:
                                    case Types.LONGVARCHAR:
                                        out.print("'");
                                        out.print(
                                                rs.getString(i).replaceAll("\n", "\\\\n").replaceAll("'", "\\\\'"));
                                        out.print("'");
                                        break;
                                    case Types.BIGINT:
                                    case Types.DECIMAL:
                                    case Types.NUMERIC:
                                        out.print(rs.getBigDecimal(i));
                                        break;
                                    case Types.BIT:
                                        out.print(rs.getBoolean(i));
                                        break;
                                    case Types.INTEGER:
                                    case Types.SMALLINT:
                                    case Types.TINYINT:
                                        out.print(rs.getInt(i));
                                        break;
                                    case Types.REAL:
                                    case Types.FLOAT:
                                    case Types.DOUBLE:
                                        out.print(rs.getDouble(i));
                                        break;
                                    case Types.BLOB:
                                    case Types.VARBINARY:
                                    case Types.LONGVARBINARY:
                                        Blob blob = rs.getBlob(i);
                                        out.print("'");
                                        InputStream in = blob.getBinaryStream();
                                        while (true) {
                                            int b = in.read();
                                            if (b < 0) {
                                                break;
                                            }
                                            char c = (char) b;
                                            if (c == '\'') {
                                                out.print("\'");
                                            } else {
                                                out.print(c);
                                            }
                                        }
                                        out.print("'");
                                        break;
                                    case Types.CLOB:
                                        out.print("'");
                                        out.print(
                                                rs.getString(i).replaceAll("\n", "\\\\n").replaceAll("'", "\\\\'"));
                                        out.print("'");
                                        break;
                                    case Types.DATE:
                                        out.print("'" + rs.getDate(i) + "'");
                                        break;
                                    case Types.TIMESTAMP:
                                        out.print("'" + rs.getTimestamp(i) + "'");
                                        break;
                                    default:
                                        throw new RuntimeException("TODO: handle type code " + md.getColumnType(i)
                                                + " (name " + md.getColumnTypeName(i) + ")");
                                    }
                                }
                            }
                            out.print(")");
                        }
                        if (insert) {
                            out.println(";");
                            insert = false;
                        }

                        out.println("/*!40000 ALTER TABLE `" + tableName + "` ENABLE KEYS */;");
                        out.println("UNLOCK TABLES;");
                        out.println();
                    }
                }
            } finally {
                conn.close();
            }

            // Now we mark this as a child
            out.println("-- Now mark this as a child database");
            if (uuidForChild == null)
                uuidForChild = SyncUtil.generateUuid();
            out.println("update global_property set property_value = '" + uuidForChild + "' where property = '"
                    + SyncConstants.PROPERTY_SERVER_UUID + "';");

            // Write the footer of the DDL script
            {
                out.println("/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;");
                out.println("/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;");
                out.println("/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;");
                out.println("/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;");
                out.println("/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;");
                out.println("/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;");
                out.println("/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;");
                out.println("/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;");
            }
            out.flush();
            out.close();
        } catch (IOException ex) {
            log.error("IOException", ex);

        } catch (SQLException ex) {
            log.error("SQLException", ex);
        }
    }

    /*
     * Called at Openmrs sync child: imports the DDL backup of the parent server
     * from an input stream generated from the parent DB @Impl: Reads the DDL
     * statement line by line and updates the child DB
     */
    public void importParentDB(InputStream in) {
        try {
            Connection conn = sessionFactory.getCurrentSession().connection();
            Statement statement = conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY);
            BufferedReader reader = new BufferedReader(new InputStreamReader(in));
            String line;
            StringBuffer query = new StringBuffer();
            boolean queryEnds = false;

            while ((line = reader.readLine()) != null) {
                if ((line.startsWith("#") || line.startsWith("--"))) {
                    continue;
                }
                queryEnds = line.endsWith(";");
                query.append(line);
                if (queryEnds) {
                    try {
                        statement.execute(query.toString());
                    } catch (SQLException ex) {
                        log.warn(ex);
                        ex.printStackTrace();
                    }
                    query.setLength(0);
                }
            }
            in.close();

        } catch (IOException ex) {
            log.warn(ex);
            ex.printStackTrace();
        } catch (SQLException ex) {
            log.warn(ex);
            ex.printStackTrace();
        }
    }

    public void generateDataFile(File outFile, String[] ignoreTables) {
        // TODO totally breaks if someone isn't using mysql as the backend
        // TODO get custom location of mysql instead of just relying on path?
        // TODO make this linux compatible
        String[] props = getConnectionProperties();
        String username = props[0];
        String password = props[1];
        String database = props[2];
        String host = props[3];
        String port = props[4];
        try {
            if (!outFile.exists())
                outFile.createNewFile();
        } catch (IOException io) {
            log.warn(io.toString());
        }

        List<String> commands = new ArrayList<String>();
        commands.add("mysqldump");
        commands.add("-u" + username);
        commands.add("-p" + password);
        commands.add("-h" + host);
        commands.add("-P" + port);
        commands.add("--hex-blob");
        commands.add("-q");
        commands.add("-e");
        commands.add("--single-transaction");
        commands.add("-r");
        commands.add(outFile.getAbsolutePath());
        commands.add(database);

        // mark the tables to ignore
        for (String table : ignoreTables) {
            table = table.trim();
            if (StringUtils.hasLength(table)) {
                commands.add("--ignore-table");
                commands.add(database + "." + table);
            }
        }

        String output;
        if (OpenmrsConstants.UNIX_BASED_OPERATING_SYSTEM)
            output = execCmd(outFile.getParentFile(), commands.toArray(new String[] {}));
        else
            output = execCmd(null, commands.toArray(new String[] {}));

        if (output != null && output.length() > 0) {
            log.debug("Exec called: " + Arrays.asList(commands));
            log.debug("Output of exec: " + output);
        }

    }

    /**
     * Auto generated method comment
     * 
     * @return
     */
    private String[] getConnectionProperties() {
        Properties props = Context.getRuntimeProperties();

        // username, password, database, host, port
        String[] connProps = { "test", "test", "openmrs", "localhost", "3306" };

        String username = (String) props.get("database.username");
        if (username == null)
            username = (String) props.get("connection.username");
        if (username != null)
            connProps[0] = username;

        String password = (String) props.get("database.password");
        if (password == null)
            password = (String) props.get("connection.password");
        if (password != null)
            connProps[1] = password;
        // get database name
        String database = "openmrs";
        String host = "localhost";
        String port = "3306";
        String connectionUrl = (String) props.get("connection.url");
        if (connectionUrl == null)
            connectionUrl = (String) props.get("connection.url");
        if (connectionUrl != null) {
            //jdbc:mysql://localhost:3306/openmrs
            //jdbc:mysql:mxj://127.0.0.1:3317/openmrs?
            //Assuming the last full colon will be that before the port
            int lastColonPos = connectionUrl.lastIndexOf(':');
            int slash = connectionUrl.indexOf('/', lastColonPos);
            int qmark = connectionUrl.indexOf('?', lastColonPos);
            database = connectionUrl.substring(slash + 1, qmark != -1 ? qmark : connectionUrl.length());
            connProps[2] = database;

            //Assuming that port is explicitly set
            int doubleSlashPos = connectionUrl.indexOf("://");
            host = connectionUrl.substring(doubleSlashPos + 3, lastColonPos);
            connProps[3] = host;

            port = connectionUrl.substring(lastColonPos + 1, slash);
            connProps[4] = port;
        }

        return connProps;
    }

    /**
     * @param cmdWithArguments
     * @param wd
     * @return
     */
    private String execCmd(File wd, String[] cmdWithArguments) {
        log.debug("executing command: " + Arrays.toString(cmdWithArguments));

        StringBuffer out = new StringBuffer();
        try {
            Process p = (wd != null) ? Runtime.getRuntime().exec(cmdWithArguments, null, wd)
                    : Runtime.getRuntime().exec(cmdWithArguments);

            out.append("Normal cmd output:\n");
            Reader reader = new InputStreamReader(p.getInputStream());
            BufferedReader input = new BufferedReader(reader);
            int readChar = 0;
            while ((readChar = input.read()) != -1) {
                out.append((char) readChar);
            }
            input.close();
            reader.close();

            out.append("ErrorStream cmd output:\n");
            reader = new InputStreamReader(p.getErrorStream());
            input = new BufferedReader(reader);
            readChar = 0;
            while ((readChar = input.read()) != -1) {
                out.append((char) readChar);
            }
            input.close();
            reader.close();

            Integer exitValue = p.waitFor();

            log.debug("Process exit value: " + exitValue);

        } catch (Exception e) {
            log.error("Error while executing command: '" + cmdWithArguments + "'", e);
        }

        log.debug("execCmd output: \n" + out.toString());

        return out.toString();
    }

    public void execGeneratedFile(File generatedDataFile) {
        // TODO this depends on mysql being on the path
        // TODO fix this so that queries are parsed out and run linebyline?

        String[] props = getConnectionProperties();
        String username = props[0];
        String password = props[1];
        String database = props[2];
        String host = props[3];
        String port = props[4];

        String path = generatedDataFile.getAbsolutePath();
        path = path.replace("\\", "/");
        // replace windows file separator with
        // forward slash

        String[] commands = { "mysql", "-e", "source " + path, "-f", "-u" + username, "-p" + password, "-h" + host,
                "-P" + port, "-D" + database };
        String output;
        if (OpenmrsConstants.UNIX_BASED_OPERATING_SYSTEM)
            output = execCmd(generatedDataFile.getParentFile(), commands);
        else
            output = execCmd(null, commands);

        if (output != null && output.length() > 0) {
            log.error("Exec call: " + Arrays.asList(commands));
            log.error("Output of exec: " + output);
        }
    }

    public <T extends OpenmrsObject> T getOpenmrsObjectByUuid(Class<T> clazz, String uuid) {
        Criteria crit = sessionFactory.getCurrentSession().createCriteria(clazz);
        crit.add(Restrictions.eq("uuid", uuid));
        return (T) crit.uniqueResult();
    }

    public <T extends OpenmrsObject> T getOpenmrsObjectByPrimaryKey(String classname, Object primaryKey) {
        Criteria crit;
        try {
            crit = sessionFactory.getCurrentSession().createCriteria(Context.loadClass(classname));
            crit.add(Restrictions.idEq(primaryKey));
            return (T) crit.uniqueResult();
        } catch (ClassNotFoundException e) {
            log.warn("getOpenmrsObjectByPrimaryKey couldn't find class: " + classname, e);
            return null;
        }
    }

    public <T extends OpenmrsObject> String getUuidForOpenmrsObject(Class<T> clazz, String id) {
        Criteria crit = sessionFactory.getCurrentSession().createCriteria(clazz);
        crit.add(Restrictions.idEq(id));
        crit.setProjection(Projections.property("uuid"));
        List<Object[]> rows = crit.list();
        Object[] rowOne = rows.get(0);
        return (String) rowOne[0]; // get the first column of the first row
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#processCollection(java.lang.Class,
     *      java.lang.String, java.lang.String)
     */
    public void processCollection(Class collectionType, String incoming, String originalRecordUuid)
            throws Exception {

        OpenmrsObject owner = null;
        String ownerClassName = null;
        String ownerCollectionPropertyName = null;
        String ownerUuid = null;
        String ownerCollectionAction = null; //is this coll update or recreate?
        NodeList nodes = null;
        Set entries = null;
        int i = 0;
        boolean needsRecreate = false;

        //first find out what kid of set we are dealing with:
        //Hibernate PersistentSortedSet == TreeSet, note this is derived from PersistentSet so we have to test for it first
        //Hibernate PersistentSet == HashSet
        if (!org.hibernate.collection.PersistentSet.class.isAssignableFrom(collectionType)) {
            //don't know how to process this collection type
            log.error("Do not know how to process this collection type: " + collectionType.getName());
            throw new SyncIngestException(SyncConstants.ERROR_ITEM_BADXML_MISSING, null, incoming, null);
        }

        //next, pull out the owner node and get owner instance: 
        //we need reference to owner object before we start messing with collection entries
        nodes = SyncUtil.getChildNodes(incoming);
        if (nodes == null) {
            throw new SyncIngestException(SyncConstants.ERROR_ITEM_BADXML_MISSING, null, incoming, null);
        }
        for (i = 0; i < nodes.getLength(); i++) {
            if ("owner".equals(nodes.item(i).getNodeName())) {
                //pull out collection owner info: class name of owner, its uuid, and name of poperty on owner that holds this collection
                ownerClassName = ((Element) nodes.item(i)).getAttribute("type");
                ownerCollectionPropertyName = ((Element) nodes.item(i)).getAttribute("properyName");
                ownerCollectionAction = ((Element) nodes.item(i)).getAttribute("action");
                ownerUuid = ((Element) nodes.item(i)).getAttribute("uuid");
                break;
            }
        }
        if (ownerUuid == null) {
            log.error("Owner uuid is null while processing collection.");
            throw new SyncIngestException(SyncConstants.ERROR_ITEM_BADXML_MISSING, null, incoming, null);
        }
        owner = (OpenmrsObject) SyncUtil.getOpenmrsObj(ownerClassName, ownerUuid);

        //we didn't get the owner record: throw an exception
        //TODO: in future, when we have conflict resolution, this may be handled differently
        if (owner == null) {
            log.error("Cannot retrieve the collection's owner object.");
            log.error("Owner info: " + "\nownerClassName:" + ownerClassName + "\nownerCollectionPropertyName:"
                    + ownerCollectionPropertyName + "\nownerCollectionAction:" + ownerCollectionAction
                    + "\nownerUuid:" + ownerUuid);
            throw new SyncIngestException(SyncConstants.ERROR_ITEM_BADXML_MISSING, null, incoming, null);
        }

        //NOTE: we cannot just new up a collection and assign to parent:
        //if hibernate mapping has cascade deletes, it will orphan existing collection and hibernate will throw error
        //to that effect: "A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance"
        //*only* if this is recreate; clear up the existing collection and start over
        Method m = null;
        m = SyncUtil.getGetterMethod(owner.getClass(), ownerCollectionPropertyName);
        if (m == null) {
            log.error(
                    "Cannot retrieve getter method for ownerCollectionPropertyName:" + ownerCollectionPropertyName);
            log.error("Owner info: " + "\nownerClassName:" + ownerClassName + "\nownerCollectionPropertyName:"
                    + ownerCollectionPropertyName + "\nownerCollectionAction:" + ownerCollectionAction
                    + "\nownerUuid:" + ownerUuid);
            throw new SyncIngestException(SyncConstants.ERROR_ITEM_BADXML_MISSING, null, incoming, null);
        }
        entries = (Set) m.invoke(owner, (Object[]) null);

        /*Two instances where even after this we may need to create a new collection:
         * a) when collection is lazy=false and it is newly created; then asking parent for it will
         * not return new & empty proxy, it will return null
         * b) Special recreate logic:
         * if fetched owner instance has nothing attached, then it is safe to just create brand new collection
         * and assign it to owner without worrying about getting orphaned deletes error
         * if owner has something attached, then we process recreate as delete/update; 
         * that is clear out the existing entries and then proceed to add ones received via sync. 
         * This code essentially mimics hibernate org.hibernate.engine.Collections.prepareCollectionForUpdate()
         * implementation. 
         * NOTE: The unfortunate bi-product of this approach is that this series of events will not produce 
         * 'recreate' event in the interceptor: thus parent's sync journal entries will look slightly diferently 
         * from what child was sending up: child sent up single 'recreate' collection action however
         * parent will instead have single 'update' with deletes & updates in it. Presumably, this is a distinction
         * without a difference.
         */

        if (entries == null) {
            if (org.hibernate.collection.PersistentSortedSet.class.isAssignableFrom(collectionType)) {
                needsRecreate = true;
                entries = new TreeSet();
            } else if (org.hibernate.collection.PersistentSet.class.isAssignableFrom(collectionType)) {
                needsRecreate = true;
                entries = new HashSet();
            }
        }

        if (entries == null) {
            log.error("Was not able to retrieve reference to the collection using owner object.");
            log.error("Owner info: " + "\nownerClassName:" + ownerClassName + "\nownerCollectionPropertyName:"
                    + ownerCollectionPropertyName + "\nownerCollectionAction:" + ownerCollectionAction
                    + "\nownerUuid:" + ownerUuid);
            throw new SyncIngestException(SyncConstants.ERROR_ITEM_BADXML_MISSING, null, incoming, null);
        }

        //clear existing entries before adding new ones:
        if ("recreate".equals(ownerCollectionAction)) {
            entries.clear();
        }

        //now, finally process nodes, phew!!
        for (i = 0; i < nodes.getLength(); i++) {
            if ("entry".equals(nodes.item(i).getNodeName())) {
                String entryClassName = ((Element) nodes.item(i)).getAttribute("type");
                String entryUuid = ((Element) nodes.item(i)).getAttribute("uuid");
                String entryAction = ((Element) nodes.item(i)).getAttribute("action");
                Object entry = SyncUtil.getOpenmrsObj(entryClassName, entryUuid);

                // objects like Privilege, Role, and GlobalProperty might have different
                // uuids for different objects
                if (entry == null && SyncUtil.hasNoAutomaticPrimaryKey(entryClassName)) {
                    String key = ((Element) nodes.item(i)).getAttribute("primaryKey");
                    entry = getOpenmrsObjectByPrimaryKey(entryClassName, key);
                }

                if (entry == null) {
                    // blindly ignore this entry if it doesn't exist and we're trying to delete it
                    if (!"delete".equals(entryAction)) {
                        //the object not found: most likely cause here is data collision

                        log.error("Was not able to retrieve reference to the collection entry object by uuid.");
                        log.error("Entry info: " + "\nentryClassName: " + entryClassName + "\nentryUuid: "
                                + entryUuid + "\nentryAction: " + entryAction);
                        log.error("Sync record original uuid: " + originalRecordUuid);
                        throw new SyncIngestException(SyncConstants.ERROR_ITEM_UUID_NOT_FOUND,
                                ownerClassName + " missing " + entryClassName + "," + entryUuid, incoming, null);
                    }
                } else if ("update".equals(entryAction)) {
                    if (!OpenmrsUtil.collectionContains(entries, entry)) {
                        entries.add(entry);
                    }
                } else if ("delete".equals(entryAction)) {
                    OpenmrsUtil.collectionContains(entries, entry);

                    if (!entries.remove(entry)) {
                        //couldn't find entry in collection: hmm, bad implementation of equals?
                        //fall back to trying to find the item in entries by uuid
                        OpenmrsObject toBeRemoved = null;
                        for (Object o : entries) {
                            if (o instanceof OpenmrsObject) {
                                if (entryUuid.equals(((OpenmrsObject) o).getUuid())) {
                                    toBeRemoved = (OpenmrsObject) o;
                                    break;
                                }
                            }
                        }
                        if (toBeRemoved == null) {
                            //the item to be removed was not located in the collection: log it for reference and continue
                            log.warn("Was not able to process collection entry delete.");
                            log.warn("Owner info: " + "\nownerClassName:" + ownerClassName
                                    + "\nownerCollectionPropertyName:" + ownerCollectionPropertyName
                                    + "\nownerCollectionAction:" + ownerCollectionAction + "\nownerUuid:"
                                    + ownerUuid);
                            log.warn("entry info: " + "\nentryClassName:" + entryClassName + "\nentryUuid:"
                                    + entryUuid);
                            log.warn("Sync record original uuid: " + originalRecordUuid);
                        } else {
                            //finally, remove it from the collection
                            entries.remove(toBeRemoved);
                        }
                    }

                } else {
                    log.error("Unknown collection entry action, action was: " + entryAction);
                    throw new SyncIngestException(SyncConstants.ERROR_ITEM_NOT_COMMITTED, ownerClassName, incoming,
                            null);
                }
            }
        }

        /*
         * Pass the original uuid to interceptor: this will prevent the change
         * from being sent back to originating server. 
         */
        HibernateSyncInterceptor.setOriginalRecordUuid(originalRecordUuid);

        //assign collection back to the owner if it is recreated
        if (needsRecreate) {
            SyncUtil.setProperty(owner, ownerCollectionPropertyName, entries);
        }

        //finally, trigger update
        try {
            //no need to mess around with precommit actions for collections, at least
            //at this point
            SyncUtil.updateOpenmrsObject(owner, ownerClassName, ownerUuid);
        } catch (Exception e) {
            log.error("Unexpected exception occurred while processing hibernate collections", e);
            throw new SyncIngestException(SyncConstants.ERROR_ITEM_NOT_COMMITTED, ownerClassName, incoming, null);
        }
    }

    /* For full description of how this works read class comments for 
     * {@link SyncSubclassStub}.
     * 
     * (non-Javadoc)
     * @see org.openmrs.module.sync.api.db.SyncDAO#processSyncSubclassStub(org.openmrs.module.sync.SyncSubclassStub)
     */
    public void processSyncSubclassStub(SyncSubclassStub stub) {
        Connection connection = sessionFactory.getCurrentSession().connection();

        boolean stubInsertNeeded = false;
        PreparedStatement ps = null;
        int internalDatabaseId = 0;

        // check if there is a row with a matching person record and no patient record 
        try {
            // becomes something like "select person_id from person where uuid = x" or "select concept_id from concept where uuid = x"
            ps = connection.prepareStatement(
                    "SELECT " + stub.getParentTableId() + " FROM " + stub.getParentTable() + " WHERE uuid = ?");
            ps.setString(1, stub.getUuid());
            ps.execute();
            ResultSet rs = ps.getResultSet();
            if (rs.next()) {
                stubInsertNeeded = true;
                internalDatabaseId = rs.getInt(stub.getParentTableId());
            } else {
                stubInsertNeeded = false;
            }

            //this should get no rows
            ps = connection.prepareStatement("SELECT " + stub.getSubclassTableId() + " FROM "
                    + stub.getSubclassTable() + " WHERE " + stub.getSubclassTableId() + " = ?");
            ps.setInt(1, internalDatabaseId);
            ps.execute();
            if (ps.getResultSet().next()) {
                stubInsertNeeded = false;
            }
            ps.close();
            ps = null;

        } catch (SQLException e) {
            log.error(
                    "Error while trying to see if this person is a patient already (or concept is a concept numeric already)",
                    e);
        }
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e) {
                log.error("Error generated while closing statement", e);
            }
        }

        if (stubInsertNeeded) {
            try {
                //insert the stub
                String sql = "INSERT INTO " + stub.getSubclassTable() + " (" + stub.getSubclassTableId()
                        + "COLUMNNAMEGOESHERE) VALUES (?COLUMNVALUEGOESHERE)";

                if (CollectionUtils.isNotEmpty(stub.getRequiredColumnNames())
                        && CollectionUtils.isNotEmpty(stub.getRequiredColumnValues())
                        && CollectionUtils.isNotEmpty(stub.getRequiredColumnClasses())) {
                    for (int x = 0; x < stub.getRequiredColumnNames().size(); x++) {
                        String column = stub.getRequiredColumnNames().get(x);
                        sql = sql.replace("COLUMNNAMEGOESHERE", ", " + column + "COLUMNNAMEGOESHERE");
                        sql = sql.replace("COLUMNVALUEGOESHERE", ", ?COLUMNVALUEGOESHERE");
                    }
                }

                sql = sql.replace("COLUMNNAMEGOESHERE", "");
                sql = sql.replace("COLUMNVALUEGOESHERE", "");

                ps = connection.prepareStatement(sql);

                ps.setInt(1, internalDatabaseId);

                if (CollectionUtils.isNotEmpty(stub.getRequiredColumnNames())
                        && CollectionUtils.isNotEmpty(stub.getRequiredColumnValues())
                        && CollectionUtils.isNotEmpty(stub.getRequiredColumnClasses())) {

                    for (int x = 0; x < stub.getRequiredColumnValues().size(); x++) {
                        String value = stub.getRequiredColumnValues().get(x);
                        String className = stub.getRequiredColumnClasses().get(x);
                        Class c;
                        try {
                            c = Context.loadClass(className);
                            ps.setObject(x + 2, SyncUtil.getNormalizer(c).fromString(c, value));
                        } catch (ClassNotFoundException e) {
                            log.error("Unable to convert classname into a Class object " + className);
                        }

                    }
                }

                ps.executeUpdate();

                //*and* create sync item for this
                HibernateSyncInterceptor.addSyncItemForSubclassStub(stub);
                log.debug("Sync Inserted " + stub.getParentTable() + " Stub for " + stub.getUuid());
            } catch (SQLException e) {
                log.warn("SQL Exception while trying to create a " + stub.getParentTable() + " stub", e);
            } finally {
                if (ps != null) {
                    try {
                        ps.close();
                    } catch (SQLException e) {
                        log.error("Error generated while closing statement", e);
                    }
                }
            }
        }

        return;
    }

    /**
     * (non-Javadoc)
     * 
     * @see org.openmrs.module.sync.api.db.SyncDAO#isConceptIdValidForUuid(int, java.lang.String)
     */
    public boolean isConceptIdValidForUuid(Integer conceptId, String uuid) {

        if (uuid == null || conceptId == null) {
            return true;
        }

        boolean ret = true; //assume all is well until proven otherwise
        PreparedStatement ps = null;
        Connection connection = sessionFactory.getCurrentSession().connection();
        int foundId = 0;
        String foundUuid = null;

        try {
            ps = connection
                    .prepareStatement("SELECT concept_id, uuid FROM concept WHERE (uuid = ? AND concept_id <> ?)"
                            + "OR (uuid <> ? AND concept_id = ?)");
            ps.setString(1, uuid);
            ps.setInt(2, conceptId);
            ps.setString(3, uuid);
            ps.setInt(4, conceptId);
            ps.execute();
            ResultSet rs = ps.getResultSet();
            if (rs.next()) {
                ret = false;
                foundId = rs.getInt("concept_id");
                foundUuid = rs.getString("uuid");
                String msg = "Found inconsistent data during concept ingest." + "\nto be added conceptid/uuid are:"
                        + conceptId + "/" + uuid + "\nfound in DB conceptid/uuid:" + foundId + "/" + foundUuid;
                log.error(msg);
            }
            ;

            ps.close();
            ps = null;
        } catch (SQLException e) {
            log.error("Error while doing isConceptIdValidForUuid.", e);
            ret = false;
        }
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e) {
                log.error("Error generated while closing statement", e);
            }
        }

        return ret;
    }

    public Long getCountOfSyncRecords(RemoteServer server, Date from, Date to, SyncRecordState... states) {
        Criteria criteria = sessionFactory.getCurrentSession().createCriteria(SyncRecord.class);
        criteria.setProjection(Projections.rowCount());

        if (from != null)
            criteria.add(Restrictions.gt("timestamp", from)); // greater than

        if (to != null)
            criteria.add(Restrictions.le("timestamp", to)); // less-than or equal

        // only check for matching states if any were passed in
        if (states != null && states.length > 0)
            criteria.add(Restrictions.in("state", states));

        if (server != null) {
            criteria.createCriteria("serverRecords", "sr");
            criteria.add(Restrictions.in("sr.state", states));
            criteria.add(Restrictions.eq("sr.syncServer", server));
        }

        return (Long) criteria.uniqueResult();
    }

    //this is a utility method that i used for Sync-180
    //won't hurt to leave it around -- may be useful in the future
    // MG: I'm commenting this out since the CriteriaLoader method signature has 
    // changed in the most recent version of Hibernate and so this method
    // now causes a compile issue

    /**
    private String getSQL(Criteria crit) {
       String ret = "";
       CriteriaImpl c = (CriteriaImpl) crit;
       SessionImpl s = (SessionImpl) c.getSession();
       SessionFactoryImplementor factory = (SessionFactoryImplementor) s.getSessionFactory();
       String[] implementors = factory.getImplementors(c.getEntityOrClassName());
       CriteriaLoader loader = new CriteriaLoader((OuterJoinLoadable) factory.getEntityPersister(implementors[0]), factory,
          c, implementors[0], s.getEnabledFilters());
       try {
     Field f = OuterJoinLoader.class.getDeclaredField("sql");
     f.setAccessible(true);
     String sql = (String) f.get(loader);
     return sql;
       }
       catch (Exception ex) {
     //pass
       }
       return ret;
    }
    */

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#getOlderSyncRecordInState(org.openmrs.module.sync.SyncRecord,
     *      java.util.EnumSet)
     */
    public SyncRecord getOlderSyncRecordInState(SyncRecord syncRecord, EnumSet<SyncRecordState> states) {
        Criteria criteria = sessionFactory.getCurrentSession().createCriteria(SyncRecord.class);
        criteria.createAlias("serverRecords", "serverRecord", Criteria.LEFT_JOIN);
        criteria.add(Restrictions.le("timestamp", syncRecord.getTimestamp()));
        // We need to look for a lower recordId since some may have the same timestamp.
        criteria.add(Restrictions.lt("recordId", syncRecord.getRecordId()));
        // We need to look for errors in both SyncRecord and SyncRecordServer.
        criteria.add(
                Restrictions.or(Restrictions.in("state", states), Restrictions.in("serverRecord.state", states)));
        criteria.addOrder(Order.desc("timestamp"));
        criteria.addOrder(Order.desc("recordId"));
        criteria.setMaxResults(1);
        return (SyncRecord) criteria.uniqueResult();
    }

    /**
     * @see org.openmrs.module.sync.api.db.SyncDAO#getSyncServerRecord(java.lang.Integer)
     */
    @Override
    public SyncServerRecord getSyncServerRecord(Integer syncServerRecordId) throws DAOException {
        return (SyncServerRecord) sessionFactory.getCurrentSession().get(SyncServerRecord.class,
                syncServerRecordId);
    }

    /**
     * @see SyncDAO#getMostRecentFullyCommittedRecordId()
     */
    public int getMostRecentFullyCommittedRecordId() {
        Set<String> committedStates = new HashSet<String>();
        for (SyncRecordState s : SyncConstants.SYNC_RECORD_COMMITTED_STATES) {
            committedStates.add(s.name());
        }
        StringBuilder q = new StringBuilder();
        if (getParentServer() != null) {
            q.append("SELECT record_id FROM sync_record WHERE state IN (:states) ORDER BY record_id DESC");
        } else {
            q.append("SELECT sr1.record_id FROM sync_record sr1 ");
            q.append("INNER JOIN sync_server_record ssr1 ON sr1.record_id = ssr1.record_id ");
            q.append("WHERE ssr1.state IN (:states) AND NOT EXISTS (");
            q.append("   SELECT sr2.record_id FROM sync_record sr2 ");
            q.append("   INNER JOIN sync_server_record ssr2 ON sr2.record_id=ssr2.record_id ");
            q.append("   WHERE sr1.record_id=sr2.record_id AND ssr2.state NOT IN (:states)");
            q.append(") ORDER BY sr1.record_id DESC");
        }

        Query allCommittedSSRQuery = sessionFactory.getCurrentSession().createSQLQuery(q.toString());
        allCommittedSSRQuery.setParameterList("states", committedStates);
        allCommittedSSRQuery.setMaxResults(1);

        Object result = allCommittedSSRQuery.uniqueResult();
        if (result != null) {
            return Integer.parseInt(result.toString());
        }
        return -1;
    }
}