XMLCallbackWrapper.java

/*
** Module   : XMLCallbackWrapper
** Abstract : SAX reader handler specific features implementation
**
** Copyright (c) 2013-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 EVK 20131030 Created initial version.
** 002 CA  20140108 Added the "validateModes" arg (set to true) to the reachableInternalEntry call.
** 003 CA  20220222 Fixed insertAttribute, to process a qualified attribute name properly.
**     CA  20220930 Refactored the callback invocation to be performed via a call-site and InvokeConfig, to
**                  allow caching of the resolved target.
** 004 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 com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
import org.xml.sax.InputSource;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.*;
import java.util.*;
import java.util.logging.*;

import static com.goldencode.p2j.util.ControlFlowOps.ArgValidationErrors;

/**
 * This class provides implementations for all of the possible 4gl-sax callbacks.
 *
 */
class XMLCallbackWrapper
{
   /** Anonymous log instance. */
   private static final CentralLogger LOG = CentralLogger.get(XMLCallbackWrapper.class.getName());

   /** The procedure name to be associated with the given handle object.*/
   private final handle procHandle;

   /** The SaxReader instance to be associated with the given handle object.*/
   private final handle saxReader;

   /**
    * Character callback may use memptr and longchar in parameters, this variable will be
    * initialize once in constructor. If memptr is not exists in character callback(or callback
    * not exists), than will be use longchar by default.
    */
   private final boolean isMemptrInCharacterCallback;

   /**
    * IgnorableWhitespace callback may use memptr and longchar in parameters, this variable will
    * be initialize once in constructor. If memptr is not exists in ignorableWhitespace callback
    * (or callback not exists), than will be use longchar by default.
    */
   private final boolean isMemptrInIgnorableWhitespaceCallback;

   /**
    * Default constructor accepting a handle to procedure and SaxReader instance.
    *
    * @param    procHandle
    *           The procedure name to be associated with the given handle object.
    * @param    saxReader
    *           The SaxReader instance to be associated with the given handle object.
    */
   XMLCallbackWrapper(handle procHandle, handle saxReader)
   {
      this.procHandle = procHandle;
      this.saxReader = saxReader;
      ArgValidationErrors res = validateCallback(CallBack.Characters,
                                                 new memptr(),
                                                 new character());
      isMemptrInCharacterCallback = res == ArgValidationErrors.NO_ERROR;

      res = validateCallback(CallBack.IgnorableWhitespace,
                             new memptr(),
                             new character());
      isMemptrInIgnorableWhitespaceCallback = res == ArgValidationErrors.NO_ERROR;
   }

   /**
    * Receive notification of a notation declaration. If same callback is not exists in 4gl code
    * then do nothing.
    *
    * @param    notationDeclaration
    *           NotationDeclaration xml event which provide all information about notation.
    *
    * @throws   XMLStreamException
    *           if any problem in callback, i.e. mismatch parameters or wrong types in callback.
    */
   void notationDecl(NotationDeclaration notationDeclaration)
   throws XMLStreamException
   {
      invokeProcedure(CallBack.NotationDecl,
                      new character(notationDeclaration.getName()),
                      new character(notationDeclaration.getPublicId()),
                      new character(notationDeclaration.getSystemId()));
   }

   /**
    * Receive notification of the beginning of the document. If same callback is not exists in 4gl
    * code then do nothing.
    *
    * @throws   XMLStreamException
    *           if any problem in callback, i.e. mismatch parameters or wrong types in callback.
    */
   void startDocument()
   throws XMLStreamException
   {
      invokeProcedure(CallBack.StartDocument);
   }

   /**
    * Receive notification of the ending of the document. If same callback is not exists in 4gl
    * code then do nothing.
    *
    * @throws   XMLStreamException
    *           if any problem in callback, i.e. mismatch parameters or wrong types in callback.
    */
   public void endDocument()
   throws XMLStreamException
   {
      invokeProcedure(CallBack.EndDocument);
   }

