package routines;

/*
 * Change History:
 * V4.0.1: Update operation should not enrich empty text in Change List
 */
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Hashtable;
import java.util.Objects;
import java.util.Properties;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;

/*
 * user specification: the function's comment should contain keys as follows: 1. write about the function's comment.but
 * it must be before the "{talendTypes}" key.
 * 
 * 2. {talendTypes} 's value must be talend Type, it is required . its value should be one of: String, char | Character,
 * long | Long, int | Integer, boolean | Boolean, byte | Byte, Date, double | Double, float | Float, Object, short |
 * Short
 * 
 * 3. {Category} define a category for the Function. it is required. its value is user-defined .
 * 
 * 4. {param} 's format is: {param} <type>[(<default value or closed list values>)] <name>[ : <comment>]
 * 
 * <type> 's value should be one of: string, int, list, double, object, boolean, long, char, date. <name>'s value is the
 * Function's parameter name. the {param} is optional. so if you the Function without the parameters. the {param} don't
 * added. you can have many parameters for the Function.
 * 
 * 5. {example} gives a example for the Function. it is optional.
 */
public class LcagXmlTools4 {
	private static String DIFF_ROOT_NAME = "Update";
	
	public enum UpdateMode {
		ENRICH,
		UPDATE,
		DIFF_ONLY,
		OPT_CREATE_TAGS,
		OPT_DELETE_TAGS
	}
	
	private static LcagLogger log = LcagLogger.getLogger(LcagXmlTools.class);
       
    /**
     * Returns a Dom4J document from aXML where aXML can be a one of
     * Talend XML Document, Dom4J Document or Element, XML String, XML File Name
     * 
     * {talendTypes} Document
     * 
     * {Category} LCAG-XML Tools
     * 
     * {param} object(sourceXML) aXML : a String, a Document or a Node representing XML 
     */
	public static Document getDocument(Object aXML) throws DocumentException {

		Document doc = null;
		
		if (aXML instanceof routines.system.Document) {
    		doc = ((routines.system.Document)aXML).getDocument();
    	} else if (aXML instanceof Document) {
    		doc = (Document)aXML;
    	} else if (aXML instanceof Element) {
    		Element elem = (Element)aXML;
    		Element newElem = (Element)elem.clone();
    		
    		doc = DocumentHelper.createDocument(newElem);
    	} else if (aXML instanceof String) {
    		String xml = ((String)aXML).trim();
    		
    		if (! xml.startsWith("<") ) {
	    		// Check if the argument is a valid path to a file:
				try {
					Path path = Paths.get(xml);
	    		
					if (Files.isReadable(path)) {
						// Yes, it is a file, now assume its a XML file and read the contents:
	    				xml = new String(Files.readAllBytes(path));
	    			} 		
				} catch (Exception e) {
					log.error("LcagXmlTools.getDocument: Could not read file '%s'" , xml);
				}
    		}
    		
    		SAXReader reader = new SAXReader();
    		StringReader stringReader = new StringReader(xml);
    		
    		log.debug("LcagXmlTools.getDocument: XML to parse: " + xml);
    		doc = reader.read(stringReader);
    		
    		//String xml2 = doc.asXML();
    		//xml = xml;
    	}
		return doc;
	}

	/**
     * Returns a Talend document from aXML where aXML can be a Talend XML Document or a Dom4J.
     * 
     * {talendTypes} Document
     * 
     * {Category} LCAG-XML Tools
     * 
     * {param} object(sourceXML) aXML : a String, a Document or a Node representing XML 
     */
	public static routines.system.Document getTalendDocument(Object aXML) throws DocumentException {
		routines.system.Document result;
		
		if (aXML instanceof routines.system.Document) {
			result = (routines.system.Document)aXML;
		} else {
			result = new routines.system.Document();
			Document doc = getDocument(aXML);

			result.setDocument(doc);
		}
		return result;
	}
	
	public static Element getElement(Object aXML) throws DocumentException {
		Element result = null;
		
		if (aXML instanceof Element) {
			result = (Element)aXML;
		} else {
			Document doc = getDocument(aXML);
			
			result = doc.getRootElement();
		}
		
		return result;
	}

    /**
     * writeXMLToFile: Write XML into a File. Overwrites file if exists.
     *
     *
     * {talendTypes} void
     *
     * {Category} LCAG-XML Tools
     *
     * {param} Object aXML: XML represent as String, Talend Dcoument, Dom4J Document or Dom4J Element
     *
     * {param} String fileName: Name of File to write.
     *
     * {example} writeXMLToFile("<a>Some Text</a>", "File.xml")
     */
	public static void writeXMLToFile(Object aXML, String fileName) throws DocumentException, IOException {
		Document doc = getDocument(aXML);
		
		if (doc == null) return;

		File file = new File(fileName);
		String fPath = file.getAbsolutePath();
		
		FileWriter writer = new FileWriter(fPath);
		doc.write(writer);
		
		writer.close();
	}

    /**
     * Create a Talend Document with a named root element. 
     * 
     * {talendTypes} Document
     * {Category} LCAG-XML Tools
	 *
     * {param} string rootTag:	name of root element
     * {example}  createTalendDocument("a") create <a></a>
     * @throws DocumentException 
     */
	public static routines.system.Document createTalendDocument(String rootTag) throws DocumentException {
		
		if (rootTag.indexOf("/") >= 0) {
			throw new IllegalArgumentException("Xpath argument may not contain slash character '/'");
		}

		routines.system.Document result = xpathCreateTalendDocument(rootTag);
		return result;

	}
	
	/**
     * 
     * xpathGetAsDoc: Gets the node at the given xpath and returns it as a Talend Docuument 
     * 
     * {talendTypes} Document
     * {Category} LCAG-XML Tools
     * 
     * {param} Object(xmlObject) aXML: 	  output: Talend or Dom4J xml document to inspect 
     * {param} Object(xpath) 	 xp:   	  input: xpath lookup string 
     * 
     * {example} xpathGetAsDoc(doc, "/Station/Offices")
     */
    public static routines.system.Document xpathGetAsDoc(Object aXML, String xp) throws DocumentException
    {
    	Document 	doc = null;
    	Node		node;
    	routines.system.Document result = null;
    	
    	doc = getDocument(aXML);
    	
    	if (doc == null || xp == null) return result;
    	
    	node = doc.selectSingleNode( xp );

    	if (node != null) {
    		result = new routines.system.Document();
    		doc = getDocument(node);
    		result.setDocument(doc);
    	}
    	
    	return result;
    }
	
	/**
     * 
     * xpathHasElement: Returns true if the given aXML object has a XML tag at the XÃƒÅ“ath specified.
     * 
     * @param aXML 	output: Talend or Dom4J or String xml document to be changed
     * @param xp	input: xpath lookup string
     * @return true iff element exists
     * @throws DocumentException 
	 *
     * {talendTypes} Document
     * {Category} LCAG-XML Tools
     * 
     * {param} Object(xmlObject) aXML: 	  output: Talend or Dom4J xml document to be changed 
     * {param} Object(xpath) 	 xp:   	  input: xpath lookup string 
     *  
     * {example} xpathHasElement(doc, "Currency/Id")
     */
    public static boolean xpathHasElement(Object aXML, String xp) throws DocumentException {
    	Document 	doc = null;
    	Node		node;
    	String		contentText = null;

		routines.system.Document result = getTalendDocument(aXML);
		    	
    	doc = result.getDocument();
    	    	
    	if (doc == null || xp == null) return false;
    	
    	node = doc.selectSingleNode(xp);
    	   	
    	return (node != null);
    }

