/*
 * WellFormednessFilter.java
 * Copyright (C) 1999,2000,2001 The Free Software Foundation
 * 
 * This file is part of GNU JAXP, a library.
 *
 * GNU JAXP is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * GNU JAXP is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * Linking this library statically or dynamically with other modules is
 * making a combined work based on this library.  Thus, the terms and
 * conditions of the GNU General Public License cover the whole
 * combination.
 *
 * As a special exception, the copyright holders of this library give you
 * permission to link this library with independent modules to produce an
 * executable, regardless of the license terms of these independent
 * modules, and to copy and distribute the resulting executable under
 * terms of your choice, provided that you also meet, for each linked
 * independent module, the terms and conditions of the license of that
 * module.  An independent module is a module which is not derived from
 * or based on this library.  If you modify this library, you may extend
 * this exception to your version of the library, but you are not
 * obliged to do so.  If you do not wish to do so, delete this
 * exception statement from your version. 
 */

package gnu.xml.pipeline;

import java.util.EmptyStackException;
import java.util.Stack;

import gnu.xml.aelfred2.SAXDriver;
import org.xml.sax.*;
import org.xml.sax.ext.*;


/**
 * This filter reports fatal exceptions in the case of event streams that
 * are not well formed.  The rules currently tested include: <ul>
 *
 *	<li>setDocumentLocator ... may be called only before startDocument
 *
 *	<li>startDocument/endDocument ... must be paired, and all other
 *	calls (except setDocumentLocator) must be nested within these.
 *
 *	<li>startElement/endElement ... must be correctly paired, and
 *	may never appear within CDATA sections.
 *
 *	<li>comment ... can't contain "--"
 *
 *	<li>character data ... can't contain "]]&gt;"
 *
 *	<li>whitespace ... can't contain CR
 *
 *	<li>whitespace and character data must be within an element
 *
 *	<li>processing instruction ... can't contain "?&gt;" or CR
 *
 *	<li>startCDATA/endCDATA ... must be correctly paired.
 *
 *	</ul>
 *
 * <p> Other checks for event stream correctness may be provided in
 * the future.  For example, insisting that
 * entity boundaries nest correctly,
 * namespace scopes nest correctly,
 * namespace values never contain relative URIs,
 * attributes don't have "&lt;" characters;
 * and more.
 *
 * @author David Brownell
 */
public final class WellFormednessFilter extends EventFilter
{
    private boolean		startedDoc;
    private Stack		elementStack = new Stack ();
    private boolean		startedCDATA;
    private String		dtdState = "before";

    
    /**
     * Swallows all events after performing well formedness checks.
     */
	// constructor used by PipelineFactory
    public WellFormednessFilter ()
	{ this (null); }


    /**
     * Passes events through to the specified consumer, after first
     * processing them.
     */
	// constructor used by PipelineFactory
    public WellFormednessFilter (EventConsumer consumer)
    {
	super (consumer);

	setContentHandler (this);
	setDTDHandler (this);
	
	try {
	    setProperty (LEXICAL_HANDLER, this);
	} catch (SAXException e) { /* can't happen */ }
    }

    /**
     * Resets state as if any preceding event stream was well formed.
     * Particularly useful if it ended through some sort of error,
     * and the endDocument call wasn't made.
     */
    public void reset ()
    {
	startedDoc = false;
	startedCDATA = false;
	elementStack.removeAllElements ();
    }


    private SAXParseException getException (String message)
    {
	SAXParseException	e;
	Locator			locator = getDocumentLocator ();

	if (locator == null)
	    return new SAXParseException (message, null, null, -1, -1);
	else
	    return new SAXParseException (message, locator);
    }

    private void fatalError (String message)
    throws SAXException
    {
	SAXParseException	e = getException (message);
	ErrorHandler		handler = getErrorHandler ();

	if (handler != null)
	    handler.fatalError (e);
	throw e;
    }

    /**
     * Throws an exception when called after startDocument.
     *
     * @param locator the locator, to be used in error reporting or relative
     *	URI resolution.
     *
     * @exception IllegalStateException when called after the document
     *	has already been started
     */
    public void setDocumentLocator (Locator locator)
    {
	if (startedDoc)
	    throw new IllegalStateException (
		    "setDocumentLocator called after startDocument");
	super.setDocumentLocator (locator);
    }

    public void startDocument () throws SAXException
    {
	if (startedDoc)
	    fatalError ("startDocument called more than once");
	startedDoc = true;
	startedCDATA = false;
	elementStack.removeAllElements ();
	super.startDocument ();
    }

    public void startElement (
	String uri, String localName,
	String qName, Attributes atts
    ) throws SAXException
    {
	if (!startedDoc)
	    fatalError ("callback outside of document?");
	if ("inside".equals (dtdState))
	    fatalError ("element inside DTD?");
	else
	    dtdState = "after";
	if (startedCDATA)
	    fatalError ("element inside CDATA section");
	if (qName == null || "".equals (qName))
	    fatalError ("startElement name missing");
	elementStack.push (qName);
	super.startElement (uri, localName, qName, atts);
    }

