/**
 * Copyright 2005-2018 The Kuali Foundation
 *
 * Licensed under the Educational Community License, Version 2.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://www.opensource.org/licenses/ecl2.php
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.kuali.rice.kim.document.rule;

import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.List;
import java.util.Map;

import javax.xml.namespace.QName;

import org.apache.commons.lang.StringUtils;
import org.kuali.rice.core.api.CoreConstants;
import org.kuali.rice.core.api.membership.MemberType;
import org.kuali.rice.core.api.resourceloader.GlobalResourceLoader;
import org.kuali.rice.core.api.uif.RemotableAttributeError;
import org.kuali.rice.core.api.util.RiceKeyConstants;
import org.kuali.rice.core.api.util.VersionHelper;
import org.kuali.rice.kim.api.KimConstants;
import org.kuali.rice.kim.api.identity.IdentityService;
import org.kuali.rice.kim.api.identity.principal.Principal;
import org.kuali.rice.kim.api.permission.Permission;
import org.kuali.rice.kim.api.responsibility.Responsibility;
import org.kuali.rice.kim.api.role.Role;
import org.kuali.rice.kim.api.role.RoleService;
import org.kuali.rice.kim.api.services.KimApiServiceLocator;
import org.kuali.rice.kim.api.type.KimAttributeField;
import org.kuali.rice.kim.api.type.KimType;
import org.kuali.rice.kim.bo.ui.KimDocumentRoleMember;
import org.kuali.rice.kim.bo.ui.KimDocumentRolePermission;
import org.kuali.rice.kim.bo.ui.KimDocumentRoleQualifier;
import org.kuali.rice.kim.bo.ui.KimDocumentRoleResponsibility;
import org.kuali.rice.kim.bo.ui.KimDocumentRoleResponsibilityAction;
import org.kuali.rice.kim.bo.ui.RoleDocumentDelegationMember;
import org.kuali.rice.kim.bo.ui.RoleDocumentDelegationMemberQualifier;
import org.kuali.rice.kim.document.IdentityManagementRoleDocument;
import org.kuali.rice.kim.framework.role.RoleTypeService;
import org.kuali.rice.kim.framework.services.KimFrameworkServiceLocator;
import org.kuali.rice.kim.framework.type.KimTypeService;
import org.kuali.rice.kim.impl.common.attribute.KimAttributeBo;
import org.kuali.rice.kim.impl.responsibility.AddResponsibilityEvent;
import org.kuali.rice.kim.impl.responsibility.AddResponsibilityRule;
import org.kuali.rice.kim.impl.responsibility.KimDocumentResponsibilityRule;
import org.kuali.rice.kim.impl.responsibility.ResponsibilityBo;
import org.kuali.rice.kim.impl.responsibility.ResponsibilityInternalService;
import org.kuali.rice.kim.impl.services.KimImplServiceLocator;
import org.kuali.rice.kim.impl.type.KimTypeLookupableHelperServiceImpl;
import org.kuali.rice.kim.rule.event.ui.AddDelegationEvent;
import org.kuali.rice.kim.rule.event.ui.AddDelegationMemberEvent;
import org.kuali.rice.kim.rule.event.ui.AddMemberEvent;
import org.kuali.rice.kim.rule.event.ui.AddPermissionEvent;
import org.kuali.rice.kim.rule.ui.AddDelegationMemberRule;
import org.kuali.rice.kim.rule.ui.AddDelegationRule;
import org.kuali.rice.kim.rule.ui.AddMemberRule;
import org.kuali.rice.kim.rule.ui.AddPermissionRule;
import org.kuali.rice.kim.rules.ui.KimDocumentMemberRule;
import org.kuali.rice.kim.rules.ui.KimDocumentPermissionRule;
import org.kuali.rice.kim.rules.ui.RoleDocumentDelegationMemberRule;
import org.kuali.rice.kim.rules.ui.RoleDocumentDelegationRule;
import org.kuali.rice.kns.kim.type.DataDictionaryTypeServiceHelper;
import org.kuali.rice.kns.rules.TransactionalDocumentRuleBase;
import org.kuali.rice.krad.data.KradDataServiceLocator;
import org.kuali.rice.krad.document.Document;
import org.kuali.rice.krad.util.GlobalVariables;
import org.kuali.rice.krad.util.KRADConstants;
import org.kuali.rice.krad.util.MessageMap;
import org.kuali.rice.ksb.api.KsbApiServiceLocator;
import org.kuali.rice.ksb.api.bus.Endpoint;
import org.kuali.rice.ksb.api.bus.ServiceBus;

/**
 * @author Kuali Rice Team (rice.collab@kuali.org)
 */
public class IdentityManagementRoleDocumentRule extends TransactionalDocumentRuleBase implements AddPermissionRule, AddResponsibilityRule, AddMemberRule, AddDelegationRule, AddDelegationMemberRule {
//	protected static final Logger LOG = Logger.getLogger( IdentityManagementRoleDocumentRule.class );

    public static final int PRIORITY_NUMBER_MIN_VALUE = 1;
    public static final int PRIORITY_NUMBER_MAX_VALUE = 11;

	protected AddResponsibilityRule addResponsibilityRule;
	protected AddPermissionRule  addPermissionRule;
	protected AddMemberRule  addMemberRule;
	protected AddDelegationRule addDelegationRule;
	protected AddDelegationMemberRule addDelegationMemberRule;
	protected Class<? extends AddResponsibilityRule> addResponsibilityRuleClass = KimDocumentResponsibilityRule.class;
	protected Class<? extends AddPermissionRule> addPermissionRuleClass = KimDocumentPermissionRule.class;
	protected Class<? extends AddMemberRule> addMemberRuleClass = KimDocumentMemberRule.class;
	protected Class<? extends AddDelegationRule> addDelegationRuleClass = RoleDocumentDelegationRule.class;
	protected Class<? extends AddDelegationMemberRule> addDelegationMemberRuleClass = RoleDocumentDelegationMemberRule.class;

