package routines;

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

import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.Namespace;
import org.dom4j.Node;
import org.dom4j.VisitorSupport;
import org.dom4j.XPath;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;
import org.dom4j.tree.DefaultElement;

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

	private static final class NameSpaceSetter extends VisitorSupport {
		Namespace newNamespace;

		public NameSpaceSetter() {
			this.newNamespace = Namespace.NO_NAMESPACE;
		}

		public NameSpaceSetter(Namespace aNs) {
			this.newNamespace = aNs;
		}

		public void visit(Document document) {
			DefaultElement elem = (DefaultElement) document.getRootElement();
			elem.setNamespace(this.newNamespace);
			document.getRootElement().additionalNamespaces().clear();
		}

		public void visit(Namespace namespace) {
			namespace.detach();
		}

		public void visit(Attribute node) {
			if (node.toString().contains("xmlns") || node.toString().contains("xsi:")) {
				node.detach();
			}
		}

		public void visit(Element node) {
			if (node instanceof DefaultElement) {
				DefaultElement aNode = (DefaultElement) node;

				aNode.setNamespace(this.newNamespace);
			}
		}
	}

	/**
	 * Replace root namespace prefix in entire document
	 * 
	 * @FIXME: Does not work at all! (must use accept() method to run the visitor!)
	 * @param aDoc
	 * @param aNs
	 * @return
	 */
	public static Namespace setNamespace(Document aDoc, Namespace aNs) {
		Element rootElement = aDoc.getRootElement();
		Namespace result = rootElement.getNamespace();

		NameSpaceSetter setter = new NameSpaceSetter(aNs);

		return result;
	}

	/*
	 * Replace root namespace prefix in entire document s
	 */
	public static Document switchNamespaceTo(Document srcDoc, String newPrefix) {
		Element rootElem = srcDoc.getRootElement();
		Namespace ns = rootElem.getNamespace();

		String oldPrefix = ns.getPrefix();
		String xml = srcDoc.asXML();

		String oldText = String.format("schema xmlns:%s=", oldPrefix);
		String newText = String.format("schema xmlns:%s=", newPrefix);

		xml = xml.replaceFirst(oldText, newText);

		oldText = oldPrefix + ":";
		newText = newPrefix + ":";
		xml = xml.replaceAll(oldText, newText);

		Document result = null;

		try {
			result = getDocument(xml);
		} catch (DocumentException e) {
			e.printStackTrace();
		}

		return result;
	}

	public static Namespace removeNamespace(Document aDoc) {
		Element rootElement = aDoc.getRootElement();
		Namespace result = rootElement.getNamespace();

		NameSpaceSetter setter = new NameSpaceSetter();

		return result;
	}

	public static void takeoverNamespaces(Element aTarget, Element aSrc) {
		List<Namespace> srcNamespaces = aSrc.additionalNamespaces();
		List<Namespace> targetNamespaces = aTarget.additionalNamespaces();
		List<String> srcNsPrefixes = new ArrayList<String>();

		// Add missing namespaces from aSrc:
		for (Namespace ns : srcNamespaces) {
			String prefix = ns.getPrefix();
			String uri = ns.getURI();

			srcNsPrefixes.add(prefix);

			if (aTarget.getNamespaceForPrefix(prefix) == null) {
				aTarget.addNamespace(prefix, uri);
			}
		}

		// Remove namespaces not in aSrc from aTarget:
		for (Namespace ns : targetNamespaces) {
			String prefix = ns.getPrefix();

			if (!srcNsPrefixes.contains(prefix)) {
				ns.detach();
			}
		}
	}

	/**
	 * Returns a copy of the given document.
	 * 
	 * {talendTypes} Document
	 * 
	 * {Category} LCAG-XML Tools
	 * 
	 * {param} object(sourceXML) aXML : Document to copy
	 */
	public static Document cloneDocument(Document aXML) throws DocumentException {
		Document docCopy = (Document) aXML.getDocument().clone();

		return docCopy;
	}

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

			Set<Namespace> nsList = getAllNamespaces(elem);

			for (Namespace ns : nsList) {
				String prefix = ns.getPrefix();

				if (newElem.getNamespaceForPrefix(prefix) == null) {
					newElem.addNamespace(prefix, ns.getURI());
				}
			}

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

			doc = reader.read(stringReader);

			// String xml2 = doc.asXML();
			// xml = xml;
		}
		return doc;
	}

	private static Set<Namespace> getAllNamespaces(Element aElem) {
		Set<Namespace> result = new HashSet<Namespace>();

		if (aElem != null) {
			Element parent = aElem.getParent();

			Set<Namespace> parentList = getAllNamespaces(parent);

			for (Node n : aElem.content()) {
				if (n instanceof Namespace) {
					Namespace aNamespace = (Namespace) n;

					result.add(aNamespace);
				}
			}

			result.addAll(parentList);
		}

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

	/**
	 * Return a list of all "name" attribute values starting from document root to
	 * the element argument. Created to find out if an Element is a MDM Entity in
	 * EBX xsd schema (name at poistion in list must match) Other usage options:
	 * Determine depth of a element (= length of returned list)
	 * 
	 * @param elem
	 * @return
	 */
	public static List<String> getElementNamesInPath(Element elem) {
		List<String> nameList = new ArrayList<>();

		for (Element e = elem; e != null; e = e.getParent()) {
			Attribute attr = e.attribute("name");
			String name = (attr == null) ? null : attr.getValue();

			nameList.add(name);
		}

		Collections.reverse(nameList);

		return nameList;
	}

	/**
	 * 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 Document createDocument(String rootTag) throws DocumentException {
		String xml = String.format("<%s></%s>", rootTag, rootTag);

		Document result = getDocument(xml);

		return result;

	}

	public static String asXmlTag(String pTagName, Object pValue) {
		String result = String.format("<%1$s>%2$s</%1$s>\n", pTagName, pValue.toString());

		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 Document xpathGetAsDoc(Object aXML, String xp) throws DocumentException {
		Document doc = null;
		Node node;
		Document result = null;

		doc = getDocument(aXML);

		if (doc == null || xp == null)
			return result;

		node = doc.selectSingleNode(xp);

		if (node != null) {
			result = getDocument(node);
		}

		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 {
		Node node;
		String contentText = null;

		Document doc = getDocument(aXML);

		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 Document xpathRenameElement(Object aXML, String xp, String newTagName) throws DocumentException {
		Document doc = null;
		Node node;
		Document result;

		result = getDocument(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;

		Document result = getDocument(aXML);

		doc = result;

		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 {
		Node node;
		String contentText = null;

		if (aXML instanceof Node) {
			node = (Node) aXML;
		} else {
			Document doc = getDocument(aXML);
			node = doc.getRootElement();
		}

		if (node == null || xp == null)
			return null;

		node = node.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 Document xpathSetValue(Object aXML, String xp, Object... valueList) throws DocumentException {
		Document doc = null;
		Node node;
		String text = null;

		Document result = getDocument(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 Document xpathSetValueOrAddTag(Object aXML, String xp, Object value, final Object referenceXML)
			throws DocumentException {
		Document result = LcagXmlTools.getDocument(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;
	}

	/**
	 * Like selectSingeNode of dom4j but fixes a bug: BUG Fixed: The library
	 * function does not find existing elements if the xpath has a condition on an
	 * attribute with a namespace, like
	 * xsd:annotation/xsd:documentation[@xml:lang="en-US"] ALTHOUGH this works fine
	 * in TestCase LcagXmlTest !!
	 * 
	 * @param aNode - node to start search
	 * @param xp    - xpath to select
	 * @return
	 */
	public static Element selectSingleElement(Node aNode, String xp, boolean doDebug) {
		Element aElem = (Element) aNode;
		Namespace ns = aElem.getNamespace();

		XPath xpath = aElem.createXPath(xp);
		Element result = (Element) xpath.selectSingleNode(aElem);

		if (false && result == null) {
			// FIXME: This fix does not work yet! Therefore disabled
			// use own implementation to verify if Dom4j bug applies
			Map<String, String> attrList = new HashMap<String, String>();
			String xpUnConditional = xpathGetConditionsOnAttributes(xp, attrList);
			aNode.asXML();
			List<Element> matchingElements = new ArrayList<Element>();
			List<Node> nodeList = aNode.selectNodes(xpUnConditional);

			for (Node n : nodeList) {
				Element elem = (Element) n;
				boolean allCondOk = true;

				for (String wantedKey : attrList.keySet()) {
					String wantedValue = attrList.get(wantedKey);
					String elemValue = elem.attributeValue(wantedKey);

					if (!wantedValue.equals(elemValue)) {
						allCondOk = false;
						break;
					}
				}

				if (allCondOk) {
					matchingElements.add(elem);
				}
			}

			if (nodeList.isEmpty() && doDebug) {
				// FIXME: Code to debug the problem
				String[] tagList = xpUnConditional.split("/");
				boolean isAbsolute = "".equals(tagList[0]);
				Node prevNode = null;

				String newXp = isAbsolute ? "/" : "";

				for (int i = 0; i < tagList.length; i++) {
					String t = tagList[i];

					newXp += t;

					if (prevNode != null) {
						nodeList = prevNode.selectNodes(t);
					}
					nodeList = aNode.selectNodes(newXp);

					int len = nodeList.size();
					if (len == 1) {
						prevNode = nodeList.get(0);
					}

					if (!isAbsolute || i > 0) {
						newXp += "/";
					}
				}
			}

			if (matchingElements.size() == 1) {
				result = matchingElements.get(0);
			}
		}

		return result;
	}

	public static Element selectSingleElement(Node aNode, String xp) {
		return selectSingleElement(aNode, xp, false);
	}

	public static Element selectSingleElement(Document aDoc, String xp) {
		return selectSingleElement(aDoc.getRootElement(), xp, false);
	}

	public static List<Node> selectNodes(Node aNode, String xp) {
		Element aElem = (Element) aNode;

		XPath xpath = aElem.createXPath(xp);
		List<Node> result = xpath.selectNodes(aElem);

		return result;
	}

	/**
	 * Parse the xpath in xp for conditions return values to the attrMap 1) the xp
	 * without the conditions 2) if mode is ON_ATTRIBUTE: add all attribute names
	 * and values found ON_TAG: add all tag names and values found ALL: combines
	 * both modes above. In this case attributes name start with a '@'
	 * 
	 * @param xp
	 * @param attrList - contains added key/values pairs found in condition
	 * @return
	 */
	private enum ModeType {
		ON_ATTRIBUTE, ON_TAG, ALL
	};

	private static String _xpathGetConditions(String xp, Map<String, String> attrMap, ModeType mode) {
		String[] tagList = xp.split("/");
		String lastTag = tagList[tagList.length - 1];

		int idx = lastTag.indexOf("[");

		if (idx >= 0) {
			// Create tag/attribute value from condition
			int len = lastTag.length();

			String condition = lastTag.substring(idx + 1, len - 1);
			String[] varValuePairs = condition.split("=");

			lastTag = lastTag.substring(0, idx);
			tagList[tagList.length - 1] = lastTag;
			xp = String.join("/", tagList);

			if (varValuePairs.length > 1) {
				String var = varValuePairs[0];
				int textLen = varValuePairs[1].length();

				String text = varValuePairs[1].substring(1, textLen - 1); // drop enclosing quotes

				if (var.startsWith("@") && mode == ModeType.ON_ATTRIBUTE) {
					var = var.substring(1);
					attrMap.put(var, text);
				} else if (mode == ModeType.ON_TAG || mode == ModeType.ALL) {
					attrMap.put(var, text);
				}
			}

		}
		return xp;
	}

	/**
	 * Parse the xpath in xp for conditions on attributes and return 1) the xp
	 * without the conditions 2) add all attributes found and their values to the
	 * attrList
	 * 
	 * @param xp
	 * @param attrMap - contains added key/values pairs found in condition
	 * @return
	 */
	private static String xpathGetConditionsOnAttributes(String xp, Map<String, String> attrMap) {
		return _xpathGetConditions(xp, attrMap, ModeType.ON_ATTRIBUTE);
	}

	/**
	 * Parse the xpath in xp for conditions on tag content and return 1) the xp
	 * without the conditions 2) add all tags found and their values to the attrList
	 * 
	 * @param xp
	 * @param attrMap - contains added key/values pairs found in condition
	 * @return
	 */
	private static String xpathGetConditionsOnTags(String xp, Map<String, String> attrMap) {
		return _xpathGetConditions(xp, attrMap, ModeType.ON_TAG);
	}

	/**
	 * Parse the xpath in xp for conditions on tags and attributes and return 1) the
	 * xp without the conditions 2) add all tags and attribute names found and their
	 * values to the attrList
	 * 
	 * @param xp
	 * @param attrMap - contains added key/values pairs found in condition
	 * @return
	 */
	private static String xpathGetConditions(String xp, Map<String, String> attrMap) {
		return _xpathGetConditions(xp, attrMap, ModeType.ALL);
	}

	/**
	 * Run a selectSingeNode of dom4j ignoring namespaces listed in the xp Note:
	 * Method not yet used because if a namespace is not yet registered in the
	 * document the document could not been read by getDocument operation before.
	 * 
	 * @param aNode
	 * @param xp    - xpath to select
	 * @return
	 */
	public static Element selectSingleElementIgnoreNS(Element aNode, String xp) {
		Element result = null;

		Namespace ns = aNode.getDocument().getRootElement().getNamespace();

		String[] list = xp.split(":");

		if (list.length > 1) { // namespaces found in xp

			for (String s : list) {
				// extract namespace name and register it so that next selectSingleNode
				// operation will not fail:
				int n = s.lastIndexOf('@'); // namespace in attribute name

				if (n < 0)
					n = s.lastIndexOf('/'); // namespace in tag name

				if (n >= 0) {
					String nsString = s.substring(n + 1);

					aNode.addNamespace(nsString, "");
				}
			}
		}
		result = (Element) aNode.selectSingleNode(xp);

		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 Element _xpathAddTag(Element toElem, String xp, Object value) throws DocumentException {
		Element node = null;
		Element parent = null;
		Element result = null;

		if (toElem == 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 = LcagXmlTools.selectSingleElement(toElem, xp);

			if (node != null) {
				if (value instanceof Element) {
					Element newValue = ((Element) value).createCopy();

					node.add(newValue);
					result = newValue;
				} else {
					String text = value.toString();

					node.setText(text);
					result = node;
				}
			} 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);

					parent = _xpathAddTag(toElem, parentXp, "");

					if (parent != null) {
						if (value instanceof Element) {
							// Add structured element to parent:
							Element newValue = ((Element) value).createCopy();

							parent.add(newValue);
							result = newValue;
						} else {
							// Add new tag and set value content:
							String text = value.toString();

							// Create tag/attribute value from condition
							Map<String, String> conditionMap = new HashMap<String, String>();

							String tagName = xpathGetConditions(tag, conditionMap);

							node = parent.addElement(tagName);
							node.setText(text);
							result = node;

							for (String var : conditionMap.keySet()) {
								String varValue = conditionMap.get(var);

								if (var.startsWith("@")) {
									var = var.substring(1);

									node.addAttribute(var, varValue);
								} else {
									Element subElem = node.addElement(var);
									subElem.setText(varValue);
								}
							}
						} // Add new tag
					}
				} else { // has no parent, add it:
					Element elem = DocumentHelper.createElement(xp);
					toElem.add(elem);

					result = elem;
				}
			}
		} catch (Exception e) {
			String path = "parent not found";
			String xml = "No parent to insert";

			if (parent != null) {
				path = parent.getPath();
				xml = parent.asXML();
			}

			throw new DocumentException(
					String.format("Parent path: %s: Exception in XML processing: %s\n%s", path, e.getMessage(), xml));
		}

		return result;
	}

	public static Element xpathAddTag(Node aXML, String xp, Object value) throws DocumentException {
		if (aXML == null)
			throw new IllegalArgumentException("Argument may not be null");

		Element rootElem = ((Element) aXML);
		Element result = _xpathAddTag(rootElem, xp, value);

		return result;
	}

	public static Document xpathAddTag(Document aXML, String xp, Object value) throws DocumentException {
		if (aXML == null)
			throw new IllegalArgumentException("Argument may not be null");

		Element rootElem = aXML.getRootElement();
		_xpathAddTag(rootElem, xp, value);

		return aXML;
	}

	public static Document xpathAddTag(Object aXML, String xp, Object value) throws DocumentException {
		Document doc = getDocument(aXML);

		Element rootElem = doc.getRootElement();
		_xpathAddTag(rootElem, xp, value);

		return doc;
	}

	public static void xpathDeleteAllTagOccurrences(Element fromElement, String xp) throws DocumentException {
		if (fromElement == null || xp == null)
			return;

		for (Node node : fromElement.selectNodes(xp)) {
			node.detach();
		}
	}

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

		Document result = getDocument(aXML);

		doc = result;

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

		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 Document deleteEmptyLeafElement(Object aXml) throws DocumentException {
		Document doc = LcagXmlTools.getDocument(aXml);
		List<String> tagList = getEmptyLeafElementPathes(doc);

		Document result = LcagXmlTools.xpathDeleteTags(doc, 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'
	 *
	 * @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 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);
		}

		return resultDoc;

	}

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

		return targetDoc;
	}

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

		return resultDoc;
	}

	/**
	 * Import a xml element by appecding it to an element in the given target
	 * 
	 * @param target
	 * @param targetXp
	 * @param source
	 * @param sourceXp
	 * @return Element added to the 'target'
	 * @throws DocumentException when the source cannot convert to a Document
	 * @throws {@link            IllegalArgumentException} when no element is
	 *                           available at the targetXp
	 */
	public static Element appendXMLSubtree(Element target, String targetXp, Object source, String sourceXp)
			throws DocumentException {
		Document sourceDoc = getDocument(source);

		if (target == null || sourceDoc == null || targetXp == null || sourceXp == null)
			return null;

		Element targetNode = (Element) target.selectSingleNode(targetXp);
		Element sourceElem = sourceDoc.getRootElement();

		Object sourceObj = sourceElem.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 = sourceNode.createCopy();

		targetNode.add(sourceNodeCopy);

		return sourceNodeCopy;
	}

	/**
	 * 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 Document mergeAndUpdateXML(Element targetElement, Element existingXmlNode, Element inputXmlNode,
			String excludeCSV, String xp) {
		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 = DocumentHelper.createDocument();
			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 = DocumentHelper.createDocument();

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

				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 Document mergeAndUpdateXML(Object targetDoc, String resultXp, Element existingXmlNode,
			Element newXmlNode, String excludeCSV) throws DocumentException {
		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 Document mergeAndUpdateXML(Object existingXML, Object inputXML, String excludeCSV, Object resultXML,
			String resultXp) throws DocumentException {
		Document result = null;
		Document existingDoc = getDocument(existingXML);
		Document inputDoc = getDocument(inputXML);

		Object resultDoc = 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 Document mergeInto(final Object targetXML, final Object srcXML) throws DocumentException {
		Document targetDoc = LcagXmlTools.getDocument(targetXML);
		Element srcElem = LcagXmlTools.getElement(srcXML);

		Document result = (Document) targetDoc.clone();
		Element targetElem = result.getRootElement();

		_mergeInto(targetElem, srcElem);

		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
	 */
	public static Document applyChangeXML(Object aMDMMessage) throws DocumentException {
		Document result = DocumentHelper.createDocument();
		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;
		}
	}

	/**
	 * Print XML documentung as pretty formatted XML String
	 * 
	 * @param doc - Document to format to Strings
	 * @return
	 */
	public static String prettyXML(Document doc) {
		String result = null;

		try {
			StringWriter sw;

			OutputFormat format = OutputFormat.createPrettyPrint();
			sw = new StringWriter();
			XMLWriter writer = new XMLWriter(sw, format);

			writer.write(doc);
			result = sw.toString();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		return result;
	}

}
