// Package 
package gov.va.med.esr.ui.person.action;

// Java Classes
import gov.va.med.esr.common.model.CommonEntityKeyFactory;
import gov.va.med.esr.common.model.ee.CDCondition;
import gov.va.med.esr.common.model.ee.CDDiagnosis;
import gov.va.med.esr.common.model.ee.CDProcedure;
import gov.va.med.esr.common.model.ee.ConflictExperience;
import gov.va.med.esr.common.model.ee.MilitaryService;
import gov.va.med.esr.common.model.ee.MilitaryServiceEpisode;
import gov.va.med.esr.common.model.ee.MilitaryServiceSiteRecord;
import gov.va.med.esr.common.model.ee.POWEpisode;
import gov.va.med.esr.common.model.ee.PurpleHeartDocument;
import gov.va.med.esr.common.model.ee.RatedDisability;
import gov.va.med.esr.common.model.ee.ReceivedEligibility;
import gov.va.med.esr.common.model.ee.SHAD;
import gov.va.med.esr.common.model.ee.SpecialFactor;
import gov.va.med.esr.common.model.financials.FinancialStatement;
import gov.va.med.esr.common.model.financials.IncomeTest;
import gov.va.med.esr.common.model.financials.PatientVisitSummary;
import gov.va.med.esr.common.model.financials.SpouseFinancials;
import gov.va.med.esr.common.model.insurance.Medicare;
import gov.va.med.esr.common.model.insurance.PrivateInsurance;
import gov.va.med.esr.common.model.lookup.Condition;
import gov.va.med.esr.common.model.lookup.Diagnosis;
import gov.va.med.esr.common.model.lookup.EligibilityType;
import gov.va.med.esr.common.model.lookup.Indicator;
import gov.va.med.esr.common.model.lookup.Procedure;
import gov.va.med.esr.common.model.lookup.VAFacility;
import gov.va.med.esr.common.model.party.Address;
import gov.va.med.esr.common.model.party.Email;
import gov.va.med.esr.common.model.party.Phone;
import gov.va.med.esr.common.model.person.Association;
import gov.va.med.esr.common.model.person.Person;
import gov.va.med.esr.common.model.person.PersonMergeInfo;
import gov.va.med.esr.common.model.person.SignatureImage;
import gov.va.med.esr.service.PersonMergeService;
import gov.va.med.esr.service.PersonService;
import gov.va.med.esr.ui.ApplicationConstants;
import gov.va.med.esr.ui.admin.action.PersonMergeWorklistSearchActionForm;
import gov.va.med.esr.ui.common.util.JspUtils;
import gov.va.med.esr.ui.person.beans.PersonMergeRowInfo;
import gov.va.med.esr.ui.person.beans.PersonMergeRowInfoMeta;
import gov.va.med.esr.ui.person.beans.PersonMergeRowInfoMetaData;
import gov.va.med.fw.conversion.ConversionService;
import gov.va.med.fw.model.AbstractKeyedEntity;
import gov.va.med.fw.model.EntityKey;
import gov.va.med.fw.service.ServiceException;
import gov.va.med.fw.ui.UIConstants;
import gov.va.med.fw.ui.struts.ValueListActionUtils;
import gov.va.med.fw.ui.valuelist.InvalidArgumentValueListException;
import gov.va.med.fw.ui.valuelist.MaxRecordsExceededValueListException;
import gov.va.med.fw.ui.valuelist.MissingCapabilityValueListException;
import gov.va.med.fw.ui.valuelist.TimeoutValueListException;
import gov.va.med.fw.util.StringUtils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import net.mlw.vlh.ValueList;
import net.mlw.vlh.ValueListInfo;

import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.beanutils.NestedNullException;
import org.apache.commons.beanutils.PropertyUtilsBean;
import org.apache.commons.collections.comparators.NullComparator;
import org.apache.commons.lang.Validate;
import org.apache.struts.Globals;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.action.ActionMessage;
import org.apache.struts.util.MessageResources;

/**
 * This struts action is used to support the Person Merge Worklist page.
 * 
 * @author Andrew Pach
 * @version 3.0
 */
public class PersonMergeWorklistAction extends PersonMergeAction {
	// Struts forwards
    public static final String FORWARD_DISPLAY = "display";
	public static final String FORWARD_SUCCESS = "success";
	public static final String FORWARD_FAILURE = "failure";
	public static final String FORWARD_PERSON_DETAIL = "personMergeDetail";
	public static final String FORWARD_PERSON_FIND_DETAIL = "personMergeFindDetail";

	// Valuelist Constants
	public static final String PERSON_MERGE_WORKLIST_TABLE_ID = "personMergeWorklistSearchTableId";
    public static final String PERSON_MERGE_WORKLIST_SEARCH_ADAPTER = "personMergeWorklistSearchAdapter";
    public static final String PERSON_MERGE_SEARCH_WORKLIST_SEARCH_ADAPTER = "personMergeSearchWorklistSearchAdapter";
	public static final String PERSON_MERGE_WORKLIST_VALUELIST_REQUEST_KEY = "vlhlist";

	// Keys
	public static final String SELECTED_PERSON_MERGE_INFO_ID_REQUEST_KEY = "selectedPersonMergeInfoId";
	public static final String PRIMARY_PERSON_KEY = "primaryPersonKey";
	public static final String DEPRECATED_PERSON_KEY = "deprecatedPersonKey";
	public static final String PRIMARY_PERSON_DEMOGRAPHIC_FORM_KEY = "primaryPersonDemographicFormKey";
	public static final String DEPRECATED_PERSON_DEMOGRAPHIC_FORM_KEY = "deprecatedPersonDemographicFormKey";

	// Conversion Services
	private ConversionService mergeDemographicsConversionService = null;
	private ConversionService demographicPersonalConversionService = null;

	/**
	 * Default constructor.
	 */
	public PersonMergeWorklistAction() {
		super();
	}

	/**
	 * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
	 */
	public void afterPropertiesSet() {
	}

    /**
     * Displays the main page.
     *
     * @param mapping Struts action mapping for this action
     * @param form Struts form bean for this action
     * @param request The Http Request
     * @param response The Http Response
     *
     * @return A struts action forward for where we will go next.
     * @throws Exception If there are any errors during processing.
     */
    public ActionForward display(ActionMapping mapping, ActionForm form,
        HttpServletRequest request, HttpServletResponse response) throws Exception
    {
        // Get the value list information
        ValueList valueList = ValueListActionUtils.getValueList(form,
            request, PERSON_MERGE_WORKLIST_TABLE_ID, PERSON_MERGE_WORKLIST_SEARCH_ADAPTER);

        // Backup the ValueListInfo into session and store the resultant ValueList on the request
        ValueListActionUtils
            .setValueList(request, valueList, PERSON_MERGE_WORKLIST_VALUELIST_REQUEST_KEY);

        // Return to the display
        return mapping.findForward(FORWARD_DISPLAY);
    }
    
	/**
	 * Displays the veteran merge search page.
	 * 
	 * @param mapping
	 *            Struts action mapping for this action
	 * @param form
	 *            Struts form bean for this action
	 * @param request
	 *            The Http Request
	 * @param response
	 *            The Http Response
	 * 
	 * @return A struts action forward for where we will go next.
	 * @throws Exception
	 *             If there are any errors during processing.
	 */
	public ActionForward mergeSearch(ActionMapping mapping, ActionForm form,
			HttpServletRequest request, HttpServletResponse response)
			throws Exception {
		// Clear the search form
		PersonMergeWorklistSearchActionForm personMergeWorklistSearchActionForm = (PersonMergeWorklistSearchActionForm) form;
		personMergeWorklistSearchActionForm.clear();

		// Forward to the overview screen
		return mapping.findForward(FORWARD_SUCCESS);
	}

