XmlPatternWorker.java
/*
** Module : XmlPatternWorker.java
** Abstract : Pattern worker which assists with XML AST processing
**
** Copyright (c) 2005-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- --------------------------- Description ---------------------------
** 001 ECF 20050608 @21459 Created initial version. Provides a callback
** library to manipulate XML ASTs from user
** expressions.
** 002 ECF 20050711 @21690 Added saveAst parameter to persistTarget
** user function. This persists the intermediate
** AST form alongside the XML document with an
** "xast" extension.
** 003 ECF 20060201 @24283 Added capability to find a specific child
** element of an XML AST. Matches element name
** and specific attributes.
** 004 SIY 20070615 @34062 Fixed NPE in matchAttribute().
** 005 GES 20090429 @42070 Match package and class name changes.
** 006 GES 20090515 @42239 Moved to AstManager from AstRegistry/AstPersister.
** 008 GES 20090518 @42412 Import change.
** 009 CA 20100121 @44546 Fixed matchAttribute. Added getAttributeValue to
** the library, which searches a set for the value
** of the attribute with the given name.
** 010 CA 20101104 Fixed bug in findElement(...) method - the "name"
** parameter must be included in the search.
** 011 EVK 20130903 Updated method call parameters.
** 012 OM 20131128 Removed javadoc warning for parse() method.
** 013 ECF 20150330 Added makeSystemID function to generate a system ID from the current
** jar file or standard P2J build structure.
** 014 CA 20150406 Fixed makeSystemID, when the URL protocol is "file".
** 015 CA 20200412 Added incremental conversion support.
** 016 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.io.*;
import java.net.*;
import java.security.*;
import java.util.*;
import javax.xml.parsers.*;
import com.goldencode.p2j.util.logging.*;
import org.w3c.dom.*;
import com.goldencode.ast.*;
import com.goldencode.util.*;
import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.convert.*;
import org.xml.sax.SAXException;
/**
* A pattern worker that provides services to support the manipulation of
* XML ASTs and their persistence in document object model (DOM) form.
*/
public final class XmlPatternWorker
extends AbstractConversionWorker
implements XmlTokenTypes
{
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(XmlPatternWorker.class);
/**
* Default constructor which initializes callback library.
*/
public XmlPatternWorker()
{
super();
setLibrary(new Library());
}
/**
* General purpose XML AST creation function which can create a
* <code>XmlAst</code> node using the given token type and text.
* The created node will then be attached to the specified parent. This
* is used in cases where the conversion is simple enough to drive
* it completely from the rule set rather than from a custom worker.
*
* @param type
* The token type for the XML AST.
* @param text
* The text for the AST.
* @param parent
* The ID of the parent AST node.
*
* @return The unique ID if the AST was created successfully or
* <code>null</code> if the parent ID is invalid, if the type
* is not valid or on any unexpected error during AST
* creation.
*/
public static Aast createAst(int type, String text, Aast parent)
{
XmlAst ast = new XmlAst();
initializeAst(ast, type, text, parent, null);
return ast;
}
/**
* Scan all attributes for the given AST set and return the one which
* matches with the given name (case-sensitively).
*
* @param attrSet
* AST set which to be searched
* @param name
* Target attribute name for which the value is retrieved.
*
* @return the attribute value, if there is such an attribute;
* <code>null</code> otherways
*
* @see #matchAttribute(Aast, String, String)
*/
public static String getAttributeValue(Aast attrSet, String name)
{
Aast next = null;
boolean foundName = false;
Iterator<Aast> iter = attrSet.iterator();
while (iter.hasNext())
{
next = iter.next();
int type = next.getType();
String text = next.getText();
// this flag never needs to be reset so long as there is always a
// CONTENT node as a child (or grand-child) of the ATTRIBUTE_NODE
// since this method will return before the depth-first traversal
// leaves the found ATTRIBUTE_NODE subtree
foundName = foundName || (type == ATTRIBUTE_NODE && text.equals(name));
if (foundName && type == CONTENT)
{
// a node can have only one attribute with a certain name, so it
// is safe to return the value for the first found attribute with
// the desired name
return text;
}
}
return null;
}
/**
* Resolve <code>constant</code> to a literal which can be compiled into
* expressions.
*
* @param constant
* Constant indicating a XML token name.
*
* @return Token type associated with the token name.
*/
@Override
public Object resolveConstant(String constant)
{
return XmlAst.resolveConstant(constant);
}
/**
* Scan all attributes in the given attribute set AST and determine
* whether any of them match the given name and value parameters. Both
* the name and value must match exactly (case-sensitively).
* <p>
* The normal AST hierarchy for the value of an attribute is
* ATTRIBUTE_NODE with a child of type CONTENT. However, there are casess
* when a TEXT node exists between the ATTRIBUTE_NODE and the CONTENT node.
* To handle this case, the search is performed in a depth-first way. After
* the ATTRIBUTE_NODE with the correct name is found, the next CONTENT node
* in the depth-first traversal will be the value to be checked.
*
* @param attrSet
* Attribute set AST to scan.
* @param name
* Target attribute name to match.
* @param value
* Target attribute value to match.
*
* @return <code>true</code> if the given attribute set contains an
* attribute which exactly matches the given name and value;
* else <code>false</code>.
*/
private boolean matchAttribute(Aast attrSet, String name, String value)
{
Aast next = null;
boolean foundName = false;
Iterator<Aast> iter = attrSet.iterator();
while (iter.hasNext())
{
next = iter.next();
int type = next.getType();
String text = next.getText();
// this flag never needs to be reset so long as there is always a
// CONTENT node as a child (or grand-child) of the ATTRIBUTE_NODE
// since this method will return before the depth-first traversal
// leaves the found ATTRIBUTE_NODE subtree
foundName = foundName || (type == ATTRIBUTE_NODE && text.equals(name));
if (foundName && type == CONTENT)
{
// a node can have only one attribute with a certain name, so it
// is safe to return the value for the first found attribute with
// the desired name
return text.equals(value);
}
}
return false;
}
/**
* Helper to create, edit, move and delete XML ASTs.
*/
public class Library
{
/**
* General purpose XML AST creation function which can create a
* <code>XmlAst</code> node using the given token type and text.
* The created node will then be attached to the specified parent. This
* is used in cases where the conversion is simple enough to drive
* it completely from the rule set rather than from a custom worker.
*
* @param type
* The token type for the XML AST.
* @param text
* The text for the AST.
* @param parent
* The ID of the parent AST node.
*
* @return The unique ID if the AST was created successfully or
* <code>null</code> if the parent ID is invalid, if the type
* is not valid or on any unexpected error during AST
* creation.
*/
public Aast createAst(int type, String text, Aast parent)
{
return XmlPatternWorker.createAst(type, text, parent);
}
/**
* Convert the current target XML AST into a DOM and save it to file.
* This will normalize a filename to the project root and write the
* XML document to that file. Parent directories are created if
* necessary.
*
* @param root
* Root of the XML AST.
* @param filename
* Project-relative filename for XML document file.
* @param saveAst
* <code>true</code> to persist the intermediate, AST form of
* the XML document. If <code>true</code>, the AST is persisted
* with an <code>xast</code> extension, alongside the XML file.
*
* @throws ConfigurationException
* if a project home directory has not been configured.
* @throws ParserConfigurationException
* if the XML parser used to create the DOM has been
* configured incorrectly.
* @throws IOException
* if any I/O error occurs writing the DOM to file or
* creating the necessary directories.
*/
public void persistTarget(XmlAst root, String filename, boolean saveAst)
throws ConfigurationException,
ParserConfigurationException,
IOException
{
File file = Configuration.toFile(filename);
File dir = file.getParentFile();
if (!dir.exists())
{
dir.mkdirs();
}
if (saveAst)
{
try
{
AstManager.get().saveTree(root, filename + ".xast", false);
}
catch (AstException exc)
{
LOG.warning("", exc);
}
}
Document dom = XmlAst.astToDom(root);
XmlHelper.write(dom, file);
}
/**
* Compose a system ID for XML validation purposes. First, try to create the system ID
* using the jar file from which the current class was loaded (note: this assumes the DTD
* or XML schema exists in this same jar!). If that doesn't work, create the system ID
* based on the P2J_HOME configuration parameter and the standard P2J build structure,
* assuming the resource will be loaded from within the "build/classes/" directory tree.
*
* @param appendPath
* An optional, relative path to append to the end of the system ID.
*
* @return System ID string as described above.
*
* @throws URISyntaxException
* if the URL for the enclosing jar file cannot be converted to a URI.
* @throws ConfigurationException
* if there is a problem determining P2J_HOME.
*/
public String makeSystemID(String appendPath)
throws URISyntaxException,
ConfigurationException
{
StringBuilder buf = new StringBuilder();
// try building system ID from this class' containing jar's path (if loaded from a jar)
CodeSource cs = getClass().getProtectionDomain().getCodeSource();
if (cs != null)
{
URL location = cs.getLocation();
File f = new File(location.getFile());
if (f.isDirectory())
{
// if the file is in a class folder, use the folder directly.
buf.append(location.toURI().toString());
}
else
{
buf.append("jar:").append(location.toURI().toString()).append("!/");
}
}
else
{
// class was not loaded from a jar, or we couldn't access it; try building path from
// standard P2J classes directory structure
String home = Configuration.home();
buf.append(home).append("/build/classes/");
}
if (appendPath != null)
{
buf.append(appendPath);
if (!appendPath.endsWith("/"))
{
buf.append("/");
}
}
return buf.toString();
}
/**
* Parse an XML document and return its data as an XML AST.
*
* @param filename
* Name of file at which XML document resides.
* @param validate
* <code>true</code> to use a validating parser,
* <code>false</code> to parse the document without validation.
* @param systemID
* A base for resolving relative URIs. Used if validating with a system DOCTYPE
* which specifies a relative URI for the DTD.
*
* @return XML document's data in AST form.
*
* @throws ParserConfigurationException
* if the XML parser has been configured incorrectly.
* @throws IOException
* if an I/O error occurs during parsing or stream cleanup.
* @throws SAXException
* if an error occurs parsing the XML within the specified
* file.
*/
public XmlAst parse(String filename, boolean validate, String systemID)
throws Exception
{
Document dom = XmlHelper.parse(filename, validate, systemID, false);
return XmlAst.domToAst(dom);
}
/**
* Given an XML AST node, attempt to find a child element with the
* specified name, and whose attributes match the given attribute
* name/value pairs. Only immediate children are checked.
*
* @param parent
* AST node whose children are iterated to find a match.
* @param name
* Name of the target element to be matched.
* @param attrPairs
* An array of strings which must contain an even number of
* elements, representing interleaved attribute name/value
* pairs. When iterated two indexes at a time, each <i>i</i>th
* element represents an attribute's name, while the <i>i +
* 1</i>th element represents its value.
*
* @return The XML AST child node which matches the specified criteria,
* or <code>null</code> if no such match is found.
*/
public XmlAst findElement(XmlAst parent, String name, Object[] attrPairs)
{
int len = attrPairs.length;
if ((len % 2) > 0)
{
throw new IllegalArgumentException("Invalid attribute name/value pairs");
}
if (name == null)
{
throw new IllegalArgumentException("The name of the element must be specified !");
}
Aast next = null;
Aast target = null;
while (target == null)
{
next = parent.getImmediateChild(ELEMENT_NODE, next);
if (next == null)
{
break;
}
if (!name.equals(next.getText()))
{
continue;
}
Aast attrSet = next.getImmediateChild(ATTR_SET, null);
for (int i = 0; attrSet != null && i < len; i += 2)
{
if (matchAttribute(attrSet, (String) attrPairs[i], (String) attrPairs[i + 1]))
{
target = next;
break;
}
}
}
return (XmlAst) target;
}
/**
* Scan all attributes for the given AST set and return the one which
* matches with the given name (case-sensitively).
*
* @param attrSet
* AST set which to be searched
* @param name
* Target attribute name for which the value is retrieved.
*
* @return the attribute value, if there is such an attribute;
* <code>null</code> otherways
*
* @see #matchAttribute(Aast, String, String)
*/
public String getAttributeValue(Aast attrSet, String name)
{
return XmlPatternWorker.getAttributeValue(attrSet, name);
}
}
}