/*
 *   Copyright 2013 Brian Rosenberger (Brutex Network)
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */

package net.brutex.xservices.ws.impl;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.util.Iterator;
import java.util.List;
import javax.jws.WebService;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;

import net.brutex.xservices.types.AttributeType;
import net.brutex.xservices.types.NamespaceListType;
import net.brutex.xservices.types.NamespaceType;
import net.brutex.xservices.types.StringSplitType;
import net.brutex.xservices.types.ant.FileResource;
import net.brutex.xservices.ws.XServicesFault;
import net.brutex.xservices.ws.XmlService;

import org.apache.axiom.om.OMAbstractFactory;
import org.apache.axiom.om.OMAttribute;
import org.apache.axiom.om.OMCloneOptions;
import org.apache.axiom.om.OMComment;
import org.apache.axiom.om.OMContainer;
import org.apache.axiom.om.OMDocument;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMFactory;
import org.apache.axiom.om.OMNode;
import org.apache.axiom.om.OMProcessingInstruction;
import org.apache.axiom.om.OMText;
import org.apache.axiom.om.OMXMLBuilderFactory;
import org.apache.axiom.om.xpath.AXIOMXPath;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jaxen.JaxenException;
import org.jaxen.SimpleNamespaceContext;

/**
 * @author Brian Rosenberger, bru(at)brutex.de
 * 
 */
@WebService(targetNamespace = "http://ws.xservices.brutex.net", endpointInterface = "net.brutex.xservices.ws.XmlService", serviceName = "XmlService")
public class XmlServiceImpl implements XmlService {
	private static final Logger logger = LogManager.getLogger();

	public String insertNodesFromFile(FileResource res, NamespaceListType nsList, String xpath, String xmlFragment)
			throws XServicesFault {
		try {
			AXIOMXPath axp = new AXIOMXPath(xpath);
			InputStream is = res.getAntResource(null).getInputStream();
			OMDocument sourcedoc = OMXMLBuilderFactory.createOMBuilder(is).getDocument();
			OMDocument fragdoc = null;
			if ((xmlFragment != null) && (new String(xmlFragment).length() > 0)) {
				fragdoc = OMXMLBuilderFactory.createOMBuilder(new StringReader("<XS>" + xmlFragment + "</XS>"))
						.getDocument();
			} else {
				throw new XServicesFault("No xmldata to insert.");
			}

			// Initialize XPath context
			SimpleNamespaceContext context = createContext(nsList);
			axp.setNamespaceContext(context);
			axp.addNamespaces(fragdoc.getOMDocumentElement());

			OMDocument document = insertNodes(sourcedoc, axp, fragdoc);

			StringWriter sw = new StringWriter();
			XMLOutputFactory xof = XMLOutputFactory.newInstance();
			XMLStreamWriter writer = xof.createXMLStreamWriter(sw);
			document.serialize(writer);

			this.logger.trace(sw.getBuffer().toString());
			return sw.getBuffer().toString();
		} catch (JaxenException e) {
			e.printStackTrace();
			throw new XServicesFault(e);
		} catch (IOException e) {
			e.printStackTrace();
			throw new XServicesFault(e);
		} catch (XMLStreamException e) {
			e.printStackTrace();
			throw new XServicesFault(e);
		}
	}

	public String replaceNodesFromFile(FileResource res, NamespaceListType nsList, String xpath, String xmlFragment)
			throws XServicesFault {
		try {
			AXIOMXPath axp = new AXIOMXPath(xpath);
			InputStream is = res.getAntResource(null).getInputStream();
			OMDocument sourcedoc = OMXMLBuilderFactory.createOMBuilder(is).getDocument();
			OMDocument fragdoc = null;
			if ((xmlFragment != null) && (new String(xmlFragment).length() > 0)) {
				fragdoc = OMXMLBuilderFactory.createOMBuilder(new StringReader("<XS>" + xmlFragment + "</XS>"))
						.getDocument();
			} else {
				throw new XServicesFault("No xmldata to insert.");
			}

			// Initialize XPath context
			SimpleNamespaceContext context = createContext(nsList);
			axp.setNamespaceContext(context);
			axp.addNamespaces(fragdoc.getOMDocumentElement());

			OMDocument document = replaceNodes(sourcedoc, axp, fragdoc);

			StringWriter sw = new StringWriter();
			XMLOutputFactory xof = XMLOutputFactory.newInstance();
			XMLStreamWriter writer = xof.createXMLStreamWriter(sw);
			document.serialize(writer);

			this.logger.trace(sw.getBuffer().toString());
			return sw.getBuffer().toString();
		} catch (JaxenException e) {
			e.printStackTrace();
			throw new XServicesFault(e);
		} catch (XMLStreamException e) {
			e.printStackTrace();
			throw new XServicesFault(e);
		} catch (IOException e) {
			e.printStackTrace();
			throw new XServicesFault(e);
		}
	}

