org.openmrs.module.sync.api.impl.SyncServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.openmrs.module.sync.api.impl.SyncServiceImpl.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.impl;

import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Calendar;
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.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.openmrs.GlobalProperty;
import org.openmrs.OpenmrsObject;
import org.openmrs.Patient;
import org.openmrs.Privilege;
import org.openmrs.Role;
import org.openmrs.api.APIException;
import org.openmrs.api.AdministrationService;
import org.openmrs.api.context.Context;
import org.openmrs.api.db.DAOException;
import org.openmrs.api.db.SerializedObjectDAO;
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.SyncServerClass;
import org.openmrs.module.sync.SyncStatistic;
import org.openmrs.module.sync.SyncUtil;
import org.openmrs.module.sync.api.SyncService;
import org.openmrs.module.sync.api.db.SyncDAO;
import org.openmrs.module.sync.api.db.hibernate.HibernateSyncInterceptor;
import org.openmrs.module.sync.ingest.SyncImportRecord;
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;

/**
 * Default implementation of the {@link SyncService}
 */
public class SyncServiceImpl implements SyncService {

    private SyncDAO dao;

    private List<Class<OpenmrsObject>> allOpenmrsObjects;

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

    private static Set<String> serverClassesCollection;

    private SerializedObjectDAO serializedObjectDao;

    public void setSerializedObjectDao(SerializedObjectDAO serializedObjectDao) {
        this.serializedObjectDao = serializedObjectDao;
    }

    public SerializedObjectDAO getSerializedObjectDao() {
        return serializedObjectDao;
    }

    private SyncDAO getSynchronizationDAO() {
        return dao;
    }

    public void setSyncDAO(SyncDAO dao) {
        this.dao = dao;
    }

    public void setAllObjectsObjects(List<Class<OpenmrsObject>> openmrsObjects) {
        log.fatal("Got openmrs objects: " + openmrsObjects);

        this.allOpenmrsObjects = openmrsObjects;
    }

    public List<Class<OpenmrsObject>> getAllOpenmrsObjects() {

        return this.allOpenmrsObjects;
    }

    /**
     * @see org.openmrs.api.SyncService#createSyncRecord(org.openmrs.module.sync.SyncRecord)
     */

    public void createSyncRecord(SyncRecord record) throws APIException {
        this.createSyncRecord(record, record.getOriginalUuid());
    }

    public void createSyncRecord(SyncRecord record, String originalUuidPassed) throws APIException {

        if (record != null) {
            // here is a hack to get around the fact that hibernate decides to commit transactions when it feels like it
            // otherwise, we could run this in the ingest methods
            RemoteServer origin = null;
            int idx = originalUuidPassed.indexOf("|");
            if (idx > -1) {
                log.debug("originalPassed is " + originalUuidPassed);
                String originalUuid = originalUuidPassed.substring(0, idx);
                String serverUuid = originalUuidPassed.substring(idx + 1);
                log.debug("serverUuid is " + serverUuid + ", and originalUuid is " + originalUuid);
                record.setOriginalUuid(originalUuid);
                origin = Context.getService(SyncService.class).getRemoteServer(serverUuid);
                if (origin != null) {
                    if (origin.getServerType().equals(RemoteServerType.PARENT)) {
                        record.setState(SyncRecordState.COMMITTED);
                    }
                } else {
                    log.warn("Could not get remote server by uuid: " + serverUuid);
                }
            }

            // before creation, we need to make sure that we create matching entries for each server (server-record relationship)
            Set<SyncServerRecord> serverRecords = record.getServerRecords();
            if (serverRecords == null) {
                log.debug("IN createSyncRecord(), SERVERRECORDS ARE NULL, SO SETTING DEFAULTS");
                serverRecords = new HashSet<SyncServerRecord>();
                List<RemoteServer> servers = this.getRemoteServers();
                if (servers != null) {
                    for (RemoteServer server : servers) {
                        // we only need to create extra server-records for servers that are NOT the parent - the parent state is kept in the actual sync record
                        if (!server.getServerType().equals(RemoteServerType.PARENT)) {
                            SyncServerRecord serverRecord = new SyncServerRecord(server, record);
                            // can't compare with .equals because of so many variables in it. SYNC-227
                            if (server != null && origin != null
                                    && server.getServerId().equals(origin.getServerId())) {
                                log.info("this record came from server " + origin.getNickname()
                                        + ", so we will set its status to commmitted");
                                serverRecord.setState(SyncRecordState.COMMITTED);
                            }
                            serverRecords.add(serverRecord);
                        }
                    }
                }
                record.setServerRecords(serverRecords);
            }

            getSynchronizationDAO().createSyncRecord(record);
        }
    }

