SaxReaderImpl.java

/*
** Module   : SaxReaderImpl
** Abstract : SAX reader specific features implementation.
**
** Copyright (c) 2013-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --------------------------------------------Description-----------------------------------
** 001 EVL 20130208 Created initial version. Implementation is limited to the stubs to get the
**                  converted java code compiled.
** 002 CA  20130221 Added ADM-DATA and UNIQUE-ID support. 
** 003 EVL 20130320 Spelling fixes for comments.
** 004 EVK 20131001 Implement methods and class logic.
** 005 EVK 20140113 Added method resourceDelete to properly remove the object.
** 009 CA  20181029 Fixed a typo in SCHEMA-PATH setter.
** 010 CA  20190314 Ignore validation errors related to missing DTD.
** 011 HC  20200102 Implemented ADD-SCHEMA-LOCATION on SAX-reader object.
** 012 CA  20210629 Fixed startDocument and endPrefixMapping calls.
**                  For prolog errors, report only the first one via the handler callback procedure, and do
**                  not raise an ERROR.
**     CA  20210709 XML parse must use a pristine byte stream and use the reader to decode it (using the 
**                  encoding as specified at the XML prolog).  Also, the XML version is forced to 1.1, to
**                  allow ISO control characters; FWD will automatically escape them.
**     EVL 20220528 Ensure the memptr based SAX source has finished with new line separator.  Otherwise the
**                  parser will fail at the end of the file even if the content is properly formatted.
**     EVL 20220529 Improved fix for re-make memptr data source to include missing new line separator.
**     CA  20220707 Added 'MemptrInputStream.read(byte[] array, int off, int len)', to improve performance.
**     CA  20220930 Refactored the callback invocation to be performed via a call-site and InvokeConfig, to
**                  allow caching of the resolved target.
** 013 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 014 SP  20250319 Fixed bytesRead calculation in MemptrInputStream.read(byte[], off, len).
** 015 SP  20250327 Removed the addition of new line separator in MemptrInputStream source.
*/

/*
** 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 javax.xml.stream.*;
import javax.xml.stream.events.*;
import javax.xml.stream.util.XMLEventAllocator;
import java.io.*;
import java.net.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.*;

import com.ctc.wstx.api.WstxInputProperties;
import com.ctc.wstx.cfg.ParsingErrorMsgs;
import com.ctc.wstx.evt.DefaultEventAllocator;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.ErrorManager;
import com.goldencode.p2j.util.logging.*;
import com.goldencode.util.XmlHelper;

import org.codehaus.stax2.*;
import org.codehaus.stax2.validation.*;

/**
 * Implementation SAX-Reader specific features of the SAX objects.
 */
