org.kuali.kra.award.awardhierarchy.sync.service.AwardSyncServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.kuali.kra.award.awardhierarchy.sync.service.AwardSyncServiceImpl.java

Source

/*
 * Kuali Coeus, a comprehensive research administration system for higher education.
 * 
 * Copyright 2005-2015 Kuali, Inc.
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.kuali.kra.award.awardhierarchy.sync.service;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.kuali.coeus.common.framework.version.VersionStatus;
import org.kuali.coeus.common.framework.version.history.VersionHistory;
import org.kuali.coeus.common.framework.version.history.VersionHistoryService;
import org.kuali.coeus.common.framework.auth.perm.KcAuthorizationService;
import org.kuali.coeus.sys.framework.service.KcServiceLocator;
import org.kuali.coeus.sys.framework.workflow.KcWorkflowService;
import org.kuali.kra.award.AwardDocumentRule;
import org.kuali.kra.award.awardhierarchy.AwardHierarchy;
import org.kuali.kra.award.awardhierarchy.AwardHierarchyService;
import org.kuali.kra.award.awardhierarchy.sync.AwardSyncChange;
import org.kuali.kra.award.awardhierarchy.sync.AwardSyncException;
import org.kuali.kra.award.awardhierarchy.sync.AwardSyncLog;
import org.kuali.kra.award.awardhierarchy.sync.AwardSyncStatus;
import org.kuali.kra.award.awardhierarchy.sync.helpers.AwardSyncHelper;
import org.kuali.kra.award.document.AwardDocument;
import org.kuali.kra.award.home.Award;
import org.kuali.kra.award.home.AwardService;
import org.kuali.kra.award.infrastructure.AwardPermissionConstants;
import org.kuali.rice.kew.api.KewApiConstants;
import org.kuali.rice.kew.api.exception.WorkflowException;
import org.kuali.rice.kim.api.identity.Person;
import org.kuali.rice.kim.api.identity.PersonService;
import org.kuali.rice.krad.UserSession;
import org.kuali.rice.krad.bo.AdHocRoutePerson;
import org.kuali.rice.krad.bo.AdHocRouteRecipient;
import org.kuali.rice.krad.bo.PersistableBusinessObject;
import org.kuali.rice.krad.service.BusinessObjectService;
import org.kuali.rice.krad.service.DocumentService;
import org.kuali.rice.krad.util.GlobalVariables;
import org.springframework.core.task.TaskExecutor;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;

import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;

/**
 * Award Hierarchy Sync Service Implementation.
 */
public class AwardSyncServiceImpl implements AwardSyncService {

    protected static final String VALIDATION_SUCCESS_MESSAGE = "Valid";
    protected static final String VALIDATION_FAILURE_MESSAGE = "Invalid";
    protected static final String SYNC_SAVED_MESSAGE = "Saved";
    protected static final String SYNC_SUCCESS_MESSAGE = "Completed";
    protected static final String SYNC_FAILURE_MESSAGE = "Failed";
    protected static final String CHANGE_LOG_SUCCESS = "Success";

    protected static final String IGNORED_MESSAGE_KEYS = "error.award.person.credit.split.,error.award.person.unit.credit.split.,error.awardProjectPerson.uncertified";
    protected final Log LOG = LogFactory.getLog(AwardSyncServiceImpl.class);

    private AwardSyncHelpersService awardSyncHelpersService;
    private AwardSyncCreationService awardSyncCreationService;
    private AwardSyncUtilityService awardSyncUtilityService;
    private AwardHierarchyService awardHierarchyService;
    private AwardService awardService;
    private VersionHistoryService versionHistoryService;
    private BusinessObjectService businessObjectService;
    private DocumentService documentService;
    private AwardSyncSelectorService awardSyncSelectorService;
    private TaskExecutor syncExecutor;
    @SuppressWarnings("unchecked")
    private PersonService personService;
    private KcWorkflowService kraWorkflowService;
    private KcAuthorizationService kraAuthorizationService;

    @Override
    public void validateHierarchyChanges(Award award) {
        runSync(award, SyncType.VALIDATE);
    }

    @Override
    public void applyAwardSyncChangesToHierarchy(Award award) {
        runSync(award, SyncType.SYNC);
    }

