XmlImport.java

/*
** Module   : XmlImport.java
** Abstract : Read XML content into a temp-table.
**
** Copyright (c) 2017-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 ECF 20171016 Created initial version.
** 002 CA  20180511 Added XML-NODE-NAME table option support.
** 003 ECF 20190123 Replaced TempTableSchema with XmlTempTableSchema. Refactored ExtentTracker
**                  inner class into a separate, top-level class.
** 004 OM  20190327 Renamed DataSource to avoid conflicts with DataSet source.
** 005 OM  20190821 Added DataSet reading support.
** 006 OM  20190909 Added TempTable read and schema validation.
** 007 ECF 20200906 New ORM implementation.
**     CA  20200906 Batch error fix.
** 008 OM  20201120 Added missing features: useLobs, schema location, field mapping.
**                  Fixed restoring table/fields attributes: case-sensitivity, decimals, xml uri, xml prefix,
**                  xml namespace, read-mode, etc
**         20210106 Dropped false parameter of TEMP-TABLE-PREPARE API.
**         20210108 Improve error handling when reading XML serialized files.
**     OM  20210309 Do not use DmoMeta as key in TableMapper because temp-tables may share the same
**                  DmoMeta instance.
**     CA  20210510 Fixed JSON and XML serialization when XML-NODE-NAME/SERIALIZE-NAME field options are set.
**     CA  20210511 Removed XmlTempTableSchema class, as it was obsolete.
**     CA  20210629 Use prodata:wordIndex when reading the table schema.
**     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.
**     OM  20210830 Fixed openReader() and readTableSchema().
**     CA  20211112 Fixed BLOB deserialization and cases when the table has the XML-NODE-NAME/SERIALIZE-NAME
**                  set, but the schema is not included.
**     OM  20220201 Added read support for elements added in XmlExport:H009/20220120.
**     CA  20220218 Ensure the group and decimal separators are ',' and '.' during record read.
**     OM  20220328 Stop and return false if validation errors occur while reading a table.
**     OM  20220422 Rewrite the read in REPLACE mode.
**     CA  20221006 Do not access tableHandle() in the FWD runtime, get the TempTable instance directly.  
**                  Refs #6826
**     TJD 20220504 Java 11 compatibility minor changes
**     CA  20220520 Added support for TEXT and ATTRIBUTE XML-NODE-TYPE values.
**     CA  20220608 NESTED relation fields are automatically copied from the parent to the child table.  Not 
**                  fully tested or implemented.
**     OM  20220914 Use MAX-WIDTH to generate custom sized varchar columns.
**     IAS 20220926 Fixed READ-XMLSCHEMA support.
**     IAS 20221014 More fixes to READ-XMLSCHEMA support.
**     IAS 20221018 Fixed processing of 'now' and 'today' INITIAL attribute value.
**     IAS 20221116 Fixed READ-XMLSCHEMA support for DATASET.
**     IAS 20221121 Replaced IllegalStateException with correct 4GL warning.
**     IAS 20221123 Fixed error message for error 12135. Added names' reset on import error.
**     SVL 20230110 P2JIndex.components() provides direct access to the components array in order to improve
**                  performance.
**     HC  20230116 Replaced some handle usages with the actual wrapped resources for
**                  performance.
**     CA  20230116 Avoid using handle.unwrap, handle.getReference or other BDT usage from within FWD runtime.
**     HC  20230119 Removed the use of CaseInsensitiveString for CaseInsensitiveHashMap to improve
**                  performance.
** 009 OM  20230330 Added NPE guard for dataset:namespace-prefix.
** 010 IAS 20230330 Added support for recursive DATA-SET
**                  Fixed support for NESTED DATA-RELATIONs and multiple namespaces.
**                  Added support for relations with FOREIGN-KEY-HIDDEN = true.
** 011 IAS 20230425 Avoid NPE if the namespace is not specified.
**                  Skip "trivial" values of LABEL and COLUMN-LABEL.
**         20230501 Fixed support for the prodata:initial="prodata:unknown".
**         20230502 Added support for the "prodata:uniqueIndex" attribute of the "index" node.
** 012 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 013 IAS 20230519 Fixed support for DATASET:READ-XML from the XML containing embedded schema. 
** 014 RAA 20230525 Creating a new BaseDataType instance is now done using BaseDataTypeFactory.
** 015 IAS 20230526 Extracted common logic to the 'Importer'.
**     IAS 20230530 Added support for inferring DATASET/TEMP-TABLE structure from XML.
** 016 CA  20230620 Fixed a NPE when the XML has no namespace at the dataset.
**     CA  20230630 'readTableSchema' must just verify the schema if the default buffer of the table already 
**                  exists.
** 017 CA  20230724 Further reduce context-local usage.
** 018 CA  20230907 Added support for X-Document source.  X-Noderef is not yet supported.
** 019 OM  20231027 Nested child tables are not required to be found after all fields from parent table.
** 020 OM  20230922 Added back filter of pseudo path ('.//') in the name setter of the child in a relation.
** 021 CA  20231208 Better detection of extent fields.
** 022 OM  20240118 Fixed the name of nested tables when reading dataset schema. Removed useless casts.
** 023 AI  20231221 Added support for single record syntax for READ-XML.
**     AI  20240125 Added check for comments and whitespace while checking for single record syntax.
** 024 CA  20240201 If the XML row has fields which are not resolved to the current table, then silently 
**                  ignore these fields.
** 025 DDF 20240415 Show error when importing an invalid xml file in a dataset.
** 026 CA  20240523 Fixed DATASET:WRITE-/READ-XML when before tables or (in)active relations are involved.  
** 027 TJD 20240123 Java 17 compatibility updates
** 028 SP  20240627 Error 4065 must not set ERROR-STATUS:ERROR flag.
** 029 AS  20240802 Throw 13514 when dataset name and temp-table name is not found in
**                  the XML file in importDataset().
** 030 AS  20240801 Added external dataset schema support in importDataset().
** 031 OM  20241128 Multi-tenant runtime support: selected the proper persistence context, eventually
**                  based on [sharedDb] parameter.
** 032 AS  20250204 Reduced the logging level to fine for 'unknown attribute' message.
** 033 SP  20250417 Row suffix is not mandatory for table rows when reading.
** 034 ES  20250327 Use seralizationName and xmlNodeName of the buffer, instead of temp-table.
**                  In case of TEMP-TABLE:readXml use attributes of the defaultBuffer. 
**     ES  20250328 Use the XML-NODE-NAME attribute of the buffer in DATASET:READ-XML.
*/

/*
** 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.persist.serial;

import java.io.*;
import java.lang.reflect.*;
import java.nio.file.*;
import java.util.*;
import java.util.Stack;
import java.util.function.*;
import java.util.logging.*;
import javax.xml.namespace.*;
import javax.xml.stream.*;
import org.apache.commons.lang3.tuple.*;
import com.ctc.wstx.api.WstxInputProperties;
import com.goldencode.p2j.convert.*;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.persist.Record;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.persist.orm.Property;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.ErrorManager;
import com.goldencode.p2j.util.logging.*;
import com.goldencode.p2j.xml.*;
import com.goldencode.util.*;

import static com.goldencode.p2j.persist.serial.SerializeOptions.*;

/**
 * An implementation of the 4GL READ-XML method, which parses an XML stream and reads the content
 * into a temp-table.
 * <p>
 * Runtime support is partial at this time; not all modes and features are supported. PRODATASET
 * is not supported at this time.
 */