    /**
     * @see org.openmrs.api.SyncService#createSyncImportRecord(org.openmrs.module.sync.SyncImportRecord)
     */
    public void createSyncImportRecord(SyncImportRecord record) throws APIException {
        getSynchronizationDAO().createSyncImportRecord(record);
    }

    /**
     * @see org.openmrs.api.SyncService#getNextSyncRecord()
     */
    public SyncRecord getFirstSyncRecordInQueue() throws APIException {
        return getSynchronizationDAO().getFirstSyncRecordInQueue();
    }

    /**
     * @see org.openmrs.api.SyncService#getSyncRecords(java.lang.String)
     */
    public List<SyncRecord> getSyncRecords(String query) throws APIException {
        return getSynchronizationDAO().getSyncRecords(query);
    }

    /**
     * @see org.openmrs.api.SyncService#getSyncRecord(java.lang.Integer)
     */
    public SyncRecord getSyncRecord(Integer id) throws APIException {
        return getSynchronizationDAO().getSyncRecord(id);
    }

    /**
     * @see org.openmrs.api.SyncService#getSyncRecord(java.lang.String)
     */
    public SyncRecord getSyncRecord(String uuid) throws APIException {
        return getSynchronizationDAO().getSyncRecord(uuid);
    }

    public SyncRecord getSyncRecordByOriginalUuid(String originalUuid) throws APIException {
        return getSynchronizationDAO().getSyncRecordByOriginalUuid(originalUuid);
    }

    /**
     * @see org.openmrs.api.SyncService#getLatestRecord()
     */
    public SyncRecord getLatestRecord() throws APIException {
        return getSynchronizationDAO().getLatestRecord();
    }

    /**
     * @see org.openmrs.api.SyncService#getEarliestRecord(Date)
     */
    public SyncRecord getEarliestRecord(Date afterDate) throws APIException {
        return getSynchronizationDAO().getEarliestRecord(afterDate);
    }

    /**
     * @see SyncService#getNextRecord(SyncRecord)
     */
    public SyncRecord getNextRecord(SyncRecord record) {
        return getSynchronizationDAO().getNextRecord(record);
    }

    /**
     * @see SyncService#getPreviousRecord(SyncRecord)
     */
    public SyncRecord getPreviousRecord(SyncRecord record) {
        return getSynchronizationDAO().getPreviousRecord(record);
    }

    /**
     * @see org.openmrs.api.SyncService#getSyncRecord(java.lang.String)
     */
    public SyncImportRecord getSyncImportRecord(String uuid) throws APIException {
        return getSynchronizationDAO().getSyncImportRecord(uuid);
    }

    /**
     * @see org.openmrs.module.sync.api.SyncService#getOlderSyncRecordInState(org.openmrs.module.sync.SyncRecord,
     *      java.util.EnumSet)
     */
    public SyncRecord getOlderSyncRecordInState(SyncRecord syncRecord, EnumSet<SyncRecordState> states)
            throws APIException {
        return getSynchronizationDAO().getOlderSyncRecordInState(syncRecord, states);
    }

    /**
     * @see org.openmrs.api.SyncService#getSyncImportRecords(org.openmrs.module.sync.engine.SyncRecordState)
     */
    public List<SyncImportRecord> getSyncImportRecords(SyncRecordState... state) throws APIException {
        return getSynchronizationDAO().getSyncImportRecords(state);
    }

    /**
     * @see org.openmrs.api.SyncService#getSyncRecords()
     */
    public List<SyncRecord> getSyncRecords() throws APIException {
        return getSynchronizationDAO().getSyncRecords();
    }

    /**
     * @see org.openmrs.api.SyncService#getSyncRecords(org.openmrs.module.sync.engine.SyncRecordState)
     */
    public List<SyncRecord> getSyncRecords(SyncRecordState state) throws APIException {
        return getSynchronizationDAO().getSyncRecords(state);
    }

    /**
     * @see org.openmrs.api.SyncService#getSyncRecords(org.openmrs.module.sync.engine.SyncRecordState, Integer maxSyncRecords, Integer)
     */
    public List<SyncRecord> getSyncRecords(SyncRecordState[] states, Integer maxSyncRecords, Integer firstRecordId)
            throws APIException {
        return this.getSyncRecords(states, false, maxSyncRecords, firstRecordId);
    }

