org.kuali.coeus.common.committee.impl.rules.CommitteeDocumentRuleBase.java Source code

Java tutorial

Introduction

Here is the source code for org.kuali.coeus.common.committee.impl.rules.CommitteeDocumentRuleBase.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.coeus.common.committee.impl.rules;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.kuali.coeus.common.committee.impl.bo.*;
import org.kuali.coeus.common.committee.impl.document.CommitteeDocumentBase;
import org.kuali.coeus.common.committee.impl.lookup.keyvalue.CommitteeIdValuesFinderBase;
import org.kuali.coeus.common.committee.impl.rule.AddCommitteeMembershipRoleRule;
import org.kuali.coeus.common.committee.impl.rule.AddCommitteeMembershipRule;
import org.kuali.coeus.common.committee.impl.rule.event.*;
import org.kuali.coeus.common.committee.impl.rule.event.CommitteeScheduleEventBase.ErrorType;
import org.kuali.coeus.common.framework.custom.KcDocumentBaseAuditRule;
import org.kuali.coeus.common.framework.unit.Unit;
import org.kuali.coeus.common.framework.unit.UnitService;
import org.kuali.coeus.sys.framework.rule.KcBusinessRule;
import org.kuali.coeus.sys.framework.rule.KcDocumentEventBaseExtension;
import org.kuali.coeus.sys.framework.rule.KcTransactionalDocumentRuleBase;
import org.kuali.coeus.sys.framework.service.KcServiceLocator;
import org.kuali.coeus.sys.framework.validation.ErrorReporter;
import org.kuali.kra.infrastructure.Constants;
import org.kuali.kra.infrastructure.KeyConstants;
import org.kuali.rice.core.api.util.KeyValue;
import org.kuali.rice.kew.api.KewApiConstants;
import org.kuali.rice.kew.api.exception.WorkflowException;
import org.kuali.rice.kew.routeheader.service.RouteHeaderService;
import org.kuali.rice.krad.bo.PersistableBusinessObject;
import org.kuali.rice.krad.document.Document;
import org.kuali.rice.krad.rules.rule.DocumentAuditRule;
import org.kuali.rice.krad.service.BusinessObjectService;
import org.kuali.rice.krad.service.DocumentService;
import org.kuali.rice.krad.service.KRADServiceLocator;

import java.sql.Date;
import java.util.ArrayList;
import java.util.List;

/**
 * This is the main business rule class for the CommitteeBase Document.  It
 * is responsible for customized workflow related business rule checking such
 * saving, routing, etc.  All committee specific actions, e.g. adding members,
 * this class will act as a controller and forward the rules checking to 
 * another class within this package.
 */
