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);
      }
   }
}