   /**
    * Receive notification of the start of a Namespace mapping. If same callback is not exists in
    * 4gl code then do nothing.
    *
    * @param    prefix
    *           The Namespace prefix being declared.
    * @param    uri
    *           The Namespace URI mapped to the prefix
    *
    * @throws   XMLStreamException
    *           if any problem in callback, i.e. mismatch parameters or wrong types in callback.
    */
   public void startPrefixMapping(String prefix, String uri)
   throws XMLStreamException
   {
      invokeProcedure(CallBack.StartPrefixMapping,
                      new character(prefix == null ? "" : prefix),
                      new character(uri));
   }

   /**
    * Receive notification of the end of a Namespace mapping. If same callback is not exists in
    * 4gl code then do nothing.
    *
    * @param    prefix
    *           The Namespace prefix being declared.
    *
    * @throws   XMLStreamException
    *           if any problem in callback, i.e. mismatch parameters or wrong types in callback.
    */
   public void endPrefixMapping(String prefix)
   throws XMLStreamException
   {
      invokeProcedure(CallBack.EndPrefixMapping, new character(prefix));
   }

   /**
    * Receive notification of the start of an element. If same callback is not exists in 4gl code
    * then do nothing.
    *
    * @param    startElement
    *           Provides access to information about start element.
    *
    * @throws   XMLStreamException
    *           if any problem in callback, i.e. mismatch parameters or wrong types in callback.
    */
   public void startElement(StartElement startElement)
   throws XMLStreamException
   {
      QName qName = startElement.getName();

      Iterator attributes = startElement.getAttributes();

      SaxAttributes saxAttributes = new SaxAttributesImpl();
      while(attributes.hasNext()) {
         Attribute attribute = (Attribute)attributes.next();

         QName qNameAttr = attribute.getName();
         String prefix = qNameAttr.getPrefix();
         String localPart = qNameAttr.getLocalPart();
         String qn = prefix + (prefix.isEmpty() ? "" : ":") + localPart;
         
         saxAttributes.insertAttribute(qn, attribute.getValue(), qNameAttr.getNamespaceURI());
      }

      invokeProcedure(CallBack.StartElement,
                      new character(qName.getNamespaceURI()),
                      new character(qName.getLocalPart()),
                      new character(qName.getPrefix() + qName.getLocalPart()),
                      new handle(saxAttributes));

   }

   /**
    * Receive notification of the end of an element. If same callback is not exists in 4gl code
    * then do nothing.
    *
    * @param    endElement
    *           Provides access to information about end element.
    *
    * @throws   XMLStreamException
    *           if any problem in callback, i.e. mismatch parameters or wrong types in callback.
    */
   public void endElement(EndElement endElement)
   throws XMLStreamException
   {
      QName qName = endElement.getName();
      invokeProcedure(CallBack.EndElement,
                      new character(qName.getNamespaceURI()),
                      new character(qName.getLocalPart()),
                      new character(qName.getPrefix() + qName.getLocalPart()));
   }

   /**
    * Receive notification of character data inside an element. If same callback is not exists in
    * 4gl code then do nothing.
    *
    * @param    characters
    *           Provides access to information about characters element.
    *
    * @throws   XMLStreamException
    *           if any problem in callback, i.e. mismatch parameters or wrong types in callback.
    */
   public void characters(Characters characters)
   throws XMLStreamException
   {
      String data = characters.getData();
      if (isMemptrInCharacterCallback)
      {
         byte[] arr = data.getBytes();
         invokeProcedure(CallBack.Characters, new memptr(arr), arr.length);
      }
      else
      {
         invokeProcedure(CallBack.Characters, new longchar(data), data.length());
      }
   }

   /**
    * Receive notification of ignorable whitespace in element content. If same callback is not
    * exists in 4gl code then do nothing.
    *
    * @param    characters
    *           Provides access to information about characters element.
    *
    * @throws   XMLStreamException
    *           if any problem in callback, i.e. mismatch parameters or wrong types in callback.
    */
   public void ignorableWhitespace(Characters characters)
   throws XMLStreamException
   {
      String data = characters.getData();
      if (isMemptrInIgnorableWhitespaceCallback)
      {
         byte[] arr = data.getBytes();
         invokeProcedure(CallBack.IgnorableWhitespace, new memptr(arr), arr.length);
      }
      else
      {
         invokeProcedure(CallBack.IgnorableWhitespace, new longchar(data), data.length());
      }
   }