	private static IdentityService identityService;
	private static ResponsibilityInternalService responsibilityInternalService;

	protected AttributeValidationHelper attributeValidationHelper = new AttributeValidationHelper();

	// KULRICE-4153
	protected ActiveRoleMemberHelper activeRoleMemberHelper = new ActiveRoleMemberHelper();

    protected IdentityService getIdentityService() {
        if ( identityService == null) {
            identityService = KimApiServiceLocator.getIdentityService();
        }
        return identityService;
    }

    @Override
    protected boolean processCustomSaveDocumentBusinessRules(Document document) {
        if (!(document instanceof IdentityManagementRoleDocument)) {
            return false;
        }

        IdentityManagementRoleDocument roleDoc = (IdentityManagementRoleDocument)document;

        boolean valid = true;
        GlobalVariables.getMessageMap().addToErrorPath(KRADConstants.DOCUMENT_PROPERTY_NAME);
        boolean validRoleNamespace = validRoleNamespace(roleDoc);
        boolean validRoleName = validRoleName(roleDoc);
        if (!(validRoleNamespace && validRoleName)) {
            return false;
        }
        valid &= validDuplicateRoleName(roleDoc);
        valid &= validPermissions(roleDoc);
        valid &= validResponsibilities(roleDoc);
        //KULRICE-6858 Validate group members are in identity system
        valid &= validRoleMemberPrincipalIDs(roleDoc.getModifiedMembers());
        getDictionaryValidationService().validateDocumentAndUpdatableReferencesRecursively(document, getMaxDictionaryValidationDepth(), true, false);
        if ( !KimTypeLookupableHelperServiceImpl.hasDerivedRoleTypeService(roleDoc.getKimType())
                && canUserAssignRoleMembers(roleDoc) ) {
	        //valid &= validAssignRole(roleDoc);
        	// KULRICE-4153 & KULRICE-4818
        	// Use the Active Role Member Helper to retrieve only those Role Members & Delegation member that are active
        	// If a member or delegation is active on a Role, and they have an inactive Role Qualifier, Role Qualifier validation will fail
        	// If a member or delegation is inactive on a Role, and they have an inactive Role Qualifier, Role Qualifier validation will pass
            List<KimDocumentRoleMember> activeRoleMembers = activeRoleMemberHelper.getActiveRoleMembers(roleDoc.getMembers());
            List<KimDocumentRoleMember> newActiveRoleMembers = activeRoleMemberHelper.getActiveRoleMembers(roleDoc.getModifiedMembers());
            List<RoleDocumentDelegationMember> activeRoleDelegationMembers = activeRoleMemberHelper.getActiveDelegationRoleMembers(roleDoc.getDelegationMembers());

            valid &= validateRoleQualifier(newActiveRoleMembers, roleDoc.getKimType());
	        valid &= validRoleMemberActiveDates(roleDoc.getModifiedMembers());
	        valid &= validateDelegationMemberRoleQualifier(newActiveRoleMembers, activeRoleDelegationMembers, roleDoc.getKimType(), activeRoleMembers);
	        valid &= validDelegationMemberActiveDates(roleDoc.getDelegationMembers());
	        valid &= validRoleMembersResponsibilityActions(roleDoc.getModifiedMembers());
        }
        valid &= validRoleResponsibilitiesActions(roleDoc.getResponsibilities());
        GlobalVariables.getMessageMap().removeFromErrorPath(KRADConstants.DOCUMENT_PROPERTY_NAME);

        return valid;
    }
    /**
     * Ensures the {@link IdentityManagementRoleDocument} role namespace is not null or an empty string.
     * @param roleDoc the {@link IdentityManagementRoleDocument} to validate.
     * @return TRUE if the role namespace is not null or an empty string, FALSE otherwise.
     */
    protected boolean validRoleNamespace(IdentityManagementRoleDocument roleDoc) {
        boolean validRoleNamespace = false;

        if(StringUtils.isNotBlank(roleDoc.getRoleNamespace())) {
            validRoleNamespace = true;
        }
        else{
            GlobalVariables.getMessageMap().putError("document.roleNamespace",
                    RiceKeyConstants.ERROR_EMPTY_ENTRY, new String[] {"Role Namespace"});
        }

        return validRoleNamespace;
    }
    /**
     * ensures the {@link IdentitymangaementRoleDocument} role name is not null or an empty string
     * @param roleDoc the {@link IdentityManagementRoleDocument} to validate.
     * @return TRUE if the role name is not null or an empty string, FALSE otherwise.
     */
    protected boolean validRoleName(IdentityManagementRoleDocument roleDoc) {
        boolean validRoleName = false;

        if(StringUtils.isNotBlank(roleDoc.getRoleName())) {
            validRoleName = true;
        }
        else{
            GlobalVariables.getMessageMap().putError("document.roleName",
                    RiceKeyConstants.ERROR_EMPTY_ENTRY, new String[] {"Role Name"});
        }

        return validRoleName;
    }

	protected boolean canUserAssignRoleMembers(IdentityManagementRoleDocument document){
        boolean rulePassed = true;
        Map<String,String> additionalPermissionDetails = new HashMap<String,String>();
        additionalPermissionDetails.put(KimConstants.AttributeConstants.NAMESPACE_CODE, document.getRoleNamespace());
        additionalPermissionDetails.put(KimConstants.AttributeConstants.ROLE_NAME, document.getRoleName());
		if((document.getMembers()!=null && document.getMembers().size()>0) ||
				(document.getDelegationMembers()!=null && document.getDelegationMembers().size()>0)){
			if(!getDocumentDictionaryService().getDocumentAuthorizer(document).isAuthorizedByTemplate(
					document, KimConstants.NAMESPACE_CODE, KimConstants.PermissionTemplateNames.ASSIGN_ROLE,
					GlobalVariables.getUserSession().getPrincipalId(), additionalPermissionDetails, null)){
	            rulePassed = false;
			}
		}
		return rulePassed;
	}

