/* SAXHandler.java


	Purpose:
	Description:
	History:
	2001/10/25 12:34:46, Create, Tom M. Yeh.

Copyright (C) 2001 Potix Corporation. All Rights Reserved.

{{IS_RIGHT
	This program is distributed under LGPL Version 2.1 in the hope that
	it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.idom.input;

import java.io.IOException;
import java.net.URL;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.ext.DeclHandler;
import org.xml.sax.ext.LexicalHandler;
import org.xml.sax.helpers.DefaultHandler;
import org.zkoss.idom.Attribute;
import org.zkoss.idom.Document;
import org.zkoss.idom.Element;
import org.zkoss.idom.Group;
import org.zkoss.idom.Item;
import org.zkoss.idom.Namespace;
import org.zkoss.util.resource.Locators;

/**
 * The SAX Handler.
 * It implements ContentHandler, LexicalHandler and DeclHandler.
 * It is the caller's job to set up this handler properly if required.
 *
 * <p>This class doesn't depend on SAXBuilder, so it can be used in any
 * other place, e.g., javax.xml.transform.sax.SAXResult.
 *
 * @author tomyeh
 * @see SAXBuilder
 */
public class SAXHandler extends DefaultHandler
implements LexicalHandler, DeclHandler {
	private static final Logger log = LoggerFactory.getLogger(SAXHandler.class);

	/** The iDOM factory. */
	protected IDOMFactory _factory;

	//-- options --//
	/** Whether to ignore ignorable whitespace */
	private boolean _ignoreWhitespaces = false;
	/** Whether expansion of entities should occur */
	private boolean _expandEntities = true;
	/** Whether to convert CDATA to Text and coalesce them. */
	private boolean _coalescing = false;
	/** Whether to ignore comments. */
	private boolean _ignoreComments = false;
	/** The error handler. */
	private ErrorHandler _errHandler = null;
	/** The entity resolver. */
	private EntityResolver _resolver = null;

	/** The Document being created. */
	protected Document _doc = null;
	/** Locator. */
	protected Locator _loc = null;
	/** Indicator of whether we are in a DTD. */
	protected boolean _inDTD = false;
	/** Indicator of whether we are in a CDATA. */
	protected boolean _inCData = false;
	/** The Group stack. The top one is the current group being processed. */
	protected Stack<Group> _stack = null;
	/** The namespaces in between startPrefixMapping and endPrefixMapping. */
	protected List<Namespace> _declNamespaces = null;
	/** Temporary holder for the internal subset. */
	private StringBuffer _internSubset = null;
	/** Whether it is in internal subset. */
	private boolean _inInternSubset = false;

	/**
	 * Constructor.
	 *
	 * @param factory the iDOM factory; null for DefaultIDOMFactory.
	 */
	public SAXHandler(IDOMFactory factory) {
		_factory = factory != null ? factory: new DefaultIDOMFactory();
	}
	/**
	 * Constructor.
	 */
	public SAXHandler() {
		_factory = new DefaultIDOMFactory();
	}

	//-- options --//
	/**
	 * Tests whether to ignore whitespaces in element content.
	 */
	public final boolean isIgnoringElementContentWhitespace() {
		return _ignoreWhitespaces;
	}
	/**
	 * Sets whether the parser should eliminate whitespace in 
	 * element content. They are known as "ignorable whitespace". 
	 * Only whitespace which is contained within element content that has
	 * an element only content model will be eliminated (see XML Rec 2.10).
	 *
	 * <p>For this setting to take effect requires that validation be turned on.
	 *
	 * <p>Default: false.
	 *
	 * @param ignore Whether to ignore whitespaces in element content.
	 */
	public final void setIgnoringElementContentWhitespace(boolean ignore) {
		_ignoreWhitespaces = ignore;
	}

	/**
	 * Tests whether to expand entity reference nodes.
	 */
	public final boolean isExpandEntityReferences() {
		return _expandEntities;
	}
	/**
	 * Sets whether to expand entities during parsing.
	 * A true means to expand entities as normal content.  A false means to
	 * leave entities unexpanded as <code>EntityReference</code> objects.
	 *
	 * <p>Default: true.
	 *
	 * @param expand whether entity expansion should occur.
	 */
	public final void setExpandEntityReferences(boolean expand) {
		_expandEntities = expand;
	}

	/**
	 * Indicates whether or not the factory is configured to produce parsers
	 * which converts CDATA to Text and appends it to the adjacent (if any)
	 * Text node.
	 *
	 * <p>Default: false.
	 *
	 * @return true if the factory is configured to produce parsers which
	 * converts CDATA nodes to Text nodes
	 * and appends it to the adjacent (if any) Text node; false otherwise.
	 */
	public final boolean isCoalescing() {
		return _coalescing;
	}
	/**
	 * Specifies that the parser produced by this code will convert
	 * CDATA to Text and append it to the adjacent (if any) text.
	 *
	 * <p>Default: false.
	 */
	public final void setCoalescing(boolean coalescing) {
		_coalescing = coalescing;
	}

	/**
	 * Indicates whether or not the factory is configured to produce parsers
	 * which ignores comments.
	 *
	 * <p>Default: false.
	 *
	 * @return true if the factory is configured to produce parsers
	 * which ignores comments; false otherwise.
	 */
	public final boolean isIgnoringComments() {
		return _ignoreComments;
	}
	/**
	 * Specifies that the parser produced by this code will ignore comments.
	 *
	 * <p>Default: false.
	 */
	public final void setIgnoringComments(boolean ignoreComments) {
		_ignoreComments = ignoreComments;
	}

	/**
	 * Specifies the org.xml.sax.ErrorHandler to be used to report errors
	 * present in the XML document to be parsed.
	 * <p>Default: null -- to use the default implementation and behavior.
	 */
	public final void setErrorHandler(ErrorHandler eh) {
		_errHandler = eh;
	}
	/**
	 * Gets the org.xml.sax.ErrorHandler.
	 *
	 * @return the error handler; null to use the default implementation
	 */
	public final ErrorHandler getErrorHandler() {
		return _errHandler;
	}

	/**
	 * Specifies the org.xml.sax.EntityResolver to be used to resolve
	 * entities present in the XML document to be parsed.
	 * <p>Default: null -- to use the default implementation and behavior.
	 */
	public final void setEntityResolver(org.xml.sax.EntityResolver er) {
		_resolver = er;
	}
	/**
	 * Gets the org.xml.sax.EntityResolver.
	 *
	 * @return the entity resolver; null to use the default implementation
	 */
	public final EntityResolver getEntityResolver() {
		return _resolver;
	}

	//-- Extra utilities for the caller to use --//
	/**
	 * Gets the document being created.
	 * Called to retrieve the iDOM tree after parsed.
	 */
	public final Document getDocument() {
		return _doc;
	}

	/**
	 * Gets the iDOM factory. Null for DefaultIDOMFactory.THE.
	 */
	public final IDOMFactory getIDOMFactory() {
		return _factory;
	}
	/**
	 * Sets the iDOM factory. Null for DefaultIDOMFactory.THE.
	 */
	public final void setIDOMFactory(IDOMFactory factory) {
		_factory = factory;
	}

	//-- protected utilities --//
	/**
	 * Attaches the locator to the item.
	 */
	protected final void attachLocator(Item vtx) {
		if (_loc != null)
			vtx.setLocator(new MyLoc(_loc));
	}

	/** Returns the top group, or null if not available.
	 */
	protected final Group getTopGroup() {
		return _stack.isEmpty() ? null: _stack.peek();
	}
	/**
	 * Adds the item to the current group; also attach the locator.
	 */
	protected final void addToCurrentGroup(Item vtx) {
		attachLocator(vtx);
		_stack.peek().getChildren().add(vtx);
	}
	/**
	 * Adds a new group to the current group as a child,
	 * and pushes the new group to be the new current group.
	 */
	protected final void pushGroup(Group group) {
		if (_stack.isEmpty())
			assert(group instanceof Document);
		else
			addToCurrentGroup(group);

		_stack.push(group);
	}
	/**
	 * Pops out the current group, and the one under it becomes the
	 * new current group.
	 */
	protected final void popGroup() {
		_stack.pop().coalesce(false);
	}

	//-- org.xml.sax.ext.DeclHandler --//
	public void externalEntityDecl(String name, String pubId, String sysId)
	throws SAXException {
//		if (log.finerable()) log.finer("externalEntityDecl: " + name + " p:" + pubId + " s:" + sysId);

		if (!_inInternSubset)
			return;

		_internSubset.append("  <!ENTITY ").append(name);
		if (pubId != null)
			_internSubset.append(" PUBLIC \"").append(pubId).append("\" ");
		if (sysId != null)
			_internSubset.append(" SYSTEM \"").append(sysId).append("\" ");
		_internSubset.append(">\n");
	}

	public void internalEntityDecl(String name, String value)
	throws SAXException { 
//		if (log.finerable()) log.finer("internalEntityDecl: " + name + '=' + value);

		if (!_inInternSubset)
			return;

		_internSubset.append("  <!ENTITY ").append(name).append(" \"")
			.append(value).append("\">\n");
	}

	public void attributeDecl(String eName, String aName, String type,
	String valueDefault, String value) throws SAXException {
		if (!_inInternSubset)
			return;

		_internSubset.append("  <!ATTLIST ").append(eName).append(' ')
			.append(aName).append(' ').append(type).append(' ');
		if (valueDefault != null) {
			_internSubset.append(valueDefault);
		} else {
			_internSubset.append('"').append(value).append('"');
		}
		if ((valueDefault != null) && (valueDefault.equals("#FIXED"))) {
			_internSubset.append(" \"").append(value).append('"');
		}
		_internSubset.append(">\n");
	}

	public void elementDecl(String name, String model) throws SAXException {
		if (!_inInternSubset)
			return;

		_internSubset.append("  <!ELEMENT ").append(name).append(' ')
			.append(model).append(">\n");
	}

	//-- org.xml.sax.ext.LexicalHandler --//
	public void startDTD(String name, String pubId, String sysId)
	throws SAXException {
//		if (log.finerable()) log.finer("start DTD: " + name + " p:" + pubId + " s:" + sysId);

		addToCurrentGroup(_factory.newDocType(name, pubId, sysId));
		_inDTD = true;
		_internSubset = new StringBuffer(); //TY: start use it
		_inInternSubset = true;
	}

	//NOTE: xerces does not invoke endDTD if nothing in there
	public void endDTD() throws SAXException {
//		if (log.finerable()) log.finer("end DTD: \"" + _internSubset + '"');

		_doc.getDocType().setInternalSubset(_internSubset.toString());
		_inDTD = false;
		_internSubset = null; //TY: no longer used
		_inInternSubset = false;
	}

	public void startEntity(String name) throws SAXException {
//		if (log.finerable()) log.finer("startEntity: " + name);

		//A "[dtd]" entity indicates the beginning of the external subset
		if (name.equals("[dtd]")) {
			_inInternSubset = false;
			return;
		}

		if (!isExpandEntityReferences() && !_inDTD && !entityToSkip(name)) {
			pushGroup(_factory.newEntityRef(name));
		}
	}
	/** Tests whether the name is something that always expanded. */
	private boolean entityToSkip(String name) {
		//To speed up the performance, we don't compare all strings
		//Rather, we use something similar to finite-state machine.
		switch (name.charAt(0)) {
		case 'a': return name.equals("amp") || name.equals("apos");
		case 'g': return name.equals("gt");
		case 'l': return name.equals("lt");
		case 'q': return name.equals("quot");
		}
		return false;
	}

	public void endEntity(String name) throws SAXException {
//		if (log.finerable()) log.finer("endEntity: " + name);

		if (name.equals("[dtd]")) {
			_inInternSubset = false;
			return;
		}
		if (!isExpandEntityReferences() && !_inDTD && !entityToSkip(name)) {
			popGroup();
		}
	}

	public void startCDATA() throws SAXException {
		_inCData = true;
	}

	public void endCDATA() throws SAXException {
		_inCData = false;
	}

	public void comment(char[] ch, int start, int length) throws SAXException {
		if (length == 0 || isIgnoringComments())
			return; //ignore zero length

		String data = new String(ch, start, length);
		if (_inDTD && _inInternSubset && !isExpandEntityReferences()) {
			_internSubset.append("  <!--").append(data).append("-->\n");
		}
		if (!_inDTD && data.length() != 0) {
			addToCurrentGroup(_factory.newComment(data));
		}
	}

	//-- org.xml.sax.ContentHandler --//
	public void startDocument() throws SAXException {
		_declNamespaces = new LinkedList<Namespace>();
		_stack = new Stack<Group>();

		_doc = _factory.newDocument(null, null);
		pushGroup(_doc);
	}
	public void endDocument() throws SAXException {
		popGroup();
		assert(_stack.isEmpty());

		_stack = null;
		_loc = null;
		_declNamespaces = null;
	}

	public void setDocumentLocator(Locator locator) {
		_loc = locator;
	}

	public void processingInstruction(String target, String data)
	throws SAXException {
		addToCurrentGroup(_factory.newProcessingInstruction(target, data));
	}

	public void startPrefixMapping(String prefix, String uri)
	throws SAXException {
//		if (log.finerable()) log.finer(message("start prefix: " + prefix + ", " + uri, _loc));

		final Namespace ns = Namespace.getSpecial(prefix);
		if (ns == null || !ns.getURI().equals(uri))
			_declNamespaces.add(0, new Namespace(prefix, uri));
			//TY: add at the head to speed up the searching
	}

	public void endPrefixMapping(String prefix) throws SAXException {
//		if (log.finerable()) log.finer(message("end prefix: " + prefix, _loc));

		for (Iterator<Namespace> itr = _declNamespaces.iterator(); itr.hasNext();) {
			final Namespace ns = itr.next();
			if (prefix.equals(ns.getPrefix())) {
				itr.remove();
				break;
			}
		}
	}
	private static String message(String message, Locator loc) {
		return org.zkoss.xml.Locators.format(message, loc);
	}

	public void
	startElement(String nsURI, String lname, String tname, Attributes attrs)
	throws SAXException {
//		if (log.finerable())
//			log.finer(message("start element: nsURI=\"" + nsURI + "\", lname=" + lname + ", tname=" + tname
//				+ " attr#=" + attrs.getLength(), _loc));

		//create the element
		if (tname == null || tname.length() == 0) //just in case
			tname = lname;
		final Element element = newElement(nsURI, tname);
			//note: in crimson, lname might be empty, so...

		//Handle attributes
		for (int j=0, len=attrs.getLength(); j<len; j++) {
			String attQname = attrs.getQName(j);
			String attLname = attrs.getLocalName(j);

			if (attQname == null || attQname.length() == 0) //just in case
				attQname = attLname;

			final Attribute attr;
			int kp = attQname.indexOf(':');
			if (kp >= 0) {
				final String prefix = attQname.substring(0, kp);
				final Namespace ns = element.getNamespace(prefix);
				if (ns == null)
					throw new SAXException("Unknown prefix: "+prefix+" at "+_loc+"\nDo you forget to turn on namespace-aware");
				attr = _factory.newAttribute(
					ns, attQname.substring(kp + 1), attrs.getValue(j));
					//if prefix, attLname might be empty so use attQname
			} else {
				attr = _factory.newAttribute(
					attLname == null || attLname.length() == 0 ?
						attQname: attLname, //crimson might use empty for AttLname
					attrs.getValue(j));
			}
			attachLocator(attr);
			element.getAttributeItems().add(attr);
			//Don't use setAttribute, to can detect replicated attributes
		}
	}
	private Element newElement(String nsURI, String tname)
	throws SAXException {
		if (nsURI == null) nsURI = "";

		final int j = tname.indexOf(':');
		final String prefix = j >= 0 ? tname.substring(0, j): "";
		final String lname = j >= 0 ? tname.substring(j + 1): tname;

		Namespace ns;
		final Group parent = getTopGroup();
		if (parent instanceof Element) {
			ns = ((Element)parent).getNamespace(prefix);
			if (ns != null && !ns.getURI().equals(nsURI)) //override
				ns = null; //create a no-namespace element first
		} else {
			ns = Namespace.getSpecial(prefix);
		}

		if (ns == null) {
			if (_declNamespaces.size() > 0) {
				for (Namespace n: _declNamespaces)
					if (n.getPrefix().equals(prefix) && n.getURI().equals(nsURI)) {
//						if (log.finerable()) log.finer("Namespace found in _declNamespaces: "+n);
						ns = n;
						break; //found
					}
			}
			if (ns == null && nsURI.length() > 0) {
//				if (log.finerable()) log.finer("Create namespace: "+prefix+" "+nsURI);
				ns = new Namespace(prefix, nsURI);
			}
		}

		final Element element = ns != null ?
			_factory.newElement(ns, lname): _factory.newElement(lname);

		//add to element's add. namespaces
		if (_declNamespaces.size() > 0) {
			for (Namespace n: _declNamespaces)
				element.addDeclaredNamespace(n);
			_declNamespaces.clear();
		}

		pushGroup(element);
		return element;
	}
	public void endElement
	(String nsURI, String lname, String tname) throws SAXException {
//		if (log.finerable()) log.finer("end element: " + nsURI + ", " + lname + ", " + tname);

		popGroup();
	}

	public void characters(char[] ch, int start, int length)
	throws SAXException {
		if (length == 0) return; //ignore zero length

		//Note: Element's add will coalesce consecutive CDATA or Text
		final String data = new String(ch, start, length);
		if (getTopGroup() instanceof Document) {
			if (data.trim().length() > 0)
				throw new SAXException("Adding non-empty text to Document: "+data);
			return; //Under transforming, it is possible to have this case
		}

		if (_inCData && !isCoalescing())
			addToCurrentGroup(_factory.newCData(data));
		else
			addToCurrentGroup(_factory.newText(data));
	}

	public void ignorableWhitespace(char[] ch, int start, int length)
	throws SAXException {
		if (length == 0 || isIgnoringElementContentWhitespace())
			return;

		addToCurrentGroup(_factory.newText(new String(ch, start, length)));
	}

	//-- org.xml.sax.DTDHandler --//
	public void notationDecl(String name, String publicID, String systemID)
	throws SAXException {
		if (!_inInternSubset)
			return;

		_internSubset.append("  <!NOTATION ").append(name)
			.append(" \"").append(systemID).append("\">\n");
	}

	public void unparsedEntityDecl
	(String name, String pubId, String sysId, String notationName)
	throws SAXException {
//		if (log.finerable()) log.finer("externalEntityDecl: " + name + " p:" + pubId + " s:" + sysId + " n:" + notationName);

		if (!_inInternSubset)
			return;

		_internSubset.append("  <!ENTITY ").append(name);

		if (pubId != null)
			_internSubset.append(" PUBLIC \"").append(pubId).append("\" ");
		if (sysId != null)
			_internSubset.append(" SYSTEM \"").append(sysId).append("\" ");

		_internSubset.append(" NDATA ").append(notationName).append(">\n");
	}

	//-- org.xml.sax.EntityResolver --//
	public InputSource resolveEntity(String publicId, String systemId)
	throws SAXException {
//		if (log.finerable()) log.finer("resolveEntity public="+publicId+" system="+systemId);

		EntityResolver er = getEntityResolver();
		if (er != null) {
			try {
				return er.resolveEntity(publicId, systemId);
			} catch (IOException ex) {
				//unfortunately, DefaultHandler doesn't throw IOException,
				//so we have to wrap it
				throw new SAXException(ex);
			}
		} else {
			InputSource is = defaultResolveEntity(publicId, systemId);
//			if (is == null && log.finerable()) log.finer("Unable to resolve public="+publicId+" system="+systemId);
			return is;
		}
	}
	/** The default entity resolver.
	 * It is used if {@link #setEntityResolver} is not called.
	 * This implementation searches the class path under /metainfo/xml.
	 *
	 * <p>For example, http://java.sun.com/xml/ns/portlet/portlet-app_1_0.xsd
	 * is asked.
	 * It searches from classpath
	 * for /metainfo/xml/java.sun.com/xml/ns/portlet/portlet-app_1_0.xsd
	 * and /metainfo/xml/portlet-app_1_0.xsd
	 */
	protected InputSource defaultResolveEntity(String publicId, String systemId)
	throws SAXException {
		if (systemId == null)
			return null;

		//Try the original form first if "file:/" found (including jar:file:/)
		//We don't need to handle others, because http:// will be resolved
		//by parser. And, if we do it here, we lost the chance to improve
		//performance by loading it locally by below codes
		if (systemId.indexOf("file:/") >= 0) {
			try {
				final InputSource is = new InputSource(new URL(systemId).openStream());
				is.setSystemId(systemId);
//				if (log.finerable()) log.finer("Entity found "+systemId);
				return is;
			} catch (Exception ex) {
//				if (log.finerable()) log.finer("Unable to open "+systemId);
			}
		}

		final String PREFIX = "/metainfo/xml";
		final org.zkoss.util.resource.Locator loader = Locators.getDefault();

		URL url = null;
		int j = systemId.indexOf("://");
		if (j > 0) {
			final String resId = PREFIX + systemId.substring(j + 2);
			url = loader.getResource(resId);
		}
		if (url == null) {
			j = systemId.lastIndexOf('/');
			final String resId = j >= 0 ? PREFIX + systemId.substring(j): PREFIX + '/' + systemId;
			url = loader.getResource(resId);
		}
		if (url != null) {
//			if (log.finerable()) log.finer("Entity resovled to "+url);
			try {
				final InputSource is = new InputSource(url.openStream());
				is.setSystemId(url.toExternalForm());
				return is;
			} catch (IOException ex) {
				throw new SAXException(ex); //not possible because Locator is used
			}
		}
		return null;
	}

	//-- org.xml.sax.ErrorHandler --//
	public void warning(SAXParseException ex) throws SAXException {
		ErrorHandler eh = getErrorHandler();
		if (eh != null) {
			eh.warning(ex);
		} else {
			log.warn(message(ex.getMessage(), _loc));
		}
	}
	public void error(SAXParseException ex) throws SAXException {
		ErrorHandler eh = getErrorHandler();
		if (eh != null) {
			eh.error(ex);
		} else {
			log.error(message(ex.getMessage(), _loc));
			throw ex;
		}
	}
	public void fatalError(SAXParseException ex) throws SAXException {
		ErrorHandler eh = getErrorHandler();
		if (eh != null) {
			eh.fatalError(ex);
		} else {
			if (log.isDebugEnabled()) log.debug(message(ex.getMessage(), _loc));
			throw ex;
		}
	}

	private static class MyLoc implements org.zkoss.xml.Locator, java.io.Serializable {
		protected final int _colno, _lnno;
		protected final String _pubId, _sysId;

		private MyLoc(org.xml.sax.Locator loc) {
			_pubId = loc.getPublicId();
			_sysId = loc.getSystemId();
			_lnno  = loc.getLineNumber();
			_colno = loc.getColumnNumber();
		}
		
		public final int getColumnNumber() {
			return _colno;
		}
		
		public final int getLineNumber() {
			return _lnno;
		}
		
		public final String getPublicId() {
			return _pubId;
		}
		
		public final String getSystemId() {
			return _sysId;
		}
		
		public final String toString() {
			return '['+_pubId+':'+_sysId+':'+_lnno+':'+_colno+']';
		}
	}
}
