/* StdXMLParser.java                                               NanoXML/Java
 *
 * $Revision: 1.5 $
 * $Date: 2002/03/24 11:37:00 $
 * $Name: RELEASE_2_2_1 $
 *
 * This file is part of NanoXML 2 for Java.
 * Copyright (C) 2000-2002 Marc De Scheemaecker, All Rights Reserved.
 *
 * This software is provided 'as-is', without any express or implied warranty.
 * In no event will the authors be held liable for any damages arising from the
 * use of this software.
 *
 * Permission is granted to anyone to use this software for any purpose,
 * including commercial applications, and to alter it and redistribute it
 * freely, subject to the following restrictions:
 *
 *  1. The origin of this software must not be misrepresented; you must not
 *     claim that you wrote the original software. If you use this software in
 *     a product, an acknowledgment in the product documentation would be
 *     appreciated but is not required.
 *
 *  2. Altered source versions must be plainly marked as such, and must not be
 *     misrepresented as being the original software.
 *
 *  3. This notice may not be removed or altered from any source distribution.
 */

package processing.xml;


import java.io.Reader;
import java.util.Enumeration;
import java.util.Properties;
import java.util.Vector;


/**
 * StdXMLParser is the core parser of NanoXML.
 *
 * @author Marc De Scheemaecker
 * @version $Name: RELEASE_2_2_1 $, $Revision: 1.5 $
 */
public class StdXMLParser {

   /**
    * The builder which creates the logical structure of the XML data.
    */
   private StdXMLBuilder builder;


   /**
    * The reader from which the parser retrieves its data.
    */
   private StdXMLReader reader;


   /**
    * The entity resolver.
    */
   private XMLEntityResolver entityResolver;


   /**
    * The validator that will process entity references and validate the XML
    * data.
    */
   private XMLValidator validator;


   /**
    * Creates a new parser.
    */
   public StdXMLParser()
   {
      this.builder = null;
      this.validator = null;
      this.reader = null;
      this.entityResolver = new XMLEntityResolver();
   }


   /**
    * Cleans up the object when it's destroyed.
    */
   protected void finalize()
      throws Throwable
   {
      this.builder = null;
      this.reader = null;
      this.entityResolver = null;
      this.validator = null;
      super.finalize();
   }


   /**
    * Sets the builder which creates the logical structure of the XML data.
    *
    * @param builder the non-null builder
    */
   public void setBuilder(StdXMLBuilder builder)
   {
      this.builder = builder;
   }


   /**
    * Returns the builder which creates the logical structure of the XML data.
    *
    * @return the builder
    */
   public StdXMLBuilder getBuilder()
   {
      return this.builder;
   }


   /**
    * Sets the validator that validates the XML data.
    *
    * @param validator the non-null validator
    */
   public void setValidator(XMLValidator validator)
   {
      this.validator = validator;
   }


   /**
    * Returns the validator that validates the XML data.
    *
    * @return the validator
    */
   public XMLValidator getValidator()
   {
      return this.validator;
   }


   /**
    * Sets the entity resolver.
    *
    * @param resolver the non-null resolver
    */
   public void setResolver(XMLEntityResolver resolver)
   {
      this.entityResolver = resolver;
   }


   /**
    * Returns the entity resolver.
    *
    * @return the non-null resolver
    */
   public XMLEntityResolver getResolver()
   {
      return this.entityResolver;
   }


   /**
    * Sets the reader from which the parser retrieves its data.
    *
    * @param reader the reader
    */
   public void setReader(StdXMLReader reader)
   {
      this.reader = reader;
   }


   /**
    * Returns the reader from which the parser retrieves its data.
    *
    * @return the reader
    */
   public StdXMLReader getReader()
   {
      return this.reader;
   }


   /**
    * Parses the data and lets the builder create the logical data structure.
    *
    * @return the logical structure built by the builder
    *
    * @throws XMLException.n3.nanoxml.XMLException
    *		if an error occurred reading or parsing the data
    */
   public Object parse()
      throws XMLException
   {
      try {
         this.builder.startBuilding(this.reader.getSystemID(),
                                    this.reader.getLineNr());
         this.scanData();
         return this.builder.getResult();
      } catch (XMLException e) {
         throw e;
      } catch (Exception e) {
         throw new XMLException(e);
      }
   }