    protected boolean validRoleMemberPrincipalIDs(List<KimDocumentRoleMember> roleMembers) {
        boolean valid = true;
        List<String> principalIds = new ArrayList<String>();
        for(KimDocumentRoleMember roleMember: roleMembers) {
            if (StringUtils.equals(roleMember.getMemberTypeCode(), KimConstants.KimGroupMemberTypes.PRINCIPAL_MEMBER_TYPE.getCode()) ) {
                principalIds.add(roleMember.getMemberId());
            }
        }
        if(!principalIds.isEmpty())       {
            List<Principal> validPrincipals = getIdentityService().getPrincipals(principalIds);
            for(KimDocumentRoleMember roleMember: roleMembers) {
                if (StringUtils.equals(roleMember.getMemberTypeCode(), MemberType.PRINCIPAL.getCode()) ) {
                    boolean validPrincipalId = false;
                    if(validPrincipals != null && !validPrincipals.isEmpty())       {
                        for(Principal validPrincipal: validPrincipals) {
                            if(roleMember.getMemberId().equals(validPrincipal.getPrincipalId()))     {
                             validPrincipalId = true;
                            }
                        }
                    }
                    if(!validPrincipalId) {
                        GlobalVariables.getMessageMap().putError("document.member.memberId", RiceKeyConstants.ERROR_MEMBERID_MEMBERTYPE_MISMATCH,
                                new String[] {roleMember.getMemberId()});
                        valid = false;
                    }
                }
            }
        }
        return valid;
    }

    @SuppressWarnings("unchecked")
	protected boolean validDuplicateRoleName(IdentityManagementRoleDocument roleDoc){
        Role role = KimApiServiceLocator.getRoleService().getRoleByNamespaceCodeAndName(roleDoc.getRoleNamespace(),
                roleDoc.getRoleName());
    	boolean rulePassed = true;
    	if(role!=null){
    		if(role.getId().equals(roleDoc.getRoleId())) {
                rulePassed = true;
            }
    		else{
	    		GlobalVariables.getMessageMap().putError("document.roleName",
	    				RiceKeyConstants.ERROR_DUPLICATE_ENTRY, new String[] {"Role Name"});
	    		rulePassed = false;
    		}
    	}
    	return rulePassed;
    }

    protected boolean validRoleMemberActiveDates(List<KimDocumentRoleMember> roleMembers) {
    	boolean valid = true;
		int i = 0;
    	for(KimDocumentRoleMember roleMember: roleMembers) {
   			valid &= validateActiveDate("document.members["+i+"].activeToDate", roleMember.getActiveFromDate(), roleMember.getActiveToDate());
    		i++;
    	}
    	return valid;
    }

    protected boolean validDelegationMemberActiveDates(List<RoleDocumentDelegationMember> delegationMembers) {
    	boolean valid = true;
		int i = 0;
    	for(RoleDocumentDelegationMember delegationMember: delegationMembers) {
   			valid &= validateActiveDate("document.delegationMembers[" + i + "].activeToDate",
                       delegationMember.getActiveFromDate(), delegationMember.getActiveToDate());
    		i++;
    	}
    	return valid;
    }

    protected boolean validPermissions(IdentityManagementRoleDocument document){
    	Permission kimPermissionInfo;
    	boolean valid = true;
    	int i = 0;
    	for(KimDocumentRolePermission permission: document.getPermissions()){
    		kimPermissionInfo = permission.getPermission();
    		if(!permission.isActive() && !hasPermissionToGrantPermission(permission.getPermission(), document)){
    	        GlobalVariables.getMessageMap().putError("permissions["+i+"].active", RiceKeyConstants.ERROR_ASSIGN_PERMISSION,
    	        		new String[] {kimPermissionInfo.getNamespaceCode(), kimPermissionInfo.getTemplate().getName()});
    	        valid = false;
    		}
    		i++;
    	}
    	return valid;
    }

    protected boolean validResponsibilities(IdentityManagementRoleDocument document){
    	ResponsibilityBo kimResponsibilityImpl;
    	boolean valid = true;
    	int i = 0;
    	for(KimDocumentRoleResponsibility responsibility: document.getResponsibilities()){
    		kimResponsibilityImpl = responsibility.getKimResponsibility();
    		if(!responsibility.isActive() && !hasPermissionToGrantResponsibility(ResponsibilityBo.to(responsibility.getKimResponsibility()), document)){
    	        GlobalVariables.getMessageMap().putError("responsibilities["+i+"].active", RiceKeyConstants.ERROR_ASSIGN_RESPONSIBILITY,
    	        		new String[] {kimResponsibilityImpl.getNamespaceCode(), kimResponsibilityImpl.getTemplate().getName()});
    	        valid = false;
    		}
    		i++;
    	}
    	return valid;
    }

    protected boolean validRoleResponsibilitiesActions(List<KimDocumentRoleResponsibility> roleResponsibilities){
        int i = 0;
        boolean rulePassed = true;
    	for(KimDocumentRoleResponsibility roleResponsibility: roleResponsibilities){
    		if(!getResponsibilityInternalService().areActionsAtAssignmentLevelById(roleResponsibility.getResponsibilityId())) {
    			validateRoleResponsibilityAction("document.responsibilities["+i+"].roleRspActions[0].priorityNumber", roleResponsibility.getRoleRspActions().get(0));
            }
        	i++;
    	}
    	return rulePassed;
    }