    public void endElement (String uri, String localName, String qName)
    throws SAXException
    {
	if (!startedDoc)
	    fatalError ("callback outside of document?");
	if (startedCDATA)
	    fatalError ("element inside CDATA section");
	if (qName == null || "".equals (qName))
	    fatalError ("endElement name missing");
	
	try {
	    String	top = (String) elementStack.pop ();

	    if (!qName.equals (top))
		fatalError ("<" + top + " ...>...</" + qName + ">");
	    // XXX could record/test namespace info
	} catch (EmptyStackException e) {
	    fatalError ("endElement without startElement:  </" + qName + ">");
	}
	super.endElement (uri, localName, qName);
    }

    public void endDocument () throws SAXException
    {
	if (!startedDoc)
	    fatalError ("callback outside of document?");
	dtdState = "before";
	startedDoc = false;
	super.endDocument ();
    }


    public void startDTD (String root, String publicId, String systemId)
    throws SAXException
    {
	if (!startedDoc)
	    fatalError ("callback outside of document?");
    if ("before" != dtdState)
	    fatalError ("two DTDs?");
	if (!elementStack.empty ())
	    fatalError ("DTD must precede root element");
	dtdState = "inside";
	super.startDTD (root, publicId, systemId);
    }

    public void notationDecl (String name, String publicId, String systemId)
    throws SAXException
    {
// FIXME: not all parsers will report startDTD() ...
// we'd rather insist we're "inside".
    if ("after" == dtdState)
	    fatalError ("not inside DTD");
	super.notationDecl (name, publicId, systemId);
    }

    public void unparsedEntityDecl (String name,
    	String publicId, String systemId, String notationName)
    throws SAXException
    {
// FIXME: not all parsers will report startDTD() ...
// we'd rather insist we're "inside".
    if ("after" == dtdState)
	    fatalError ("not inside DTD");
	super.unparsedEntityDecl (name, publicId, systemId, notationName);
    }

    // FIXME:  add the four DeclHandler calls too

    public void endDTD ()
    throws SAXException
    {
	if (!startedDoc)
	    fatalError ("callback outside of document?");
	if ("inside" != dtdState)
	    fatalError ("DTD ends without start?");
	dtdState = "after";
	super.endDTD ();
    }

    public void characters (char ch [], int start, int length)
    throws SAXException
    {
	int here = start, end = start + length;
	if (elementStack.empty ())
	    fatalError ("characters must be in an element");
	while (here < end) {
	    if (ch [here++] != ']')
		continue;
	    if (here == end)	// potential problem ...
		continue;
	    if (ch [here++] != ']')
		continue;
	    if (here == end)	// potential problem ...
		continue;
	    if (ch [here++] == '>')
		fatalError ("character data can't contain \"]]>\"");
	}
	super.characters (ch, start, length);
    }

    public void ignorableWhitespace (char ch [], int start, int length)
    throws SAXException
    {
	int here = start, end = start + length;
	if (elementStack.empty ())
	    fatalError ("characters must be in an element");
	while (here < end) {
	    if (ch [here++] == '\r')
		fatalError ("whitespace can't contain CR");
	}
	super.ignorableWhitespace (ch, start, length);
    }

    public void processingInstruction (String target, String data)
    throws SAXException
    {
	if (data.indexOf ('\r') > 0)
	    fatalError ("PIs can't contain CR");
	if (data.indexOf ("?>") > 0)
	    fatalError ("PIs can't contain \"?>\"");
    }

    public void comment (char ch [], int start, int length)
    throws SAXException
    {
	if (!startedDoc)
	    fatalError ("callback outside of document?");
	if (startedCDATA)
	    fatalError ("comments can't nest in CDATA");
	int here = start, end = start + length;
	while (here < end) {
	    if (ch [here] == '\r')
		fatalError ("comments can't contain CR");
	    if (ch [here++] != '-')
		continue;
	    if (here == end)
		fatalError ("comments can't end with \"--->\"");
	    if (ch [here++] == '-')
		fatalError ("comments can't contain \"--\"");
	}
	super.comment (ch, start, length);
    }

    public void startCDATA ()
    throws SAXException
    {
	if (!startedDoc)
	    fatalError ("callback outside of document?");
	if (startedCDATA)
	    fatalError ("CDATA starts can't nest");
	startedCDATA = true;
	super.startCDATA ();
    }

    public void endCDATA ()
    throws SAXException
    {
	if (!startedDoc)
	    fatalError ("callback outside of document?");
	if (!startedCDATA)
	    fatalError ("CDATA end without start?");
	startedCDATA = false;
	super.endCDATA ();
    }
}