    public static String xpathGetTagName(Object aXML, String xp) throws DocumentException {
    	Document 	doc = null;
    	Element		rootElem;
    	String		contentText = null;

		routines.system.Document result = getTalendDocument(aXML);
		    	
    	doc = result.getDocument();
    	    	
    	if (doc == null || xp == null) return null;
    	
    	rootElem = doc.getRootElement();
    	
    	
    	if (rootElem != null) {
        	Node node = rootElem.selectSingleNode(xp);

        	if (node != null) 
        		contentText = node.getName();
    	}
    	
    	return contentText;
    }
    
    /**
     * 
     * xpathGetValue: gets the value of the first element located at a xpath expression xp. 
     * 				  Returns value as text if successful else null. 
     * 
     * @param aXML 	output: Talend or Dom4J or String xml document to be changed
     * @param xp	input: xpath lookup string
     * @return the value of the xp element
     * @throws DocumentException 
	 *
     * {talendTypes} Document
     * {Category} LCAG-XML Tools
     * 
     * {param} Object(xmlObject) aXML: 	  output: Talend or Dom4J xml document to be changed 
     * {param} Object(xpath) 	 xp:   	  input: xpath lookup string 
     *  
     * {example} xpathGetValue(doc, "Currency/Id")
     */
    public static String xpathGetValue(Object aXML, String xp) throws DocumentException {
    	Document 	doc = null;
    	Node		node;
    	String		contentText = null;

		routines.system.Document result = getTalendDocument(aXML);
		    	
    	doc = result.getDocument();
    	    	
    	if (doc == null || xp == null) return null;
    	
    	node = doc.selectSingleNode(xp);
    	
    	if (node != null) {
    		contentText = node.getStringValue();
    	}
    	
    	return contentText;
    }
    
    /**
     * 
     * xpathGetListOfValues: gets the list of values of the first repeated element located at a xpath expression xp. 
     * 				  Returns list of values as String List if successful else null. 
     * 
     * @param aXML 	output: Talend or Dom4J or String xml document to be changed
     * @param xp	input: xpath lookup string
     * @return List of all values found that match the xpath. Returns empty list if aXML is null or no element found.
     * @throws DocumentException 
	 *
     * {talendTypes} Document
     * {Category} LCAG-XML Tools
     * 
     * {param} Object(xmlObject) aXML: 	  output: Talend or Dom4J xml document to be changed 
     * {param} Object(xpath) 	 xp:   	  input: xpath lookup string 
     *  
     * {example} xpathGetListOfValues("<outer><A>a</A><B>b1</B><B>b2</B></outer>, "//B") should return {"b1", "b2"}
     */
    public static List<String> xpathGetListOfValues(Object aXML, String xp) throws DocumentException {
    	Document 	doc = null;
    	List<String>	result = new ArrayList<String>();

		routines.system.Document srcDoc = getTalendDocument(aXML);
		    	
    	doc = srcDoc.getDocument();
    	    	
    	if (doc == null || xp == null) return result;
    	
    	List<Node> nodeList = doc.selectNodes(xp);
    	
    	for (Node node : nodeList) {
    		String contentText = node.getStringValue();
    		
    		result.add(contentText);
    	}
    	
    	return result;
    }

    /**
     * 
     * xpathSetValue: Sets the value of the first element located at a xpath expression xp to a new value. 
     * 				  Returns old value as text if successful else null. 
     * 
     * @param aXML 	output: Talend or Dom4J or String xml document to be changed
     * @param xp	input: xpath lookup string
     * @param valueList: variable list of objects. The first one not null is used to set the value 
     * @return the changed Talend Document
     * @throws DocumentException 
	 *
     * {talendTypes} Document
     * {Category} LCAG-XML Tools
     * 
     * {param} Object(xmlObject) aXML: 	  output: Talend or Dom4J xml document to be changed 
     * {param} Object(xpath) 	 xp:   	  input: xpath lookup string 
     * {param} Object(values) valueList:  input: variable list of objects. The first one not null is used to set the value 
     * 
     * {example} xpathSetValue(doc, "/Station/Offices/OfficeHours[Company=\"LH\"]/OpeningHours/Weekdays", "5")
     */
    public static routines.system.Document xpathSetValue(Object aXML, String xp, Object... valueList) throws DocumentException
    {
    	Document 	doc = null;
    	Node		node;
    	String 		text = null;

		routines.system.Document result = getTalendDocument(aXML);

		// get the first non-null value into aValue
    	for (Object obj: valueList) {
    		if (obj != null) {
    			text = obj.toString();
    			break;
    		}
    	}
    	
    	doc = result.getDocument();
    	
    	if (doc == null || xp == null) return null;
    	
    	node = doc.selectSingleNode( xp );

    	if (node != null) {
    		node.setText(text);
    	}
    	
    	return result;
    }

    /**
     * 
     * xpathAddTag: Adds the 'value' to the first element located at a xpath expression xp:
     * 				If the 'value' is a XML Entity, the whole Element subtree will be added to xp, otherwise set only the text.
     * 				If the xp does not exist yet check if the parent exists and create the xpath (add to parent)    
     * 				
     * Returns modified aXML object. 
     * 
     * @param aXML 	output: Talend or Dom4J or String xml document to be changed
     * @param xp	input: xpath lookup to see where to add the value
     * @param value value to add - can be a Dom4J Element tree 
     * @return the changed Talend Document
     * @throws DocumentException 
	 *
     * {talendTypes} Document
     * {Category} LCAG-XML Tools
     * 
     * {param} Object(xmlObject) aXML: 	output: Talend or Dom4J xml document to be changed 
     * {param} Object(xpath) 	   xp:  input: xpath lookup to see where to add the value
     * {param} Object(values)   value:  input: value to add - can be a Dom4J Element tree  
     * 
     * {example} xpathAddTag("<X></X>, "//X/newtag", "5") adds tag newtag with value 5 to XML within root element <X>
     */
    public static routines.system.Document xpathAddTag(Object aXML, String xp, Object value) throws DocumentException
    {
    	Element 	doc = null;
    	Element		node = null;

		routines.system.Document result = getTalendDocument(aXML);

    	doc = getElement(result);
    	
    	if (doc == null || xp == null || value == null)
    		throw new IllegalArgumentException("Argument may not be null");

		if (! (value instanceof String)) {
			try { 
				// First try and check if this is a compatible XML tree part to insert...
				value = LcagXmlTools.getElement(value);
			} catch (Exception e) {
				// ... it is not, so convert to String
				value = value.toString();
			}
		}

		try {
			node = (Element)doc.selectSingleNode( xp );
			
			if (node != null) {
				if (value instanceof Element) {
					Element newValue = ((Element) value).createCopy();
					
					node.add(newValue);
				} else {
					String text = value.toString();
					
					node.setText(text);
				}
			} else { // xpath does not yet exist => check if parent exists and add to parent:
				int n = xp.lastIndexOf("/");
			
				if (n >= 0) {
					// xpath has a parent
					String tag = xp.substring(n+1);
	    		
					String  parentXp = xp.substring(0, n);
					Object parentObj = doc.selectSingleNode( parentXp );

					if (parentObj == null)
						throw new DocumentException(String.format(
								"Exception in XML processing: parent %s not available to insert %s", parentXp, tag
								));

					Element parent = getElement(parentObj);
					
					
					if (parent != null) {
		    			if (value instanceof Element) {
		    				// add element to parent
		    				Element newValue = ((Element) value).createCopy();
		    				
		    				parent.add(newValue);
		    			} else {
		    				// Create new element for the tag and set value content:
		    				String text = value.toString();
	
		    				node = parent.addElement(tag);
		    				node.setText(text);
		    			}
					}
				}
			} 
		} catch (Exception e) {
			throw new DocumentException("Exception in XML processing");
		}
		
    	return result;
    }