    protected boolean validRoleMembersResponsibilityActions(List<KimDocumentRoleMember> roleMembers){
        int i = 0;
        int j;
        boolean rulePassed = true;
    	for(KimDocumentRoleMember roleMember: roleMembers){
    		j = 0;
    		if(roleMember.getRoleRspActions()!=null && !roleMember.getRoleRspActions().isEmpty()){
	    		for(KimDocumentRoleResponsibilityAction roleRspAction: roleMember.getRoleRspActions()){
	    			validateRoleResponsibilityAction("document.members["+i+"].roleRspActions["+j+"].priorityNumber", roleRspAction);
		        	j++;
	    		}
    		}
    		i++;
    	}
    	return rulePassed;
    }

    protected boolean validateRoleResponsibilityAction(String errorPath, KimDocumentRoleResponsibilityAction roleRspAction){
    	boolean rulePassed = true;
    	/*if(StringUtils.isBlank(roleRspAction.getActionPolicyCode())){
    		GlobalVariables.getErrorMap().putError(errorPath,
    				RiceKeyConstants.ERROR_EMPTY_ENTRY, new String[] {"Action Policy Code"});
    		rulePassed = false;
    	}
    	if(roleRspAction.getPriorityNumber()==null){
    		GlobalVariables.getErrorMap().putError(errorPath,
    				RiceKeyConstants.ERROR_EMPTY_ENTRY, new String[] {"Priority Number"});
    		rulePassed = false;
    	}
    	if(StringUtils.isBlank(roleRspAction.getActionTypeCode())){
    		GlobalVariables.getErrorMap().putError(errorPath,
    				RiceKeyConstants.ERROR_EMPTY_ENTRY, new String[] {"Action Type Code"});
    		rulePassed = false;
    	}*/
    	if(roleRspAction.getPriorityNumber()!=null &&
    			(roleRspAction.getPriorityNumber()<PRIORITY_NUMBER_MIN_VALUE
    					|| roleRspAction.getPriorityNumber()>PRIORITY_NUMBER_MAX_VALUE)){
    		GlobalVariables.getMessageMap().putError(errorPath,
   				RiceKeyConstants.ERROR_PRIORITY_NUMBER_RANGE, new String[] {PRIORITY_NUMBER_MIN_VALUE+"", PRIORITY_NUMBER_MAX_VALUE+""});
    		rulePassed = false;
    	}

    	return rulePassed;
    }

    protected boolean validateRoleQualifier(List<KimDocumentRoleMember> roleMembers, KimType kimType){
		List<RemotableAttributeError> validationErrors = new ArrayList<RemotableAttributeError>();

		int memberCounter = 0;
		int roleMemberCount = 0;
		List<RemotableAttributeError> errorsTemp;
		Map<String, String> mapToValidate;
        KimTypeService kimTypeService = KimFrameworkServiceLocator.getKimTypeService(kimType);
        GlobalVariables.getMessageMap().removeFromErrorPath(KRADConstants.DOCUMENT_PROPERTY_NAME);
        final List<KimAttributeField> attributeDefinitions = kimTypeService.getAttributeDefinitions(kimType.getId());
        final Set<String> uniqueAttributeNames = figureOutUniqueQualificationSet(roleMembers, attributeDefinitions);

		for(KimDocumentRoleMember roleMember: roleMembers) {
			errorsTemp = Collections.emptyList();
			mapToValidate = attributeValidationHelper.convertQualifiersToMap(roleMember.getQualifiers());

            VersionedService<RoleTypeService> versionedRoleTypeService = getVersionedRoleTypeService(kimType);
            boolean shouldNotValidate = true;
            if (versionedRoleTypeService != null) {
                boolean versionOk = VersionHelper.compareVersion(versionedRoleTypeService.getVersion(),
                        CoreConstants.Versions.VERSION_2_1_2)!=-1? true:false;
                if(versionOk) {
                    shouldNotValidate = versionedRoleTypeService.getService().shouldValidateQualifiersForMemberType( MemberType.fromCode(roleMember.getMemberTypeCode()));
                } else {
                    shouldNotValidate = false;
                }
            }
            if(!shouldNotValidate){
				errorsTemp = kimTypeService.validateAttributes(kimType.getId(), mapToValidate);
				validationErrors.addAll(attributeValidationHelper.convertErrorsForMappedFields(
                        "members[" + memberCounter + "]", errorsTemp));
		        memberCounter++;
			}
			if (uniqueAttributeNames.size() > 0) {
				validateUniquePersonRoleQualifiersUniqueForRoleMembership(roleMember, roleMemberCount, roleMembers, uniqueAttributeNames, validationErrors);
			}

			roleMemberCount += 1;
    	}

		GlobalVariables.getMessageMap().addToErrorPath(KRADConstants.DOCUMENT_PROPERTY_NAME);

    	if (validationErrors.isEmpty()) {
    		return true;
    	} 
    	attributeValidationHelper.moveValidationErrorsToErrorMap(validationErrors);
    	return false;
    }

    /**
     * Finds the names of the unique qualification attributes which this role should be checking against
     *
     * @param memberships the memberships (we take the qualification from the first)
     * @param attributeDefinitions information about the attributeDefinitions
     * @return a Set of unique attribute ids (with their indices, for error reporting)
     */
    protected Set<String> figureOutUniqueQualificationSet(List<KimDocumentRoleMember> memberships, List<KimAttributeField> attributeDefinitions) {
    	Set<String> uniqueAttributeIds = new HashSet<String>();

    	if (memberships != null && memberships.size() > 1) { // if there aren't two or more members, doing this whole check is kinda silly
    		KimDocumentRoleMember membership = memberships.get(0);

    		for (KimDocumentRoleQualifier qualifier : membership.getQualifiers()) {
        		if (qualifier != null && qualifier.getKimAttribute() != null && !StringUtils.isBlank(qualifier.getKimAttribute().getAttributeName())) {
    	    		final KimAttributeField relatedDefinition = DataDictionaryTypeServiceHelper.findAttributeField(
                            qualifier.getKimAttribute().getAttributeName(), attributeDefinitions);

    	    		if (relatedDefinition != null && relatedDefinition.isUnique()) {
    	    			uniqueAttributeIds.add(qualifier.getKimAttrDefnId()); // it's unique - add it to the Set
    	    		}
        		}
    		}
    	}

    	return uniqueAttributeIds;
    }

