/*******************************************************************************
 * 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.SimpleDateFormat;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.Enumeration;
import java.util.GregorianCalendar;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Properties;
import java.util.Vector;
import java.util.regex.Matcher;

import lu.tudor.santec.gecamed.core.ejb.entity.beans.GECAMedEntityBean;
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;


public class Entity2XMLMapper extends EntityMapper
	{
//---------------------------------------------------------------------------
//***************************************************************************
//* Constants	                                                            *
//***************************************************************************
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
//***************************************************************************
//* Constructor	                                                            *
//***************************************************************************
//---------------------------------------------------------------------------

public Entity2XMLMapper (Document p_XMLDocument, boolean p_CheckXPath)
	{
	super(p_XMLDocument, p_CheckXPath);
	}

//---------------------------------------------------------------------------
//***************************************************************************
//* Primitives	                                                            *
//***************************************************************************
//---------------------------------------------------------------------------
/**
 * The createNodesByXPath method creates the specified number of nodes in the
 * DOM Document, starting from the specified base node and using the specified
 * XPath as their path.
 * @param p_Node specifies the base node to start creating nodes from.
 * @param p_XPath to nodes to be created expressed relative to specified base
 * node.
 * @param p_Number specifies the total number of nodes to create
 * @return A collection holding the newly created nodes.
 */
//---------------------------------------------------------------------------

private Collection<Node> createNodesByXPath (Node p_Node, String p_XPath, int p_Number)
	{
	Vector<Node>		l_Nodes			= null;
	Node				l_Node 			= null;
	Node				l_CurrentNode 	= null;
	int					l_Index     	= 0;
	
	l_Nodes = new Vector <Node> ();
	
	try	{
		l_CurrentNode = XPathAPI.selectSingleNode (p_Node, p_XPath, m_CheckXPath);
		
		if (l_CurrentNode == null) l_CurrentNode = p_Node;
		
		for (l_Index = 0; l_Index < p_Number; l_Index++)
			{
			l_Node = this.createNodeByXPath (l_CurrentNode,p_XPath);
			if (l_Node != null)
				{
				l_Nodes.add(l_Node);
				}	
			}	
		}
	catch (Exception p_Exception)
		{
		this.log(Level.ERROR, "Error while building nodes for XPath :" + p_XPath, p_Exception);
		}
	
	return l_Nodes;
	}

//---------------------------------------------------------------------------
/**
 * Resolves the specified XPath expression, using the specified node as the
 * base node and sets the target node to the specified value.
 * @param p_Node specifies the base node for the specified XPath.
 * @param p_XPathExpression specifies the XPath to the target node expressed 
 * relative to the specified base node.
 * @param p_Value specifies the value to set the target node to.
 */
//---------------------------------------------------------------------------

private void resolveXPathExpression (Node p_Node, String p_XPathExpression, String p_Value)
	{
	Matcher	l_XPathMatcher;	
	
	l_XPathMatcher = c_XPathPattern.matcher(p_XPathExpression);
	if (l_XPathMatcher.matches())
		{
		if (l_XPathMatcher.groupCount() > 1)
			{
			this.setValueByXPath (p_Node,l_XPathMatcher.group(1), l_XPathMatcher.group(3),p_Value);	
			}
		else this.setValueByXPath (p_Node,l_XPathMatcher.group(1), null,p_Value);		
		}
	}

//---------------------------------------------------------------------------
/**
 * Sets the arguments specified in complex mappings, i.e. everything that
 * is not a direct mapping. Only arguments which are not instructions and which
 * have an xpath expression on their right hand side will be resolved, i.e set
 * to the specified values.
 * @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.
 * @param p_Values specifies the values to substitute right hand side of xpath
 * arguments with.
 */
//---------------------------------------------------------------------------

private void resolveArguments (Node p_Node, String p_Arguments, Hashtable <String,String> p_Values)
	{
	Hashtable <String,String> l_Arguments;
	Enumeration <String>	  l_Keys = null;
	String					  l_Key;	
	String					  l_Value;
	
	if (p_Values == null) return;
	
	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 = p_Values.get (l_Key);		
			this.resolveXPathExpression (p_Node, l_Arguments.get(l_Key),l_Value);
			}
		}
	}
	
//---------------------------------------------------------------------------
/**
 * Maps the value of the specified property from the specified bean using the
 * provided mapping. The specified node is the base node that together with 
 * the mapping identifies the target node that will receive the property's
 * value.
 * @param p_Node specifies the base node that mapping is relative to.
 * @param p_Bean specifies the bean to get value of specified property from.
 * @param p_Property specifies the property to get value of.
 * @param p_Mapping specifies together with the base node the target node
 * that will receive the retrieved property value.
 */
