package routines;

import java.io.StringReader;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

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 LcagXmlTools {
	private static String DIFF_ROOT_NAME = "diff";
	public static SimpleDateFormat DATE_FORMAT_ISO8601 = new SimpleDateFormat("yyyy-MM-dd"); 
	
	public enum UpdateMode {
		ENRICH,
		UPDATE,
		ALLOW_DELETE
	}
	
	private static LcagLogger log = LcagLogger.getLogger(LcagXmlTools.class);
       
    /**
     * Returns a copy of the given document.
     * 
     * {talendTypes} Document
     * 
     * {Category} LCAG-XML Tools
     * 
     * {param} object(sourceXML) aXML : Document to copy 
     */
	public static routines.system.Document cloneDocument(routines.system.Document aXML) throws DocumentException {
		Document docCopy = (Document)aXML.getDocument().clone();
		
		routines.system.Document result = getTalendDocument(docCopy);
		
		return result;
	}
	
	/**
     * Returns a Dom4J 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 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;
    		
    		// 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) {
			}

    		SAXReader reader = new SAXReader();
    		StringReader stringReader = new StringReader(xml);
    		
    		log.debug("getDocument: XML to parse: " + xml);
    		doc = reader.read(stringReader);
    		
    		//String xml2 = doc.asXML();
    		//xml = xml;
    	}
		return doc;
	}

	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;
	}

	/**
     * 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;
	}
	
    /**
     * 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();
	}

	public static routines.system.Document createTalendDocument(String rootTag) throws DocumentException {		
		String xml = String.format("<%s></%s>", rootTag, rootTag);
		
		routines.system.Document result = getTalendDocument(xml);
		
		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} string(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} string(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);
    }

	/**
     * xpathRenameElement: Rename the name of a tag at a given xpath. 
     * Note: Does not copy the input aXML
     * 
     * @param aXML 	output: Talend or Dom4J or String xml document to be changed
     * @param xp	input: xpath lookup string for element to rename
     * @param newTageName:	input: new name of tag addressed by xpath
     * @return Document with renamed tag or null if not successful
     * @throws DocumentException 
	 *
     * {talendTypes} Document
     * {Category} LCAG-XML Tools
     * 
     * {param} Object(xmlObject) aXML: 	  output: Talend or Dom4J xml document to be changed 
     * {param} string(xpath) 	 xp:   	  input: xpath lookup string 
     * {param} string(newTageName) 	 newTageName:	input: new name of tag addressed by xpath 
     *  
     * {example} xpathRenameElement(doc, "//Update", "update")
     */
    public static routines.system.Document xpathRenameElement(Object aXML, String xp, String newTagName) throws DocumentException {
    	Document 	doc = null;
    	Node		node;
    	routines.system.Document result;
    	
		result = getTalendDocument(aXML);
		
    	if (result == null || xp == null || newTagName == null) return null;
    	
    	doc = result.getDocument();
    	if (doc == null) return null;

    	node = doc.selectSingleNode(xp);
    	
    	if (node == null) {
    		result = null;
    	} else {
    		node.setName(newTagName);
    	}
    	   	
    	return result;
    }

    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;
    }
    
    /**
     * 
     * xpathSetValue: 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} string(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;
    }
    
    /**
     * 
     * 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} string(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;
    }

    /**
     * xpathSetValueOrAddTag: Sets the value of the first element located at a xpath expression xp to a new value. 
	 *                Creates the Tag if it does not yet exist at the same position as within the reference XML.
     * 				  Returns the changed document if successful, else throws DocumentException 
     * 				  Version: since 3.4
     * 
     * @param aXML 	output: Talend or Dom4J or String xml document to be changed
     * @param xp	input: xpath lookup string
     * @param value input value to set
     * @param referenceXML	input: to create the tag if missing
     * @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} string(xpath) 	 xp:   	  input: xpath lookup string 
     * {param} Object(value)     value:      input: value to set (uses toString)
     * {param} Object(referenceXML) ref:  input: to create the tag if missing
     * 
     */
    public static routines.system.Document xpathSetValueOrAddTag(Object aXML, String xp, Object value, final Object referenceXML) throws DocumentException {
    	routines.system.Document result = LcagXmlTools.getTalendDocument(aXML);
    	
    	if (result == null) 
    		throw new IllegalArgumentException(String.format("Input document is null, cannot set %s to %s", xp, value));
    	
    	if (value == null) 
    		throw new IllegalArgumentException(String.format("Seting %s to null value is not supported", xp, value));
    	
    	String text = value.toString();
    	
    	org.dom4j.Document doc = result.getDocument();
    	
    	if (doc == null || xp == null) 
        	throw new IllegalArgumentException(String.format("Input Talend document content is empty, cannot set %s to %s", xp, value));
    		
		String pathList[] = xp.split("/");
		
		if (pathList.length < 2) 
        	throw new IllegalArgumentException(String.format("Xpath must indicate parent, cannot set %s to %s", xp, value));

		org.dom4j.Element inputElem = (org.dom4j.Element)doc.selectSingleNode( xp );

    	if (inputElem == null) {
    		org.dom4j.Document refDoc = LcagXmlTools.getDocument(referenceXML);
    			
    		
    		if (refDoc == null)
            	throw new IllegalArgumentException(String.format("Reference document is null, cannot set %s to %s", xp, value));

    		org.dom4j.Element refElem = (org.dom4j.Element)refDoc.selectSingleNode( xp );

    		if (refElem == null) 
            	throw new IllegalArgumentException(String.format("Reference document does not contain tag %s", xp));

    		String lookupParentTag = pathList[pathList.length - 2];
    		org.dom4j.Element refParentElem = refElem.getParent();
    		String refPath = "/" + refParentElem.getPath();

    		// Get Parent node in input document:
    		inputElem = (org.dom4j.Element)doc.selectSingleNode(refPath);
    		
    		if (inputElem == null) {
            	throw new IllegalArgumentException(String.format("Input document parent tag %s does does not exist at xpath %s", refParentElem.getName(), lookupParentTag));
    		} else if (! java.util.Objects.equals(lookupParentTag, refParentElem.getName())) {
            	throw new IllegalArgumentException(String.format("Reference document parent tag %s does not match parent tag in xpath %s", refParentElem.getName(), lookupParentTag));
    		} else if (! java.util.Objects.equals(lookupParentTag, inputElem.getName())) {
            	throw new IllegalArgumentException(String.format("Input document parent tag %s does not match parent tag in xpath %s", inputElem.getName(), lookupParentTag));
    		} else {
    			// Find  position to add in input document:
        		List<org.dom4j.Element> elems = refParentElem.elements();
        		
        		int position = elems.indexOf(refElem);

        		elems = inputElem.elements();
        		org.dom4j.Element newElem = org.dom4j.DocumentHelper.createElement(refElem.getName());
        		newElem.setText(text);
        		
        		elems.add(position, newElem); // adds after position
    		}
    		
    	} else {
    		inputElem.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} string(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 = 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;
    }
    
	/**
	 * Find leaf elements that have no text and return their xPath location
	 * @param aXml
	 * @return
	 * @throws DocumentException
	 */
	public static List<String> getEmptyLeafElementPathes(Object aXml) throws DocumentException {
		List<String>	result = new ArrayList<String>();
		Element elem = LcagXmlTools.getElement(aXml);
		
		String text = elem.getTextTrim();
		List<Element> elements = elem.elements();

		// Add path if 
		if ( elements.isEmpty() && "".equals(text) ) {
			result.add( elem.getPath() );
		}

		// Recurse through all sub elements:
		for (Element e : elements ) {
			List<String> childResults = getEmptyLeafElementPathes(e);
			result.addAll(childResults);
		}
		return result;	
	}
	
	/**
	 * Delete all leaf elements that have no text and return the result document
	 * @param aXml
	 * @return
	 * @throws DocumentException
	 */
	public static routines.system.Document deleteEmptyLeafElement(Object aXml)  throws DocumentException {
		org.dom4j.Document doc = LcagXmlTools.getDocument(aXml);
		List<String>	tagList = getEmptyLeafElementPathes(doc);
		
		routines.system.Document result = LcagXmlTools.xpathDeleteTags(doc, tagList);
		
		return result;
	}

	/**
     * xpathMerge:  Update content of 'aTargetXML' by the content inside a Node structure addressed sub-element named 'srcTagName'
     *    Elements listed in the sub-structure at 'srcTagName' but missing in 'aTargetXML' will not be created
     *    
     * NOTE: This function is not yet used and is deprecated. Use xpathEnrich or xpathUpdate instead.
     *
     * @param aTargetXML the XML to change. Supported types
     * @param srcTagName name of top-level XML element in aTargetXML to use as source for the update
     * @throws DocumentException 
	 *
     * {talendTypes} void
     * {Category} LCAG-XML Tools
     * 
     */
    @Deprecated
    public static void xpathMergeV1(Object aTargetXML, String srcTagName) throws DocumentException
    {
    	String targetXML = null;
    	Document 	srcDoc = null;
    	Document 	targetDoc = null;
    	Node		srcRootNode;
    	
    	targetDoc = getDocument(aTargetXML);
    	srcRootNode = targetDoc.selectSingleNode("//" + srcTagName);
    	srcDoc = getDocument(srcRootNode);
    	

    	if (srcDoc == null || targetDoc == null) return;
    	
    	String targetPath = targetDoc.getPath();
  		List<Node> targetNodes = targetDoc.selectNodes("/*//*");
    		
		// Replace all Elements in tartgetDoc by elements in srcSrc that match by the element name: 
		for (Node targetNode: targetNodes) {
			if (!(targetNode instanceof Element))
				continue;
			
			Element targetElem = (Element)targetNode;
			String name = targetNode.getName();
			String xp = "/*//" + name;
			
			if (name.equals(srcTagName)) {
				continue;
			}
			
			Node srcNode = srcDoc.selectSingleNode(xp); // Node in target to replace by node in srcDoc
			Element srcElem = (Element)srcNode;
			
			if (srcElem != null && "".equals(targetElem.getText() ) ) {
				Element newNode = srcElem.createCopy();
				Element targetParent = targetElem.getParent();
				
				targetElem.detach();
				targetParent.add(newNode);
			} 
			
		} 
		srcRootNode.detach();
    }
    
    @Deprecated
    public static boolean existsXPath(Object aXML, String xp) throws DocumentException {
    	
    	Document doc = getDocument(aXML);
    	
    	log.warn("Function is deprecated, use xpathHasElement istead");
    	if (doc == null)
    		return false;
    	
    	Node node = doc.selectSingleNode(xp);
    	
    	return (node != null);
    }

    /**
     * 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'  
     *
     * @param aXML The XML to evaluate. can be an XML object or a XML String
     * @param expressionString to use for evaluating the XML.
     * @throws DocumentException, IllegalArgumentException
	 *
     * {talendTypes} void
     * {Category} LCAG-XML Tools
     * 
     */
    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 = DATE_FORMAT_ISO8601.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
	 */
	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'
	 * 
	 */
	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'
     */
    private static routines.system.Document _xpathUpdate(Object aTargetXML, Object aSourceXML, UpdateMode mode) throws DocumentException
    {
    	Document 	srcDoc = null;
    	Document 	targetDoc = null;
    	
    	srcDoc = getDocument(aSourceXML);
    	targetDoc = getDocument(aTargetXML);
    	
    	String targetXML = targetDoc.asXML();
    	

    	if (srcDoc == null || targetDoc == null) return null;
    	
  		List<Node> targetNodes = targetDoc.selectNodes("//*");
    		
		// Replace all Elements in tartgetDoc by elements in srcSrc that match by the element name: 
		for (Node targetNode: targetNodes) {
			if (!(targetNode instanceof Element))
				continue;
			
			Element targetElem = (Element)targetNode;
			String name = targetNode.getName();
			String xp = "//" + name;
			
			
			Node srcNode = srcDoc.selectSingleNode(xp); // Node in target to replace by node in srcDoc
			
			if (srcNode == null)
				continue;
			
	    	String targetPath = targetElem.getPath();
	    	String srcPath    = srcNode.getPath();

	    	Element srcElem = (Element)srcNode;
					
			String srcText = srcElem.getTextTrim();
			String targetText = targetElem.getTextTrim();
			String targetXp = targetElem.getPath();

			// Check if element found in source is in the same subtree as in the target 
			if (! targetPath.endsWith(srcPath)) {
				// same name but different subtrees => ignore
				continue;
			}
			
			//TODO: May still not work correctly when replacing a non-leaf node in the target by a subtree from source
			if ( mode == UpdateMode.UPDATE || 
					("".equals(targetText) && ! "".equals(srcText) && (mode == UpdateMode.ENRICH || mode == UpdateMode.ALLOW_DELETE)) ) {
				Element newElem = srcElem.createCopy();
				Element targetParent = targetElem.getParent();
				List elements = targetParent.elements();
				
				int idx = targetParent.indexOf(targetElem);
				
				// Add newNode directly before targetElem
				elements.add(idx, newElem);
				// ... and delete the targetElem
				targetElem.detach();
			} else if ("".equals(srcText) && !"".equals(targetText) && mode == UpdateMode.ALLOW_DELETE) {
				targetElem.detach();
			}
		}
		
		routines.system.Document result = new routines.system.Document();
		result.setDocument(targetDoc);
		return result;
    }
    
    /**
     * Updates 'aTargetXML' XML object from 'aSourceXML' object if no text is available yet 'aTargetXML' 
     * 
     * @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 xpathEnrich(Object aTargetXML, Object aSourceXML) throws DocumentException
    {
    	return _xpathUpdate(aTargetXML, aSourceXML, UpdateMode.ENRICH);
    }
    
    /**
     * xpathUpdate: 
     * 	Updates 'aTargetXML' XML object from 'aSourceXML' object overwriting existing text in 'aTargetXML' if availble in 'aSourceXML' 
     * 
     * @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'
     * @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 xpathUpdate(Object aTargetXML, Object aSourceXML) throws DocumentException
    {
    	return _xpathUpdate(aTargetXML, aSourceXML, UpdateMode.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;
    }
    
    private static void addItem(Element targetElem, String path, String newVal, String oldVal) {
    	List elems = targetElem.elements();
    	Node insertBeforeElem = targetElem.selectSingleNode("OperationType");
    	int insertBeforePos = elems.indexOf(insertBeforeElem);
    	Element item;

    	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 
		if (newVal != null)
			item.addElement("newValue").setText(newVal);
		if (oldVal != null)
			item.addElement("oldValue").setText(oldVal);
    }
    
    private static void addItem(Element targetElem, String path, String newVal, String oldVal, boolean ignoreEmptyOrNull) {
    	List elems = targetElem.elements();
    	Node insertBeforeElem = targetElem.selectSingleNode("OperationType");
    	int insertBeforePos = elems.indexOf(insertBeforeElem);
    	Element item;

    	if (ignoreEmptyOrNull && "".equals(newVal) && !("".equals(oldVal))) {
    		log.debug("Ignoring change on " + path + ": " + oldVal + "<=>" + newVal);
    		return;
    	}
    	
    	if ("".equals(newVal) && "".equals(oldVal)) {
    		return;
    	}
    	
    	log.debug("AddItem-Values Old/New: " + oldVal + "/" + newVal);
    	
    	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 
		if (newVal != null)
			item.addElement("newValue").setText(newVal);
		if (oldVal != null)
			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 inputXmlNode with those in existingXmlNode and add a XML subtree into targetElement with all differences found.
     * Elements in inputXmlNode 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 targetElement
     * @param existingXmlNode
     * @param inputXmlNode
     * @param excludeCSV
     * @param xp
     * @return
     */
    public static routines.system.Document diffXMLNodesSICHERUNG20190604(Element targetElement, Element existingXmlNode, Element inputXmlNode, String excludeCSV, String xp) {
    	routines.system.Document result = null;
    	log.debug("diffXMLNodes(Element targetElement, Element existingXmlNode");
    	
    	if (existingXmlNode == null) {
    		// shall never happen
    		throw new IllegalArgumentException("existingXmlNode not set");
    	} else if (inputXmlNode == null) {
    		// shall never happen    		
    		throw new IllegalArgumentException("inputXmlNode not set");
    	} else if (xp == null && ! targetElement.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(targetElement.getDocument());
    	} else {
    		String existingTagName  = existingXmlNode.getName();
    		String inputTagName = inputXmlNode.getName();

    		// 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)) 
    				return result;
    		}
    		
    		if (!existingTagName.equals(inputTagName)) {
    			String p1 = existingXmlNode.getPath();
    			String p2 = inputXmlNode.getPath();
    			throw new IllegalArgumentException(String.format("Comparing incompatible XML tags (%s <=> %s)", p1, p2));
    		}
    		
    		String existingTagValue = existingXmlNode.getText().trim();
    		String inputTagValue = inputXmlNode.getText().trim();
    		
    		// Compare the current element values:
    		if (! Objects.equals(existingTagValue,  inputTagValue)) {
    			Document doc;
	    		result = new routines.system.Document();
	    		
	    		if (targetElement == null) {
    	    		// caller wants to have new document to show the differences => create the document
    	    		doc = DocumentHelper.createDocument();
    	    		
    	    		targetElement = doc.addElement(DIFF_ROOT_NAME);
    	    	} else {
    	    		// set result to the document passed by caller (where the differences shall be put into):
    	    		doc = targetElement.getDocument();
    	    	}
    	    	
	    		result.setDocument(doc);

	    		// Append the difference found to the target element, not ignoring empty content in newValue: 
    			//addItem(targetElement, xp, inputTagValue, existingTagValue);
    			addItem(targetElement, xp, inputTagValue, existingTagValue, false);
    		}

    		for (Object obj: existingXmlNode.elements()) {
        		Element e1 = (Element)obj;
        		
        		existingTagName = e1.getName();
        		
        		Element e2 = inputXmlNode.element(existingTagName);
        		
        		if (e2 != null) {
            		String path = (xp == null) ? existingTagName : xp + "/" + existingTagName;
            		
            		// recursively walk through entire subtree:
            		routines.system.Document subResult = diffXMLNodesSICHERUNG20190604(targetElement, e1, e2, excludeCSV, path);
            		
            		// prevent return null although a first difference was found earlier:
            		if (result == null)
            			result = subResult;
        		}
        	}    		
    	}
    	
    	return result;
    }
            
    public static routines.system.Document diffXMLNodesSICHERUNG20190604(Object targetDoc, String resultXp, Element existingXmlNode, Element inputXmlNode, String excludeCSV) throws DocumentException {
    	routines.system.Document  result = null;
    	log.debug("diffXMLNodes(Object targetDoc, String resultXp:"+resultXp);
    
    	
    	if (targetDoc != null) {
    		Document doc = getDocument(targetDoc);
    		
    		if (resultXp == null) 
    			throw new IllegalArgumentException("resultXp xpath string may not be null when targetDoc is not null");
    		
    		org.dom4j.XPath insertXPath = doc.createXPath(resultXp);
    				
    		Element insertIntoNode = (Element)insertXPath.selectSingleNode(doc);
        	result = diffXMLNodesSICHERUNG20190604(insertIntoNode, existingXmlNode, inputXmlNode, excludeCSV, null);
    	} else {
    		result = diffXMLNodesSICHERUNG20190604(null, existingXmlNode, inputXmlNode, excludeCSV, null);
    	}

    	return result;
    }
        
    public static routines.system.Document  diffXMLNodesSICHERUNG20190604(Element xmlNode1, Element xmlNode2) throws DocumentException {
    	return diffXMLNodesSICHERUNG20190604(null, null, xmlNode1, xmlNode2, null);
    }
    
    public static routines.system.Document diffXMLSICHERUNG20190604(Object existingXML, Object inputXML, String excludeCSV, Object resultXML, String resultXp) throws DocumentException 
    { 
    	routines.system.Document result = null; 
    	Document existingDoc = getDocument(existingXML); 
    	Document inputDoc = getDocument(inputXML);
    	log.debug("resultXp:"+resultXp);
    	
    	
        Element existingRootElement = existingDoc == null ? null : existingDoc.getRootElement(); 
        Element inputRootElement = inputDoc == null ? null : inputDoc.getRootElement();
        log.debug("existingRootElement:"+existingRootElement.toString());
        
        Object resultDoc = (resultXML instanceof routines.system.Document) ? resultXML : getDocument(resultXML);

        result = diffXMLNodesSICHERUNG20190604(resultDoc, resultXp, existingRootElement, inputRootElement, excludeCSV);

        return result; 
     }
    
    public static routines.system.Document diffXMLSICHERUNG20190603(Object existingXML, Object inputXML, String excludeCSV, Object resultXML, String resultXp) throws DocumentException 
    {
    	routines.system.Document  result = null;
    	Document existingDoc = getDocument(existingXML);
    	Document inputDoc = getDocument(inputXML);
  
    	Object resultDoc = (resultXML instanceof routines.system.Document) ? resultXML : getDocument(resultXML);
    	
    	result = diffXMLNodesSICHERUNG20190604(resultDoc, resultXp, existingDoc.getRootElement(), inputDoc.getRootElement(), excludeCSV);
    	
    	return result;
    }
    
    /**
     * Compare all element values in existingXmlNode with those in inputXmlNode and add a XML subtree into targetElement with all differences found.
     * Elements in inputXmlNode with relative path contained in excludeCSV are ignored. 
     * Also items which are set in existing and empty in input are ignored.
     * The function returns a copy of the targetElement as a document with an added tag for the differences.
     * 
     * 2021-02-01 NOTE: It looks like that the current implementation does not merge at all. It only creates Update Items 
     * 
     * 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 targetElement
     * @param existingXmlNode
     * @param inputXmlNode
     * @param excludeCSV
     * @param xp
     * @return
     */
    public static routines.system.Document mergeAndUpdateXML(Element targetElement, Element existingXmlNode, Element inputXmlNode, String excludeCSV, String xp) {
    	routines.system.Document result = null;
    	
    	if (inputXmlNode == null) {
    		// shall never happen    		
    		throw new IllegalArgumentException("inputXmlNode not set");
    	} else if (xp == null && ! targetElement.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(targetElement.getDocument());
    	} else {
    		String existingTagName  = null;
    		String existingTagValue = null;
    		String inputTagName = inputXmlNode.getName();
    		
    		if (existingXmlNode != null) {
    			existingTagName  = existingXmlNode.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 = existingXmlNode.getPath();
	    			String p2 = inputXmlNode.getPath();
	    			throw new IllegalArgumentException(String.format("Comparing incompatible XML tags (%s <=> %s)", p1, p2));
	    		}
	    		
	    		existingTagValue = existingXmlNode.getText().trim();
    		}
    		    		
    		String inputTagValue = inputXmlNode.getText().trim();
    		
    		// Compare the current element values:
    		if (existingTagValue == null || ! Objects.equals(existingTagValue,  inputTagValue)) {
    			Document doc;
	    		result = new routines.system.Document();
	    		
	    		if (targetElement == null) {
    	    		// caller wants to have new document to show the differences => create the document
    	    		doc = DocumentHelper.createDocument();
    	    		
    	    		targetElement = doc.addElement(DIFF_ROOT_NAME);
    	    	} else {
    	    		// set result to the document passed by caller (where the differences shall be put into):
    	    		doc = targetElement.getDocument();
    	    	}
    	    	
	    		result.setDocument(doc);

	    		// Append the difference found to the target element, but ignore if existing is filled and input is empty
    			addItem(targetElement, xp, inputTagValue, existingTagValue, true);
    		}
    		
    		for (Object obj: inputXmlNode.elements()) {
        		Element e1 = (Element)obj;
        		
        		inputTagName = e1.getName();
        		log.debug("Examining Tag " + inputTagName);
        		
        		Element e2 = existingXmlNode == null ? null : existingXmlNode.element(inputTagName);
        		
        		routines.system.Document subResult;
        		String path = (xp == null) ? inputTagName : xp + "/" + inputTagName;
        		
        		if (e2 == null) {
        			// Node has to be created empty in original/with empty sub nodes
        			// at the same time xpath and values are added to the target
        			e2 = (Element)e1.clone();
        			e2.setText("");

        			for (Object o: e2.elements()) {
        				((Element)o).setText("");
        			}
        			//addItem(targetElement, path, inputTagValue, existingTagValue, true);
        		}
        		
        		// recursively walk through entire subtree:
        		subResult = mergeAndUpdateXML(targetElement, e2, e1, 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 
    public static routines.system.Document mergeAndUpdateXML(Object targetDoc, String resultXp, Element existingXmlNode, Element newXmlNode, String excludeCSV) throws DocumentException {
		routines.system.Document result = null;
    	
    	if (targetDoc != null) {
    		Document doc = getDocument(targetDoc);
    		
    		if (resultXp == null) 
    			throw new IllegalArgumentException("resultXp xpath string may not be null when targetDoc is not null");
    		
    		org.dom4j.XPath insertXPath = doc.createXPath(resultXp);
    				
    		Element insertIntoNode = (Element)insertXPath.selectSingleNode(doc);
        	result = mergeAndUpdateXML(insertIntoNode, existingXmlNode, newXmlNode, excludeCSV, null);
    	} else {
    		// FIXME: 2021-02-01: Looks like this path is never used. It creates a null pointer exception when used
    		result = mergeAndUpdateXML(null, existingXmlNode, newXmlNode, excludeCSV, null);
    	}

    	return result;
    }
    
    // 2021-02-01 NOTE: It looks like that the current implementation does not merge at all. It only creates Update Items 
    public static routines.system.Document mergeAndUpdateXML(Object existingXML, Object inputXML, String excludeCSV, Object resultXML, String resultXp) throws DocumentException 
    {
    	routines.system.Document  result = null;
    	Document existingDoc = getDocument(existingXML);
    	Document inputDoc = getDocument(inputXML);
  
    	Object resultDoc = (resultXML instanceof routines.system.Document) ? resultXML : getDocument(resultXML);
    	
    	Element existingRootElement = existingDoc == null ? null : existingDoc.getRootElement();
		Element inputRootElement = inputDoc == null ? null : inputDoc.getRootElement();
		
		result = mergeAndUpdateXML(resultDoc, resultXp, existingRootElement, inputRootElement, excludeCSV);
    	
    	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;
	}
	
    public static routines.system.Document diffXML(Object existingXML, Object inputXML) throws DocumentException 
    {
    	return diffXMLSICHERUNG20190604(existingXML, inputXML, null, null, null);
    }
    
    /**
     * 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 
     */
    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;
    	  }
    	}
} 