    public static routines.system.Document xpathDeleteTags(Object aXML, List<String> xpathTagList) throws DocumentException
    {
    	Document 	doc = null;
    	Node		node;
    	String 		text = null;

		routines.system.Document result = getTalendDocument(aXML);

    	doc = result.getDocument();
    	
    	if (doc == null) return null;
    	if (xpathTagList == null) return result;
    	
    	for (String xp: xpathTagList) {
        	node = doc.selectSingleNode( xp );

        	if (node != null) {
        		node.detach();
        	}
    	}

    	return result;
    }

    public static routines.system.Document xpathDeleteTags(Object aXML, String... tagXpathList) throws DocumentException
    {
		List<String> tagList = new ArrayList<String>();
		
    	for (String xp: tagXpathList) {
    		List<String> csvList = java.util.Arrays.asList(xp.split(";"));
    		
    		tagList.addAll(csvList);
    	}

		routines.system.Document result = xpathDeleteTags(aXML, tagList);

    	return result;
    }
    
    /**
     * xpathMatches:  Evaluates if tag values in a XML tree match conditions in an expression string. 
     *    
     * Presently the function supports the following comparison operators:
     *    =, >=, <=, >, <, contains
     * and the logical operators "and" and "or"
     * 
     * NOTE: 
     * 1. All arguments and operators must be seperated by blanc characters
     * 2. Brackets are not yet supported
     * 3. Precedence of "and" over "or" is not implemented
     * 4. An argument that is not resolve a xpath in the XML is considered as a String!  
     *    So do not encapsulate Strings by any quotes in the expression string.
     * 5. Dates are also interpreted as ISO8601 strings (yyyy-mm-dd). You can use 'today'  
     *
     * {talendTypes} void
     * {Category} LCAG-XML Tools

     * @param aXML The XML to evaluate. can be an XML object or a XML String
     * @param expressionString to use for evaluating the XML.
	 *
     * 
     */
    public static boolean xpathMatches(Object aXML, String expressionString) throws DocumentException, IllegalArgumentException {
    	Document doc = getDocument(aXML);
    	
    	if (doc == null || expressionString == null)
    		return false;
    	
    	boolean result = true;
    	String[] condArgs = expressionString.split(" ");
    	String arg1 = null;
    	String arg2 = null;
    	String operator = null;
    	String logicalOperator = null;
    	
    	// parse condition string:
    	for (String arg: condArgs) {
    		Node node;
    		
    		arg = arg.trim();
    		if ("".equals(arg)) continue;
    		
    		if ("today".equalsIgnoreCase(arg)) 
    			arg = LcagDateTime.MDM_DATE_FORMAT.format(new Date());
    		
    		// set operator and arguments:
    		if (arg1 == null) {
    			try { 
    				node = doc.selectSingleNode(arg);
  	    			arg1 = node.getText().trim();
    			} catch (Exception e) {
    				arg1 = arg;
    			}
    		} else if (operator == null) { 
    			operator = arg.toLowerCase();
    		} else if (arg2 == null) {
    			try { 
    				node = doc.selectSingleNode(arg);
  	    			arg2 = node.getText().trim();
    			} catch (Exception e) {
    				arg2 = arg;
    			}
    		} else if ("and".equalsIgnoreCase(arg) || "or".equalsIgnoreCase(arg) ) {
    			logicalOperator = arg.toLowerCase();
    		}
    		
    		// Evaluate expression if both arguments and operator are present:
    		if (arg1 != null && arg2 != null && operator != null && logicalOperator == null) {
    			if ("==".equals(operator)) {
    				result = arg1.equalsIgnoreCase(arg2);
    			} else if ("contains".equals(operator)) {
    				result = arg1.contains(arg2);
    			} else if (operator.startsWith(">") || operator.startsWith("<")) {
    				int i = arg1.compareToIgnoreCase(arg2);
    				
    				if (i == 0 && operator.endsWith("=")) {
    					result = true;
    				} else if (i > 0) {
    					result = operator.startsWith(">");
    				} else {
    					result = operator.startsWith("<");    					
    				}
    			} else {
    				throw new IllegalArgumentException(String.format("Operator '%s' not supported", operator));
    			}
    			   			
    		}
    		
    		if (logicalOperator != null) {
	    		// Check if further evaluation of expression is still needed:
	    		if (result && "and".equals(logicalOperator)) {
	    			arg1 = arg2 = operator = logicalOperator = null;
	    			continue;	
	    		} else if (result == false && "or".equals(logicalOperator)) {
	    			arg1 = arg2 = operator = logicalOperator = null;
	    			continue;
	    		} else {
	    			break; // decision is done => no more evaluations needed
	    		}
    		}
    	}

    	return result;
    }

    
    /**
	 * Fills all fields with empty content in targetElement with data from second 'srcDoc' object
	 */
    @Deprecated
	private static void fillupElement(Element targetElement, Document srcDoc) throws DocumentException {		
		if (srcDoc == null || targetElement == null) return;

		String path = targetElement.getPath();
		Element srcNode = (Element)srcDoc.selectSingleNode(path);
		
		if (srcNode != null) {			
			String srcValue = srcNode.getTextTrim();
			String targetValue = targetElement.getTextTrim();
			
			if ("".equals(targetValue)) {
				targetElement.setText(srcValue);
			}
		
			// Replace all Elements in tartgetDoc by elements in srcSrc that match by the element name: 
			for (Object li: targetElement.elements()) {
				Element e = (Element)li;
				log.debug(e.getName() + "=" + e.getTextTrim());
				//log.debug(targetElement.asXML());
				fillupElement(e, srcDoc);
			}
		}
	}

	/**
	 * Creates a new object based on first arguments and fill all fields with empty content with data from second 'aSourceXML' object.
	 * aBaseXML and aSourceXML must have the same XML structure
	 * 
	 * @param aBaseXML	    the base XML object to fill up. Supported parameter types: Talend Document, dom4j Document or Node, XML String
	 * @param aSourceXML	the XML to take source data from to full up. Supported parameter types: Same as for 'aTargetXML'
     * @return new object with empty fields filled by values from 'aSourceXML'
	 * 
	 */
    @Deprecated
	public static routines.system.Document  fillupXML(Object aBaseXML, Object aSourceXML) throws DocumentException
	{
		Document 	srcDoc = null;
		Document 	baseDoc = null;
		Document	resultDoc = null;
		Element 	baseElement = null;
		
		srcDoc = getDocument(aSourceXML);
		baseDoc = getDocument(aBaseXML);
		
		if (baseDoc == null) 
			return null;
		
		resultDoc = (Document)baseDoc.clone();
		
		if (srcDoc != null) { 
			baseElement = resultDoc.getRootElement();
			//log.debug(baseElement.asXML());

			fillupElement(baseElement, srcDoc);
		}
		
		routines.system.Document result = new routines.system.Document();
		result.setDocument(resultDoc);
		return result;
		
	}

