// MobyParser.java
//
//    Created: August 2005
//
// This file is a component of the BioMoby project.
// Copyright Martin Senger (martin.senger@gmail.com).
//

package org.biomoby.shared.parser;

import org.biomoby.shared.MobyException;
import org.biomoby.shared.MobyService;
import org.biomoby.shared.Utils;
import org.biomoby.shared.data.MobyProvisionInfo;
import org.biomoby.shared.datatypes.MobyObject;
import org.biomoby.shared.datatypes.MobyXref;
import org.biomoby.shared.datatypes.MapDataTypesIfc;

import org.tulsoft.tools.loaders.ICreator;
import org.tulsoft.shared.GException;
import org.tulsoft.tools.xml.XMLUtils2;
import org.tulsoft.tools.xml.XMLErrorHandler;

import org.apache.commons.lang.StringUtils;

import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.Locator;
import org.xml.sax.Attributes;
import org.xml.sax.XMLReader;
import org.xml.sax.InputSource;

import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import java.util.Stack;
import java.util.Map;
import java.util.HashMap;
import java.util.Set;
import java.util.HashSet;
import java.io.InputStream;
import java.io.Reader;
import java.io.IOException;

/**
 * The MobyParser is able to read a Biomoby service/client XML data,
 * parse them and create from them an instance of {@link
 * MobyPackage}. The parser can be invoked by using a static method
 * {@link MobyPackage#createFromXML MobyPackage.createFromXML}. <p>
 *
 * The parser depends on generated Java classes that define all
 * Biomoby data types. There is a generator {@link
 * org.biomoby.service.generator.DataTypesGenerator
 * DataTypesGenerator} that produces such classes into a package <a
 * href="http://biomoby.org/moby-live/Java/docs/APIservices/org/biomoby/shared/datatypes/package-summary.html"
 * target="_top"><tt>org.biomoby.shared.datatypes</tt></a>. <p>
 *
 * There is a situation when the parser tries to substitute an unknown
 * data type by a known one. If the parsing encoutered an XML tag
 * depicting a <em>top-level</em> data object, or a member object, and
 * if there is no class available for such object, the parser can be
 * instructed (in its {@link #MobyParser(String) non-default
 * constructor} to create a substituted object whose name was given in
 * the parser constructor (there is also a constructor where you can
 * get more than one such substitution object, depicted by its article
 * name). This is to prevent situation when a long-time running and
 * deployed service suddenly gets a request from a client that uses
 * more up-to-date list of data types. It would be bad to let such
 * service die (or minimally respond unproperly) just because its
 * classes were generated too long ago. <p>
 *
 * Because also skeletons for services can be generated, it is easy to
 * ensure that a service knows its "the most specialized" data type it
 * can still serve, and that it passes it to the parser
 * constructor. <p>
 *
 * If the parser finds an unknown object/tag but no substitute was
 * passed in the parser constructor, it prints a warning and ignores
 * the whole object, with all its descendants.
 *
 * The parser also produces a warning if it makes the substitution
 * described above. Both these warnings should signal that new data
 * types should be generated, and services restarted. Fortunately, it
 * does not happen often at all. <p>
 *
 * If the parser finds another problem, usually related to the invalid
 * XML, it raises a {@link org.biomoby.shared.MobyException
 * MobyException} with error message containing line and column close
 * to place where the error happened. <p>
 *
 * One possible problem would be when article names of the member data
 * types in the parsed XML do not correspond to what was registered in
 * the Biomoby registry (or to its more specialized childern). <p>
 *
 * You can test parser by using a simple <tt>TestingMobyParser</tt>
 * client. This is how to invoke it and how to get its help:
 *
 * <pre>
 * build/run/run-moby-parser -h
 * </pre>
 *
 * A test input file is available in <tt>data</tt> directory. To use
 * it type:
 *
 * <pre>
 * build/run/run-moby-parser -r data/parser-test-input.xml
 * </pre>
 *
 * The <tt>-r</tt> option causes that the parser not only parses the
 * input file into an instance of {@link MobyPackage} but also
 * converts this instance back (reverse) into an XML. The resulting
 * XML is not formally identical to the original - it may have
 * different formatting, it probably has more XML namespace prefixes
 * (they are everywhere), but more importantly, it may not reflect all
 * data that was in the original input. This is because the parser
 * ignores all values that are not carried by and only by the Biomoby
 * primitives types (as it was allowed before big change in the summer
 * 2005). <p>
 *
 * If you wish to use the parser in your own code, here is how to do
 * it:
 * <pre>
 * import org.biomoby.shared.parser.MobyPackage;
 * import org.biomoby.shared.MobyException;
 *
 * try {
 *    MobyPackage moby = MobyPackage.createFromXML (new File ("input.xml"));
 *
 *    // do something with 'moby'
 *    ...
 *    } catch (MobyException e) {
 *    ...
 *    }
 * </pre>
 *
 * If you want to add the "fallback" data type (as explained above),
 * add its name as a second parameter:
 * <pre>
 * MobyPackage moby = MobyPackage.createFromXML (new File ("input.xml"),
 *                                               "GenericSequence");
 * </pre>
 *
 * For a constructor with more fallbacks, check generated skeletons
 * (for example the sample skeleton
 * <tt>net.jmoby.samples.ConcatSequencesSkel</tt>). <p>

 * All XML tags and all attribute names that are recognized and
 * processed by this parser are stored as constants in class {@link
 * MobyTags}. <p>
 *
 * The parser is not thread-safe. Make a new instance for each parsed
 * input. <p>
 *
 * @author <A HREF="mailto:martin.senger@gmail.com">Martin Senger</A>
 * @version $Id: MobyParser.java,v 1.9 2008/03/02 12:45:26 senger Exp $
 */