	public ActionForward clear(ActionMapping mapping, ActionForm form,
			HttpServletRequest request, HttpServletResponse response) throws Exception{
		return this.mergeSearch(mapping, form, request, response);
	}
	/**
	 * Performs a person search based on the search criteria entered by the
	 * user.
	 * 
	 * @param mapping
	 *            An action mapping
	 * @param form
	 *            A form bean
	 * @param request
	 *            A http request from an search.jsp page
	 * @param response
	 *            A http response to stream data to a next page
	 * 
	 * @return An action forward class encapsulating information about a next
	 *         page
	 * @throws Exception
	 *             if any errors occurred while trying to process the person
	 *             search.
	 */
	public ActionForward search(ActionMapping mapping, ActionForm form,
			HttpServletRequest request, HttpServletResponse response)
			throws Exception {
		// ActionForward that defines where we will forward to next
		ActionForward forward = null;

		// Ensure we have a valid search form
		PersonMergeWorklistSearchActionForm personMergeWorklistSearchActionForm = (form instanceof PersonMergeWorklistSearchActionForm) ? (PersonMergeWorklistSearchActionForm) form
				: null;
		Validate.notNull(personMergeWorklistSearchActionForm,
				"PersonMergeWorklistSearchActionForm bean is null");

		try {

			// Get the value list information
			ValueList valueList = ValueListActionUtils.getValueList(form,
					request, PERSON_MERGE_WORKLIST_TABLE_ID,
					PERSON_MERGE_SEARCH_WORKLIST_SEARCH_ADAPTER);

			// Check if any results were found
			ValueListInfo info = valueList.getValueListInfo();
			if (info.getTotalNumberOfEntries() == 0) {
				// Clear the search form
				personMergeWorklistSearchActionForm.clear();

				// No results were found so generate an error and forward to the
				// failure page
				addActionMessage(
						request,
						new ActionMessage(
								ApplicationConstants.MessageKeys.ERRORS_SEARCH_NORESULT));
				forward = mapping.findForward(FORWARD_FAILURE);
			} else {
				if (info.getTotalNumberOfEntries() == 1) {
					// 1 result was found so automatically retrieve the person
					// and forward to
					// the overview screen.
					List results = valueList.getList();
					PersonMergeInfo personMergeInfo = (PersonMergeInfo) results
							.get(0);
					this.putSandboxEntry(request,
							SELECTED_PERSON_MERGE_INFO_ID_REQUEST_KEY,
							personMergeInfo.getEntityKey()
									.getKeyValueAsString());

					// Forward to the overview page
					forward = mapping.findForward(FORWARD_PERSON_FIND_DETAIL);
				}

				if (forward == null) {
					// Backup the ValueListInfo into session and store the
					// resultant ValueList on the request
					ValueListActionUtils.setValueList(request, valueList,
							PERSON_MERGE_WORKLIST_VALUELIST_REQUEST_KEY);

					// Forward to the success URL (i.e. display results)
					forward = mapping.findForward(FORWARD_SUCCESS);
				}
			}
		} catch (MaxRecordsExceededValueListException ex) {
			// The search exceeded the maximum number of records allowed so
			// generate the appropriate
			// error message.
			if ((ex.getTotalRecords() > 0) && (ex.getRecordLimit() > 0)) {
				// We know both the total number of records that would have been
				// returned as well
				// as the configured limit.
				addActionMessage(
						request,
						new ActionMessage(
								ApplicationConstants.MessageKeys.ERRORS_MAX_RECORDS_EXCEEDED,
								new Integer(ex.getTotalRecords()), new Integer(
										ex.getRecordLimit())));
			} else {
				// We only know the configured limit
				if (ex.getRecordLimit() > 0) {
					addActionMessage(
							request,
							new ActionMessage(
									ApplicationConstants.MessageKeys.ERRORS_MAX_RECORDS_EXCEEDED_RECORD_LIMIT,
									new Integer(ex.getRecordLimit())));
				} else {
					// Provide a generic message
					addActionMessage(
							request,
							new ActionMessage(
									ApplicationConstants.MessageKeys.ERRORS_MAX_RECORDS_EXCEEDED_NO_PARAMS));
				}
			}

			// Forward back to the search screen
			forward = mapping.findForward(FORWARD_FAILURE);
		} catch (MissingCapabilityValueListException ex) {
			// The user is not permitted to perform this search
			addActionMessage(
					request,
					new ActionMessage(
							ApplicationConstants.MessageKeys.ERRORS_INVALID_SEARCH_PERMISSION));
			forward = mapping.findForward(FORWARD_FAILURE);
		} catch (TimeoutValueListException ex) {
			// The user is not permitted to perform this search
			addActionMessage(request, TIMEOUT_ERROR_KEY);
			forward = mapping.findForward(FORWARD_FAILURE);
		} catch (InvalidArgumentValueListException ex) {
			// The user entered an invalid argument.
			addActionMessage(request, INVALID_ARGUMENT_KEY);
			forward = mapping.findForward(FORWARD_FAILURE);
		}

		// Return the forward
		return forward;
	}

	/**
	 * Selects the PersonMergeInfo record to start merging.
	 * 
	 * @param mapping
	 *            Struts action mapping for this action
	 * @param form
	 *            Struts form bean for this action
	 * @param request
	 *            The Http Request
	 * @param response
	 *            The Http Response
	 * 
	 * @return A struts action forward for where we will go next.
	 * @throws Exception
	 *             If there are any errors during processing.
	 */
	public ActionForward selectPerson(ActionMapping mapping, ActionForm form,
			HttpServletRequest request, HttpServletResponse response)
			throws Exception {
		// Get the Person Merge Info ID to merge
		String personMergeInfoId = request
				.getParameter(SELECTED_PERSON_MERGE_INFO_ID_REQUEST_KEY);
		if (personMergeInfoId == null) {
			personMergeInfoId = (String) this.getSandboxEntry(request,
					SELECTED_PERSON_MERGE_INFO_ID_REQUEST_KEY);
		}
		Validate.notNull(personMergeInfoId, "Person Merge Info ID is null.");

		// Check if a person merge info map is already in session. If so, that
		// means that
		// the user left previously without clicking one of the on-screen
		// buttons.
		Map personMergeInfoMap = getPersonMergeInfoMapFromSession(request);
		if (personMergeInfoMap != null) {
			// Get the person merge info from the previous time the user doing a
			// merge
			PersonMergeInfo personMergeInfo = (PersonMergeInfo) personMergeInfoMap
					.get(PERSON_MERGE_INFO_KEY);
			if (personMergeInfo != null) {
				// If the person merge info Id is the same as the one the user
				// is selcting
				// now, just forward to the detail screen since all the data is
				// already primed.
				if (personMergeInfoId.equals(personMergeInfo.getEntityKey()
						.getKeyValueAsString())) {
					// Go to the person merge detail (i.e. the first page)
					return mapping.findForward(FORWARD_PERSON_DETAIL);
				}
			}
		}

		// Create and populate the person merge info map which will contain all
		// the JSP
		// and merge needed information.
		personMergeInfoMap = new HashMap();

		// Retrieve the PersonMergeInfo object based on the passed in ID
		PersonMergeService personMergeService = getPersonMergeService();
		PersonMergeInfo personMergeInfo = personMergeService
				.getPersonMergeInfo(CommonEntityKeyFactory
						.createPersonMergeInfoEntityKey(personMergeInfoId));

		PersonService personService = getPersonService();
		Person primaryPerson = personService.getPerson(personMergeInfo
				.getPrimaryPersonEntityKey());
		Person deprecatedPerson = personService.getPerson(personMergeInfo
				.getDeprecatedPersonEntityKey());

		// Populate the previously saved user selections
		populateUserSelections(personMergeInfoMap, personMergeInfo);

		// Populate the beans
		populateBeans(request, personMergeInfoMap, primaryPerson,
				deprecatedPerson);

		// Place the person merge info map into the session
		setPersonMergeInfoMapInSession(request, personMergeInfoMap);

		// Lock the PersonMergeInfo data
		personMergeInfo.setRecordLocked(Boolean.TRUE);
		personMergeInfo = personMergeService
				.updatePersonMergeInfo(personMergeInfo);

		// Store the updated person merge info
		personMergeInfoMap.put(PERSON_MERGE_INFO_KEY, personMergeInfo);

		// Unlock any previous person merge info if the user left without
		// clicking one of our
		// buttons.
		unlockPreviousPersonMerge(request);

		// Set entityKey in Session, so that session timeout listener can make
		// sure to
		// unlock it in case user leaves without clicking one of the on-screen
		// buttons.
		setLockedMergeInfoEntityKey(request, personMergeInfo.getEntityKey());

		if (logger.isDebugEnabled()) {
			logger.debug("Locking person merge record with ID "
					+ personMergeInfo.getEntityKey().getKeyValueAsString()
					+ " on session " + request.getSession().getId()
					+ " and lock key placed in session.");
		}

		// Go to the person merge detail (i.e. the first page)
		return mapping.findForward(FORWARD_PERSON_DETAIL);
	}

