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