    @Override
    public AwardDocument getAwardLockingHierarchyForSync(AwardDocument awardDocument, String principalId) {
        AwardHierarchy hierarchy = getAwardHierarchyService()
                .loadAwardHierarchy(awardDocument.getAward().getAwardNumber());
        if (hierarchy != null) {
            Person person = getPersonService().getPerson(principalId);
            while (hierarchy.getParent() != null) {
                hierarchy = hierarchy.getParent();
                Award parentAward = getPendingAward(hierarchy.getAwardNumber());
                if (parentAward != null && !parentAward.getSyncChanges().isEmpty()) {
                    try {
                        AwardDocument parentDocument = (AwardDocument) getDocumentService()
                                .getByDocumentHeaderId(parentAward.getAwardDocument().getDocumentNumber());
                        if (getKraWorkflowService().isEnRoute(parentDocument)
                                && !(parentDocument.getDocumentHeader().getWorkflowDocument()
                                        .getRoutedByPrincipalId().equals(principalId))) {
                            return parentDocument;
                        }
                    } catch (WorkflowException e) {
                        LOG.error("Error loading document to check for award sync lock", e);
                    }
                }
            }
        }
        return null;
    }

    /**
     * Run validation or full sync depending on syncType on the award hierarchy under award.
     * @param award
     * @param syncType
     */
    protected void runSync(final Award award, final SyncType syncType) {
        long start = System.currentTimeMillis();
        LOG.debug("Award Hierarchy Sync Starting");
        UserSession oldSession = replaceSessionWithRoutedBy(award);
        try {
            List<SyncRunnable> runnables = new ArrayList<SyncRunnable>();
            runInTransaction(new TransactionRunnable() {
                public void run() {
                    clearSyncStatuses(award);
                    setParentAwardStatus(award, true, syncType == SyncType.VALIDATE ? "Validation In Progress"
                            : "Sync Descendants In Progress");
                }
            });
            award.refresh();
            //populate XmlExport object for each change from xml in BO.
            List<AwardSyncChange> changes = award.getSyncChanges();
            for (AwardSyncChange change : changes) {
                change.setXmlExport(getAwardSyncCreationService().getXmlExport(change));
            }

            AwardHierarchy hierarchy = getAwardHierarchyService().loadAwardHierarchyBranch(award.getAwardNumber());
            for (AwardHierarchy curHierarchy : hierarchy.getChildren()) {
                runSyncOnHierarchy(award, curHierarchy, changes, syncType, runnables);
            }
            waitTillRunablesFinished(runnables);
            award.refresh();
            if (syncType == SyncType.SYNC) {
                sendSyncFYIs(award, getAllPrincipalsToNotify(runnables), runnables);
            }
            runInTransaction(new TransactionRunnable() {
                public void run() {
                    setParentAwardStatus(award, true,
                            syncType == SyncType.VALIDATE ? "Validation Complete" : "Sync Descendants Complete");
                }
            });
            long elapsed = System.currentTimeMillis() - start;
            LOG.info("Award Hierarchy Sync Finished : " + elapsed + "ms");
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            GlobalVariables.setUserSession(oldSession);
        }
    }

    /**
     * Run the sync recursively down the hierarchy.
     * @param parentAward
     * @param hierarchy
     * @param changes
     * @param syncType
     * @param runnables
     */
    protected void runSyncOnHierarchy(Award parentAward, AwardHierarchy hierarchy, List<AwardSyncChange> changes,
            SyncType syncType, List<SyncRunnable> runnables) {
        this.runSyncInThread(parentAward, hierarchy, syncType, changes, runnables);
        for (AwardHierarchy curHierarchy : hierarchy.getChildren()) {
            runSyncOnHierarchy(parentAward, curHierarchy, changes, syncType, runnables);
        }
    }

    /**
     * Clears sync status from parent award as long as it has an award number(not the parent status),
     * and it is not yet complete.
     * @param award
     * @param runnables
     */
    protected void clearSyncStatuses(Award award) {
        //clear any old logs and reset old statuses
        List<AwardSyncLog> oldLogs = new ArrayList<AwardSyncLog>();
        for (AwardSyncStatus status : award.getSyncStatuses()) {
            if (status.getAwardNumber() != null && !status.isSyncComplete()) {
                status.setStatus(null);
                status.setSyncComplete(false);
                status.setSuccess(false);
                oldLogs.addAll(status.getChangeLogs());
                oldLogs.addAll(status.getValidationLogs());
                status.getChangeLogs().clear();
                status.getValidationLogs().clear();
            }
        }
        getBusinessObjectService().delete(oldLogs);
        getBusinessObjectService().save(award.getSyncStatuses());
    }

    /**
     * Sends FYIs to all principals listed in principalsToNotify for award.
     * @param award
     * @param principalsToNotify
     * @param runnables
     * @throws WorkflowException 
     */
    protected void sendSyncFYIs(final Award award, final List<String> principalsToNotify,
            final List<SyncRunnable> runnables) throws WorkflowException {
        final List<AdHocRouteRecipient> adHocPersons = new ArrayList<AdHocRouteRecipient>();
        for (String principalId : principalsToNotify) {
            Person person = getPersonService().getPerson(principalId);
            if (person != null) {
                AdHocRoutePerson adHocPerson = new AdHocRoutePerson();
                adHocPerson.setActionRequested(KewApiConstants.ACTION_REQUEST_FYI_REQ);
                adHocPerson.setType(AdHocRoutePerson.PERSON_TYPE);
                adHocPerson.setId(person.getPrincipalName());
                adHocPersons.add(adHocPerson);
            }
        }
        this.sendAdHocRequestsInThread(award.getAwardDocument(), "Sync Descendents FYI", adHocPersons, runnables);
    }