	/**
	 * Checks to see if a previous person merge was left without exiting
	 * properly (i.e. through one of the on-screen buttons). If so, unlock the
	 * previous person merge.
	 * 
	 * @param request
	 *            The HttpServletRequest
	 */
	protected void unlockPreviousPersonMerge(HttpServletRequest request)
			throws ServiceException {
		// See if a prior "lock" entity key is in session
		Object key = getLockedMergeInfoEntityKey(request);
		if (key != null && key instanceof EntityKey) {
			// Get the old person merge info
			PersonMergeService personMergeService = getPersonMergeService();
			PersonMergeInfo oldInfo = personMergeService
					.getPersonMergeInfo((EntityKey) key);

			// Unlock the prior record and update it
			oldInfo.setRecordLocked(Boolean.FALSE);
			personMergeService.updatePersonMergeInfo(oldInfo);
		}
	}

	/**
	 * Populates the user selection map and places it in the person merge info
	 * map. Entries in the database are of the form key1=value1;key2=value2,
	 * etc.
	 * 
	 * @param personMergeInfoMap
	 *            The person merge info map
	 * @param personMergeInfo
	 *            The person merge info
	 */
	protected void populateUserSelections(Map personMergeInfoMap,
			PersonMergeInfo personMergeInfo) {
		// Create the user selection map
		HashMap userSelectionMap = new HashMap();

		// The selections are the primary and deprecated field keys separated by
		// a delimiter
		String mergeSelections = personMergeInfo.getMergeSelections();

		if (StringUtils.isNotEmpty(mergeSelections)) {
			// Get an array of individual selections and iterate through them
			String[] selectionArray = mergeSelections.split(DELIMITER);
			for (int i = 0; i < selectionArray.length; i++) {
				// For each selection, put them in the selection map.
				String selection = (String) selectionArray[i];
				String[] keyValue = selection.split(EQUALS);
				if (keyValue.length > 2) {
					// Ensure we have no more than 2 tokens (i.e. the key and
					// optional value).
					throw new IllegalArgumentException("User selection '"
							+ selection + "' contains " + keyValue.length + " "
							+ EQUALS + " characters.  Only 1 is allowed.");
				}
				userSelectionMap.put(keyValue[0],
						(keyValue.length == 2) ? keyValue[1] : null);
			}
		}

		// Put the user selection map into the person merge info map
		personMergeInfoMap.put(PERSON_USER_SELECTIONS_KEY, userSelectionMap);
	}

	/**
	 * Creates and populates PersonMergeRowInfo beans from their
	 * PersonMergeRowInfoMeta counterpart by populating the data objects. The
	 * resultant data is placed in the personMergeInfoMap. The passed in
	 * primaryObject and deprecatedObject should typically be a Person object.
	 * 
	 * @param request
	 *            The HttpServletRequest
	 * @param personMergeInfoMap
	 *            The map that will contain the PersonMergeRowInfo objects.
	 * @param primaryObject
	 *            The primary object.
	 * @param deprecatedObject
	 *            The deprecated object.
	 * 
	 * @throws Exception
	 *             if any problems were encountered.
	 */
	protected void populateBeans(HttpServletRequest request,
			Map personMergeInfoMap, Object primaryObject,
			Person deprecatedObject) throws Exception {
		// Iterate through the set of tabs
		Set groupKeySet = personMergeRowGroupInfoMetaMap.keySet();
		for (Iterator iterator = groupKeySet.iterator(); iterator.hasNext();) {
			// Get the tab name (i.e. the metaGroupKey)
			String metaGroupKey = (String) iterator.next();

			// Get the section list for the tab
			List groupMetaSectionList = (List) personMergeRowGroupInfoMetaMap
					.get(metaGroupKey);

			// Each group meta key (e.g. demographics) will contain a list of
			// sections
			// (e.g. demographics_personal, demographics_associations, etc.)
			ArrayList groupSectionList = new ArrayList();
			personMergeInfoMap.put(metaGroupKey, groupSectionList);

			// Add an arraylist of section fieldnames for the expand/collapse
			// all list
			ArrayList expColFieldnameList = new ArrayList();
			personMergeInfoMap.put(metaGroupKey
					+ PERSON_EXP_COL_FIELDNAMES_EXT_KEY, expColFieldnameList);

			// Iterate through all the sections
			for (Iterator groupMetaSectionListIter = groupMetaSectionList
					.iterator(); groupMetaSectionListIter.hasNext();) {
				// Get a section group metadata entry (e.g.
				// demographics_personal)
				PersonMergeRowInfoMeta groupMetaSection = (PersonMergeRowInfoMeta) groupMetaSectionListIter
						.next();

				// Add the section fieldname to the expand/collapse list if it
				// is a parent
				if (groupMetaSection.isParentRow()) {
					expColFieldnameList.add(groupMetaSection.getFieldName());
				}

				// Assume we will use the primary and deprecated objects while
				// adding the section
				// and its children
				Object primaryValue = primaryObject;
				Object deprecatedValue = deprecatedObject;

				// If a field propery or helper method was specified, get the
				// bean information
				// to use while adding the section and its children
				if ((StringUtils
						.isNotEmpty(groupMetaSection.getFieldProperty()))
						|| (StringUtils.isNotEmpty(groupMetaSection
								.getFieldPropertyHelperMethod()))) {
					// A field property is defined so get the field on the
					// primary and deprecated
					// objects.
					primaryValue = getBeanInfo(primaryObject, groupMetaSection,
							false);
					deprecatedValue = getBeanInfo(deprecatedObject,
							groupMetaSection, false);
				}

				// Add the section and its children
				addSectionGroupInfoWithChildren(request, primaryValue,
						deprecatedValue, groupMetaSection, groupSectionList);
			}
		}
	}