    /**
     * Checks all the qualifiers for the given membership, so that all qualifiers which should be unique are guaranteed to be unique
     *
     * @param membershipToCheck the membership to check
     * @param membershipToCheckIndex the index of the person's membership in the role (for error reporting purposes)
     * @param validationErrors Map<String, String> of errors to report
     * @return true if all unique values are indeed unique, false otherwise
     */
    protected boolean validateUniquePersonRoleQualifiersUniqueForRoleMembership(KimDocumentRoleMember membershipToCheck, int membershipToCheckIndex, List<KimDocumentRoleMember> memberships, Set<String> uniqueQualifierIds, List<RemotableAttributeError> validationErrors) {
    	boolean foundError = false;
    	int count = 0;

    	for (KimDocumentRoleMember membership : memberships) {
    		if (membershipToCheckIndex != count) {
    			if (sameMembership(membershipToCheck, membership)) {
    				if (sameUniqueMembershipQualifications(membershipToCheck, membership, uniqueQualifierIds)) {
    					foundError = true;
    					// add error to each qualifier which is supposed to be unique
    					int qualifierCount = 0;

    					for (KimDocumentRoleQualifier qualifier : membership.getQualifiers()) {
    						if (qualifier != null && uniqueQualifierIds.contains(qualifier.getKimAttrDefnId())) {
                                // for new member lines, KimAttribute is not preloaded
                                // make sure to load it here in order to obtain the name for use in error message
                                KimAttributeBo attr = qualifier.getKimAttribute();
                                String attrName = "<unknown>";
                                if (attr == null && qualifier.getKimAttrDefnId() != null) {
                                    attr = KradDataServiceLocator.getDataObjectService().find(KimAttributeBo.class, qualifier.getKimAttrDefnId());
                                }
                                if (attr != null) {
                                    attrName = attr.getAttributeName();
                                }
                                validationErrors.add(RemotableAttributeError.Builder.create("document.members["+membershipToCheckIndex+"].qualifiers["+qualifierCount+"].attrVal", RiceKeyConstants.ERROR_DOCUMENT_IDENTITY_MANAGEMENT_PERSON_QUALIFIER_VALUE_NOT_UNIQUE+":"+membership.getMemberId()+";"+attrName+";"+qualifier.getAttrVal()).build());
    						}
    						qualifierCount += 1;
    					}
    				}
    			}
    		}
    		count += 1;
    	}

    	return foundError;
    }

    /**
     * Determines if two memberships represent the same member being added: that is, the two memberships have the same type code and id
     *
     * @param membershipA the first membership to check
     * @param membershipB the second membership to check
     * @return true if the two memberships represent the same member; false if they do not, or if it could not be profitably determined if the members were the same
     */
    protected boolean sameMembership(KimDocumentRoleMember membershipA, KimDocumentRoleMember membershipB) {
    	if (!StringUtils.isBlank(membershipA.getMemberTypeCode()) && !StringUtils.isBlank(membershipB.getMemberTypeCode()) && !StringUtils.isBlank(membershipA.getMemberId()) && !StringUtils.isBlank(membershipB.getMemberId())) {
    		return membershipA.getMemberTypeCode().equals(membershipB.getMemberTypeCode()) && membershipA.getMemberId().equals(membershipB.getMemberId());
    	}
    	return false;
    }

    /**
     * Given two memberships which represent the same member, do they share qualifications?
     *
     * @param membershipA the first membership to check
     * @param membershipB the second membership to check
     * @param uniqueAttributeIds the Set of attribute definition ids which should be unique
     * @return
     */
    protected boolean sameUniqueMembershipQualifications(KimDocumentRoleMember membershipA, KimDocumentRoleMember membershipB, Set<String> uniqueAttributeIds) {
    	boolean equalSoFar = true;
    	for (String kimAttributeDefinitionId : uniqueAttributeIds) {
    		final KimDocumentRoleQualifier qualifierA = membershipA.getQualifier(kimAttributeDefinitionId);
    		final KimDocumentRoleQualifier qualifierB = membershipB.getQualifier(kimAttributeDefinitionId);

    		if (qualifierA != null && qualifierB != null) {
    			equalSoFar &= (qualifierA.getAttrVal() == null && qualifierB.getAttrVal() == null) || (qualifierA.getAttrVal() == null || qualifierA.getAttrVal().equals(qualifierB.getAttrVal()));
    		}
    	}
    	return equalSoFar;
    }

    protected KimDocumentRoleMember getRoleMemberForDelegation(
    		List<KimDocumentRoleMember> roleMembers, RoleDocumentDelegationMember delegationMember, List<KimDocumentRoleMember> modifiedRoleMembers) {
    	if((roleMembers==null && modifiedRoleMembers==null) || delegationMember==null || delegationMember.getRoleMemberId()==null) { return null; }
        for(KimDocumentRoleMember roleMember: modifiedRoleMembers){
            if(delegationMember.getRoleMemberId().equals(roleMember.getRoleMemberId())) {
                return roleMember;
            }
        }
    	for(KimDocumentRoleMember roleMember: roleMembers){
    		if(delegationMember.getRoleMemberId().equals(roleMember.getRoleMemberId())) {
                return roleMember;
            }
    	}
    	return null;
    }

