/********************************************************************
 * Copyright © 2004 VHA. All rights reserved
 ********************************************************************/
package gov.va.med.esr.common.persistent.history;

// Java Classes
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.util.ArrayList;
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 java.util.TreeSet;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.beanutils.PropertyUtilsBean;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.Validate;
import org.hibernate.Criteria;
import org.hibernate.Hibernate;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.criterion.Expression;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.dao.DataAccessException;
import org.springframework.orm.hibernate3.HibernateCallback;

import gov.va.med.esr.service.impl.ChangeEvent;
import gov.va.med.esr.service.impl.HistoricalInfo;
import gov.va.med.fw.model.AbstractVersionedEntity;
import gov.va.med.fw.model.EntityKey;
import gov.va.med.fw.model.lookup.TransactionType;
import gov.va.med.fw.persistent.DAOException;
import gov.va.med.fw.persistent.hibernate.AbstractDAOAction;
import gov.va.med.fw.persistent.hibernate.GenericDAOImpl;

// Common Classes

/**
 * @author DNS   CHENJ2
 *
 */
public class HistoryDAOImpl extends GenericDAOImpl implements
		HistoryDAO, BeanNameAware {

	private static final long serialVersionUID = 5904704406446514977L;

	// ===== properties used for getHistoryChangeTimes =====
	private static final String getChangeTimesSelectPart1 = "SELECT distinct obj";
	private static final String getChangeTimesSelectPart2 = ".modifiedOn ";

	// defined in historyDaos.xml config
	private String[][] mappedClasses = null;
	private String[] historyTimesQueries = null;

	// ===== properties used for getHistoryByChangeTime =====
	private static final String getHistoryByChangeTimeHQLPart1 = "select obj from ";
	private static final String getHistoryByChangeTimeHQLPart2 =
		" as obj where obj.id = :id order by obj.modifiedOn desc, obj.historyId desc";

	// fileters being used
	public static final String FILTER_AS_OF_DATE = "asOfDate";
	public static final String FILTER_PRIOR_TO_DATE = "priorToDate";
	public static final String FILTER_PAR_EFFECTIVE_DATE = "effectiveDate";
    public static final String FIELD_TRANSACTION_TYPE = "transactionType";

	// defined in historyDaos.xml config
	private List lazyProperties = null;
	private String rootClassName = null;
	private List historyChangeColumns = null;

	// we are retrieving a tree (e.g person) with a single root; vs. a set of objects (set of egts)
	// defaults to true.
	private boolean singleRoot = true;

	protected static final int ACCESS_ONLY = 0;
	protected static final int ACCESS_AND_DELETE = 1;

	/**
	 * A default constructor
	 */
	public HistoryDAOImpl() {
		super();
	}

	/*
	 * Retrieves the list of history change timestamps for a group of
	 * data, in reverse order.
	 *
	 * @param entityKey Key to the root object, e.g. person
	 * @see gov.va.med.esr.common.persistent.history.HistoryDAO#getHistoryChangeTimes(gov.va.med.fw.model.EntityKey)
	 */
	public Set getHistoryChangeTimes(EntityKey entityKey) throws DAOException {

		if (hasSingleRoot()) {
			Validate.notNull(entityKey, "NULL EntityKey not allowed.");
		}

		Set changeEvents = new TreeSet();

		// run generated queries using config file entries
		if (this.historyChangeColumns != null) {
			Iterator iter = this.historyChangeColumns.iterator();
			while (iter.hasNext()) {
				HistoryChangeColumns historyChangeColumns = (HistoryChangeColumns)iter.next();
				List newChangeEvents = getHistoryChangeTimesNew(entityKey, historyChangeColumns);
				changeEvents.addAll(newChangeEvents);
			}
		}
		else {
			if (mappedClasses != null) {
				for (int i=0; i<mappedClasses.length;i++) {
					changeEvents.addAll(getHistoryChangeTimes(entityKey, mappedClasses[i]));
				}
			}

			// run any customized queries
			if (historyTimesQueries != null) {
				for (int i = 0; i < historyTimesQueries.length; i++) {
					changeEvents.addAll(getHistoryChangeTimes(entityKey,
							historyTimesQueries[i]));
				}
			}
		}

		if (logger.isDebugEnabled())
            logger.debug("HistoryDaoImpl getHistoryChangeTimes: " + changeEvents);

		return changeEvents;
	}

    /**
     * Fetch deleted entities
     */
    public List getDeletedEntities(Class clazz) throws DAOException {
		Map contextData = new HashMap();
		contextData.put("clazz", clazz);
		HibernateCallback callback = new AbstractDAOAction(contextData) {
			public Object execute(Session session) {
		        Criteria criteria = session.createCriteria((Class) getContextData().get("clazz"));
		        criteria.add(Expression.eq(FIELD_TRANSACTION_TYPE,
		                TransactionType.CODE_DELETE.getName()));
		        return criteria.list();
			}
		};
		return this.getHibernateTemplate().executeFind(callback);
    }

	/**
	 *
	 * @param entityKey
	 * @param mappedClass
	 * @return
	 * @throws DAOException
	 */
	private List getHistoryChangeTimes(EntityKey entityKey, String[] objectRelationPath) throws DAOException {
		ArrayList changeEvents = new ArrayList();
		List results = null;

		try {
			if (this.hasSingleRoot()) {
				// has root parent id
				results = super.getHibernateTemplate().findByNamedParam(
						buildHistoryChangeTimesHQL(objectRelationPath),
						"parent_id", (BigDecimal) entityKey.getKeyValue());
			}
			else {
				// doesn't have root parent id
				results = super.getHibernateTemplate().find(
						buildHistoryChangeTimesHQL(objectRelationPath));
			}

			// Create Change Event Object from the results
			if (results != null) {
				for (Iterator i = results.iterator(); i.hasNext();) {
					changeEvents.add(new ChangeEvent((Timestamp) i.next(),
							entityKey));
				}
			}
		}

		catch (DataAccessException e) {
			throw new DAOException(
					"Failed to retrieve the history change dates for entry:" + objectRelationPath, e);
		}

		if (logger.isDebugEnabled())
            logger.debug("HistoryDaoImpl getHistoryChangeTimes: " + changeEvents);

		return changeEvents;
	}


	/**
	 * Execute any customized queries for history change timestamps
	 * @param entityKey
	 * @param namedQuery
	 * @return
	 * @throws DAOException
	 */
	private List getHistoryChangeTimes(EntityKey entityKey, String namedQuery) throws DAOException {
		ArrayList changeEvents = new ArrayList();
		List results = null;

		Validate.notNull(namedQuery);

		try {
			results = super.getHibernateTemplate().findByNamedQueryAndNamedParam(
					namedQuery,
					"parent_id",
					(BigDecimal) entityKey.getKeyValue());

			// Create Change Event Object from the results
			if (results != null) {
				for (Iterator i = results.iterator(); i.hasNext();) {
					changeEvents.add(new ChangeEvent((Timestamp) i.next(),
							entityKey));
				}
			}
		}
		catch (Exception e) {
			throw new DAOException(
					"Failed to retrieve the history change dates for named query:" + namedQuery, e);
		}

		if (logger.isDebugEnabled())
            logger.debug("HistoryDaoImpl getHistoryChangeTimes: " + changeEvents);

		return changeEvents;
	}


	/**
	 * Utility method to build query for get historical timestamps for one
	 * particular table.
	 * Example:
	 * For an entry of:
   		<value>FinancialStatement,person,internalDependentFinancials,reportedOn</value>
   	 *
	 * The resulting query should look like:
	 *	  select distinct obj3.modifiedOn
	 	        from FinancialStatement as obj1
	 	        	 join obj1.internalDependentFinancials as obj2
	 	             join obj2.reportedOn as obj3
	 	        where obj1.person.id = :parent_id
	 *
	 * @param objectRelationPath names of the object relations path down to the
	 *        object needing history timestamps
	 * @return the query string
	 */
	private String buildHistoryChangeTimesHQL(String[] objectRelationPath) {

		Validate.isTrue(objectRelationPath.length>=2);

		int depth = objectRelationPath.length - 1;

		String selectString = buildGetChangeTimesSelect(depth);

		String topClassName = objectRelationPath[0];
		String rootName = objectRelationPath[1];
		Validate.notNull(topClassName);

		String fromString = buildGetChangeTimesFrom(topClassName);

		String joinString = buildGetChangeTimesJoin(objectRelationPath);
		String whereClause = buildGetChangeTimesWhereClause(rootName);

		String queryString = selectString + fromString + joinString + whereClause;

		//System.out.println("Built hql string: " + queryString);
		if (logger.isDebugEnabled()) {
			logger.debug(queryString);
		}

		return queryString;
	}

	/**
	 * Build select string, example: "select distinct obj3.modifiedOn "
	 * @param depth
	 * @return
	 */
	private String buildGetChangeTimesSelect(int depth)
	{
		return (getChangeTimesSelectPart1 +
				depth +
				getChangeTimesSelectPart2);
	}

	/**
	 * Build from string, example: "from FinancialStatement as obj1"
	 * @param topClassName
	 * @return
	 */
	private String buildGetChangeTimesFrom(String topClassName)
	{
		return "FROM " + topClassName + " as obj1 ";
	}

	/**
	 * Build the join statements for all sub levels. example:
	 	        	" join obj1.internalDependentFinancials as obj2
	 	              join obj2.reportedOn as obj3"
	 * @param objectRelationPath
	 * @return
	 */
	private String buildGetChangeTimesJoin(String[] objectRelationPath) {
		String joinString = "";

		int length=objectRelationPath.length;
		if (length > 2) {
			for (int i=2; i<length; i++) {
				joinString += (" JOIN obj" + (i-1) + "." + objectRelationPath[i] + " as obj" +i);
			}
		}
		return joinString;
	}

	/**
	 * Utility method to build the where clause of the history timestamp query
	 * Example: "where obj1.person.id = :parent_id"
	 * @param parentName
	 * @return
	 */
	private String buildGetChangeTimesWhereClause(String rootName)
	{
		String	whereClause = "";

		// build a where clause only when we are retrieving a tree with a single root, such as person hiearachy
		// but if we are getting a set of all the egts, then there's no single root, therefore no parent id to use.
		if (this.hasSingleRoot()) {
				whereClause = " WHERE obj1."
				+ (StringUtils.isEmpty(rootName) ? "" : (rootName + "."))
				+ "id = :parent_id ";
		}

		return whereClause;
	}

	/* Retrieves the current and previous version of the entity
	 *
	 * @param changeEvent provides the key to the root object, and timestamp for history retrieval
	 * @see gov.va.med.esr.common.persistent.history.HistoryDAO#getHistoryByChangeTime(gov.va.med.esr.service.impl.ChangeEvent)
	 */
	public HistoricalInfo getHistoryByChangeTime(ChangeEvent changeEvent) throws DAOException
	{
		Validate.notNull(changeEvent, "NULL changeEvent not allowed.");

		if (hasSingleRoot()) {
			Validate.notNull(changeEvent.getEntityKey(), "NULL EntityKey not allowed.");
		}
		Validate.notNull(changeEvent.getTimeStamp(), "NULL Timestamp not allowed.");

		Map contextData = new HashMap();
		contextData.put("changeEvent", changeEvent);
		HibernateCallback callback = new AbstractDAOAction(contextData) {
			public Object execute(Session session) throws DAOException {
				ChangeEvent targetChangeEvent = (ChangeEvent) getContextData().get("changeEvent");
				AbstractVersionedEntity currentVersion = getCurrentVersion(session, targetChangeEvent);
				AbstractVersionedEntity previousVersion = null;

                if (targetChangeEvent.getPrevousChangeEvent() == null){
                    previousVersion = getPreviousVersion(session, targetChangeEvent);
                }
                else {
                    previousVersion = getCurrentVersion(session, targetChangeEvent.getPrevousChangeEvent());
                }
				if (previousVersion != null) {
					currentVersion.setPreviousVersion(previousVersion);
				}

				return new HistoricalInfo(targetChangeEvent,currentVersion);
			}
		};
		return (HistoricalInfo) this.getHibernateTemplate().execute(callback);
	}

	/**
	 * Access data to trigger lazy access for each data object
	 * needed for this history group
	 *
	 * @param entity
	 * @throws DAOException
	 */
	protected void accessData(AbstractVersionedEntity entity) throws DAOException
	{
		processData(entity, ACCESS_ONLY);

		/*
		 * NOTE: no longer use this simpler logic to access the public properties
		 * of the object -- there is almost always an internalSet hiding behind it.
		 * So if we are trying to access a single field (enrollmentDetermination), the
		 * internalSet may hold more than one items, with some of them being marked deleted.
		 * Then when we get the single item back, there is no gurantee we are getting the
		 * acutal one we want from the set.  If we do lazy access on the deleted object's children only,
		 * we'd have missed accessing the children of the actual object we want.
		 *
		if (getLazyProperties() == null)
			return;

		String propName = null;
		Object obj = null;
		PropertyUtilsBean propUtil = new PropertyUtilsBean();

		Set set = null;
		Map map = null;

		for (Iterator iter = getLazyProperties().iterator(); iter.hasNext();) {
			try {
				propName = (String) iter.next();

				obj = propUtil.getNestedProperty(entity, propName);

				// now perform lazy access
				if (obj instanceof Set) {
					set = (Set) obj;
					accessSet(set);
				} else if (obj instanceof Map){
					map = (Map) obj;
					accessMap(map);
				}

			} catch (NestedNullException ne) {
				// if nested property and parent value is null, then skip

			} catch (Exception e) {
				throw new DAOException("Unable to access property:" + propName,
					e);
			}
		}*/
	}

	/**
	 * Retrieves the person with asOfDate filter
	 */
	protected AbstractVersionedEntity getCurrentVersion(Session session, ChangeEvent changeEvent) throws DAOException
	{
		session.enableFilter(FILTER_AS_OF_DATE)
				.setParameter(FILTER_PAR_EFFECTIVE_DATE,changeEvent.getTimeStamp());
		session.disableFilter(FILTER_PRIOR_TO_DATE);

		AbstractVersionedEntity currentVersion = getHistoricalEntity(session, changeEvent);
		if (currentVersion != null) {
			accessData(currentVersion);
			evict(currentVersion);
			removeDeletedRecords(currentVersion);
		}
		else {
			// if currentVersion not found, create an empty holder
			try {
				currentVersion = (AbstractVersionedEntity) Class.forName(getRootClassName()).newInstance();
			} catch (Exception e) {
				throw new DAOException("Error instantating class:" + getRootClassName(), e);
			}
		}

		return currentVersion;
	}

	/**
	 * Retrieves the person with priorToDate filter
	 */
	private AbstractVersionedEntity getPreviousVersion(Session session, ChangeEvent changeEvent) throws DAOException
	{
		if (!needPreviousVersion())
			return null;
		session.disableFilter(FILTER_AS_OF_DATE);
		session.enableFilter(FILTER_PRIOR_TO_DATE)
			.setParameter(FILTER_PAR_EFFECTIVE_DATE,changeEvent.getTimeStamp());

		AbstractVersionedEntity previousVersion = getHistoricalEntity(session, changeEvent);
		if (previousVersion != null) {
			accessData(previousVersion);
			evict(previousVersion);
			removeDeletedRecords(previousVersion);
		}
		return previousVersion;
	}

	/**
	 * Retrieves historical data, executes a query so that the filters
	 * will be applied.
	 *
	 * @param changeEvent
	 * @return
	 */
	protected AbstractVersionedEntity getHistoricalEntity(Session session, ChangeEvent changeEvent) throws DAOException
	{
		AbstractVersionedEntity entity = null;

		/*List results = super.getHibernateTemplate().findByNamedParam(
				(getHistoryByChangeTimeHQLPart1 + getRootClassName() +
				getHistoryByChangeTimeHQLPart2),
				"id", (BigDecimal) (changeEvent.getEntityKey().getKeyValue()));
	*/
		String queryString = getHistoryByChangeTimeHQLPart1 + getRootClassName() + getHistoryByChangeTimeHQLPart2;
		Query q = session.createQuery(queryString);
		q.setMaxResults(1);
		q.setParameter("id", (BigDecimal) (changeEvent.getEntityKey().getKeyValue()));
		List results = q.list();

		if (results == null || results.isEmpty())
			return null;

		Iterator iter = results.iterator();
		if (iter.hasNext()) {
			entity = (AbstractVersionedEntity)iter.next();
		}

		// If root entity is deleted then evict it and return null directly
		if (entity != null && entity.isDeleted().booleanValue()) {
			evict(entity);
			entity = null;
		}

		if (entity != null && logger.isDebugEnabled())
            logger.debug("HistoryDaoImpl getHistoricalEntity: " + entity);

		return entity;
	}

	/*
	 * Removes all records marked "D" from the object graph.  Note this was
	 * done at the db retrieval level, since we need to do a max/min operation by date
	 * first.
	 * This is done after the object has been evicted from the session to prevent
	 * the deletes from triggering database operations
	 *
	 * @param entity
	 * @throws DAOException
	 */
	protected void removeDeletedRecords(AbstractVersionedEntity entity) throws DAOException
	{
		processData(entity, ACCESS_AND_DELETE);
	}

	/*
	 * Removes all records marked "D" from the object graph.  Note this was
	 * done at the db retrieval level, since we need to do a max/min operation by date
	 * first.
	 * This is done after the object has been evicted from the session to prevent
	 * the deletes from triggering database operations
	 *
	 * @param entity
	 * @throws DAOException
	 */
	protected void processData(AbstractVersionedEntity entity, int processTask) throws DAOException
	{
		if (getLazyProperties() == null)
			return;

		String propName = null;

		for (Iterator iter = getLazyProperties().iterator(); iter.hasNext();) {
			try {
				propName = (String) iter.next();

				processNestedProperty(entity, propName, processTask);

			} catch (Exception e) {
				throw new DAOException("Unable to " + (processTask==ACCESS_ONLY?"access ":"process deleted records from ")
						+ "property:" + propName, e);
			}
		}
	}

	/**
	 * Process deleted record for a nested property.  At each level, retrieve data,
	 * then recursively process the children.  Actual removal of deleted records is
	 * only done at the leaf level.
	 *
	 * At any level, if the object retrieved back is set or map, then loop through
	 * each element of the collection, and process its children
	 *
	 * @param bean
	 * @param name
	 * @throws DAOException
	 */
	protected void processNestedProperty(Object bean, String name, int processTask)
			throws DAOException {

		//System.out.println("processing delete for nested property: " + name);
		if (bean == null) {
			throw new IllegalArgumentException("No bean specified");
		}
		if (name == null) {
			throw new IllegalArgumentException("No name specified");
		}

		// if parent is an deleted object, do not process it at all, to prevent
		// unnecessary DB retrievals
		if (bean instanceof AbstractVersionedEntity) {
			if (((AbstractVersionedEntity)bean).isDeleted().booleanValue()) {
				return;
			}
		}

		int indexOfNESTED_DELIM = name.indexOf(PropertyUtils.NESTED_DELIM);

		if (indexOfNESTED_DELIM < 0) {
			// at the leaf level now
			processSimpleProperty(bean, name, processTask);
		}
		else {
			// upper level, get the parent object (or collection) at this level,
			// then recurse down
			String next = name.substring(0, indexOfNESTED_DELIM);
			bean = getSimpleDeclaredProperty(bean, next);

			if (bean == null) {
				return;
			}

			name = name.substring(indexOfNESTED_DELIM + 1);

			if (bean instanceof Set) {
				Set set = (Set) bean;
				for (Iterator i = set.iterator(); i.hasNext();) {
					processNestedProperty(i.next(), name, processTask);
				}
			} else if (bean instanceof Map) {
				Map map = (Map) bean;
				Set keys = map.keySet();

				for (Iterator i = keys.iterator(); i.hasNext();) {
					processNestedProperty(map.get(i.next()), name, processTask);
				}

			} else {
				processNestedProperty(bean, name, processTask);
			}
		}
	}

    private List getHistoryChangeTimesNew(final EntityKey entityKey, final HistoryChangeColumns historyChangeColumns) throws DAOException {
		ArrayList changeEvents = new ArrayList();
		List results = null;
		try {
			HibernateCallback callback = new AbstractDAOAction() {
				public Object execute(Session session) {
					if (historyChangeColumns.isIncludeColumnsSet() == false) {
						HashSet histColumns = getHistoryColumns(session, historyChangeColumns.getTable());
						histColumns.removeAll(historyChangeColumns.getExcludeColumnList());
						histColumns.remove(historyChangeColumns.getPrimaryKey());
						histColumns.remove(historyChangeColumns.getPartitionKey());
						historyChangeColumns.setIncludeColumnList(histColumns);
					}
					String sqlQuery = historyChangeColumns.getHistoryChangeTimesSqlQuery();
					//System.out.println("sqlQuery=" + sqlQuery);

					Query hibSqlQuery = session.createSQLQuery(sqlQuery).addScalar("RECORD_MODIFIED_DATE", Hibernate.TIMESTAMP).setString("id", entityKey.getKeyValueAsString());

				    return hibSqlQuery.list();
				}
			};
			results = this.getHibernateTemplate().executeFind(callback);

			// Create Change Event Object from the results
			if (results != null) {
				for (Iterator i = results.iterator(); i.hasNext();) {
					changeEvents.add(new ChangeEvent((Timestamp) i.next(),
							entityKey));
				}
			}

		}
		catch (DataAccessException e) {
			throw new DAOException(
					"Failed to retrieve the history change dates for entry:" + historyChangeColumns.getTable(), e);
		}

		return changeEvents;
    }

    private HashSet getHistoryColumns(Session session, String table) {

		HashSet historyColumns = new HashSet();

		Connection conn = null;
		Statement stmt = null;
		ResultSet rst = null;

		try {
			String sqlQuery = "select * from " + table + " where rownum < 2";

			 conn = session.connection();
			 stmt = conn.createStatement();
			 rst = stmt.executeQuery(sqlQuery);
			ResultSetMetaData rstMetadata = rst.getMetaData();

			int colCount = rstMetadata.getColumnCount();
			for (int i=1; i <= colCount; i++) {
				String colName = rstMetadata.getColumnName(i);
				colName = colName.toUpperCase();
				historyColumns.add(colName);
			}
		}
		catch (SQLException e) {
			throw new RuntimeException (
					"Failed to retrieve the history change columns for table:" + table, e);
		}
		finally {
			try {if (rst != null) rst.close();} catch (Exception e) {}
			try {if (stmt != null) stmt.close();} catch (Exception e) {}
			try {if (conn != null) conn.close();} catch (Exception e) {}

		}

		return historyColumns;
    }

	/**
	 *
	 * @param obj
	 * @param name
	 * @param processTask
	 * @throws DAOException
	 */
	private void processSimpleProperty(Object obj, String name, int processTask) throws DAOException
	{
		PropertyUtilsBean propUtil = new PropertyUtilsBean();

		Object accessedProperty = getSimpleDeclaredProperty(obj, name);

		// access/delete data on sets and maps
		if (accessedProperty instanceof Set) {
			processSimpleSet((Set)accessedProperty, processTask);
		} else if (accessedProperty instanceof Map) {
			processSimpleMap((Map)accessedProperty, processTask);
		}
		// for individual property, remove deleted property from parent
		else if (processTask == ACCESS_AND_DELETE
				&& accessedProperty instanceof AbstractVersionedEntity) {
			// if we are only trying to do lazy access, we are done, just
			// return.

			AbstractVersionedEntity accessedEntity = (AbstractVersionedEntity) accessedProperty;
			if (accessedEntity.isDeleted().booleanValue()) {
				try {
					propUtil.setProperty(obj, name, null);
				} catch (Exception e) {
					throw new DAOException(
							"Could not set property to null for: name=" + name
									+ ", obj=" + obj, e);
				}
			}
		}
	}

	/**
	 * Utility method to remove any deleted item from the set
	 *
	 * @param collection
	 * @throws DAOException
	 */
	private void processSimpleSet(Set collection, int processTask) throws DAOException{
		if (collection == null) return;
		AbstractVersionedEntity entity = null;

		Iterator i = null;

		for (i=collection.iterator(); i.hasNext();)
		{
			entity = (AbstractVersionedEntity) i.next();
			/*System.out.println("retrieved: " + entity.getClass().getName() +
					"id = " + entity.getEntityKey().getKeyValueAsString() +
					"historyId = " + entity.getHistoryId());*/

			if (processTask == ACCESS_AND_DELETE &&
					entity.isDeleted().booleanValue()) {
			   i.remove();
			}
		}
	}

	/**
	 * Utility method to remove any deleted item from the map
	 *
	 * @param map
	 * @throws DAOException
	 */
	private void processSimpleMap(Map map, int processTask) throws DAOException {
		if (map == null)
			return;

		Set keys = map.keySet();
		AbstractVersionedEntity entity = null;

		for (Iterator i = keys.iterator(); i.hasNext();) {
			Object obj = map.get(i.next());
			if (obj instanceof Set) {
				processSimpleSet((Set) obj, processTask);
			} else if (obj instanceof AbstractVersionedEntity) {
				entity = (AbstractVersionedEntity) obj;

				if (processTask == ACCESS_AND_DELETE &&
						entity.isDeleted().booleanValue()) {
					i.remove();
				}
			}
		}
	}

	/**
	 *
	 * @param bean
	 * @param name
	 * @return
	 * @throws IllegalAccessException
	 * @throws InvocationTargetException
	 * @throws NoSuchFieldException
	 */
	private Object getSimpleDeclaredProperty(Object bean, String name)
			throws DAOException {

		Object value = null;

		try {

			Field field = null;
			Class c = bean.getClass();
			field = getFieldFromClass(c, name);

			// if not found in this class, recurse up the parents
			while (field == null && c != null) {
				c = c.getSuperclass();
				field = getFieldFromClass(c, name);
			}
			if (field == null)
				throw new NoSuchFieldException();

			field.setAccessible(true);
			value = field.get(bean);

			// the following tried to use JavaBean access, but that only gives public methods
			/*PropertyDescriptor descriptor =
	            new PropertyDescriptor(name, bean.getClass(), "getInternalDecorations", "set"+name);
	        Method readMethod = descriptor.getReadMethod();

	        readMethod.setAccessible(true);
	        // Call the property getter and return the value
	        value = readMethod.invoke(bean, new Object[0]);
	        */
			//System.out.println("Got simple property: " + value);
		} catch (Exception e) {
			throw new DAOException("Failed to get field:" + name + " from object:" + bean, e);
		}

		return value;
	}


	/**
	 * This method will eat the exception.
	 * @param c
	 * @param name
	 * @return
	 */private Field getFieldFromClass(Class c, String name)
	{
		Field f = null;
		try {
			f = c.getDeclaredField(name);
		} catch (NoSuchFieldException e) {
		}

		return f;
	}

	public String[][] getMappedClasses() {
		return mappedClasses;
	}

	public void setMappedClasses(String[][] mappedClasses) {
		this.mappedClasses = mappedClasses;
	}

	public List getLazyProperties() {
		return lazyProperties;
	}

	public void setLazyProperties(List lazyAssociataions) {
		this.lazyProperties = lazyAssociataions;
	}

	public String getRootClassName() {
		return rootClassName;
	}

	public void setRootClassName(String rootClassName) {
		this.rootClassName = rootClassName;
	}

	public String[] getHistoryTimesQueries() {
		return historyTimesQueries;
	}

	public void setHistoryTimesQueries(String[] namedQueries) {
		this.historyTimesQueries = namedQueries;
	}

	protected void setSingleRoot(boolean singleRoot) {
		this.singleRoot = singleRoot;
	}

	protected boolean hasSingleRoot() {
		return this.singleRoot;
	}

	/**
	 * Subclass can override, so we don't retrieve the previous version
	 * @return
	 */
	protected boolean needPreviousVersion() {
		return true;
	}

	public List getHistoryChangeColumns() {
		return historyChangeColumns;
	}

	public void setHistoryChangeColumns(List dataChangeProperties) {
		this.historyChangeColumns = dataChangeProperties;
	}
}