   /**
    * Receive notification of a processing instruction. If same callback is not
    * exists in 4gl code then do nothing.
    *
    * @param    pi
    *           An interface that describes the data found in processing instructions.
    *
    * @throws   XMLStreamException
    *           if any problem in callback, i.e. mismatch parameters or wrong types in callback.
    */
   public void processingInstruction(ProcessingInstruction pi)
   throws XMLStreamException
   {
      invokeProcedure(CallBack.ProcessingInstruction, new character(pi.getTarget()),
                      new character(pi.getData()));
   }

   /**
    * Allow the application to resolve external entities. If this callback is not exists in 4gl
    * code, than this method will always return null.
    *
    * @param    publicId
    *           The public identifier, or null if none is available.
    * @param    systemId
    *           The system identifier provided in the XML document.
    *
    * @return   The new input source, or null to require the default behaviour.
    *
    * @throws   XMLStreamException
    *           if any problem in callback, i.e. mismatch parameters or wrong types in callback.
    */
   InputSource resolveEntity(String publicId, String systemId)
   throws XMLStreamException
   {
      character filePath = new character();
      memptr memPointer = new memptr();
      Boolean res = invokeProcedure(CallBack.ResolveEntity,
                                    new character(publicId),
                                    filePath,
                                    memPointer);
      if (res == null)
      {
         return null;
      }

      if (!filePath.isUnknown() || !memPointer.isUnknown())
      {
         throw new XMLStreamException();
      }

      InputSource is = null;
      if (!filePath.isUnknown())
      {
         String file = filePath.toStringMessage();
         is = new InputSource(new SaxReaderImpl.FileInputStreamWrapper(file));
      }
      else if (!memPointer.isUnknown())
      {
         is = new InputSource(new SaxReaderImpl.MemptrInputStream(memPointer));
      }

      return is;
   }

   /**
    * Receive notification of a warning messages. If same callback is not
    * exists in 4gl code then do nothing.
    *
    * @param    msg
    *           warning message
    *
    * @throws   XMLStreamException
    *           if any problem in callback, i.e. mismatch parameters or wrong types in callback.
    */
   public void warning(String msg)
   throws XMLStreamException
   {
      invokeProcedure(CallBack.Warning, new character(msg));
   }

   /**
    * Receive notification of a error messages. If same callback is not
    * exists in 4gl code then do nothing.
    *
    * @param    message
    *           error message
    *
    * @throws   XMLStreamException
    *           if any problem in callback, i.e. mismatch parameters or wrong types in callback.
    */
   public void error(String message)
   throws XMLStreamException
   {
      invokeProcedure(CallBack.Error, new character(message));
   }

   /**
    * Receive notification of a fatal errors. If same callback is not
    * exists in 4gl code then do nothing. If the callback is exists and doesn't return error, than
    * parser should stop parsing.
    *
    * @param    e
    *           Fatal error exception
    *
    * @return   {@code null}  if the callback is not exists or a fatal error.
    *           {@code true}  if the callback is successful executed but return some error.
    *           {@code false} if the callback is successful executed but doesn't return error.
    *
    * @throws   XMLStreamException
    *           if any problem in callback, i.e. mismatch parameters or wrong types in callback.
    */
   public Boolean fatalError(Exception e)
   throws XMLStreamException
   {
      return invokeProcedure(CallBack.FatalError, new character(e.getMessage()));
   }