//---------------------------------------------------------------------------

private void map (Node p_Node, GECAMedEntityBean p_Bean, String p_Property, String p_Mapping)
	{
	Object	l_Value;	
	
	l_Value = this.getProperty (p_Bean, p_Property);
	this.setValue (p_Node,p_Mapping,l_Value);
	}

//---------------------------------------------------------------------------
/**
 * assembles the where clause required to fetch an already exisiting reference
 * object from database.
 * @param p_Property specifies the property to be used to test for equality.
 * @param p_Value specifies the value the specified property will be checked
 * against.
 * @return a ready to be used where clause.
 */
//---------------------------------------------------------------------------

private WhereClause assembleWhereClause (String p_Property, Object p_Value)
	{
	WhereClause	 		l_Clause;
	HibernateCondition	l_Condition;
	String				l_Property;
	Object				l_Value;
	
	l_Clause = new WhereClause();

	if (p_Value instanceof GECAMedEntityBean)
		{
		l_Property = p_Property + ".id";
		l_Value = ((GECAMedEntityBean)p_Value).getId();	
		}
	else
		{
		l_Property = p_Property;
		l_Value	   = p_Value;
		}
	
	l_Condition = new HibernateCondition (l_Property,
										  HibernateOperator.c_EqualOperator,
										  l_Value);
    	
    l_Clause.addCondition(l_Condition);
 	
	return l_Clause;
	}

//---------------------------------------------------------------------------
/**
 * 
 */
//---------------------------------------------------------------------------

private void	setValue (Node p_Node, String p_Mapping, Object p_Value)
	{
	Matcher		l_Matcher;
	String		l_Value;
	
	l_Matcher = c_ComposedPattern.matcher(p_Mapping);
	if (l_Matcher.matches())
		{
		this.setComposedValue (p_Node,l_Matcher.group(1),l_Matcher.group(2),p_Value); 	
		return;
		}

	l_Matcher = c_CollectionPattern.matcher(p_Mapping);
	if (l_Matcher.matches())
		{
		this.setCollectionValue (p_Node, l_Matcher.group(1), l_Matcher.group(2), (Collection <GECAMedEntityBean>)p_Value);
		return;
		}
	
	l_Matcher = c_SinglePattern.matcher(p_Mapping);
	if (l_Matcher.matches())
		{
		this.setSingleValue (p_Node, l_Matcher.group(1), l_Matcher.group(2),(GECAMedEntityBean)p_Value);
		return;
		}

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

	l_Value = (p_Value != null)?p_Value.toString():"";
	
	//this.setValueByXPath (p_Node, p_Mapping, null, l_Value);
	this.resolveXPathExpression(p_Node, p_Mapping, l_Value);
	}

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

private void setComposedValue (Node p_Node, String p_Type, String p_Arguments, Object p_Value)
	{	
	if (c_TypDate.equals(p_Type.toLowerCase()) && p_Value instanceof Date)	
		{
		this.setDate(p_Node, p_Type, p_Arguments, p_Value);
		}
	}

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

private void setCollectionValue (Node p_Node, String p_Type, String p_Arguments, Collection <GECAMedEntityBean> p_Values)
	{
	Hashtable <String,String> l_Arguments;
 	String					  l_Intruction;
 	String					  l_Mapping;
 	
	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);
    		this.mapRepeating (p_Values, l_Mapping);
    		this.resetCurrentNode();
    		}
    	}
	}

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

private void setSingleValue (Node p_Node, String p_Type, String p_Arguments, GECAMedEntityBean p_Value)
	{
	Hashtable <String,String> l_Arguments;
 	String					  l_Intruction;
 	String					  l_Mapping;
 	
	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);
    		this.mapSingle (p_Value, l_Mapping);
    		this.resetCurrentNode();
    		}
    	}
	}

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

private void setReference (Node p_Node, String p_Type, String p_Arguments, Object p_Value)
	{
	Hashtable <String,String> l_Arguments;
 	String					  l_Intruction;
 	String					  l_Mapping;
	String					  l_Key;
 	GECAMedEntityBean		  l_Reference = null;
  	
	l_Arguments = this.getArguments (p_Arguments);			
	if (l_Arguments != null)
    	{
    	l_Intruction = "@" + c_InsKey;
    	if (l_Arguments.containsKey(l_Intruction))
    		{
    		l_Key   = l_Arguments.get(l_Intruction);	
    		l_Reference = this.getReference (p_Type, this.assembleWhereClause (l_Key, p_Value));
    		}
    	else if (p_Value instanceof GECAMedEntityBean) l_Reference = (GECAMedEntityBean) p_Value;	
     		
    	l_Intruction = "@" + c_InsMapping;
    	if (l_Arguments.containsKey(l_Intruction))
    		{
    		l_Mapping 		= l_Arguments.get(l_Intruction);	
    		
    		this.setCurrentNode(p_Node);
    		this.mapSingle(l_Reference, l_Mapping);
    		this.resetCurrentNode();
    		}
    	}
 	}