	public String replaceNodes(String source, String encoding, NamespaceListType nsList, String xpath,
			String xmlFragment) throws XServicesFault {
		encoding = validateEncoding(encoding);
		try {
			AXIOMXPath axp = new AXIOMXPath(xpath);
			InputStream is = new ByteArrayInputStream(source.getBytes(encoding));
			OMDocument sourcedoc = OMXMLBuilderFactory.createOMBuilder(is).getDocument();
			OMDocument fragdoc = null;
			if ((xmlFragment != null) && (new String(xmlFragment).length() > 0)) {
				fragdoc = OMXMLBuilderFactory.createOMBuilder(new StringReader("<XS>" + xmlFragment + "</XS>"))
						.getDocument();
			} else {
				throw new XServicesFault("No xmldata to insert.");
			}

			// Initialize XPath context
			SimpleNamespaceContext context = createContext(nsList);
			axp.setNamespaceContext(context);
			axp.addNamespaces(fragdoc.getOMDocumentElement());

			OMDocument document = replaceNodes(sourcedoc, axp, fragdoc);

			StringWriter sw = new StringWriter();
			XMLOutputFactory xof = XMLOutputFactory.newInstance();
			XMLStreamWriter writer = xof.createXMLStreamWriter(sw);
			document.serialize(writer);

			this.logger.trace(sw.getBuffer().toString());
			return sw.getBuffer().toString();
		} catch (JaxenException e) {
			e.printStackTrace();
			throw new XServicesFault(e);
		} catch (XMLStreamException e) {
			e.printStackTrace();
			throw new XServicesFault(e);
		} catch (UnsupportedEncodingException e) {
			throw new XServicesFault(e);
		}
	}

	@RequiresPermissions("insertNodes")
	public String insertNodes(String source, String encoding, NamespaceListType nsList, String xpath,
			String xmlFragment) throws XServicesFault {
		encoding = validateEncoding(encoding);
		try {
			AXIOMXPath axp = new AXIOMXPath(xpath);
			InputStream is = new ByteArrayInputStream(source.getBytes(encoding));
			OMDocument sourcedoc = OMXMLBuilderFactory.createOMBuilder(is).getDocument();
			OMDocument fragdoc = null;
			if ((xmlFragment != null) && (new String(xmlFragment).length() > 0)) {
				fragdoc = OMXMLBuilderFactory.createOMBuilder(new StringReader("<XS>" + xmlFragment + "</XS>"))
						.getDocument();
			} else {
				throw new XServicesFault("No xmldata to insert.");
			}

			// Initialize XPath context
			SimpleNamespaceContext context = createContext(nsList);
			axp.setNamespaceContext(context);
			axp.addNamespaces(fragdoc.getOMDocumentElement());

			OMDocument document = insertNodes(sourcedoc, axp, fragdoc);

			StringWriter sw = new StringWriter();
			XMLOutputFactory xof = XMLOutputFactory.newInstance();
			XMLStreamWriter writer = xof.createXMLStreamWriter(sw);
			document.serialize(writer);

			this.logger.trace(sw.getBuffer().toString());
			return sw.getBuffer().toString();
		} catch (JaxenException e) {
			e.printStackTrace();
			throw new XServicesFault(e);
		} catch (XMLStreamException e) {
			e.printStackTrace();
			throw new XServicesFault(e);
		} catch (UnsupportedEncodingException e) {
			throw new XServicesFault(e);
		}
	}

	public String wrapInCDATA(String data) throws XServicesFault {
		String result = "";
		String[] tokens = data.split("\\]\\]>", -1);

		for (int i = 0; i < tokens.length; i++) {
			result += tokens[i];
			if (i + 1 < tokens.length)
				result += "]]]]><![CDATA[>";
		}

		result = "<![CDATA[" + result + "]]>";
		return result;
	}