public final class XmlImport
extends Importer
implements TableReader
{
   /** Logger for objects of this type. */
   private static final CentralLogger LOG = CentralLogger.get(XmlImport.class);
   
   /** XSD schema element name */
   private static final String ELEM_SCHEMA = "schema";

   /** XSD import element name */
   private static final String ELEM_IMPORT = "import";
   
   /** Nil attribute name */
   private static final String ATTR_NIL = "nil";
   
   /** String constant for 'true' */
   private static final String TEXT_TRUE = "true";
   
   /** Verify schema mode. */
   private final Verify verifySchemaMode;
   
   /** Use CLOB/BLOB instead of character/raw type? (Value of {@code override-default-mapping} parameter). */
   private final boolean useLobs; 
   
   /** The future namespace of the read dataset. */
   private String dsNsUri = null;

   /** The future namespace of the read temp-table. */
   private String namespace = null;

   /** The future namespace prefix of the read temp-table. */
   private String ttPrefix = null;

   /** The desired mapping of fields to new type, if one is defined. */
   private final Map<String, String> fieldTypeMapping;
   
   /** 'External' schema location, if one is provided. */
   private final String schemaLocation;
   
   /** Flag indicating that we're reading external schema */
   private boolean external = false;

   /** The mapping of the legacy table names by their (case-insensitive) XML name. */
   private final Map<String, String> tablesByXmlName = new CaseInsensitiveLinkedHashMap<>();
   
   /** Flag indicating that XML document contains embedded schema */
   private boolean embeddedSchema = false;

   /** The list of read relations of a dataset being read. */
   private List<Relation> relations = null;

   /** XML NS refix for NS by NS */
   private Map<String, String> prefixByNs = new HashMap<>();
   
   /** All unique indexes in this dataset, mapped by their unique names. */
   private final Map<String, P2JIndex> uniqueIndexes = new HashMap<>();
   
   /** The stream where the data is read from. */
   private LookAheadXmlStreamReader reader = null;
   
   /**
    * The set of attributes of the current read XML element. Do NOT access it directly, instead
    * use the {@code getXmlAttribute()} method.
    */
   private Map<String, String> crtAttSet = null;
   
   /**
    * The location of the currently processed element. Used to check validation of the
    * {@code crtAttSet}, which acts as a cache. If the position of the current XML element is
    * different from {@code attrSetOffset} then {@code crtAttSet} needs to be updated.
    */
   private int attrSetOffset = 0;
   
   /**
    * Flags whether the processed table has a known schema or it should be read from file.
    * This is irrelevant when processing DataSets.
    */
   private boolean knownTableSchema = true;
   
   /** Flag indicating that the table/dataset structure should be inferred from the XML file. */ 
   private boolean inferStructure = false;
   
   /**
    *  Flag indicating that XML parsing will be self-checked
    *  (for debugging)
    */
   private boolean trackXml = false;
   
   /** Elements' stack */
   private Stack<String> elements = new Stack<>();
   
   /** Elements attributes' stack */ 
   private Stack<Map<String, String>> attrs = new Stack<>();

   /** Set of attributes and text of the field for single entry syntax. */
   private Map<String, String> singleEntryAttr = new HashMap<>();
   
   /** Flag indicating that XML contains row separators.  */
   private boolean singleEntry = false;
   
   /**
    * Constructor which configures the importer for an import of a Dataset or a TempTable/Buffer.
    * 
    * @param   source
    *          XML data source which normalizes access of various media to an input stream.
    * @param   readMode
    *          Read mode to determine how records are stored and how non-unique records are
    *          handled. Not currently honored, except temp-table records are deleted if mode
    *          is EMPTY.
    * @param   schemaLocation
    *          Path to XML schema (not currently used).
    * @param   overrideDefaultMapping
    *          Not currently used.
    * @param   fieldTypeMapping
    *          Not currently used.
    * @param   verifySchemaMode
    *          Not currently used.
    */
   public XmlImport(SourceData source,
                    String readMode,
                    String schemaLocation,
                    boolean overrideDefaultMapping,
                    Map<String, String> fieldTypeMapping,
                    String verifySchemaMode)
   {
      super(source, readMode);
      this.useLobs = overrideDefaultMapping;
      this.verifySchemaMode = (verifySchemaMode == null) ? Verify.LOOSE
                                                         : Verify.valueOf(verifySchemaMode.toUpperCase());
      this.fieldTypeMapping = fieldTypeMapping;
      this.schemaLocation = schemaLocation;
   }
   
   /**
    * Configures a dataset schema or validates it against the XML file. The other parameters are
    * set in constructor.
    * 
    * @param   ds
    *          The destination for read schema.
    * 
    * @return  {@code true} on success. On failure, {@code false} is returned and the eventual
    *          error is already displayed.
    */
   public boolean importDatasetSchema(DataSet ds)
   {
      if (!source.configureSupportedSources(SourceData.SD_READ_XMLSCHEMA, LegacyResource.DATASET + " widget"))
      {
         return false;
      }
      
      if (source.getType().equalsIgnoreCase(TargetData.TYPE_HANDLE))
      {
         return readFromHandle((importer) -> importer.importDatasetSchema(ds));
      }

      this.ds = ds;
      this.relations = new LinkedList<>();
      processHiddenFk(ds);
      
      try (Reader stream = openReader(source.getStream()))
      {
         reader = LookAheadXmlStreamReader.wrap(newXmlInputFactory().createXMLStreamReader(stream));
         
         if (!readDatasetSchema(true))
         {
            ErrorManager.recordOrShowError(13033);
            // Verify Temp-Table or dataset schema against XML Schema failed.
            return false;
         }
         return true;
      }
      catch (IOException | XMLStreamException exc)
      {
         ds.resetNames();
         return false;
      }
      finally
      {
         if (reader != null)
         {
            try
            {
               reader.close();
            }
            catch (XMLStreamException exc)
            {
               // TODO: log
            }
            reader = null;
         }
         this.ds = null;
         this.relations = null;
      }
   }
   
   /**
    * Configures a temp-table schema or validates it against the XML file. The other parameters are set in
    * the constructor.
    * 
    * @param   tt
    *          The destination for read schema.
    * 
    * @return  {@code true} on success. On failure, {@code false} is returned and the eventual error is
    *          already displayed.
    */
   public boolean importTempTableSchema(AbstractTempTable tt)
   {
      if (!source.configureSupportedSources(SourceData.SD_READ_XMLSCHEMA,
                                            LegacyResource.TEMP_TABLE + " widget"))
      {
         return false;
      }
      
      if (source.getType().equalsIgnoreCase(TargetData.TYPE_HANDLE))
      {
         return readFromHandle((importer) -> importer.importTempTableSchema(tt));
      }

      this.ctx.tt = tt;
      
      try (Reader stream = openReader(source.getStream()))
      {
         reader = LookAheadXmlStreamReader.wrap(newXmlInputFactory().createXMLStreamReader(stream));
         
         if (!readTableSchema((String) null, null, null))
         {
            if (tt.prepared().toJavaType())
            {
               ErrorManager.recordOrShowError(13033);
               // Verify Temp-Table or dataset schema against XML Schema failed.
            }
            else
            {
               ErrorManager.recordOrShowError(13032);
               // Unable to create Temp-Table or dataset schema from XML Schema.
            }
            return false;
         }
         return true;
      }
      catch (IOException | XMLStreamException exc)
      {
         return false;
      }
      finally
      {
         if (reader != null)
         {
            try
            {
               reader.close();
            }
            catch (XMLStreamException exc)
            {
               // TODO: log
            }
            reader = null;
         }
         
         this.ctx.tt = null;
      }
   }
   
   /**
    * Imports a dataset. The other parameters are set in constructor.
    *
    * @param   dataSet
    *          The destination for read data.
    * @param   loader
    *          A {@code BiConsumer} which helps loading a {@code Record} into the buffer.
    *
    * @return  {@code true} on success. On failure, {@code false} is returned and the eventual
    *          error is already displayed.
    */
   public boolean importDataset(DataSet dataSet, BiConsumer<RecordBuffer, Record> loader)
   {
      if (!source.configureSupportedSources(SourceData.SD_READ_XML, LegacyResource.DATASET + " widget"))
      {
         return false;
      }

      if (source.getType().equalsIgnoreCase(TargetData.TYPE_HANDLE))
      {
         return readFromHandle((importer) -> importer.importDataset(dataSet, loader));
      }
      
      this.loader = loader;
      this.ds = dataSet;
      
      processHiddenFk(dataSet);
      
      String dsName = null; 
      for (BufferImpl buf : dataSet.getBuffers())
      {
         if (buf.tableHandle()._isValid())
         {
            // this is a temp-table, populate the XML-NODE-NAME map
            TempTable tt = buf.tableHandleNative();
            tablesByXmlName.put(buf.getXmlNodeName().getValue(), tt.name().getValue());
         }
      }
      this.relations = new LinkedList<>();
      this.peerMapping = new HashMap<>();
      
      if (readMode == Read.EMPTY)
      {
         dataSet.emptyDataset();
      }
      
      XMLInputFactory factory = newXmlInputFactory();
      
      //If available, import schema from the schema file
      if (schemaLocation != null && !schemaLocation.isEmpty())
      {
         try (SourceData schemaSource = new SourceData("file", schemaLocation))
         {
            this.embeddedSchema = true;
            reader = LookAheadXmlStreamReader.wrap(factory.createXMLStreamReader(schemaSource.getStream()));
            readDatasetSchema(false);
         } 
         catch (IOException | XMLStreamException e) 
         {
            ErrorManager.recordOrShowError(13033);
            return false;
         }
      }
      
      try (Reader stream = openReader(source.getStream()))
      {
         reader = LookAheadXmlStreamReader.wrap(factory.createXMLStreamReader(stream));
         boolean isRoot = true;
         String nsUri = null;
         String nsPrefix = null;
         boolean dataSetNameMatch = false;
         
         while (reader.hasNext())
         {
            int type = reader.next();
            String name = null;
            switch (type)
            {
               case XMLStreamReader.START_ELEMENT:
                  name = reader.getLocalName();
                  switch (name)
                  {
                     case ELEM_SCHEMA:
                        // check "xmlns:xsd", "xmlns", and "xmlns:prodata" attributes
                        this.embeddedSchema = true;
                        prefixByNs.putAll(getXmlNamespaces()); 
                        dsNsUri = getXmlAttribute("targetNamespace");
                        List<BufferImpl> buffers = ds.getBuffers();
                        for (int nb = 0; nb < buffers.size(); nb++)
                        {
                           AbstractTempTable att = (AbstractTempTable)buffers.get(nb).tableHandleNative();
                           if (att == null)
                           {
                              continue;
                           }
                           String tableName = ((BufferImpl)att.defaultBufferHandleNative())._name();
                           tables.put(tableName, att);
                           String xmlNodeName = buffers.get(nb).getXmlNodeName().getValue();
                           tablesByXmlName.put(xmlNodeName == null ? tableName : xmlNodeName, tableName);
                        }
                          // let the schema load them
                          // tablesByXmlName.clear();
                        
                        // the [schema] node is lost at this moment and "xmlns:xsd", "xmlns", and
                        // "xmlns:prodata" attributes might not be accessible
                        if (!readDatasetSchema(false))
                        {
                           return false;
                        }
                        break;
                     
                     case "datasetChanges":
                        if (!readDatasetData())
                        {
                           return false;
                        }
                        break;
                     
                     default:
                        if (isRoot)
                        {
                           isRoot = false;
                           dsName = name;
                           nsUri = reader.getNamespaceURI();
                           nsPrefix = reader.getPrefix();
                           dataSetNameMatch = ds._name().equals(dsName)                       ||
                                              ds.getSerializeName().getValue().equals(dsName) ||
                                              ds.getXmlNodeName().getValue().equals(dsName);
                        }
                        else
                        {
                           String plainName = tablesByXmlName.get(name);
                           // data located in root node instead of "datasetChanges" node
                           int numBuf = ds.numBuffers().intValue();
                           BufferImpl buffer = null;
                           boolean found = false;
                           for (int i = 1; i <= numBuf; i++)
                           {
                              buffer = ds.getBufferByIndex(i);
                              if (buffer.getXmlNodeName().toStringMessage().equalsIgnoreCase(name) || 
                                  buffer.name().toStringMessage().equalsIgnoreCase(name))
                              {
                                 found = true;
                                 break;
                              }
                              else if (plainName != null && plainName.equals(buffer.name().toStringMessage()))
                              {
                                 found = true;
                                 break;
                              }
                           }
                           if (buffer != null)
                           {
                              boolean tableNameMatch = buffer._name().equals(plainName)                       || 
                                                       buffer.getSerializeName().getValue().equals(plainName) || 
                                                       buffer.getXmlNodeName().getValue().equals(plainName);
                              if (!tableNameMatch && !dataSetNameMatch)
                              {
                                 ErrorManager.recordOrThrowError(13514, "Dataset", ds._name(), ds.getNsUri());
                                 return false;
                              }
                           }

                           if (!found) 
                           {
                              AbstractTempTable att = tables.get(name);
                              if (att != null)
                              {
                                 found = true;
                                 buffer = (BufferImpl)att.defaultBufferHandleNative();
                              }
                           }
                           if (!found)
                           {
                              if (numBuf == 0)
                              {
                                 inferStructure = true;
                                 reader.inferMode(true);
                                 ctx.ttName = name;
                                 ctx.tt = new TempTableBuilder();
                                 String ttNsUri = reader.getNamespaceURI();
                                 String ttNsPrefix = reader.getPrefix();
                                 if (ttNsUri != null)
                                 {
                                    ctx.tt.namespaceURI(ttNsUri);
                                 }
                                 if (ttNsPrefix != null)
                                 {
                                    ctx.tt.namespacePrefix(ttNsPrefix);
                                 }
                                 if (!parseRecord(null))
                                 {
                                    return false;
                                 }
                                 break;
                              }
                              // if the table is not found in database, the record is simply skipped
                              if (!skipRecord(name))
                              {
                                 return false;
                              }
                              break;
                           }
                           
                           TemporaryBuffer ttBuffer = (TemporaryBuffer) buffer.buffer();
                           if (readMode == Read.REPLACE && !readingBefore)
                           {
                              // the REPLACE mode needs primary unique index declared for the after table
                              P2JIndex primaryIndex = ttBuffer.getDmoInfo().getPrimaryIndex(true);
                              if (primaryIndex == null)
                              {
                                 ErrorManager.recordOrShowError(13063, ttBuffer.getDmoInfo().legacyTable);
                                 // REPLACE mode requires a unique primary index in the target table <table>.
                                 return false;
                              }
                           }
                           
//                           ctx.tt = ttBuffer.getParentTable();
                           ctx.setBuffer(ttBuffer, false);
                           
                           if (!readRecord())
                           {
                              return false;
                           }
                        }
                        break;
                  }
                  break;
               
               case XMLStreamReader.END_ELEMENT:
                  if (inferStructure)
                  {
                     if (nsUri != null)
                     {
                        ds.namespaceURI(nsUri);
                     }
                     if (nsPrefix != null)
                     {
                        ds.namespacePrefix(nsPrefix);
                     }
                     
                     setupDataset(dsName);
                  }
                  break; // all OK, should return but let the parser check the rest of the input 
            }
         }
      }
      catch (XMLStreamException xexc)
      {
         ErrorManager.recordOrShowError(13035, source.getFileName(), "");
         // Error reading XML file '<file>'.
         
         int line = xexc.getLocation().getLineNumber();
         int col = xexc.getLocation().getColumnNumber();
         String err = "FATAL ERROR: file '" + source.getFileName() + "', line '" + line +
                      "', column '" + col + "', message '" + xexc.getMessage().replace('\n', ' ') + "'";
         ErrorManager.recordOrShowError(13064, err, "");
         // READ-XML encountered an error while parsing the XML Document: <error>. (13064)
         return false;
      }
      catch (IOException | PersistenceException exc)
      {
         return false;
      }
      catch (Throwable exc)
      {
         // intercept all exceptions 
         if (LOG.isLoggable(Level.SEVERE))
         {
            LOG.log(Level.SEVERE, exc.getMessage() + reader.getLocation(), exc);
         }
         // the rethrow it back
         throw exc;  
      }
      finally
      {
         if (reader != null)
         {
            try
            {
               reader.close();
            }
            catch (XMLStreamException exc)
            {
               // TODO: log
            }
            reader = null;
         }
      }
      
      return true;
   }
   
   /**
    * Reads a TEMP-TABLE from the configured source.
    *
    * @param   tempTable
    *          Temp-table in which data will be stored.
    * @param   loader
    *          A {@code BiConsumer} which helps loading a {@code Record} into the buffer.
    *
    * @return  {@code true} on success.
    */
   public boolean importTable(Buffer buffer,
                              BiConsumer<RecordBuffer, Record> loader)
   {
      AbstractTempTable tempTable = ((BufferReference) buffer).buffer().getParentTable();
      if (!source.configureSupportedSources(SourceData.SD_READ_XML, LegacyResource.TEMP_TABLE + " widget"))
      {
         return false;
      }

      if (source.getType().equalsIgnoreCase(TargetData.TYPE_HANDLE))
      {
         return readFromHandle((importer) -> importer.importTable(buffer, loader));
      }

      String nsPrefix = null;
      String nsURI = null;
      
      knownTableSchema = !tempTable._dynamic() || tempTable._prepared();
      this.loader = loader;
      
      if (knownTableSchema)
      {
         BufferImpl defaultBuffer = (BufferImpl) tempTable.defaultBufferHandleNative();
         ctx.setBuffer(defaultBuffer.buffer(), false);
         
         if (readMode == Read.EMPTY)
         {
            ctx.proxy.deleteAll();
         }
      }
      else if (schemaLocation != null)
      {
         XmlImport xmlImport = new XmlImport(this.source, null, null, this.useLobs,
                                             this.fieldTypeMapping, this.verifySchemaMode.toString());
         boolean b = xmlImport.importTempTableSchema(tempTable);
         if (!b)
         {
            return false;
         }
      }
      
      XMLInputFactory factory = newXmlInputFactory();
      try (Reader stream = openReader(source.getStream()))
      {
         reader = LookAheadXmlStreamReader.wrap(factory.createXMLStreamReader(stream));
         String tableName = ctx.schema == null ? null : ctx.schema.getTableName();
         boolean isRoot = true;
         boolean rowSeparator = false;
         
         while (reader.hasNext())
         {
            int type = reader.next();
            switch (type)
            {
               case XMLStreamReader.START_ELEMENT:
                  if (isRoot)
                  {
                     nsPrefix = reader.getPrefix();
                     nsURI = reader.getNamespaceURI();
                     if (tableName == null && tempTable.getFields().isEmpty())
                     {
                        inferStructure = true;
                        tableName = reader.getLocalName();
                        tempTable.setXmlNodeName(tableName);
                        if (nsURI != null)
                        {
                           tempTable.namespaceURI(nsURI);
                        }
                        if (nsPrefix != null)
                        {
                           tempTable.namespacePrefix(nsPrefix);
                        }
                     }
                     
                     for (int i = 0; i < reader.getAttributeCount(); i++)
                     {
                        singleEntryAttr.put(reader.getAttributeLocalName(i), reader.getAttributeValue(i));
                     }
                     isRoot = false;
                     break;
                  }
                  
                  String name = reader.getLocalName();
                  
                  if (ELEM_SCHEMA.equals(name))
                  {
                     if (!readTableSchema(tempTable, nsPrefix, nsURI))
                     {
                        ErrorManager.recordOrShowError(13033);
                        // Verify Temp-Table or dataset schema against XML Schema failed.
                        return false;
                     }
                     
                     //if (tableName == null)
                     {
                        tableName = ctx.schema.getTableName();
                     }
                     break;
                  }
                  
                  if (tableName == null)
                  {
                     ErrorManager.recordOrShowError(0, "Unsupported: Unable to read XML without schema in a dynamic unprepared TEMP-TABLE", false);
                     // TODO: handle this error
                     return false;
                  }

                  boolean comments = false;
                  String singleAttr = "";
                  while (reader.hasNext() && reader.peek() != XMLStreamReader.START_ELEMENT && reader.peek() != XMLStreamReader.END_ELEMENT)
                  {
                     type = reader.next();
                     if (type == XMLStreamReader.COMMENT)
                     {
                        singleAttr = "";
                        comments = true;
                     }
                     else if (type == XMLStreamReader.CHARACTERS && !comments)
                     {
                        singleAttr = reader.getText();
                     }
                  }
                  singleEntryAttr.put(name, singleAttr);
                  
                  ctx.ttName = tableName;
                  
                  String rowName = buffer.getXmlNodeName().toJavaType();
                  rowName = rowName + "Row";
                           
                  if (rowName.equalsIgnoreCase(name) && reader.peek() == XMLStreamReader.START_ELEMENT)
                  {   
                     rowSeparator = true;
                     if (!readRecord())
                     {
                        return false;
                     }
                  }
                  else
                  {
                     if (reader.peek() == XMLStreamReader.START_ELEMENT)
                     {
                        while (reader.hasNext())
                        {
                           rowSeparator = true;
                           type = reader.next();
                           if (type == XMLStreamReader.END_ELEMENT && name.equals(reader.getLocalName()))
                           {
                              break;
                           }
                        }
                     }
                  }
                  break;
               
               case XMLStreamReader.END_ELEMENT:
               {
                  if (reader.getLocalName().equalsIgnoreCase(tempTable.getXmlNodeName().toJavaType()))
                  {
                     if (!rowSeparator && !singleEntryAttr.isEmpty())
                     {
                        singleEntry = true;
                        return readRecord();
                     }
                     else
                     {
                        return true;
                     }
                  }
               }
            }
         }
      }
      catch (XMLStreamException xexc)
      {
         ErrorManager.recordOrShowError(13035, source.getFileName(), "");
         // Error reading XML file '<file>'.
         
         int line = xexc.getLocation().getLineNumber();
         int col = xexc.getLocation().getColumnNumber();
         String err = "FATAL ERROR: file '" + source.getFileName() + "', line '" + line +
                      "', column '" + col + "', message '" + xexc.getMessage().replace('\n', ' ') + "'";
         ErrorManager.recordOrShowError(13064, err, "");
         // READ-XML encountered an error while parsing the XML Document: <error>. (13064)
         return false;
      }
      catch (PersistenceException pexc)
      {
         // intercept all exceptions 
         if (LOG.isLoggable(Level.SEVERE))
         {
            LOG.log(Level.SEVERE, pexc.getMessage() + reader.getLocation(), pexc);
         }
         return false;
      }
      catch (IOException exc)
      {
         // this is a bit strange:
         ErrorManager.recordOrShowError(4065, false, "READ-XML", "TEMP-TABLE widget");
         // **The <attribute> attribute on the <widget id> has invalid arguments.
         return false;
      }
      finally
      {
         if (reader != null)
         {
            try
            {
               reader.close();
            }
            catch (XMLStreamException exc)
            {
               // TODO: log
            }
            reader = null;
         }
      }
      return true;
   }
   
   /**
    * Read XML/JSON data from the input source and store it in the just created empty TEMP-TABLE.
    *  
    * @param   tableName 
    *          table name
    * @param   table 
    *          TEMP-TABLE to be loaded.
    * @param   loader
    *          A {@code BiConsumer} which helps loading a {@code Record} into the buffer.
    *
    * @return  {@code true} on success.
    * 
    * @throws  PersistenceException
    *          if there is any error reading or storing XML/JSON data.
    */
   public boolean readTable(String tableName, TempTableBuilder table, BiConsumer<RecordBuffer, Record> loader)
   throws PersistenceException
   {
      Buffer parent = ctx.proxy;
      pushContext();
      this.ctx.tt = table;
      this.ctx.ttName = tableName;
      this.tables.put(tableName, table);
      String ttNsUri = reader.getNamespaceURI();
      String ttNsPrefix = reader.getPrefix();
      if (ttNsUri != null)
      {
         ctx.tt.namespaceURI(ttNsUri);
      }
      if (ttNsPrefix != null)
      {
         ctx.tt.namespacePrefix(ttNsPrefix);
      }
      char groupSep = decimal.getGroupSeparator();
      char decSep = decimal.getDecimalSeparator();
      try 
      {
         if (!setParentValues(tableName))
         {
            return false;
         }
         if (ctx.ttName.length() > 32 )
         {
            ErrorManager.recordOrShowError(
                     new int[] {13116, 13040},
                     new String[] {
                         "Temp-Table or dataset name is limited to 32 characters",
                         "Unable to infer Temp-Table or dataset schema from XML Data"
                     },
                     false, // modal
                     true,  // prefix
                     false, // isError
                     false, // asMsg
                     true   // addDot
            );
            return false;
         }
         if (!readRecord(parent))
         {
            return false;
         }
      }
      catch (XMLStreamException exc)
      {
         throw new PersistenceException(exc);
      }
      finally
      {
         decimal.setNumericFormat(decSep, groupSep);
         popContext();
      }
      return false;
      
   }
   
   /**
    * Get an inferred type of the value.
    * 
    * @param val
    *        value to be analyzed
    *
    * @return an inferred type of the value.
    * 
    * @throws PersistenceException 
    *         on invalid value type
    */
   @Override
   protected ParmType typeOf(Object val) throws PersistenceException
   {
      return ParmType.CHAR;
   }

   /**
    * Import from a handle.  Only {@link XDocument X-Document} is currently supported.
    * 
    * @param    reader
    *           The reader function.
    *           
    * @return   <code>true</code> if the import was successful.
    */
   private boolean readFromHandle(Function<XmlImport, Boolean> reader)
   {
      longchar lc = new longchar("");

      XDocumentImpl xdoc = (XDocumentImpl) ((handle) source.getSource()).get();
      boolean[] res = new boolean[1];
      ErrorManager.nestedSilent(() -> res[0] = xdoc.save("longchar", lc).booleanValue());
      if (!res[0])
      {
         return false;
      }
      
      SourceData sd = new SourceData("longchar", lc);
      XmlImport importer = new XmlImport(sd, 
                                         readMode.toString(),
                                         schemaLocation,
                                         useLobs,
                                         fieldTypeMapping,
                                         verifySchemaMode.toString());
      
      return reader.apply(importer);
   }

   /**
    * Create a {@link Reader} which has the encoding as specified in the XML file targeted by this stream.
    * <p>
    * If no encoding is specified, it defaults to UTF-8.
    * <p>
    * The reader is wrapped in a filter which automatically escapes ISO control characters which are not
    * whitespace.
    * 
    * @param stream
    *        input stream.
    * @return  A reader for this stream with the proper encoding.
    */
   private Reader openReader(InputStream stream)
   throws IOException
   {
      String encoding = XmlHelper.readXMLEncoding(stream);
      if (encoding == null)
      {
         // always default to UTF-8
         encoding = "UTF-8";
      }
      Reader reader = new InputStreamReader(stream, encoding);
      return new EscapeControlCharsReader(reader);
   }
   
   /**
    * Create a factory which treats XML 1.0 files as XML 1.1.
    * 
    * @return   A factory to read XML 1.0 files as XML 1.1, to allow control characters.
    */
   private XMLInputFactory newXmlInputFactory()
   {
      XMLInputFactory factory = XMLInputFactory.newInstance();
      factory.setProperty(WstxInputProperties.P_ALLOW_XML11_ESCAPED_CHARS_IN_XML10, true);
      
      return factory;
   }
   
   /**
    * Reads and processes a {@code TEMP-TABLE} schema.
    *
    * @param   nsPrefix
    *          The detected XML namespace prefix.
    * @param   nsUri
    *          The detected XML namespace URI.
    *
    * @return  {@code true} on success and {@code false} otherwise. When {@code false} is returned
    *          then the error message has already been dispatched to {@code ErrorManager}.
    *
    * @throws  XMLStreamException
    *          If a XML-related error occurs (like an unexpected end of stream).
    */
   private boolean readTableSchema(AbstractTempTable tempTable, String nsPrefix, String nsUri)
   throws XMLStreamException
   {
      TempTableBuilder ttb = tempTable._dynamic() ? (TempTableBuilder) tempTable : null;
      String legacyTableName = null;
      String xmlName = null;
      String undoable = null;
      
      List<String> readFields = new LinkedList<>();
      while (reader.hasNext())
      {
         int type = reader.next();
         String name = null;
         switch (type)
         {
            case XMLStreamReader.START_ELEMENT:
               if (verifySchemaMode == Verify.IGNORE && !tempTable._dynamic())
               {
                  // for static temp-tables, IGNORE mode will ignore all schema nodes
                  break;
               }
               
               name = reader.getLocalName();
               
               // index properties:
               String indexName = null;
               boolean primary = false;
               boolean unique = false;
               boolean word = false;
               
               switch (name)
               {
                  case "element":
                     if (Boolean.parseBoolean(getXmlAttribute("prodata:proTempTable")))
                     {
                        // this is the table definition
                        legacyTableName = getXmlAttribute("prodata:tableName");
                        xmlName = getXmlAttribute("name");
                        undoable = getXmlAttribute("prodata:undo");
                        
                        if (ttb != null)
                        {
                           tables.put(xmlName, ttb);
                           tablesByXmlName.put(xmlName + "Row", xmlName);
                        }
                        
                        break;
                     }
                     
                     String fieldType = getXmlAttribute("type");
                     
                     if (fieldType != null)
                     {
                        String fieldName = getXmlAttribute("name");
                        String extentMax = getXmlAttribute("maxOccurs");
                        String ablDataType = getXmlAttribute("prodata:dataType");
                        String nullable = getXmlAttribute("nullable");
                        String defaultStrVal = getXmlAttribute("default");
                        String decimals = getXmlAttribute("prodata:decimals");
                        String format = getXmlAttribute("prodata:format");
                        String cs = getXmlAttribute("prodata:caseSensitive");
                        String optional = getXmlAttribute("nillable");
                        
                        // this a field definition
                        String ablType = get4GLType(
                              fieldType, ablDataType, nullable, useLobs, legacyTableName, fieldName);
                        if (ttb != null)
                        {
                           ParmType parmType = TempTableBuilder.validateDatatype(ablType);
                           if (parmType == null)
                           {
                              ErrorManager.recordOrShowError(13201, ablType, fieldName);
                              // Invalid data type override '<type>' for field '<field>'.
                              ErrorManager.recordOrShowError(13032);
                              // Unable to create Temp-Table or dataset schema from XML Schema.
                              return false;
                           }
                           
                           if (format == null)
                           {
                              format = TempTableBuilder.getDefaultDataTypeFormat(ablType);
                           }
                           long extent = extentMax == null ? 0 : Long.parseLong(extentMax);
                           BaseDataType defVal = null;
                           if (BaseDataType.class.isAssignableFrom(parmType.getParamType()) && 
                               defaultStrVal != null)
                           {
                              try
                              {
                                 Constructor<? extends BaseDataType> ctor =
                                       (Constructor<? extends BaseDataType>)
                                             parmType.getParamType().getConstructor(String.class);
                                 defVal = ctor.newInstance(defaultStrVal);
                              }
                              catch (ReflectiveOperationException e)
                              {
                                 return false;
                              }
                           }
                           
                           ttb.addField(new P2JField(fieldName, parmType, extent, format, defVal, null, null,
                                                     Boolean.parseBoolean(cs), null, null, false, null,
                                                     null, null, null, !Boolean.parseBoolean(optional),
                                                     decimals == null ? 0 : Integer.parseInt(decimals), 0));
                        }
                        else if (verifySchemaMode != Verify.IGNORE)
                        {
                           TableMapper.LegacyFieldInfo lfi = TableMapper.getLegacyFieldInfo(
                                 tempTable, fieldName);
                           if (lfi == null)
                           {
                              if (verifySchemaMode == Verify.STRICT)
                              {
                                 ErrorManager.recordOrShowError(13091, legacyTableName, fieldName);
                                 return false;
                              }
                           }
                           else if (!Util.checkType(lfi.getDataType(), fieldType, ablDataType) ||
                                    !Util.checkExtent(lfi.getExtent(), extentMax)              ||
                                    !Util.checkMandatory(lfi.isMandatory(), nullable))
                           {
                              ErrorManager.recordOrShowError(13089, fieldName, legacyTableName);
                              return false;
                           }
                           
                           readFields.add(fieldName);
                        }
                     }
                     break;
                  
                  case ELEM_SCHEMA:
                     // check "xmlns:xsd", "xmlns", and "xmlns:prodata" attributes
                     break;
                  
                  case "unique":
                     // found an unique index
                     indexName = getXmlAttribute("prodata:indexName");
                     primary = Boolean.parseBoolean(getXmlAttribute("prodata:primaryIndex"));
                     word = Boolean.parseBoolean(getXmlAttribute("prodata:wordIndex"));
                     if (!readIndex(getXmlAttribute("name"), indexName, nsPrefix, primary, true, word, false))
                     {
                        return false;
                     }
                     break;
                  
                  case "index":
                     // found an non-unique index
                     indexName = getXmlAttribute("prodata:indexName");
                     primary = Boolean.parseBoolean(getXmlAttribute("prodata:primaryIndex"));
                     unique = Boolean.parseBoolean(getXmlAttribute("prodata:uniqueIndex"));
                     word = Boolean.parseBoolean(getXmlAttribute("prodata:wordIndex"));
                     if (!readIndex(getXmlAttribute("name"), indexName, nsPrefix, primary, unique, word, true))
                     {
                        return false;
                     }
                     break;
                  
                  case "complexType":
                  case "sequence":
                  case "annotation":
                  case "appinfo":
                     // tolerate them, nothing to do ta this time
                     break;
               }
               break;
            
            case XMLStreamReader.END_ELEMENT:
               name = reader.getLocalName();
               if (ELEM_SCHEMA.equals(name))
               {
                  if (legacyTableName == null || legacyTableName.isEmpty())
                  {
                     legacyTableName = xmlName;
                  }
                  
                  if (ttb == null)
                  {
                     if (verifySchemaMode == Verify.STRICT)
                     {
                        List<TableMapper.LegacyFieldInfo> allFields =
                              TableMapper.getLegacyOrderedList(tempTable.defaultBufferHandleNative(), false);
                        for (TableMapper.LegacyFieldInfo field : allFields)
                        {
                           String fwdName = field.getLegacyName();
                           if (!readFields.remove(fwdName))
                           {
                              ErrorManager.recordOrShowError(13090, fwdName, legacyTableName);
                              // XML Schema definition for field '<field>' in table '<table>' not found.
                              return false;
                           }
                        }
                     }
                     return true;
                  }
                  
                  if (!ttb._prepared())
                  {
                     ttb.setCanUndo(new logical(Boolean.parseBoolean(undoable)));
                     String b4table = b4tables.get(legacyTableName);
                     boolean ok = ttb.tempTablePrepare(legacyTableName, b4table);
                     if (!ok)
                     {
                        // TODO: report error? already reported? 
                        return false;
                     }
                     ttb.setXmlNodeName(xmlName);
                     ttb.namespaceURI(nsUri);
                     ttb.namespacePrefix(nsPrefix);
                     BufferImpl defaultBuffer = (BufferImpl) ttb.defaultBufferHandleNative();
                     ctx.setBuffer(defaultBuffer.buffer(), false);
                  }
                  
                  return true;
               }
               break;
         }
      }
      
      throw new XMLStreamException("Unexpected EOS in readTableSchema().", reader.getLocation());
   }
   
   /**
    * Reads and processes a {@code DataSet} schema. If the {@code DataSet} is in CLEAR state, the
    * structure is initialized and its tables and relations are set up. Otherwise the schema is
    * checked according to {@code verifySchemaMode}.
    *
    * @param   verify
    *          When {@code true} the schema is only verified. Otherwise the methods attempts to create the
    *          read schema into the dataset.
    * @return  {@code true} on success and {@code false} otherwise. When {@code false} is returned
    *          then the error message has already been dispatched to {@code ErrorManager}.  
    * 
    * @throws  XMLStreamException
    *          If a XML-related error occurs (like an unexpected end of stream).
    */
   private boolean readDatasetSchema(boolean verify)
   throws XMLStreamException
   {
      Relation keyrefRel = null;
      String nsPrefix = null;
      while (reader.hasNext())
      {
         int type = reader.next();
         String name = null; 
         switch (type)
         {
            case XMLStreamReader.START_ELEMENT:
               if (verifySchemaMode == Verify.IGNORE && !ds._dynamic())
               {
                  // for static datasets, IGNORE mode will ignore all schema nodes
                  break;
               }
               
               name = reader.getLocalName();
               
               // index properties:
               String indexName = null;
               boolean primary = false;
               boolean unique = false;
               boolean word = false;
               
               switch (name)
               {
                  case "element":
                     boolean isDS = Boolean.parseBoolean(getXmlAttribute("prodata:proDataSet"));
                     if (isDS)
                     {
                        String datasetName = getXmlAttribute("prodata:datasetName");
                        if (datasetName == null)
                        {
                           // if dedicated attribute not found, use the xml name attribute
                           datasetName = getXmlAttribute("name");
                        }
                        if (ds._dynamic() && (!embeddedSchema || ds.getBuffers().isEmpty()))
                        {
                           ds.name(datasetName);
                           nsPrefix = getXmlAttribute("prodata:prefix");
                           if (nsPrefix != null)
                           {
                              ds.namespacePrefix(nsPrefix);
                           }
                           if (dsNsUri != null)
                           {
                              ds.namespaceURI(dsNsUri);
                           }
                        }
                        else if (!ds._dynamic() && 
                                 (!ds._name().equals(datasetName) || 
                                  (dsNsUri != null && !ds.getNsUri().equals(dsNsUri))))
                        {
                           ErrorManager.recordOrShowWarning(13514, "Dataset", ds._name(), ds.getNsUri());
                           // <Temp-Table or Dataset> name '<name>' in namespace '<namespace>' not found in XML Document.
                           return false;
                        }
                        // else simply ignore it (?)
                     }
                     else 
                     {
                        String tableName = getXmlAttribute("prodata:tableName");
                        AbstractTempTable att = null;
                        String tableRef = null;
                        if (tableName == null)
                        {
                           // if dedicated attribute not found, use the xml name attribute
                           tableName = getXmlAttribute("name");
                        }
                        if (tableName == null)
                        {
                          tableRef = getXmlAttribute("ref");
                          att = tables.get(removeNsPrefix(tableRef));
                          if (att == null)
                          {
                             ErrorManager.recordOrShowError(
                                new int[] {13135, 13032}, 
                                new String[] {
                                   "Unable to find element or attribute definition for '" + 
                                               removeNsPrefix(tableRef) + "'",
                                   "Unable to create Temp-Table or dataset schema from XML Schema"
                                }, 
                                // modal, prefix, isError, asMsg, addDot
                                false, false, false, false, true);
                             throw new XMLStreamException("Unknown table reference: [" 
                                                              + tableRef + "]");
                          }
                          tableName = removeNsPrefix(tableRef);
                        }
                        handle buffer = ds.bufferHandle(tableName);
                        if (verifySchemaMode == Verify.STRICT && verify && !buffer._isValid())
                        {
                           // TODO: 4GL will test the other way around: collects all table definitions from
                           //       file and attempt to match dataset in that collection, throwing 10779
                           //       errors first. Then the difference is computed and 13138 is eventually raised
                           
                           ErrorManager.recordOrShowError(13138, tableName);
                           // Temp-table '<table>' not found in XML Schema
                           return false;
                        }
                        if (!readTableSchema(tableName, null))
                        {
                           return false;
                        }
                     }
                     break;
                     
                  case ELEM_IMPORT:
                     if (ds != null)
                     {
                        String schemaLocation = getXmlAttribute("schemaLocation");
                        String extPath = !Serializator.TYPE_FILE.equalsIgnoreCase(source.getType()) 
                              ? null 
                              : Paths.get(source.getFileName()).getParent().toString() + "/" + schemaLocation;
                        if (extPath == null)
                        {
                           ErrorManager.recordOrShowError(
                              new int[] {293, 13101, 13032}, 
                              new String[] {
                                 "** \"" + schemaLocation + "\" was not found",
                                 "Unable to load imported or included schema: " + schemaLocation,
                                 "Unable to create Temp-Table or dataset schema from XML Schema"
                              }, 
                              // modal, prefix, isError, asMsg, addDot
                              false, false, false, false, true);
                           throw new XMLStreamException("Failed to import schema " + schemaLocation);
                        }
                        String ns = getXmlAttribute("namespace");
                        XmlImport ext = new XmlImport(new SourceData("file", extPath), 
                                 null, schemaLocation, false, null, null);
                        ext.external = true;
                        ext.namespace = ns;
                        ext.ds = ds;
                        TempTableBuilder ttb = new TempTableBuilder();
                        if (!ext.importTempTableSchema(ttb)) 
                        {
                           return false;
                        }
                        tables.putAll(ext.tables);
                        tablesByXmlName.putAll(ext.tablesByXmlName);
                        
                     }
                     break;
                     
                  case ELEM_SCHEMA:
                     Map<String, String> attrs = getXmlAttributes();
                     prefixByNs.putAll(getXmlNamespaces()); 
                     // check "xmlns:xsd", "xmlns", and "xmlns:prodata" attributes
                     String ns = getXmlAttribute("targetNamespace");
                     if (ns != null)
                     {
                        ds.namespaceURI(ns);
                     }
                     break;
                     
                  case "unique":
                     // found an unique index
                     indexName = getXmlAttribute("prodata:indexName");
                     primary = Boolean.parseBoolean(getXmlAttribute("prodata:primaryIndex"));
                     word = Boolean.parseBoolean(getXmlAttribute("prodata:wordIndex"));
                     if (!readIndex(getXmlAttribute("name"), indexName, nsPrefix, primary, true, word, false))
                     {
                        return false;
                     }
                     break;
                     
                  case "index":
                     // found an non-unique index
                     indexName = getXmlAttribute("prodata:indexName");
                     primary = Boolean.parseBoolean(getXmlAttribute("prodata:primaryIndex"));
                     unique = Boolean.parseBoolean(getXmlAttribute("prodata:uniqueIndex"));
                     word = Boolean.parseBoolean(getXmlAttribute("prodata:wordIndex"));
                     if (!readIndex(getXmlAttribute("name"), indexName, nsPrefix, primary, unique, word, true))
                     {
                        return false;
                     }
                     break;
                     
                  case "keyref":
                     if (keyrefRel != null)
                     {
                        throw new XMLStreamException(
                              "Unexpected nested xsd:keyref in readDatasetSchema().", reader.getLocation());
                     }
                     
                     // found a relation based on a unique index
                     indexName = getXmlAttribute("refer");
                     P2JIndex uidx = uniqueIndexes.get(removeNsPrefix(indexName));
                     if (uidx == null)
                     {
                        throw new XMLStreamException(
                              "Failed to locate unique index '" + indexName + "' in readDatasetSchema().",
                              reader.getLocation());
                     }
                     boolean nested = Boolean.parseBoolean(getXmlAttribute("prodata:nested"));
                     boolean fkHidden = Boolean.parseBoolean(getXmlAttribute("prodata:foreignKeyHidden"));
                     boolean recursive = Boolean.parseBoolean(getXmlAttribute("prodata:recursive"));
                     boolean active = !Boolean.parseBoolean(getXmlAttribute("prodata:notActive"));
                     keyrefRel = new Relation(getXmlAttribute("name"), uidx, nested, fkHidden, recursive, active);
                     relations.add(keyrefRel);
                     break;
                     
                  case "relation":
                     // found a relation
                     relations.add(new Relation(
                           getXmlAttribute("name"),
                           getXmlAttribute("prodata:parent"),
                           getXmlAttribute("prodata:child"),
                           getXmlAttribute("prodata:relationFields"),
                           Boolean.parseBoolean(getXmlAttribute("prodata:nested")),
                           Boolean.parseBoolean(getXmlAttribute("prodata:foreignKeyHidden")),
                           Boolean.parseBoolean(getXmlAttribute("prodata:recursive")),
                           !Boolean.parseBoolean(getXmlAttribute("prodata:notActive"))
                           ));
                     break;
                     
                  case "selector":
                     if (keyrefRel != null)
                     {
                        keyrefRel.setChild(removeNsPrefix(getXmlAttribute("xpath")));
                     }
                     
                     // otherwise, it is not related to unique index based relation
                     break;
                     
                  case "field":
                     if (keyrefRel != null)
                     {
                        keyrefRel.addChildField(removeNsPrefix(getXmlAttribute("xpath")));
                     }
                     
                     // otherwise, it is not related to unique index based relation
                     break;
                     
                  case "complexType":
                  case "sequence":
                  case "annotation":
                  case "appinfo":
                     // tolerate them, nothing to do at this time
                     break;
               }
               break;
               
            case XMLStreamReader.END_ELEMENT:
               name = reader.getLocalName(); // the name of the element being closed
               
               switch(name)
               {
                  case ELEM_SCHEMA:
                     processDsBuffers();
                     int numTables = ds.numBuffers().intValue();
                     if (verifySchemaMode == Verify.STRICT)
                     {
                        // be sure all tables from ds were read from the file and that their 
                        // fields are matching each other:
                        for (int k = 0; k < numTables; k++)
                        {
                           BufferImpl buff = ds.getBufferByIndex(k + 1);
                           
                           TempTableSchema schema = new TempTableSchema(buff.buffer(), false);
                           String xmlName = schema.getTableName();
                           if (xmlName != null && !xmlName.equals(buff.doGetName()))
                           {
                              xmlName = tablesByXmlName.get(xmlName);
                           }
                           AbstractTempTable att = tables.get(xmlName);
                           if (att == null)
                           {
                              ErrorManager.recordOrShowError(13079, xmlName);
                              // XML Schema definition for temp-table '<table>' not found.
                              return false;
                           }
                        }
                        
                        // check the other way around: all read tables must be present in DS:
                        for (Map.Entry<String, AbstractTempTable> entry : tables.entrySet())
                        {
                           String dstt = entry.getKey();
                           if (!ds.bufferHandle(dstt)._isValid())
                           {
                              ErrorManager.recordOrShowError(13083, dstt);
                              // XML Schema contains extra temp-table definition '<table>'.
                              return false;
                           }
                        }
                        
                        // now check the relations: the problem is the relation name is irrelevant,
                        // we need to do a parent/child search
                        int dsRelCnt = ds.getNumRelations().intValue();
                        List<XmlImport.Relation> relationsCopy = new ArrayList<>(relations);
                        for (int k = 0; k < dsRelCnt; k++)
                        {
                           boolean found = false;
                           DataRelation dsRel = ds.getRelationNative(k + 1);
                           BufferImpl chBuff = dsRel.getChildBufferNative();
                           BufferImpl paBuff = dsRel.getParentBufferNative();
                           for (Relation thisRel : relations)
                           {
                              if (chBuff.doGetName().equalsIgnoreCase(thisRel.child) &&
                                    paBuff.doGetName().equalsIgnoreCase(thisRel.parent))
                              {
                                 if (!dsRel.getRelationFields().toJavaType().equalsIgnoreCase(thisRel.getFields()))
                                 {
                                    ErrorManager.recordOrShowError(13081, dsRel.name().toJavaType());
                                    // XML Schema definition for data-relation between '<table>' and '<table>' not found.
                                    return false;
                                 }
                                 
                                 found = true;
                                 relationsCopy.remove(thisRel);
                              }
                           }
                           if (!found)
                           {
                              ErrorManager.recordOrShowError(13082, paBuff.doGetName(), chBuff.doGetName());
                              // XML Schema definition for data-relation between '<table>' and '<table>' not found.
                              return false;
                           }
                        }
                        
                        // now check the other way around:
                        if (!relationsCopy.isEmpty())
                        {
                           ErrorManager.recordOrShowError(13086, relationsCopy.get(0).name);
                           // XML Schema contains extra data-relation definition '<relation>'. ()
                           return false;
                        }
                     }
                     
                     if (!ds._dynamic())
                     {
                        // in non-STRICT mode: schema is used only for dynamic datasets in CLEAR
                        // state to populate them update the tablesByXmlName map:
//                     tablesByXmlName.clear();
                        for (int k = 0; k < numTables; k++)
                        {
                           BufferImpl buff = ds.getBufferByIndex(k + 1);
                           TempTableSchema schema = new TempTableSchema(buff.buffer(), false);
                           String xmlName = schema.getTableName();
                           if (xmlName != null && !xmlName.equals(buff.doGetName()))
                           {
                              tablesByXmlName.put(xmlName, buff.doGetName());
                           }
                        }
                        
                        return true;
                     }
                     
                     for (Map.Entry<String, AbstractTempTable> entry : tables.entrySet())
                     {
                        AbstractTempTable att = entry.getValue();
                        if (att instanceof StaticTempTable)
                        {
                           continue;
                        }
                        
                        TempTableBuilder ttb = (TempTableBuilder) att;
                        if (ttb._prepared())
                        {
                           continue;
                        }
                        
                        String tableName = entry.getKey();
                        String b4table = b4tables.get(tableName);
                        boolean ok = ttb.tempTablePrepare(tableName, b4table);
                        if (ok)
                        {
                           // set them in dataset:
                           BufferImpl defBuffer = (BufferImpl) ttb.defaultBufferHandleNative();
                           if (!ds.addBuffer(defBuffer).getValue())
                           {
                              // TODO: report error? already reported? 
                              return false;
                           }
                        }
                        else
                        {
                           // TODO: report error? already reported? 
                           return false;
                        }
                     }
                     
                     if (ds._dynamic() && verifySchemaMode != Verify.STRICT) // if (!verify)
                     {
                        // add the relations, if not already added (?)
                        for (Relation rel : relations)
                        {
                           handle hRel = ds.addRelationImpl(
                                 ds.bufferHandle(rel.parent), ds.bufferHandle(rel.child),
                                 new character(rel.getFields()), 
                                    null,  //reposition
                                    new logical(rel.nested), 
                                    null,  // active
                                    new logical(rel.recursive), 
                                    new logical(rel.fkHidden), 
                                    true);
                           if (hRel == null)
                           {
                              continue; // duplicate relation
                           }
                           DataRelation relation = (DataRelation) hRel.getResource();
                           if (relation != null)
                           {
                              relation.name(rel.name);
                              relation.setNested(rel.nested);
                              relation.setActive(rel.active);
                           }
                        }
                     }
                     
                     // we can return now, the schema is fully read 
                     return true;
                     
                  case "keyref":
                     if (keyrefRel == null)
                     {
                        throw new XMLStreamException(
                              "Unexpected end of node keyref in readDatasetSchema().", reader.getLocation());
                     }
                     
                     keyrefRel = null;
                     break;
               }
               break;
         }
      }
      
      throw new XMLStreamException("Unexpected EOS in readDatasetSchema().", reader.getLocation());
   }

   /**
    * Remove the namespace prefix from the qualified name
    * 
    * @param  qname
    *         qualified name to be simplified
    *
    * @return name w/o prefix
    */
   private String removeNsPrefix(String qname)
   {
      int pos = qname.indexOf(':');
      return pos > 0 ? qname.substring(pos + 1) : qname;
      
   }
   /**
    * Process buffers for the imported dataset tables.
    */
   private void processDsBuffers()
   {
      if (!ds._dynamic())
      {
         return;
      }
      
      BufferImpl[] dsBuffers = new BufferImpl[tables.size()];
      int nb = 0;
      int np = 0;
      for (Map.Entry<String, AbstractTempTable> e: tables.entrySet())
      {
         if (e.getValue()._prepared())
         {
            dsBuffers[nb++] = (BufferImpl)e.getValue().defaultBufferHandleNative();
         }
         else
         {
            np++;
            TempTableBuilder ttb = (TempTableBuilder)e.getValue();
            String tableName = e.getKey();
            String b4table = b4tables.get(tableName);
            ttb.tempTablePrepare(tableName, b4table);
            dsBuffers[nb++] = (BufferImpl)ttb.defaultBufferHandleNative();
         }
      }
      if (np > 0)
      {
         ds.setBuffers(dsBuffers);
      }
   }
   
   /**
    * Reads and processes an {@code AbstractTempTable} schema. If the {@code TempTable} is in CLEAR state, the
    * structure is initialized. Otherwise the schema is checked according to {@code verifySchemaMode}.
    *
    * @param   xmlNodeName
    *          The detected XML node name.
    * @param   nsPrefix
    *          The detected XML namespace prefix.
    * @param   nsUri
    *          The detected XML namespace URI.
    *
    * @return  {@code true} on success and {@code false} otherwise. When {@code false} is returned
    *          then the error message has already been dispatched to {@code ErrorManager}.
    *
    * @throws  XMLStreamException
    *          If a XML-related error occurs (like an unexpected end of stream).
    */
   private boolean readTableSchema(String xmlNodeName, String nsPrefix, String nsUri)
   throws XMLStreamException
   {
      boolean undo = false;
      while (reader.hasNext())
      {
         int type = reader.next();
         String name = null; 
         switch (type)
         {
            case XMLStreamReader.START_ELEMENT:
               if (verifySchemaMode == Verify.IGNORE && !ds._dynamic())
               {
                  // for static datasets, IGNORE mode will ignore all schema nodes
                  break;
               }
               
               name = reader.getLocalName();
               if (trackXml)
               {
                  elements.push(name);
                  attrs.push(getXmlAttributes());
               }
               // index properties:
               String indexName = null;
               boolean primary = false;
               boolean unique = false;
               boolean word = false;
               
               switch (name)
               {
                  case "element":
                     boolean isTT = Boolean.parseBoolean(getXmlAttribute("prodata:proTempTable"));
                     if (isTT || external) 
                     {
                        String tableName = getXmlAttribute("prodata:tableName");
                        if (tableName == null)
                        {
                           // if dedicated attribute not found, use the xml name attribute 
                           tableName = getXmlAttribute("name");
                        }
                        else if (xmlNodeName == null && !tableName.equals(getXmlAttribute("name")))
                        {
                           xmlNodeName = getXmlAttribute("name");
                        }
                        if (nsPrefix == null)
                        {
                           nsPrefix = getXmlAttribute("prodata:prefix");
                        }
                        undo = Boolean.parseBoolean(getXmlAttribute("prodata:undo"));
                        
                        AbstractTempTable ctt = null;
                        if (ctx.ttName != null && !tableName.equals(ctx.ttName))
                        {
                           ctt = ctx.tt;
                           ctx.tt = null;
                        }
                        ctx.ttName = tableName;
                        ttPrefix = nsPrefix;
                        try
                        {
                           if (!readTableSchema(tableName, nsPrefix))
                           {
                              return false;
                           }
                        }
                        finally
                        {
                           if (ctt != null)
                           {
                              ctx.tt = ctt;
                           }
                        }
                     }
                     else 
                     {
                        // what? log something?
                        return false;
                     }
                     break;
                     
                  case ELEM_SCHEMA:
                     if (nsUri == null)
                     {
                        nsUri = getXmlAttribute("targetNamespace");
                     }
                     // check "xmlns:xsd", "xmlns", and "xmlns:prodata" attributes
                     break;
                     
                  case "unique":
                     // found an unique index
                     indexName = getXmlAttribute("prodata:indexName");
                     primary = Boolean.parseBoolean(getXmlAttribute("prodata:primaryIndex"));
                     word = Boolean.parseBoolean(getXmlAttribute("prodata:wordIndex"));
                     if (!readIndex(getXmlAttribute("name"), indexName, nsPrefix, primary, true, word, false))
                     {
                        return false;
                     }
                     break;
                     
                  case "index":
                     // found an non-unique index
                     indexName = getXmlAttribute("prodata:indexName");
                     primary = Boolean.parseBoolean(getXmlAttribute("prodata:primaryIndex"));
                     unique = Boolean.parseBoolean(getXmlAttribute("prodata:uniqueIndex"));
                     word = Boolean.parseBoolean(getXmlAttribute("prodata:wordIndex"));
                     if (!readIndex(getXmlAttribute("name"), indexName, nsPrefix, primary, unique, word, true))
                     {
                        return false;
                     }
                     break;
                     
                  case ELEM_IMPORT :
                  case "complexType":
                  case "sequence":
                  case "annotation":
                  case "appinfo":
                     // tolerate them, nothing to do at this time
                     break;
               }
               break;
               
            case XMLStreamReader.END_ELEMENT:
               name = reader.getLocalName();
               if (trackXml)
               {
                  if (!elements.peek().equals(name))
                  {
                     throw new XMLStreamException(
                              String.format("Bad XML - expected '%s': %s, got '%s'", 
                              elements.peek(), attrs.peek().toString(),name));
                  }
                  elements.pop();
                  attrs.pop();
               }
               if (ELEM_SCHEMA.equals(name))
               {
                  if (ctx.tt._dynamic() && !ctx.tt.prepared().booleanValue())
                  {
                     TempTableBuilder ttb = (TempTableBuilder) ctx.tt;
                     ttb.setCanUndo(new logical(undo));
                     if (xmlNodeName != null)
                     {
                        ttb.setXmlNodeName(xmlNodeName);
                     }
                     if (nsPrefix != null)
                     {
                        ttb.namespacePrefix(nsPrefix);
                     }
                     if (nsUri != null)
                     {
                        ttb.namespaceURI(nsUri);
                     }
                     String b4table = b4tables.get(ctx.ttName);
                     if (!external && !ttb.tempTablePrepare(ctx.ttName, b4table))
                     {
                        // TODO: report error? already reported? 
                        return false;
                     }
                  }
               }
               break;
         }
      }
      
      return true;
      //throw new XMLStreamException("Unexpected EOS in readTableSchema().", reader.getLocation());
   }
   
   /**
    * Reads and process an index from the XML stream.
    * 
    * @param   indexName
    *          The index name (aka XML name).
    * @param   originalName
    *          The index's original name only if the xml name was altered because of collisions.
    * @param   nsPrefix
    *          The detected XML namespace prefix.
    * @param   primary
    *          {@code true} if this is a primary index.
    * @param   unique
    *          {@code true} is this is an unique index.
    * @param   word
    *          {@code true} is this is a word index.
    * @param   asIndex
    *          {@code true} is this taken from the 'index' node.
    *
    * @return  {@code true} on success and {@code false} otherwise. When {@code false} is returned
    *          then the error message has already been dispatched to {@code ErrorManager}.  
    *
    * @throws  XMLStreamException
    *          If a XML-related error occurs (like an unexpected end of stream).
    */
   private boolean readIndex(String indexName,
                             String originalName,
                             String nsPrefix,
                             boolean primary,
                             boolean unique,
                             boolean word,
                             boolean asIndex)
   throws XMLStreamException
   {
      if (originalName == null)
      {
         originalName = indexName;
      }
      
      P2JIndex uidx = null;
      AbstractTempTable att = null;
      while (reader.hasNext())
      {
         int type = reader.next();
         switch (type)
         {
            case XMLStreamReader.START_ELEMENT:
               String name = reader.getLocalName();
               switch (name)
               {
                  case "table": // in case of NON-unique indexes
                     att = tables.get(getXmlAttribute("name"));
                     if (att != null && !att._prepared())
                     {
                        att.addNewIndex(new character(originalName),
                                        new logical(unique), 
                                        new logical(primary), 
                                        new logical(word));
                     }
                     
                     // if the table is already prepared, the indexes in schema are not verified, not even if
                     // STRICT mode was specified.
                     break;
                     
                  case "selector": // in case of unique indexes
                     String xpath = getXmlAttribute("xpath");
                     if (xpath.startsWith(".//"))
                     {
                        xpath = xpath.substring(3);
                     }
                     
                     xpath = removeNsPrefix(xpath);
                     xpath = tablesByXmlName.get(xpath);
                     //   if (xpath.endsWith("Row")) 
                     //   {
                     //      xpath = xpath.substring(0, xpath.length() - 3);
                     //   }
                     
                     att = tables.get(xpath);
                     if (att != null && !att._prepared())
                     {
                        att.addNewIndex(new character(originalName), 
                                        new logical(unique), 
                                        new logical(primary), 
                                        new logical(word));
                     }
                     
                     if (unique) // should be true, anyway
                     {
                        uidx = new P2JIndex(xpath, originalName, true);
                        if (uniqueIndexes.put(indexName, uidx) != null)
                        {
                           throw new XMLStreamException(
                                 "Name collision for unique indexes in readDatasetSchema().", 
                                 reader.getLocation());
                        }
                     }
                     // if the table is already prepared, the indexes in schema are not verified, not even if
                     // STRICT mode was specified.
                     break;
                     
                  case "field":
                     String field = removeNsPrefix(getXmlAttribute(unique && !asIndex ? "xpath" : "name"));
                     boolean desc = Boolean.parseBoolean(getXmlAttribute("prodata:descending"));
                     if (att != null && !att._prepared())
                     {
                        att.addFieldToIndex(new character(originalName),
                                            new character(field),
                                            new character(desc ? "desc" : "asc"));
                     }
                     if (uidx != null)
                     {
                        uidx.addComponent(field);
                     }
                     // if the table is already prepared, the indexes in schema are not verified, not even if
                     // STRICT mode was specified.
                     break;
               }
               break;
            
            case XMLStreamReader.END_ELEMENT:
               String localName = reader.getLocalName();
               if ("unique".equals(localName) || "index".equals(localName))
               {
                  return true; // all OK
               }
         }
      }
      
      throw new XMLStreamException("Unexpected EOS in readIndex().", reader.getLocation());
   }
   
   /**
    * Reads data from XML stream and populates the tables of a {@code DataSet}.
    *
    * @return  {@code true} on success and {@code false} otherwise. When {@code false} is returned
    *          then the error message has already been dispatched to {@code ErrorManager}.  
    *
    * @throws  XMLStreamException
    *          if a XML-related error occurs (like an unexpected end of stream).
    * @throws  PersistenceException
    *          if a persistence error occurs while saving data to database.
    */
   private boolean readDatasetData()
   throws XMLStreamException, 
          PersistenceException
   {
      String dsName = null;
      while (reader.hasNext())
      {
         int type = reader.next();
         String name = null;
         switch (type)
         {
            case XMLStreamReader.START_ELEMENT:
               name = reader.getLocalName();
               if (dsName == null)
               {
                  dsName = name;
               }
               else if ("datasetChanges".equals(name))
               {
                  // the root-node
               }
               else if ("before".equals(name))
               {
                  readingBefore = true;
               }
               else // must be a table name
               {
                  BufferImpl after = ds.getBufferByName(name);
                  if (after == null)
                  {
                     String legacy = tablesByXmlName.get(name);
                     if (legacy != null)
                     {
                        after = ds.getBufferByName(legacy);
                     }
                     if (after == null)
                     {
                        // TODO: handle this error
                        return false;
                     }
                  }
                  BufferImpl buffer = after;
                  if (readingBefore)
                  {
                     buffer = after.beforeBufferNative();
                     if (buffer == null)
                     {
                        // TODO: handle this error
                        return false;
                     }
                     buffer.setUpBeforeBuffer(true);
                  }
                  
                  ctx.setBuffer(buffer.buffer(), false);
                  if (!readRecord())
                  {
                     return false;
                  }
               }
               
               break;
            
            case XMLStreamReader.END_ELEMENT:
               name = reader.getLocalName();
               if ("datasetChanges".equals(name))
               {
                  return true; // all OK
               }
               break;
         }
      }
      throw new XMLStreamException("Unexpected EOS in readDatasetData().", reader.getLocation());
   }
   
   /**
    * The read table is automatically added to the dataset if one is processed.
    * 
    * Note that the {@code <xsd:element>} node for this table was already processed
    *      (no look-ahead) in order to decide to call this method.
    * 
    * @param   tableName
    *          The name of the table which is being processed.
    * @param   nsPrefix
    *          The detected XML namespace prefix.
    * 
    * @return  {@code true} if the operation is successful. If the operation fails, {@code false} is returned.
    * 
    * @throws  XMLStreamException
    *          in the event of an exception occurred while reading the XML stream.
    */
   private boolean readTableSchema(String tableName, String nsPrefix)
   throws XMLStreamException
   {
      // detect the working mode
      AbstractTempTable att = tables.get(tableName);
      boolean validate = att != null && att._prepared();
      DmoMeta toMatch = null; // the DMO to verify against
      if (validate && ds != null)
      {
         BufferImpl existingBuffer = ds.getBufferByName(tableName);
         if (existingBuffer != null)
         {
            // if the default buffer of the table already exists we need just to verify the schema of the
            // static/dynamic table, not to create it from the ground up. In this case [att] is null.
            toMatch = existingBuffer.buffer().getDmoInfo();
            att = existingBuffer.buffer().getParentTable();
         }
      }
      if (toMatch == null && att == null)
      {
         att = (ctx.tt == null) ? new TempTableBuilder() : (TempTableBuilder) ctx.tt;
      }
      if (!validate)
      {
         if (namespace != null)
         {
            att.namespaceURI(namespace);
         }
         if (nsPrefix != null)
         {
            att.namespacePrefix(nsPrefix);
         }
         this.tables.putIfAbsent(tableName, att);
         String xmlName = getXmlAttribute("name");
         this.tablesByXmlName.put(xmlName, tableName);
         
         String canUndo = getXmlAttribute("prodata:undo");
         att.setCanUndo(logical.of(Boolean.parseBoolean(canUndo)));
      }
      String b4Table = getXmlAttribute("prodata:beforeTable");
      if (b4Table != null)
      {
         b4tables.put(tableName, b4Table);
      }
      
      Set<Property> fieldsToMatch = null;
      if (verifySchemaMode == Verify.STRICT && toMatch != null)
      {
         fieldsToMatch = new LinkedHashSet<>();
         toMatch.getFields(false).forEachRemaining(fieldsToMatch::add);
      }
      
      // allows to distinct between table-ELEMENT and field-ELEMENT
      boolean moreFields = true;
      // allows to add fields in a correct order if some of them are XML attributes
      SortedMap<Integer, P2JField> fields = new TreeMap<>();
      while (reader.hasNext())
      {
         int tokType = reader.next();
         String name = null;
         switch (tokType)
         {
            case XMLStreamReader.START_ELEMENT:
               name = reader.getLocalName();
               if (trackXml)
               {
                  elements.push(name);
                  attrs.push(getXmlAttributes());
               }
               // index properties:
               String indexName = null;
               boolean primary = false;
               boolean unique = false;
               boolean word = false;
               String xmlNodeType = null; 
               switch (name)
               {
                  case "attribute":
                     xmlNodeType = "ATTRIBUTE";
                  case "element":
                     String userOrder = getXmlAttribute("prodata:userOrder");
                     String _fieldName = getXmlAttribute("name");
                     String _nodeName = getXmlAttribute("prodata:fieldName");
                     String fieldName = _nodeName != null ? _nodeName : _fieldName;
                     String xmlNodeName = _nodeName != null ? _fieldName : null;
                     String help = getXmlAttribute("prodata:help");
                     String codePage = getXmlAttribute("prodata:columnCodepage");
                     String type = getXmlAttribute("type");
                     String fieldLabel = getXmlAttribute("prodata:label");
                     if (_fieldName != null && _fieldName.equals(fieldLabel) || "".equals(fieldLabel))
                     {
                        fieldLabel = null;
                     }
                     String fieldColumnLabel = getXmlAttribute("prodata:columnLabel");
                     if ("".equals(fieldColumnLabel))
                     {
                        fieldColumnLabel = null;
                     }
                     String extentMax = getXmlAttribute("maxOccurs");
                     String extentMin = getXmlAttribute("minOccurs");
                     // true extent fields have [extentMax = extentMin], with strictly positive value
                     if (extentMax == null || !extentMax.equals(extentMin) || "0".equals(extentMax))
                     {
                        // most likely, extentMax='1' and extentMin='0'; this is a scalar field
                        extentMax = extentMin = null;
                     }
                     
                     if (type == null)
                     {
                        if (getXmlAttribute("ref") == null)
                        {
                           // in this case the read [name] attribute is, in fact, the name of the nested table
                           String legacy = getXmlAttribute("prodata:tableName");
                           String prefix = getXmlAttribute("prodata:prefix");
                           if (!readTableSchema(legacy == null ? getXmlAttribute("name") : legacy, prefix))
                           {
                              return false;
                           }
                        }
                     }
                     else 
                     {
                        String ablType = get4GLType(type,
                                                    getXmlAttribute("prodata:dataType"),
                                                    getXmlAttribute("nullable"),
                                                    useLobs && toMatch == null,
                                                    tableName,
                                                    fieldName);
                        boolean cs = Boolean.parseBoolean(getXmlAttribute("prodata:caseSensitive"));
                        int decimals;
                        try
                        {
                           decimals = Integer.parseInt(getXmlAttribute("prodata:decimals"), 10);
                        }
                        catch (NumberFormatException e)
                        {
                           decimals = 0;
                        }
                        
                        String defaultStrVal = getXmlAttribute("default");
                        if (defaultStrVal == null)
                        {
                           defaultStrVal = getXmlAttribute("prodata:initial");
                           if (defaultStrVal != null && defaultStrVal.startsWith("prodata:"))
                           {
                              defaultStrVal = defaultStrVal.substring("prodata:".length());
                           }
                        }
                        
                        ParmType fieldType = TempTableBuilder.validateDatatype(ablType);
                        if (fieldType == null)
                        {
                           ErrorManager.recordOrShowError(13201, ablType, fieldName);
                           // Invalid data type override '<type>' for field '<field>'.
                           ErrorManager.recordOrShowError(13032);
                           // Unable to create Temp-Table or dataset schema from XML Schema.
                           return false;
                        }
                        
                        String format = getXmlAttribute("prodata:format"); //TempTableBuilder.getDefaultDataTypeFormat(ablType);
                        long extent = extentMax == null ? 0 : Long.parseLong(extentMax);
                        
                        BaseDataType defVal = null;
                        Class<?> fwdType = fieldType.getParamType();
                        if (BaseDataType.class.isAssignableFrom(fwdType) && defaultStrVal != null)
                        {
                           if (fwdType == clob.class || fwdType == blob.class)
                           {
                              ErrorManager.recordOrShowError(13143, defaultStrVal, fieldName);
                              // Unable to set initial value '<value>' from XML Schema for field '<field>'.
                              return false;
                           }
                           
                           if (fwdType == date.class && "today".equalsIgnoreCase(defaultStrVal))
                           {
                                defVal = new character("today"); 
                           }
                           else if ((fwdType == datetime.class || fwdType == datetimetz.class) &&
                                     "now".equalsIgnoreCase(defaultStrVal))
                           {
                              defVal = new character("now"); 
                           }
                           else if ("unknown".equals(defaultStrVal))
                           {
                              defVal = new character();
                           }
                           else
                           {
                              try
                              {
                                 Constructor<? extends BaseDataType> ctor = 
                                    (Constructor<? extends BaseDataType>) fwdType.getConstructor(String.class);
                                 defVal = ctor.newInstance(defaultStrVal);
                              }
                              catch (NoSuchMethodException | IllegalAccessException | 
                                     InstantiationException | InvocationTargetException e)
                              {
                                 ErrorManager.recordOrShowError(13143, defaultStrVal, fieldName);
                                 // Unable to set initial value '<value>' from XML Schema for field '<field>'.
                                 return false;
                              }
                           }
                        }
                        
                        if (toMatch == null)
                        {
                           P2JField newField = new P2JField(
                                 fieldName, fieldType, extent, format, defVal, 
                                 fieldLabel, fieldColumnLabel,
                                 cs, codePage, help, false, null, null, 
                                 xmlNodeName, xmlNodeType, false, decimals, 0);
                           if (userOrder == null)
                           {
                              ((TempTableBuilder) att).addField(newField);
                           }
                           else 
                           {
                              fields.put(Integer.parseInt(userOrder), newField);
                           }
                        }
                        else if (verifySchemaMode != Verify.IGNORE)
                        {
                           Property prop = toMatch.byLegacyName(fieldName);
                           if (prop == null || !prop._ablType.equals(ablType) || prop.extent != extent)
                           {
                              if (verifySchemaMode == Verify.STRICT)
                              {
                                 ErrorManager.recordOrShowError(13201, ablType, fieldName);
                                 return false;
                              }
                           }
                           if (extent != 0 && useLobs && 
                               ("character".equalsIgnoreCase(ablType) ||
                                "raw".equalsIgnoreCase(ablType)))
                           {
                              // LOBS (BLOB or CLOB) cannot have extents
                              ErrorManager.recordOrShowError(13141, fieldName);
                              // Invalid 'maxOccurs' attribute value set for BLOB or CLOB field '<field>'.
                              ErrorManager.recordOrShowError(13033);
                              // Verify Temp-Table or dataset schema against XML Schema failed.
                              return false;
                           }
                           if (prop != null && !prop._ablType.equals(ablType))
                           {
                              ErrorManager.recordOrShowError(13089, fieldName, tableName);
                              return false;
                           }
                           
                           if (fieldsToMatch != null) // implies [verifySchemaMode == Verify.STRICT]
                           {
                              fieldsToMatch.remove(prop);
                           }
                        }
                     }
                     break;
                  
                  case "unique":
                     // found an unique index
                     indexName = getXmlAttribute("prodata:indexName");
                     primary = Boolean.parseBoolean(getXmlAttribute("prodata:primaryIndex"));
                     word = Boolean.parseBoolean(getXmlAttribute("prodata:wordIndex"));
                     if (!readIndex(getXmlAttribute("name"), indexName, nsPrefix, primary, true, word, false))
                     {
                        return false;
                     }
                     break;
                  
                  case "index":
                     // found an non-unique index
                     indexName = getXmlAttribute("prodata:indexName");
                     primary = Boolean.parseBoolean(getXmlAttribute("prodata:primaryIndex"));
                     unique = Boolean.parseBoolean(getXmlAttribute("prodata:uniqueIndex"));
                     word = Boolean.parseBoolean(getXmlAttribute("prodata:wordIndex"));
                     if (!readIndex(getXmlAttribute("name"), indexName, nsPrefix, primary, unique, word, true))
                     {
                        return false;
                     }
                     break;
                  
                  case "complexType":
                  case "sequence":
                     // tolerate them, nothing to do at this time
                     break;
               }
               break;
            
            case XMLStreamReader.END_ELEMENT:
               name = reader.getLocalName();
               if (trackXml)
               {
                  if (!elements.peek().equals(name))
                  {
                     throw new XMLStreamException(
                              String.format("Bad XML - expected '%s': %s, got '%s'", 
                              elements.peek(), attrs.peek().toString(),name));
                     }
                  elements.pop();
                  attrs.pop();
               }
               if ("complexType".equals(name))
               {
                  moreFields = false;
                  if (fieldsToMatch != null && !fieldsToMatch.isEmpty()) // implies [verifySchemaMode == Verify.STRICT]
                  {
                     Property fail = fieldsToMatch.iterator().next();
                     ErrorManager.recordOrShowError(13090, fail.legacy, name);
                     // XML Schema definition for field '<field>' in table '<table>' not found.
                     return false;
                  }
               } 
               if (!moreFields)
               {
                  if (validate)
                  {
                     BufferImpl buffer = (BufferImpl) att.defaultBufferHandleNative();
                     DmoMeta meta = buffer.buffer().getDmoInfo();
                     Map<String, Property> dsFields = new HashMap<>();
                     meta.getFields(false).forEachRemaining(p -> dsFields.put(p.legacy, p));
                     for(P2JField field: fields.values())
                     {
                        Property fp = dsFields.get(field.getName());
                        if (fp == null)
                        {
                           continue;
                        }
                        if (!fp._ablType.equals(field.getType().toString()))
                        {
                           ErrorManager.recordOrShowWarning(13089, field.getName(), tableName);
                           // XML Schema definition for field '%1' in table '%2' does not match Temp-Table field definition
                           ErrorManager.recordOrShowWarning(13033);
                           // Verify Temp-Table or dataset schema against XML Schema failed"
                           return false;
                        }
                     }
                  }
                  else
                  {
                     fields.values().stream().forEach(((TempTableBuilder) att)::addField);
                  }
                  return true;
               }
               break;
         }
      }
      
      throw new XMLStreamException("Unexpected EOS in readTableSchema().", reader.getLocation());
   }

   /**
    * Skip the whole subtree because the record belongs to an unknown table. Fast forward to the end tag of
    * the record.
    * 
    * @param   tableName
    *          The table name.
    *
    * @return  {@code true} on success. Otherwise an error is issued before returning {@code false}.
    *
    * @throws  XMLStreamException
    *          if the XML stream cannot be parsed. 
    */
   private boolean skipRecord(String tableName)
   throws XMLStreamException
   {
      while (reader.hasNext())
      {
         int type = reader.next();
         if (type == XMLStreamReader.END_ELEMENT)
         {
            String name = reader.getLocalName();
            if (tableName.equals(name))
            {
               return true; // all OK
            }
         }
      }
      throw new XMLStreamException("Unexpected EOS in readSchema().", reader.getLocation());
   }
   
   /**
    * Read XML content from the stream corresponding with a single data record and store it in
    * the temp-table.
    * <p>
    * This version ensures that the group and decimal separators are ',' and '.' for the duration of the
    * deserialization.
    * 
    * @throws  XMLStreamException
    *          if there is an error reading XML content from the stream.
    * @throws  PersistenceException
    *          if there is an error storing data in the temp-table.
    */
   private boolean readRecord()
   throws XMLStreamException,
          PersistenceException
   {
      return readRecord(null);
   }

   /**
    * Read XML content from the stream corresponding with a single data record and store it in
    * the temp-table.
    * <p>
    * This version ensures that the group and decimal separators are ',' and '.' for the duration of the
    * deserialization.
    * 
    * @param   parent
    *          The parent buffer, if this is called for a NESTED relation fields.
    *          
    * @throws  XMLStreamException
    *          if there is an error reading XML content from the stream.
    * @throws  PersistenceException
    *          if there is an error storing data in the temp-table.
    */
   private boolean readRecord(Buffer parent)
   throws XMLStreamException,
          PersistenceException
   {
      if (ds == null && ctx.proxy != null && ((BufferImpl) ctx.proxy).isBeforeBuffer())
      {
         String field = "?";
         if (reader.hasNext() && reader.nextTag() == XMLStreamReader.START_ELEMENT)
         {
            field = reader.getLocalName();
         }
         ErrorManager.recordOrShowError(13053, field, ctx.proxy.name().toStringMessage());
         // Unable to update field '<field>' in table '<table>'
         return false;
      }
      char groupSep = decimal.getGroupSeparator();
      char decSep = decimal.getDecimalSeparator();

      try
      {
         decimal.setNumericFormat('.', ',');
         
         return ctx.tt._prepared() ? readRecord2(parent) : parseRecord(parent);
      }
      finally
      {
         decimal.setNumericFormat(decSep, groupSep);
      }
   }
   
   /**
    * Read XML content from the stream corresponding with a single data record and store it in
    * the temp-table.
    * 
    * @param   parent
    *          The parent buffer, if this is called for a NESTED relation fields.
    * 
    * @throws  XMLStreamException
    *          if there is an error reading XML content from the stream.
    * @throws  PersistenceException
    *          if there is an error storing data in the temp-table.
    */
   private boolean readRecord2(Buffer parent)
   throws XMLStreamException,
          PersistenceException
   {
      if (readMode != Read.REPLACE)
      {
         processRelations(parent);
      }
      
      String name = null;
      boolean batchError = true;
      RecordBuffer recBuffer = ((BufferImpl) ctx.proxy).buffer();
      Map<String, String> readValues = (readMode == Read.REPLACE) ? new HashMap<>() : null;
      
      try
      {
         if (readMode != Read.REPLACE)
         {
            RecordBuffer.startBatch(bufMan, true);
         }
         
         fillFromParent();
         
         if (singleEntry)
         {
            for (Map.Entry<String, String> entry : singleEntryAttr.entrySet())
            {
               String attrName = entry.getKey();
               String value = entry.getValue();
               
               // skip the surrogate fields [id], [rowState] 
               if ("id".equals(attrName))
               {
                  processId(recBuffer, value);
               }
               else if ("rowState".equals(attrName))
               {
                  processRowState(recBuffer, value);
               }
               else
               {
                  TempTableSchema.Column column = ctx.schema.getColumn(attrName);
                  
                  if (column == null)
                  {
                     // silently ignored
                     continue;
                  }
                  
                  if (readMode == Read.REPLACE)
                  {
                     // in REPLACE mode, store the read values temporarily
                     Integer index = ctx.extentTracker.getNext(attrName);
                     
                     readValues.put(index != null ?
                                          attrName + " " + ctx.extentTracker.getNext(attrName) :
                                          attrName, value);
                  }
                  else
                  {
                     setField(column, attrName, value);
                  }
               }
            }
            
            batchError = false;
            return true;
         }
         
         // read row attributes
         int attrs = reader.isStartElement() ? reader.getAttributeCount() : 0;
         for (int i = 0; i < attrs; i++)
         {
            String attrName = reader.getAttributeLocalName(i);
            String value = reader.getAttributeValue(i);
            
            // skip the surrogate fields [id], [rowState] 
            if ("id".equals(attrName))
            {
               processId(recBuffer, value);
            }
            else if ("rowState".equals(attrName))
            {
               processRowState(recBuffer, value);
            }
            else
            {
               TempTableSchema.Column column = ctx.schema.getColumn(attrName);
               
               if (column == null)
               {
                  LOG.fine("Unknown attribute " + attrName);
                  continue;
               }
               
               if (readMode == Read.REPLACE)
               {
                  // in REPLACE mode, store the read values temporarily
                  Integer index = ctx.extentTracker.getNext(attrName);
                  
                  readValues.put(index != null ?
                                       attrName + " " + ctx.extentTracker.getNext(attrName) :
                                       attrName, value);
               }
               else
               {
                  setField(column, attrName, value);
               }
            }
         }
         
         // read the TEXT field
         for (TempTableSchema.Column column : ctx.schema.textColumns())
         {
            String value = readText();
            String colName = column.getXmlNodeName().toLowerCase();
            
            if (readMode == Read.REPLACE)
            {
               // in REPLACE mode, store the read values temporarily
               Integer index = ctx.extentTracker.getNext(colName);
               
               readValues.put(index != null ? colName + " " + ctx.extentTracker.getNext(colName) : colName,
                              value);
            }
            else
            {
               setField(column, colName, value);
            }
         }
         
         // read row's child elements
         fields: while (reader.hasNext())
         {
            int type = reader.next();
            switch (type)
            {
               case XMLStreamReader.START_ELEMENT:
                  name = reader.getLocalName();
                  if (getXmlAttribute("prods:id") != null)
                  {
                     // this is a nested DataSet table: allow batch to end and call recursively
                     batchError = false;
                     if (!processNestedRecord())
                     {
                        return false;
                     }
                     continue fields;
                  }
                  
                  TempTableSchema.Column column = ctx.schema.getColumn(name);
                  if (column == null)
                  {
                     // maybe this is a record of a nested table of the dataset?
                     if (ds != null)
                     {
                        if (ds.bufferHandle(name)._isValid())
                        {
                           if (!processNestedRecord())
                           {
                              return false;
                           }
                           continue fields;
                        }
                        
                        String legacy = tablesByXmlName.get(name);
                        if (legacy != null && ds.getBufferByName(legacy) != null)
                        {
                           if (!processNestedRecord())
                           {
                              return false;
                           }
                           continue fields;
                        }
                        AbstractTempTable att = tables.get(name);
                        if (att != null)
                        {
                           if (!processNestedRecord())
                           {
                              return false;
                           }
                           continue fields;
                        }
                     }
                     
                     // fields not matching the current table are silently ignored; call 'readText()' as this  
                     // advances after the END_ELEMENT token
                     readText();
                     continue fields;
                  }
                  
                  boolean isNil = checkNil(column);
                  String value = readText();
                  if (readMode == Read.REPLACE)
                  {
                     // in REPLACE mode, store the read values temporarily
                     Integer index = ctx.extentTracker.getNext(name);
                     
                     readValues.put(index != null ? name + " " + ctx.extentTracker.getNext(name) : name,
                                    isNil ? null : value);
                  }
                  else
                  {
                     setField(column, name, isNil ? null : value);
                  }
                  break;
               
               case XMLStreamReader.END_ELEMENT:
                  batchError = false;
                  return true;
            }
         }
         batchError = false;
      }
      catch (XMLStreamException xmlEx)
      {
         ErrorManager.recordOrShowError(13035, source.getFileName(), "");
         // Error reading XML file '<file>'.
         return false;
      }
      finally
      {
         ctx.extentTracker.reset();
         
         if (readMode == Read.REPLACE)
         {
            boolean batched = false;
            try
            {
               Boolean foundConflict = null;
               DmoMeta dmoInfo = recBuffer.getDmoInfo();
               P2JIndex primaryIndex = dmoInfo.getPrimaryIndex(true);
               
               if (primaryIndex != null && primaryIndex.isUnique())
               {
                  List<Object> params = new ArrayList<>();
                  StringBuilder sb = new StringBuilder();
                  sb.append("from ").append(dmoInfo.getDmoImplInterface().getName()).append(" where ");
                  
                  if (recBuffer.isTemporary()) // always, right?
                  {
                     sb.append("_multiplex=?");
                     params.add(recBuffer.getMultiplexID()); // not null if temp-table
                  }
                  
                  Set<String> indexComps = new HashSet<>();
                  ArrayList<P2JIndexComponent> comps = primaryIndex.components();
                  for (int i = 0; i < comps.size(); i++)
                  {
                     P2JIndexComponent comp = comps.get(i);
                     String fieldName = comp.getLegacyName();
                     if (!readValues.containsKey(fieldName)) // this will not match an extent field
                     {
                        foundConflict = false; // not full field-set for the primary unique index
                        break;
                     }
                     
                     if (!params.isEmpty())
                     {
                        sb.append(" and ");
                     }
                     
                     sb.append(comp.getPropertyName()).append("=?");
                     params.add(readValues.get(fieldName));
                     indexComps.add(fieldName);
                  }
                  
                  if (foundConflict == null)
                  {
                     Query uq = Session.createQuery(sb.toString());
                     for (int i = 0; i < params.size(); i++)
                     {
                        uq.setParameter(i, params.get(i));
                     }
                     
                     Object dmo = uq.uniqueResult(recBuffer.getPersistence().getSession(!dmoInfo.multiTenant));
                     foundConflict = (dmo != null);
                     
                     // check if we have the full index field-set but not a matching record
                     if (foundConflict)
                     {
                        loader.accept(recBuffer, (Record) dmo); // loads the record into the buffer
                        
                        RecordBuffer.startBatch(bufMan, true);
                        batched = true;
                        for (Map.Entry<String, String> entry : readValues.entrySet())
                        {
                           String keyName = entry.getKey();
                           if (!indexComps.contains(keyName))// do not overwrite the key/index component
                           {
                              int k = keyName.indexOf(' ');
                              int extIndex = -1;
                              if (k > 0)
                              {
                                 extIndex = Integer.parseInt(keyName.substring(k + 1));
                                 keyName = keyName.substring(0, k);
                              }
                              
                              setField(ctx.schema.getColumn(keyName), keyName, entry.getValue(), extIndex);
                           }
                        }
                     }
                  }
               }
               
               if (foundConflict == null)
               {
                  throw new RuntimeException("Internal error");
               }
               else if (!foundConflict)
               {
                  ctx.proxy.create();
                  
                  RecordBuffer.startBatch(bufMan, true);
                  batched = true;
                  for (Map.Entry<String, String> entry : readValues.entrySet())
                  {
                     String keyName = entry.getKey();
                     int k = keyName.indexOf(' ');
                     int extIndex = -1;
                     if (k > 0)
                     {
                        extIndex = Integer.parseInt(keyName.substring(k + 1));
                        keyName = keyName.substring(0, k - 1);
                     }
                     
                     setField(ctx.schema.getColumn(keyName), keyName, entry.getValue(), extIndex);
                  }
               }
            }
            finally
            {
               if (batched)
               {
                  ErrorManager.ErrorHelper eh = ErrorManager.getErrorHelper();
                  boolean wm = eh.isWarningMode();
                  if (!wm)
                  {
                     // this is mostly for APPEND mode, the other ones will not encounter issues here
                     eh.setWarningMode(true);
                  }
                  int validationErrors = RecordBuffer.endBatch(bufMan, batchError);
                  if (!wm)
                  {
                     eh.setWarningMode(false);
                  }
                  // if (ErrorManager.error().toJavaType())
                  if (ErrorManager.isPendingError() || validationErrors != 0)
                  {
                     ((BufferImpl) ctx.proxy).drop();
                     ErrorManager.recordOrShowError(15366, true, ctx.schema.getName(), "");
                     // Unable to update indexes for table '<table>'. (15366)
                     return false;
                  }
               }
               // else some exception occurred preventing the RecordBuffer to start the batch
            }
         }
         else
         {
            if (readMode == Read.MERGE)
            {
               boolean validated = false;
               try
               {
                  validated = recBuffer.validate(false);
               }
               catch (ValidationException e)
               {
                  // ignore, no 'expected' exception will be thrown
               }
               
               // was there an unique conflict with this record?
               if (!validated)
               {
                  // merge mode: ignore it, just skip to next record
                  ((BufferImpl) ctx.proxy).drop();
               }
            }
            
            ErrorManager.ErrorHelper eh = ErrorManager.getErrorHelper();
            boolean wm = eh.isWarningMode();
            if (!wm)
            {
               // this is mostly for APPEND mode, the other ones will not encounter issues here
               eh.setWarningMode(true);
            }
            int validationErrors = RecordBuffer.endBatch(bufMan, batchError);
            if (!wm)
            {
               eh.setWarningMode(false);
            }
            if (ErrorManager.isPendingError() || validationErrors != 0)
            {
               ((BufferImpl) ctx.proxy).drop();
               ErrorManager.recordOrShowError(13055, true, ctx.schema.getName(), "");
               // Unable to update indexes for table '<table>'. (13055)
               return false;
            }
         }
      }
      
      // if we reached this point something went wrong
      throw new XMLStreamException("Unexpected EOS in readRecord().", reader.getLocation());
   }
   
   /**
    * Process a single nested record. It may have other nested records as well. 
    * 
    * @return  {@code true} on success.
    * 
    * @throws XMLStreamException
    * @throws PersistenceException
    */
   boolean processNestedRecord()
   throws XMLStreamException, PersistenceException
   {
      // push current configuration and call recursively readTableContent().
      // TODO: cache [schema], [ctx.proxy] and [extentTracker] for better performance?
      pushContext();
      
      String name = reader.getLocalName();
      BufferImpl buffer = ds.getBufferByName(name);
      
      if (buffer == null)
      {
         String legacy = tablesByXmlName.get(name);
         if (legacy != null)
         {
            buffer = ds.getBufferByName(legacy);
         }
         if (buffer == null)
         {
            AbstractTempTable att = tables.get(name);
            buffer = (BufferImpl) att.defaultBufferHandleNative();
         }
         
         if (buffer == null)
         {
            // TODO: handle this error
            popContext();
            return false;
         }
      }
      
      ctx.ttName = name;
      ctx.setBuffer(buffer.buffer(), false);
      setParentValues(name);
      
      if (!readRecord(peekContext().proxy))
      {
         popContext();
         return false;
      }
      popContext(); // restore configuration for next row
      return true;
   }
   
   /**
    * Pre-process nested relations. 
    * 
    * @param   parent
    *          The parent buffer, if this is called for a NESTED relation fields.
    */
   private void processRelations(Buffer parent)
   {
      ctx.proxy.create();

      if (parent != null && ds != null)
      {
         List<DataRelation> rels = this.ds.getRelations(parent, true, false, true);

         // this needs to be done first, as there may be other nested records which need to 'inherit'
         // these fields
         for (DataRelation rel : rels)
         {
            if (!rel.isNested().booleanValue())
            {
               continue;
            }
            
            Buffer buf = rel.getChildBufferNative();
            if (buf == ctx.proxy)
            {
               // set all relation fields from the parent buffer to the child buffer
               String[] fields = rel.getRelationFieldArray();
               if (fields == null)
               {
                  // TODO: PARENT-ID and other types of relations
                  continue;
               }
               
               for (int i = 0; i < fields.length; i += 2)
               {
                  String pfield = fields[i];
                  String chfield = fields[i + 1];
                  
                  buf.dereference(chfield, parent.dereference(pfield));
               }
            }
         }
      }
   }
   
   /**
    * Parse XML record to retrieve fields' data, prepare table. populate the first record, and store it in
    * the temp-table.
    * 
    * @param   parent
    *          The parent buffer, if this is called for a NESTED relation fields.
    * 
    * @throws  XMLStreamException
    *          if there is an error reading XML content from the stream.
    * @throws  PersistenceException
    *          if there is an error storing data in the temp-table.
    */
   private boolean parseRecord(Buffer parent)
   throws XMLStreamException,
          PersistenceException
   {
      String name = null;
      boolean persisted = false;
      Map<String, Object> readValues = new LinkedHashMap<>();
      Set<String> attributes = new HashSet<>();
      FieldMaker maker = (ttb, fname, val) -> addField(ttb, fname, val, attributes::contains);
      
      try
      {
         // read row attributes
         int attrs = reader.getAttributeCount();
         for (int i = attrs - 1; i >= 0; i--)
         {
            String attributeLocalName = reader.getAttributeLocalName(i);
            addValue(readValues, attributeLocalName, reader.getAttributeValue(i));
            attributes.add(attributeLocalName);
         }
         
         // TODO: read the TEXT field (?)

         // read row's child elements
         int nest = 0;
         boolean isNil = false;
         StringBuilder sb = new StringBuilder(); 
         fields: while (reader.hasNext())
         {
            int type = reader.peek();
            switch (type)
            {
               case XMLStreamReader.CHARACTERS:
               case XMLStreamReader.CDATA:
                  reader.next();
                  if (nest == 1)
                  {
                     sb.append(reader.getText());
                  }
                  break;
               
               case XMLStreamReader.START_ELEMENT:
                  nest++;
                  if (ds != null && getXmlAttribute("prods:id") != null)
                  {
                     // this is a nested DataSet table: the current row is assumed complete,
                     // allow batch to end and call recursively
                     if (!persist(maker, readValues, parent != null))
                     {
                        return false;
                     }
                     persisted = true;
                     reader.next();
                     name = reader.getLocalName();
                     if (!readNested(name))
                     {
                        return false;
                     }
                     break;
                  }
                  if (nest > 1)
                  {
                     if (ds != null)
                     {
                        if (!persisted)
                        {
                           if (!persist(maker, readValues, parent != null))
                           {
                              return false;
                           }
                           persisted = true;
                        }
                        if (!readNested(name))
                        {
                           return false;
                        }
                        nest = 0;
                        break;
                     }
                     else
                     {
                        throw new PersistenceException("Unexpected nested element: " + name);
                     }
                  }
                  reader.next();
                  name = reader.getLocalName();
                  isNil = checkNil(null);
                  break;
               
               case XMLStreamReader.END_ELEMENT:
                  reader.next();
                  if( --nest == -1)
                  {
                     return persisted ? true : persist(maker, readValues, parent != null);
                  }
                  String value = sb.toString();
                  sb.setLength(0);
                  addValue(readValues, name, isNil ? null : value);
                  isNil = false;
            }
         }
      }
      catch (XMLStreamException xmlEx)
      {
         ErrorManager.recordOrShowError(13035, source.getFileName(), "");
         // Error reading XML file '<file>'.
         return false;
      }
      
      return persisted ? true : persist(maker, readValues, parent != null);
   }
   
   /**
    * Read nested table data.
    * 
    * @param   name
    *          The name of the table to be populated.
    *
    * @return  <code>true</code> in success
    * 
    * @throws PersistenceException
    *         in error
    */
   private boolean readNested(String name)
   throws PersistenceException
   {
      AbstractTempTable att = tables.get(name);
      if (att == null)
      {
         att = new TempTableBuilder();
         inferredRelations.add(Pair.of(ctx.ttName, name));
      }
      return !att.readTable(name, this);
   }
   
   /**
    * Add named value to a map of values, with extent handling.
    * 
    * @param   readValues
    *          The map which colelcts the key/values pairs.
    * @param   name
    *          The key.
    * @param   value
    *          The value associated with the key. If there is already a scalar value associated with the key,
    *          it is replaced by a list containing the extent data. If the old value is already a list the
    *          {@code value} is appended to it.
    */
   private void addValue(Map<String, Object> readValues, String name, String value)
   {
      Object other = readValues.putIfAbsent(name, value);
      if (other != null)
      {
         if (other instanceof List)
         {
            ((List<Object>)other).add(value);
         }
         else
         {
            List<Object> list = new ArrayList<>();
            list.add(other);
            list.add(value);
            readValues.replace(name, list);
         }
      }
   }
   
   /**
    * Read text content from within an element or a CDATA section.
    * 
    * @return  Text which was read.
    * 
    * @throws  XMLStreamException
    *          if there is an error reading XML content from the stream.
    */
   private String readText()
   throws XMLStreamException
   {
      StringBuilder bld = new StringBuilder();
      while (reader.hasNext())
      {
         int type = reader.next();
         switch (type)
         {
            case XMLStreamReader.CHARACTERS:
            case XMLStreamReader.CDATA:
               bld.append(reader.getText());
               break;
            case XMLStreamReader.END_ELEMENT:
               return bld.toString();
         }
      }
      
      return null;
   }
   
   /**
    * Check whether the current temp-table value read from XML content is null. If so, check
    * whether the associated column allows null values.
    * 
    * @param   column
    *          Schema information for the associated column.
    * 
    * @return  {@code true} if the XML content is nil; else {@code false}.
    * 
    * @throws  ErrorConditionException
    *          if value is nil and column does not allow null values.
    */
   private boolean checkNil(TempTableSchema.Column column)
   {
      // read attributes for current element
      int attrs = reader.getAttributeCount();
      for (int i = 0; i < attrs; i++)
      {
         String name = reader.getAttributeLocalName(i);
         String value = reader.getAttributeValue(i);
         
         if (ATTR_NIL.equals(name))
         {
            if (TEXT_TRUE.equals(value))
            {
               if (column != null && !column.isNillable())
               {
                  ErrorManager.recordOrThrowError(0,
                                                  "Column not nillable: " + column.getFieldName(),
                                                  false);
               }
               
               return true;
            }
            
            return false;
         }
      }
      
      return false;
   }
   
   /**
    * Obtain the value of an attribute for the currently processed XML element. The method uses a
    * cache map in order not to process the attributes multiple times. Once they are collected,
    * the attribute values are kept until a request for other element is issued. The method uses
    * {@code attrSetOffset} to compare the location of the current element. If they are different,
    * the cache is updated. Note that the cache is updated only on request, the current of 
    * {@code crtAttSet} may be stalled if the {@code getXmlAttribute()} was not yet called for the
    * new element.
    * 
    * @param   fullName
    *          The full-name of the attribute.
    * 
    * @return  The value of the attribute if found and {@code null} if there is no such attribute
    *          in current element.
    */
   private String getXmlAttribute(String fullName)
   {
      if (reader.getLocation().getCharacterOffset() != attrSetOffset)
      {
         attrSetOffset = reader.getLocation().getCharacterOffset();
         crtAttSet = new HashMap<>();
         for (int k = 0; k < reader.getAttributeCount(); k++)
         {
            QName qName = reader.getAttributeName(k);
            crtAttSet.put(qName.getPrefix().isEmpty() ? qName.getLocalPart() :
                                qName.getPrefix() + ":" + qName.getLocalPart(),
                          reader.getAttributeValue(k));
         }
      }
      return crtAttSet.get(fullName);
   }

   /**
    * Get map of attributes by name.
    * 
    * @return the map of attributes by name.
    */
   private Map<String, String> getXmlAttributes()
   {
      if (reader.getLocation().getCharacterOffset() != attrSetOffset)
      {
         attrSetOffset = reader.getLocation().getCharacterOffset();
         crtAttSet = new HashMap<>();
         for (int k = 0; k < reader.getAttributeCount(); k++)
         {
            QName qName = reader.getAttributeName(k);
            crtAttSet.put(qName.getPrefix().isEmpty() ? qName.getLocalPart() :
                                qName.getPrefix() + ":" + qName.getLocalPart(),
                          reader.getAttributeValue(k));
         }
      }
      return crtAttSet;
   }

   /**
    * Get map of NS prefixes by namespace.
    * 
    * @return the map of NS prefixes by namespace.
    */
   private Map<String, String> getXmlNamespaces()
   {
      Map<String, String> prefixByNs = new HashMap<>();
      for (int k = 0; k < reader.getNamespaceCount(); k++)
      {
         prefixByNs.put(reader.getNamespaceURI(k), 
                        reader.getNamespacePrefix(k));
      }
      prefixByNs.put(reader.getNamespaceURI(), "");
      return prefixByNs;
   }
   
   /**
    * Helper method for detecting the 4GL field type using XML data types. Since some of them
    * overlap, the additional parameters are used to distinct between them. 
    *
    * @param   type
    *          The pure {@code xsd} type. 
    * @param   proType
    *          Additional information needed to distinct the original 4GL type.
    * @param   nullable
    *          Whether the original field type was nullable.
    * @param   useLobs
    *          If {@code true}, {@code clob} and {@code blob} types will be used instead of {@code character}
    *          and {@code raw}, respectively.
    * @param   tableName
    *          Optional. If specified the table name is looked up in the type map instead of the type
    *          specified in XML file.
    * @param   fieldName
    *          Optional. If specified the field name is looked up in the type map instead of the type
    *          specified in XML file.
    *
    * @return  The original 4GL type of the field. If one cannot be detected using the provided
    *          data, {@code null} is returned.
    *
    * @see com.goldencode.p2j.persist.serial.Util#xsdMap
    * @see com.goldencode.p2j.persist.serial.Util#xsdProdataMap
    */
   private String get4GLType(String type,
                             String proType,
                             String nullable,
                             boolean useLobs,
                             String tableName,
                             String fieldName)
   {
      if (type == null)
      {
         return null;
      }
      
      if (fieldTypeMapping != null && fieldName != null)
      {
         // TODO: is un-scoped field lookup necessary?
         String prefType = fieldTypeMapping.get(fieldName);
         if (prefType != null)
         {
            return prefType;
         }
         
         if (tableName != null)
         {
            prefType = fieldTypeMapping.get(tableName + "." + fieldName);
            if (prefType != null)
            {
               return prefType;
            }
         }
      }
      
      switch (type)
      {
         case "xsd:string":
            return useLobs || "prodata:clob".equals(proType) ? "clob" : "character";
         case "xsd:int":
            return "integer";
         case "xsd:decimal":
            return "decimal";
         case "xsd:date":
            return "date";
         case "xsd:boolean":
            return "logical";
         case "xsd:dateTime":
            return "prodata:dateTime".equals(proType) ? "datetime" : "datetime-tz";
         case "xsd:base64Binary":
            if ("prodata:rowid".equals(proType))
            {
               return "rowid";
            }
            return useLobs || "prodata:blob".equals(proType) ? "blob" : "raw";
         case "xsd:long":
            if ("prodata:recid".equals(proType))
            {
               return "recid";
            }
            else if ("prodata:handle".equals(proType))
            {
               return "handle";
            }
            else if ("prodata:comHandle".equals(proType))
            {
               return "comhandle";
            }
            else
            {
               return "int64";
            }
      }
      
      // TOOD: log this, unknown datatype
      return null;
   }
   
   /**
    * Add field to the temp-table.
    * 
    * @param   ttb
    *          The destination temp-table builder.
    * @param   name
    *          The name of the field to be added.
    * @param   val
    *          The value of field. It is used for inferring the type of the new field.
    * @param   isAttr
    *          A predicate which allows identification of fields with {@code ATTRIBUTE} node type.
    *
    * @throws  PersistenceException
    *          If the type of the field cannot be inferred from the provided value.
    */
   private void addField(TempTableBuilder ttb, String name, Object val, Predicate<String> isAttr)
   throws PersistenceException
   {
      String type = typeOf(val).toString();
      if (val instanceof List)
      {
         int ext = ((List<Object>) val).size();
         ttb.addNewField(new character(name), new character(type), new integer(ext));
      }
      else
      {
         if (isAttr.test(name))
         {
            P2JField field = new P2JField(name, typeOf(val), 
                     0, null, null, null, null, false,null, null, false, null, null, null,
                     "ATTRIBUTE", false, 0, 0);
            ttb.addField(field);
         }
         else
         {
            ttb.addNewField(new character(name), new character(type));
         }
      }
   }
   
   /**
    * Internal structure for storing a {@code DataSet} relation until the full set of tables is
    * read.
    */
   private static class Relation
   {
      /** The relation name. */
      private final String name;
      
      /** The name of the parent buffer. */
      private final String parent;
      
      /** The name of the child buffer. */
      private String child;
      
      /** The set of fields for this relation. */
      private final String fields;
      
      /** Flags the nested relations. */
      private final boolean nested;
      
      /** Flags the FOREIHN-KEY-HIDDEN relations. */
      private final boolean fkHidden;

      private final boolean recursive;
      
      /** The list of child's fields involved in this relation. */
      private final List<String> childFields;
   
      /** The lists of parent's fields involved in this relation. */
      private final List<String> parentFields;

      /** Flag indicating if the relation is active. */
      private final boolean active;
      
      /**
       * The constructor initializes the internal data.
       * 
       * @param   name
       *          The relation name.
       * @param   parent
       *          The name of the parent buffer.
       * @param   child
       *          The name of the child buffer.
       * @param   fields
       *          The set of fields for this relation.
       * @param   nested
       *          {@code true} for nested relations.
       * @param   fkHidden
       *          {@code true} for foreign-key-hidden relations 
       * @param   recursive 
       *          {@code true} for recursive relations 
       * @param   active 
       *          {@code true} for active relations 
       */
      public Relation(String  name, 
                      String  parent, 
                      String  child, 
                      String  fields, 
                      boolean nested, 
                      boolean fkHidden, 
                      boolean recursive, 
                      boolean active)
      {
         this.name = name;
         this.parent = parent;
         this.child = child;
         this.fields = fields;
         this.nested = nested;
         this.fkHidden = fkHidden;
         this.recursive = recursive;
         this.childFields = null; // not used with this constructor
         this.parentFields = null; // not used with this constructor
         this.active = active;
      }
      
      /**
       * The constructor initializes the internal data.
       *
       * @param   name
       *          The relation name.
       * @param   parentIndex
       *          The name of the parent table in this relation.
       * @param   nested
       *          {@code true} for nested relations.
       * @param   fkHidden 
       *          {@code true} for foreign-key-hidden relations.
       * @param   recursive 
       *          {@code true} for recursive relations.
       * @param   active 
       *          {@code true} for active relations 
       */
      public Relation(String  name, 
                      P2JIndex parentIndex, 
                      boolean  nested, 
                      boolean  fkHidden, 
                      boolean  recursive, 
                      boolean  active)
      {
         this.name = name;
         this.parent = parentIndex.getTable();
         this.nested = nested;
         this.fkHidden = fkHidden;
         this.recursive = recursive;
         this.fields = null;  // not used with this constructor
         this.childFields = new ArrayList<>();
         this.parentFields = new ArrayList<>();
         this.active = active;
         
         ArrayList<P2JIndexComponent> comps = parentIndex.components();
         for (int i = 0; i < comps.size(); i++)
         {
            P2JIndexComponent comp = comps.get(i);
            parentFields.add(comp.getLegacyName());
         }
      }
      
      /**
       * Sets the name of the child buffer.
       * 
       * @param   xpath
       *          The name of the child buffer, as read from XML.
       */
      public void setChild(String xpath)
      {
         child = xpath.startsWith(".//") ? xpath.substring(3) : xpath;
      }
      
      /**
       * Adds a new field to be used on child side of the relation.
       *
       * @param   field
       *          The name of the field.
       */
      public void addChildField(String field)
      {
         childFields.add(field);
      }
      
      /**
       * Get the set of fields which drives this relation.
       * 
       * @return  the set of pairs or fields of this relation.
       */
      public String getFields()
      {
         if (fields != null)
         {
            return fields;
         }
         
         // else compute it:
         StringBuilder sb = new StringBuilder();
         int size = Math.min(childFields.size(), parentFields.size()); // should be equals, actually!
         for (int i = 0; i < size; i++)
         {
            if (sb.length() != 0)
            {
               sb.append(",");
            }
            sb.append(parentFields.get(i)).append(",").append(childFields.get(i));
         }
         return sb.toString();
      }
   }
}