	/**
	 * Generic matching method for all types of objects accross Person's.
	 * Different objects are compared differently based on their class type.
	 * 
	 * @param object1
	 *            The first object
	 * @param object2
	 *            The second object
	 * 
	 * @return True if the objects match or false if not.
	 */
	public boolean match(Object object1, Object object2) {
		// ************************* Eligibility match methods
		// ************************* //

		// Match RatedDisability
		if (RatedDisability.class.isAssignableFrom(object1.getClass())) {
			RatedDisability object1Disability = (RatedDisability) object1;
			RatedDisability object2Disability = (RatedDisability) object2;
			String object1DisabilityCode = null;
			String object2DisabilityCode = null;
			if ((object1Disability !=null) && (object1Disability.getDisability() != null) && (object1Disability.getDisability().getCode() != null))
				object1DisabilityCode = object1Disability.getDisability().getCode();
			if ((object2Disability !=null) && (object2Disability.getDisability() != null) && (object2Disability.getDisability().getCode() != null))
				object2DisabilityCode = object2Disability.getDisability().getCode();
			return (object1DisabilityCode != null && object2DisabilityCode != null) ? object1DisabilityCode.equals(object2DisabilityCode) : false;
		}		

		// Match PurpleHeartDocument
		if (PurpleHeartDocument.class.isAssignableFrom(object1.getClass())) {
			return true;
		}

		// Match POWEpisode
		if (POWEpisode.class.isAssignableFrom(object1.getClass())) {
			return true;
		}

		// Match CDCondition
		if (CDCondition.class.isAssignableFrom(object1.getClass())) {
			Condition condition1 = ((CDCondition) object1).getCondition();
			Condition condition2 = ((CDCondition) object2).getCondition();
			return (condition1 != null && condition2 != null) ? condition1
					.equals(condition2) : false;
		}

		// Match CDProcedure
		if (CDProcedure.class.isAssignableFrom(object1.getClass())) {
			Procedure procedure1 = ((CDProcedure) object1).getProcedure();
			Procedure procedure2 = ((CDProcedure) object2).getProcedure();
			return (procedure1 != null && procedure2 != null) ? procedure1
					.equals(procedure2) : false;
		}

		// Match CDDiagnosis
		if (CDDiagnosis.class.isAssignableFrom(object1.getClass())) {
			Diagnosis diagnosis1 = ((CDDiagnosis) object1).getDiagnosis();
			Diagnosis diagnosis2 = ((CDDiagnosis) object2).getDiagnosis();
			return (diagnosis1 != null && diagnosis2 != null) ? diagnosis1
					.equals(diagnosis2) : false;
		}

		// Match ReceivedEligibility
		if (ReceivedEligibility.class.isAssignableFrom(object1.getClass())) {
			EligibilityType type1 = ((ReceivedEligibility) object1).getType();
			EligibilityType type2 = ((ReceivedEligibility) object2).getType();
			return (type1 != null && type2 != null) ? type1.equals(type2)
					: false;
		}

		// ************************* Demographics match methods
		// ************************* //

		// Associations are always equal
		if (Association.class.isAssignableFrom(object1.getClass())) {
			return true;
		}

		// Address are considered the same if their types are the same
		if (Address.class.isAssignableFrom(object1.getClass())) {
			Address object1Address = (Address) object1;
			Address object2Address = (Address) object2;
			String object1AddressCode = null;
			String object2AddressCode = null;
			if ( (object1Address !=null) && (object1Address.getType() !=null) && (object1Address.getType().getCode() !=null) )
				object1AddressCode = object1Address.getType().getCode();
			if ( ( object2Address !=null) && (object2Address.getType() !=null) && (object2Address.getType().getCode() !=null) )
				object2AddressCode = object2Address.getType().getCode();
			return (object1AddressCode != null && object2AddressCode != null) ? object1AddressCode.equals(object2AddressCode) : false;   
		}

		// Email Addresses are considered the same if their types are the same
		if (Email.class.isAssignableFrom(object1.getClass())) {
			Email object1Email = (Email) object1;
			Email object2Email = (Email) object2;
			String object1EmailCode = null;
			String object2EmailCode = null;
			if ( (object1Email !=null) && (object1Email.getType() !=null) && (object1Email.getType().getCode() !=null) )
				object1EmailCode = object1Email.getType().getCode();
			if ( ( object2Email !=null) && (object2Email.getType() !=null) && (object2Email.getType().getCode() !=null) )
				object2EmailCode = object2Email.getType().getCode();
			return (object1EmailCode != null && object2EmailCode != null) ? object1EmailCode.equals(object2EmailCode) : false;   
		}		

		// Phone Numbers are considered the same if their types are the same
		if (Phone.class.isAssignableFrom(object1.getClass())) {
			Phone object1Phone = (Phone) object1;
			Phone object2Phone = (Phone) object2;
			String object1PhoneCode = null;
			String object2PhoneCode = null;
			if ( (object1Phone !=null) && (object1Phone.getType() !=null) && (object1Phone.getType().getCode() !=null) )
				object1PhoneCode = object1Phone.getType().getCode();
			if ( ( object2Phone !=null) && (object2Phone.getType() !=null) && (object2Phone.getType().getCode() !=null) )
				object2PhoneCode = object2Phone.getType().getCode();
			return (object1PhoneCode != null && object2PhoneCode != null) ? object1PhoneCode.equals(object2PhoneCode) : false;   
		}		

		// MilitaryServiceSiteRecords are considered the same if their sites the
		// same
		if (MilitaryServiceSiteRecord.class
				.isAssignableFrom(object1.getClass())) {
			MilitaryServiceSiteRecord object1SiteRec = (MilitaryServiceSiteRecord) object1;
			MilitaryServiceSiteRecord object2SiteRec = (MilitaryServiceSiteRecord) object2;
			String object1SiteRecCode = null;
			String object2SiteRecCode = null;
			if ( (object1SiteRec !=null) && (object1SiteRec.getSite() !=null) && (object1SiteRec.getSite().getCode() !=null) )
				object1SiteRecCode = object1SiteRec.getSite().getCode();
			if ( ( object2SiteRec !=null) && (object2SiteRec.getSite() !=null) && (object2SiteRec.getSite().getCode() !=null) )
				object2SiteRecCode = object2SiteRec.getSite().getCode();
			return (object1SiteRecCode != null && object2SiteRecCode != null) ? object1SiteRecCode.equals(object2SiteRecCode) : false;   
		}

		// FacilityFeeBasis are considered the same if the visited facility is
		// same
		if (FacilityFeeBasis.class.isAssignableFrom(object1.getClass())) {
			FacilityFeeBasis object1ffb = (FacilityFeeBasis) object1;
			FacilityFeeBasis object2ffb = (FacilityFeeBasis) object2;
			String object1ffbCode = null;
			String object2ffbCode = null;
			if ( (object1ffb !=null) && (object1ffb.getFacility() !=null) && (object1ffb.getFacility().getCode() !=null) )
				object1ffbCode = object1ffb.getFacility().getCode();
			if ( ( object2ffb !=null) && (object2ffb.getFacility() !=null) && (object2ffb.getFacility().getCode() !=null) )
				object2ffbCode = object2ffb.getFacility().getCode();
			return (object1ffbCode != null && object2ffbCode != null) ? object1ffbCode.equals(object2ffbCode) : false;   
		}		

		// SignatureImage are considered the same if their submitting facility
		// is same
		if (SignatureImage.class.isAssignableFrom(object1.getClass())) {
			SignatureImage object1SigImage = (SignatureImage) object1;
			SignatureImage object2SigImage = (SignatureImage) object2;
			String object1Code = null;
			String object2Code = null;

			if ((object1SigImage != null) && (object1SigImage.getSiteSubmittingImage() != null) && (object1SigImage.getSiteSubmittingImage().getCode() != null))
				   object1Code = object1SigImage.getSiteSubmittingImage().getCode();

			if ((object2SigImage != null) && (object2SigImage.getSiteSubmittingImage() != null) && (object2SigImage.getSiteSubmittingImage().getCode() != null ))
				   object2Code = object2SigImage.getSiteSubmittingImage().getCode();

			return (object1Code != null && object2Code != null) ? object1Code.equals(object2Code) : false;                    
		}

		// Military Service Episodes are always equal
		if (MilitaryServiceEpisode.class.isAssignableFrom(object1.getClass())) {
			return true;
		}
		
		// Conflict Experiences are always equal
		if (ConflictExperience.class.isAssignableFrom(object1.getClass())) {
			return true;
		}

		// Medicare objects are considered the same if their sites and their
		// types are the same
		if (Medicare.class.isAssignableFrom(object1.getClass())) {
			Medicare object1Medicare = (Medicare) object1;
			Medicare object2Medicare = (Medicare) object2;
			boolean object1PartA = ((object1Medicare.isEnrolledInPartA() != null) && (Boolean.TRUE
					.equals(object1Medicare.isEnrolledInPartA())));
			boolean object2PartA = ((object2Medicare.isEnrolledInPartA() != null) && (Boolean.TRUE
					.equals(object2Medicare.isEnrolledInPartA())));
			return ((object1Medicare.getReportSite().getStationNumber()
					.equals(object2Medicare.getReportSite().getStationNumber())) && (object1PartA == object2PartA));
		}

		// Private Insurance objects are considered the same if their sites are
		// the same
		if (PrivateInsurance.class.isAssignableFrom(object1.getClass())) {
			PrivateInsurance object1Insurance = (PrivateInsurance) object1;
			PrivateInsurance object2Insurance = (PrivateInsurance) object2;
			String object1InsuranceNum = null;
			String object2InsuranceNum = null;
			if ( (object1Insurance !=null) && (object1Insurance.getReportSite() !=null) && (object1Insurance.getReportSite().getStationNumber() !=null) )
				object1InsuranceNum = object1Insurance.getReportSite().getStationNumber();
			if ( ( object2Insurance !=null) && (object2Insurance.getReportSite() !=null) && (object2Insurance.getReportSite().getStationNumber() !=null) )
				object2InsuranceNum = object2Insurance.getReportSite().getStationNumber();
			return (object1InsuranceNum != null && object2InsuranceNum != null) ? object1InsuranceNum.equals(object2InsuranceNum) : false;   
		}		


		// ************************* Facility match methods
		// ************************* //

		// Match VAFacility
		if (VAFacility.class.isAssignableFrom(object1.getClass())) {
			VAFacility type1 = ((VAFacility) object1);
			VAFacility type2 = ((VAFacility) object2);
			return (type1 != null && type2 != null) ? type1.equals(type2)
					: false;
		}

		// Match VAFacility
		if (PatientVisitSummary.class.isAssignableFrom(object1.getClass())) {
			return true;
		}

		// Match VAFacility
		if (SignatureImage.class.isAssignableFrom(object1.getClass())) {
			return true;
		}

		// Use the default "equals" check
		return super.match(object1, object2);
	}