	public StringSplitType selectXPath(String source, String encoding, NamespaceListType nsList, String xpath)
			throws XServicesFault {
		encoding = validateEncoding(encoding);
		try {
			StringSplitType rarray = new StringSplitType();
			AXIOMXPath axp = new AXIOMXPath(xpath);
			InputStream is = new ByteArrayInputStream(source.getBytes(encoding));
			OMDocument sourcedoc = OMXMLBuilderFactory.createOMBuilder(is).getDocument();

			// Initialize XPath context
			SimpleNamespaceContext context = createContext(nsList);

			axp.setNamespaceContext(context);
			List results = axp.selectNodes(sourcedoc);
			for (Object o : results) {
				String text = null;

				if (o instanceof OMNode) {
					switch (((OMNode) o).getType()) {
					case OMNode.TEXT_NODE:
						text = ((OMText) o).getText();
						break;
					case OMNode.COMMENT_NODE:
						text = ((OMComment) o).getValue();
						break;
					case OMNode.PI_NODE:
						text = ((OMProcessingInstruction) o).getValue();
						break;
					default:
						StringWriter sw = new StringWriter();
						XMLOutputFactory xof = XMLOutputFactory.newInstance();
						XMLStreamWriter writer = xof.createXMLStreamWriter(sw);
						((OMNode) o).serialize(writer);
						writer.flush();
						text = sw.toString();
					}
				} else if (o instanceof OMAttribute) {
					text = ((OMAttribute) o).getAttributeValue();
				} else {
					text = String.valueOf(o);
				}
				rarray.addStringMatch(text);
			}

			return rarray;
		} catch (JaxenException e) {
			e.printStackTrace();
			throw new XServicesFault(e);
		} catch (XMLStreamException e) {
			// TODO Auto-generated catch block
			throw new XServicesFault(e.getMessage());
		} catch (UnsupportedEncodingException e) {
			throw new XServicesFault(e);
		}
	}

	public String setAttribute(String source, String encoding, NamespaceListType nsList, String xpath,
			AttributeType attr) throws XServicesFault {
		encoding = validateEncoding(encoding);
		try {
			StringSplitType rarray = new StringSplitType();
			AXIOMXPath axp = new AXIOMXPath(xpath);
			InputStream is = new ByteArrayInputStream(source.getBytes(encoding));
			OMDocument sourcedoc = OMXMLBuilderFactory.createOMBuilder(is).getDocument();
			OMFactory fac = OMAbstractFactory.getOMFactory();

			// Initialize XPath context
			SimpleNamespaceContext context = createContext(nsList);

			axp.setNamespaceContext(context);
			List results = axp.selectNodes(sourcedoc);
			for (Object o : results) {
				String text = null;

				if (o instanceof OMNode) {
					switch (((OMNode) o).getType()) {
					case OMNode.ELEMENT_NODE:
						OMElement node = ((OMElement) o);
						if (attr.value == null) {
							node.removeAttribute(node.getAttribute(new QName(attr.name)));
						} else {
							node.addAttribute(attr.name, attr.value, node.getNamespace());
						}
						break;
					default:
						throw new XServicesFault("XPath expression did not match an element node.");
					}
				} else {
					throw new XServicesFault("XPath expression did not match a node.");
				}
			}

			StringWriter sw = new StringWriter();
			XMLOutputFactory xof = XMLOutputFactory.newInstance();
			XMLStreamWriter writer = xof.createXMLStreamWriter(sw);
			sourcedoc.serialize(writer);
			writer.flush();
			return sw.toString();
		} catch (JaxenException e) {
			e.printStackTrace();
			throw new XServicesFault(e);
		} catch (XMLStreamException e) {
			// TODO Auto-generated catch block
			throw new XServicesFault(e.getMessage());
		} catch (UnsupportedEncodingException e) {
			throw new XServicesFault(e);
		}
	}