    /**
     * Updates 'aTargetXML' XML object from 'aSourceXML' object depending on the operation 'mode', see {@link UpdateMode}
     * 
     * @param aTargetXML	the XML to change. Supported parameter types: Talend Document, dom4j Document or Node, XML String
     * @param aSourceXML	the XML to take source data from. Supported parameter types: Same as for 'aTargetXML'
     * @param mode	 the mode of operation:
     * @return Changed Talend Document
     * 
     * ENRICH:		 Update 'aTargetXML' using data from 'aSourceXML' but skip fields in 'aSourceXML' if it exists in 'aTargetXML' and contains already some text.
     * UPDATE:		 Update 'aTargetXML' using data from 'aSourceXML' overwriting existing text in 'aTargetXML'
     * ALLOW_DELETE: Update 'aTargetXML' using data from 'aSourceXML'. 
     * 		Delete all elements with empty text in original where the text is empty in 'aSourceXML'
     */
    protected static routines.system.Document _xpathUpdate(Object aTargetXML, final Object aSourceXML, EnumSet<UpdateMode> mode, String excludeXpCSV, String changeListLocationInTargetXp) throws DocumentException
    {
    	Document 	srcDoc = null;
    	Document 	targetDoc = null;
    	List<String> excludeList = null;
    	Element 	changeListLocationInTargetElem = null;

    	boolean diffOnlyMode = mode.contains(UpdateMode.DIFF_ONLY);
    	boolean newResultDocCreated = false; 
    	srcDoc = 	getDocument(aSourceXML);
    	targetDoc = getDocument(aTargetXML);
		routines.system.Document result = new routines.system.Document();
    	
		String targetXML = targetDoc.asXML();
    	
    	if (srcDoc == null) {
    		log.warn("_xpathUpdate: source XML argument is null: Nothing happend: mode:%s, exclude: %s, xp: %s", mode.toString(), excludeXpCSV, changeListLocationInTargetXp); 
    		return null;
    	}

    	if (excludeXpCSV != null) 
    		excludeList = Arrays.asList(excludeXpCSV.split(";"));

    	Element targetRootElem = targetDoc.getRootElement();
    	Element lastTargetElem = null;
    	String  srcRootTag  = srcDoc.getRootElement().getName();
    	List<Node> srcNodes = srcDoc.selectNodes("//*");
    		
		if (changeListLocationInTargetXp == null) {
			result.setDocument(targetDoc);
		} else {
			changeListLocationInTargetElem = (Element)targetRootElem.selectSingleNode(changeListLocationInTargetXp);

			if (changeListLocationInTargetElem == null) {
	    		// caller wants to have new document to show the differences => create the document
	    		result = xpathCreateTalendDocument(changeListLocationInTargetXp);
	    		newResultDocCreated = true;
	    		
	    		changeListLocationInTargetElem = (Element)result.getDocument().selectSingleNode(changeListLocationInTargetXp);
	    	} 
		}		

		// Replace all Elements in targetDoc by elements in srcSrc that match by the element name: 
		for (Node srcNode: srcNodes) {
			if (!(srcNode instanceof Element))
				continue;
			
	    	Element srcElem = (Element)srcNode;
	    	String srcPath = srcElem.getPath();
			String srcText = srcElem.getTextTrim();
			String targetText = null;
			String name = srcNode.getName();
			boolean ignore = false;

			// check if the current element shall be excluded from comparison:
    		ignore = checkIfIgnoreByPathRegExList(srcPath, excludeList);
			if (ignore) 
				continue;

    		// Lookup srcPath in target. Consider target should have same substructure as src. 
			Node targetNode = targetDoc.selectSingleNode("/" + srcPath); // Add slash to make srcPath start with '//'
			boolean isChangeCandidate = false;
			
			if (targetNode == null) {
				// check if we like to insert the tag from the source:
				
				if (mode.contains(UpdateMode.OPT_CREATE_TAGS)) {
					Element srcParentElem = srcNode.getParent();
					String	srcParentXP = srcParentElem.getPath();
					boolean insertAfterLastFound = true;
					Element newElem = srcElem.createCopy();

					// find place to insert:
					if (lastTargetElem != null) {
						String p = lastTargetElem.getPath();
						insertAfterLastFound = p.endsWith(srcPath);
					}
						
					if (insertAfterLastFound) {
						// insert after element previously found:
						if (! diffOnlyMode) _addElementAfter(newElem, lastTargetElem);
						isChangeCandidate = true;
					} else { // last Target element unknown 
						// find by same parent				
						Element targetElem = (Element)targetRootElem.selectSingleNode("/" + srcParentXP);
					
						if (targetElem != null) {
							if (! diffOnlyMode) targetElem.add(newElem);
							isChangeCandidate = true;
						}
					}
				}
			} else { // Node was found in target
				Element targetElem = (Element)targetNode;
		    	String targetPath    = targetNode.getPath();
				
		    	targetText = targetElem.getTextTrim();
				lastTargetElem = targetElem; // remember for possible insert of next missing element
				
				// Check if element found in target is in the same subtree as in the target 
				if (! targetPath.endsWith(srcPath)) {
					// same name but different subtrees => ignore
					continue;
				}
				
				if (mode.contains(UpdateMode.ENRICH)) {
					if ( "".equals(targetText) && ! "".equals(srcText) && !srcText.equals(targetText))
							if (! diffOnlyMode) targetElem.setText(srcText);
							isChangeCandidate = true;
				} else if (mode.contains(UpdateMode.UPDATE)) {
					boolean srcElemIsLeaf = srcElem.elements().isEmpty();
					if ( mode.contains(UpdateMode.OPT_DELETE_TAGS) && "".equals(srcText) && srcElemIsLeaf ) {
						if (! diffOnlyMode) targetElem.detach();
					} else if (! srcText.equals(targetText)){
						if (! diffOnlyMode) targetElem.setText(srcText);
						
						// Add only to change list if it is a real update, not an enrich operation
						if (! "".equals(targetText) ) isChangeCandidate = true;
					}					
				} 

			}
			
			// Build Change List if xpath is given in argument for its location:
			if (changeListLocationInTargetElem != null) {
				if (isChangeCandidate) {
					// strip root tag name from srcPath (include 2 slashes like in /Station/Code 
					srcPath = srcPath.substring(srcRootTag.length() + 2);
					
					addItemToChangeList(changeListLocationInTargetElem, srcPath, targetText, srcText, false);
				}
			}
			
		} // End of for-loop src XML iteration
		
		if (! newResultDocCreated) {
			Document resultDoc = (Document)targetDoc.clone();
			result.setDocument(resultDoc);
		}
		return result;
    }

    /**
     * Create a Talend Document from a xpath String. Builds a chain of empty elements. 
     * 
     * {talendTypes} Document
     * {Category} LCAG-XML Tools
	 *
     * {param} string xp:	Xpath to create elements from
     * {example}  xpathCreateDocument("/a/b") create <a><b/></a>
     * @throws DocumentException 
     */
	public static routines.system.Document xpathCreateTalendDocument(String xp) throws DocumentException {
		String NOT_ALLOWED = "*[]=.";
		
		Document doc = DocumentHelper.createDocument();
		Element e = null;
		
		for (int i=0; i < NOT_ALLOWED.length(); i++) {
			char c = NOT_ALLOWED.charAt(i);
			
			if (xp.indexOf(c) >= 0)
				throw new IllegalArgumentException(String.format("Xpath argument may not contain search character %c", c));
		}
			
		String[] tagList = xp.split("/");
		for (String tag : tagList) {
			if ("".equals(tag)) continue;
			
			e = (e == null) ? doc.addElement(tag) : e.addElement(tag);
		}

		return getTalendDocument(doc);
	}

	private static void _addElementAfter(Element newElem, Element srcElem) {
		Element targetParentElem = srcElem.getParent();
		String	targetParentXP = targetParentElem.getPath();
		List<Element> elements = targetParentElem.elements();
		
		// Check if the target parent is the same as the source parent:
		int idx = targetParentElem.indexOf(srcElem);
		
		// Add newNode directly before targetElem
		elements.add(idx, newElem);
	}