    /**
     * Sets the parent award status for the sync. This is done by creating and updating a sync status that has no award.
     * @param parentAward
     * @param success
     * @param newStatus
     * @param runnables
     */
    protected void setParentAwardStatus(Award parentAward, boolean success, String newStatus) {
        AwardSyncStatus status = getParentAwardStatus(parentAward);
        if (status != null) {
            status.setStatus(newStatus);
            status.setSuccess(success);
        } else {
            status = new AwardSyncStatus();
            status.setParentAwardId(parentAward.getAwardId());
            status.setStatus(newStatus);
            status.setSuccess(success);
            parentAward.getSyncStatuses().add(status);
        }
        getBusinessObjectService().save(status);
    }

    /**
     * Finds the parents award status log entry, the one without an award listed.
     * @param parentAward
     * @return
     */
    protected AwardSyncStatus getParentAwardStatus(Award parentAward) {
        parentAward.refreshReferenceObject("syncStatuses");
        for (AwardSyncStatus status : parentAward.getSyncStatuses()) {
            if (status.getAwardNumber() == null) {
                return status;
            }
        }
        return null;
    }

    /**
     * Find or create a new award status for the award number.
     * @param parentAward
     * @param awardNumber
     * @return
     */
    protected AwardSyncStatus findAwardSyncStatus(Award parentAward, String awardNumber) {
        for (AwardSyncStatus status : parentAward.getSyncStatuses()) {
            if (StringUtils.equals(awardNumber, status.getAwardNumber())) {
                return status;
            }
        }
        AwardSyncStatus status = new AwardSyncStatus();
        status.setParentAwardId(parentAward.getAwardId());
        status.setAwardNumber(awardNumber);
        return status;
    }

    /**
     * Replace the UserSession with one for the user who routed the parent award.
     * @param parentAward
     * @return
     */
    protected UserSession replaceSessionWithRoutedBy(Award parentAward) {
        String routedByUserId = parentAward.getAwardDocument().getDocumentHeader().getWorkflowDocument()
                .getRoutedByPrincipalId();
        Person person = getPersonService().getPerson(routedByUserId);
        UserSession oldSession = GlobalVariables.getUserSession();
        GlobalVariables.setUserSession(new UserSession(person.getPrincipalName()));
        return oldSession;
    }

    /**
     * Check and validate the status of the award. Must have an active version and
     * there must not be a pending version.
     * @param awardNumber
     * @param awardStatus
     * @param failureMessage
     * @return
     */
    protected Award checkAwardVersions(String awardNumber, AwardSyncStatus awardStatus, String failureMessage) {
        Award oldAward = getPendingAward(awardNumber);
        if (oldAward != null) {
            logFailure(awardStatus, failureMessage, "Award has an outstanding pending version.");
            awardStatus.setAwardId(oldAward.getAwardId());
        } else {
            oldAward = getActiveAward(awardNumber);
            if (oldAward == null) {
                logFailure(awardStatus, failureMessage, "Award does not have an active version.");
            } else {
                awardStatus.setAwardId(oldAward.getAwardId());
            }
        }
        return oldAward;
    }