    /**
     * @see org.openmrs.module.sync.api.SyncService#getSyncRecords(org.openmrs.module.sync.SyncRecordState[],
     *      org.openmrs.module.sync.server.RemoteServer, java.lang.Integer, java.lang.Integer)
     */
    public List<SyncRecord> getSyncRecords(SyncRecordState[] states, RemoteServer server, Integer maxSyncRecords,
            Integer firstRecordId) throws APIException {
        List<SyncRecord> temp = null;
        List<SyncRecord> ret = null;

        if (server != null) {
            if (server.getServerType().equals(RemoteServerType.PARENT)) {
                ret = this.getSyncRecords(states, maxSyncRecords, firstRecordId);
            } else {
                ret = getSynchronizationDAO().getSyncRecords(states, false, maxSyncRecords, server, firstRecordId);
            }
        }

        // filter out classes that are not supposed to be sent to the specified server
        // and update their status
        if (ret != null) {
            temp = new ArrayList<SyncRecord>();
            for (SyncRecord record : ret) {
                if (server.shouldBeSentSyncRecord(record)) {
                    record.setForServer(server);
                    temp.add(record);

                } else {
                    log.warn("Omitting record with " + record.getContainedClasses() + " for server: "
                            + server.getNickname() + " with server type: " + server.getServerType());
                    if (server.getServerType().equals(RemoteServerType.PARENT)) {
                        record.setState(SyncRecordState.NOT_SUPPOSED_TO_SYNC);
                    } else {
                        // if not the parent, we have to update the record for this specific server
                        Set<SyncServerRecord> records = record.getServerRecords();
                        for (SyncServerRecord serverRecord : records) {
                            if (serverRecord.getSyncServer().equals(server)) {
                                serverRecord.setState(SyncRecordState.NOT_SUPPOSED_TO_SYNC);
                            }
                        }
                        record.setServerRecords(records);
                    }
                    this.updateSyncRecord(record);
                }
            }
            ret = temp;
        }

        return ret;
    }

    /**
     * @see org.openmrs.module.sync.api.SyncService#getSyncRecords(org.openmrs.module.sync.SyncRecordState[],
     *      boolean, java.lang.Integer)
     */
    public List<SyncRecord> getSyncRecords(SyncRecordState[] states, boolean inverse, Integer maxSyncRecords,
            Integer firstRecordId) throws APIException {
        return getSynchronizationDAO().getSyncRecords(states, inverse, maxSyncRecords, null, firstRecordId);
    }

    /**
     * @see org.openmrs.api.SyncService#updateSyncRecord(org.openmrs.module.sync.SyncRecord)
     */
    public void updateSyncRecord(SyncRecord record) throws APIException {
        getSynchronizationDAO().updateSyncRecord(record);
    }

    /**
     * @see org.openmrs.api.SyncService#deleteSyncRecord(org.openmrs.module.sync.SyncRecord)
     */
    public void deleteSyncRecord(SyncRecord record) throws APIException {
        getSynchronizationDAO().deleteSyncRecord(record);
    }

    /**
     * @see org.openmrs.api.SyncService#updateSyncImportRecord(org.openmrs.module.sync.SyncImportRecord)
     */
    public void updateSyncImportRecord(SyncImportRecord record) throws APIException {
        getSynchronizationDAO().updateSyncImportRecord(record);
    }

    /**
     * @see org.openmrs.api.SyncService#deleteSyncRecord(org.openmrs.module.sync.SyncRecord)
     */
    public void deleteSyncImportRecord(SyncImportRecord record) throws APIException {
        getSynchronizationDAO().deleteSyncImportRecord(record);
    }

    /**
     * @see org.openmrs.api.SyncService#deleteSyncImportRecordsByServer(java.lang.Integer)
     */
    public void deleteSyncImportRecordsByServer(Integer serverId) throws APIException {
        getSynchronizationDAO().deleteSyncImportRecordsByServer(serverId);
    }

    /**
     * @see org.openmrs.api.SyncService#getSyncRecordsSince(java.util.Date)
     */
    public List<SyncRecord> getSyncRecordsSince(Date from) throws APIException {
        return getSynchronizationDAO().getSyncRecords(from, null, null, null, true);
    }

    /**
     * @see org.openmrs.api.SyncService#getSyncRecordsBetween(java.util.Date, java.util.Date)
     */
    public List<SyncRecord> getSyncRecordsBetween(Date from, Date to) throws APIException {
        return getSynchronizationDAO().getSyncRecords(from, to, null, null, true);
    }

    /**
     * @see org.openmrs.module.sync.api.SyncService#getSyncRecords(java.lang.Integer,
     *      java.lang.Integer)
     */
    public List<SyncRecord> getSyncRecords(Integer firstRecordId, Integer numberToReturn) throws APIException {
        return getSynchronizationDAO().getSyncRecords(null, null, firstRecordId, numberToReturn, false);
    }

