/*
 * Copyright 2003, 2004  The Apache Software Foundation
 * 
 * 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 org.apache.ws.commons.serialize;

import javax.xml.XMLConstants;

import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.ext.LexicalHandler;
import org.xml.sax.helpers.AttributesImpl;

/** Serializes a DOM node into a stream of SAX events.
 */
public class DOMSerializer {
	private boolean namespaceDeclarationAttribute;
	private boolean parentsNamespaceDeclarationDisabled;
	private boolean startingDocument = true;
	
	/** Sets whether XML namespace declarations are being serialized as
	 * attributes or as SAX events (default).
	 * @param pXmlDeclarationAttribute True, if a namespace declaration
	 * is being transmitted as an XML attribute. False otherwise.
	 */
	public void setNamespaceDeclarationAttribute(boolean pXmlDeclarationAttribute) {
		namespaceDeclarationAttribute = pXmlDeclarationAttribute;
	}
	
	/** Returns whether XML declarations are being serialized as
	 * attributes or as SAX events (default).
	 * @return True, if a namespace declaration
	 * is being transmitted as an XML attribute. False otherwise.
	 */
	public boolean isNamespaceDeclarationAttribute() {
		return namespaceDeclarationAttribute;
	}
	
	/** Returns whether XML declarations present in the parent nodes
	 * are being serialized (default) or not. This option takes effect
	 * only if the namespace declarations are sent as events. In other
	 * words, if the <code>namespaceDeclarationAttribute</code>
	 * properts is false.
	 * @param pParentsXmlDeclarationDisabled True, if namespace
	 * declarations of the parent nodes are disabled, false otherwise.
	 */
	public void setParentsNamespaceDeclarationDisabled(boolean pParentsXmlDeclarationDisabled) {
		parentsNamespaceDeclarationDisabled = pParentsXmlDeclarationDisabled;
	}
	
	/** Sets whether XML declarations present in the parent nodes
	 * are being serialized (default) or not. This option takes effect
	 * only if the namespace declarations are sent as events. In other
	 * words, if the <code>namespaceDeclarationAttribute</code>
	 * properts is false.
	 * @return True, if namespace declarations of the parent nodes are
	 * disabled, false otherwise.
	 */
	public boolean isParentsNamespaceDeclarationDisabled() {
		return parentsNamespaceDeclarationDisabled;
	}

	/** Returns, whether <code>startDocument</code> and
	 * <code>endDocument</code> events are generated for
	 * document nodes.
	 * @return True (default), if <code>startDocument</code> and
	 * <code>endDocument</code> events are being generated.
	 * False otherwise.
	 */
	public boolean isStartingDocument() {
		return startingDocument;
	}

	/** Sets, whether <code>startDocument</code> and
	 * <code>endDocument</code> events are generated for
	 * document nodes.
	 * @param pStartingDocument True (default), if
	 * <code>startDocument</code> and
	 * <code>endDocument</code> events are being generated.
	 * False otherwise.
	 */
	public void setStartingDocument(boolean pStartingDocument) {
		startingDocument = pStartingDocument;
	}

	/** Serializes the childs of <code>pNode</code>.
	 * @param pNode The parent node, whose childs are being serialized.
	 * @param pHandler The target handler.
	 * @throws SAXException The target handler reported an error.
	 */
	protected void doSerializeChilds(Node pNode, ContentHandler pHandler)
			throws SAXException {
		for (Node child = pNode.getFirstChild();  child != null;
		child = child.getNextSibling()) {
			doSerialize(child, pHandler);
		}
	}

	/** Initially creates startPrefixMapping events for the nodes parents. This
	 * is invoked only, if {@link #isNamespaceDeclarationAttribute()},
	 * and {@link #isParentsNamespaceDeclarationDisabled()} are false.
	 * @param pNode The node, for which namespace declarations are being
	 * created.
	 * @param pHandler The target handler.
	 * @throws SAXException The target handler reported an error.
	 */
	private void parentsStartPrefixMappingEvents(Node pNode, ContentHandler pHandler)
			throws SAXException {
		if (pNode != null) {
			parentsStartPrefixMappingEvents(pNode.getParentNode(), pHandler);
			if (pNode.getNodeType() == Node.ELEMENT_NODE) {
				startPrefixMappingEvents(pNode, pHandler);
			}
		}
	}

