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