    /**
     * @see org.openmrs.module.sync.api.SyncService#deleteSyncRecords(org.openmrs.module.sync.SyncRecordState[],
     *      java.util.Date)
     */
    public Integer deleteSyncRecords(SyncRecordState[] states, Date to) throws APIException {

        // if no states passed in, then decide based on current server setup
        if (states == null || states.length == 0) {

            if (getParentServer() == null) {
                // if server is not a leaf node (only a parent)
                // state does not matter (but will always be NEW)
                states = new SyncRecordState[] { SyncRecordState.NOT_SUPPOSED_TO_SYNC, SyncRecordState.NEW };
            } else {
                // if a server is a leaf node, then only delete states that 
                // have been successfully sent to the parent already
                states = new SyncRecordState[] { SyncRecordState.NOT_SUPPOSED_TO_SYNC, SyncRecordState.COMMITTED };
            }
        }

        return getSynchronizationDAO().deleteSyncRecords(states, to);
    }

    /**
     * @see org.openmrs.api.SyncService#getGlobalProperty(java.lang.String)
     */
    public String getGlobalProperty(String propertyName) throws APIException {
        return getSynchronizationDAO().getGlobalProperty(propertyName);
    }

    /**
     * @see org.openmrs.api.SyncService#setGlobalProperty(String propertyName, String propertyValue)
     */
    public void setGlobalProperty(String propertyName, String propertyValue) throws APIException {
        getSynchronizationDAO().setGlobalProperty(propertyName, propertyValue);
    }

    /**
     * @see org.openmrs.api.SyncService#saveRemoteServer(org.openmrs.module.sync.engine.RemoteServer)
     */
    public RemoteServer saveRemoteServer(RemoteServer server) throws APIException {
        if (server != null) {
            Set<SyncServerClass> serverClasses = server.getServerClasses();
            if (serverClasses == null) {
                log.warn("IN CREATEREMOTESERVER(), SERVERCLASSES ARE NULL, SO SETTING DEFAULTS");
                serverClasses = new HashSet<SyncServerClass>();
                List<SyncClass> classes = this.getSyncClasses();
                if (classes != null) {
                    for (SyncClass syncClass : classes) {
                        SyncServerClass serverClass = new SyncServerClass(server, syncClass);
                        serverClasses.add(serverClass);
                    }
                }
                server.setServerClasses(serverClasses);
            }

            server = getSynchronizationDAO().saveRemoteServer(server);
            refreshServerClassesCollection();

            return server;
        }
        return null;
    }

    /**
     * @see org.openmrs.api.SyncService#deleteRemoteServer(org.openmrs.module.sync.engine.RemoteServer)
     */
    public void deleteRemoteServer(RemoteServer server) throws APIException {
        getSynchronizationDAO().deleteRemoteServer(server);
    }

    public RemoteServer getRemoteServer(Integer serverId) throws APIException {
        return getSynchronizationDAO().getRemoteServer(serverId);
    }

    public RemoteServer getRemoteServer(String uuid) throws APIException {
        return getSynchronizationDAO().getRemoteServer(uuid);
    }

    public RemoteServer getRemoteServerByUsername(String username) throws APIException {
        return getSynchronizationDAO().getRemoteServerByUsername(username);
    }

    public List<RemoteServer> getRemoteServers() throws APIException {
        return getSynchronizationDAO().getRemoteServers();
    }

    public RemoteServer getParentServer() throws APIException {
        return getSynchronizationDAO().getParentServer();
    }

    /**
     * Returns globally unique identifier of the local server. This value uniquely indentifies
     * server in all data exchanges with other servers.
     */
    public String getServerUuid() throws APIException {
        return Context.getAdministrationService().getGlobalProperty(SyncConstants.PROPERTY_SERVER_UUID);
    }

    /**
     * Updates globally unique identifier of the local server.
     */
    public void saveServerUuid(String uuid) throws APIException {
        Context.getService(SyncService.class).setGlobalProperty(SyncConstants.PROPERTY_SERVER_UUID, uuid);
    }

    /**
     * Returns server friendly name for sync purposes. It should be assigned by convention to be
     * unique in the synchronization network of servers. This value can be used to scope values that
     * are otherwise unique only locally (such as integer primary keys).
     */
    public String getServerName() throws APIException {
        return Context.getAdministrationService().getGlobalProperty(SyncConstants.PROPERTY_SERVER_NAME);
    }

    /**
     * Updates/saves the user friendly server name for sync purposes.
     */
    public void saveServerName(String name) throws APIException {
        Context.getService(SyncService.class).setGlobalProperty(SyncConstants.PROPERTY_SERVER_NAME, name);
    }

    public String getAdminEmail() {
        return Context.getService(SyncService.class).getGlobalProperty(SyncConstants.PROPERTY_SYNC_ADMIN_EMAIL);
    }