	private static boolean checkIfIgnoreByPathRegExList(String pathToCheck, List<String> excludeXpList) {
		boolean ignore = false;
		
		if (excludeXpList != null && ! excludeXpList.isEmpty()) {		
			for (String xp : excludeXpList) {
				if (xp.equals("")) continue;
				
				if (pathToCheck.endsWith(xp) || pathToCheck.startsWith(xp) || pathToCheck.matches(xp)) {
					log.trace("Excluded path %s matching regex %s", pathToCheck, xp);
					ignore = true;
					break;
				}
			}

		}
		return ignore;
	}
    
    /**
     * Updates empty values in 'aTargetXML' XML object from 'aSourceXML' object. 
     * 
     * @param aTargetXML	the XML to change. Supported types: Talend Document, dom4j Document or Node, XML String
     * @param aSourceXML	the XML to take source data from. Supported parameter types: Same as for 'aTargetXML'
     * @return Changed Talend Document
     * 
     * @throws DocumentException
     * 
     * {talendTypes} Document
     * 
     * {Category} LCAG-XML Tools
     * 
     * {param} Object aTargetXML: the XML to change. Supported types: Talend Document, dom4j Document or Node, XML String
     * {param} Object aSourceXML: the XML to take source data from. Supported parameter types: Same as for 'aTargetXML'
     */
    public static routines.system.Document enrichXMLValues(Object aTargetXML, Object aSourceXML) throws DocumentException
    {
    	return _xpathUpdate(aTargetXML, aSourceXML, EnumSet.of(UpdateMode.ENRICH), null, null);
    }
    
    /**
     * updateXML: 
     * Updates 'aTargetXML' XML object from 'aSourceXML' object if no text is available yet 'aTargetXML'. Updates values
     * and merges elements still missing into target. Modifies the aTargetXML if it is not a String or a file name.
     * Returns a true copy of the result.   
     * 
     * @throws DocumentException
     * 
     * {talendTypes} Document
     * {Category} LCAG-XML Tools
     * 
     * {param} object aTargetXML:	the XML to change. Supported types: Talend Document, dom4j Document or Node, XML String, XML File
     * {param} object aSourceXML:	the XML to take source data from. Supported parameter types: Same as for 'aTargetXML'
     * 
     */
    public static routines.system.Document updateXML(Object aTargetXML, final Object aSourceXML) throws DocumentException
    {
    	return _xpathUpdate(aTargetXML, aSourceXML, EnumSet.of(UpdateMode.UPDATE, UpdateMode.OPT_CREATE_TAGS), null, null);
    }
    
    /**
     * updateXml: 
     * Updates 'aTargetXML' XML object from 'aSourceXML' object if no text is available yet 'aTargetXML'. Updates values
     * and merges elements still missing into target if they are not expicitely excluded.
     * Modifies the aTargetXML if it is not a String or a file name. 
     * Returns a true copy of the result.   
     * 
     * @throws DocumentException
     * 
     * {talendTypes} Document
     * {Category} LCAG-XML Tools
     * 
     * {param} object aTargetXML:	the XML to change. Supported types: Talend Document, dom4j Document or Node, XML String, XML File
     * {param} object aSourceXML:	the XML to take source data from. Supported parameter types: Same as for 'aTargetXML'
     * {param} string excludeCSV:	list xpathes to exclude from source. Applies if the elemnt path starts with, ends with or matches
     * 								one list item. 
     * 
     */
    public static routines.system.Document updateXml(Object aTargetXML, final Object aSourceXML, String excludeCSV) throws DocumentException
    {
    	return _xpathUpdate(aTargetXML, aSourceXML, EnumSet.of(UpdateMode.UPDATE, UpdateMode.OPT_CREATE_TAGS), excludeCSV, null);
    }

    /**
     * updateXMLAndCreateChangeList: 
     * Updates 'aTargetXML' XML object from 'aSourceXML' object if no text is available yet 'aTargetXML'. Updates values
     * and merges elements still missing into target if they are not explicitly excluded. 
     * Creates a list of changes in the target after these were applied.
     * Modifies the aTargetXML if it is not a String or a file name. 
     * Returns a true copy of the result.   
     * 
     * {talendTypes} Document
     * {Category} LCAG-XML Tools
     * 
     * {param} object aTargetXML:	the XML to change. Supported types: Talend Document, dom4j Document or Node, XML String, XML File
     * {param} object aSourceXML:	the XML to take source data from. Supported parameter types: Same as for 'aTargetXML'
     * {param} string excludeCSV:	list xpathes to exclude from source. Applies if the elemnt path starts with, ends with or matches
     * 								one list item. 
     * {param} string("//Update") :	XPath in aTargetXML where to add the list of change Items holding path, newValue and oldValue
     * 								If the element contains a OperationType element, the list is placed before this tag, else it 
     * 								will be appended.  
     */
    public static routines.system.Document updateXMLAndCreateChangeList(Object aTargetXML, final Object aSourceXML, String excludeCSV, String changeListXP) throws DocumentException
    {
    	return _xpathUpdate(aTargetXML, aSourceXML, EnumSet.of(UpdateMode.UPDATE, UpdateMode.OPT_CREATE_TAGS), excludeCSV, "//Update");
    }
    /**
     * 
     * xpathReplaceByMD5:  Replace a node in aXML by a MD5 checksum value and return the original node as XML string. 
     * 					   Intended to simplify updates of MDM objects in the Talend MDM DB. use in tXMLMap or tMap components to flatten complex substructures.  The checksum 
     * 					   may be used later to re-inject the returned original XML string before sending the XML to the MDMOutput or Bulk-Load component.
     * 
     * {talendTypes} String
     * 
     * {Category} LCAG-XML Tools
     * 
     * {param} object aXML: 	  output: Talend or Dom4J xml document to be changed 
     * {param} object xp:   	  input: xpath lookup string 
     * @throws NoSuchAlgorithmException 
     * @throws DocumentException 
     * 
     */
    public static String xpathReplaceByMD5(Object aXML, String xp) throws NoSuchAlgorithmException, DocumentException
    {
    	Document 	doc = null;
    	Node		node;
    	String 		checkSum = null;
    	
    	doc = getDocument(aXML);

    	if (doc == null) return null;
    	
    	node = doc.selectSingleNode( xp );
    	
    	String xml = node.asXML();
    	
    	if (xml != null) {
    		checkSum = DataMasking.createMD5(xml);
    		
    		List<Node> nodes = node.selectNodes("./*");
    		
    		for (Node n: nodes) {
    			n.detach();
			} 
    	}
    	
    	node.setText(checkSum);
    	
    	return xml;
    }
    
    /**
     * Add a Talend Item (Element path, newValue, oldValue) to the change list.  
     * @param targetElem		- target element to added the Item. Adds Item before OperationType tag or at the end if not available.
     * @param path				- XPath of the cahnged field within the MDM item where the change was done
     * @param newVal			- new value as represented in the MDM after the change
     * @param oldVal			- value before the change was done
     * @param ignoreEmptyOrNull	- if true: do nothing if the content of the field was deleted 
     */
    private static void addItemToChangeList(Element targetElem, String path, String newVal, String oldVal, boolean ignoreEmptyOrNull) {
    	List<Element> elems = targetElem.elements();
    	Node insertBeforeElem = targetElem.selectSingleNode("OperationType");
    	int insertBeforePos = elems.indexOf(insertBeforeElem);
    	Element item;

    	// Check if an update for this update item was already done:
    	String xp = String.format("Item[path=\"%s\"]",  path);    	
    	item = (Element)targetElem.selectSingleNode(xp);
    	
    	if (item != null) {
    		throw new UnsupportedOperationException(String.format(
    				"Cannot update target: Already has an change Item for path %s, new/old value = %s/%s",
    				path, newVal, oldVal )
    				);
    	}
    	

    	if (oldVal == null)
    		oldVal = "";
    	
    	if (newVal == null)
    		newVal = "";

    	if (ignoreEmptyOrNull && StringUtil.isEmpty(newVal) && ! StringUtil.isEmpty(oldVal)) {
    		log.debug("Ignoring change on " + path + ": " + oldVal + "<=>" + newVal);
    		return;
    	}
    	
    	if (StringUtil.isEmpty(newVal) && StringUtil.isEmpty(oldVal)) {
    		return;
    	}
    	
    	log.debug("AddItem-Values Old/New: %s/%s (path: %s)", oldVal, newVal, path);
    	    	
    	if (insertBeforePos < 0) {
    		// Simply add a new item to target.
    		item = targetElem.addElement("Item");
    	} else {
    		// Insert int MDM update structure directly before the OperationType tag:
    		item = DocumentHelper.createElement("Item");
    		elems.add(insertBeforePos, item);
    	}
    			
		item.addElement("path").setText(path);
		// IMPORTANT: Next sequence of newValue/oldValue must comply to MDM_UpdateClass in DataModel !!
		//TODO: Change sequence in Data Model and here to become compatible to MDM internal Update report generated from MDM Web UI 
		item.addElement("newValue").setText(newVal);
		item.addElement("oldValue").setText(oldVal);
    }