    protected boolean validateDelegationMemberRoleQualifier(List<KimDocumentRoleMember> modifiedRoleMembers,
    		List<RoleDocumentDelegationMember> delegationMembers, KimType kimType, List<KimDocumentRoleMember> nonModifiedRoleMembers){
		List<RemotableAttributeError> validationErrors = new ArrayList<RemotableAttributeError>();
		boolean valid;
		int memberCounter = 0;
		List<RemotableAttributeError> errorsTemp;
		Map<String, String> mapToValidate;
        KimTypeService kimTypeService = KimFrameworkServiceLocator.getKimTypeService(kimType);
        GlobalVariables.getMessageMap().removeFromErrorPath(KRADConstants.DOCUMENT_PROPERTY_NAME);
        KimDocumentRoleMember roleMember;
        String errorPath;
        final List<KimAttributeField> attributeDefinitions = kimTypeService.getAttributeDefinitions(kimType.getId());
        final Set<String> uniqueQualifierAttributes = figureOutUniqueQualificationSetForDelegation(delegationMembers, attributeDefinitions);

		for(RoleDocumentDelegationMember delegationMember: delegationMembers) {
			errorPath = "delegationMembers["+memberCounter+"]";
			mapToValidate = attributeValidationHelper.convertQualifiersToMap(delegationMember.getQualifiers());
			if(!delegationMember.isRole()){
				errorsTemp = kimTypeService.validateAttributes(kimType.getId(), mapToValidate);
				validationErrors.addAll(
						attributeValidationHelper.convertErrorsForMappedFields(errorPath, errorsTemp));
			}
			roleMember = getRoleMemberForDelegation(nonModifiedRoleMembers, delegationMember, modifiedRoleMembers);
			if(roleMember==null){
				valid = false;
				GlobalVariables.getMessageMap().putError("document.delegationMembers["+memberCounter+"]", RiceKeyConstants.ERROR_DELEGATE_ROLE_MEMBER_ASSOCIATION, new String[]{});
			} else{
				errorsTemp = kimTypeService.validateUnmodifiableAttributes(
								kimType.getId(),
								attributeValidationHelper.convertQualifiersToMap(roleMember.getQualifiers()),
								mapToValidate);
				validationErrors.addAll(
						attributeValidationHelper.convertErrorsForMappedFields(errorPath, errorsTemp) );
			}
			if (uniqueQualifierAttributes.size() > 0) {
				validateUniquePersonRoleQualifiersUniqueForRoleDelegation(delegationMember, memberCounter, delegationMembers, uniqueQualifierAttributes, validationErrors);
			}
	        memberCounter++;
    	}
		GlobalVariables.getMessageMap().addToErrorPath(KRADConstants.DOCUMENT_PROPERTY_NAME);
    	if (validationErrors.isEmpty()) {
    		valid = true;
    	} else {
    		attributeValidationHelper.moveValidationErrorsToErrorMap(validationErrors);
    		valid = false;
    	}
    	return valid;
    }

    /**
     * Finds the names of the unique qualification attributes which this role should be checking against
     *
     * @param memberships the memberships (we take the qualification from the first)
     * @param attributeDefinitions information about the attributeDefinitions
     * @return a Set of unique attribute ids (with their indices, for error reporting)
     */
    protected Set<String> figureOutUniqueQualificationSetForDelegation(List<RoleDocumentDelegationMember> memberships, List<KimAttributeField> attributeDefinitions) {
    	Set<String> uniqueAttributeIds = new HashSet<String>();

    	if (memberships != null && memberships.size() > 1) { // if there aren't two or more members, doing this whole check is kinda silly
    		RoleDocumentDelegationMember membership = memberships.get(0);

    		for (RoleDocumentDelegationMemberQualifier qualifier : membership.getQualifiers()) {
        		if (qualifier != null && qualifier.getKimAttribute() != null && !StringUtils.isBlank(qualifier.getKimAttribute().getAttributeName())) {
    	    		final KimAttributeField relatedDefinition = DataDictionaryTypeServiceHelper.findAttributeField(
                            qualifier.getKimAttribute().getAttributeName(), attributeDefinitions);

    	    		if (relatedDefinition.isUnique()) {
    	    			uniqueAttributeIds.add(qualifier.getKimAttrDefnId()); // it's unique - add it to the Set
    	    		}
        		}
    		}
    	}

    	return uniqueAttributeIds;
    }

    /**
     * Checks all the qualifiers for the given membership, so that all qualifiers which should be unique are guaranteed to be unique
     *
     * @param delegationMembershipToCheck the membership to check
     * @param membershipToCheckIndex the index of the person's membership in the role (for error reporting purposes)
     * @param validationErrors Map<String, String> of errors to report
     * @return true if all unique values are indeed unique, false otherwise
     */
    protected boolean validateUniquePersonRoleQualifiersUniqueForRoleDelegation(RoleDocumentDelegationMember delegationMembershipToCheck, int membershipToCheckIndex, List<RoleDocumentDelegationMember> delegationMemberships, Set<String> uniqueQualifierIds, List<RemotableAttributeError> validationErrors) {
    	boolean foundError = false;
    	int count = 0;

    	for (RoleDocumentDelegationMember delegationMembership : delegationMemberships) {
    		if (membershipToCheckIndex != count) {
    			if (sameDelegationMembership(delegationMembershipToCheck, delegationMembership)) {
    				if (sameUniqueDelegationMembershipQualifications(delegationMembershipToCheck, delegationMembership, uniqueQualifierIds)) {
    					foundError = true;
    					// add error to each qualifier which is supposed to be unique
    					int qualifierCount = 0;

    					for (RoleDocumentDelegationMemberQualifier qualifier : delegationMembership.getQualifiers()) {
    						if (qualifier != null && uniqueQualifierIds.contains(qualifier.getKimAttrDefnId())) {
    							validationErrors.add(RemotableAttributeError.Builder.create("document.delegationMembers["+membershipToCheckIndex+"].qualifiers["+qualifierCount+"].attrVal", RiceKeyConstants.ERROR_DOCUMENT_IDENTITY_MANAGEMENT_PERSON_QUALIFIER_VALUE_NOT_UNIQUE+":"+qualifier.getKimAttribute().getAttributeName()+";"+qualifier.getAttrVal()).build());
    						}
    						qualifierCount += 1;
    					}
    				}
    			}
    		}
    		count += 1;
    	}

    	return foundError;
    }