   /**
    * Scans the XML data for elements.
    *
    * @throws java.lang.Exception
    *     if something went wrong
    */
   protected void scanData() throws Exception {
      while ((! this.reader.atEOF()) && (this.builder.getResult() == null)) {
         String str = XMLUtil.read(this.reader, '&');
         char ch = str.charAt(0);
         if (ch == '&') {
            XMLUtil.processEntity(str, this.reader, this.entityResolver);
            continue;
         }

         switch (ch) {
            case '<':
               this.scanSomeTag(false, // don't allow CDATA
                                null,  // no default namespace
                                new Properties());
               break;

            case ' ':
            case '\t':
            case '\r':
            case '\n':
               // skip whitespace
               break;

            default:
               XMLUtil.errorInvalidInput(reader.getSystemID(),
                                         reader.getLineNr(),
                                         "`" + ch + "' (0x"
                                         + Integer.toHexString((int) ch)
                                         + ')');
         }
      }
   }


   /**
    * Scans an XML tag.
    *
    * @param allowCDATA true if CDATA sections are allowed at this point
    * @param defaultNamespace the default namespace URI (or null)
    * @param namespaces list of defined namespaces
    *
    * @throws java.lang.Exception
    *     if something went wrong
    */
   protected void scanSomeTag(boolean allowCDATA,
                              String defaultNamespace,
                              Properties namespaces)
      throws Exception
   {
      String str = XMLUtil.read(this.reader, '&');
      char ch = str.charAt(0);

      if (ch == '&') {
         XMLUtil.errorUnexpectedEntity(reader.getSystemID(),
                                       reader.getLineNr(),
                                       str);
      }

      switch (ch) {
         case '?':
            this.processPI();
            break;

         case '!':
            this.processSpecialTag(allowCDATA);
            break;

         default:
            this.reader.unread(ch);
            this.processElement(defaultNamespace, namespaces);
      }
   }


   /**
    * Processes a "processing instruction".
    *
    * @throws java.lang.Exception
    *     if something went wrong
    */
   protected void processPI()
      throws Exception
   {
      XMLUtil.skipWhitespace(this.reader, null);
      String target = XMLUtil.scanIdentifier(this.reader);
      XMLUtil.skipWhitespace(this.reader, null);
      Reader r = new PIReader(this.reader);
      
      if (!target.equalsIgnoreCase("xml")) {
         this.builder.newProcessingInstruction(target, r);
      }

      r.close();
   }


   /**
    * Processes a tag that starts with a bang (&lt;!...&gt;).
    *
    * @param allowCDATA true if CDATA sections are allowed at this point
    *
    * @throws java.lang.Exception
    *     if something went wrong
    */
   protected void processSpecialTag(boolean allowCDATA)
      throws Exception
   {
      String str = XMLUtil.read(this.reader, '&');
      char ch = str.charAt(0);

      if (ch == '&') {
         XMLUtil.errorUnexpectedEntity(reader.getSystemID(),
                                       reader.getLineNr(),
                                       str);
      }

      switch (ch) {
         case '[':
            if (allowCDATA) {
               this.processCDATA();
            } else {
               XMLUtil.errorUnexpectedCDATA(reader.getSystemID(),
                                            reader.getLineNr());
            }

            return;

         case 'D':
            this.processDocType();
            return;

         case '-':
            XMLUtil.skipComment(this.reader);
            return;
      }
   }


   /**
    * Processes a CDATA section.
    *
    * @throws java.lang.Exception
    *     if something went wrong
    */
   protected void processCDATA() throws Exception {
      if (! XMLUtil.checkLiteral(this.reader, "CDATA[")) {
         XMLUtil.errorExpectedInput(reader.getSystemID(),
                                    reader.getLineNr(),
                                    "<![[CDATA[");
      }

      this.validator.PCDataAdded(this.reader.getSystemID(),
                                 this.reader.getLineNr());
      Reader r = new CDATAReader(this.reader);
      this.builder.addPCData(r, this.reader.getSystemID(),
                             this.reader.getLineNr());
      r.close();
   }