	/**
	 * Add the section group information to the group info list and all its
	 * children
	 * 
	 * @param primaryObject
	 *            The primary object
	 * @param deprecatedObject
	 *            The deprecated object
	 * @param groupMeta
	 *            The group meta data entry
	 * @param groupInfoList
	 *            The group information list to add the group info to
	 * 
	 * @throws Exception
	 *             if any problems were encountered
	 */
	protected void addSectionGroupInfoWithChildren(HttpServletRequest request,
			Object primaryObject, Object deprecatedObject,
			PersonMergeRowInfoMeta groupMeta, List groupInfoList)
			throws Exception {
		// Create a group information row info entry for the section
		PersonMergeRowInfo groupInfo = addSectionGroupInfo(request, groupMeta,
				groupInfoList);

		// Add the children for the section
		addGroupChildren(request, groupInfo, primaryObject, deprecatedObject);

		// Adjust section group info to remove unnecessary check boxes now that
		// the children
		// have been added
		adjustParentGroupInfo(groupInfo);
	}

	/**
	 * Adjusts the parent group information by possibly removing the user
	 * selection if there is nothing that should be selectable based on the
	 * child selections.
	 * 
	 * @param parentInfo
	 *            the parent information
	 */
	protected void adjustParentGroupInfo(PersonMergeRowInfo parentInfo) {
		// Only process if our parent is selectable as a whole
		if (parentInfo.isSelectable()) {
			// Assume we will make our parent unselected for both primary and
			// deprecated
			boolean unselectPrimary = true;
			boolean unselectDeprecated = true;
			boolean childSelectable = false;

			// Iterate through all the parent group info's children. If any item
			// is selectable
			// and contains an input type, then don't "unselect" it. Also, if no
			// child items are
			// selectable, then also don't unselect it since the parent is the
			// only thing
			// that is selectable.
			List children = parentInfo.getChildren();
			for (Iterator iterator = children.iterator(); iterator.hasNext();) {
				PersonMergeRowInfo childInfo = (PersonMergeRowInfo) iterator
						.next();
				if (childInfo.isSelectable()) {
					// At least one child is selectable so set a flag
					childSelectable = true;

					// Check if the item has an input field present
					if (StringUtils.isNotEmpty(childInfo.getPrimaryInputType())) {
						unselectPrimary = false;
					}
					if (StringUtils.isNotEmpty(childInfo
							.getDeprecatedInputType())) {
						unselectDeprecated = false;
					}
				}
			}

			// If we are a parent row with no children (e.g. a parent with an
			// empty collection),
			// then we want to still remove the input types from the parent.
			if ((!childSelectable) && (parentInfo.isParentRow())
					&& (children.size() == 0)) {
				childSelectable = true;
			}

			// If we are a parent row and all the children recursively have all
			// the data
			// the same (e.g. two identical records), then we want to still
			// remove the input
			// types from the parent.
			if ((!childSelectable) && (parentInfo.isParentRow())
					&& (parentInfo.getAllChildrenSame())) {
				childSelectable = true;
			}

			// Only adjust the parent selection if at least one child is
			// selectable
			if (childSelectable) {
				// Adjust the input type if any entries should be unselected
				if (unselectPrimary) {
					parentInfo.setPrimaryInputType(null);
					parentInfo.setPrimaryObject(null);
				}
				if (unselectDeprecated) {
					parentInfo.setDeprecatedInputType(null);
					parentInfo.setDeprecatedObject(null);
				}
			}
		}
	}

	/**
	 * Adds group (non-section) information and its children (e.g. the list of
	 * associates, addresses, etc.).
	 * 
	 * @param request
	 *            The HttpServletRequest
	 * @param sectionGroupInfo
	 *            The parent section group info
	 * @param primaryObject
	 *            The primary object for the set
	 * @param deprecatedObject
	 *            The deprecated object for the set
	 * @param groupMeta
	 *            The group metadata
	 * @param count
	 *            The iteration count for this group
	 * 
	 * @throws Exception
	 *             if any problems were encountered
	 */
	protected void addGroupInfoWithChildren(HttpServletRequest request,
			PersonMergeRowInfo sectionGroupInfo, Object primaryObject,
			Object deprecatedObject, PersonMergeRowInfoMeta groupMeta, int count)
			throws Exception {
		// Get the message resources in case we need to generate error messages
		MessageResources messageResources = ((MessageResources) request
				.getAttribute(Globals.MESSAGES_KEY));

		// Create a group information row info entry
		PersonMergeRowInfo groupInfo = new PersonMergeRowInfo(groupMeta);

		// Store the iteration count
		groupInfo.setCount(count);

		// Store the entity keys for the primary and deprecated objects
		if ((primaryObject != null)
				&& (AbstractKeyedEntity.class.isAssignableFrom(primaryObject
						.getClass()))) {
			AbstractKeyedEntity primaryEntityKey = (AbstractKeyedEntity) primaryObject;
			groupInfo.setPrimaryEntityKey(primaryEntityKey.getEntityKey()
					.getKeyValueAsString());
		}
		if ((deprecatedObject != null)
				&& (AbstractKeyedEntity.class.isAssignableFrom(deprecatedObject
						.getClass()))) {
			AbstractKeyedEntity deprecatedEntityKey = (AbstractKeyedEntity) deprecatedObject;
			groupInfo.setDeprecatedEntityKey(deprecatedEntityKey.getEntityKey()
					.getKeyValueAsString());
		}

		// Add the group information to the parent section group
		sectionGroupInfo.addChild(groupInfo);

		// Add the children (if any are present)
		if ((primaryObject != null) || (deprecatedObject != null)) {
			addGroupChildren(request, groupInfo, primaryObject,
					deprecatedObject);
		}

		// Set the default field types
		setDefaultFieldTypes(groupInfo);

		// Adjust the input types as needed
		if (groupInfo.isSelectable()) {
			String fieldLabel = messageResources.getMessage(sectionGroupInfo
					.getGroupFieldMessageKey());
			groupInfo.setPrimaryObject("Select Surviving " + fieldLabel);
			groupInfo.setDeprecatedObject("Select Deprecated " + fieldLabel);

			if (groupInfo.getSelectableChildrenSame()) {
				// If both sets of children objects are the same and it's single
				// select,
				// null out the input type and data values so they aren't
				// selectable by the user
				if (groupInfo.isSingleSelect()) {
					groupInfo.setPrimaryObject("");
					groupInfo.setDeprecatedObject("");
					groupInfo.setPrimaryInputType(null);
					groupInfo.setDeprecatedInputType(null);
				}
			} else {
				// If the primary or deprecated object is empty, null it's input
				// type out.
				if (primaryObject == null) {
					groupInfo.setPrimaryInputType(null);
					groupInfo.setPrimaryObject("");
				}
				if (deprecatedObject == null) {
					groupInfo.setDeprecatedInputType(null);
					groupInfo.setDeprecatedObject("");
				}
			}
		} else {
			// Nothing is selectable so blank out the data values and input
			// types
			groupInfo.setPrimaryObject("");
			groupInfo.setDeprecatedObject("");
		}
	}