	private OMDocument insertNodes(OMDocument xmldocument, AXIOMXPath axp, OMDocument xmlfragment)
			throws XServicesFault {
		List<?> olist = null;
		try {
			olist = axp.selectNodes(xmldocument.getOMDocumentElement());
			this.logger.debug("XPath '" + axp.toString() + "' has " + olist.size() + " matches.");
			this.logger.trace("XPath root expression is: '" + axp.debug() + "'.");
		} catch (JaxenException e) {
			throw new XServicesFault(e.getMessage(), e);
		}
		if (olist.size() == 0)
			throw new XServicesFault(Messages.getString("XmlService.no_match", new Object[] { axp.toString() }));

		// Prepare children to insert
		xmlfragment.build();

		// Determine what has been matched
		OMContainer match = null;
		for (Object o : olist) {
			Iterator<?> children = xmlfragment.getOMDocumentElement().getChildren();
			if ((o instanceof OMNode)) {
				OMNode node = (OMNode) o;
				switch (node.getType()) {
				case OMNode.ELEMENT_NODE:
					if ((o instanceof OMElement))
						match = (OMElement) o;
					if ((o instanceof OMDocument))
						match = ((OMDocument) o).getOMDocumentElement();
					this.logger.debug(Messages.getString("XmlService.8"));
					break;
				case OMNode.TEXT_NODE:
					match = ((OMText) o).getParent();
					this.logger.debug(Messages.getString("XmlService.9"));
					break;
				case OMNode.COMMENT_NODE:
					// match = node.getParent();
					match = (OMContainer) node;
					this.logger.debug(Messages.getString("XmlService.10"));
					break;
				default:
					this.logger
							.error("XPath matched " + o.getClass().getCanonicalName() + " Node Type:" + node.getType());
					this.logger.error(Messages.getString("XmlService.11"));
					throw new XServicesFault(Messages.getString("XmlService.12"));
				}
			} else {
				this.logger.error("XPath matched " + o.getClass().getCanonicalName());
				this.logger.error(Messages.getString("XmlService.11"));
				throw new XServicesFault(Messages.getString("XmlService.12"));
			}

			while (children.hasNext()) {
				OMNode container = (OMNode) children.next();
				match.addChild((OMNode) container.clone(new OMCloneOptions()));
			}
		}

		xmldocument.build();
		return xmldocument;
	}

	private OMDocument replaceNodes(OMDocument xmldocument, AXIOMXPath axp, OMDocument xmlfragment)
			throws XServicesFault {

		List<?> olist = null;
		try {
			olist = axp.selectNodes(xmldocument.getOMDocumentElement());
			this.logger.debug("XPath '" + axp.toString() + "' has " + olist.size() + " matches.");
			this.logger.trace("XPath root expression is: '" + axp.debug() + "'.");
		} catch (JaxenException e) {
			throw new XServicesFault(e.getMessage(), e);
		}
		if (olist.size() == 0)
			throw new XServicesFault(Messages.getString("XmlService.no_match", new Object[] { axp.toString() }));

		// Prepare children to insert
		xmlfragment.build();

		// Determine what has been matched
		OMNode match = null;
		for (Object o : olist) {
			Iterator<?> children = xmlfragment.getOMDocumentElement().getChildren();
			if ((o instanceof OMNode)) {
				OMNode node = (OMNode) o;
				switch (node.getType()) {
				case OMNode.ELEMENT_NODE:
					if ((o instanceof OMElement))
						match = (OMElement) o;
					if ((o instanceof OMDocument))
						match = ((OMDocument) o).getOMDocumentElement();
					this.logger.debug(Messages.getString("XmlService.8"));
					break;
				default:
					this.logger
							.error("XPath matched " + o.getClass().getCanonicalName() + " Node Type:" + node.getType());
					this.logger.error(Messages.getString("XmlService.11"));
					throw new XServicesFault(Messages.getString("XmlService.12"));
				}
			} else {
				this.logger.error("XPath matched " + o.getClass().getCanonicalName());
				this.logger.error(Messages.getString("XmlService.11"));
				throw new XServicesFault(Messages.getString("XmlService.12"));
			}

			while (children.hasNext()) {
				OMNode container = (OMNode) children.next();
				match.insertSiblingBefore((OMNode) container.clone(new OMCloneOptions()));
			}
			match.detach();
		}
		xmldocument.build();
		return xmldocument;
	}

	private SimpleNamespaceContext createContext(NamespaceListType nsList) {
		// Initialize XPath context
		SimpleNamespaceContext context = new SimpleNamespaceContext();
		if (nsList != null) {
			for (NamespaceType ns : nsList.getNamespaces()) {
				context.addNamespace(ns.getPrefix(), ns.getUri().toString());
				this.logger.debug(
						Messages.getString("XmlService.0") + ns.getPrefix() + "=\"" + ns.getUri().toString() + "\"'");
			}
		} else {
			logger.debug("No namespaces defined.");
		}
		return context;
	}

	private String validateEncoding(String encoding) throws XServicesFault {
		if (encoding == null || encoding.equals("")) {
			encoding = Charset.defaultCharset().displayName();
		}
		try {
			Charset.isSupported(encoding);
		} catch (IllegalCharsetNameException e) {
			throw new XServicesFault("Endcoding '" + encoding + "' is not supported by this JRE.");
		}
		logger.debug("Setting source xml string encoding to '" + encoding + "'");
		return encoding;
	}

}