public class MobyParser
    extends XMLErrorHandler
    implements MobyTags {

    private static org.apache.commons.logging.Log log =
	org.apache.commons.logging.LogFactory.getLog (MobyParser.class);

    Locator locator;
    XMLReader parser = null;
    String lowestKnownDataType = null;
    Map<String,String> lowestKnownDataTypes = new HashMap<String,String>();

    /**************************************************************************
     * Default constructor.
     **************************************************************************/
    public MobyParser() {
	super();
    }

    /**************************************************************************
     * Another constructor, taking the "fallback" data type name. See
     * the full documentation at the top of this class what is
     * "fallback" data type and when it is used.
     **************************************************************************/
    public MobyParser (String lowestKnownDataType) {
	super();
	if (StringUtils.isBlank (lowestKnownDataType))
	    this.lowestKnownDataType = null;
	else
	    this.lowestKnownDataType = lowestKnownDataType;
    }

    /**************************************************************************
     * Another constructor, taking more "fallback" data type names
     * (indexed by their article names). See the full documentation at
     * the top of this class what is "fallback" data type and when it
     * is used.
     **************************************************************************/
    public MobyParser (Map<String,String> lowestKnownDataTypes) {
	super();
	this.lowestKnownDataTypes = lowestKnownDataTypes;
    }

    /**************************************************************************
     * Parse the contents of the given file.
     **************************************************************************/
    public MobyPackage parse (String xmlFilename)
	throws MobyException {
	return _parse (new InputSource (xmlFilename));
    }

    /**************************************************************************
     * Parse the contents coming from the given input stream.
     *************************************************************************/
    public MobyPackage parse (InputStream xml)
	throws MobyException {
	return _parse (new InputSource (xml));
    }

    /**************************************************************************
     * Parse the contents coming from the given reader.
     *************************************************************************/
    public MobyPackage parse (Reader xmlReader)
	throws MobyException {
	return _parse (new InputSource (xmlReader));
    }

    /*********************************************************************
     * Set document locator. There is usually no need to use it from
     * outside the parser.
     ********************************************************************/
    public void setDocumentLocator (Locator l) {
        this.locator = l;
    }

    /*********************************************************************
     * Static initializers.
     ********************************************************************/

    //
    // This is a list of #PCDATA elements. It is converted into a
    // HashMap which is then used in 'startElement()' where all these
    // elements are treated the same. Then in the 'endElement()', I
    // still use a big if-else statement because they all go to
    // different places.
    //
    static String[] pcdataNamesArray = {
        NOTES,
	EXCEPTIONCODE,
	EXCEPTIONMESSAGE,
        SERVICECOMMENT,
	VALUE,
	XREF
    };
    static String[] pcdataNamesArrayForPrimitives = {
	MOBYSTRING,
	MOBYINTEGER,
	MOBYFLOAT,
	MOBYBOOLEAN,
	MOBYDATETIME
    };
    static Set<String> pcdataNames = new HashSet<String>();
    static {
	for (int i = 0; i < pcdataNamesArray.length; i++) {
	    pcdataNames.add (pcdataNamesArray[i]);
	}
    }
    static Set<String> pcdataNamesForPrimitives = new HashSet<String>();
    static {
	for (int i = 0; i < pcdataNamesArrayForPrimitives.length; i++) {
	    pcdataNamesForPrimitives.add (pcdataNamesArrayForPrimitives[i]);
	}
    }

    /*********************************************************************
     * Globals used during XML parsing - it is thread safe because
     * they all are used from the parser.parse() method which is
     * called only from a private and synchronized method _parse(),
     * and the Parser itself does not use any threads (at least I hope
     * :-)).
     ********************************************************************/

    MapDataTypesIfc mapDataTypes;  // dynamically created (org.biomoby.shared.datatypes.MapDataTypes)
    Stack<Object> objectStack;  // it has elements of type Object
    Stack<StringBuffer> pcdataStack;  // it has elements of type StringBuffer
    boolean readingMobyObject;   // true if inside Simple
    boolean readingXrefs;        // true if inside CrossReference
    boolean readingProvision;    // true if inside Provision[Information]
    boolean inSubstitution;  // true when just using 'lowestKnownDataType'
    boolean inServiceNotes;      // true if inside serviceNotes tag
    boolean inMobyException;     // true if inside mobyException tag
    int ignoring;   // count depth of ignored (unknown) data objects
    MobyPackage result; // here is the whole result...

    /*********************************************************************
     * Load mapping class between Biomoby data types names and their
     * Java representations. <p>
     *
     * @throws MobyException if the mapping class was not found (which
     * can happen because the mapping class is usually generated
     * together with generating all data types - so if you forgot to
     * do it first, the class does not exist and an exception is
     * thrown)
     ********************************************************************/
    public static MapDataTypesIfc loadB2JMapping()
	throws MobyException {

	final String MAPPING_CLASS = "org.biomoby.shared.datatypes.MapDataTypes";

	// KEEP THIS HERE!  This is not only about making sure that
	// the class MapDataTypes was generated (as the message
	// indicates) but also that is properly loaded before being
	// used - see explanation in the class itself.
	try {
	    MapDataTypesIfc mapping =
		(MapDataTypesIfc)ICreator.createInstance (MAPPING_CLASS);
	    mapping.makeSureItIsLoaded();
	    return mapping;
	} catch (Throwable e) {
	    throw new MobyException
		("Class '" + MAPPING_CLASS + "' was not found.\n" +
		 "It may indicate that you have not generated all Biomoby data types from a Biomoby registry.\n" +
		 "See http://www.biomoby.org/moby-live/Java/docs/Moses.html for details.\n" +
		 "If you are a jMoby developer just type: ant moses-datatypes.\n" +
		 "Or perhaps, they just need to be compiled: ant moses-compile.");
	}
    }

    /*********************************************************************
     * This is the main method doing all the job...
     *
     * Notes for developers/maintainers:
     *
     * The parser has several modes, controlled by various global
     * boolean, some of them are listed here:
     *
     *    'readingXrefs' indicates that a cross references section is
     * being read.
     *
     *    'readingProvision; indicates that a provision section is
     * being read.
     *
     *    'readingMobyObject' indicates that we are currently inside a
     * Simple tag.
     *
     *    'ignoring' happens when an unknown top-level object was
     * found BUT we do not have any substitutee (see below
     * 'inSubstitution' mode) so we are just ignoring this object and
     * all its descendants (with a warning message).
     *
     *    The same mode is also entered when an unknown member object
     * was found. But here we issue a warning message only if we are
     * NOT in substitution mode (in a substitution mode it is normal
     * to found unknown objects).
     *
     *    'inSubstitution' is a mode when a top-level object (not a
     * member object) was not found AND a substitutee was provided in
     * the constructor ("lowestKnownDataType").
     *
     *    Note, that both 'ignoring' and 'inSubstitution' modes should
     * happen only when the available (generated) data type classes
     * are not up-to-date (or if someone is sending us bogus objects).
     ********************************************************************/
    private synchronized MobyPackage _parse (InputSource xmlSource)
	throws MobyException {

 	try {
	    // Create a parser and register handlers (do it only once)
	    if (parser == null)
		parser = XMLUtils2.makeXMLParser (this);

	    mapDataTypes = loadB2JMapping();

	    // Parse it!
	    parser.parse (xmlSource);
	    if (result == null) {
		throw new MobyException ("Parsing XML failed, and I do not know why. \n" +
					 "Panic... (or send the XML to a jMoby developer)\n");
	    }
	    return result;

	} catch (GException e) {
	    throw new MobyException ("Error in creating XML parser " + e.getMessage());

	} catch (SAXException e) {
	    throw new MobyException ("Error in the XML input.\n" +
				     XMLUtils2.getFormattedError (e));
	} catch (IOException e) {
	    throw new MobyException ("Error by reading XML input: " + e.toString());

	} catch (Error e) {
	    throw new MobyException ("Serious or unexpected error!\n" +
				     e.toString(), e);
	}
    }

    /*********************************************************************
     *
     *                 XML-SAX 2.0 handler routines.
     *
     ********************************************************************/

    /*********************************************************************
     * Called at the beginning of the parsed document.
     ********************************************************************/
    public void startDocument()
	throws SAXException {
	objectStack = new Stack<Object>();
	pcdataStack = new Stack<StringBuffer>();
	ignoring = 0;
    }

    /*********************************************************************
     * Called at the end of the parsed document.
     ********************************************************************/
    public void endDocument()
	throws SAXException {
 	objectStack = null;
 	pcdataStack = null;
	ignoring = 0;
    }

    /*********************************************************************
     * Called when an element starts.
     ********************************************************************/
    public void startElement (String namespaceURI, String name, String qName,
			      Attributes attrs)
	throws SAXException {

	// do nothing; just ignore
	if (ignoring > 0) {
	    ignoring++;
	    return;
	}

	//
	// Tags that can have #PCDATA elements...  For them I create
	// an entry on pcdataStack (which will be removed in
	// endElement() so it does not hurt if there are no text data
	// found).
	//
	if (pcdataNames.contains (name)) {
  	    pcdataStack.push (new StringBuffer());
	}
	if (readingMobyObject) {
	    if (pcdataNamesForPrimitives.contains (name)) {
		pcdataStack.push (new StringBuffer());
	    }
	}

	//
	// This is the most interesting part - the Biomoby data
	// objects (carrying the real data). Do it only if we are
	// inside a Simple...
	//
	if (readingMobyObject) {

	    //
	    // first process optional CRIBs and PIBs
	    //
	    if (name.equals (PROVISIONINFORMATION)) {
		objectStack.push (new MobyProvisionInfo());
		readingProvision = true;

	    } else if (name.equals (CROSSREFERENCE)) {
		readingXrefs = true;

	    } else if (readingXrefs) {
		if (name.equals (XREF)) {
		    MobyXref xref = new MobyXref();
		    xref.setId (getValue (attrs, OBJ_ID));
		    xref.setNamespace (getValue (attrs, OBJ_NAMESPACE));
		    try {
			xref.setEvidenceCode (getValue (attrs, EVIDENCECODE));
		    } catch (MobyException e) {
			throw error (e.getMessage());
		    }
		    String serviceName = getValue (attrs, SERVICENAME);
		    if (serviceName != null) {
			MobyService service = new MobyService (serviceName);
			service.setAuthority (getValue (attrs, AUTHURI));
			xref.setService (service);
		    }
		    objectStack.push (xref);

		} else if (name.equals (MOBYOBJECT)) {
		    MobyXref xref = new MobyXref();
		    xref.setNamespace (getValue (attrs, OBJ_NAMESPACE));
		    xref.setId (getValue (attrs, OBJ_ID));
		    xref.setSimple (true);
		    objectStack.push (xref);
		}

	    } else if (readingProvision) {
		if (name.equals (SERVICESOFTWARE)) {
		    MobyProvisionInfo info = ((MobyProvisionInfo)vPeek (MobyProvisionInfo.class));
		    info.setSoftwareName (getValue (attrs, SOFTWARENAME));

 		    String version = getValue (attrs, SOFTWAREVERSION);
		    if (version == null) version = getValue (attrs, PLAINVERSION);
 		    info.setSoftwareVersion (version);

 		    String comment = getValue (attrs, SOFTWARECOMMENT);
		    if (comment == null) comment = getValue (attrs, COMMENT);
		    info.setSoftwareComment (comment);

		} else if (name.equals (SERVICEDATABASE)) {
		    MobyProvisionInfo info = ((MobyProvisionInfo)vPeek (MobyProvisionInfo.class));
		    info.setDBName (getValue (attrs, DATABASENAME));

		    String version = getValue (attrs, DATABASEVERSION);
		    if (version == null) version = getValue (attrs, PLAINVERSION);
		    info.setDBVersion (version);

		    String comment = getValue (attrs, DATABASECOMMENT);
		    if (comment == null) comment = getValue (attrs, COMMENT);
		    info.setDBComment (comment);
		}

	    } else {

		//
		// finally, here we deal with the real data objects
		//
		try {
		    Class theClass = mapDataTypes.getClass (name);
		    if (theClass == null) {

			Object obj = objectStack.peek();

			// is this a 'top-level' object?
			if (obj instanceof MobySimple) {

			    // ...and only if we have a substitute
			    if (lowestKnownDataType != null) {
				theClass = mapDataTypes.getClass (lowestKnownDataType);
			    // ...or more substitutes
			    } else {
				String articleName = ((MobySimple)obj).getName();
				if (articleName != null &&
				    lowestKnownDataTypes.containsKey (articleName)) {
				    theClass =
					mapDataTypes.getClass (lowestKnownDataTypes.get (articleName));
				} else {
				    // ...if we are in a collection,
				    // we need to use the article name
				    // of that collection
				    MobyCollection collObj = peekCollection();
				    if (collObj != null) {
					articleName = collObj.getName();
					if (articleName != null &&
					    lowestKnownDataTypes.containsKey (articleName)) {
					    theClass =
						mapDataTypes.getClass
						(lowestKnownDataTypes.get (articleName));
					}
				    }
				}
			    }

			    // ...whose Class is known to us
			    if (theClass != null) {
				inSubstitution = true;
				if (log.isWarnEnabled()) {
				    log.warn ("Object '" + name +
					      "' substituted by '" +
					      theClass.getSimpleName() + "'.");
				}
			    }

			} else {
			    // no, it is a member (and an unknown one)
			    String articleName = getValue (attrs, ARTICLENAME);
			    theClass = articleName2Class (obj, articleName);
			    if (theClass != null && log.isWarnEnabled()) {
				log.warn ("Object '" + obj.getClass().getSimpleName() +
					  "' has an unknown member '" + name +
					  "' (article name '" + articleName +
					  "'). Substituted by '" +
					  theClass.getSimpleName() +
					  "'.");
			    }
			}
		    }

		    if (theClass != null) {
			MobyObject mobyObj = (MobyObject)ICreator.createInstance (theClass);
			mobyObj.setNamespace (getValue (attrs, OBJ_NAMESPACE));
			mobyObj.setId (getValue (attrs, OBJ_ID));
			// note that the articleName makes sense here only for HAS[A] objects
			mobyObj.setName (getValue (attrs, ARTICLENAME));
			objectStack.push (mobyObj);
		    } else {
			// if we still do not have any class from this
			// element, we ignore it - and also all its children
			ignoring++;
			if (! inSubstitution)
			    log.warn ("Ignoring unknown element '" + name + "'.");
		    }
		} catch (MobyException e) {
		    throw error (e.getMessage());
		} catch (GException e) {
		    throw error (e.getMessage());
		}
	    }

	//
	// The tags not related with the real Biomoby data objects
	//

	} else if (name.equals (MOBY)) {
	    objectStack.push (new MobyPackage());

	} else if (name.equals (MOBYCONTENT)) {
	    String str = getValue (attrs, AUTHORITY);
	    if (str != null)
		((MobyPackage)vPeek (MobyPackage.class)).setAuthority (str);

	} else if (name.equals (MOBYDATA)) {
	    MobyJob job = new MobyJob();
	    job.setId (getValue (attrs, QUERYID));
	    objectStack.push (job);

	} else if (name.equals (SERVICENOTES)) {
	    inServiceNotes = true;

	} else if (name.equals (MOBYEXCEPTION)) {
	    if (inServiceNotes) {
		ServiceException se = new ServiceException();
		se.setSeverity (getValue (attrs, SEVERITY));
		se.setJobId (getValue (attrs, REFQUERYID));
		se.setDataName (getValue (attrs, REFELEMENT));
		objectStack.push (se);
		inMobyException = true;
	    }

	} else if (name.equals (SIMPLE)) {
	    MobySimple simple = new MobySimple();
	    simple.setName (getValue (attrs, ARTICLENAME));
	    objectStack.push (simple);
	    readingMobyObject = true;

	} else if (name.equals (COLLECTION)) {
	    MobyCollection collection = new MobyCollection();
	    collection.setName (getValue (attrs, ARTICLENAME));
	    objectStack.push (collection);

	} else if (name.equals (PARAMETER)) {
	    MobyParameter parameter = new MobyParameter();
	    parameter.setName (getValue (attrs, ARTICLENAME));
	    objectStack.push (parameter);

	}
    }

    /*********************************************************************
     * Called at the end of an element.
     ********************************************************************/
    public void endElement (String namespaceURI, String name, String qName)
	throws SAXException {
	Object obj, obj2;

	// do nothing; just ignore less
	if (ignoring > 0) {
	    ignoring--;
	    return;
	}

	//
	// The tags carrying the real Biomoby data (remember that they
	// are dealt with only when inside a Simple, and only when
	// their class is known)
	// 

	if (readingMobyObject) {
	    if (name.equals (SIMPLE)) {
		obj = objectStack.pop();    // this is MobySimple
		obj2 = objectStack.peek();  // this is a container for Simples
		if (obj2 instanceof MobyJob)
		    ((MobyJob)obj2).addDataElement ((MobySimple)obj);
		else if (obj2 instanceof MobyCollection)
		    ((MobyCollection)obj2).addData ((MobySimple)obj);
		else
		    throw error ("A Simple element should not be in '" + obj2.getClass().getName() + "'.");
		readingMobyObject = false;

	    } else if (name.equals (PROVISIONINFORMATION)) {
		obj = objectStack.pop();
		((MobyObject)vPeek (MobyObject.class)).setProvision ((MobyProvisionInfo)obj);
		readingProvision = false;
		
	    } else if (name.equals (CROSSREFERENCE)) {
		readingXrefs = false;

	    } else if (readingXrefs) {
		if (name.equals (XREF)) {
		    obj = pcdataStack.pop();
		    ((MobyXref)vPeek (MobyXref.class)).setDescription (new String ((StringBuffer)obj));
		    obj = objectStack.pop();
		    ((MobyObject)vPeek (MobyObject.class)).addXref ((MobyXref)obj);

		} else if (name.equals (MOBYOBJECT)) {
		    obj = objectStack.pop();
		    ((MobyObject)vPeek (MobyObject.class)).addXref ((MobyXref)obj);
		}

	    } else if (readingProvision) {
		if (name.equals (SERVICECOMMENT)) {
		    obj = pcdataStack.pop();
		    ((MobyProvisionInfo)vPeek (MobyProvisionInfo.class)).setComment (new String ((StringBuffer)obj));
		}

	    } else {
		//
		// finally, here we deal with the real data objects
		//

		// this is the just-finished MobyObject
		MobyObject mobyObj = (MobyObject)objectStack.pop();

		try {

		    // primitive types may have a PCDATA value
		    if (pcdataNamesForPrimitives.contains (name)) {
			String value = new String (pcdataStack.pop());
			mobyObj.setValue (value);
		    }

		    // put just-finished MobyObject into its container
		    obj2 = objectStack.peek();
		    if (obj2 instanceof MobySimple) {
			((MobySimple)obj2).setData (mobyObj);
			// ...and forget about (potential) substitution
			inSubstitution = false;

		    } else {
			String methodName = articleName2methodName (mobyObj);
			try {
			    callMethod ((MobyObject)obj2, methodName, mobyObj);
			} catch (SAXException e2) {
			    log.warn ("Object type '" + name +
				      "' in object '" + obj2.getClass().getSimpleName() +
				      "' (or in its child), with article name '" + mobyObj.getName() +
				      "', is ignored.");
			}
		    }

		} catch (MobyException e) {
		    throw error (e.getMessage());
		}
	    }

	    //
	    // Non-real-data tags...
	    //

	} else if (name.equals (MOBY)) {
	    if (objectStack.isEmpty())
		throw error ("Nothing came out from the parsed XML data.");
	    obj = objectStack.pop();
	    if (! (obj instanceof MobyPackage))
		throw error ("The input XML does not start with a MOBY tag.");
	    result = (MobyPackage)obj;

	} else if (name.equals (MOBYCONTENT)) {
	    // nothing to do

	} else if (name.equals (SERVICENOTES)) {
	    inServiceNotes = false;

	} else if (name.equals (MOBYEXCEPTION)) {
	    if (inServiceNotes) {
		obj = objectStack.pop();
		((MobyPackage)vPeek (MobyPackage.class)).addException ((ServiceException)obj);
		inMobyException = false;
	    }

	} else if (name.equals (MOBYDATA)) {
	    obj = objectStack.pop();
	    ((MobyPackage)vPeek (MobyPackage.class)).addJob ((MobyJob)obj);

	} else if (name.equals (COLLECTION)) {
	    obj = objectStack.pop();
	    ((MobyJob)vPeek (MobyJob.class)).addDataElement ((MobyCollection)obj);

	} else if (name.equals (PARAMETER)) {
	    obj = objectStack.pop();
	    ((MobyJob)vPeek (MobyJob.class)).addDataElement ((MobyParameter)obj);

	    //
	    // ...non-real-data tags with text contents
	    //

	} else if (name.equals (NOTES)) {
	    obj = pcdataStack.pop();
	    if (inServiceNotes) {
		((MobyPackage)vPeek (MobyPackage.class)).setServiceNotes (new String ((StringBuffer)obj));
	    }
	} else if (name.equals (EXCEPTIONCODE)) {
	    obj = pcdataStack.pop();
	    if (inMobyException) {
		try {
		    int code = new Integer (new String ((StringBuffer)obj)).intValue();
		    ((ServiceException)vPeek (ServiceException.class)).setErrorCode (code);
		} catch (NumberFormatException exnum) {
		}
	    }
	} else if (name.equals (EXCEPTIONMESSAGE)) {
	    obj = pcdataStack.pop();
	    if (inMobyException) {
		((ServiceException)vPeek (ServiceException.class))
		    .setMessage (new String ((StringBuffer)obj));
	    }
	} else if (name.equals (VALUE)) {
	    obj = pcdataStack.pop();
	    ((MobyParameter)vPeek (MobyParameter.class)).setValue (new String ((StringBuffer)obj));

	}
    }

    /*********************************************************************
     * Called for #PCDATA.
     ********************************************************************/
    public void characters (char[] ch, int start, int length) {
	// ignore white-spaces, and text where should not be any
        if (pcdataStack.empty()) return;

        StringBuffer buf = pcdataStack.peek();
        buf.append (ch, start, length);
    }

    /*********************************************************************
     *
     ********************************************************************/
    protected SAXParseException error (String message) {
 	return new SAXParseException ("", locator, new MobyException (message));
    }

    /*********************************************************************
     *
     ********************************************************************/
    protected Object vPeek (Class shouldBeThere)
	throws SAXException {
	Object obj = objectStack.peek();
	if (shouldBeThere.isInstance (obj))
	    return obj;
	throw error ("Wrong XML: Expected '" + shouldBeThere.getName() +
		     "' - but found '" + obj.getClass().getName() + "'.");
    }

    /*********************************************************************
     *
     ********************************************************************/
    static protected String getValue (Attributes attrs, String name) {
	String str = attrs.getValue (name);
 	if (str == null)
 	    str = attrs.getValue (MOBY_XML_NS, name);
	return str;
    }

    /*********************************************************************
     *
     ********************************************************************/
    static protected String articleName2methodName (MobyObject mobyObj) {

	return "set_" + 
	    Utils.mobyEscape
	    ( Utils.javaEscape
	      ( Utils.checkOrCreateArticleName (mobyObj.getName(),
						mobyObj.getClass().getName())));
    }

    /*********************************************************************
     * An 'obj' should be a MobyObject instance that should have a
     * method for setting given 'articleName'. If not return null. If
     * yes, use reflection to find what is the return type of this
     * method and return such class.
     *
     * This is used when an object has an unknown member (its type is
     * unknown because it can be a more specialized one, but its
     * article name is known).
     ********************************************************************/
    static protected Class articleName2Class (Object obj,
					      String articleName) {
	if (! (obj instanceof MobyObject)) {
	    log.error ("Unexpected object of type '" + obj.getClass().getName() +
		       "' when a MobyObj was expected.");
	    return null;
	}
	if (StringUtils.isBlank (articleName)) {
	    log.error ("An unknown member found in object '" +
		       obj.getClass().getName() +
		       "' that even does not have any article name.");
	    return null;
	}

	// this is the method we are looking for
	String methodName =
	    "getMoby_" + Utils.mobyEscape (Utils.javaEscape (articleName.trim()));

	// here are all methods of the class whose member is unknown
	for (Method method: obj.getClass().getMethods()) {
	    if (! methodName.equals (method.getName()))
		continue;
	    Class returnType = method.getReturnType();
	    if (returnType.isArray()) {
		return returnType.getComponentType();
	    } else {
		return returnType;
	    }
	}
	log.error ("An unknown member found in object '" +
		   obj.getClass().getName() +
		   "' with unrecognized article name '" +
		   articleName + "'.");
	return null;
    }

    /*********************************************************************
     * Return the second last object from the 'objectStack' if it is a
     * MobyCollectin, otherwise it returns null.
     ********************************************************************/
    protected MobyCollection peekCollection() {
	if (objectStack.size() < 2)
	    return null;
	Object obj = objectStack.get (objectStack.size() - 2);
	if (obj instanceof MobyCollection) {
	    return (MobyCollection)obj;
	} else {
	    return null;
	}
    }

    /*********************************************************************
     * Call a method (named 'methodName') on object 'actor', using
     * 'parameter'.
     ********************************************************************/
    protected void callMethod (MobyObject actor,
			       String methodName,
			       MobyObject parameter)
	throws SAXException {

	try {
	    Method method = actor.getClass().getMethod (methodName, new Class[] { parameter.getClass() });
	    method.invoke (actor, new Object[] { parameter });

	} catch (NoSuchMethodException e) {
	    StringBuffer buf = new StringBuffer();
	    buf.append ("Method '" + methodName);
	    buf.append ("' was not found in the object '" + actor.getClass().getName());
	    buf.append ("'.\nThis is a configuration error (call a jMoby developer).\n\n");
	    buf.append ("The object has only following public methods:\n");
	    Method[] methods = actor.getClass().getMethods();
	    for (int i = 0; i < methods.length; i++) {
		buf.append ("\t");
		buf.append (methods[i]);
		buf.append ("\n");
	    }
	    throw error (new String (buf));

	} catch (IllegalAccessException e) {
	    throw error ("IllegalAccessException: " + e.getMessage());
	} catch (IllegalArgumentException e) {
	    throw error ("IllegalArgumentException: " + e.getMessage());
	} catch (InvocationTargetException e) {
	    throw error ("InvocationTargetException: " + e.getTargetException().toString());
	}
    }

}