    /**
     * Run the actual sync changes against the specific award in the hierarchy.
     * @param parentAward
     * @param hierarchy
     * @param syncType
     * @param changes
     * @param principalsToNotify
     * @throws RuntimeException
     */
    protected void runSyncChanges(Award parentAward, AwardHierarchy hierarchy, final SyncType syncType,
            List<AwardSyncChange> changes, final List<String> principalsToNotify) {
        long start = System.currentTimeMillis();
        LOG.debug("Award Hierarchy Sync Started for " + hierarchy.getAwardNumber());
        boolean result = true;
        UserSession oldSession = null;
        String successMessage = null;
        String failureMessage = null;
        if (syncType == SyncType.VALIDATE) {
            successMessage = VALIDATION_SUCCESS_MESSAGE;
            failureMessage = VALIDATION_FAILURE_MESSAGE;
        } else if (syncType == SyncType.SYNC) {
            successMessage = SYNC_SAVED_MESSAGE;
            failureMessage = SYNC_FAILURE_MESSAGE;
        }
        final AwardSyncStatus awardStatus = findAwardSyncStatus(parentAward, hierarchy.getAwardNumber());
        try {
            //make sure our session is that of the document submitter, we usually won't have a session here at all, but
            //save and restore the existing session in case.
            oldSession = replaceSessionWithRoutedBy(parentAward);
            if (!awardStatus.isSyncComplete()) {
                Award oldAward = checkAwardVersions(hierarchy.getAwardNumber(), awardStatus, failureMessage);
                if (oldAward != null) {
                    if (getAwardSyncSelectorService().isAwardInvolvedInSync(oldAward, changes)) {
                        //check to see if this award has already had this award sync applied and was completed successfully.
                        //If it has then the workflow step has probably been restarted due to an error and we will not
                        //redo the sync, otherwise clear status and logs from validation or previous runs.
                        if (!hasAwardPermission(awardStatus, oldAward, parentAward.getAwardDocument()
                                .getDocumentHeader().getWorkflowDocument().getRoutedByPrincipalId())) {
                            logFailure(awardStatus, failureMessage,
                                    "Sync submitter does not have modify permission on Award.");
                            return;
                        }

                        final AwardDocument awardDocument;
                        Award award = null;
                        if (syncType == SyncType.SYNC) {
                            awardDocument = versionAndPrepareAwardDocument(parentAward, oldAward, awardStatus);
                        } else {
                            awardDocument = loadAwardDocument(oldAward);
                        }
                        if (awardDocument == null) {
                            logFailure(awardStatus, failureMessage, "Unable to get document.");
                            return;
                        }
                        award = awardDocument.getAward();
                        result &= applyAndValidateChanges(award, awardStatus, changes);
                        if (result) {
                            finalizeAwardStatus(awardDocument, awardStatus, successMessage, syncType,
                                    principalsToNotify);
                            finalizeAward(awardDocument, awardStatus);
                        } else {
                            awardStatus.setStatus(failureMessage);
                            awardStatus.setSuccess(false);
                        }
                    } else {
                        awardStatus.setStatus("No changes");
                        awardStatus.setSuccess(true);
                    }
                }
            }
        } catch (Exception e) {
            LOG.error("Error applying sync", e);
            awardStatus.addValidationLog("Error applying sync. See system log for details.", false, null);
            awardStatus.setStatus(failureMessage);
            awardStatus.setSuccess(false);
            throw new RuntimeException(e);
        } finally {
            //persist status in another transaction to avoid any rollbacks
            saveInTransaction(awardStatus);
            GlobalVariables.setUserSession(oldSession);
            long elapsed = System.currentTimeMillis() - start;
            LOG.info("Finished award sync - " + hierarchy.getAwardNumber() + " : " + elapsed + "ms");
        }
    }

    /**
     * Apply the list of changes against the award and then validate the changes.
     * @param award
     * @param awardStatus
     * @param changes
     * @return
     */
    protected boolean applyAndValidateChanges(Award award, AwardSyncStatus awardStatus,
            List<AwardSyncChange> changes) {
        boolean result = true;
        for (AwardSyncChange change : changes) {
            if (getAwardSyncSelectorService().isChangeApplicableToAward(award, change)) {
                result &= applyChange(award, change, awardStatus);
            }
        }
        try {
            result &= validateModifiedAward(award.getAwardDocument(), awardStatus);
        } catch (Exception e) {
            LOG.error("Unable to validate award during sync.", e);
            result = false;
            awardStatus.addValidationLog("Validation failed due to unexpected error. See system log for details.",
                    false, null);
        }
        return result;
    }

    /**
     * Finalize the award sync. If running validation this only requires setting the
     * AwardSyncStatus to final. If running full sync the award is saved, FYI list generated
     * and award blanket approved.
     * @param awardDocument
     * @param awardStatus
     * @param successMessage
     * @param syncType
     * @param principalsToNotify
     * @throws WorkflowException
     */
    protected void finalizeAwardStatus(AwardDocument awardDocument, AwardSyncStatus awardStatus,
            String successMessage, SyncType syncType, List<String> principalsToNotify) throws WorkflowException {
        if (syncType == SyncType.SYNC) {
            getDocumentService().saveDocument(awardDocument);
            principalsToNotify.addAll(getNotificationList(awardDocument));
            awardStatus.setSyncComplete(true);
            awardStatus.setAwardId(awardDocument.getAward().getAwardId());
            awardStatus.refreshReferenceObject("award");
        }
        awardStatus.setStatus(successMessage);
        awardStatus.setSuccess(true);
    }