   /**
    * Processes a document type declaration.
    *
    * @throws java.lang.Exception
    *		if an error occurred reading or parsing the data
    */
   protected void processDocType() throws Exception {
      if (! XMLUtil.checkLiteral(this.reader, "OCTYPE")) {
         XMLUtil.errorExpectedInput(reader.getSystemID(),
                                    reader.getLineNr(),
                                    "<!DOCTYPE");
         return;
      }

      XMLUtil.skipWhitespace(this.reader, null);
      String systemID = null;
      StringBuffer publicID = new StringBuffer();
      /*String rootElement =*/ XMLUtil.scanIdentifier(this.reader);
      //System.out.println("rootElement is " + rootElement);
      XMLUtil.skipWhitespace(this.reader, null);
      char ch = this.reader.read();

      if (ch == 'P') {
         systemID = XMLUtil.scanPublicID(publicID, reader);
         XMLUtil.skipWhitespace(this.reader, null);
         ch = this.reader.read();
      } else if (ch == 'S') {
         systemID = XMLUtil.scanSystemID(reader);
         XMLUtil.skipWhitespace(this.reader, null);
         ch = this.reader.read();
      }

      if (ch == '[') {
         this.validator.parseDTD(publicID.toString(),
                                 this.reader,
                                 this.entityResolver,
                                 false);
         XMLUtil.skipWhitespace(this.reader, null);
         ch = this.reader.read();
      }

      if (ch != '>') {
         XMLUtil.errorExpectedInput(reader.getSystemID(),
                                    reader.getLineNr(),
                                    "`>'");
      }

      // TODO DTD checking is currently disabled, because it breaks 
      //      applications that don't have access to a net connection 
      //      (since it insists on going and checking out the DTD).
      if (false) {
    	  if (systemID != null) {
    		  Reader r = this.reader.openStream(publicID.toString(), systemID);
    		  this.reader.startNewStream(r);
    		  this.reader.setSystemID(systemID);
    		  this.reader.setPublicID(publicID.toString());
    		  this.validator.parseDTD(publicID.toString(),
    				  			      this.reader,	
    				  			      this.entityResolver,
    				  			      true);
    	  }
      }
   }


