/*******************************************************************************
 * This file is part of GECAMed.
 * 
 * GECAMed is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License (L-GPL) as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * GECAMed is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License (L-GPL)
 * along with GECAMed.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * GECAMed is Copyrighted by the Centre de Recherche Public Henri Tudor (http://www.tudor.lu)
 * (c) CRP Henri Tudor, Luxembourg, 2008
 *******************************************************************************/
package lu.tudor.santec.gecamed.core.utils.entitymapper;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.Enumeration;
import java.util.GregorianCalendar;
import java.util.Hashtable;
import java.util.LinkedHashSet;
import java.util.Properties;
import java.util.regex.Matcher;

import lu.tudor.santec.gecamed.core.ejb.entity.beans.GECAMedEntityBean;
import lu.tudor.santec.gecamed.core.utils.querybuilder.Condition;
import lu.tudor.santec.gecamed.core.utils.querybuilder.HibernateCondition;
import lu.tudor.santec.gecamed.core.utils.querybuilder.HibernateOperator;
import lu.tudor.santec.gecamed.core.utils.querybuilder.WhereClause;
import lu.tudor.santec.gecamed.importexport.utils.XPathAPI;

import org.apache.log4j.Level;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class XML2EntityMapper extends EntityMapper {

//---------------------------------------------------------------------------
//***************************************************************************
//* Constants	                                                            *
//***************************************************************************
//---------------------------------------------------------------------------

//---------------------------------------------------------------------------
//***************************************************************************
//* Constructor	                                                            *
//***************************************************************************
//---------------------------------------------------------------------------
	
	public XML2EntityMapper(Document p_XMLDocument) {
		this(p_XMLDocument, true);
	}
	
	public XML2EntityMapper(Document p_XMLDocument, boolean checkXPath) {
		super(p_XMLDocument, checkXPath);
	}

//---------------------------------------------------------------------------
//***************************************************************************
//* General Primitives                                                      *
//***************************************************************************
//---------------------------------------------------------------------------
	/**
	 * Creates a new instance of the specified GECAMedEntityBean class.
	 * 
	 * @param p_ClassName
	 *            specifies the class name of the GECAMedEntityBean to
	 *            instantiate.
	 * @return A new instance of the specified class or <code>null</code> if
	 *         instantiation failed.
	 */
//---------------------------------------------------------------------------

	private GECAMedEntityBean instantiate(String p_ClassName) {
		GECAMedEntityBean l_Bean = null;

		try {
			l_Bean = GECAMedEntityBean.newInstanceOf(p_ClassName);
		} catch (Exception p_Exception) {
			this.log(Level.FATAL, "Failed to instantiate " + p_ClassName, p_Exception);
		}

		return l_Bean;
	}

//---------------------------------------------------------------------------
	/**
	 * Maps data identified by the specified mapping from specified node to
	 * the specified property of specified bean.
	 * 
	 * @param p_Node
	 *            specifies the DOM node to get data from.
	 * @param p_Bean
	 *            specifies the Bean that the mapped data is destined for.
	 * @param p_Property
	 *            specifies the property of the specified bean that should
	 *            hold the mapped data.
	 * @param p_Mapping
	 *            specifies the data to retrieve from specified node.
	 * @return the original bean having the specified set to the mapped data
	 *         if mapping was successful. In case of an error, the unmodified
	 *         bean will
	 *         be returned.
	 */
//---------------------------------------------------------------------------

	private GECAMedEntityBean map(Node p_Node, GECAMedEntityBean p_Bean, String p_Property, String p_Mapping) {
		return this.setProperty(p_Bean, p_Property, this.getValue(p_Node, p_Mapping));
	}

//---------------------------------------------------------------------------

//---------------------------------------------------------------------------

	private NodeList getIteratorByXPath(Node p_Node, String p_XPath) {
		NodeList l_nodeList = null;

		try {
			l_nodeList = XPathAPI.selectNodeIterator(p_Node, p_XPath, m_CheckXPath);
		} catch (Exception p_Exception) {
			this.log(Level.ERROR, "Error while retrieving iterator for XPath :" + p_XPath, p_Exception);
		}

		return l_nodeList;
	}

//---------------------------------------------------------------------------
	/**
	 * Resolves the specified XPath expression, using the specified node as the
	 * base node and returns the corresponding value.
	 */
//---------------------------------------------------------------------------

	private String resolveXPathExpression(Node p_Node, String p_XPathExpression) {
		Matcher l_XPathMatcher;
		String l_Value = null;

		l_XPathMatcher = c_XPathPattern.matcher(p_XPathExpression);
		if (l_XPathMatcher.matches()) {
			if (l_XPathMatcher.groupCount() > 1) {
				l_Value = this.getValueByXPath(p_Node, l_XPathMatcher.group(1), l_XPathMatcher.group(3));
			} else
				l_Value = this.getValueByXPath(p_Node, l_XPathMatcher.group(1), null);
		}
		return l_Value;
	}

//---------------------------------------------------------------------------
	/**
	 * Returns the arguments specified in complex mappings, i.e. everything that
	 * is not a direct mapping. Arguments which are not instructions and which
	 * have an xpath expression on their right hand side will be resolved before
	 * being returned.
	 * 
	 * @param p_Node
	 *            specifies the base node to start from to resolve xpath
	 *            expressions,
	 *            if any.
	 * @param p_Arguments
	 *            is string that's holding all the arguments.
	 * @return a hashtable containing all arguments
	 */
//---------------------------------------------------------------------------

	private Hashtable<String, String> resolveArguments(Node p_Node, String p_Arguments) {
		Hashtable<String, String> l_Arguments;
		Enumeration<String> l_Keys = null;
		String l_Key;
		String l_Value;

		l_Arguments = this.getArguments(p_Arguments);
		if (l_Arguments != null)
			l_Keys = l_Arguments.keys();

		while ((l_Keys != null) && l_Keys.hasMoreElements()) {
			l_Key = l_Keys.nextElement();
			if (this.getInstruction(l_Key) == c_None) {
				l_Value = this.resolveXPathExpression(p_Node, l_Arguments.get(l_Key));
				if (l_Value == null)
					l_Value = "";
				l_Arguments.remove(l_Key);
				l_Arguments.put(l_Key, l_Value);
			}
		}

		return l_Arguments;
	}

//---------------------------------------------------------------------------
	/**
	 * Assembles a where clause using the specified properties as search
	 * criteria.
	 * Every single property will be equality tested against the value returned
	 * by the corresponding mapping. All property tests will be AND combined
	 * to a single where clause.
	 * 
	 * @param p_Node
	 *            specifies the base node to retrieve mapping values from.
	 * @param p_Properties
	 *            specifies the properties to be assembled into a
	 *            where clause.
	 * @return a ready to use where clause.
	 */
//---------------------------------------------------------------------------

	private WhereClause assembleWhereClause(Node p_Node, Properties p_Properties) {
		Enumeration l_Keys = null;
		String l_Key;
		String l_Mapping;
		Object l_Value;

		WhereClause l_Clause;
		HibernateCondition l_Condition;

		l_Clause = new WhereClause();
		l_Clause.setOperator(HibernateOperator.c_AndOperator);

		l_Keys = p_Properties.keys();
		while (l_Keys != null && l_Keys.hasMoreElements()) {
			l_Key = (String) l_Keys.nextElement();
			l_Mapping = p_Properties.getProperty(l_Key);
			l_Value = this.getValue(p_Node, l_Mapping);

			// =======================================================================
			// = If mapped value is an instance of GECAMedEntity bean, then
			// we'll test
			// = Ids for equality.
			// =======================================================================

			if (l_Value instanceof GECAMedEntityBean) {
				l_Condition = new HibernateCondition(l_Key + ".id", HibernateOperator.c_EqualOperator,
						((GECAMedEntityBean) l_Value).getId());
			}

			// =======================================================================
			// = If mapped value is NULL, we'll have to use an 'is null' check
			// instead
			// = of an equality test.
			// =======================================================================

			else if (l_Value == null) {
				l_Condition = new HibernateCondition(l_Key, HibernateOperator.c_IsOperator, Condition.c_Null);
			}

			// =======================================================================
			// = All other cases will be handled as a plain equality test.
			// =======================================================================

			else {
				l_Condition = new HibernateCondition(l_Key, HibernateOperator.c_EqualOperator, l_Value);
			}
			l_Clause.addCondition(l_Condition);
		}

		return l_Clause;
	}

//---------------------------------------------------------------------------
	/**
	 * Returns the value corresponding to the specified mapping using the
	 * specified
	 * DOM node as source.
	 * 
	 * @param p_Node
	 *            specified the DOM node to read value from.
	 * @param p_Mapping
	 *            specifies which data to get from specified node.
	 * @return the value read from specified node matching specified mapping.
	 * @see #getComposedValue(Node, String, String)
	 * @see #getCollectionValue(Node, String, String)
	 * @see #getSingleValue(Node, String, String)
	 * @see #getReference(Node, String, String)
	 */
//---------------------------------------------------------------------------

	private Object getValue(Node p_Node, String p_Mapping) {
		Matcher l_Matcher;

		l_Matcher = c_ComposedPattern.matcher(p_Mapping);
		if (l_Matcher.matches()) {
			return this.getComposedValue(p_Node, l_Matcher.group(1), l_Matcher.group(2));
		}

		l_Matcher = c_CollectionPattern.matcher(p_Mapping);
		if (l_Matcher.matches()) {
			return this.getCollectionValue(p_Node, l_Matcher.group(1), l_Matcher.group(2));
		}

		l_Matcher = c_SinglePattern.matcher(p_Mapping);
		if (l_Matcher.matches()) {
			return this.getSingleValue(p_Node, l_Matcher.group(1), l_Matcher.group(2));
		}

		l_Matcher = c_ReferencePattern.matcher(p_Mapping);
		if (l_Matcher.matches()) {
			return this.getReference(p_Node, l_Matcher.group(1), l_Matcher.group(2));
		}

		// l_String = this.getValueByXPath(p_Node,p_Mapping,null);
		return this.resolveXPathExpression(p_Node, p_Mapping);
	}

//---------------------------------------------------------------------------
	/**
	 * The getComposedValue methods handles mappings of composed type.
	 * 
	 * @param p_Node
	 *            specifies the base node to start evaluation of XPaths from
	 * @param p_Type
	 *            specifies the type of composed mapping we're dealing with.
	 *            Accepted values so far are:
	 *            <ul>
	 *            <li>Date</li>
	 *            </ul>
	 * @param p_Arguments
	 *            specifies extra arguments that where specified inside
	 *            the composed mapping.
	 * @return the value resulting from the composed mapping.
	 */
//---------------------------------------------------------------------------

	private Object getComposedValue(Node p_Node, String p_Type, String p_Arguments) {
		Object l_Value = null;

		if (c_TypDate.equals(p_Type.toLowerCase())) {
			l_Value = this.getDate(p_Node, p_Arguments);
		}
		return l_Value;
	}

//---------------------------------------------------------------------------
	/**
	 * The getSingleValue method handles mappings of type single.
	 * 
	 * @param p_Node
	 *            specifies the base node to start evaluation of XPaths from
	 * @param p_Type
	 *            specifies the type or class of the bean to map.
	 * @param p_Arguments
	 *            specifies extra arguments that where specified inside
	 *            the single mapping.
	 * @return an initialized GECAMedentity Bean resulting from the single
	 *         mapping
	 */
//---------------------------------------------------------------------------

	private GECAMedEntityBean getSingleValue(Node p_Node, String p_Type, String p_Arguments) {
		Hashtable<String, String> l_Arguments;
		String l_Intruction;
		String l_Mapping;
		GECAMedEntityBean l_Value = null;

		l_Arguments = this.getArguments(p_Arguments);
		if (l_Arguments != null) {
			l_Intruction = "@" + c_InsMapping;
			if (l_Arguments.containsKey(l_Intruction)) {
				l_Mapping = l_Arguments.get(l_Intruction);
				this.setCurrentNode(p_Node);
				l_Value = this.mapSingle(p_Type, l_Mapping);
				this.resetCurrentNode();
			}
		}

		return l_Value;
	}

//---------------------------------------------------------------------------
	/**
	 * The getCollectionValue method handles mappings of type collection.
	 * 
	 * @param p_Node
	 *            specifies the base node to start evaluation of XPaths from
	 * @param p_Type
	 *            specifies the type or class of the beans to map.
	 * @param p_Arguments
	 *            specifies extra arguments that where specified inside
	 *            the collection mapping.
	 * @return an initialized collection holding beans resulting from the
	 *         collection
	 *         mapping
	 */
//---------------------------------------------------------------------------

	private Collection<?> getCollectionValue(Node p_Node, String p_Type, String p_Arguments) {
		Hashtable<String, String> l_Arguments;
		String l_Intruction;
		String l_Mapping;
		Collection<?> l_Collection = null;

		l_Arguments = this.getArguments(p_Arguments);
		if (l_Arguments != null) {
			l_Intruction = "@" + c_InsMapping;
			if (l_Arguments.containsKey(l_Intruction)) {
				l_Mapping = l_Arguments.get(l_Intruction);
				this.setCurrentNode(p_Node);
				l_Collection = this.mapRepeating(p_Type, l_Mapping);
				this.resetCurrentNode();
			}
		}

		return l_Collection;
	}

//---------------------------------------------------------------------------
	/**
	 * The getReference method handles mappings of type reference.
	 * 
	 * @param p_Node
	 *            specifies the base node to start evaluation of XPaths from
	 * @param p_Type
	 *            specifies the type or class of the bean to reference.
	 * @param p_Arguments
	 *            specifies extra arguments that where specified inside
	 *            the reference mapping.
	 * @return an already existing instance of the specified bean class from
	 *         database, or <code>null</code> if none matched to specified
	 *         criteria.
	 * @see #getReference(String, WhereClause)
	 */
//---------------------------------------------------------------------------

	private Object getReference(Node p_Node, String p_Type, String p_Arguments) {
		Hashtable<String, String> l_Arguments;
		Properties l_Properties;
		String l_Intruction;
		String l_Mapping;
		String l_Key;
		GECAMedEntityBean l_Reference = null;
		Object l_Value = null;

		l_Arguments = this.getArguments(p_Arguments);
		if (l_Arguments != null) {
			l_Intruction = "@" + c_InsMapping;
			if (l_Arguments.containsKey(l_Intruction)) {
				l_Mapping = l_Arguments.get(l_Intruction);
				l_Properties = this.loadMappingFile(l_Mapping);
				if (l_Properties != null) {
					l_Reference = this.getReference(p_Type, this.assembleWhereClause(p_Node, l_Properties));
				}
			}

			if (l_Reference == null)
				return null;

			l_Intruction = "@" + c_InsKey;
			if (l_Arguments.containsKey(l_Intruction)) {
				l_Key = l_Arguments.get(l_Intruction);
				l_Value = this.getProperty(l_Reference, l_Key);
				return l_Value;
			}
		}

		return l_Reference;
	}

//---------------------------------------------------------------------------
//***************************************************************************
//* Composed Mapping Primitives	                                            *
//***************************************************************************
//---------------------------------------------------------------------------
	/**
	 * The getDate method is a helper method for the getComposedValue method.
	 * The method handles and processes composed mappings of type Date.
	 * 
	 * @param p_Node
	 *            specifies the base node to start evaluation of XPaths from
	 * @param p_Arguments
	 *            specifies extra arguments that where specified inside
	 *            the composed mapping of type date.
	 * @return
	 */
//---------------------------------------------------------------------------

	private Date getDate(Node p_Node, String p_Arguments) {
		Hashtable<String, String> l_Arguments;

		int l_Day = 0;
		int l_Month = 1;
		int l_Year = 0;
		int l_Hour = 0;
		int l_Minute = 0;
		int l_Second = 0;

		GregorianCalendar l_Calendar;
		SimpleDateFormat l_DateFormat;
		Date l_Date = null;

		l_Arguments = this.resolveArguments(p_Node, p_Arguments);

		// =======================================================================
		// = Composed Mappings of type Date come in two flavors. First flavor is
		// = a string representation of Date in value and explicit format
		// specification
		// =======================================================================

		if (l_Arguments.containsKey("@" + EntityMapper.c_InsFormat)) {
			l_DateFormat = new SimpleDateFormat(l_Arguments.get("@" + EntityMapper.c_InsFormat));

			try {
				l_Date = l_DateFormat.parse(l_Arguments.get("value"));
			} catch (ParseException p_Exception) {
				l_Date = null;
			} catch (Exception p_Exception) {
				l_Date = null;
			}
		}

		// =======================================================================
		// Second flavor gathers the constituting fields for the date from
		// different nodes or attributes
		// =======================================================================

		else {
			try {
				l_Day = (l_Arguments.get("day") != null) ? Integer.parseInt(l_Arguments.get("day")) : 0;
				l_Month = (l_Arguments.get("month") != null) ? Integer.parseInt(l_Arguments.get("month")) : 1;
				l_Year = (l_Arguments.get("year") != null) ? Integer.parseInt(l_Arguments.get("year")) : 0;
				l_Hour = (l_Arguments.get("hour") != null) ? Integer.parseInt(l_Arguments.get("hour")) : 0;
				l_Minute = (l_Arguments.get("minute") != null) ? Integer.parseInt(l_Arguments.get("minute")) : 0;
				l_Second = (l_Arguments.get("second") != null) ? Integer.parseInt(l_Arguments.get("second")) : 0;

				l_Calendar = new GregorianCalendar(l_Year, l_Month - 1, l_Day, l_Hour, l_Minute, l_Second);

				l_Date = l_Calendar.getTime();
			} catch (NumberFormatException p_Exception) {
			}
		}
		return l_Date;
	}

//---------------------------------------------------------------------------
//***************************************************************************
//* Class Body	                                                            *
//***************************************************************************
//---------------------------------------------------------------------------
	/**
	 * Creates a single instance of the GECAMedEntityBean specified by
	 * p_Classname
	 * and initializes it with data retrieved from XML data (contained in DOM
	 * Document
	 * specified at construction time) by using the provided mapping file.
	 * 
	 * @param p_ClassName
	 *            specifies the type of GECAMedEntityBean to create and
	 *            initialize
	 * @param p_MappingFile
	 *            specifies the mapping file to be used to map data
	 *            from DOM Document to GECAMedEntityBean's properties.
	 * @return a newly created and initialized GECAMedEntityBean of the
	 *         specified
	 *         type whose properties have been set according to the specified
	 *         mapping file.
	 */
//---------------------------------------------------------------------------

	public GECAMedEntityBean mapSingle(String p_ClassName, String p_MappingFile) {
		Properties l_Properties;
		Enumeration l_Keys = null;
		String l_Key;
		String l_Mapping;
		GECAMedEntityBean l_Bean = null;
		Node l_Node = null;

		if ((p_ClassName == null) || (p_MappingFile == null))
			return null;

		l_Bean = this.instantiate(p_ClassName);
		if (l_Bean == null)
			return null;

		l_Properties = this.loadMappingFile(p_MappingFile);
		if (l_Properties != null)
			l_Keys = l_Properties.keys();

		if (l_Keys == null)
			return l_Bean;

		while (l_Keys.hasMoreElements()) {
			l_Key = (String) l_Keys.nextElement();
			l_Mapping = l_Properties.getProperty(l_Key);
			if (l_Mapping == null)
				continue;

			l_Node = this.getCurrentNode();

			l_Bean = this.map(l_Node, l_Bean, l_Key, l_Mapping);
		}

		return l_Bean;
	}

//---------------------------------------------------------------------------
	/**
	 * Creates multiple instances of GECAMedEntityBeans specified by p_Classname
	 * and initializes them with data retrieved from XML data (contained in DOM
	 * Document
	 * specified at construction time) by using the provided mapping file.
	 * 
	 * @param p_ClassName
	 *            specifies the type of GECAMedEntityBean to create and
	 *            initialize
	 * @param p_MappingFile
	 *            specifies the mapping file to be used to map data
	 *            from DOM Document to GECAMedEntityBean's properties.
	 * @return a collection of newly created and initialized GECAMedEntityBean
	 *         of the
	 *         specified type whose properties have been set according to the
	 *         specified
	 *         mapping file. The method creates as many instances of specified
	 *         GECAMedEntityBeans
	 *         as there are matching nodes in the DOM Document.
	 */
//---------------------------------------------------------------------------

	public Collection<?> mapRepeating(String p_ClassName, String p_MappingFile) {
		Properties l_Properties;
		Enumeration l_Keys = null;
		String l_Key;
		String l_Mapping;
		String l_BasePath = null;
		GECAMedEntityBean l_Bean = null;
		Collection<GECAMedEntityBean> l_Beans = null;

//	Collection <Node>				l_Nodes = null;
		NodeList l_NodeList;
		int l_Index = 0;

		if ((p_ClassName == null) || (p_MappingFile == null))
			return null;
		l_Properties = this.loadMappingFile(p_MappingFile);
		if (l_Properties != null) {
			l_Key = "@" + c_InsRepeating;
			l_BasePath = l_Properties.getProperty(l_Key);
			if (l_BasePath != null)
				l_Properties.remove(l_Key);
		}

		if (l_BasePath == null)
			return l_Beans;

		l_Beans = new LinkedHashSet<GECAMedEntityBean>();

		l_NodeList = this.getIteratorByXPath(this.getCurrentNode(), l_BasePath);
		if (!l_BasePath.endsWith("/"))
			l_BasePath += "/";

		if (l_NodeList != null) {
			for (int i = 0; i < l_NodeList.getLength(); i++) {
				Node l_Node = l_NodeList.item(i);
				l_Bean = this.instantiate(p_ClassName);
				l_Keys = l_Properties.keys();

				while (l_Keys.hasMoreElements()) {
					l_Key = (String) l_Keys.nextElement();
					l_Mapping = l_Properties.getProperty(l_Key);
					if (l_Mapping == null)
						continue;

					if (this.getInstruction(l_Mapping) == c_Index) {
						l_Bean = this.setProperty(l_Bean, l_Key, Integer.valueOf(l_Index));
						continue;
					}

					l_Mapping = l_Mapping.replace(l_BasePath, "");

					l_Bean = this.map(l_Node, l_Bean, l_Key, l_Mapping);
				}

				l_Beans.add(l_Bean);
				l_Index++;
			}
		}

		return l_Beans;
	}

//---------------------------------------------------------------------------
//***************************************************************************
//* End of Class                                                            *
//***************************************************************************
//---------------------------------------------------------------------------

}