	/**
	 * Add the section group information to the group info list. Sections may
	 * contain group children for sets or non-group children for non-sets (e.g.
	 * Personal, Associates, etc.). Note that section group's children are not
	 * added with this method.
	 * 
	 * @param request
	 *            The HttpServletRequest
	 * @param groupMeta
	 *            The group meta data entry
	 * @param groupInfoList
	 *            The group information list to add the group info to
	 * 
	 * @return the created PersonMergeRowInfo "section group info" entry that
	 *         was added
	 * @throws Exception
	 *             if any problems were encountered
	 */
	protected PersonMergeRowInfo addSectionGroupInfo(
			HttpServletRequest request, PersonMergeRowInfoMeta groupMeta,
			List groupInfoList) throws Exception {
		// Get the message resources in case we need to generate error messages
		MessageResources messageResources = ((MessageResources) request
				.getAttribute(Globals.MESSAGES_KEY));

		// Create a section group information row info entry
		PersonMergeRowInfo groupInfo = new PersonMergeRowInfo(groupMeta);

		// Set the default field types
		setDefaultFieldTypes(groupInfo);

		// Set the primary and deprecated data value text.
		if (groupInfo.isSelectable()) {
			String fieldLabel = messageResources.getMessage(groupInfo
					.getFieldMessageKey());
			groupInfo.setPrimaryObject("Select All Surviving " + fieldLabel);
			groupInfo
					.setDeprecatedObject("Select All Deprecated " + fieldLabel);
		} else {
			groupInfo.setPrimaryObject("");
			groupInfo.setDeprecatedObject("");
		}

		// Add the section group to the group list
		groupInfoList.add(groupInfo);

		// Return the newly created section group information
		return groupInfo;
	}

	/**
	 * Add the group information to the group info list
	 * 
	 * @param groupInfo
	 *            The group information to add the children to
	 * @param primaryObject
	 *            The primary object
	 * @param deprecatedObject
	 *            The deprecated object
	 * @param request
	 *            The HttpServletRequest
	 * 
	 * @throws Exception
	 *             if any problems were encountered
	 */
	protected void addGroupChildren(HttpServletRequest request,
			PersonMergeRowInfo groupInfo, Object primaryObject,
			Object deprecatedObject) throws Exception {
		// Get the message resources in case we need to generate error messages
		MessageResources messageResources = ((MessageResources) request
				.getAttribute(Globals.MESSAGES_KEY));

		// Process all the properties of the group
		List childMetaList = (List) personMergeRowInfoMetaMap.get(groupInfo
				.getFieldName());
		if (childMetaList != null) {
			for (Iterator childIterator = childMetaList.iterator(); childIterator
					.hasNext();) {
				PersonMergeRowInfoMeta childMeta = (PersonMergeRowInfoMeta) childIterator
						.next();
				if (childMeta.isParentRow()) {
					// We have another parent node that needs to be processed.
					// Get the primary and deprecated objects.
					Object primaryValue = getBeanInfo(primaryObject, childMeta,
							false);
					Object deprecatedValue = getBeanInfo(deprecatedObject,
							childMeta, false);

					// Try to get either the primary of deprecated object who
					// has data populated
					// so we can determine if we have a collection to process or
					// just a single value
					Object typeCheck = primaryValue == null ? deprecatedValue
							: primaryValue;
					if (typeCheck instanceof Collection) {
						// Process the collection

						// Create a new parent group row info entry for this
						// child
						// (e.g. military service episodes)
						PersonMergeRowInfo parentInfo = new PersonMergeRowInfo(
								childMeta);

						// Set the default field types
						setDefaultFieldTypes(parentInfo);

						// Set the primary and deprecated data value text.
						if (parentInfo.isSelectable()) {
							String fieldLabel = messageResources
									.getMessage(parentInfo.getFieldMessageKey());
							parentInfo.setPrimaryObject("Select All Surviving "
									+ fieldLabel);
							parentInfo
									.setDeprecatedObject("Select All Deprecated "
											+ fieldLabel);
						} else {
							parentInfo.setPrimaryObject("");
							parentInfo.setDeprecatedObject("");
						}

						// Add the parent group to the main group
						groupInfo.addChild(parentInfo);

						// Create two wrapper sets so we can manipulate the
						// elements
						ArrayList primaryList = primaryValue == null ? new ArrayList()
								: new ArrayList((Collection) primaryValue);
						ArrayList deprecatedList = deprecatedValue == null ? new ArrayList()
								: new ArrayList((Collection) deprecatedValue);

						// Sort the lists on the entity key to produce some
						// reproducable order
						// if a helper method has not been configured.
						if (StringUtils.isEmpty(parentInfo
								.getFieldPropertyHelperMethod())) {
							Collections.sort(primaryList, new BeanComparator(
									ENTITY_KEY_SORT_PROPERTY,
									new NullComparator()));
							Collections.sort(deprecatedList,
									new BeanComparator(
											ENTITY_KEY_SORT_PROPERTY,
											new NullComparator()));
						}

						// Keep count of collection iteration
						int count = 0;

						// Iterate through the deprecated set first to process
						// each element
						for (Iterator iter = deprecatedList.iterator(); iter
								.hasNext();) {
							// Get one deprecated object from the set
							Object setDeprecatedObject = iter.next();

							// Try to find a matching element from the primary
							// set
							Object setPrimaryObject = null;
							for (Iterator matchIter = primaryList.iterator(); ((matchIter
									.hasNext()) && (setPrimaryObject == null));) {
								Object primaryMatchObject = matchIter.next();
								if (match(primaryMatchObject,
										setDeprecatedObject)) {
									setPrimaryObject = primaryMatchObject;
								}
							}

							// Add this deprecated object (and possibly the
							// matching primary
							// object) as a group information element
							// (e.g. a military service episode)
							addGroupInfoWithChildren(request, parentInfo,
									setPrimaryObject, setDeprecatedObject,
									childMeta, count);
							count++;

							// If we found a match, remove it from the primary
							// set
							if (setPrimaryObject != null) {
								primaryList.remove(setPrimaryObject);
							}
						}

						// Now process any elements left in the primary set.
						// Each of them don't
						// have a matching element
						for (Iterator iter = primaryList.iterator(); iter
								.hasNext();) {
							// Get the primary object from the set
							Object setPrimaryObject = iter.next();

							// Add the primary object as a group information
							// element
							addGroupInfoWithChildren(request, parentInfo,
									setPrimaryObject, null, childMeta, count);
							count++;
						}

						// Adjust the section group information if necessary
						adjustParentGroupInfo(parentInfo);
					} else {
						// A non-set field was found so just add one group
						// information entry
						addGroupInfoWithChildren(request, groupInfo,
								primaryValue, deprecatedValue, childMeta, -1);
					}
				} else {
					// Add the child as a leaf node
					addChildLeaf(groupInfo, new PersonMergeRowInfo(childMeta),
							primaryObject, deprecatedObject);
				}
			}
		}
	}