	/** Finally creates endPrefixMapping events for the nodes parents. This
	 * is invoked only, if {@link #isNamespaceDeclarationAttribute()},
	 * and {@link #isParentsNamespaceDeclarationDisabled()} are false.
	 * @param pNode The node, for which namespace declarations are being
	 * created.
	 * @param pHandler The target handler.
	 * @throws SAXException The target handler reported an error.
	 */
	private void parentsEndPrefixMappingEvents(Node pNode, ContentHandler pHandler)
			throws SAXException {
		if (pNode != null) {
			if (pNode.getNodeType() == Node.ELEMENT_NODE) {
				endPrefixMappingEvents(pNode, pHandler);
			}
			parentsEndPrefixMappingEvents(pNode.getParentNode(), pHandler);
		}
	}

	/** Creates startPrefixMapping events for the node <code>pNode</code>.
	 * @param pNode The node being serialized.
	 * @param pHandler The target handler.
	 * @throws SAXException The target handler reported an error.
	 */
	private void startPrefixMappingEvents(Node pNode, ContentHandler pHandler)
			throws SAXException {
		NamedNodeMap nnm = pNode.getAttributes();
		if (nnm != null) {
			for (int i = 0;  i < nnm.getLength();  i++) {
				Node attr = nnm.item(i);
				if (XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(attr.getNamespaceURI())) {
					String prefix;
					if (XMLConstants.XMLNS_ATTRIBUTE.equals(attr.getPrefix())) {
						prefix = attr.getLocalName();
					} else if (XMLConstants.XMLNS_ATTRIBUTE.equals(attr.getNodeName())) {
						prefix = "";
					} else {
						throw new IllegalStateException("Unable to parse namespace declaration: " + attr.getNodeName());
					}
					String uri = attr.getNodeValue();
					if (uri == null) {
						uri = "";
					}
					pHandler.startPrefixMapping(prefix, uri);
				}
			}
		}
	}
	
	/** Creates endPrefixMapping events for the node <code>pNode</code>.
	 * @param pNode The node being serialized.
	 * @param pHandler The target handler.
	 * @throws SAXException The target handler reported an error.
	 */
	private void endPrefixMappingEvents(Node pNode, ContentHandler pHandler)
			throws SAXException {
		NamedNodeMap nnm = pNode.getAttributes();
		if (nnm != null) {
			for (int i = nnm.getLength()-1;  i >= 0;  i--) {
				Node attr = nnm.item(i);
				if (XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(attr.getNamespaceURI())) {
					String prefix = attr.getLocalName();
					pHandler.endPrefixMapping(prefix);
				}
			}
		}
	}

	private void characters(ContentHandler pHandler, String pValue, boolean pCdata)
			throws SAXException {
		LexicalHandler lh;
		if (pCdata) {
			lh = (pHandler instanceof LexicalHandler) ? (LexicalHandler) pHandler : null;
		} else {
			lh = null;
		}
		if (lh != null) {
			lh.startCDATA();
		}
		pHandler.characters(pValue.toCharArray(), 0, pValue.length());
		if (lh != null) {
			lh.endCDATA();
		}
	}

	/** Converts the given node <code>pNode</code> into a
	 * stream of SAX events, which are fired into the
	 * content handler <code>pHandler</code>.
	 * @param pNode The node being serialized.
	 * @param pHandler The target handler.
	 * @throws SAXException The target handler reported an error.
	 */
	public void serialize(Node pNode, ContentHandler pHandler)
			throws SAXException {
		if (!isNamespaceDeclarationAttribute()  &&
				!isParentsNamespaceDeclarationDisabled()) {
			parentsStartPrefixMappingEvents(pNode.getParentNode(), pHandler);
		}
		doSerialize(pNode, pHandler);
		if (!isNamespaceDeclarationAttribute()  &&
				!isParentsNamespaceDeclarationDisabled()) {
			parentsEndPrefixMappingEvents(pNode.getParentNode(), pHandler);
		}
	}