    public void saveAdminEmail(String email) {
        Context.getService(SyncService.class).setGlobalProperty(SyncConstants.PROPERTY_SYNC_ADMIN_EMAIL, email);
    }

    /**
     * @see org.openmrs.api.SyncService#saveSyncClass(org.openmrs.module.sync.SyncClass)
     */
    public void saveSyncClass(SyncClass syncClass) throws APIException {
        getSynchronizationDAO().saveSyncClass(syncClass);
        refreshServerClassesCollection();
    }

    /**
     * @see org.openmrs.api.SyncService#deleteSyncClass(org.openmrs.module.sync.SyncClass)
     */
    public void deleteSyncClass(SyncClass syncClass) throws APIException {
        getSynchronizationDAO().deleteSyncClass(syncClass);
        refreshServerClassesCollection();
    }

    public SyncClass getSyncClass(Integer syncClassId) throws APIException {
        return getSynchronizationDAO().getSyncClass(syncClassId);
    }

    public List<SyncClass> getSyncClasses() throws APIException {
        return getSynchronizationDAO().getSyncClasses();
    }

    public SyncClass getSyncClassByName(String className) throws APIException {
        return getSynchronizationDAO().getSyncClassByName(className);
    }

    /**
     * @see org.openmrs.api.SyncService#deleteOpenmrsObject(org.openmrs.synchronization.OpenmrsObject)
     */
    public void deleteOpenmrsObject(OpenmrsObject o) throws APIException {
        getSynchronizationDAO().deleteOpenmrsObject(o);
    }

    /**
     * Changes flush sematics, delegating directly to the corresponsing DAO method.
     * 
     * @see org.openmrs.api.SyncService#setFlushModeManual()
     * @see org.openmrs.api.db.hibernate.HibernateSyncDAO#setFlushModeManual()
     */
    public void setFlushModeManual() throws APIException {
        getSynchronizationDAO().setFlushModeManual();
    }

    /**
     * Changes flush sematics, delegating directly to the corresponsing DAO method.
     * 
     * @see org.openmrs.api.SyncService#setFlushModeAutomatic()
     * @see org.openmrs.api.db.hibernate.HibernateSyncDAO#setFlushModeAutomatic()
     */
    public void setFlushModeAutomatic() throws APIException {
        getSynchronizationDAO().setFlushModeAutomatic();
    }

    /**
     * Performs peristence layer flush, delegating directly to the corresponsing DAO method.
     * 
     * @see org.openmrs.api.SyncService#flushSession()
     * @see org.openmrs.api.db.hibernate.HibernateSyncDAO#flushSession()
     */
    public void flushSession() throws APIException {
        getSynchronizationDAO().flushSession();
    }

    /**
     * Processes save/update to instance of OpenmrsObject by persisting it into local persistance
     * store.
     * 
     * @param object instance of OpenmrsObject to be processed.
     * @return
     * @throws APIException
     */
    //@Authorized({"Manage Synchronization Records"})
    public void saveOrUpdate(OpenmrsObject object) throws APIException {
        getSynchronizationDAO().saveOrUpdate(object);
    }