	protected void addChildLeaf(PersonMergeRowInfo groupInfo,
			PersonMergeRowInfo childInfo, Object primaryObject,
			Object deprecatedObject) throws Exception {
		// Populate the bean information for the primary and deprecated objects
		childInfo.setPrimaryObject(getBeanInfo(primaryObject, childInfo, true));
		childInfo.setDeprecatedObject(getBeanInfo(deprecatedObject, childInfo,
				true));

		// Set the default field types
		setDefaultFieldTypes(childInfo);

		// Adjust the input types as needed
		if (childInfo.isSelectable()) {
			// If both children objects are the same, null out the input type so
			// it isn't
			// selectable by the user
			if (childInfo.getObjectsSame()) {
				if (childInfo.isSingleSelect()) {
					childInfo.setPrimaryInputType(null);
					childInfo.setDeprecatedInputType(null);
				}
			} else {
				// If the primary or deprecated object is empty, null it's input
				// type out.
				if (StringUtils.isEmpty(childInfo.getPrimaryObjectAsString())) {
					childInfo.setPrimaryInputType(null);
				}
				if (StringUtils
						.isEmpty(childInfo.getDeprecatedObjectAsString())) {
					childInfo.setDeprecatedInputType(null);
				}
			}
		}

		// Add the property to the group information as a child
		groupInfo.addChild(childInfo);
	}

	/**
	 * Defaults the input field types.
	 * 
	 * @param info
	 *            The person merge row info
	 */
	protected void setDefaultFieldTypes(PersonMergeRowInfo info) {
		if (info.isSelectable()) {
			info.setDeprecatedInputType(CHECKBOX);
			info.setPrimaryInputType(CHECKBOX);
		} else {
			info.setPrimaryInputType(null);
			info.setDeprecatedInputType(null);
		}
	}

	/**
	 * Gets a particular field from a bean using reflection. The resultant data
	 * is made ready for the UI by calling JspUtils if formatResult is true.
	 * 
	 * @param sourceObject
	 *            The object whose bean property will be read from.
	 * @param mergeRowInfoMetaData
	 *            The person merge row information
	 * @param formatResult
	 *            If true, JspUtils.displayValue(s) will be called on the
	 *            resultant data. Otherwise, the raw data will be returned.
	 * 
	 * @return The object value associated with the specified fieldProperty on
	 *         the sourceObject.
	 * @throws Exception
	 *             if any problems were encountered.
	 */
	protected Object getBeanInfo(Object sourceObject,
			PersonMergeRowInfoMetaData mergeRowInfoMetaData,
			boolean formatResult) throws Exception {
		String fieldProperty = mergeRowInfoMetaData.getFieldProperty();
		String fieldPropertyHelperMethod = mergeRowInfoMetaData
				.getFieldPropertyHelperMethod();
		boolean displayField = mergeRowInfoMetaData.getDisplayField();

		// If we shouldn't display the field, return the empty string
		if (!displayField) {
			return ("");
		}

		Object objectValue = null;
		Object fieldPropertyHelperParam = sourceObject;

		if (((StringUtils.isNotEmpty(fieldProperty)) || (StringUtils
				.isNotEmpty(fieldPropertyHelperMethod)))) {
			// If the fieldProperty was specified, get the objectValue based on
			// the property.
			if ((sourceObject != null)
					&& (StringUtils.isNotEmpty(fieldProperty))) {
				if (fieldProperty.equals("this")) {
					objectValue = sourceObject;
				} else {
					// Get the bean property
					PropertyUtilsBean propertyUtils = new PropertyUtilsBean();
					try {
						objectValue = propertyUtils.getNestedProperty(
								sourceObject, fieldProperty);
					} catch (NestedNullException ex) {
						// Ignore since this will leave the field null
					} catch (NullPointerException ex) {
						// Ignore since this will leave the field null
					} catch (NoSuchMethodException noEx) {
						throw new RuntimeException("Class name = "
								+ sourceObject.getClass().getName()
								+ ", fieldProperty = " + fieldProperty, noEx);
					}
				}

				// Set the field property helper parameter
				fieldPropertyHelperParam = objectValue;
			}

			// If a helper method was specified, get the object value using the
			// helper method.
			if (StringUtils.isNotEmpty(fieldPropertyHelperMethod)) {
				// Find the offset for the Class.Method name. The Class is
				// optional.
				int index = fieldPropertyHelperMethod.lastIndexOf(".");
				if (index == fieldPropertyHelperMethod.length() - 1) {
					throw new IllegalArgumentException(
							"The field property helper method '"
									+ fieldPropertyHelperMethod
									+ "' can't end with a '.'");
				}

				// Get the class name and method name portion of the property
				String className = index == -1 ? null
						: fieldPropertyHelperMethod.substring(0, index);
				String methodName = index == -1 ? fieldPropertyHelperMethod
						: fieldPropertyHelperMethod.substring(index + 1,
								fieldPropertyHelperMethod.length());

				// Either call the method name on "this" object or the class
				// specified
				Object targetObject = null;
				if (StringUtils.isEmpty(className)) {
					targetObject = this;
				} else {
					targetObject = Class.forName(className);
				}

				// Call the helper method
				objectValue = executeMethod(targetObject, methodName,
						fieldPropertyHelperParam, mergeRowInfoMetaData);
			}
		}

		if (formatResult) {
			if (objectValue instanceof Set) {
				return JspUtils.displayValues((Set) objectValue, "", null);
			} else {
				return JspUtils.displayValue(objectValue, "", null);
			}
		}

		// Return the raw data
		return objectValue;
	}

	public ConversionService getMergeDemographicsConversionService() {
		return mergeDemographicsConversionService;
	}

	public void setMergeDemographicsConversionService(
			ConversionService mergeDemographicsConversionService) {
		this.mergeDemographicsConversionService = mergeDemographicsConversionService;
	}

	public ConversionService getDemographicPersonalConversionService() {
		return demographicPersonalConversionService;
	}

	public void setDemographicPersonalConversionService(
			ConversionService demographicPersonalConversionService) {
		this.demographicPersonalConversionService = demographicPersonalConversionService;
	}

	/**
	 * Returns a custom sorted list of military service site records where the
	 * first record will be the HEC site record followed by all other records
	 * being sorted based on the entity key.
	 * 
	 * @param ms
	 *            The military service
	 * @param metaData
	 *            The person meta data
	 * @return The sorted list
	 */
	public List getMilitaryServiceSiteRecords(MilitaryService ms,
			PersonMergeRowInfoMetaData metaData) {
		// Create a list of military service site records to return
		List returnSiteRecords = new ArrayList();

		if (ms != null) {
			// Get a sorted list of the site records to start out. The initial
			// list must
			// be sorted to ensure a predictable order every time
			Set allMSSiteRecords = (ms != null) ? ms
					.getMilitaryServiceSiteRecords() : null;
			ArrayList siteRecordList = allMSSiteRecords == null ? new ArrayList()
					: new ArrayList((Collection) allMSSiteRecords);
			Collections.sort(siteRecordList, new BeanComparator(
					ENTITY_KEY_SORT_PROPERTY, new NullComparator()));

			// Add the HEC site record first so it will be first in the list on
			// the UI
			MilitaryServiceSiteRecord hecRecord = ms
					.getHECMilitaryServiceSiteRecord();
			if (hecRecord != null) {
				returnSiteRecords.add(hecRecord);
			}

			// Add all other site records next
			for (Iterator iter = siteRecordList.iterator(); iter.hasNext();) {
				MilitaryServiceSiteRecord siteRecord = (MilitaryServiceSiteRecord) iter
						.next();
				if ((siteRecord != null)
						&& (!(siteRecord.getSite().getCode()
								.equals(VAFacility.CODE_HEC.getName())))) {
					returnSiteRecords.add(siteRecord);
				}
			}
		}

		// Return our custom sorted list of site records
		return returnSiteRecords;
	}