//---------------------------------------------------------------------------
//***************************************************************************
//* Composed Mapping Primitives	                                            *
//***************************************************************************
//---------------------------------------------------------------------------

private void setDate (Node p_Node, String p_Type, String p_Arguments, Object p_Value)
	{
	Hashtable <String,String>	l_Arguments;
	Hashtable <String,String>  	l_Values;
 	GregorianCalendar			l_Calendar;	
	SimpleDateFormat			l_DateFormat;
 	
	l_Values = new Hashtable <String,String> ();
	
	l_Arguments = this.getArguments (p_Arguments);
			
	if (l_Arguments.containsKey("@" + EntityMapper.c_InsFormat))
		{
		l_DateFormat = new SimpleDateFormat (l_Arguments.get("@" + EntityMapper.c_InsFormat));
		this.resolveXPathExpression (p_Node, l_Arguments.get("value"), l_DateFormat.format((Date)p_Value));			
		}
	else
		{
		l_Calendar = new GregorianCalendar ();
		l_Calendar.setTime((Date)p_Value);	
	
		l_Values.put ("day",   Integer.valueOf (l_Calendar.get(Calendar.DAY_OF_MONTH)).toString());
		l_Values.put ("month", Integer.valueOf (l_Calendar.get(Calendar.MONTH) + 1).toString());
		l_Values.put ("year",  Integer.valueOf (l_Calendar.get(Calendar.YEAR)).toString());
		l_Values.put ("hour",  Integer.valueOf (l_Calendar.get(Calendar.HOUR)).toString());
		l_Values.put ("minute",Integer.valueOf (l_Calendar.get(Calendar.MINUTE)).toString());
		l_Values.put ("second",Integer.valueOf (l_Calendar.get(Calendar.SECOND)).toString());
	
		this.resolveArguments (p_Node, p_Arguments, l_Values);
		}
	}

//---------------------------------------------------------------------------
//***************************************************************************
//* Class Body	                                                            *
//***************************************************************************
//---------------------------------------------------------------------------

public void mapSingle (GECAMedEntityBean p_Bean, String p_MappingFile)
	{
	Properties				l_Properties;
	Enumeration    			l_Keys	= null;
	String         			l_Key;
	String		   			l_Mapping;
	Node					l_Node = null;
	
   	if ((p_Bean == null) || (p_MappingFile == null)) return;
	  	
	l_Properties	 = this.loadMappingFile(p_MappingFile);
    if (l_Properties != null) l_Keys = l_Properties.keys();	
 	
	if (l_Keys == null) return;
	
	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();
		
		this.map (l_Node, p_Bean, l_Key, l_Mapping);
    	}
	}

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

public void mapRepeating (Collection <?> p_Beans, String p_MappingFile)
	{
	Properties						l_Properties;
	Enumeration    					l_Keys	= null;
	String         					l_Key;
	String		   					l_Mapping;
	String							l_BasePath = null;
	GECAMedEntityBean				l_Bean = null;
	Iterator <?>					l_BeanIterator;
	
	Collection <Node>				l_Nodes = null;
	Iterator <Node>					l_NodeIterator;
	Node							l_Node;
	
  	if ((p_Beans == null) || (p_MappingFile == null)) return;  	
	
  	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_Node = this.getCurrentNode();
    
    l_Nodes = this.createNodesByXPath(l_Node, l_BasePath, p_Beans.size());
 	if (!l_BasePath.endsWith("/")) l_BasePath += "/";
    
    l_BeanIterator = p_Beans.iterator();
    l_NodeIterator = l_Nodes.iterator();
    
    while (l_BeanIterator.hasNext() && l_NodeIterator.hasNext())
    	{
    	l_Bean = (GECAMedEntityBean) l_BeanIterator.next();
    	l_Node = l_NodeIterator.next();
    	
 		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)
				{
				continue;
				}
			
			l_Mapping = l_Mapping.replace(l_BasePath, "");
			
			this.map (l_Node, l_Bean, l_Key, l_Mapping);	    	
	    	}
    	}
 	}
		
//---------------------------------------------------------------------------
//***************************************************************************
//* End of Class                                                            *
//***************************************************************************
//---------------------------------------------------------------------------

	}