    /**
     * Attempts to load the award document for the specified award. This ensures that the workflow document is not null and 
     * can be used for validation.
     * @param awardStatus
     * @param award
     * @param errorMessage
     * @param runnables
     * @return
     */
    protected AwardDocument loadAwardDocument(Award award) {
        try {
            AwardDocument awardDocument = (AwardDocument) getDocumentService()
                    .getByDocumentHeaderId(award.getAwardDocument().getDocumentNumber());
            //minor hack for the pseudo award document that can possibly contain hundred of awards that would
            //then all be validated.
            awardDocument.getAwardList().clear();
            awardDocument.getAwardList().add(award);
            return awardDocument;
        } catch (WorkflowException e) {
            LOG.error("Unable to load award document.", e);
            return null;
        }
    }

    /**
     * Set the status to failure and set the status to the message and add the log message.
     * @param awardStatus
     * @param statusMessage
     * @param logMessage
     */
    protected void logFailure(AwardSyncStatus awardStatus, String statusMessage, String logMessage) {
        awardStatus.setStatus(statusMessage);
        awardStatus.setSuccess(false);
        awardStatus.addValidationLog(logMessage, false, null);
    }

    /**
     * Ensure that the person specified by principalId has modify permission on the award specified.
     * @param awardStatus
     * @param award
     * @param principalId
     * @param errorMessage
     * @param runnables
     * @return
     */
    protected boolean hasAwardPermission(AwardSyncStatus awardStatus, Award award, String principalId) {
        return getKraAuthorizationService().hasPermission(principalId, award,
                AwardPermissionConstants.MODIFY_AWARD.getAwardPermission());
    }

    /**
     * Run the {@link AwardDocumentRule#processSaveDocument} and {@link AwardDocumentRule#processRunAuditBusinessRules}
     * against award. Add all messages generated from running the rules to logList as {@link AwardSyncLog}. 
     * Return false if any of the error keys generated by the rules are not in the ignoredMessageKeys list.
     * @param award
     * @param logs
     * @return
     */
    protected boolean validateModifiedAward(AwardDocument award, AwardSyncStatus awardStatus) {
        AwardDocumentRule rule = new AwardDocumentRule();
        boolean result = true;
        if (!rule.processSaveDocument(award)) {
            getAwardSyncUtilityService().getLogsFromSaveErrors(awardStatus);
        }
        if (!rule.processRunAuditBusinessRules(award)) {
            getAwardSyncUtilityService().getLogsFromAuditErrors(awardStatus);
        }
        String[] ignoredErrors = IGNORED_MESSAGE_KEYS.split(",");
        for (AwardSyncLog log : awardStatus.getValidationLogs()) {
            if (!StringUtils.startsWithAny(log.getMessageKey(), ignoredErrors) && !log.isSuccess()) {
                result = false;
            } else {
                log.setSuccess(true);
            }
        }
        return result;
    }

    /**
     * Apply the specific change to the award using the specified syncHelper.
     * @param award
     * @param syncHelper
     * @param change
     * @throws Exception
     */
    protected void applyChange(Award award, AwardSyncHelper syncHelper, AwardSyncChange change) throws Exception {
        if (getAwardSyncSelectorService().isChangeApplicableToAward(award, change)) {
            syncHelper.applySyncChange(award, change);
        }
    }

    /**
     * Get the syncHelper from the syncHelpers for the given className. If the className is a full qualified class name,
     * only the class name itself is used.
     * @param className
     * @return
     */
    protected AwardSyncHelper getSyncHelper(String className) {
        return getAwardSyncHelpersService().getSyncHelper(className);
    }

    /**
     * Get the active version of an award if it exists.
     * @param awardNumber
     * @return
     */
    protected Award getActiveAward(String awardNumber) {
        VersionHistory vh = getVersionHistoryService().findActiveVersion(Award.class, awardNumber);
        if (vh != null) {
            return (Award) vh.getSequenceOwner();
        } else {
            return null;
        }
    }

    /**
     * Get the pending version for the award if it exists.
     * @param awardNumber
     * @return
     */
    protected Award getPendingAward(String awardNumber) {
        List<VersionHistory> histories = getVersionHistoryService().loadVersionHistory(Award.class, awardNumber);
        for (VersionHistory history : histories) {
            if (history.getStatus() == VersionStatus.PENDING) {
                return (Award) history.getSequenceOwner();
            }
        }
        return null;
    }