	public String getShadIndicatorName(SHAD shad,
			PersonMergeRowInfoMetaData metaData) {
		String returnValue = "";
		if (shad != null) {
			Indicator indicator = shad.getShadIndicator();
			if (indicator != null) {
				returnValue = indicator.getName();
			}
		}
		return returnValue;
	}

	public String getSpecialFactorIndicatorName(SpecialFactor specialFactor,
			PersonMergeRowInfoMetaData metaData) {
		String returnValue = "";
		if (specialFactor != null) {
			Indicator indicator = specialFactor.getSpecialFactorIndicator();
			if (indicator != null) {
				returnValue = indicator.getName();
			}
		}
		return returnValue;
	}

	public IncomeTest getCurrentIncomeTest(Person person,
			PersonMergeRowInfoMetaData metaData) {
		return this.getPersonHelperService().getCurrentIncomeTest(person);
	}

	public FinancialStatement getCurrentgetFinancialStatement(Person person,
			PersonMergeRowInfoMetaData metaData) {
		IncomeTest incomeTest = this.getCurrentIncomeTest(person, metaData);
		return (incomeTest != null) ? person.getFinancialStatement(incomeTest
				.getIncomeYear()) : null;
	}

	public Set getBeneficiaryTravels(Person person,
			PersonMergeRowInfoMetaData metaData) {
		IncomeTest incomeTest = this.getCurrentIncomeTest(person, metaData);
		return (incomeTest != null) ? person.getBeneficiaryTravels(incomeTest
				.getIncomeYear()) : new HashSet();
	}

	public SpouseFinancials getActiveSpouseFinancials(Person person,
			PersonMergeRowInfoMetaData metaData) {
		FinancialStatement stmt = getCurrentgetFinancialStatement(person,
				metaData);
		return (stmt != null) ? stmt.getActiveSpouseFinancials() : null;
	}

	public Set getSpouseFinancials(Person person,
			PersonMergeRowInfoMetaData metaData) {
		FinancialStatement stmt = getCurrentgetFinancialStatement(person,
				metaData);
		return (stmt != null) ? stmt.getSpouseFinancials() : null;
	}

	public Set getDependentFinancials(Person person,
			PersonMergeRowInfoMetaData metaData) {
		FinancialStatement stmt = getCurrentgetFinancialStatement(person,
				metaData);
		return (stmt != null) ? stmt.getDependentFinancials() : new HashSet();
	}

	/**
	 * Returns a custom sorted list of medicare insurnace records. The list is
	 * ordered with HEC records followed by other non-HEC site records. Also,
	 * Part A records are placed before Part B records.
	 * 
	 * @param person
	 *            The person
	 * @param metaData
	 *            The person meta data
	 * @return The sorted list
	 */
	public List getMedicareInsurances(Person person,
			PersonMergeRowInfoMetaData metaData) {
		Map partAMap = new HashMap();
		Map partBMap = new HashMap();
		List stationSortList = new ArrayList();
		List returnList = new ArrayList();

		// Get the medicare insurance set
		Set medicareInsuranceSet = person.getMedicareInsurances();

		// Place the medicare objects in individual part A and part B maps keyed
		// by station number.
		// Also, keep track of the list of station numbers for ordering (except
		// HEC).
		for (Iterator iterator = medicareInsuranceSet.iterator(); iterator
				.hasNext();) {
			Medicare medicare = (Medicare) iterator.next();
			String stationNumber = medicare.getReportSite().getStationNumber();
			if ((!stationSortList.contains(stationNumber))
					&& (!VAFacility.CODE_HEC.getName().equals(stationNumber))) {
				stationSortList.add(stationNumber);
			}

			// Note that when Part B is set, Part A may also be set since you
			// can't have a Part B
			// without a Part A. Just remember that when both are set, that is
			// the Part B record.
			if ((medicare.isEnrolledInPartB() != null)
					&& (Boolean.TRUE.equals(medicare.isEnrolledInPartB()))) {
				partBMap.put(stationNumber, medicare);
			} else {
				if ((medicare.isEnrolledInPartA() != null)
						&& (Boolean.TRUE.equals(medicare.isEnrolledInPartA()))) {
					partAMap.put(stationNumber, medicare);
				} else {
					throw new IllegalArgumentException(
							"Medicare insurance with Id "
									+ medicare.getEntityKey()
											.getKeyValueAsString()
									+ " doesn't contain " + "Part A or Part B.");
				}
			}
		}

		// Sort the station list collection
		Collections.sort(stationSortList);

		// Add the HEC station number first
		if (partAMap.get(VAFacility.CODE_HEC.getName()) != null) {
			returnList.add(partAMap.get(VAFacility.CODE_HEC.getName()));
		}
		if (partBMap.get(VAFacility.CODE_HEC.getName()) != null) {
			returnList.add(partBMap.get(VAFacility.CODE_HEC.getName()));
		}

		// Add the other non-HEC station numbers
		for (Iterator iterator = stationSortList.iterator(); iterator.hasNext();) {
			String stationNumber = (String) iterator.next();

			if (partAMap.get(stationNumber) != null) {
				returnList.add(partAMap.get(stationNumber));
			}
			if (partBMap.get(stationNumber) != null) {
				returnList.add(partBMap.get(stationNumber));
			}
		}

		// Return the ordered insurance list
		return returnList;
	}

	/**
	 * Returns a custom sorted list of private insurnace records. The list is
	 * ordered with HEC records followed by other non-HEC site records.
	 * 
	 * @param person
	 *            The person
	 * @param metaData
	 *            The person meta data
	 * @return The sorted list
	 */
	public List getPrivateInsurances(Person person,
			PersonMergeRowInfoMetaData metaData) {
		Map insuranceMap = new HashMap();
		List stationSortList = new ArrayList();
		List returnList = new ArrayList();

		// Get the private insurance set
		Set privateInsuranceSet = person.getPrivateInsurances();

		// Place the preicate insurance objects in a map keyed by station
		// number.
		// Also, keep track of the list of station numbers for ordering (except
		// HEC).
		for (Iterator iterator = privateInsuranceSet.iterator(); iterator
				.hasNext();) {
			PrivateInsurance privateInsurance = (PrivateInsurance) iterator
					.next();
			// ESR 4.0_CodeCR13473 - Handle Null Pointer Exception with Veteran Merge
			if (privateInsurance != null 
					&& privateInsurance.getReportSite() != null 
					&& privateInsurance.getReportSite().getStationNumber() != null) {
				String stationNumber = privateInsurance.getReportSite()
						.getStationNumber();
				if ((!stationSortList.contains(stationNumber))
						&& (!VAFacility.CODE_HEC.getName().equals(stationNumber))) {
					stationSortList.add(stationNumber);
				}
				insuranceMap.put(stationNumber, privateInsurance);
			}
		}

		// Sort the station list collection
		Collections.sort(stationSortList);

		// Add the HEC station number first
		if (insuranceMap.get(VAFacility.CODE_HEC.getName()) != null) {
			returnList.add(insuranceMap.get(VAFacility.CODE_HEC.getName()));
		}

		// Add the other non-HEC station numbers
		for (Iterator iterator = stationSortList.iterator(); iterator.hasNext();) {
			String stationNumber = (String) iterator.next();

			if (insuranceMap.get(stationNumber) != null) {
				returnList.add(insuranceMap.get(stationNumber));
			}
		}

		// Return the ordered insurance list
		return returnList;
	}

	/**
	 * Initialize and get the Struts lookup dispatch method map hashmap.
	 * 
	 * @return the method map
	 * @see org.apache.struts.actions.LookupDispatchAction#getKeyMethodMap()
	 */
	protected Map getKeyMethodMap() {
		Map map = new HashMap();
		map.put("button.display", "display");
		map.put("button.refresh", "display");
		map.put("button.selectPerson", "selectPerson");
		map.put("button.search", "search");
		map.put("button.research", "research");
		map.put("button.clear", "clear");
		map.put("button.cancel", "clear");
        map.put("button.mergeSearch", "mergeSearch");
		return map;
	}
}
