# XmlAst.java

    /*
    ** Module   : XmlAst.java
    ** Abstract : An XML-specific AST.
    **
    ** Copyright (c) 2005-2023, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
    ** 001 ECF 20050421   @20839 Created initial version.
    ** 002 ECF 20050608   @21457 Added resolveConstant method. Resolves XML AST token type names to integral types.
    ** 003 GES 20090515   @42238 Moved AST-specific static helpers here from the XmlHelper.
    ** 004 GES 20090518   @42411 Import change.
    ** 005 SVL 20180209          Added ability to set namespace URI of root node
    ** 006 CA  20180412          Improved performance of domToAst, when loading XMLs with nodes which have 100k's
    **                           of children.
    ** 007 CA  20221010          Avoid reflection when creating a new instance - added 'newInstance' method.
    **                           Refs #6813
    **     TJD 20220504          Java 11 compatibility minor changes
    ** 008 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
    */
    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program 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 Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.xml;

    import java.util.*;
    import antlr.*;
    import com.goldencode.p2j.util.logging.*;
    import org.w3c.dom.*;
    import javax.xml.parsers.*;
    import com.goldencode.ast.*;
    import com.goldencode.util.*;

    /**
     * Provides symbolic token name lookups that are specific to the XML AST.
     * All other function is implemented in the superclass.
     * <p>
     * The token types of an XML AST represent the structural elements of an
     * XML Document Object Model (e.g., DTD, element, attribute, text node,
     * etc.).
     * <p>
     * Currently, line and column number and filename information is not used.
     *
     * @see  XmlTokenTypes
     */
    public class XmlAst
    extends AnnotatedAst
    implements XmlTokenTypes
    {
       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(XmlAst.class);

       /** Map of token types to token names */
       private static final Map TOKEN_NAMES = new HashMap();

       /** Map of token names to token types */
       private static final Map TOKEN_TYPES = new HashMap();

       // Initialize token maps.
       static
       {
          Object[] mapping =
          {
             Integer.valueOf(ATTRIBUTE_NODE)             ,
             "ATTRIBUTE_NODE"                        ,
             Integer.valueOf(CDATA_SECTION_NODE)         ,
             "CDATA_SECTION_NODE"                    ,
             Integer.valueOf(COMMENT_NODE)               ,
             "COMMENT_NODE"                          ,
             Integer.valueOf(DOCUMENT_FRAGMENT_NODE)     ,
             "DOCUMENT_FRAGMENT_NODE"                ,
             Integer.valueOf(DOCUMENT_NODE)              ,
             "DOCUMENT_NODE"                         ,
             Integer.valueOf(DOCUMENT_TYPE_NODE)         ,
             "DOCUMENT_TYPE_NODE"                    ,
             Integer.valueOf(ELEMENT_NODE)               ,
             "ELEMENT_NODE"                          ,
             Integer.valueOf(ENTITY_NODE)                ,
             "ENTITY_NODE"                           ,
             Integer.valueOf(ENTITY_REFERENCE_NODE)      ,
             "ENTITY_REFERENCE_NODE"                 ,
             Integer.valueOf(NOTATION_NODE)              ,
             "NOTATION_NODE"                         ,
             Integer.valueOf(PROCESSING_INSTRUCTION_NODE),
             "PROCESSING_INSTRUCTION_NODE"           ,
             Integer.valueOf(TEXT_NODE)                  ,
             "TEXT_NODE"                             ,
             Integer.valueOf(PUBLIC_ID)                  ,
             "PUBLIC_ID"                             ,
             Integer.valueOf(SYSTEM_ID)                  ,
             "SYSTEM_ID"                             ,
             Integer.valueOf(INTERNAL_SUBSET)            ,
             "INTERNAL_SUBSET"                       ,
             Integer.valueOf(ATTR_SET)                   ,
             "ATTR_SET"                              ,
             Integer.valueOf(CONTENT)                    ,
             "CONTENT"                               ,
          };

          int len = mapping.length;
          for (int i = 0; i < len; i += 2)
          {
             TOKEN_NAMES.put(mapping[i], mapping[i + 1]);
             TOKEN_TYPES.put(mapping[i + 1], mapping[i]);
          }
       }

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

       /**
        * Constructor which accepts and wrappers token.
        *
        * @param   token
        *          Token upon which this node is based.
        */
       public XmlAst(Token token)
       {
          super(token);
       }

       /**
        * Convenience constructor to set token type and text.
        *
        * @param   type
        *          Token type.
        * @param   text
        *          AST text. May be <code>null</code>.
        */
       public XmlAst(int type, String text)
       {
          super();

          setType(type);
          if (text != null)
          {
             setText(text);
          }
       }

       /**
        * Convenience constructor to set token type, text, and parent. This
        * AST is set as a child of <code>parent</code>, and <code>parent</code>
        * is set as this AST's parent.
        *
        * @param   parent
        *          Parent AST. If <code>null</code>, the resulting AST is an
        *          orphan.
        * @param   type
        *          Token type.
        * @param   text
        *          AST text. May be <code>null</code>.
        */
       public XmlAst(Aast parent, int type, String text)
       {
          this(type, text);

          if (parent != null)
          {
             parent.addChild(this);
             this.setParent(parent);
          }
       }

       /**
        * Resolve the given token name to a numeric token type. The lookup is
        * case-insensitive.
        *
        * @param   tokenName
        *          String constant representing token name.
        *
        * @return  Integral token type or <code>null</code> if name could not
        *          be resolved.
        */
       public static Object resolveConstant(String tokenName)
       {
          return TOKEN_TYPES.get(tokenName.toUpperCase());
       }

       /**
        * Generate a DOM <code>Document</code> object based upon an XML AST.
        * Assumes the AST hierarchy is not malformed.
        *
        * @param   ast
        *          XML AST upon which the DOM node hierarchy will be based.
        *          This AST node must be the document root (i.e., type
        *          <code>DOCUMENT_NODE</code>).
        *
        * @return  The XML document which corresponds with the data stored in
        *          <code>ast</code>.
        *
        * @throws  ParserConfigurationException
        *          if the XML parser has been configured incorrectly.
        */
       public static Document astToDom(XmlAst ast)
       throws ParserConfigurationException
       {
          return astToDom(ast, null);
       }

       /**
        * Generate a DOM <code>Document</code> object based upon an XML AST.
        * Assumes the AST hierarchy is not malformed.
        *
        * @param   ast
        *          XML AST upon which the DOM node hierarchy will be based.
        *          This AST node must be the document root (i.e., type
        *          <code>DOCUMENT_NODE</code>).
        * @param   namespaceURI
        *          Namespace URI of root node. If <code>null</code>, then the
        *          node is created without namespace URI.
        *
        * @return  The XML document which corresponds with the data stored in
        *          <code>ast</code>.
        *
        * @throws  ParserConfigurationException
        *          if the XML parser has been configured incorrectly.
        */
       public static Document astToDom(XmlAst ast, String namespaceURI)
       throws ParserConfigurationException
       {
          if (ast.getType() != DOCUMENT_NODE)
          {
             throw new IllegalArgumentException(
                "AST must be of type DOCUMENT_NODE");
          }

          return (Document) astToDomNode(ast, null, null, namespaceURI);
       }

       /**
        * Generate a DOM hierarchy based upon an XML AST. If <code>ast</code>
        * represents an XML document root, no other parameters are necessary.
        * This method recursively calls itself to populate each level of nodes
        * with its appropriate children. Assumes the AST hierarchy is not
        * malformed.
        *
        * @param   ast
        *          XML AST upon which the DOM node hierarchy will be based.
        * @param   doc
        *          Document Object Model document which is used to create
        *          all necessary subnodes. Should be <code>null</code> iff
        *          <code>ast</code> represents the document root (i.e., is of
        *          type <code>DOCUMENT_NODE</code>. In this case, a Document
        *          instance will be created and passed recursively for use
        *          at lower levels.
        * @param   parent
        *          The DOM node to which the newly created child node will be
        *          appended. It should be <code>null</code> if <code>ast</code>
        *          represents the document root.
        *
        * @return  The DOM node which corresponds with the data stored in
        *          <code>ast</code>.
        *
        * @throws  ParserConfigurationException
        *          if the XML parser has been configured incorrectly.
        */
       public static Node astToDomNode(XmlAst ast, Document doc, Node parent)
       throws ParserConfigurationException
       {
          return astToDomNode(ast, doc, parent, null);
       }

       /**
        * Generate a DOM hierarchy based upon an XML AST. If <code>ast</code>
        * represents an XML document root, no other parameters are necessary.
        * This method recursively calls itself to populate each level of nodes
        * with its appropriate children. Assumes the AST hierarchy is not
        * malformed.
        *
        * @param   ast
        *          XML AST upon which the DOM node hierarchy will be based.
        * @param   doc
        *          Document Object Model document which is used to create
        *          all necessary subnodes. Should be <code>null</code> iff
        *          <code>ast</code> represents the document root (i.e., is of
        *          type <code>DOCUMENT_NODE</code>. In this case, a Document
        *          instance will be created and passed recursively for use
        *          at lower levels.
        * @param   parent
        *          The DOM node to which the newly created child node will be
        *          appended. It should be <code>null</code> if <code>ast</code>
        *          represents the document root.
        * @param   namespaceURI
        *          Namespace URI of root node. If <code>null</code>, then the
        *          node is created without namespace URI.
        *
        * @return  The DOM node which corresponds with the data stored in
        *          <code>ast</code>.
        *
        * @throws  ParserConfigurationException
        *          if the XML parser has been configured incorrectly.
        */
       public static Node astToDomNode(XmlAst ast, Document doc, Node parent, String namespaceURI)
       throws ParserConfigurationException
       {
          if (doc != null && parent == null)
          {
             return null;
          }

          Node domNode = null;
          XmlAst astChild = null;
          boolean append = true;
          switch (ast.getType())
          {
             case DOCUMENT_NODE:
                // We don't have enough information to create the XML document
                // from this node alone;  we have to look at our children.

                // Look for a document type node first.
                astChild = (XmlAst) ast.getImmediateChild(DOCUMENT_TYPE_NODE,
                                                          null);
                if (astChild != null)
                {
                   Aast pubAst = astChild.getImmediateChild(PUBLIC_ID, null);
                   Aast sysAst = astChild.getImmediateChild(SYSTEM_ID, pubAst);
                   Aast subAst = astChild.getImmediateChild(INTERNAL_SUBSET,
                                                            sysAst);
                   String pubId = (pubAst != null ? pubAst.getText() : null);
                   String sysId = (sysAst != null ? sysAst.getText() : null);
                   String subId = (subAst != null ? subAst.getText() : null);

                   domNode = doc = XmlHelper.newDocument(astChild.getText(),
                                                         pubId,
                                                         sysId,
                                                         namespaceURI);
                   break;
                }

                // If no DOCTYPE, look for the first element child.
                astChild = (XmlAst) ast.getImmediateChild(ELEMENT_NODE, null);
                if (astChild != null)
                {
                   domNode = doc = XmlHelper.newDocument(astChild.getText(),
                                                         null,
                                                         null,
                                                         namespaceURI);
                }

                break;

             case PROCESSING_INSTRUCTION_NODE:
                // TODO: implement
                break;

             case DOCUMENT_TYPE_NODE:
                // Nothing to do;  we processed in DOCUMENT_NODE case above.
                // We don't want to process our children individually, so return
                // instead of breaking.
                return null;

             case ENTITY_NODE:
                // TODO: implement
                break;

             case ENTITY_REFERENCE_NODE:
                // TODO: implement
                break;

             case NOTATION_NODE:
                // TODO: implement
                break;

             case DOCUMENT_FRAGMENT_NODE:
                // TODO: implement
                break;

             case COMMENT_NODE:

                domNode = doc.createComment(null);

                // Special case:  comment is a direct child of document node and
                // comes before root element node.
                Aast astParent = ast.getParent();
                if (astParent.getType() == DOCUMENT_NODE)
                {
                   Aast root = astParent.getImmediateChild(ELEMENT_NODE, null);
                   if (root != null && root.getIndexPos() > ast.getIndexPos())
                   {
                      Node refChild = doc.getDocumentElement();
                      if (refChild instanceof DocumentType)
                      {
                         refChild = refChild.getNextSibling();
                      }
                      doc.insertBefore(domNode, refChild);
                      append = false;
                   }
                }

                break;

             case ELEMENT_NODE:
                if (ast.getParent().getType() == DOCUMENT_NODE)
                {
                   domNode = doc.getDocumentElement();
                   append = false;   // already a child of the document node
                }
                else
                {
                   domNode = doc.createElement(ast.getText());
                }
                break;

             case ATTR_SET:
                domNode = parent;
                parent = null;
                break;

             case ATTRIBUTE_NODE:
                Attr attr = doc.createAttribute(ast.getText());
                ((Element) parent).setAttributeNode(attr);
                domNode = attr;
                parent = null;
                break;

             case TEXT_NODE:
                domNode = doc.createTextNode(null);
                break;

             case CDATA_SECTION_NODE:
                domNode = doc.createCDATASection(null);
                break;

             case CONTENT:
                switch (ast.getParent().getType())
                {
                   case ATTRIBUTE_NODE:
                      ((Attr) parent).setValue(ast.getText());
                      break;

                   case COMMENT_NODE:
                   case TEXT_NODE:
                   case CDATA_SECTION_NODE:
                      ((CharacterData) parent).setData(ast.getText());
                      break;

                   case PROCESSING_INSTRUCTION_NODE:
                      // TODO: implement
                      break;

                   default:
                      throw new IllegalArgumentException(
                         "Invalid DOM node for a CONTENT AST:  " + domNode);
                }

                break;

             default:
                throw new IllegalArgumentException("Invalid XML AST:  " + ast);
          }

          // Append child node to parent.
          if (append && domNode != null && parent != null)
          {
             parent.appendChild(domNode);
          }

          // Recursively process child ASTs.
          astChild = (XmlAst) ast.getFirstChild();
          while (astChild != null)
          {
             astToDomNode(astChild, doc, domNode);
             astChild = (XmlAst) astChild.getNextSibling();
          }

          return domNode;
       }

       /**
        * Generate an AST based upon a Document Object Model node hierarchy.
        * Leading and trailing whitespace is trimmed from the value string
        * (if any) of a node before that text is set into an AST. Empty DOM
        * text nodes are dropped.
        *
        * @param   domNode
        *          A DOM node whose structure and contents will be used to
        *          generate the AST.
        *
        * @return  The AST which represents the analog to <code>domNode</code>.
        */
       public static XmlAst domToAst(Node domNode)
       {
          return domToAst(null, domNode, true);
       }

       /**
        * Generate an AST based upon a Document Object Model node hierarchy.
        * Optionally attach it to a parent AST as a child and make that AST
        * its parent. This method calls itself recursively to process child
        * nodes and DOM element attributes.
        *
        * @param   parent
        *          Optional parent AST for the new AST. May be <code>null</code>.
        * @param   domNode
        *          A DOM node whose structure and contents will be used to
        *          generate the AST.
        * @param   trimWhite
        *          If <code>true</code>, leading and trailing whitespace will
        *          be trimmed from the value string (if any) of a node before
        *          inserting that text into a content AST. Extra space and
        *          formatting characters, such as newlines, which are embedded
        *          <em>within</em> the value text <strong>are not</strong>
        *          removed. This flag also causes empty TEXT nodes to be
        *          discarded, such that no corresponding AST is created for
        *          such nodes.
        *
        * @return  The AST which represents the analog to <code>domNode</code>.
        */
       public static XmlAst domToAst(Aast parent, Node domNode, boolean trimWhite)
       {
          String name = domNode.getNodeName();
          int nodeType = (0xFFFF & domNode.getNodeType());

          String value = domNode.getNodeValue();
          if (trimWhite)
          {
             value = StringHelper.safeTrim(value);
          }

          boolean hasValue = (value != null && value.length() > 0);

          if (!hasValue && nodeType == TEXT_NODE && trimWhite)
          {
             // Skip this text node if we're not preserving whitespace and
             // it's empty.
             return null;
          }

          // Create AST which is the DOM node's analog.
          XmlAst ast = new XmlAst(parent, nodeType, name);
          // save the last child added to 'ast' node
          XmlAst currentChild = null;

          if (hasValue && nodeType != ATTRIBUTE_NODE)
          {
             // Attach a child to hold content.
             currentChild = new XmlAst(ast, XmlAst.CONTENT, value);
          }

          // Process public and system IDs for DOCTYPE node.
          if (nodeType == DOCUMENT_TYPE_NODE)
          {
             DocumentType docType = (DocumentType) domNode;

             String publicId = docType.getPublicId();
             if (publicId != null && publicId.trim().length() > 0)
             {
                currentChild = new XmlAst(ast, XmlAst.PUBLIC_ID, publicId);
             }

             String systemId = docType.getSystemId();
             if (systemId != null && systemId.trim().length() > 0)
             {
                currentChild = new XmlAst(ast, XmlAst.SYSTEM_ID, systemId);
             }

             String intSubset = docType.getInternalSubset();
             if (intSubset != null && intSubset.trim().length() > 0)
             {
                currentChild = new XmlAst(ast, XmlAst.INTERNAL_SUBSET, intSubset);
             }
          }

          // Process attributes. Group all attributes together under an
          // ATTR_SET AST.
          NamedNodeMap attrs = domNode.getAttributes();
          if (attrs != null)
          {
             XmlAst attrSetAst = new XmlAst(ast, XmlAst.ATTR_SET, "attributes");
             currentChild = attrSetAst;

             int len = attrs.getLength();
             for (int i = 0; i < len; i++)
             {
                domToAst(attrSetAst, attrs.item(i), trimWhite);
             }
          }

          // Handle child nodes recursively.
          Node domChild = domNode.getFirstChild();
          while (domChild != null)
          {
             XmlAst next = domToAst(null, domChild, trimWhite);
             domChild = domChild.getNextSibling();

             if (next == null)
             {
                continue;
             }

             next.setParent(ast);

             // antlr's implementation of addChild is bad: it iterates all children (to find the
             // right-most child) and it links the new child to this right-most child via
             // 'node.right = newNode'. this loop is very expensive when we try to parse nodes with
             // 100k's of children, as the resulting performance is O(n^2) (where n can be > 100k).
             // for this reason, we keep the last child for the current node and we link the next
             // child to it, explicitly.

             if (currentChild == null)
             {
                ast.setFirstChild(next);
                currentChild = next;
             }
             else
             {
                currentChild.setNextSibling(next);
                currentChild = next;
             }
          }

          return ast;
       }

       /**
        * Translates a text representation of a token type into the
        * actual integer value.
        *
        * @param    tokenName
        *           The text representation of the token type.
        *
        * @return   A valid integer token type value or -1 if no match was found.
        */
       public int lookupTokenType(String tokenName)
       {
          Integer type = (Integer) TOKEN_TYPES.get(tokenName);

          return (type != null ? type.intValue() : -1);
       }

       /**
        * Translates an integer value of a token type into the associated
        * human readable text representation.
        *
        * @param    type
        *           The integer token type.
        *
        * @return   A valid symbolic token type name or <code>null</code> if
        *           no match was found.
        */
       public String lookupTokenName(int type)
       {
          return (String) TOKEN_NAMES.get(Integer.valueOf(type));
       }

       /**
        * Returns the symbolic representation of this AST's token type.
        *
        * @return  Symbolic token name for this AST's token type.
        */
       public String getSymbolicTokenType()
       {
          return lookupTokenName(getType());
       }

       /**
        * Create a new instance of this AST implementation.
        *
        * @return   A new {@link XmlAst} instance.
        */
       @Override
       protected AnnotatedAst newInstance()
       {
          return new XmlAst();
       }

       /**
        * Test harness. Does the following:
        * <ul>
        * <li>parses an XML document;
        * <li>converts it to an XML AST;
        * <li>dumps the AST content to <code>stdout</code>;
        * <li>converts the AST to an XML DOM; and
        * <li>writes the DOM to a file named <code>xml_ast_test.xml</code>in the
        *     current directory.
        * </ul>
        *
        * @param   args
        *          Expects a single argument: the name of the XML file to parse.
        */
       public static void main(String[] args)
       {
          try
          {
             if (args.length != 1)
             {
                System.out.println("Usage:  java <pkg>.XmlAst <XML_filename>");
                System.exit(-1);
             }

             Document dom = XmlHelper.parse(args[0]);
             XmlAst ast = domToAst(dom);
             System.out.println(ast.dumpTree());
             Document dom2 = astToDom(ast);
             XmlHelper.write(dom2, "test.xml");
          }
          catch (Exception exc)
          {
             LOG.severe("", exc);
          }
       }
    }