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