    /**
     * Gets stats for the server: 1. Sync Records count by server by state 2. If any sync records
     * are in 'pending'/failed state and it has been > 24hrs, add statistic for it 3. count of
     * 'pending' sync records (i.e. the ones that are not in complete or error state
     * 
     * @param fromDate start date
     * @param toDate end date
     * @return
     * @throws DAOException
     */
    public Map<RemoteServer, LinkedHashSet<SyncStatistic>> getSyncStatistics(Date fromDate, Date toDate)
            throws DAOException {

        Map<RemoteServer, LinkedHashSet<SyncStatistic>> stats = getSynchronizationDAO().getSyncStatistics(fromDate,
                toDate);

        //check out the info for the servers: if any records are pending and are older than 1 day, add flag to stats
        for (Map.Entry<RemoteServer, LinkedHashSet<SyncStatistic>> entry1 : stats.entrySet()) {
            Long pendingCount = 0L;
            for (SyncStatistic syncStat : entry1.getValue()) {
                if (syncStat.getType() == SyncStatistic.Type.SYNC_RECORD_COUNT_BY_STATE) {
                    if (syncStat.getName() != SyncRecordState.ALREADY_COMMITTED.toString()
                            && syncStat.getName() != SyncRecordState.COMMITTED.toString()
                            && syncStat.getName() != SyncRecordState.NOT_SUPPOSED_TO_SYNC.toString()) {
                        pendingCount = pendingCount + ((syncStat.getValue() == null) ? 0L
                                : Long.parseLong(syncStat.getValue().toString()));
                    }
                }
            }

            //add pending count
            entry1.getValue().add(new SyncStatistic(SyncStatistic.Type.SYNC_RECORDS_PENDING_COUNT,
                    SyncStatistic.Type.SYNC_RECORDS_PENDING_COUNT.toString(), pendingCount)); //careful, manipulating live collection

            //if some 'stale' records found see if it has been 24hrs since last sync
            RemoteServer server = entry1.getKey();

            if (server.getLastSync() != null) {
                Calendar lastSync = Calendar.getInstance();
                lastSync.setTime(server.getLastSync());
                Calendar threeDayThreshold = Calendar.getInstance();
                threeDayThreshold.add(Calendar.HOUR, -72); // check if last sync is more than 3 days ago
                Calendar oneDayThreshold = Calendar.getInstance();
                oneDayThreshold.add(Calendar.HOUR, -24); // check if last sync is more than 3 days ago

                if (lastSync.before(threeDayThreshold)) {
                    entry1.getValue().add(new SyncStatistic(SyncStatistic.Type.LAST_SYNC_REALLY_LONG_TIME_AGO,
                            SyncStatistic.Type.LAST_SYNC_REALLY_LONG_TIME_AGO.toString(), pendingCount)); //careful, manipulating live collection
                } else if (lastSync.before(oneDayThreshold)) {
                    entry1.getValue().add(new SyncStatistic(SyncStatistic.Type.LAST_SYNC_TIME_SOMEWHAT_TROUBLESOME,
                            SyncStatistic.Type.LAST_SYNC_TIME_SOMEWHAT_TROUBLESOME.toString(), pendingCount)); //careful, manipulating live collection
                }
            }
        }

        return stats;
    }

    public <T extends OpenmrsObject> T getOpenmrsObjectByUuid(Class<T> clazz, String uuid) {
        T ret = dao.getOpenmrsObjectByUuid(clazz, uuid);
        if (ret == null) {
            try {
                ret = serializedObjectDao.getObjectByUuid(clazz, uuid); //sync-205
            } catch (Exception ex) {
                //pass -- not sure if catch/try is necessary
            }
        }

        return ret;
    }

    /**
     * @see org.openmrs.api.SynchronizationService#exportChildDB(java.lang.String,
     *      java.io.OutputStream)
     */
    public void exportChildDB(String guidForChild, OutputStream os) throws APIException {
        getSynchronizationDAO().exportChildDB(guidForChild, os);
    }

    /**
     * @see org.openmrs.api.SynchronizationService#importParentDB(java.io.InputStream)
     */
    public void importParentDB(InputStream in) throws APIException {
        getSynchronizationDAO().importParentDB(in);
        //Delete any data kept into sync journal after clone of the parent DB
        for (SyncRecord record : this.getSynchronizationDAO().getSyncRecords()) {
            this.getSynchronizationDAO().deleteSyncRecord(record);
        }
    }

    /**
     * @see org.openmrs.module.sync.api.SyncService#generateDataFile()
     */
    public File generateDataFile() throws APIException {
        File dir = SyncUtil.getSyncApplicationDir();
        String fileName = SyncConstants.CLONE_IMPORT_FILE_NAME + SyncConstants.SYNC_FILENAME_MASK.format(new Date())
                + ".sql";
        String[] ignoreTables = { "hl7_in_archive", "hl7_in_queue", "hl7_in_error", "formentry_archive",
                "formentry_queue", "formentry_error", "sync_class", "sync_import", "sync_record", "sync_server",
                "sync_server_class", "sync_server_record" };

        File outputFile = new File(dir, fileName);
        getSynchronizationDAO().generateDataFile(outputFile, ignoreTables);
        return outputFile;
    }

    /**
     * @see org.openmrs.module.sync.api.SyncService#execGeneratedFile(java.io.File)
     */
    public void execGeneratedFile(File file) throws APIException {
        AdministrationService adminService = Context.getAdministrationService();

        // preserve this server's sync settings
        List<GlobalProperty> syncGPs = adminService.getGlobalPropertiesByPrefix("sync.");

        getSynchronizationDAO().execGeneratedFile(file);

        // save those GPs again
        for (GlobalProperty gp : syncGPs) {
            adminService.saveGlobalProperty(gp);
        }

        //Delete any data in sync record after import of the parent DB
        for (SyncRecord record : this.getSynchronizationDAO().getSyncRecords()) {
            this.getSynchronizationDAO().deleteSyncRecord(record);
        }
    }