	/** Converts the given node <code>pNode</code> into a
	 * stream of SAX events, which are fired into the
	 * content handler <code>pHandler</code>. Unlike
	 * {@link #serialize(Node, ContentHandler)}, this method
	 * doesn't call
	 * {@link #parentsStartPrefixMappingEvents(Node, ContentHandler)},
	 * and
	 * {@link #parentsEndPrefixMappingEvents(Node, ContentHandler)}.
	 * @param pNode The node being serialized.
	 * @param pHandler The target handler.
	 * @throws SAXException The target handler reported an error.
	 */
	protected void doSerialize(Node pNode, ContentHandler pHandler)
			throws SAXException {
		switch (pNode.getNodeType()) {
			case Node.DOCUMENT_NODE:
				boolean startDocumentEvent = isStartingDocument();
				if (startDocumentEvent) {
					pHandler.startDocument();
				}
				doSerializeChilds(pNode, pHandler);
				if (startDocumentEvent) {
					pHandler.endDocument();
				}
				break;
			case Node.DOCUMENT_FRAGMENT_NODE:
				doSerializeChilds(pNode, pHandler);
				break;
			case Node.ELEMENT_NODE:
				AttributesImpl attr = new AttributesImpl();
				boolean isNamespaceDeclarationAttribute = isNamespaceDeclarationAttribute();
				if (!isNamespaceDeclarationAttribute) {
					startPrefixMappingEvents(pNode, pHandler);
				}
				NamedNodeMap nnm = pNode.getAttributes();
				if (nnm != null) {
					for (int i = 0;  i < nnm.getLength();  i++) {
						Node a = nnm.item(i);
						if (isNamespaceDeclarationAttribute  ||
								!XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(a.getNamespaceURI())) {
							String aUri = a.getNamespaceURI();
							String aLocalName = a.getLocalName();
							String aNodeName = a.getNodeName();
							if (aLocalName == null) {
								if (aUri == null  ||  aUri.length() == 0) {
									aLocalName = aNodeName;
								} else {
									throw new IllegalStateException("aLocalName is null");
								}
							}
							attr.addAttribute(aUri == null ? "" : aUri, aNodeName,
									aLocalName, "CDATA", a.getNodeValue());
						}
					}
				}
				String nUri = pNode.getNamespaceURI();
				if (nUri == null) {
					nUri = "";
				}
				pHandler.startElement(nUri, pNode.getLocalName(),
						pNode.getNodeName(), attr);
				doSerializeChilds(pNode, pHandler);
				pHandler.endElement(nUri, pNode.getLocalName(),
						pNode.getNodeName());
				if (!isNamespaceDeclarationAttribute) {
					endPrefixMappingEvents(pNode, pHandler);
				}
				break;
			case Node.TEXT_NODE:
				characters(pHandler, pNode.getNodeValue(), false);
				break;
			case Node.CDATA_SECTION_NODE:
				characters(pHandler, pNode.getNodeValue(), true);
				break;
			case Node.PROCESSING_INSTRUCTION_NODE:
				pHandler.processingInstruction(pNode.getNodeName(), pNode.getNodeValue());
				break;
			case Node.ENTITY_REFERENCE_NODE:
				pHandler.skippedEntity(pNode.getNodeName());
				break;
			case Node.COMMENT_NODE:
				if (pHandler instanceof LexicalHandler) {
					String s = pNode.getNodeValue();
					((LexicalHandler) pHandler).comment(s.toCharArray(), 0, s.length());
				}
				break;
			default:
				throw new IllegalStateException("Unknown node type: " + pNode.getNodeType());
		}
	}
}