@SuppressWarnings("unchecked")
public abstract class CommitteeDocumentRuleBase extends KcTransactionalDocumentRuleBase
        implements KcBusinessRule, AddCommitteeMembershipRule, AddCommitteeMembershipRoleRule, DocumentAuditRule {

    private static final String PROPERTY_NAME_TERM_START_DATE = "document.committeeList[0].committeeMemberships[%1$s].termStartDate";
    private static final String PROPERTY_NAME_TERM_END_DATE = "document.committeeList[0].committeeMemberships[%1$s].termEndDate";
    private static final String PROPERTY_NAME_ROLE_CODE_ADD = "committeeHelper.newCommitteeMembershipRoles[%1$s].membershipRoleCode";
    private static final String PROPERTY_NAME_ROLE_CODE = "document.committeeList[0].committeeMemberships[%1$s].membershipRoles[%2$s].membershipRoleCode";
    private static final String PROPERTY_NAME_ROLE_START_DATE = "document.committeeList[0].committeeMemberships[%1$s].membershipRoles[%2$s].startDate";
    private static final String PROPERTY_NAME_ROLE_END_DATE = "document.committeeList[0].committeeMemberships[%1$s].membershipRoles[%2$s].endDate";
    private static final String PROPERTY_NAME_RESEARCH_AREA_CODE = "committeeHelper.newCommitteeMembershipExpertise[%1$s].researchAreaCode";
    private static final Log LOG = LogFactory.getLog(CommitteeDocumentRuleBase.class);

    private static final String SEPERATOR = ".";
    private static final String PROPERTY_NAME_INACTIVE_AREAS_OF_EXPERTISE_PREFIX = "document.committeeList[0].committeeMemberships[%1$s].areasOfExpertise.inactive";
    private static final String COMMITTEE_ID_FIELD = "document.committeeList[0].committeeId";
    private static final String COMMITTEE_NAME_FIELD = "document.committeeList[0].committeeName";
    private static final String COMMITTEE_HOME_UNIT_NUMBER_FIELD = "document.committeeList[0].homeUnitNumber";
    private static final String REVIEW_TYPE_ERROR_PROPERTY_NAME = "document.committeeList[0].reviewTypeCode";

    private static final String INACTIVE_RESEARCH_AREAS_PREFIX = "document.committeeList[0].committeeResearchAreas.inactive";

    @Override
    protected boolean processCustomRouteDocumentBusinessRules(Document document) {
        boolean retval = true;

        retval &= super.processCustomRouteDocumentBusinessRules(document);

        return retval;
    }

    @Override
    public boolean processCustomSaveDocumentBusinessRules(Document document) {
        boolean valid = true;

        valid &= validateCommitteeId((CommitteeDocumentBase) document);
        valid &= validateUniqueCommitteeId((CommitteeDocumentBase) document);
        valid &= validateUniqueCommitteeName((CommitteeDocumentBase) document);
        valid &= validateHomeUnit((CommitteeDocumentBase) document);
        valid &= validateCommitteeTypeSpecificData((CommitteeDocumentBase) document);
        valid &= validateCommitteeMemberships((CommitteeDocumentBase) document);
        valid &= processScheduleRules((CommitteeDocumentBase) document);

        return valid;
    }

    /**
     * This method will validate the committee data w.r.t constraints that are specific to committee type that was chosen.
     * Currently these constraints are 1. (for IRB only) all research areas that are chosen must be active, and
     * 2. the proper review type corresponding to the committee type is chosen.
     * @param document
     * @return
     */
    private boolean validateCommitteeTypeSpecificData(CommitteeDocumentBase document) {
        boolean valid = true;
        // delegate actual validation logic to the business logic wrapper
        valid &= validateCommitteeResearchAreas(document.getCommittee());
        // delegate actual validation logic to the business logic wrapper
        valid &= validateReviewType(document.getCommittee());
        return valid;
    }

    /**
     * Verify that the committee id is not the DEFAULT_CORRESPONDENCE_TEMPLATE constant.  
     * This value is reserved for the default protocol correspondence template.
     * @param document CommitteeBase Document
     * @return true if valid; otherwise false
     */
    private boolean validateCommitteeId(CommitteeDocumentBase document) {
        CommitteeBase committee = document.getCommittee();
        if (StringUtils.equalsIgnoreCase(committee.getCommitteeId(), Constants.DEFAULT_CORRESPONDENCE_TEMPLATE)) {
            reportError(COMMITTEE_ID_FIELD, KeyConstants.ERROR_COMMITTEE_INVALID_ID);
            return false;
        } else {
            return true;
        }
    }

    /**
     * Verify that we are not saving a committee with a duplicate CommitteeBase ID.
     * In other words, each committee must have a unique CommitteeBase ID.
     * @param document CommitteeBase Document
     * @return true if valid; otherwise false
     */
    private boolean validateUniqueCommitteeId(CommitteeDocumentBase document) {

        CommitteeBase committee = document.getCommittee();
        boolean valid = true;
        if (committee.getSequenceNumber() == 1 && (document.getDocumentHeader().getWorkflowDocument().isInitiated()
                || document.getDocumentHeader().getWorkflowDocument().isSaved())) {
            if (getCommitteeIds(document.getDocumentNumber()).contains(committee.getCommitteeId())) {
                valid = false;
            } else {
                // TODO : when committeeId & docstatuscode are populated properly, then the following is not needed.
                try {
                    for (CommitteeDocumentBase workflowCommitteeDocument : getCommitteesDocumentsFromWorkflow(
                            document.getDocumentNumber())) {

                        CommitteeBase workflowCommittee = workflowCommitteeDocument.getCommittee();
                        LOG.info("get doc content for doc " + workflowCommitteeDocument.getDocumentNumber());
                        // There is no conflict if we are only modifying the same committee.

                        //if (!StringUtils.equals(workflowCommitteeDocument.getDocumentNumber(), document.getDocumentNumber())) {

                        // We have a conflict if we find a different committee in the database
                        // and it has the same ID as the committee we are trying to save
                        // while it's not a older version (lower sequence number) of this committee.

                        if (StringUtils.equals(workflowCommittee.getCommitteeId(), committee.getCommitteeId())
                                && (workflowCommittee.getSequenceNumber() >= committee.getSequenceNumber())) {
                            valid = false;
                        }
                        //}
                    }
                } catch (WorkflowException e) {
                    LOG.info(e.getMessage());
                }
            }
        }
        if (!valid) {
            reportError(COMMITTEE_ID_FIELD, KeyConstants.ERROR_COMMITTEE_DUPLICATE_ID);
        }
        return valid;
    }

    private List<CommitteeDocumentBase> getCommitteesDocumentsFromWorkflow(String docNumber)
            throws WorkflowException {

        List<CommitteeDocumentBase> documents = (List<CommitteeDocumentBase>) KcServiceLocator
                .getService(BusinessObjectService.class).findAll(getCommitteeDocumentBOClassHook());
        List<CommitteeDocumentBase> result = new ArrayList<CommitteeDocumentBase>();
        for (CommitteeDocumentBase commDoc : documents) {
            // documents that have not been approved
            if ((commDoc.getCommitteeList() == null || commDoc.getCommitteeList().size() == 0)
                    && StringUtils.isBlank(commDoc.getCommitteeId())
                    && !StringUtils.equals(commDoc.getDocumentNumber(), docNumber)) {
                // Need this step to retrieve workflow document

                CommitteeDocumentBase workflowCommitteeDoc = (CommitteeDocumentBase) KcServiceLocator
                        .getService(DocumentService.class).getByDocumentHeaderId(commDoc.getDocumentNumber());
                // Get XML of workflow document
                String content = KcServiceLocator.getService(RouteHeaderService.class)
                        .getContent(workflowCommitteeDoc.getDocumentHeader().getWorkflowDocument().getDocumentId())
                        .getDocumentContent();

                // Create committee from XML and add to the document
                workflowCommitteeDoc.getCommitteeList().add(populateCommitteeFromXmlDocumentContents(content));
                if (!workflowCommitteeDoc.getDocumentHeader().getWorkflowDocument().isCanceled()) {
                    result.add(workflowCommitteeDoc);
                }
            }
        }
        return result;
    }

    protected abstract Class<? extends CommitteeDocumentBase> getCommitteeDocumentBOClassHook();

    /*
     * get a list of committeeIds that are in approved or saved committee docs.
     */
    private List<String> getCommitteeIds(String docNumber) {

        List<String> result = new ArrayList<String>();
        List<CommitteeDocumentBase> committeeDocss = (List<CommitteeDocumentBase>) KcServiceLocator
                .getService(BusinessObjectService.class).findAll(getCommitteeDocumentBOClassHook());
        for (CommitteeDocumentBase committeeDoc : committeeDocss) {
            if (StringUtils.isNotBlank(committeeDoc.getCommitteeId())
                    && !result.contains(committeeDoc.getCommitteeId())
                    && StringUtils.isNotBlank(committeeDoc.getDocStatusCode())
                    && !committeeDoc.getDocStatusCode().equals(KewApiConstants.ROUTE_HEADER_CANCEL_CD)
                    && !StringUtils.equals(committeeDoc.getDocumentNumber(), docNumber)) {
                result.add(committeeDoc.getCommitteeId());
            }
        }
        return result;
    }

    /*
     * Create a CommitteeBase object and populate it from the xml.
     */
    private CommitteeBase populateCommitteeFromXmlDocumentContents(String xmlDocumentContents) {
        CommitteeBase committee = null;
        if (!StringUtils.isEmpty(xmlDocumentContents)) {

            committee = (CommitteeBase) getBusinessObjectFromXML(xmlDocumentContents,
                    getCommitteeBOClassHook().getName());

        }
        return committee;
    }

    protected abstract Class<? extends CommitteeBase> getCommitteeBOClassHook();

    /**
     * Retrieves substring of document contents from maintainable tag name. Then use xml service to translate xml into a business
     * object.
     */
    private PersistableBusinessObject getBusinessObjectFromXML(String xmlDocumentContents, String objectTagName) {
        String objXml = StringUtils.substringBetween(xmlDocumentContents, "<" + objectTagName + ">",
                "</" + objectTagName + ">");
        objXml = "<" + objectTagName + ">" + objXml + "</" + objectTagName + ">";
        PersistableBusinessObject businessObject = (PersistableBusinessObject) KRADServiceLocator
                .getXmlObjectSerializerService().fromXml(objXml);
        return businessObject;
    }

    /**
     * Verify that we are not saving a committee with a duplicate CommitteeBase Name.
     * In other words, each committee must have a unique CommitteeBase Name.
     * @param document CommitteeBase Document
     * @return true if valid; otherwise false
     */
    private boolean validateUniqueCommitteeName(CommitteeDocumentBase document) {
        CommitteeBase committee = document.getCommittee();
        CommitteeIdValuesFinderBase committeeIdValuesFinder = getNewCommitteeIdValuesFinderInstanceHook();
        List<KeyValue> committeeIdNamePairList = committeeIdValuesFinder.getKeyValues();
        for (KeyValue committeeIdNamePair : committeeIdNamePairList) {
            if (StringUtils.equalsIgnoreCase(committeeIdNamePair.getValue(), committee.getCommitteeName())
                    && StringUtils.isNotBlank((String) committeeIdNamePair.getKey()) && !StringUtils
                            .equalsIgnoreCase((String) committeeIdNamePair.getKey(), committee.getCommitteeId())) {
                reportError(COMMITTEE_NAME_FIELD, KeyConstants.ERROR_COMMITTEE_DUPLICATE_NAME);
                return false;
            }
        }

        return true;
    }

    protected abstract CommitteeIdValuesFinderBase getNewCommitteeIdValuesFinderInstanceHook();

    /**
     * Verify that the unit number if is valid.  We can ignore a blank
     * home unit number since it is a required field and that business logic
     * will flag a blank value as invalid.
     * @param document the CommitteeBase document
     * @return true if valid; otherwise false
     */
    private boolean validateHomeUnit(CommitteeDocumentBase document) {

        boolean valid = true;

        String homeUnitNumber = document.getCommittee().getHomeUnitNumber();
        if (!StringUtils.isBlank(homeUnitNumber)) {
            UnitService unitService = KcServiceLocator.getService(UnitService.class);
            Unit homeUnit = unitService.getUnit(homeUnitNumber);
            if (homeUnit == null) {
                valid = false;
                reportError(COMMITTEE_HOME_UNIT_NUMBER_FIELD, KeyConstants.ERROR_INVALID_UNIT, homeUnitNumber);
            }
        }

        return valid;
    }

    private boolean validateCommitteeMemberships(CommitteeDocumentBase committeeDocument) {
        boolean isValid = true;
        List<CommitteeMembershipBase> committeeMemberships = committeeDocument.getCommittee()
                .getCommitteeMemberships();

        for (CommitteeMembershipBase committeeMembership : committeeMemberships) {
            int membershipIndex = committeeMemberships.indexOf(committeeMembership);
            isValid &= isValidTermStartEndDates(committeeMembership, membershipIndex);
            isValid &= isValidRoles(committeeMembership, membershipIndex);
            isValid &= hasExpertise(committeeMembership, membershipIndex);
            // To keep the errors more comprehensible the role overlap check is done after other errors are resolved
            if (isValid) {
                isValid &= hasNoTermOverlap(committeeMemberships, committeeMembership, membershipIndex);
            }
            isValid &= checkResearchAreasForCommitteeMember(committeeMembership, membershipIndex);
        }
        return isValid;
    }

    /**
     * Check that the person does not have other entries whose term overlap.
     * (A member may not have the same role for overlapping time periods.)
     *
     * Checks are done only against records that are ahead of this one since these
     * have passes validations and therefore have valid term dates.
     * This method also displays the appropriate error message.
     * 
     * @param committeeMemberships - the committee memberships of the current committee
     * @param committeeMembership - the committee membership which contains the to be validated data
     * @param membershipIndex - the index position of the committeeMembership
     * @return <code>true</code> if the role does not overlap with another role, <code>false</code> otherwise
     */
    private boolean hasNoTermOverlap(List<CommitteeMembershipBase> committeeMemberships,
            CommitteeMembershipBase committeeMembership, int membershipIndex) {
        boolean isValid = true;

        for (int i = 0; i < membershipIndex; i++) {
            CommitteeMembershipBase tmpMember = committeeMemberships.get(i);
            if (tmpMember.isSamePerson(committeeMembership) && committeeMembership.getTermStartDate() != null
                    && committeeMembership.getTermEndDate() != null && tmpMember.getTermStartDate() != null
                    && tmpMember.getTermEndDate() != null) {
                if (isWithinPeriod(committeeMembership.getTermStartDate(), tmpMember.getTermStartDate(),
                        tmpMember.getTermEndDate())) {
                    isValid = false;
                    reportError(String.format(PROPERTY_NAME_TERM_START_DATE, membershipIndex),
                            KeyConstants.ERROR_COMMITTEE_MEMBERSHIP_PERSON_DUPLICATE,
                            tmpMember.getFormattedTermStartDate(), tmpMember.getFormattedTermEndDate());
                } else if (isWithinPeriod(committeeMembership.getTermEndDate(), tmpMember.getTermStartDate(),
                        tmpMember.getTermEndDate())) {
                    isValid = false;
                    reportError(String.format(PROPERTY_NAME_TERM_END_DATE, membershipIndex),
                            KeyConstants.ERROR_COMMITTEE_MEMBERSHIP_PERSON_DUPLICATE,
                            tmpMember.getFormattedTermStartDate(), tmpMember.getFormattedTermEndDate());
                }
            }
        }

        return isValid;
    }

    /**
     * Verify the Term Start and Term End dates
     * 
     * Date validation is done by the data dictionary.  
     * Validate that Term End date is greater than or equal to the Term Start date.
     * 
     * This method also displays the appropriate error message.
     * 
     * @param committeeMembership - the committeeMembership which contains the to be validated data
     * @param membershipIndex - the index position of the committeeMembership
     * @return <code>true</code> if the term start and end dates are valid, <code>false</code> otherwise
     */
    private boolean isValidTermStartEndDates(CommitteeMembershipBase committeeMembership, int membershipIndex) {
        boolean isValid = true;

        if (committeeMembership.getTermStartDate() != null && committeeMembership.getTermEndDate() != null
                && committeeMembership.getTermEndDate().before(committeeMembership.getTermStartDate())) {
            isValid = false;
            reportError(String.format(PROPERTY_NAME_TERM_END_DATE, membershipIndex),
                    KeyConstants.ERROR_COMMITTEE_MEMBERSHIP_TERM_END_DATE_BEFORE_TERM_START_DATE);
        }

        return isValid;
    }

    /**
     * Verify Roles
     * 
     * @param committeeMembership - the committeeMembership which contains the to be validated data
     * @param membershipIndex - the index position of the committeeMembership
     * @return <code>true</code> if the roles are valid, <code>false</code> otherwise
     */
    private boolean isValidRoles(CommitteeMembershipBase committeeMembership, int membershipIndex) {
        boolean isValid = true;
        List<CommitteeMembershipRole> membershipRoles = committeeMembership.getMembershipRoles();

        isValid &= hasRoles(committeeMembership, membershipIndex);

        for (CommitteeMembershipRole membershipRole : membershipRoles) {
            int roleIndex = membershipRoles.indexOf(membershipRole);
            isValid &= isValidRoleStartEndDates(membershipRole, membershipIndex, roleIndex);
            isValid &= roleDatesWithinTermDates(committeeMembership, membershipRole, membershipIndex, roleIndex);
            // To keep the errors more comprehensible the role overlap check is done after other errors are resolved
            if (isValid) {
                isValid &= hasNoRoleOverlap(committeeMembership, membershipRole, membershipIndex, roleIndex);
            }
        }

        return isValid;
    }

    /**
     * Verify that the committee membership has at least one role assigned.
     *  
     * This method also displays the appropriate error message.
     * 
     * @param committeeMembership - the committeeMembership which contains the to be validated data
     * @param membershipIndex - the index position of the committeeMembership
     * @return <code>true</code> when the committee membership has at least one role assigned, <code>false</code> otherwise
     */
    private boolean hasRoles(CommitteeMembershipBase committeeMembership, int membershipIndex) {
        boolean hasExpertise = true;

        if (committeeMembership.getMembershipRoles().isEmpty()) {
            hasExpertise = false;
            reportError(String.format(PROPERTY_NAME_ROLE_CODE_ADD, membershipIndex),
                    KeyConstants.ERROR_COMMITTEE_MEMBERSHIP_ROLE_MISSING);
        }

        return hasExpertise;
    }

    /**
     * Verify the Role Start and Role End dates.
     * 
     * Date validation is done by the data dictionary.  
     * Validate that Role End date is greater than or equal to the Role Start date.
     * 
     * This method also displays the appropriate error message.
     * 
     * @param membershipRole - the membershipRole which contains the to be validated data
     * @param membershipIndex - the index position of the committeeMembership
     * @param roleIndex - the index position of the membershipRole
     * @return <code>true</code> if the role start and end dates are valid, <code>false</code> otherwise
     */
    private boolean isValidRoleStartEndDates(CommitteeMembershipRole membershipRole, int membershipIndex,
            int roleIndex) {
        boolean isValid = true;

        if (membershipRole.getStartDate() != null && membershipRole.getEndDate() != null
                && membershipRole.getEndDate().before(membershipRole.getStartDate())) {
            isValid = false;
            reportError(String.format(PROPERTY_NAME_ROLE_END_DATE, membershipIndex, roleIndex),
                    KeyConstants.ERROR_COMMITTEE_MEMBERSHIP_ROLE_END_DATE_BEFORE_ROLE_START_DATE);
        }

        return isValid;
    }

    /**
     * Verify that the role dates are within the term period.
     * 
     * This method also displays the appropriate error message.
     * 
     * @param committeeMembership - the committeeMembership of whom the membershipRole is to be validated
     * @param membershipRole - the membershipRole which contains the to be validated data
     * @param membershipIndex - the index position of the committeeMembership
     * @param indexOf - the index position of the membershipRole
     * @return <code>true</code> if the role dates are within the term period, <code>false</code> otherwise
     */
    private boolean roleDatesWithinTermDates(CommitteeMembershipBase committeeMembership,
            CommitteeMembershipRole membershipRole, int membershipIndex, int roleIndex) {
        boolean isValid = true;

        if ((committeeMembership.getTermStartDate() != null) && (committeeMembership.getTermEndDate() != null)
                && (membershipRole.getStartDate() != null) && (membershipRole.getEndDate() != null)) {
            if (hasDateOutsideCommitteeMembershipTerm(committeeMembership, membershipRole.getStartDate())) {
                isValid = false;
                reportError(String.format(PROPERTY_NAME_ROLE_START_DATE, membershipIndex, roleIndex),
                        KeyConstants.ERROR_COMMITTEE_MEMBERSHIP_ROLE_START_DATE_OUTSIDE_TERM);
            }
            if (hasDateOutsideCommitteeMembershipTerm(committeeMembership, membershipRole.getEndDate())) {
                isValid = false;
                reportError(String.format(PROPERTY_NAME_ROLE_END_DATE, membershipIndex, roleIndex),
                        KeyConstants.ERROR_COMMITTEE_MEMBERSHIP_ROLE_END_DATE_OUTSIDE_TERM);
            }
        }

        return isValid;
    }

    /**
     * Check if the date is outside the committee membership term.
     * If any of the date are null the method returns false.
     * 
     * @param committeeMembership - the committeeMembership whose term we are comparing against
     * @param date - the date to be checked
     * @return <code>true</code> if the date is outside the committee membership term, <code>false</code> otherwise
     */
    private boolean hasDateOutsideCommitteeMembershipTerm(CommitteeMembershipBase committeeMembership, Date date) {
        boolean isOutside = false;
        if ((committeeMembership.getTermStartDate() != null) && (committeeMembership.getTermEndDate() != null)
                && (date != null)) {
            if (date.before(committeeMembership.getTermStartDate())
                    || date.after(committeeMembership.getTermEndDate())) {
                isOutside = true;
            }
        }
        return isOutside;
    }

    /**
     * Check that the role does not have other entries whose time periods overlap.
     * (A member may not have the same role for overlapping time periods.)
     * 
     * This method also displays the appropriate error message.
     * 
     * @param committeeMembership - the committeeMembership of whom the membershipRole is to be validated
     * @param membershipRole - the membershipRole which contains the to be validated data
     * @param membershipIndex - the index position of the committeeMembership
     * @param indexOf - the index position of the membershipRole
     * @return <code>true</code> if the role does not overlap with another role, <code>false</code> otherwise
     */
    private boolean hasNoRoleOverlap(CommitteeMembershipBase committeeMembership,
            CommitteeMembershipRole membershipRole, int membershipIndex, int roleIndex) {
        boolean isValid = true;

        for (CommitteeMembershipRole tmpRole : committeeMembership.getMembershipRoles()) {
            if (roleIndex != committeeMembership.getMembershipRoles().indexOf(tmpRole)
                    && membershipRole.getMembershipRoleCode().equals(tmpRole.getMembershipRoleCode())) {
                if (isWithinPeriod(membershipRole.getStartDate(), tmpRole.getStartDate(), tmpRole.getEndDate())
                        || isWithinPeriod(membershipRole.getEndDate(), tmpRole.getStartDate(),
                                tmpRole.getEndDate())) {
                    isValid = false;
                    reportError(String.format(PROPERTY_NAME_ROLE_CODE, membershipIndex, roleIndex),
                            KeyConstants.ERROR_COMMITTEE_MEMBERSHIP_ROLE_DUPLICATE);
                }
            }
        }

        return isValid;
    }

    /**
     * Verify that a date is within a period
     * 
     * @param date - the date that needs to be within the period
     * @param periodStart - the date on which the period begins
     * @param periodEnd - the date on which the period ends
     * @return <code>true</code> if date is within the period, <code>false</code> otherwise
     */
    private boolean isWithinPeriod(Date date, Date periodStart, Date periodEnd) {
        return !(date.before(periodStart) || date.after(periodEnd));
    }

    /**
     * Verify that the committee membership has at least one expertise assigned.
     *  
     * @param committeeMembership - the committeeMembership which contains the to be validated data
     * @param membershipIndex - the index position of the committeeMembership
     * @return <code>true</code> when the committee membership has at least one expertise assigned, <code>false</code> otherwise
     */
    private boolean hasExpertise(CommitteeMembershipBase committeeMembership, int membershipIndex) {
        boolean hasExpertise = true;

        if (committeeMembership.getMembershipExpertise().isEmpty()) {
            hasExpertise = false;
            reportError(String.format(PROPERTY_NAME_RESEARCH_AREA_CODE, membershipIndex),
                    KeyConstants.ERROR_COMMITTEE_MEMBERSHIP_EXPERTISE_MISSING);
        }

        return hasExpertise;
    }

    /**
     * This method will check if all the research areas that have been added to a committee member as 'area of expertise' are indeed active.
     * It is declared public because it will be invoked from the action class for committee members as well.
     * @param document
     * @return
     */
    public boolean checkResearchAreasForCommitteeMember(CommitteeMembershipBase committeeMember,
            int membershipIndex) {

        boolean inactiveFound = false;
        StringBuffer inactiveResearchAreaIndices = new StringBuffer();

        List<CommitteeMembershipExpertiseBase> cmes = committeeMember.getMembershipExpertise();
        // iterate over all the research areas for this committee member looking for inactive research areas
        if (CollectionUtils.isNotEmpty(cmes)) {
            int raIndex = 0;
            for (CommitteeMembershipExpertiseBase cme : cmes) {
                if (cme.getResearchArea() != null && !(cme.getResearchArea().isActive())) {
                    inactiveFound = true;
                    inactiveResearchAreaIndices.append(raIndex).append(SEPERATOR);
                }
                raIndex++;
            }
        }
        // if we found any inactive research areas in the above loop, report as a single error key suffixed by the list of indices of the inactive areas
        if (inactiveFound) {
            String committeeMemberInactiveAreasOfExpertiseErrorPropertyKey = String
                    .format(PROPERTY_NAME_INACTIVE_AREAS_OF_EXPERTISE_PREFIX, membershipIndex) + SEPERATOR
                    + inactiveResearchAreaIndices.toString();
            reportError(committeeMemberInactiveAreasOfExpertiseErrorPropertyKey,
                    KeyConstants.ERROR_COMMITTEE_MEMBERSHIP_EXPERTISE_INACTIVE);
        }

        return !inactiveFound;
    }

    @Override
    public boolean processRunAuditBusinessRules(Document document) {
        return new KcDocumentBaseAuditRule().processRunAuditBusinessRules(document);
    }

    @Override
    public boolean processAddCommitteeMembershipBusinessRules(
            AddCommitteeMembershipEvent addCommitteeMembershipEvent) {
        return new CommitteeMembershipRule()
                .processAddCommitteeMembershipBusinessRules(addCommitteeMembershipEvent);
    }

    @Override
    public boolean processAddCommitteeMembershipRoleBusinessRules(
            AddCommitteeMembershipRoleEvent addCommitteeMembershipRoleEvent) {
        return new CommitteeMembershipRule()
                .processAddCommitteeMembershipRoleBusinessRules(addCommitteeMembershipRoleEvent);
    }

    @Override
    public boolean processRules(KcDocumentEventBaseExtension event) {
        boolean retVal = false;
        retVal = event.getRule().processRules(event);
        return retVal;
    }

    /*
     * A few schedules related rules.
     */
    private boolean processScheduleRules(CommitteeDocumentBase committeeDocument) {
        boolean retval = true;

        CommitteeScheduleTimeEvent scheduleTimeEvent = new CommitteeScheduleTimeEvent(Constants.EMPTY_STRING,
                committeeDocument, null, committeeDocument.getCommittee().getCommitteeSchedules(),
                ErrorType.HARDERROR);
        retval &= scheduleTimeEvent.getRule().processRules(scheduleTimeEvent);

        CommitteeScheduleDateConflictEvent scheduleDateConfliceEvent = new CommitteeScheduleDateConflictEvent(
                Constants.EMPTY_STRING, committeeDocument, null,
                committeeDocument.getCommittee().getCommitteeSchedules(), ErrorType.HARDERROR);
        retval &= scheduleDateConfliceEvent.getRule().processRules(scheduleDateConfliceEvent);

        CommitteeScheduleDeadlineEvent scheduleDeadlineEvent = new CommitteeScheduleDeadlineEvent(
                Constants.EMPTY_STRING, committeeDocument, null,
                committeeDocument.getCommittee().getCommitteeSchedules(), ErrorType.HARDERROR);
        retval &= scheduleDeadlineEvent.getRule().processRules(scheduleDeadlineEvent);
        return retval;

    }

    /**
     * This method will check if all the research areas that have been added to the committee are indeed active.
     * It is declared public because it will be invoked from the action class for committee as well.
     * @param document
     * @return
     */
    public boolean validateCommitteeResearchAreas(CommitteeBase committee) {
        boolean inactiveFound = false;
        StringBuffer inactiveResearchAreaIndices = new StringBuffer();

        // iterate over all the research areas for the committee BO looking for inactive research areas
        List<CommitteeResearchAreaBase> cras = committee.getCommitteeResearchAreas();
        if (CollectionUtils.isNotEmpty(cras)) {
            int raIndex = 0;
            for (CommitteeResearchAreaBase cra : cras) {
                if (!(cra.getResearchAreas().isActive())) {
                    inactiveFound = true;
                    inactiveResearchAreaIndices.append(raIndex).append(SEPERATOR);
                }
                raIndex++;
            }
        }
        // if we found any inactive research areas in the above loop, report as a single error key suffixed by the list of indices of the inactive areas
        if (inactiveFound) {
            String committeeResearchAreaInactiveErrorPropertyKey = INACTIVE_RESEARCH_AREAS_PREFIX + SEPERATOR
                    + inactiveResearchAreaIndices.toString();
            KcServiceLocator.getService(ErrorReporter.class).reportError(
                    committeeResearchAreaInactiveErrorPropertyKey,
                    KeyConstants.ERROR_COMMITTEE_RESEARCH_AREA_INACTIVE);
        }

        return !inactiveFound;
    }

    // check that the review type corresponding to the committee type is non-null (not performing XOR check) 
    public boolean validateReviewType(CommitteeBase committee) {
        boolean valid = true;
        if (StringUtils.isBlank(committee.getReviewTypeCode())) {
            // add error message
            KcServiceLocator.getService(ErrorReporter.class).reportError(REVIEW_TYPE_ERROR_PROPERTY_NAME,
                    KeyConstants.ERROR_COMMITTEE_REVIEW_TYPE_REQUIRED);
            valid = false;
        }
        return valid;
    }

}