    /**
     * Return a new XML document that is a copy of the given target where the XML subtree at the sourceXp XPath of the source XML object
     * is imported as a new sub-element at the  targetXp XPath of the target.
     * 
     * The function supports XML stringsm Talend documents, Dom4J documents or nodes as target and source XML objects.
     *  
     * @param target
     * @param targetXp
     * @param source
     * @param sourceXp
     * @return New Talend Document object or null if some argument is null or the sourceXp cannot be found
     * @throws DocumentException when the source or target cannot convert to a Document 
     * @throws {@link IllegalArgumentException} when no element is available at the targetXp
     */
    
    public static routines.system.Document importXMLSubtree(Object target, String targetXp, Object source, String sourceXp) throws DocumentException {
    	Document targetDoc = getDocument(target);
    	Document sourceDoc = getDocument(source);
    	
    	if (targetDoc == null || sourceDoc == null || targetXp == null || sourceXp == null)
    		return null;

    	Document resultDoc = (Document)targetDoc.clone();
    	
    	Element targetNode = (Element)resultDoc.selectSingleNode(targetXp);
    	
    	Object sourceObj = sourceDoc.selectSingleNode(sourceXp);
    	Element sourceNode = null;
    	
    	if (sourceObj instanceof Element) {
    		sourceNode = (Element)sourceObj;
    	} else if ( sourceObj instanceof Document) {
    		sourceNode = ((Document)sourceObj).getRootElement();
    	}
    	
    	if (sourceNode == null)
    		return null;
    	
    	if (targetNode == null)
    		throw new IllegalArgumentException("Target does not contain an element at path " + targetXp);
    	
    	Element sourceNodeCopy = (Element)sourceNode.clone();
    	
    	targetNode.add(sourceNodeCopy);
    	
    	routines.system.Document resultTalendDoc = getTalendDocument(resultDoc);

    	return resultTalendDoc;
    }
    
    /**
     * Compare all element values in inputXmlElem with those in existingXmlElem and add a XML subtree with tag Name Update into resultElem with all differences found.
     * Elements in inputXmlElem with relative path contained in excludeCSV are ignored.
     * The function returns a copy of the targetElement as a document with an added tag for the differences.
     * 
     * Prerequisite: targetElement does not yet contain any "Update/Item" tag. In such case the function assumes that a comparison
     * was already done and just reurn the target document itself without any modifications.
	 *
     * @param resultElem		- If not null it contains the XML with added change list as Item list with path, newValue and oldValue Tags in tag Update
     * 							  If null, the functions only the change list as result  
     * @param existingXmlElem	- usually an XML representing an entity in MDM DB or a MDM UI XML  
     * @param inputXmlElem		- The function compares elements starting at XPath xp to existingXmlElem. If xp is null, all element are compared 
     * @param excludeCSV		- Semicolon separated list of tag names to ignore when comparing  
     * @param xp				- XPath to start comparison
     * @return					- Returns a copy of resultElem
     */
    @Deprecated
    private static routines.system.Document __diffXML(Element resultElem, final Element existingXmlElem, final Element inputXmlElem, String excludeCSV, String xp) {
    	routines.system.Document result = null;
		Document resultDoc;
    	
		if (resultElem == null) {
    		// caller wants to have new document to show the differences => create the document
    		resultDoc = DocumentHelper.createDocument();
    		resultElem = resultDoc.addElement(DIFF_ROOT_NAME);
    	} 
		
    	if (inputXmlElem == null) {
    		// shall never happen    		
    		throw new IllegalArgumentException("inputXmlElem not set");
    	} else if (xp == null && ! resultElem.elements("Item").isEmpty()){
    		// Target already contains Items (e.g. it may come from the MDM Web UI.) => Just return the target as document
    		result = new routines.system.Document();
    		result.setDocument(resultElem.getDocument());
    	} else {
    		String existingTagName  = null;
    		String existingTagValue = null;
    		String inputTagName = inputXmlElem.getName();
    		
    		if (existingXmlElem != null) {
    			existingTagName  = existingXmlElem.getName();
	    		log.debug("Old/New: " + existingTagName + "/" + inputTagName);

    			// check if the current element shall be excluded from comparison:
	    		if (xp != null && excludeCSV != null) {
	    			List<String> tagNames = Arrays.asList(excludeCSV.split(";"));
	    			if (tagNames.contains(xp)) {
	    				log.debug("Omitting " + xp);
	    				return result;
	    			}
	    		}

	    		if (!existingTagName.equals(inputTagName)) {
	    			String p1 = existingXmlElem.getPath();
	    			String p2 = inputXmlElem.getPath();
	    			throw new IllegalArgumentException(String.format("Comparing incompatible XML tags (%s <=> %s)", p1, p2));
	    		}
	    		
	    		existingTagValue = existingXmlElem.getText().trim();
    		}
    		    		
    		String inputTagValue = inputXmlElem.getText().trim();
    		
    		// Compare the current element values:
    		if (existingTagValue == null || ! Objects.equals(existingTagValue,  inputTagValue)) {
	    		result = new routines.system.Document();
	    		
  	    		// set result to the document passed by caller (where the differences shall be put into):
   	    		resultDoc = resultElem.getDocument();
    	    	
	    		result.setDocument(resultDoc);

	    		// Append the difference found to the target element, but ignore if existing is filled and input is empty
    			addItemToChangeList(resultElem, xp, inputTagValue, existingTagValue, true);
    		}
    		
    		// Iterate through all object in inputXml
    		for (Object inputObj: inputXmlElem.elements()) {
        		Element inputElem = (Element)inputObj;
        		
        		inputTagName = inputElem.getName();
        		log.debug("Examining Tag " + inputTagName);
        		
        		Element existingElem = existingXmlElem == null ? null : existingXmlElem.element(inputTagName);
        		
        		routines.system.Document subResult;
        		String path = (xp == null) ? inputTagName : xp + "/" + inputTagName;
        		
        		if (existingElem == null) {
        			// Node has to be created empty in original/with empty sub nodes
        			existingElem = (Element)inputElem.clone();
        			existingElem.setText("");

        			for (Object existingObj: existingElem.elements()) {
        				((Element)existingObj).setText("");
        			}
        			// Add xpath and values are added to the target
        			// addItemToChangeList(resultElem, path, inputTagValue, existingTagValue, true /* ignoreEmty */);
        		}
        		
        		// recursively walk through entire subtree:
        		subResult = __diffXML(resultElem, existingElem, inputElem, excludeCSV, path);
        		
        		// prevent return null although a first difference was found earlier:
        		if (result == null)
        			result = subResult;
        	}
    	}

    	return result;
	}