    /**
     * Determines if two memberships represent the same member being added: that is, the two memberships have the same type code and id
     *
     * @param membershipA the first membership to check
     * @param membershipB the second membership to check
     * @return true if the two memberships represent the same member; false if they do not, or if it could not be profitably determined if the members were the same
     */
    protected boolean sameDelegationMembership(RoleDocumentDelegationMember membershipA, RoleDocumentDelegationMember membershipB) {
    	if (!StringUtils.isBlank(membershipA.getMemberTypeCode()) && !StringUtils.isBlank(membershipB.getMemberTypeCode()) && !StringUtils.isBlank(membershipA.getMemberId()) && !StringUtils.isBlank(membershipB.getMemberId())) {
    		return membershipA.getMemberTypeCode().equals(membershipB.getMemberTypeCode()) && membershipA.getMemberId().equals(membershipB.getMemberId());
    	}
    	return false;
    }

    /**
     * Given two memberships which represent the same member, do they share qualifications?
     *
     * @param membershipA the first membership to check
     * @param membershipB the second membership to check
     * @param uniqueAttributeIds the Set of attribute definition ids which should be unique
     * @return
     */
    protected boolean sameUniqueDelegationMembershipQualifications(RoleDocumentDelegationMember membershipA, RoleDocumentDelegationMember membershipB, Set<String> uniqueAttributeIds) {
    	boolean equalSoFar = true;
    	for (String kimAttributeDefinitionId : uniqueAttributeIds) {
    		final RoleDocumentDelegationMemberQualifier qualifierA = membershipA.getQualifier(kimAttributeDefinitionId);
    		final RoleDocumentDelegationMemberQualifier qualifierB = membershipB.getQualifier(kimAttributeDefinitionId);

    		if (qualifierA != null && qualifierB != null) {
    			equalSoFar &= (qualifierA.getAttrVal() == null && qualifierB.getAttrVal() == null) || (qualifierA.getAttrVal() == null || qualifierA.getAttrVal().equals(qualifierB.getAttrVal()));
    		}
    	}
    	return equalSoFar;
    }

	protected boolean validateActiveDate(String errorPath, Timestamp activeFromDate, Timestamp activeToDate) {
		// TODO : do not have detail bus rule yet, so just check this for now.
		boolean valid = true;
		if (activeFromDate != null && activeToDate !=null && activeToDate.before(activeFromDate)) {
	        MessageMap errorMap = GlobalVariables.getMessageMap();
            errorMap.putError(errorPath, RiceKeyConstants.ERROR_ACTIVE_TO_DATE_BEFORE_FROM_DATE);
            valid = false;

		}
		return valid;
	}

	/**
	 *
	 * This method checks to see if adding a role to role membership
	 * creates a circular reference.
	 *
	 * @param addMemberEvent
	 * @return true  - ok to assign, no circular references
	 *         false - do not make assignment, will create circular reference.
	 */
	protected boolean checkForCircularRoleMembership(AddMemberEvent addMemberEvent)
	{
		KimDocumentRoleMember newMember = addMemberEvent.getMember();
		if (newMember == null || StringUtils.isBlank(newMember.getMemberId())){
			MessageMap errorMap = GlobalVariables.getMessageMap();
			errorMap.putError("member.memberId", RiceKeyConstants.ERROR_INVALID_ROLE, new String[] {""});
			return false;
		}
		Set<String> roleMemberIds = null;
		// if the role member is a role, check to make sure we won't be creating a circular reference.
		// Verify that the new role is not already related to the role either directly or indirectly
		if (newMember.isRole()){
			// get all nested role member ids that are of type role
			RoleService roleService = KimApiServiceLocator.getRoleService();
			roleMemberIds = roleService.getRoleTypeRoleMemberIds(newMember.getMemberId());

			// check to see if the document role is not a member of the new member role
			IdentityManagementRoleDocument document = (IdentityManagementRoleDocument)addMemberEvent.getDocument();
			if (roleMemberIds.contains(document.getRoleId())){
				MessageMap errorMap = GlobalVariables.getMessageMap();
				errorMap.putError("member.memberId", RiceKeyConstants.ERROR_ASSIGN_ROLE_MEMBER_CIRCULAR, new String[] {newMember.getMemberId()});
				return false;
			}
		}
		return true;
	}

	/**
	 * @return the addResponsibilityRule
	 */
	public AddResponsibilityRule getAddResponsibilityRule() {
		if(addResponsibilityRule == null){
			try {
				addResponsibilityRule = addResponsibilityRuleClass.newInstance();
			} catch ( Exception ex ) {
				throw new RuntimeException( "Unable to create AddResponsibilityRule instance using class: " + addResponsibilityRuleClass, ex );
			}
		}
		return addResponsibilityRule;
	}

	/**
	 * @return the addPermissionRule
	 */
	public AddPermissionRule getAddPermissionRule() {
		if(addPermissionRule == null){
			try {
				addPermissionRule = addPermissionRuleClass.newInstance();
			} catch ( Exception ex ) {
				throw new RuntimeException( "Unable to create AddPermissionRule instance using class: " + addPermissionRuleClass, ex );
			}
		}
		return addPermissionRule;
	}