    /**
     * Wait forever until all runnables are finished.
     * @param runnables
     * @throws AwardSyncException 
     */
    protected void waitTillRunablesFinished(List<SyncRunnable> runnables) throws AwardSyncException {
        int numberOfWaits = 0;
        while (!areRunnablesFinished(runnables)) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
            }
            if (numberOfWaits++ > 10000) {
                throw new AwardSyncException("Runnables have failed to complete in a reasonable amount of time.",
                        false);
            }
        }
    }

    /**
     * 
     * Collects all prinicipalsToNotify from {@link #RunSyncOnAward} runnables in the list
     * of runnables.
     * @param runnables
     * @return
     */
    protected List<String> getAllPrincipalsToNotify(List<SyncRunnable> runnables) {
        List<String> allPrincipalsToNotify = new ArrayList<String>();
        for (SyncRunnable runnable : runnables) {
            if (runnable instanceof RunSyncOnAward) {
                for (String principalId : ((RunSyncOnAward) runnable).getPrincipalsToNotify()) {
                    if (!allPrincipalsToNotify.contains(principalId)) {
                        allPrincipalsToNotify.add(principalId);
                    }
                }
            }
        }

        return allPrincipalsToNotify;
    }

    /**
     * 
     * Loops through all runnables and check their finished flags.
     * @param runnables
     * @return
     */
    protected synchronized boolean areRunnablesFinished(List<SyncRunnable> runnables) {
        boolean finished = true;
        ListIterator<SyncRunnable> iter = runnables.listIterator();
        while (iter.hasNext()) {
            SyncRunnable runnable = iter.next();
            if (!runnable.getFinished()) {
                finished = false;
            }
        }
        return finished;
    }

    /**
     * Apply single sync change to award.
     * @param parentAward
     * @param award
     * @param change
     * @param logList
     * @return
     */
    protected boolean applyChange(Award award, AwardSyncChange change, AwardSyncStatus awardStatus) {
        boolean result = true;
        try {
            applyChange(award, getSyncHelper(change.getClassName()), change);
            awardStatus.addChangeLog(change, CHANGE_LOG_SUCCESS, true);
        } catch (AwardSyncException e) {
            result &= e.isSuccess();
            awardStatus.addChangeLog(change, e.getStatusMessage(), e.isSuccess());
        } catch (Exception e) {
            LOG.error("Error applying changes to award " + award.getAwardNumber(), e);
            result = false;
            awardStatus.addChangeLog(change, "Error", false);
        }
        return result;
    }

    /**
     * Create new version of oldAward and prepare document for sync by setting document
     * description, transaction type, etc.
     * @param parentAward
     * @param oldAward
     * @param logList
     * @return
     */
    protected AwardDocument versionAndPrepareAwardDocument(Award parentAward, Award oldAward,
            AwardSyncStatus awardStatus) {
        AwardDocument awardDocument = oldAward.getAwardDocument();
        //hack to work around placeholder award document that can have many many awards attached to it
        //and we need the specific award to be the only one on the document.
        awardDocument.getAwardList().clear();
        awardDocument.getAwardList().add(oldAward);
        AwardDocument newAwardDocument = null;
        try {
            newAwardDocument = getAwardService().createNewAwardVersion(awardDocument);
        } catch (Exception e) {
            LOG.error("Sync Failure while trying to version Award " + oldAward.getAwardNumber(), e);
            awardStatus.setStatus(SYNC_FAILURE_MESSAGE + "(Versioning Error)");
            awardStatus.setSuccess(false);
            throw new RuntimeException(e);
        }
        Award award = newAwardDocument.getAward();
        newAwardDocument.getDocumentHeader().setDocumentDescription(
                "Created by Award " + parentAward.getAwardNumber() + " Ver " + parentAward.getSequenceNumber());
        award.setAwardTransactionTypeCode(parentAward.getAwardTransactionTypeCode());
        award.setNoticeDate(parentAward.getNoticeDate());
        award.getAwardCurrentActionComments()
                .setComments("Synchronize Descendants from Award " + parentAward.getAwardNumber());
        return newAwardDocument;
    }

    /**
     * Save award and generate a list of workflow recipients as if this document
     * were normally routed to later be used in an FYI list of the parent award.
     * @param awardDocument
     * @return
     * @throws WorkflowException
     */
    protected List<String> getNotificationList(AwardDocument awardDocument) throws WorkflowException {
        List<String> principalsToNotify = new ArrayList<String>();
        Award award = awardDocument.getAward();
        //save as a normal award
        award.setSyncChild(false);
        getDocumentService().saveDocument(awardDocument);
        principalsToNotify.addAll(getAwardSyncUtilityService().buildListForFYI(awardDocument));
        //set to hierarchy sync child to avoid any requests being sent during blanket approve
        //and resave as it will recreate the workflow doc for the correct user
        award.setSyncChild(true);
        getDocumentService().saveDocument(awardDocument);
        return principalsToNotify;
    }

    /**
     * Blanket Approve the award document.
     * @param newAwardDocument
     * @throws WorkflowException
     */
    protected void finalizeAward(AwardDocument awardDocument, AwardSyncStatus awardSyncStatus) {
        try {
            if (awardSyncStatus.isSuccess() && awardSyncStatus.isSyncComplete()) {
                awardDocument = (AwardDocument) getDocumentService()
                        .getByDocumentHeaderId(awardDocument.getDocumentNumber());
                getDocumentService().blanketApproveDocument(awardDocument, "Award Hierarchy Sync Routed Document",
                        null);
                awardSyncStatus.setStatus(SYNC_SUCCESS_MESSAGE);
            }
        } catch (Exception e) {
            awardSyncStatus.setSyncComplete(false);
            logFailure(awardSyncStatus, SYNC_FAILURE_MESSAGE, "Failure occured while approving document.");
            throw new RuntimeException(e);
        }
    }

    /**
     * Save or delete object in a separate thread using {@link #SaveBo}.
     * @param object
     * @param delete
     * @param runnables
     */
    protected void saveInTransaction(final Object object) {
        TransactionTemplate template = new TransactionTemplate(
                (PlatformTransactionManager) KcServiceLocator.getService("transactionManager"));
        template.setPropagationBehavior(Propagation.REQUIRES_NEW.value());
        template.execute(new TransactionCallback() {
            @SuppressWarnings("unchecked")
            public Object doInTransaction(TransactionStatus status) {
                if (object instanceof PersistableBusinessObject) {
                    getBusinessObjectService().save((PersistableBusinessObject) object);
                } else if (object instanceof List) {
                    getBusinessObjectService().save((List) object);
                }
                return null;
            }
        });
    }

    /**
     * Run the runnable in a separate transaction, commiting when finished or rolling back
     * if an exception is generated.
     * @param runnable
     */
    protected void runInTransaction(final TransactionRunnable runnable) {
        final UserSession session = GlobalVariables.getUserSession();
        syncExecutor.execute(new SyncRunnable() {
            public void run() {
                GlobalVariables.setUserSession(session);
                TransactionTemplate template = new TransactionTemplate(
                        (PlatformTransactionManager) KcServiceLocator.getService("transactionManager"));
                template.setPropagationBehavior(Propagation.REQUIRES_NEW.value());
                template.execute(new TransactionCallback() {
                    public Object doInTransaction(TransactionStatus status) {
                        runnable.run();
                        return null;
                    }
                });
            }
        });
    }

    /**
     * 
     * Run {@link AwardSyncServiceImpl#applyChanges} through the use of {@link #RunSyncOnAward}.
     * @param parentAward
     * @param currentVersion
     * @param changes
     * @param runnables
     */
    protected void runSyncInThread(Award parentAward, AwardHierarchy hierarchy, SyncType syncType,
            List<AwardSyncChange> changes, List<SyncRunnable> runnables) {
        SyncRunnable newRunnable = new RunSyncOnAward(parentAward, hierarchy, syncType, changes);
        runnables.add(newRunnable);
        getSyncExecutor().execute(newRunnable);
    }

    /**
     * 
     * Send ad hoc requests in a thread. This is used to make sure the requests are
     * processed before the document moves to the next step in workflow and then the 
     * requests get ignored.
     * @param document
     * @param annotation
     * @param recipients
     * @param runnables
     */
    protected void sendAdHocRequestsInThread(AwardDocument document, String annotation,
            List<AdHocRouteRecipient> recipients, List<SyncRunnable> runnables) {
        SyncRunnable newRunnable = new SendAdHocRequests(document, annotation, recipients,
                GlobalVariables.getUserSession());
        runnables.add(newRunnable);
        getSyncExecutor().execute(newRunnable);

    }

    /**
     * Validation or running full sync
     */
    protected enum SyncType {
        SYNC(), VALIDATE();
    }

    /**
     * Abstract class that is used to generate runnables to run in transactions.
     */
    protected abstract class TransactionRunnable {
        public abstract void run();
    }

    /**
     * Parent for thread based runnables related to award sync to provide a finished flag that
     * supports ensuring all threads are finished.
     */
    protected abstract class SyncRunnable implements Runnable {
        protected boolean finished;

        public void run() {
            setFinished(true);
        }

        public synchronized boolean getFinished() {
            return finished;
        }

        public synchronized void setFinished(boolean finished) {
            this.finished = finished;
        }
    }

    /**
     * 
     * Runnable that calls {@link AwardSyncServiceImpl#applyChanges} in a separate thread.
     */
    protected class RunSyncOnAward extends SyncRunnable {
        protected Award parentAward;
        protected AwardHierarchy hierarchy;
        protected SyncType syncType;
        protected List<AwardSyncChange> changes;
        protected List<String> principalsToNotify = new ArrayList<String>();

        public RunSyncOnAward(Award parentAward, AwardHierarchy hierarchy, SyncType syncType,
                List<AwardSyncChange> changes) {
            this.parentAward = parentAward;
            this.hierarchy = hierarchy;
            this.syncType = syncType;
            this.changes = changes;
        }

        @Override
        public synchronized void run() {
            try {
                TransactionTemplate template = new TransactionTemplate(
                        (PlatformTransactionManager) KcServiceLocator.getService("transactionManager"));
                template.setPropagationBehavior(Propagation.REQUIRES_NEW.value());
                template.execute(new TransactionCallback() {
                    public Object doInTransaction(TransactionStatus status) {
                        runSyncChanges(parentAward, hierarchy, syncType, changes, principalsToNotify);
                        return null;
                    }
                });
            } catch (Exception e) {
                //error message has been caught and handled, but must be thrown to rollback
                //transaction.
            } finally {
                super.run();
            }
        }

        public synchronized List<String> getPrincipalsToNotify() {
            return principalsToNotify;
        }

    }

    /**
     * Runnable that sends a list of ad hoc requests in a separate thread.
     */
    protected class SendAdHocRequests extends SyncRunnable {
        protected AwardDocument awardDocument;
        protected String annotation;
        protected List<AdHocRouteRecipient> recipients;
        protected UserSession session;

        public SendAdHocRequests(AwardDocument awardDocument, String annotation,
                List<AdHocRouteRecipient> recipients, UserSession session) {
            this.awardDocument = awardDocument;
            this.annotation = annotation;
            this.recipients = recipients;
            this.session = session;
        }

        public void run() {
            UserSession oldSession = GlobalVariables.getUserSession();
            try {
                GlobalVariables.setUserSession(session);
                getDocumentService().sendAdHocRequests(awardDocument, annotation, recipients);
            } catch (WorkflowException e) {
                LOG.error("Error sending Ad Hoc requests for Award Sync", e);
            } finally {
                super.run();
                GlobalVariables.setUserSession(oldSession);
            }
        }
    }

    protected AwardHierarchyService getAwardHierarchyService() {
        return awardHierarchyService;
    }

    public void setAwardHierarchyService(AwardHierarchyService awardHierarchyService) {
        this.awardHierarchyService = awardHierarchyService;
    }

    protected AwardService getAwardService() {
        return awardService;
    }

    public void setAwardService(AwardService awardService) {
        this.awardService = awardService;
    }

    protected VersionHistoryService getVersionHistoryService() {
        return versionHistoryService;
    }

    public void setVersionHistoryService(VersionHistoryService versionHistoryService) {
        this.versionHistoryService = versionHistoryService;
    }

    protected BusinessObjectService getBusinessObjectService() {
        return businessObjectService;
    }

    public void setBusinessObjectService(BusinessObjectService businessObjectService) {
        this.businessObjectService = businessObjectService;
    }

    protected DocumentService getDocumentService() {
        return documentService;
    }

    public void setDocumentService(DocumentService documentService) {
        this.documentService = documentService;
    }

    protected TaskExecutor getSyncExecutor() {
        return syncExecutor;
    }

    public void setSyncExecutor(TaskExecutor syncExecutor) {
        this.syncExecutor = syncExecutor;
    }

    protected AwardSyncSelectorService getAwardSyncSelectorService() {
        return awardSyncSelectorService;
    }

    public void setAwardSyncSelectorService(AwardSyncSelectorService awardSyncSelectorService) {
        this.awardSyncSelectorService = awardSyncSelectorService;
    }

    @SuppressWarnings("unchecked")
    protected PersonService getPersonService() {
        return personService;
    }

    @SuppressWarnings("unchecked")
    public void setPersonService(PersonService personService) {
        this.personService = personService;
    }

    protected KcWorkflowService getKraWorkflowService() {
        return kraWorkflowService;
    }

    public void setKraWorkflowService(KcWorkflowService kraWorkflowService) {
        this.kraWorkflowService = kraWorkflowService;
    }

    protected AwardSyncHelpersService getAwardSyncHelpersService() {
        return awardSyncHelpersService;
    }

    public void setAwardSyncHelpersService(AwardSyncHelpersService awardSyncHelpersService) {
        this.awardSyncHelpersService = awardSyncHelpersService;
    }

    protected AwardSyncCreationService getAwardSyncCreationService() {
        return awardSyncCreationService;
    }

    public void setAwardSyncCreationService(AwardSyncCreationService awardSyncCreationService) {
        this.awardSyncCreationService = awardSyncCreationService;
    }

    protected AwardSyncUtilityService getAwardSyncUtilityService() {
        return awardSyncUtilityService;
    }

    public void setAwardSyncUtilityService(AwardSyncUtilityService awardSyncUtilityService) {
        this.awardSyncUtilityService = awardSyncUtilityService;
    }

    protected KcAuthorizationService getKraAuthorizationService() {
        return kraAuthorizationService;
    }

    public void setKraAuthorizationService(KcAuthorizationService kraAuthorizationService) {
        this.kraAuthorizationService = kraAuthorizationService;
    }
}