    /**
     * Determines if given object is to be sync-ed assuming sync as a feature is turned on. This is
     * done by: <br/>
     * 1. type has to implement OpenmrsObject interface 2. comparing the type of the object against
     * the types in the DB configured for exclusion from sync.
     * 
     * @see org.openmrs.module.sync.api.SyncService#execGeneratedFile(java.io.File)
     */
    public Boolean shouldSynchronize(Object entity) throws APIException {
        Boolean ret = true;

        // OpenmrsObject *only*.
        if (!(entity instanceof OpenmrsObject)) {
            if (log.isDebugEnabled())
                log.debug("Do nothing. Flush with type that does not implement OpenmrsObject, type is:"
                        + entity.getClass().getName());
            return false;
        }

        //if the server classes haven't been loaded yet, do it now
        if (serverClassesCollection == null) {
            refreshServerClassesCollection();
        }

        //now verify
        if (serverClassesCollection != null) {
            String type = entity.getClass().getName();
            for (String temp : serverClassesCollection) {
                if (type.startsWith(temp)) {
                    ret = false;
                    break;
                }
            }
        }

        return ret;

    }

    /***
     * Refreshes static helper collection. This is a perf optimization to avoid fetching the
     * sync_server_classes on every call to {@link #shouldSynchronize(Object)} Remarks:<br/>
     * The algorithm is as follows: - if no servers to talk to are setup (i.e. no rows in
     * sync_server_class) then use sync_class only - else only use the classes that are setup in all
     * servers (i.e.) for the class/type to be excluded it has to be setup for exclusion in all
     * servers
     */
    public static synchronized void refreshServerClassesCollection() {

        List<RemoteServer> servers = Context.getService(SyncService.class).getRemoteServers();
        Set<String> serverClasses = new HashSet<String>();

        if (servers == null || servers.size() == 0) {
            //this is easy, just use the defaults
            for (SyncClass sc : Context.getService(SyncService.class).getSyncClasses()) {
                if (!sc.getDefaultReceiveFrom() && !sc.getDefaultSendTo())
                    serverClasses.add(sc.getName());
            }
        } else {
            //some sync servers are set up
            Map<String, Integer> helperMap = new HashMap<String, Integer>();

            //crank through and count up the types & occurrences
            for (RemoteServer server : servers) {
                for (String temp : server.getClassesNotReceived()) {
                    if (helperMap.containsKey(temp)) {
                        //already there, just increment the count
                        Integer iTemp = helperMap.get(temp) + 1;
                        helperMap.put(temp, iTemp);
                    } else {
                        //not there yet, just add with count of 0
                        helperMap.put(temp, 1);
                    }
                }
                for (String temp : server.getClassesNotSent()) {
                    if (helperMap.containsKey(temp)) {
                        //already there, just increment the count
                        Integer iTemp = helperMap.get(temp) + 1;
                        helperMap.put(temp, iTemp);
                    } else {
                        //not there yet, just add with count of 0
                        helperMap.put(temp, 1);
                    }
                }
            }

            //now, walk the map and only use the types where occurrence count = 2 x nbr or servers
            //i.e. the type was listed on all servers as both don't send and don't receive
            int targetCount = servers.size() * 2;
            for (String type : helperMap.keySet()) {
                if (helperMap.get(type).equals(targetCount)) {
                    serverClasses.add(type);
                }
            }
        }

        //now assign
        serverClassesCollection = serverClasses;
    }

    public String getPrimaryKey(OpenmrsObject obj) {
        if (obj instanceof Privilege) {
            return ((Privilege) obj).getPrivilege();
        } else if (obj instanceof Role) {
            return ((Role) obj).getRole();
        } else if (obj instanceof GlobalProperty) {
            return ((GlobalProperty) obj).getProperty();
        } else {
            return null;
        }
    }

    /**
     * Handles the odd case of saving patient who already has person record. This method is invoked
     * by sync AOP advice on save of new patient (see
     * {@link org.openmrs.module.sync.advice.SavePatientAdvice}) in order to generate a necessary
     * sync item for the actions taken inside of
     * {@link org.openmrs.api.db.hibernate.HibernatePatientDAO#savePatient(Patient)}. The
     * compensating logic resides in
     * {@link HibernateSyncInterceptor#addSyncItemForSubclassStub(SyncSubclassStub)}.
     */
    public void handleInsertPatientStubIfNeeded(Patient p) throws APIException {

        SyncSubclassStub stub = new SyncSubclassStub(p, "person", "person_id", "patient", "patient_id", null, null,
                null);
        stub.addColumn("voided", 0);
        Integer userId = 0;
        if (p.getCreator() != null)
            userId = p.getCreator().getUserId();
        else
            userId = Context.getAuthenticatedUser().getUserId();

        stub.addColumn("creator", userId);
        stub.addColumn("date_created", p.getDateCreated());

        handleInsertSubclassIfNeeded(stub);
    }