    // 2021-02-01 NOTE: It looks like that the current implementation does not merge at all. It only creates Update Items 
    @Deprecated
    private static routines.system.Document _diffXML(Object resultXml, String resultInsertChangeListXp, 
    		final Object existingXml, final Object inputXml, String excludeCSV) throws DocumentException {
    	
		routines.system.Document result = null;
		
    	Document existingDoc = getDocument(existingXml);
    	Document inputDoc = getDocument(inputXml);
  
    	Document resultDoc = getDocument(resultXml);
    	
    	Element existingXmlElem = existingDoc == null ? null : existingDoc.getRootElement();
		Element inputXmlElem = inputDoc == null ? null : inputDoc.getRootElement();
		
    	
    	if (resultDoc != null) {   		
    		if (resultInsertChangeListXp == null) 
    			throw new IllegalArgumentException("resultXp xpath string may not be null when targetDoc is not null");
    		
    		org.dom4j.XPath insertXPath = resultDoc.createXPath(resultInsertChangeListXp);
    				
    		Element resultElem = (Element)insertXPath.selectSingleNode(resultDoc);
        	result = __diffXML(resultElem, existingXmlElem, inputXmlElem, excludeCSV, null /* start of XP recursion */);
    	} else {
    		// Create only change list (in Update tag as list of Item with oldValue/newValue)
    		// FIXME: 2021-02-01: Looks like this path is never used. It creates a null pointer exception when used
    		result = __diffXML(null /* to create only change list */, existingXmlElem, inputXmlElem, excludeCSV, null /* start of X recursion */);
    	}

    	return result;
    }
    
    @Deprecated
    public static routines.system.Document mergeXMLAndCreateChangeList(Object resultXml, String resultInsertChangeListXp, final Object inputXml, String excludeCSV) throws DocumentException 
    {
    	routines.system.Document  result = null;
		
    	result = _xpathUpdate(resultXml, inputXml, EnumSet.of(UpdateMode.UPDATE, UpdateMode.OPT_CREATE_TAGS),
    			excludeCSV, resultInsertChangeListXp);
		// result = _diffXML(resultXml, resultInsertChangeListXp, existingXml, inputXml, excludeCSV);
    	
    	return result;
    }
    
    /**
     * Compare all element values in inputXmlElem with those in existingXmlElem and add a XML subtree with tag Name Update into resultElem with all differences found.
     * Elements in inputXmlElem with relative path contained in excludeCSV are ignored.
     * The function returns a copy of the targetElement as a document with an added tag for the differences.
     * 
     * Note: Parameters of type Object allow the XML representations as String, File name, Talend Document, Dom4J Document or Dom4J Element.  
     * 
     * {talendTypes} Document
     *
     * {Category} LCAG-XML Tools
     *
     * {param} object resultXml: If not null it contains the XML with added Change List as Item list with path, newValue and oldValue Tags in tag Update
     * 							  If null, the functions only the change list as result  
     *
     * {param} string resultInsertChangeListXp: XPath to compare to and where to insert the Change List calculated by this function
     * 
     * {param} object existingXml: usually an XML representing an existing entity in MDM DB or a MDM UI XML
     *
     * {param} string excludeCSV:   CSV list of pathes to exclude from comparison  
	 *
     * {example} diffXMLAndCreateChangeList(resultXml, "exchange/report/Update", existingXml, "Id;LastupdateTimeStamp")
     */
    public static routines.system.Document diffXMLAndCreateChangeList(Object resultXml, String resultInsertChangeListXp, final Object existingXml, String excludeCSV) throws DocumentException 
    {
    	routines.system.Document  result = null;
		
		result = _xpathUpdate(resultXml, existingXml, EnumSet.of(UpdateMode.UPDATE, UpdateMode.DIFF_ONLY), excludeCSV, resultInsertChangeListXp);
    	
    	return result;
    }

    private static void _mergeInto(Node targetNode, final Node srcNode) throws DocumentException {
		Node targetRootNode = targetNode.selectSingleNode("/");
		Element srcElem = (Element)srcNode;
		
		String srcPath = srcNode.getPath();
		
		Element targetElem = (Element)targetRootNode.selectSingleNode("/" + srcPath);
		
		if (targetElem == null) {
			// look up src parent in target:
			Element 	  srcParentElement = srcNode.getParent();
			String parentXP = srcParentElement.getPath();
			
			srcElem = srcElem.createCopy();
			targetElem = (Element)targetRootNode.selectSingleNode("/" + parentXP);
			
			if (targetElem != null) {
				targetElem.add(srcElem);
			}
		} else {
			// element already exists in target: Update value only
			String srcText = srcElem.getText();
			
			targetElem.setText(srcText);
			List<Node> children = srcElem.selectNodes("*");
			
			for (Node child : children) {
				_mergeInto(targetElem, child);
			}
		}
	}
	
    /**
     * Merges the srcXML into targetXML and returns the result as a new Talend Document. 
     * - Overwrites test contents in existing target tags
     * - Creates new tags in target for those that are only available in srcXML provided that both have the same parent  
     * 
     * {talendTypes} Document
     * 
     * {Category} LCAG-XML Tools
     * 
     * {param} object(targetXML) targetXML : a String, a Document or a Node representing XML, where srcXML shall be merged into 
     * {param} object(srcXML)    srcXML : a String, a Document or a Node representing XML 
     */
	public static routines.system.Document mergeInto(final Object targetXML, final Object srcXML) throws DocumentException {
		routines.system.Document result;
		Document targetDoc = LcagXmlTools.getDocument(targetXML);
		Element srcElem =  LcagXmlTools.getElement(srcXML);
		
		Document tmpResult = (Document)targetDoc.clone();
		Element targetElem = tmpResult.getRootElement();
		
		_mergeInto(targetElem, srcElem);
		
		result = getTalendDocument(tmpResult);
		return result;
	}
	
    /**
     * Applies changes listed in a MDM WEB UI message in tags exchange/report/Update/Item to exchange/Item section and return a new result 
     * document. The section may differ when a job changed the Update section (e.g. the update/load jobs can apply changes of case letters.
     * @param aMDMMessage 
     * @return
     * @throws DocumentException 
     */
	@Deprecated
    public static routines.system.Document applyChangeXML(Object aMDMMessage) throws DocumentException {
    	routines.system.Document result = new routines.system.Document();
    	Document srcDoc = getDocument(aMDMMessage);

    	if (srcDoc == null) return null;
    	
    	Document resultDoc = (Document)srcDoc.clone();
    	List<?> srcItems = srcDoc.selectNodes("//exchange/Update/Item");
    	Element resultItem = (Element)resultDoc.selectSingleNode("//exchange/Item/*");
    	
    	for (Object srcItemNode: srcItems) {
    		Element srcItem = (Element)srcItemNode;
    		String path = srcItem.elementText("path");
    		
    		if (path == null) continue;
    		
    		Element resultElem = (Element)resultItem.selectSingleNode(path);
    		
    		String newValue = srcItem.elementText("newValue");

    		if (newValue == null && resultElem != null) {
    			// delete in element in target
    			resultElem.detach();
    		} else if (resultElem == null) {
    			// add item to result;
    			throw new UnsupportedOperationException("Creating new subelements is not yet supported");
    		} else { // exists in both:
    			String oldValue = resultElem.getText();
    		
    			if (! oldValue.equals(newValue)) {
    				log.debug(String.format("applyChangeXML: updating %s from %s => %s", path, oldValue, newValue));
    				
    				resultElem.setText(newValue);
    			}
    		}
    	}
    	
    	return result;
    }
    