   /**
    * Method can invoke procedure callback and valid input parameters. If specified callback is
    * not exists then do nothing.
    *
    * @param    callBack
    *           Callback entity.
    * @param    args
    *           Array of input/output arguments.
    *
    * @return   {@code null}  if the callback is not exists or a fatal error.
    *           {@code true}  if the callback is successful executed but return some error.
    *           {@code false} if the callback is successful executed but doesn't return error.
    *
    * @throws   XMLStreamException
    *           if any problem in callback, i.e. mismatch parameters or wrong types in callback.
    */
   private Boolean invokeProcedure(CallBack callBack, Object... args)
   throws XMLStreamException
   {
      if (procHandle == null || !ProcedureManager.isProcedure(procHandle))
      {
         return null;
      }

      ArgValidationErrors result = validateCallback(callBack, args);
      if (result == null)
      {
         return null;
      }

      if (result == ArgValidationErrors.INCORRECT_COUNT)
      {
         throw new MismatchedParametersException(callBack.name());
      }
      if (result != ArgValidationErrors.NO_ERROR)
      {
         throw new MismatchedNumberParametersException(callBack.name());
      }
      SelfManager.pushSelf(saxReader);
      character returnValue = null;
      try
      {
         callBack.getCallSite()
                 .clone()
                 .setTarget(callBack.name())
                 .setInHandle(procHandle)
                 .setModes(callBack.getModes())
                 .setArguments(args)
                 .executeForResource((WrappedResource) saxReader.get());
         
         returnValue = ControlFlowOps.getReturnValue();
      }
      catch (Exception exc)
      {
         LOG.logp(Level.SEVERE,
                  "XMLCallbackWrapper",
                  "invokeProcedure",
                  "Could not load!",
                  exc);
         return null;
      }
      finally
      {
         SelfManager.popSelf();
      }

      return returnValue != null && !returnValue.isUnknown() && !returnValue.getValue().isEmpty();
   }

   /**
    * Check if an internal procedure or function is reachable and valid. This searches the
    * specified handle and all the super-procedures.
    *
    * @param    callBack
    *           Callback entity.
    * @param    args
    *           Array of input/output arguments.
    *
    * @return   Argument validation error, or NO_ERROR if errors is not exists.
    */
   private ArgValidationErrors validateCallback(CallBack callBack, Object... args)
   {
      return ControlFlowOps.reachableInternalEntry(procHandle,
                                                   callBack.name(), false,
                                                   callBack.getModes(),
                                                   true,
                                                   args);
   }

   /**
    * List of possible callbacks of SAX-READER object:
    * <ul>
    * <li>{@link #StartDocument} -Start Document callback.</li>
    * <li>{@link #EndDocument} - End Document callback.</li>
    * <li>{@link #ResolveEntity} - ResolveEntity callback.</li>
    * <li>{@link #NotationDecl} - Notation Declaration callback.</li>
    * <li>{@link #UnparsedEntityDecl} - Unparsed Entity Declaration callback.</li>
    * <li>{@link #StartPrefixMapping} - Start Prefix Mapping callback.</li>
    * <li>{@link #EndPrefixMapping} - End Prefix Mapping callback.</li>
    * <li>{@link #StartElement} - Start Element callback.</li>
    * <li>{@link #EndElement} - End Element callback.</li>
    * <li>{@link #Characters} - Characters callback.</li>
    * <li>{@link #IgnorableWhitespace} - Ignorable Whitespace callback.</li>
    * <li>{@link #ProcessingInstruction} - Processing Instruction callback.</li>
    * <li>{@link #Warning} - Warning callback.</li>
    * <li>{@link #Error} - Error callback.</li>
    * <li>{@link #FatalError} - FatalError callback.</li>
    * </ul>
    */
   enum CallBack
   {
      StartDocument(),
      EndDocument(),
      ResolveEntity("IIOO"),  //maybe memptr
      NotationDecl("III"),
      UnparsedEntityDecl("IIII"),
      StartPrefixMapping("II"),
      EndPrefixMapping("I"),
      StartElement("IIII"),
      EndElement("III"),
      Characters("II"),
      IgnorableWhitespace("II"),
      ProcessingInstruction("II"),
      Warning("I"),
      Error("I"),
      FatalError("I");

      /** defined input/output modes for procedure*/
      private final String modes;
      
      /** The call-site for this callback. */
      private final InvokeConfig callSite;

      /** default constructor*/
      CallBack()
      {
         this(null);
      }

      /**
       * Special constructor accepting array with possible input type parameters.
       *
       * @param   modes
       *          String of possible input/output parameters type, e.g "I", "O", "U".
       */
      CallBack(String modes)
      {
         this.modes = modes;
         this.callSite = new InvokeConfig();
      }

      /**
       * Gets the input/output parameter modes.
       *
       * @return   see above.
       */
      String getModes()
      {
         return modes;
      }
      
      /**
       * Get the {@link #callSite}.
       * 
       * @return   Seea bove.
       */
      public InvokeConfig getCallSite()
      {
         return callSite;
      }
   }
}