    public void handleInsertSubclassIfNeeded(SyncSubclassStub stub) {
        if (stub == null || stub.getId() == null || stub.getUuid() == null) {
            return;
        }

        // changing the flush mode temporarily so that nothing is flushed
        // to the db while we are checking for the uuids
        boolean wasFlushModeManualAlready = dao.setFlushModeManual();
        try {
            //check if person obj exists
            Object parentId = null;
            Object subclassId = null;

            // TODO: Fix this logic when patient_id != person_id anymore
            List<List<Object>> rows = executeSQLPrivilegeSafe("select " + stub.getParentTableId() + " from "
                    + stub.getParentTable() + " where uuid = '" + stub.getUuid() + "'", true);
            if (rows.size() > 0)
                parentId = rows.get(0).get(0);

            rows = executeSQLPrivilegeSafe(
                    "select " + stub.getSubclassTableId() + " from " + stub.getSubclassTable() + " where "
                            + stub.getSubclassTableId() + " = (select " + stub.getParentTableId() + " from "
                            + stub.getParentTable() + " where uuid = '" + stub.getUuid() + "')",
                    true);

            if (rows.size() > 0)
                subclassId = rows.get(0).get(0);

            if (parentId != null && subclassId == null) {
                //bingo!
                log.info("Create of new parent " + stub.getParentTable()
                        + " who is already other object detected, uuid: " + stub.getUuid());

                HibernateSyncInterceptor.addSyncItemForSubclassStub(stub);
            }
        } finally {
            // only reset this if we really changed it when setting it to manual
            if (!wasFlushModeManualAlready)
                dao.setFlushModeAutomatic();
        }

        return;
    }

    public Long getCountOfSyncRecords(RemoteServer server, Date from, Date to, SyncRecordState... states)
            throws APIException {
        return dao.getCountOfSyncRecords(server, from, to, states);
    }

    /**
     * Utility method for wrapping executeSQL calls in SQL LEVEL ACCESS privilege, if necessary
     * 
     * @param sql
     * @param selectOnly
     * @return
     */
    private List<List<Object>> executeSQLPrivilegeSafe(String sql, boolean selectOnly) {
        String privilege = OpenmrsConstants.PRIV_SQL_LEVEL_ACCESS;

        if (!Context.isAuthenticated() || !Context.hasPrivilege(privilege)) {
            try {
                Context.addProxyPrivilege(privilege);
                return Context.getAdministrationService().executeSQL(sql, selectOnly);
            } finally {
                Context.removeProxyPrivilege(privilege);
            }

        } else
            return Context.getAdministrationService().executeSQL(sql, selectOnly);
    }

    public Integer backportSyncRecords(RemoteServer server, Date date) {
        int count = 0;
        SyncRecord firstRecord = getEarliestRecord(date);
        SyncRecord latestRecord = getLatestRecord();

        // we have no sync records, quit early
        if (firstRecord == null)
            return 0;

        // not sure how this would happen without the previous one, but just in case.
        if (latestRecord == null)
            return 0;

        Integer firstRecordId = 0;
        Integer latestRecordId = latestRecord.getRecordId();

        System.out.println("first record id: " + firstRecord.getRecordId());
        System.out.println("latest record id: " + latestRecord.getRecordId());

        boolean recordsFound = false;

        do {
            recordsFound = false;

            // only getting a small number at a time so that we don't get an OOM
            for (SyncRecord record : getSynchronizationDAO().getSyncRecords(null, null, firstRecordId, 35, true)) {
                recordsFound = true;
                System.out.println("record id: " + record.getRecordId());
                firstRecordId = record.getRecordId();
                SyncServerRecord serverRecord = record.getServerRecord(server);
                if (serverRecord == null) {
                    // if this record is not being sent to this server yet, add it as a SyncServerRecord and send it
                    record.addServerRecord(server);
                    updateSyncRecord(record); // persist to the db
                    count++;
                    System.out.println("saved record id: " + record.getRecordId());
                }
                if (count % 50 == 0) {
                    // flush every so often so we don't get an OOM
                    Context.flushSession();
                    Context.clearSession();
                }
            }

        } while (recordsFound == true && firstRecordId != latestRecordId);

        return count;
    }

    /**
     * @see SyncService#getMostRecentFullyCommittedRecordId()
     */
    public int getMostRecentFullyCommittedRecordId() {
        return getSynchronizationDAO().getMostRecentFullyCommittedRecordId();
    }

    /**
     * @see org.openmrs.module.sync.api.SyncService#getSyncServerRecord(java.lang.Integer)
     */
    public SyncServerRecord getSyncServerRecord(Integer syncServerRecordId) throws APIException {
        return getSynchronizationDAO().getSyncServerRecord(syncServerRecordId);
    }

}