   /**
    * Processes a regular element.
    *
    * @param defaultNamespace the default namespace URI (or null)
    * @param namespaces list of defined namespaces
    *
    * @throws java.lang.Exception
    *     if something went wrong
    */
   protected void processElement(String defaultNamespace,
                                 Properties namespaces)
      throws Exception
   {
      String fullName = XMLUtil.scanIdentifier(this.reader);
      String name = fullName;
      XMLUtil.skipWhitespace(this.reader, null);
      String prefix = null;
      int colonIndex = name.indexOf(':');

      if (colonIndex > 0) {
         prefix = name.substring(0, colonIndex);
         name = name.substring(colonIndex + 1);
      }

      Vector<String> attrNames = new Vector<String>();
      Vector<String> attrValues = new Vector<String>();
      Vector<String> attrTypes = new Vector<String>();

      this.validator.elementStarted(fullName,
                                    this.reader.getSystemID(),
                                    this.reader.getLineNr());
      char ch;

      for (;;) {
         ch = this.reader.read();

         if ((ch == '/') || (ch == '>')) {
            break;
         }

         this.reader.unread(ch);
         this.processAttribute(attrNames, attrValues, attrTypes);
         XMLUtil.skipWhitespace(this.reader, null);
      }

      Properties extraAttributes = new Properties();
      this.validator.elementAttributesProcessed(fullName,
                                                extraAttributes,
                                                this.reader.getSystemID(),
                                                this.reader.getLineNr());
      Enumeration<?> en = extraAttributes.keys();

      while (en.hasMoreElements()) {
         String key = (String) en.nextElement();
         String value = extraAttributes.getProperty(key);
         attrNames.addElement(key);
         attrValues.addElement(value);
         attrTypes.addElement("CDATA");
      }

      // post 1.2.1, just treat namespaces like any other attribute
//      for (int i = 0; i < attrNames.size(); i++) {
//         String key = (String) attrNames.elementAt(i);
//         String value = (String) attrValues.elementAt(i);
//         //String type = (String) attrTypes.elementAt(i);

//         if (key.equals("xmlns")) {
//            defaultNamespace = value;
//         } else if (key.startsWith("xmlns:")) {
//            namespaces.put(key.substring(6), value);
//         }
//      }

      if (prefix == null) {
         this.builder.startElement(name, prefix, defaultNamespace,
                                   this.reader.getSystemID(),
                                   this.reader.getLineNr());
      } else {
         this.builder.startElement(name, prefix,
                                   namespaces.getProperty(prefix),
                                   this.reader.getSystemID(),
                                   this.reader.getLineNr());
      }

      for (int i = 0; i < attrNames.size(); i++) {
         String key = (String) attrNames.elementAt(i);

//         if (key.startsWith("xmlns")) {
//            continue;
//         }

         String value = (String) attrValues.elementAt(i);
         String type = (String) attrTypes.elementAt(i);
         colonIndex = key.indexOf(':');

         if (colonIndex > 0) {
            String attPrefix = key.substring(0, colonIndex);
            key = key.substring(colonIndex + 1);
            this.builder.addAttribute(key, attPrefix,
                                      namespaces.getProperty(attPrefix),
                                      value, type);
         } else {
            this.builder.addAttribute(key, null, null, value, type);
         }
      }

      if (prefix == null) {
         this.builder.elementAttributesProcessed(name, prefix,
                                                 defaultNamespace);
      } else {
         this.builder.elementAttributesProcessed(name, prefix,
                                                 namespaces
                                                   .getProperty(prefix));
      }

      if (ch == '/') {
         if (this.reader.read() != '>') {
            XMLUtil.errorExpectedInput(reader.getSystemID(),
                                       reader.getLineNr(),
                                       "`>'");
         }

         this.validator.elementEnded(name,
                                     this.reader.getSystemID(),
                                     this.reader.getLineNr());

         if (prefix == null) {
            this.builder.endElement(name, prefix, defaultNamespace);
         } else {
            this.builder.endElement(name, prefix,
                                    namespaces.getProperty(prefix));
         }

         return;
      }

      StringBuffer buffer = new StringBuffer(16);

      for (;;) {
         buffer.setLength(0);
         String str;

         for (;;) {
            XMLUtil.skipWhitespace(this.reader, buffer);
            str = XMLUtil.read(this.reader, '&');

            if ((str.charAt(0) == '&') && (str.charAt(1) != '#')) {
               XMLUtil.processEntity(str, this.reader,
                                     this.entityResolver);
            } else {
               break;
            }
         }

         if (str.charAt(0) == '<') {
            str = XMLUtil.read(this.reader, '\0');

            if (str.charAt(0) == '/') {
               XMLUtil.skipWhitespace(this.reader, null);
               str = XMLUtil.scanIdentifier(this.reader);

               if (! str.equals(fullName)) {
                  XMLUtil.errorWrongClosingTag(reader.getSystemID(),
                                               reader.getLineNr(),
                                               name, str);
               }

               XMLUtil.skipWhitespace(this.reader, null);

               if (this.reader.read() != '>') {
                  XMLUtil.errorClosingTagNotEmpty(reader.getSystemID(),
                                                  reader.getLineNr());
               }

               this.validator.elementEnded(fullName,
                                           this.reader.getSystemID(),
                                           this.reader.getLineNr());
               if (prefix == null) {
                   this.builder.endElement(name, prefix, defaultNamespace);
               } else {
                   this.builder.endElement(name, prefix,
                                           namespaces.getProperty(prefix));
               }
               break;
            } else { // <[^/]
               this.reader.unread(str.charAt(0));
               this.scanSomeTag(true, //CDATA allowed
                                defaultNamespace,
                                (Properties) namespaces.clone());
            }
         } else { // [^<]
            if (str.charAt(0) == '&') {
               ch = XMLUtil.processCharLiteral(str);
               buffer.append(ch);
            } else {
               reader.unread(str.charAt(0));
            }
            this.validator.PCDataAdded(this.reader.getSystemID(),
                                       this.reader.getLineNr());
            Reader r = new ContentReader(this.reader,
                                         this.entityResolver,
                                         buffer.toString());
            this.builder.addPCData(r, this.reader.getSystemID(),
                                   this.reader.getLineNr());
            r.close();
         }
      }
   }


   /**
    * Processes an attribute of an element.
    *
    * @param attrNames contains the names of the attributes.
    * @param attrValues contains the values of the attributes.
    * @param attrTypes contains the types of the attributes.
    *
    * @throws java.lang.Exception
    *     if something went wrong
    */
   protected void processAttribute(Vector<String> attrNames,
                                   Vector<String> attrValues,
                                   Vector<String> attrTypes)
      throws Exception
   {
      String key = XMLUtil.scanIdentifier(this.reader);
      XMLUtil.skipWhitespace(this.reader, null);

      if (! XMLUtil.read(this.reader, '&').equals("=")) {
         XMLUtil.errorExpectedInput(reader.getSystemID(),
                                    reader.getLineNr(),
                                    "`='");
      }

      XMLUtil.skipWhitespace(this.reader, null);
      String value = XMLUtil.scanString(this.reader, '&',
                                        this.entityResolver);
      attrNames.addElement(key);
      attrValues.addElement(value);
      attrTypes.addElement("CDATA");
      this.validator.attributeAdded(key, value,
                                    this.reader.getSystemID(),
                                    this.reader.getLineNr());
   }

}