public class SaxReaderImpl
extends SaxEntityImpl
implements SaxReader
{
   /** Anonymous log instance. */
   private static final CentralLogger LOG = CentralLogger.get(SaxReaderImpl.class.getName());

   /** Current status of SAX-Writer */
   private ReadStatus readStatus = ReadStatus.SAX_UNINITIALIZED;

   /** Current input source of SAX-Reader */
   private InputStream inputStream = null;

   /** Sax-parser object */
   private XMLStreamReader2 saxParser;

   /** helps XMLStreamReader to allocate xml events*/
   private XMLEventAllocator allocator;

   /** Special wrapper to call callback procedures*/
   private XMLCallbackWrapper callbackWrapper;

   /** Specify the location of the SAX callback procedures */
   private handle hCallback;

   /** Indicates if the parser validates the XML document against DTD or not. */
   private boolean validate = true;

   /** Indicates if the namespace processing is suppressed or not. */
   private boolean suppress = false;

   /** Schema path location variable */
   private String schemaPaths = null;

   /** Schema locations variable */
   private String schemaLocation = "";

   /** No name Schema location variable */
   private String noNameSchemaLocation = "";

   /** Flag indicating if the parser had a prolog error. */
   private boolean prologError = false;
   
   /**
    * Default constructor.
    */
   public SaxReaderImpl()
   {
      // no-op
   }

   /**
    * Find DTD in path.
    *
    * @param    workPath
    *           File path or directory. May be relative path. This path is specify in 4gl
    *           SCHEMA-PATH attribute.
    *
    * @param    systemID
    *           DTD schema filename.
    * @param    baseURI
    *           Working directory. May used if workPath is not absolute path.
    *
    * @return   If DTD schema is found, then return InputStream for this schema.
    */
   private static InputStream findSchemaInPath(String workPath, String systemID, String baseURI)
   {
      File path = new File(workPath);

      if (path.isAbsolute())
      {
         return getInputStream(systemID, path);
      }
      else //path may be relative
      {
         URI baseUri = null;
         try
         {
            baseUri = new URI(baseURI);
         }
         catch (URISyntaxException ignored)
         {
            return null;
         }
         String basePath = baseUri.getPath();
         File relativePath = new File(basePath, workPath);

         if (relativePath.exists())
         {
            return getInputStream(systemID, relativePath);
         }
      }
      return null;
   }

   /**
    * Find fileName. 1) If path is file and name is equals to fileName, then return InputStream.
    * 2) if path is directory and inside directory exists file with name fileName, then return
    * InputStream. In other cases return null.
    *
    * @param    fileName
    *           File to find
    * @param    path
    *           May be file which method is try to find or directory in which need to find file.
    *
    * @return   see above.
    */
   private static InputStream getInputStream(String fileName, File path)
   {
      if (path.isFile())
      {
         if (fileName.equals(path.getName()))
         {
            return new FileInputStreamWrapper(path.getAbsolutePath());
         }
         else
         {
            return null;
         }
      }
      else  //directory
      {
         File newPath = new File(path, fileName);
         if (newPath.exists())
         {
            return new FileInputStreamWrapper(newPath.getAbsolutePath());
         }
         else
         {
            return null;
         }
      }
   }

   /**
    * Gets the value of the handler atribute for the given SAX Reader object.
    *
    * @return   The handle variable representing the handler attribute.
    */
   @Override
   public handle getHandler()
   {
      return new handle(hCallback);
   }

   /**
    * Sets the value of the handler atribute for the given SAX Reader object.
    *
    * @param    hCallback
    *           The handle of the new callback handler to be set for the handler attribute.
    */
   @Override
   public void setHandler(handle hCallback)
   {
      if (hCallback.isUnknown())
      {
         invalidArgumentAssignError("HANDLE");
         return;
      }
      this.hCallback = new handle(hCallback);
   }

   /**
    * Gets the value of the current 1-based column in XML source.
    *
    * @return   The integer value of the current column.
    */
   @Override
   public integer getLocatorColumnNumber()
   {
      if (readStatus == ReadStatus.SAX_UNINITIALIZED)
      {
         return new integer();
      }
      return new integer(saxParser.getLocation().getColumnNumber());
   }

   /**
    * Gets the value of the current 1-based line number in XML source.
    *
    * @return   The integer value of the current line.
    */
   @Override
   public integer getLocatorLineNumber()
   {
      if (readStatus == ReadStatus.SAX_UNINITIALIZED)
      {
         return new integer();
      }
      return new integer(saxParser.getLocation().getLineNumber());
   }

   /**
    * Gets the current value of the public identifier of the current XML source.
    *
    * @return   The character value representing the current locator-public-id attribute.
    */
   @Override
   public character getLocatorPublicId()
   {
      if (readStatus == ReadStatus.SAX_UNINITIALIZED)
      {
         return new character();
      }
      return new character(saxParser.getLocation().getPublicId());
   }

   /**
    * Gets the current value of the system identifier of the current XML source.
    *
    * @return   The character value representing the current locator-system-id attribute.
    */
   @Override
   public character getLocatorSystemId()
   {
      if (readStatus == ReadStatus.SAX_UNINITIALIZED)
      {
         return new character();
      }
      return new character(saxParser.getLocation().getSystemId());
   }

   /**
    * Gets the current value of the parse status. The default value is
    * {@link SaxReader.ReadStatus#SAX_UNINITIALIZED}. The other returned values are
    * {@link SaxReader.ReadStatus#SAX_COMPLETE},
    * {@link SaxReader.ReadStatus#SAX_PARSER_ERROR} or {@link SaxReader.ReadStatus#SAX_RUNNING}.
    *
    * @return   The integer value representing the current status of the parse.
    */
   @Override
   public integer getParseStatus()
   {
      return new integer(readStatus.getStatus());
   }

   /**
    * Gets the current value of the namespace location pair mapping.
    *
    * @return   The character value representing the current schema-location attribute.
    */
   @Override
   public character getSchemaLocation()
   {
      return new character(schemaLocation);
   }

   /**
    * Sets the current value of the namespace location pair mapping.
    *
    * @param    location
    *           The new value the current schema-location attribute.
    */
   @Override
   public void setSchemaLocation(String location)
   {
      setSchemaLocation(new character(location));
   }

   /**
    * Gets the delimiter separated list of directory paths used to locate XML DTD associated with
    * a particular XML document.
    *
    * @return   The character value representing the current schema-path attribute.
    */
   @Override
   public character getSchemaPath()
   {
      return new character(schemaPaths);
   }

   /**
    * Sets the delimiter separated list of directory paths used to locate XML DTD associated with
    * a particular XML document.
    *
    * @param    path
    *           The new value the current schema-path attribute.
    */
   @Override
   public void setSchemaPath(String path)
   {
      setSchemaPath(new character(path));
   }

   /**
    * Indicates if the namespace processing is suppressed or not.
    *
    * @return   The logical value representing the current suppress-namespace-processing
    *           attribute.
    */
   @Override
   public logical getSuppressNamespaceProcessing()
   {
      return new logical(suppress);
   }

   /**
    * Redefines whether the namespace processing is suppressed or not.
    *
    * @param    suppress
    *           The new value the current suppress-namespace-processing attribute.
    */
   @Override
   public void setSuppressNamespaceProcessing(boolean suppress)
   {
      setSuppressNamespaceProcessing(new logical(suppress));
   }
   
   /**
    * Indicates if the strict entity resolution is enabled.
    *
    * @return   The logical value representing the current value.
    */
   @Override
   public logical getStrictEntityResolution()
   {
      // TODO
      return new logical();
   }

   /**
    * Enable or disable strict entity resolution.
    *
    * @param    strict
    *           The new value for the attribute.
    */
   @Override
   public void setStrictEntityResolution(logical strict)
   {
      // TODO
   }

   /**
    * Enable or disable strict entity resolution.
    *
    * @param    strict
    *           The new value for the attribute.
    */
   @Override
   public void setStrictEntityResolution(boolean strict)
   {
      setStrictEntityResolution(new logical(strict));
   }

   /**
    * Get the entity expansion limit.
    *
    * @return   The integer value of the attribute.
    */
   @Override
   public integer getEntityExpansionLimit()
   {
      // TODO
      return new integer();
   }

   /**
    * Set the entity expansion limit.
    *
    * @param    limit
    *           The new value for the attribute.
    */
   @Override
   public void setEntityExpansionLimit(int64 limit)
   {
      // TODO
   }   

   /**
    * Set the entity expansion limit.
    *
    * @param    limit
    *           The new value for the attribute.
    */
   @Override
   public void setEntityExpansionLimit(long limit)
   {
      setEntityExpansionLimit(new int64(limit));
   }
   
   /**
    * Indicates if the parser validates the XML document against DTD or not.
    *
    * @return   The logical value representing the current validation-enabled attribute.
    */
   @Override
   public logical getValidationEnabled()
   {
      return new logical(validate);
   }

   /**
    * Redefines whether the parser will validate the XML document against DTD or not.
    *
    * @param    enable
    *           The new value the current validation-enabled attribute.
    */
   @Override
   public void setValidationEnabled(boolean enable)
   {
      setValidationEnabled(new logical(enable));
   }

   /**
    * Executing single call parse of an XML document associated with a SAX-Reader object.
    * This method is equivalent of call saxParseFirst() and then saxParseNext()
    */
   @Override
   public void saxParse()
   {
      saxParseFirst();
      while(readStatus == ReadStatus.SAX_RUNNING)
      {
         saxParseNext();
      }
   }

   /**
    * Initializes and begins progressive-scan parse of an XML document associated with a
    * SAX-Reader object.
    */
   @Override
   public void saxParseFirst()
   {
      if (inputStream == null)
      {
         saxParseError("SAX-PARSE-FIRST", "No input file specified");
         return;
      }

      try
      {
         inputStream.reset();
      }
      catch (IOException ignored)
      {
         saxParseError("SAX-PARSE-FIRST", "No input file specified");
         return;
      }

      try
      {
         XMLInputFactory xmlif = XMLInputFactory2.newInstance();
         xmlif.setProperty(WstxInputProperties.P_ALLOW_XML11_ESCAPED_CHARS_IN_XML10, true);
         xmlif.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE);
         xmlif.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, !suppress);
         xmlif.setProperty(XMLInputFactory.SUPPORT_DTD, validate);
         xmlif.setProperty(XMLInputFactory.IS_VALIDATING, validate);
         callbackWrapper = hCallback != null && hCallback._isValid() 
                              ? new XMLCallbackWrapper(hCallback, new handle(this)) 
                              : null;
         final boolean isValidate = validate;
         xmlif.setProperty(XMLInputFactory.RESOLVER, new XMLResolver()
         {
            @Override
            public Object resolveEntity(String publicID,
                                        String systemID,
                                        String baseURI,
                                        String namespace)
            throws XMLStreamException
            {
               if (!isValidate || callbackWrapper == null)
               {
                  return null;
               }
               Object result = callbackWrapper.resolveEntity(publicID, systemID);
               if (result != null)
               {
                  return result;
               }

               //find dtd schema in file path
               if (schemaPaths == null || schemaPaths.isEmpty())
               {
                  return null;
               }

               try
               {
                  URL url = new URL(systemID);
                  String protocol = url.getProtocol();
                  if ("http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol) ||
                      "ftp".equalsIgnoreCase(protocol) || "ftps".equalsIgnoreCase(protocol))
                  {
                     return url.openStream();
                  }
               }
               catch (MalformedURLException ignored)
               {
                  // no-op
               }
               catch (IOException e)
               {
                  throw new XMLStreamException(e);
               }

               String[] paths = schemaPaths.split(",");

               for (String path : paths)
               {
                  InputStream inPath = findSchemaInPath(path.trim(), systemID, baseURI);
                  if (inPath != null)
                  {
                     return inPath;
                  }
               }
               throw new XMLStreamException("DTD schema is not found.");
            }
         });

         xmlif.setProperty(XMLInputFactory.REPORTER, new XMLReporter()
         {
            @Override
            public void report(String   message,
                               String   errorType,
                               Object   relatedInformation,
                               Location location)
            throws XMLStreamException
            {
               if (validate && "doctype declaration".equals(errorType))
               {
                  // ignore validation errors related to missing DTD
                  return;
               }
               
               if (callbackWrapper != null)
               {
                  callbackWrapper.error(message);
               }
            }
         });
         xmlif.setEventAllocator(DefaultEventAllocator.getDefaultInstance());
         
         String encoding = XmlHelper.readXMLEncoding(inputStream);
         if (encoding == null)
         {
            // always default to UTF-8
            encoding = "UTF-8";
         }
         Reader filtered = new EscapeControlCharsReader(new InputStreamReader(inputStream, encoding));
         saxParser = (XMLStreamReader2) xmlif.createXMLStreamReader(filtered);
         allocator = xmlif.getEventAllocator();

         if (validate)
         {
            addValidation();
         }

         int eventType = saxParser.getEventType();
         if (eventType != XMLStreamConstants.START_DOCUMENT)
         {
            saxParseError("SAX-PARSE-FIRST", "Unexpected error");
            return;
         }
         if (callbackWrapper != null)
         {
            callbackWrapper.startDocument();
         }
         readStatus = ReadStatus.SAX_RUNNING;
      }
      catch (IllegalArgumentException | XMLStreamException | IOException ignored)
      {
         readStatus = ReadStatus.SAX_UNINITIALIZED;
         saxParseError("SAX-PARSE-FIRST", "Unexpected error");
      }
   }

   /**
    * Continues progressive-scan parse of an XML document associated with a SAX-Reader object.
    */
   @Override
   public void saxParseNext()
   {
      if (readStatus == ReadStatus.SAX_COMPLETE)
      {
         return;
      }

      if (readStatus == ReadStatus.SAX_UNINITIALIZED)
      {
         saxParseError("SAX-PARSE-NEXT", "No input file specified");
         return;
      }

      try
      {
         if (!saxParser.hasNext() && readStatus == ReadStatus.SAX_RUNNING)
         {
            readStatus = ReadStatus.SAX_COMPLETE;
            return;
         }
         int eventType = saxParser.next();
         if (callbackWrapper == null)
         {
            return;
         }
         
         XMLEvent event = getXMLEvent(saxParser);
         switch (eventType)
         {
            case XMLStreamConstants.START_ELEMENT:
               //emulate startPrefixMapping
               for (int i = 0; i < saxParser.getNamespaceCount(); i++)
               {
                  callbackWrapper.startPrefixMapping(saxParser.getNamespacePrefix(i),
                                                     saxParser.getNamespaceURI(i));
               }
               callbackWrapper.startElement(event.asStartElement());
               break;
            case XMLStreamConstants.END_ELEMENT:
               callbackWrapper.endElement(event.asEndElement());
               //emulate startPrefixMapping
               for (int i = saxParser.getNamespaceCount() - 1; i >= 0; i--)
               {
                  callbackWrapper.endPrefixMapping(saxParser.getNamespacePrefix(i));
               }
               break;
            case XMLStreamConstants.PROCESSING_INSTRUCTION:
               callbackWrapper.processingInstruction((ProcessingInstruction)event);
               break;
            case XMLStreamConstants.CHARACTERS:
               callbackWrapper.characters(event.asCharacters());
               break;
            case XMLStreamConstants.COMMENT:
               break;
            case XMLStreamConstants.SPACE:
               callbackWrapper.ignorableWhitespace(event.asCharacters());
               break;
            case XMLStreamConstants.START_DOCUMENT:
               callbackWrapper.startDocument();
               break;
            case XMLStreamConstants.END_DOCUMENT:
               callbackWrapper.endDocument();
               break;
            case XMLStreamConstants.NOTATION_DECLARATION:
               callbackWrapper.notationDecl((NotationDeclaration)event);
               break;
            case XMLStreamConstants.DTD:
            case XMLStreamConstants.ENTITY_DECLARATION:
            case XMLStreamConstants.ENTITY_REFERENCE:
            case XMLStreamConstants.ATTRIBUTE:
            case XMLStreamConstants.CDATA:
            case XMLStreamConstants.NAMESPACE:
               break;
            default:
               saxParseError("SAX-PARSE-NEXT", "Unexpected error");
               break;
         }
      }
      catch (MismatchedNumberParametersException e)
      {
         mismatchCallbackParameters("SAX-PARSE-NEXT", e.callbackName);
         readStatus = ReadStatus.SAX_PARSER_ERROR;
      }
      catch (MismatchedParametersException e)
      {
         mismatchCallbackNumberParameters(e.callbackName);
         readStatus = ReadStatus.SAX_PARSER_ERROR;
      }
      catch (XMLStreamException e)
      {
         boolean inProlog = e.getMessage().indexOf(ParsingErrorMsgs.SUFFIX_IN_PROLOG) >= 0;;
         if (inProlog)
         {
            if (prologError)
            {
               return;
            }
            
            
            prologError = true;
         }
         
         LOG.log(Level.SEVERE, "SaxReaderImpl", e);
         try
         {
            if (callbackWrapper == null)
            {
               readStatus = ReadStatus.SAX_PARSER_ERROR;
               return;
            }
            
            Boolean isReturn = callbackWrapper.fatalError(e);
            if (isReturn != null && !isReturn)
            {
               readStatus = ReadStatus.SAX_PARSER_ERROR;
               return;
            }
         }
         catch (XMLStreamException ignored) //impossible error
         {
            // no-op
         }

         if (!prologError)
         {
            saxParseError("SAX-PARSE-NEXT", "Unexpected error");
            readStatus = ReadStatus.SAX_PARSER_ERROR;
         }
      }
   }

   /**
    * Specifies the input source of the XML document to be parsed by a SAX-Reader object.
    *
    * @param    mode
    *           The type of the object to process. Can be 'FILE', 'HANDLE', 'MEMPTR' or
    *           'LONGCHAR'.
    * @param    sourceObject
    *           The appropriate object to process.
    *
    * @return   The <code>true</code> in case of success <code>false</code> otherwise.
    */
   @Override
   public logical setInputSource(String mode, Object sourceObject)
   {
      return setInputSource(new character(mode), sourceObject);
   }

   /**
    * Specifies the input source of the XML document to be parsed by a SAX-Reader object.
    *
    * @param    mode
    *           The type of the object to process. Can be 'FILE', 'HANDLE', 'MEMPTR' or
    *           'LONGCHAR'.
    * @param    source
    *           The appropriate object to process.
    *
    * @return   The <code>true</code> in case of success <code>false</code> otherwise.
    */
   @Override
   public logical setInputSource(character mode, Object source)
   {
      final String readMode;
      if (mode.isUnknown())
      {
         readMode = "file";
      }
      else
      {
         readMode = mode.getValue();
      }

      if ("file".equalsIgnoreCase(readMode))
      {
         String fileName = null;
         if (source instanceof String)
         {
            fileName = (String) source;
         }
         if (source instanceof character &&  !((character) source).isUnknown())
         {
            fileName = ((character) source).toStringMessage();
         }
         if (fileName == null)
         {
            invalidArgumentError("SET-INPUT-SOURCE");
            return new logical(false);
         }
         inputStream = new FileInputStreamWrapper(fileName);
      }
      else if ("stream".equalsIgnoreCase(readMode))
      {
         LOG.warning("Stream is not supported");
         inputStream = new InputStream()
         {
            @Override
            public int read()
            throws IOException
            {
               return -1;
            }
         };
         return new logical(true);
      }
      else if ("memptr".equalsIgnoreCase(readMode))
      {
         if (!(source instanceof memptr))
         {
            invalidArgumentError("SET-INPUT-SOURCE");
            return new logical(false);
         }
         inputStream = new MemptrInputStream((memptr) source);
      }
      else if ("stream-handle".equalsIgnoreCase(readMode))
      {
         LOG.warning("Stream-handle is not supported");
         inputStream = new InputStream()
         {
            @Override
            public int read()
            throws IOException
            {
               return -1;
            }
         };
         return new logical(true);
      }
      else if ("longchar".equalsIgnoreCase(readMode))
      {
         if (!(source instanceof longchar))
         {
            invalidArgumentError("SET-INPUT-SOURCE");
            return new logical(false);
         }
         inputStream = new LongcharInputStream((longchar) source);
      }
      else
      {
         String errMsg = "Argument for SET-INPUT-SOURCE must be a 'file', 'longchar', 'memptr', " +
                         "or 'handle'";
         ErrorManager.recordOrShowError(10516, errMsg, false, false, false);
         return new logical(false);
      }

      return new logical(true);
   }

   /**
    * Sets the current value of the namespace location pair mapping.
    *
    * @param    location
    *           The new value the current schema-location attribute.
    */
   @Override
   public void setSchemaLocation(character location)
   {
      if (location.isUnknown())
      {
         invalidArgumentError("SCHEMA-LOCATION");
      }
      schemaLocation = location.toStringMessage();
   }

   /**
    * Adding new match pair for namespace schema and physical file location.
    *
    * @param   namespace
    *          Target namespace of the schema or am empty string or Unknown value.
    * @param   location
    *          Location of the XML schema file.
    *
    * @return  The <code>true</code> in case of success <code>false</code> otherwise.
    */
   @Override
   public logical addSchemaLocation(character namespace, character location)
   {
      if (!BaseDataType.isAllKnown(namespace, location))
      {
         return new logical(false);
      }

      String ns = namespace.toStringMessage().trim();
      String loc = location.toStringMessage().trim();

      if (ns.isEmpty() || loc.isEmpty())
      {
         return new logical(false);
      }

      try
      {
         // both namespace and location must be valid uris
         URI.create(ns);
         URI.create(loc);
      }
      catch (IllegalArgumentException ex)
      {
         return new logical(false);
      }

      schemaLocation = (schemaLocation.isEmpty() ? "" : schemaLocation + " ") + ns + " " + loc;
      return new logical(true);
   }

   /**
    * Sets the delimiter separated list of directory paths used to locate XML DTD associated with
    * a particular XML document.
    *
    * @param    path
    *           The new value the current schema-path attribute.
    */
   @Override
   public void setSchemaPath(character path)
   {
      if (path.isUnknown())
      {
         invalidArgumentAssignError("SCHEMA-PATH");
         return;
      }

      schemaPaths = path.toStringMessage();
   }

   /**
    * Gets the value of the XML schema file for elements with no namespaces.
    *
    * @return   The value of the nonamespace-schema-location attribute.
    */
   @Override
   public character getNonamespaceSchemaLocation()
   {
      return new character(noNameSchemaLocation);
   }

   /**
    * Setting the new value for the nonamespace-schema-location attribute.
    *
    * @param    location
    *           New value of the nonamespace-schema-location attribute to set.
    */
   @Override
   public void setNonamespaceSchemaLocation(character location)
   {
      if (location.isUnknown())
      {
         invalidArgumentAssignError("NONAMESPACE-SCHEMA-LOCATION");
         return;
      }

      noNameSchemaLocation = location.toStringMessage();
   }

   /**
    * Redefines whether the namespace processing is suppressed or not.
    *
    * @param    suppress
    *           The new value the current suppress-namespace-processing attribute.
    */
   @Override
   public void setSuppressNamespaceProcessing(logical suppress)
   {
      this.suppress = suppress.booleanValue();
   }

   /**
    * Redefines whether the parser will validate the XML document against DTD or not.
    *
    * @param    enable
    *           The new value the current validation-enabled attribute.
    */
   @Override
   public void setValidationEnabled(logical enable)
   {
      validate = enable.booleanValue();
   }

   /**
    * Causes parser to stop parsing the XML document.
    *
    * @return   The <code>true</code> in case of success <code>false</code> otherwise.
    */
   @Override
   public logical stopParsing()
   {
      readStatus = ReadStatus.SAX_COMPLETE;
      return new logical(true);
   }

   /**
    * Delete the resource.
    *
    * @return   <code>true</code> if the resource was deleted.
    */
   @Override
   protected boolean resourceDelete()
   {
      if (saxParser != null)
      {
         try
         {
            saxParser.closeCompletely();
         }
         catch (XMLStreamException e)
         {
            LOG.log(Level.SEVERE, "SaxReaderImpl", e);
         }
      }
      isValid = false;
      saxParser = null;
      callbackWrapper = null;
      
      // remove any cache for this resource
      ControlFlowOps.invalidateCallSiteCache(this);

      return true;
   }

   /**
    * Process and display mismatched parameters in callback.
    *
    * @param    method
    *           The read-only attribute.
    * @param    message
    *           Additional error message.
    */
   private static void saxParseError(String method, String message)
   {
      if (method == null)
      {
         throw new IllegalArgumentException("The method is null !");
      }
      String msg = "SAX parser error: %s, %s";
      String err = String.format(msg, method.toUpperCase(), message);
      ErrorManager.recordOrThrowError(14586, err, false);
   }

   /**
    * Add schema validation if it's specified.
    *
    * @throws   XMLStreamException
    *           if the schema file is incorrect or contend of file is invalid.
    */
   private void addValidation()
   throws XMLStreamException
   {
      if (noNameSchemaLocation != null && !noNameSchemaLocation.isEmpty())
      {
         XMLValidationSchemaFactory sf = XMLValidationSchemaFactory.newInstance(
                 XMLValidationSchema.SCHEMA_ID_W3C_SCHEMA);
         boolean isURL = false;
         URL url = null;
         try
         {
            url = new URL(noNameSchemaLocation);
            String protocol = url.getProtocol();
            if ("http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol))
            {
               isURL = true;
            }
         }
         catch (MalformedURLException e)
         {
            // no-op
         }
         final XMLValidationSchema vs;
         if (isURL)
         {
            vs = sf.createSchema(url);
         }
         else
         {
            vs = sf.createSchema(new FileInputStreamWrapper(noNameSchemaLocation));
         }

         saxParser.validateAgainst(vs);
      }
   }

   /**
    * This method allocates an event given the current state of the XMLStreamReader
    *
    * @param    reader
    *           The XMLStreamReader to allocate from
    *
    * @return   The event corresponding to the current reader state.
    *
    * @throws   XMLStreamException
    *           In case of I/O errors.
    */
   private XMLEvent getXMLEvent(XMLStreamReader reader)
   throws XMLStreamException
   {
      return allocator.allocate(reader);
   }

   /**
    * Process and display mismatched parameters in callback.
    *
    * @param    method
    *           The read-only attribute.
    * @param    callbackName
    *           The name of callback.
    */
   private void mismatchCallbackParameters(String method, String callbackName)
   {
      if (method == null)
      {
         throw new IllegalArgumentException("The method is null !");
      }
      String msg = "Callback error: %s sent procedure %s mismatched parameters";
      String err = String.format(msg, method.toUpperCase(), callbackName);
      ErrorManager.recordOrThrowError(10512, err, false);
   }

   /**
    * Process and display mismatched parameters in callback.
    *
    * @param    callbackName
    *           name of callback.
    */
   private void mismatchCallbackNumberParameters(String callbackName)
   {
      if (callbackName == null)
      {
         throw new IllegalArgumentException("The callbackName is null !");
      }
      String msg = "Callback error: mismatched number of parameters passed to procedure %s.";
      String err = String.format(msg, callbackName);
      ErrorManager.recordOrThrowError(10511, err, false);
   }

   /**
    * Special wrapper to wrap read from longchar in lazy mode. It's means that first try to read
    * will check if longchar is available.
    */
   private static class LongcharInputStream
   extends InputStream
   {
      /** A valid longchar object to read*/
      private longchar lchar;

      /** Lazy initialization flag.*/
      private volatile boolean isStarted;

      /** Buffered input Stream.*/
      private ByteArrayInputStream bais;

      /**
       * Constructor accepting a longchar, which will read after first call.
       *
       * @param    source
       *           A valid longchar object.
       */
      private LongcharInputStream(longchar source)
      {
         lchar = source;
         isStarted = false;
      }

      /**
       * Reads the next byte of data from the input stream. The value byte is
       * returned as an <code>int</code> in the range <code>0</code> to
       * <code>255</code>. If no byte is available because the end of the stream
       * has been reached, the value <code>-1</code> is returned.
       */
      @Override
      public int read()
      throws IOException
      {
         if (!isStarted)
         {
            isStarted = true;
            bais = new ByteArrayInputStream(lchar.toStringMessage().getBytes());
         }
         return bais.read();
      }

      /**
       * Resets the input stream to begin reading from the first character
       * of this input stream's underlying buffer.
       */
      @Override
      public synchronized void reset()
      throws IOException
      {
         isStarted = false;
      }

      /**
       * Close input stream.
       */
      @Override
      public void close()
      throws IOException
      {
         if (bais != null)
         {
            bais.close();
         }
      }
   }

   /**
    * Special wrapper to wrap read from memptr in lazy mode. It's means that first try to read
    * will check if memptr is available.
    */
   static class MemptrInputStream
   extends InputStream
   {
      /** Lazy initialization flag.*/
      private volatile boolean isStarted;

      /** A valid memptr */
      private final memptr mptr;

      /** Current read cursor position, started from index 1.*/
      private final AtomicLong position = new AtomicLong();

      /** total size of memptr. */
      private long totalSize = 0;

      /**
       * Constructor accepting a memptr, which will read after first call.
       *
       * @param    source
       *           A valid memptr object.
       */
      MemptrInputStream(memptr source)
      {
         mptr = source;
         position.set(1);
         isStarted = false;         
      }

      /**
       * Reads the next byte of data from the input stream. The value byte is
       * returned as an <code>int</code> in the range <code>0</code> to
       * <code>255</code>. If no byte is available because the end of the stream
       * has been reached, the value <code>-1</code> is returned.
       */
      @Override
      public int read()
      throws IOException
      {
         try
         {
            if (!isStarted)
            {
               isStarted = true;
               totalSize = mptr.length().longValue();
            }
            integer mptrByte = mptr.getByte(position.getAndIncrement());
            if (mptrByte.isUnknown() || totalSize < position.get())
            {
               return -1;
            }
            return mptrByte.intValue();
         }
         catch (ErrorConditionException e)
         {
            throw new IOException(e);
         }
      }
      
      /**
       * Read a chunk of bytes from the current read position in the stream to the
       * offset calculated by given length or to the <code>EOF</code>, whichever
       * comes first. The <code>EOF</code> character is not returned.
       *
       * @param    array
       *           The array in which to save bytes.
       * @param    off
       *           The offset into the array at which to start saving bytes. 
       * @param    len
       *           The maximum number of bytes to read.
       *
       * @return   The number of bytes read or -1 if <code>EOF</code> has been
       *           reached). The number of bytes read will be the smaller
       *           of the <code>len</code> parameter or the actual bytes left
       *           before the <code>EOF</code>.
       * 
       * @throws   IOException
       *           If an I/O error occurs. 
       */
      @Override
      public int read(byte[] array, int off, int len) throws IOException
      {
         if (!isStarted)
         {
            isStarted = true;
            totalSize = mptr.length().longValue();
         }
         
         int bytesRead = (int) Math.min(totalSize - position.get() + 1, len);
         if (bytesRead <= 0)
         {
            return -1;
         }
         BinaryData bytes = mptr.getBytes(position.get(), bytesRead);
         position.set(position.get() + bytesRead);
         
         byte[] read = bytes.getByteArray();
         System.arraycopy(read, 0, array, off, bytesRead);
         
         return bytesRead;
      }

      /**
       * Resets the input stream to begin reading from the first character
       * of this input stream's underlying buffer.
       */
      @Override
      public synchronized void reset()
      throws IOException
      {
         position.set(1);
         isStarted = false;
      }
   }

   /**
    * Special wrapper to wrap read from file in lazy mode. It's means that first try to read
    * will check if file is available.
    */
   static class FileInputStreamWrapper
   extends InputStream
   {
      /** File name.*/
      private final String fileName;

      /** Lazy initialization flag.*/
      private volatile boolean isStarted;

      /** Buffered input Stream.*/
      private Stream stream;

      /** The marked position in the stream. */
      private long mark = -1;

      /**
       * Constructor accepting a file name, which will read after first call.
       *
       * @param    source
       *           A file name.
       */
      FileInputStreamWrapper(String source)
      {
         isStarted = false;
         fileName = source;
      }

      /**
       * Reads the next byte of data from the input stream. The value byte is
       * returned as an <code>int</code> in the range <code>0</code> to
       * <code>255</code>. If no byte is available because the end of the stream
       * has been reached, the value <code>-1</code> is returned.
       */
      @Override
      public int read()
      throws IOException
      {
         if (!isStarted)
         {
            init(false);
         }
         int b = stream.readByte();
         if (b == -2)
         {
            return -1;
         }
         if (b == -1)
         {
            throw new IOException("An exception occurred!");
         }
         return b;
      }
      
      /**
       * Read a chunk of bytes from the current read position in the stream to the
       * offset calculated by given length or to the <code>EOF</code>, whichever
       * comes first. The <code>EOF</code> character is not returned.
       *
       * @param    array
       *           The array in which to save bytes.
       * @param    off
       *           The offset into the array at which to start saving bytes. 
       * @param    len
       *           The maximum number of bytes to read.
       *
       * @return   The number of bytes read or -1 if <code>EOF</code> has been
       *           reached). The number of bytes read will be the smaller
       *           of the <code>len</code> parameter or the actual bytes left
       *           before the <code>EOF</code>.
       * 
       * @throws   IOException
       *           If an I/O error occurs. 
       */
      @Override
      public int read(byte[] array, int off, int len) 
      throws IOException
      {
         if (!isStarted)
         {
            init(false);
         }

         byte[] bytes = stream.readBytes(len);
         if (bytes == null)
         {
            return -1;
         }
         
         System.arraycopy(bytes, 0, array, off, bytes.length);
         
         return bytes.length;
      }

      /**
       * Obtains an estimate of the number of bytes that can be read from this stream without blocking.  
       * This number is not reliable in an absolute sense, but it does specify a minimum that can be used.
       *
       * @return   The minimum number of bytes immediately available.
       */
      @Override
      public int available() 
      throws IOException
      {
         return (int) stream.available();
      }
      
      /**
       * Resets the input stream to begin reading from the first character
       * of this input stream's underlying buffer.
       */
      @Override
      public synchronized void reset()
      throws IOException
      {
         if (isStarted && mark != -1)
         {
            stream.setPos(mark);
         }
         
         mark = -1;
         isStarted = false;
      }
      
      /**
       * Mark the current position in the stream.
       * 
       * @param    readlimit
       *           This argument is not used.
       */
      @Override
      public synchronized void mark(int readlimit)
      {
         if (!isStarted)
         {
            try
            {
               init(false);
            }
            catch (IOException e)
            {
               mark = -1;
            }
         }
         
         try
         {
            mark = stream.getPos();
         }
         catch (UnsupportedOperationException | 
                IOException e)
         {
            mark = -1;
         }
      }
      
      /**
       * Tests if this stream supports the mark mode.
       * 
       * @return  Always<code>true</code>.
       */
      @Override
      public boolean markSupported()
      {
         return true;
      }
      
      /**
       * Initialization method which creates stream wrapper.
       *
       * @param    canWrite
       *           if {@code true} than file will open in read/write mode, otherwise only in
       *           read mode.
       *
       * @throws   IOException
       *           In case of I/O errors.
       */
      private void init(boolean canWrite)
      throws IOException
      {
         try
         {
            stream = StreamFactory.openFileStream(fileName, canWrite, false);
            if (stream == null)
            {
               throw new IOException("An exception occurred!");
            }
         }
         catch (ErrorConditionException e)
         {
            throw new IOException("An exception occurred!", e);
         }
         isStarted = true;
      }

      /**
       * Close input stream.
       */
      @Override
      public void close()
      throws IOException
      {
         if (stream != null)
         {
            stream.close();
         }
      }
   }
}