    public static void removeNodes(org.w3c.dom.Node node) {
    	org.w3c.dom.NodeList list = node.getChildNodes();
        for (int i = 0; i < list.getLength(); i++) {
            removeNodes(list.item(i));
        }
        boolean emptyElement = node.getNodeType() == Node.ELEMENT_NODE
                && node.getChildNodes().getLength() == 0;
        boolean emptyText = node.getNodeType() == Node.TEXT_NODE
                && node.getNodeValue().trim().isEmpty();
        if (emptyElement || emptyText) {
        	 if (
                     !"UserName".equals(node.getNodeName())
                   &&!"Source".equals(node.getNodeName())
                   &&!"newValue".equals(node.getNodeName())
                   &&!"oldValue".equals(node.getNodeName())
                   &&!"OperationType".equals(node.getNodeName())
                   
                   
              )
              {
                   node.getParentNode().removeChild(node);
              } 
            //node.getParentNode().removeChild(node);
            //log.debug("  ");
        }
    }
    
    public static Element parseXml(String string){
    	  try {
    	    SAXReader reader=new SAXReader();

    	    log.debug("parsing " + string);
       	    Document doc=reader.read(new StringReader(string));

    	    Element root=doc.getRootElement();
    	    return root;
    	  }
    	 catch (  Exception e) {
    		log.error("Could not parse " + string);
    	    e.printStackTrace();
    	    return null;
    	  }
    	}

    /**
     * createDocumentFromTemplateFile: Creates a new Talend Document from a XML Template file. The function replaces variables in the file
     * donated by ${<varname>[#<format>]} by lookup of the <varname> in the map argument. For Date variables you may add a <format>.
     * The format has the same syntax as other Java Date formats.  If #milliseconds is given the Date is transformed to UNIX milliseconds format.
     * Note: Replaces only variables listen in the map argument. Others stay as text and are not replaced by empty string (to apply the function 
     * 		 on multiple maps using subsequent function calls. 
     * 
     * {talendTypes} Document
     * 
     * {Category} LCAG-XML Tools
     * 
     * {param} object(fileName) fileName : Name and path of file to read 
     * {param} Map(globalMap) map : Maps keys to value objects.  
     * {param} contextMap(context) contextMap : Talend context variables to substitute.  
     */
	public static routines.system.Document createDocumentFromTemplateFile(String fileName, Map<String, Object> map, Properties contextMap) throws DocumentException, IOException {
		String xml = LcagFileTools.readTextFile(fileName);
		
		xml = LcagStringTools.resolveVariables(xml, map);
		xml = LcagStringTools.resolveVariables(xml, contextMap);

		routines.system.Document result = LcagXmlTools.getTalendDocument(xml);
		
		return result;
	}
	
    /**
     * createDocumentFromTemplateFile: Creates a new Talend Document from a XML Template file. The function replaces variables in the file
     * donated by ${<varname>[#<format>]} by lookup of the <varname> in the map argument. For Date variables you may add a <format>.
     * The format has the same syntax as other Java Date formats.  If #milliseconds is given the Date is transformed to UNIX milliseconds format.
     * Note: Replaces only variables listen in the map argument. Others stay as text and are not replaced by empty string (to apply the function 
     * 		 on multiple maps using subsequent function calls. 
     * 
     * {talendTypes} Document
     * 
     * {Category} LCAG-XML Tools
     * 
     * {param} object(fileName) fileName : Name and path of file to read 
     * {param} Map(globalMap) map : Maps keys to value objects.  
     */
	public static routines.system.Document createDocumentFromTemplateFile(String fileName, Map<String, Object> map) throws DocumentException, IOException {
		String xml = LcagFileTools.readTextFile(fileName);
		
		xml = LcagStringTools.resolveVariables(xml, map);

		routines.system.Document result = LcagXmlTools.getTalendDocument(xml);
		
		return result;
	}
	
    /**
     * createDocumentFromTemplateFile: Creates a new Talend Document from a XML Template file. The function replaces variables in the file
     * donated by ${<varname>[#<format>]} by lookup of the <varname> in the map argument. For Date variables you may add a <format>.
     * The format has the same syntax as other Java Date formats.  If #milliseconds is given the Date is transformed to UNIX milliseconds format.
     * Note: Replaces only variables listen in the map argument. Others stay as text and are not replaced by empty string (to apply the function 
     * 		 on multiple maps using subsequent function calls. 
     * 
     * {talendTypes} Document
     * 
     * {Category} LCAG-XML Tools
     * 
     * {param} String(resourceDir) resourceDir : Resource directory where to lookup the XML template (usually context.env_resourceGlobalDirectory) 
     * {param} String(entityType) entityType: MDM Entity to create
     * {param} Map(globalMap) map : Maps keys to value objects.  
     * {param} contextMap(context) contextMap : Talend context variables to substitute.  
     */
	public static routines.system.Document createEntityFromTemplate(String resourceDir, String entityType, Map<String, Object> globalMap, Properties contextMap) throws DocumentException, IOException {
		String fileName = String.format("%s/Empty_MDM_%s.xml", resourceDir, entityType); 

		String xml = LcagFileTools.readTextFile(fileName);
		xml = LcagStringTools.resolveVariables(xml, globalMap);
		xml = LcagStringTools.resolveVariables(xml, contextMap);

		routines.system.Document result = LcagXmlTools.getTalendDocument(xml);
		
		return result;
	}
	
		/**
     * createDocumentFromTemplateFile: Creates a new Talend Document from a XML Template file. The function replaces variables in the file
     * donated by ${<varname>[#<format>]} by lookup of the <varname> in the map argument. For Date variables you may add a <format>.
     * The format has the same syntax as other Java Date formats.  If #milliseconds is given the Date is transformed to UNIX milliseconds format.
     * Note: Replaces only variables listen in the map argument. Others stay as text and are not replaced by empty string (to apply the function 
     * 		 on multiple maps using subsequent function calls. 
     * 
     * {talendTypes} Document
     * 
     * {Category} LCAG-XML Tools
     * 
     * {param} String(resourceDir) resourceDir : Resource directory where to lookup the XML template (usually context.env_resourceGlobalDirectory) 
     * {param} Map(globalMap) map : Maps keys to value objects.  
     * {param} contextMap(context) contextMap : Talend context variables to substitute.  
     */
	public static routines.system.Document createTalendUpdateReportForEntityFromTemplate(String resourceDir, Map<String, Object> globalMap, Properties contextMap) throws DocumentException, IOException {
		if (globalMap == null) throw new IllegalArgumentException("globalMap argument must not be null");

		String fileName = resourceDir + "/Empty_MDM_TalendUpdateReportUI.xml";
		String entityType = (String)globalMap.get("EntityType");

		if ( entityType == null && contextMap != null) {
			entityType = (String)contextMap.get("EntityType");
		} 
		
		if (entityType == null) 
			throw new IllegalArgumentException("globalMap or contextMap argument must contain EntityType key");

		String xml = LcagFileTools.readTextFile(fileName);
		xml = LcagStringTools.resolveVariables(xml, globalMap);
		xml = LcagStringTools.resolveVariables(xml, contextMap);

		routines.system.Document result = LcagXmlTools.getTalendDocument(xml);
		
		fileName = String.format("%s/Empty_MDM_%s.xml", resourceDir, entityType); 
		xml = LcagFileTools.readTextFile(fileName);
		xml = LcagStringTools.resolveVariables(xml, globalMap);
		xml = LcagStringTools.resolveVariables(xml, contextMap);

		Document entity = LcagXmlTools.getDocument(xml);
		result = LcagXmlTools.importXMLSubtree(result, "//item", entity, "/");
		
		return result;
	}
} 