	/**
	 * @return the addMemberRule
	 */
	public AddMemberRule getAddMemberRule() {
		if(addMemberRule == null){
			try {
				addMemberRule = addMemberRuleClass.newInstance();
			} catch ( Exception ex ) {
				throw new RuntimeException( "Unable to create AddMemberRule instance using class: " + addMemberRuleClass, ex );
			}
		}
		return addMemberRule;
	}

	/**
	 * @return the addDelegationRule
	 */
	public AddDelegationRule getAddDelegationRule() {
		if(addDelegationRule == null){
			try {
				addDelegationRule = addDelegationRuleClass.newInstance();
			} catch ( Exception ex ) {
				throw new RuntimeException( "Unable to create AddDelegationRule instance using class: " + addDelegationRuleClass, ex );
			}
		}
		return addDelegationRule;
	}

	/**
	 * @return the addDelegationMemberRule
	 */
	public AddDelegationMemberRule getAddDelegationMemberRule() {
		if(addDelegationMemberRule == null){
			try {
				addDelegationMemberRule = addDelegationMemberRuleClass.newInstance();
			} catch ( Exception ex ) {
				throw new RuntimeException( "Unable to create AddDelegationMemberRule instance using class: " + addDelegationMemberRuleClass, ex );
			}
		}
		return addDelegationMemberRule;
	}

    public boolean processAddPermission(AddPermissionEvent addPermissionEvent) {
        return getAddPermissionRule().processAddPermission(addPermissionEvent);
    }

    public boolean hasPermissionToGrantPermission(Permission kimPermissionInfo , IdentityManagementRoleDocument document){
        return getAddPermissionRule().hasPermissionToGrantPermission(kimPermissionInfo, document);
    }

    public boolean processAddResponsibility(AddResponsibilityEvent addResponsibilityEvent) {
        return getAddResponsibilityRule().processAddResponsibility(addResponsibilityEvent);
    }

    public boolean hasPermissionToGrantResponsibility(Responsibility kimResponsibilityInfo, IdentityManagementRoleDocument document) {
        return getAddResponsibilityRule().hasPermissionToGrantResponsibility(kimResponsibilityInfo, document);
    }

    public boolean processAddMember(AddMemberEvent addMemberEvent) {
        boolean success = new KimDocumentMemberRule().processAddMember(addMemberEvent);
        success &= validateActiveDate("member.activeFromDate", addMemberEvent.getMember().getActiveFromDate(), addMemberEvent.getMember().getActiveToDate());
        success &= checkForCircularRoleMembership(addMemberEvent);
        return success;
    }

    public boolean processAddDelegation(AddDelegationEvent addDelegationEvent) {
        return getAddDelegationRule().processAddDelegation(addDelegationEvent);
    }

    public boolean processAddDelegationMember(AddDelegationMemberEvent addDelegationMemberEvent) {
        boolean success = new RoleDocumentDelegationMemberRule().processAddDelegationMember(addDelegationMemberEvent);
        RoleDocumentDelegationMember roleDocumentDelegationMember = addDelegationMemberEvent.getDelegationMember();
        success &= validateActiveDate("delegationMember.activeFromDate", roleDocumentDelegationMember.getActiveFromDate(), roleDocumentDelegationMember.getActiveToDate());
        return success;
    }

    public ResponsibilityInternalService getResponsibilityInternalService() {
    	if ( responsibilityInternalService == null ) {
    		responsibilityInternalService = KimImplServiceLocator.getResponsibilityInternalService();
    	}
		return responsibilityInternalService;
	}

    protected RoleTypeService getRoleTypeService(KimType typeInfo) {
        String serviceName = typeInfo.getServiceName();
        if (serviceName != null) {
            try {
                KimTypeService service = (KimTypeService) GlobalResourceLoader.getService(QName.valueOf(serviceName));
                if (service != null && service instanceof RoleTypeService) {
                    return (RoleTypeService) service;
                }
                return (RoleTypeService) KimImplServiceLocator.getService("kimNoMembersRoleTypeService");
            } catch (Exception ex) {
                return (RoleTypeService) KimImplServiceLocator.getService("kimNoMembersRoleTypeService");
            }
        }
        return null;
    }

    private static class VersionedService<T> {

        String version;
        T service;

        VersionedService(String version, T service) {
            this.version = version;
            this.service = service;
        }

        T getService() {
            return this.service;
        }

        String getVersion() {
            return this.version;
        }

    }

    protected VersionedService<RoleTypeService> getVersionedRoleTypeService(KimType typeInfo) {
        String serviceName = typeInfo.getServiceName();
        if (serviceName != null) {
            String version = "2.0.0"; // default version since the base services have been available since then
            RoleTypeService roleTypeService = null;

            try {

                ServiceBus serviceBus = KsbApiServiceLocator.getServiceBus();
                Endpoint endpoint = serviceBus.getEndpoint(QName.valueOf(serviceName));
                if (endpoint != null) {
                    version = endpoint.getServiceConfiguration().getServiceVersion();
                }
                KimTypeService service = (KimTypeService) GlobalResourceLoader.getService(QName.valueOf(serviceName));
                if (service != null && service instanceof RoleTypeService) {
                    roleTypeService = (RoleTypeService) service;
                } else {
                    roleTypeService = (RoleTypeService) KimImplServiceLocator.getService("kimNoMembersRoleTypeService");
                }
            } catch (Exception ex) {
                roleTypeService = (RoleTypeService) KimImplServiceLocator.getService("kimNoMembersRoleTypeService");
            }

            return new VersionedService<RoleTypeService>(version, roleTypeService);
        }

        return null;
    }

}
