DataSet.java

/*
** Module   : DataSet.java
** Abstract : Abstracts the DATASET ABL structure.
**
** Copyright (c) 2019-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 OM  20190403 Created initial version with stubbed methods.
** 002 OM  20190508 Ported createDynamicDataSet().
**     CA  20190513 DATASET can use the '::' operator.  Added 'asHandle'.
** 003 OM  20190508 Added runtime implementation for ABL methods and attributes.
**     HC  20190707 Implemented read(dataSet) and read(tempTable) methods of JsonObject legacy
**                  class.
** 004 CA  20190712 Added missing associate API.
**     OM  20190716 Fixed handling of OUTPUT dataset parameters.
**     CA  20190720 Some fixes for BY-REFERENCE and OUTPUT parameters.  These fixes are not proved
**                  and BY-REFERENCE, OUTPUT and DELETE combinations are most likely related to
**                  NUM-REFERENCES attribute.
**     OM  20190726 Callback DataSet handles are sent BY_REFERENCE.
**     CA  20190728 If a callback raises an ERROR condition, invokeCallback must return false.
**                  Also, any FILL operation must be aborted.
**                  For BY-REFERENCE/REFERENCE-ONLY transfer the buffer's record, too.
**     EVL 20190729 Fix from CA to skip same reference processing in associate() method.
**     OM  20190731 Added REFERENCE-ONLY and NUM-REFERENCES support.
**     CA  20190812 Allow this class to be mutable, to allow switching between resources, in cases
**                  of BY-REFERENCE or REFERENCE-ONLY.  WARNING: it is not allowed to access
**                  instance fields, if they are in another instance than 'this' reference!
**                  Use the 'ref()' API to get the runtime instance or add the field to an
**                  accessor method.
**     CA  20190826 Added basic support for 'inverse binding', from callee to caller, via the BIND
**                  parameter option.
**     OM  20190909 Clear the ERROR flag in some methods.
** 005 CA  20191119 Track the executing legacy class, to be able to register any buffer to its 
**                  source definition class.
**     CA  20200110 More fixes for DATASET parameters and resource delete.
** 006 CA  20200427 Allow a DATASET to be read from JSON argument, in case of a remote REST call.
** 007 OM  20201030 Invalid attribute API support for getters/setters.
**     OM  20201118 Extracted SERIALIZE-HIDDEN into independent interface.
**     OM  20201120 Fixed of methods and attributes implementations.
**     OM  20201218 Fixed implementation for error and rejected attributes/hidden fields.
**     OM  20210115 OE quirk/bug: allowed duplicate relations for same buffer pair.
**     OM  20210219 bufferHandle() accepts int64 expression parameters.
**     OM  20210309 Do not use DmoMeta as key in TableMapper because temp-tables may share the same
**                  DmoMeta instance.
**     ECF 20210511 Replaced DynamicTablesHelper.normalizeName with TextOps.rightTrimLower.
**     CA  20210609 Reworked INPUT/INPUT-OUTPUT parameters to a new approach, where they are explicitly
**                  initialized at the method's execution, and not at the caller's arguments.
**     SVL 20210614 Remote OUTPUT TABLE/DATASET-HANDLE parameters are treated like INPUT-OUTPUT parameters
**                  with no input data rows, which allows to access table structure if non-unknown
**                  table/dataset handle has been passed from the calling side.
**     OM  20210809 Completed implementation of SERIALIZE-ROW|WRITE-JSON}(JsonObject|JsonArray).
**     CA  20210831 For GET-CHANGES, only the tables which have a before-table are included - the changes 
**                  are available even if the TRACKING-CHANGES was disabled before this call.
**     CA  20210928 Implicit deletion of buffers (when the dataset gets deleted) is done only for dynamic
**                  buffers associated with dynamic temp-tables.
**     CA  20211005 DATASET can't be added to unnamed pools.
**     OM  20211006 Unbinding/output copy occur on procedure return instead of TM's scope finished.
**     CA  20211112 Allow the DATASET and TABLE parameters for remote SOAP calls to be transferred via XML 
**                  (so that relations, schema, etc is done on the server-side).
**     CA  20211227 Fixed the DATASET:NAME when neither name or serialize-name are set - defaults to ProDataSet.
**     CA  20220105 readFromXml must force UTF-8 as codepage at the longchar created to hold the XML string.
**     CA  20220106 Fixed CREATE-LIKE when relations are involved, their active flag was not being copied 
**                  properly.
**     OM  20220422 Allow Xml/JsonImport to load records into buffer without exposing package private methods.
**     OM  20221003 Added missing STREAM targets for JSON/XML serializations.
**     CA  20220930 Refactored the callback invocation to be performed via a call-site and InvokeConfig, to
**                  allow caching of the resolved target.
**     CA  20220901 Refactored scope notification support: ScopeableFactory was removed, and the registration 
**                  is now specific to each type of scopeable.  For each case, the block will be registered 
**                  for scope support (for that particular scopeable) only when the scopeable is 'active' 
**                  (i.e. unnamed streams or accumulators are used).  This allows a lazy registration of 
**                  scopeables, to avoid the unnecessary overhead of processing all the scopeables for each
**                  and every block.
**     CA  20220923 Internal var definitions must be done via TypeFactory and not UndoableFactory.
**     CA  20221006 UNIQUE-ID is kept as Java type instead of BDT.  Refs #6827
**     CA  20221031 Added JMX instrumentation for DATASET[-HANDLE] parameter processing.
**     OM  20221206 Reworked REFERENCE-ONLY attribute.
**     CA  20220524 The scope ends when the procedure returns, not on 'scopeFinished', to allow parameter
**                  processing.
**     GES 20210501 Upgraded dataset parameter processing to match API changes. Reworked associate() and
**                  createDynamicDataSet() to remove unnecessary arguments.
**     CA  20220601 Added 'derefPoly', emitted when '::' is used as r-value in a dynamic extent assignment.
**     OM  20220604 Improved dereferenciation support.
**     CA  20220606 Fixed COPY-DATASET, automatically create a before-table in the destination if the 
**                  destination temp-table is dynamic.  Raise error 12753 only if the source before-table is
**                  not empty and the destination table is static without a before-table.
**     CA  20220608 Fixed CREATE-LIKE, copy the temp-table options (like XML-NODE-NAME), too.
**     CA  20220618 Automatically delete datasets created for remote parameters.
**     CA  20220704 A bound REFERENCE-ONLY dataset must act as a normal dataset if used as an OUTPUT parameter.
**                  No instance field can be directly accessed from static methods, as the dataset instances
**                  are proxied (added 'addOutputParam' method to fix 'outputParams' field access).
**     OM  20220609 Fixed implementation of several 4GL attributes and methods.
**     CA  20220727 Fixed OO dataset/table parameters (at the method definition and method call) when there is
**                  an APPEND option.
**     CA  20220918 Automatically delete top-nav queries created dynamically by the DATASET.
**     CA  20221031 Added JMX instrumentation for DATASET[-HANDLE] parameter processing.
**     IAS 20221108 Fixed support for SCHEMA-MARSHAL == NONE.
**     IAS 20221123 Added 'resetNames()' method.
**     CA  20221123 Static DATASET instances must not be added to SESSION:FIRST-DATASET chain.
**     OM  20221129 Fine-tuning _areRelationsActive() based on recent findings.
**     SVL 20230108 Improved performance by replacing some "for-each" loops with indexed "for" loops.
**     CA  20230110 Cache the mutable info at the temp-table and not in a separate map at TableMapper.  The
**                  mutable info for a field is calculated only if this info has changed, when creating a 
**                  'like' dataset.
**     CA  20230110 Do not create 'callbacks', 'topNavQueries', 'deleteQueries' or 'refs' until they are 
**                  actually added something.
**     CA  20230110 UNIQUE-ID is generated only when is read by the application.
**                  Cache the helper for ProcedureManager, ObjectOps and others (as needed), to reduce  
**                  context-local lookup.
**     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  20230118 Eliminated some of the uses of String.toUpperCase and/or String.toLowerCase
**                  for performance.
**     CA  20230208 Backed out a change in 'OM/20221206 Reworked REFERENCE-ONLY attribute.', as it was causing
**                  regressions - the 'thisIsParam' flag was changed to 'false' in 'associateImpl'.
** 008 CA  20230303 When deleting a temp-table, the delete can be vetoed if any buffer for the temp-table is
**                  associated with an OUTPUT dataset.  Refactored to move the resource-specific code in a 
**                  'allowDelete' method.
** 009 IAS 20230405 Fixed FILL support for recursive DATA-RELATION
**                  Fixed support for recursive DATA-SET.
**                  Added 'getRelations()' method.
** 010 OM  20230315 The Dataset keeps the original proxy buffers and use these when copying data to output
**                  parameters.
** 011 IAS 20230407 Fixed 'getChanges'.
** 012 VVT 20230405 Method _getNumTopBuffers made public and minor non-functional changes.
** 013 IAS 20230414 Fixed 'addParentIdRelation' method.
**     IAS 20230501 Added 'emergency exit' for WRITE-XML
** 014 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 015 IAS 20230519 Added method for getting NS URI as String.
** 016 AL2 20230613 Use procedure helper where possible to avoid context look-ups.
** 017 CA  20230724 Further reduce context-local usage.
** 018 IT  20230809 temp-table member of a dataset cannot be delete  without deleting the dataset first.
** 019 CA  20230917 Backed out H018, the solution is to use 'allowDelete' called for an explicit DELETE OBJECT
**                  statement.
** 020 OM  20231027 Added getBufferBySerializeName() method.
** 021 HC  20240222 Enabled JMX on FWD Client.
** 022 CA  20231026 Release the buffers after 'readJson' and 'readXml' complete.
** 023 CA  20240320 Avoid BDTs within internal FWD runtime.  Replaced iterators with Java 'for', where it 
**                  applies.
** 024 CA  20240322 SET-BUFFERS(handle...) must use the DEFAULT-BUFFER-HANDLE for any TEMP-TABLE in the list.
** 025 CA  20240328 Save and expose the proxy for a static dataset.
** 026 CA  20240324 Use logical constants for TRUE, FALSE, UNKNOWN, within internal FWD runtime.
** 027 CA  20240404 Static temp-table or dataset resolution by name must be done using the currently executing
**                  type and the static temp-table/dataset's defining type.
** 028 CA  20240422 Allow the query to be reset to its initialization state, so the same instance can be used 
**                  to execute child buffer FILL operations.
** 029 CA  20240502 When a BEFORE-TABLE is being built too, for a dynamic temp-table, the associated DMOs for
**                  both the before and after table must have the after/beforeTable annotations set.
** 030 CA  20240512 All buffers (including peer buffers) from all temp-tables for this dataset must be flushed
**                  when using WRITE-XML or WRITE-JSON.
** 031 EAB 20240423 Rewrote getDataAsJson method in order to imitate the result returned by writeJson().
** 032 CA  20240523 Remote requests over appserver allow the parameter dataset to be REFERENCE-ONLY.
** 033 AS  20240729 Added support for bind parameter option in createDynamicDataSetImpl.
** 034 CA  20240809 Skip using JMX timers when JMX_DEBUG flag is not set.
** 035 AS  20240808 registerOutputDataSetHandleCopier only for Proxy in createDynamicDataSetImpl.
**                  Throw 13009 error only if the source dataset is not a handle and is not a reference.
** 036 CA  20240918 Removed default methods from interfaces and moved them as concrete implementations, as
**                  default methods cause the Java metaspace to spike one order of magnitude.
** 037 AS  20240924 Throw 4GL accurate errors in isCompatibleWith().
** 038 DDF 20241009 When provided a prefix (even the default one), the table name must not exceed 32
**                  characters.
** 039 AP  20241017 Always return the bound buffer, if it exists.
** 040 ES  20241017 Added verification for possible misplacement of tables names in pairs-list parameter
**                  in copyDatasetImpl.
** 041 CA  20241030 Improved 'isCompatibleWith' - do not check if the same DMO is used.
** 042 AS  20241104 Fixed _getNumTopBuffers for recursive relations.
** 043 RNC 20241030 Added support for XML serialization coming from a SOAP request.
** 044 AP  20241210 Improved dataset binding in order to take into account only the BIND on the caller side.
** 045 AP  20250129 Refactored getDataAsJson to use the RecordBufferSerializer and do the actual JSON
**                  serialization using jackson library.
** 046 ICP 20250123 Used integer.of to leverage caches instances.
** 047 CA  20250321 Added 'setCallback' overloads.
** 048 AS  20250404 Take into account the reposition attribute of the relations for a fill operation.
** 049 AS  20250416 For create-like, use buffer's parent table name instead of the buffer name.
** 050 AS  20250529 Improved isCompatibleWith, use dmo signatures to check for dataset compatibility.
**                  Improved copyTempTableOptions(), use existingFieldsMap() 
**                  from the dmo info for acquiring source buffer fields.
*/

/*
** 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;

import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.databind.*;
import com.goldencode.p2j.jmx.*;
import com.goldencode.p2j.persist.TemporaryBuffer.CopyTableMode;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.persist.serial.*;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
import com.goldencode.util.*;

import java.io.*;
import java.sql.*;
import java.util.*;
import java.util.function.*;
import java.util.stream.*;

/**
 * {@code DataSet} objects are collections of one or more related temp-tables. Each temp-table of
 * these can attach to a {@code DataSource} object that allows filling of the temp-table from the
 * data source, or updating the data source from the temp-table. A {@code DataSet} object can
 * optionally contain a set of data relations between the temp-tables.
 */
@LegacyResource(resource = LegacyResource.DATASET)
public class DataSet
extends HandleChain
implements ADMData,
           BufferCollection,
           CallbackProcedure,
           Clearable,
           CreateLike,
           DataSetChangeable,
           DataSetInterface,
           DataSourceModifiable,
           Dereferenceable,
           DynamicResource,
           Errorable,
           Fillable,
           JsonData,
           NamedSerializable,
           NamespaceURI,
           Rejectable,
           RefCounter,
           SerializeHiddenable,
           TempTableDuplicator,
           UniqueID,
           XmlData,
           XmlNode
{
   /** The {@code BEFORE-FILL} event string constant in upper case. */
   public static final String BEFORE_FILL_EVENT = "BEFORE-FILL";
   
   /** The {@code AFTER-FILL} event string constant in upper case. */
   public static final String AFTER_FILL_EVENT = "AFTER-FILL";
   
   /** The call-site for the {@link #BEFORE_FILL_EVENT} event callback. */
   private static final InvokeConfig BEFORE_FILL_CALL_SITE = new InvokeConfig();
   
   /** The call-site for the {@link #AFTER_FILL_EVENT} event callback. */
   private static final InvokeConfig AFTER_FILL_CALL_SITE = new InvokeConfig();
   
   /** Corresponds to UNIQUE-ID attribute. */
   private Long uuid = null;
   
   /** Instrumentation for {@link #associate}. */
   private static final NanoTimer STATIC_PARAM = NanoTimer.getInstance(FwdServerJMX.TimeStat.OrmDataSetParam);
   
   /** Instrumentation for {@link #createDynamicDataSet}. */
   private static final NanoTimer DYN_PARAM = NanoTimer.getInstance(FwdServerJMX.TimeStat.OrmDynamicDataSetParam);

   /** Logger for objects of this type. */
   private static final CentralLogger log = CentralLogger.get(DataSet.class.getName());
   
   /** The value of {@code ADM-DATA} ABL attribute. */
   private String admData = null;
   
   /** The value of {@code NAMESPACE-URI} attribute of this {@code DataSet}. */
   private String nsUri = null;
   
   /** The namespace prefix of this {@code DataSet}. */
   private String nsPrefix = null;
   
   /** The name of the XML node for serialized data of this {@code DataSet}. */
   private String xmlNodeName = null;
   
   /** The serialization name of this {@code DataSet}. The default is dataset's name. */
   private String serializeName = null;
   
   /** The default value for active attribute for first {@code DataRelation}. */
   private boolean defaultActive = true;
   
   /**
    * The current value of {@code NAME} attribute. It can be modified for dynamic {@code DataSet}s
    * but immutable for static {@code DataSet}s.
    */
   private String name = "";
   
   /**
    * The current value of boolean {@code ERROR} attribute: indicates whether an error occurred
    * during a {@code FILL} or {@code SAVE-ROW-CHANGES} operation, but can also be altered
    * programmatically (writable).
    */
   private boolean error = false;
   
   /**
    * The current value of boolean {@code REJECTED} attribute: indicates whether a change to the
    * data of this {@code DataSet} is rejected. The attribute is writable.
    */
   private boolean rejected = false;
   
   /** A map with all known callbacks. The keys MUST be in UPPERCASE.*/
   private Map<String, CallbackData> callbacks = null;
   
   /**
    * Flags the dynamically constructed objects (using {@code CREATE} as opposed to statically
    * defined objects, with {@code DEFINE}).
    */
   private final boolean dynamic;
   
   /** Flags the {@code REFERENCE-ONLY} {@code DataSet}s. */
   protected boolean referenceOnly = false;
   
   /** 
    * Flags the short period of time when the {@code Builder} constructs the object. During this
    * period, some setters are allowed for STATICally build DATASETs.
    */
   private boolean buildInProgress = false;
   
   /** Flag indicating the delete was explicitly called for a BY-REFERENCE handle. */
   private boolean postponedDelete = false;
   
   /**
    * Flags a hidden (name is not written) {@code DataSet} when serialized. This flag overlaps
    * with {@code XML-NODE-TYPE} attribute. A value of {@code false} is equivalent to
    * {@code XML-NODE-TYPE} set to "ELEMENT" (the default) and {@code false} means that the 
    * {@code XML-NODE-TYPE} is "HIDDEN".
    */
   private boolean serializeHidden = false;
   
   /** Marks the valid objects. When destroyed, the flag is set to {@code false}. */
   private boolean deleted = false;
   
   /** Current value of the {@code DATA-SOURCE-MODIFIED} attribute. */
   private boolean dataSourceModified = false;
   
   /** The ordered list of buffers contained in this {@code DataSet}. */
   private final ArrayList<BufferImpl> buffers = new ArrayList<>();
   
   /** The original proxy object associated with the  buffers contained in this {@code DataSet}. */
   private final Map<BufferImpl, BufferImpl> proxies = new IdentityHashMap<>();
   
   /** The current set of {@code TOP-NAV-QUERY} values mapped by their buffers. */
   private Map<BufferImpl, QueryWrapper> topNavQueries = null;
   
   /** The set of queries created by this dataset which need to be cleaned. */
   private Set<QueryWrapper> deleteQueries = null;
   
   /** The list of {@code DataRelation} s from this data set. */
   private final ArrayList<DataRelation> relations = new ArrayList<>();
   
   /** Destination {@code DataSet} parameters for output copy if necessary before scope closes. */
   private Set<DataSetParameter> outputParams = null;
   
   /** The list of references of this object. */
   private Set<StaticDataSet> refs = null;

   /** Back-reference to the defining static dataset proxy. */
   private StaticDataSet proxyDataSet = null;

   /** Flag indicating that the fill query for buffers can be re-used. */
   private boolean reuseFillQuery;

   /** Flag indicating that this instance will be serialized due to a SOAP request. */
   private boolean serializedBySOAP;
   
   /**
    * Factory method. Corresponds to {@code CREATE DATASET} statement. Creates a new
    * {@code DataSet} and stores the object in the supplied handle.
    * 
    * @param   dsHandle
    *          The destination handle.
    */
   public static void create(handle dsHandle)
   {
      DataSet.create(dsHandle, (character) null);
   }
   
   /**
    * Factory method. Corresponds to {@code CREATE DATASET} statement. Creates a new
    * {@code DataSet} and stores the object in the supplied handle.
    * 
    * @param   dsHandle
    *          The destination handle.
    * @param   widgetPool
    *          The widget pool to created the new {@code DataSet} into.
    */
   public static void create(handle dsHandle, String widgetPool)
   {
      DataSet.create(dsHandle, new character(widgetPool));
   }
   
   /**
    * Factory method. Corresponds to {@code CREATE DATASET} statement. Creates a new
    * {@code DataSet} and stores the object in the supplied handle.
    * 
    * @param   dsHandle
    *          The destination handle.
    * @param   widgetPool
    *          The widget pool to created the new {@code DataSet} into.
    */
   public static void create(handle dsHandle, character widgetPool)
   {
      // validate widget pool
      if (!WidgetPool.validWidgetPool(widgetPool))
      {
         return;
      }
      
      DataSet ds = new DataSet("", true, false);  // by default the name is empty string, not unknown
      dsHandle.assign(ds);
      if (widgetPool != null)
      {
         // unnamed widget pools are not allowed.
         ds.addToPool(widgetPool);
      }
   }
   
   /**
    * Associate two {@code DataSet} with one another, such that all relevant records can be copied
    * between them at prescribed times.
    *
    * @param   dsLocal
    *          The local object relative to the current procedure. If this is an INPUT parameter
    *          then this is the 'source'. This encapsulates the actual parameter sent by caller.
    * @param   dsTarget
    *          A dataset defined in current external procedure. 
    * @param   input
    *          If {@code true}, records are copied from source table to destination table
    *          immediately upon invocation of this method, otherwise they are not.
    * @param   output
    *          When {@code true}, the records are copied from destination table back to source
    *          table when the current transaction or subtransaction is committed;  otherwise they
    *          are not.
    *
    * @see TemporaryBuffer#associate(TableParameter, Temporary, boolean, boolean)
    */
   public static void associate(DataSetParameter dsLocal, DataSet dsTarget, boolean input, boolean output)
   {
      associate(dsLocal, dsTarget, input, output, ParameterOption.NONE);
   }
   
   /**
    * Associate two {@code DataSet} with one another, such that all relevant records can be copied
    * between them at prescribed times.
    * 
    * @param   dsSrc
    *          The local object relative to the current procedure. If this is an INPUT parameter
    *          then this is the 'source'. This encapsulates the actual parameter sent by caller.
    * @param   dsTarget
    *          A dataset defined in current external procedure. 
    * @param   input
    *          If {@code true}, records are copied from source table to destination table
    *          immediately upon invocation of this method, otherwise they are not.
    * @param   output
    *          When {@code true}, the records are copied from destination table back to source
    *          table when the current transaction or subtransaction is committed;  otherwise they
    *          are not.
    * @param   option
    *          Dataset parameter option (APPEND, BY-VALUE, BIND or BY-REFERENCE).
    * 
    * @see TemporaryBuffer#associate(TableParameter, Temporary, boolean, boolean)
    */
   public static void associate(DataSetParameter dsSrc,
                                DataSet          dsTarget,
                                boolean          input,
                                boolean          output,
                                ParameterOption  option)
   {
      associate(dsSrc, dsTarget, input, output, EnumSet.of(option));
   }
   
   /**
    * Associate two {@code DataSet} with one another, such that all relevant records can be copied
    * between them at prescribed times.
    * 
    * @param   dsSrc
    *          The local object relative to the current procedure. If this is an INPUT parameter
    *          then this is the 'source'. This encapsulates the actual parameter sent by caller.
    * @param   dsTarget
    *          A dataset defined in current external procedure. 
    * @param   input
    *          If {@code true}, records are copied from source table to destination table
    *          immediately upon invocation of this method, otherwise they are not.
    * @param   output
    *          When {@code true}, the records are copied from destination table back to source
    *          table when the current transaction or subtransaction is committed;  otherwise they
    *          are not.
    * @param   options
    *          Dataset parameter options (APPEND, BY-VALUE, BIND or BY-REFERENCE).
    * 
    * @see TemporaryBuffer#associate(TableParameter, Temporary, boolean, boolean)
    */
   public static void associate(DataSetParameter dsSrc,
                                DataSet          dsTarget,
                                boolean          input,
                                boolean          output,
                                EnumSet<ParameterOption>  options)
   {
      dsSrc.setInputMode(input);
      dsSrc.setOutputMode(output);
      
      associate(dsSrc, dsTarget, options);
   }

   /**
    * Associate two {@code DataSet} with one another, such that all relevant records can be copied
    * between them at prescribed times.
    *
    * @param   dsLocal
    *          The local object relative to the current procedure. If this is an INPUT parameter
    *          then this is the 'source'. This encapsulates the actual parameter sent by caller.
    * @param   dsTarget
    *          A dataset defined in current external procedure. 
    *
    * @see TemporaryBuffer#associate(TableParameter, Temporary)
    */
   public static void associate(DataSetParameter dsLocal, DataSet dsTarget)
   {
      associate(dsLocal, dsTarget, ParameterOption.NONE);
   }
   
   /**
    * Associate two {@code DataSet} with one another, such that all relevant records can be copied
    * between them at prescribed times.
    * 
    * @param   dsSrc
    *          The local object relative to the current procedure. If this is an INPUT parameter
    *          then this is the 'source'. This encapsulates the actual parameter sent by caller.
    * @param   dsTarget
    *          A dataset defined in current external procedure. 
    * @param   option
    *          Dataset parameter option (APPEND, BY-VALUE, BIND or BY-REFERENCE).
    * 
    * @see TemporaryBuffer#associate(TableParameter, Temporary)
    */
   public static void associate(final DataSetParameter dsSrc,
                                final DataSet          dsTarget,
                                ParameterOption        option)
   {
      associate(dsSrc, dsTarget, EnumSet.of(option));
   }
   
   /**
    * Associate two {@code DataSet} with one another, such that all relevant records can be copied
    * between them at prescribed times.
    * 
    * @param   dsSrc
    *          The local object relative to the current procedure. If this is an INPUT parameter
    *          then this is the 'source'. This encapsulates the actual parameter sent by caller.
    * @param   dsTarget
    *          A dataset defined in current external procedure. 
    * @param   options
    *          Dataset parameter options (APPEND, BY-VALUE, BIND or BY-REFERENCE).
    * 
    * @see TemporaryBuffer#associate(TableParameter, Temporary)
    */
   public static void associate(DataSetParameter dsSrc, DataSet dsTarget, EnumSet<ParameterOption> options)
   {
      if (FwdServerJMX.JMX_DEBUG)
      {
         STATIC_PARAM.timer(() -> associateImpl(dsSrc, dsTarget, options));
      }
      else
      {
         associateImpl(dsSrc, dsTarget, options);
      }
   }
   
   /**
    * Associate two {@code DataSet} with one another, such that all relevant records can be copied
    * between them at prescribed times.
    * 
    * @param   dsSrc
    *          The local object relative to the current procedure. If this is an INPUT parameter
    *          then this is the 'source'. This encapsulates the actual parameter sent by caller.
    * @param   dsTarget
    *          A dataset defined in current external procedure. 
    * @param   options
    *          Dataset parameter options (APPEND, BY-VALUE, BIND or BY-REFERENCE).
    * 
    * @see TemporaryBuffer#associate(TableParameter, Temporary)
    */
   private static void associateImpl(final DataSetParameter   dsSrc,
                                     final DataSet            dsTarget,
                                     EnumSet<ParameterOption> options)
   {
      boolean input  = dsSrc.isInputMode();
      boolean output = dsSrc.isOutputMode();
      boolean append = options.contains(ParameterOption.APPEND);
      
      DataSetManager.registerScopeable();
      
      StaticDataSet localDS = (StaticDataSet) dsTarget;
      if (!dsSrc.isRemoteParameter())
      {
         DataSet sourceDS = dsSrc.getOriginalDataSet();
         if (dsSrc.isBind() && options.contains(ParameterOption.BIND) && 
             sourceDS != null && !localDS.referenceOnly)
         {
            if (sourceDS.referenceOnly)
            {
               // bind from the target to the source; this is always permanent
               // CA: 'thisIsParam' was changed to 'false' because it was found to cause regressions.
               ((StaticDataSet) sourceDS).bind(localDS, input, output, true, false);
            }
            else if (dsSrc.isOriginalDataSetProxy())
            {
               String pname = localDS.instantiatingProcedure().unwrap().name().toStringMessage();
               ErrorManager.recordOrThrowError(13009, pname, localDS.name().toJavaType());
               // <program name> BIND modifier not allowed for cases where neither the caller nor the called 
               // TABLE or DATASET parameter <parameter-name> has been defined REFERENCE-ONLY and neither 
               // caller nor called parameter is a TABLE-HANDLE or DATASET-HANDLE. (13009)
            }
            
            return;
         }
         if (localDS.referenceOnly)
         {
            if (dsSrc.isByReference() || dsSrc.isBind() || options.contains(ParameterOption.BIND))
            {
               // temporary bind dsSrc.getDataset() to dsTarget
               localDS.bind(dsSrc.getDataset(), input, output, 
                            dsSrc.isBind() || options.contains(ParameterOption.BIND), false);
               {
                  // TODO: check for both usages of [bind] what happens if the check fails for a
                  //       subsequent parameter in case there are many DataSet: roll back the
                  //       bindings for the former parameter? Keep them? 
               }
               return;
            }
            
            if (!localDS.isBound())
            {
               String pname = localDS.instantiatingProcedure().unwrap().name().toStringMessage();
               String dsName = dsSrc.getDataset().name().toJavaType();
               ErrorManager.recordOrThrowError(13012, pname, dsName);
               // <program-name> BIND or BY-REFERENCE modifier required on RUN, FUNCTION or METHOD invocation
               // parameter when called TABLE or DATASET parameter <parameter-name> is REFERENCE-ONLY and is
               // still not bound. (13012)
            }
            
            // else, we continue with the normal processing, as the local REFERENCE-ONLY dataset is bound 
         }
         
         if (dsSrc.isByReference())
         {
            handle h = dsSrc.getDatasetHandle();
            
            if (h.isUnknown())
            {
               if (input)
               {
                  // TODO: error, how to map the target DATASET?
               }
               else
               {
                  DataSet targetRefDS = dsTarget.ref();
                  targetRefDS.addOutputParam(dsSrc);
                  
                  targetRefDS.pm.executeOnReturn(() -> targetRefDS.performOutputCopy(dsSrc), 
                                                 WeightFactor.FIRST);
                  return;
               }
            }
            else
            {
               // temporarily bind dsSrc.getDataset() to dsTarget
               localDS.bind(dsSrc.getDataset(), input, output, false, false);
               return;
            }
         }
         // NONE to BIND will do nothing, unless the REFERENCE-ONLY is used at target
         // BIND to NONE will error out
         else if (dsSrc.isBind() && options.contains(ParameterOption.NONE))
         {
            String procName = localDS.instantiatingProcedure().unwrap().name().toStringMessage();
            String dsName = dsSrc.getDataset().name().toJavaType();
            ErrorManager.recordOrThrowError(13009, procName, dsName);
            // <program name> BIND modifier not allowed for cases where neither the caller nor the
            // called TABLE or DATASET parameter <parameter-name> has been defined REFERENCE-ONLY and
            // neither caller nor called parameter is a TABLE-HANDLE or DATASET-HANDLE. (13009)
            return;
         }
      }
      
      if (dsSrc.getDataset() == dsTarget.ref())
      {
         // same reference, nothing to do; BY-REF/REF-ONLY still needs to do processing related
         // to keeping all mutable buffers in sync
         return;
      }
      
      if (input)
      {
         if (!dsSrc.isRemoteParameter())
         {
            DataSet dsSource = dsSrc.getDataset();
            if (dsSource != null)
            {
               int bufCnt = dsSource.numBuffers().intValue();
               for (int k = 1; k <= bufCnt; k++)
               {
                  // copy data for each corresponding temp-table
                  Buffer srcBuf = dsSource.getBufferByIndex(k);
                  Temporary srcDMO = TemporaryBuffer.getDefaultBuffer(srcBuf.tableHandle());
                  BufferImpl targetDmo = dsTarget.getBufferByIndex(k);
                  rowid srcRec = dsTarget.referenceOnly && srcBuf._available() ? srcBuf.rowID() : null;
                  boolean res = TemporaryBuffer.copyAllRows(
                                    srcRec,
                                    srcDMO,
                                    (Temporary) targetDmo,
                                    append,
                                    TemporaryBuffer.CopyTableMode.INPUT_PARAM_MODE);
                  if (!res)
                  {
                     BlockManager.returnError();
                  }
               }
            }
         }
         else if (dsSrc.getDatasetWrapper().getXmlDataset() != null)
         {
            String xmlDataset = dsSrc.getDatasetWrapper().getXmlDataset();
            readFromXml(xmlDataset, append, new handle(dsTarget));
         }
         else if (localDS.referenceOnly)
         {
            // even if we are remote, REFERENCE-ONLY is allowed - in this case, always bind target to source
            // argument
            localDS.bind(dsSrc.getDataset(), input, output, false, false);
         }
         else
         {
            int bufCnt = dsSrc.getDatasetWrapper().getTableDefs().size();
            Map<String, TableWrapper> legacyTableToWrapper = null;
            boolean fromJson = dsSrc.getDatasetWrapper().isFromJson();
            if (fromJson)
            {
               legacyTableToWrapper = new HashMap<>();
               for (int k = 1; k <= bufCnt; k++)
               {
                  TableWrapper wrapper = dsSrc.getDatasetWrapper().getTableWrapper(k - 1);
                  legacyTableToWrapper.put(wrapper.getTableName().toLowerCase(), wrapper);
               }
            }
            for (int k = 1; k <= bufCnt; k++)
            {
               // copy data for each corresponding temp-table
               BufferImpl targetDmo = dsTarget.getBufferByIndex(k);
               TableWrapper wrapper = null;
               if (fromJson)
               {
                  AbstractTempTable tt = ((AbstractTempTable) targetDmo.tableHandleNative());
                  wrapper = legacyTableToWrapper.get(tt._name().toLowerCase());
               }
               else
               {
                  wrapper = dsSrc.getDatasetWrapper().getTableWrapper(k - 1);
               }
               
               if (dsSrc.isRemoteParameter()  && !wrapper.isReceivedSchema())
               {
                  RecordBuffer rb = ((BufferReference) targetDmo).buffer();
                  DmoMeta meta = rb.getDmoInfo();
                  Iterator<Property> props = meta.getFields(false);
                  List<PropertyDefinition> pdl = new ArrayList<>();
                  props.forEachRemaining(p -> pdl.add(new PropertyDefinition(p)));
                  wrapper.setProperties(pdl);
               }
               TemporaryBuffer.insertAllRows(
                     wrapper,
                     (Temporary) targetDmo,
                     append,
                     TemporaryBuffer.CopyTableMode.INPUT_PARAM_MODE,
                     dsSrc.getParameterIndex());
            }
         }
      }
      
      if (output)
      {
         DataSet targetRefDS = dsTarget.ref();
         targetRefDS.addOutputParam(dsSrc);
         
         targetRefDS.pm.executeOnReturn(() -> targetRefDS.performOutputCopy(dsSrc), WeightFactor.FIRST);
      }
   }
   
   /**
    * Dynamically create a {@code DataSet} from the {@code DataSetParameter} provided. The new
    * {@code DataSet} will be stored in {@code dsHandle}. This method is used at the beginning of
    * a method/function in order to properly convert the {@code PARAMETER DATASET-HANDLE}
    * constructs.
    *
    * @param    dsParam
    *           The {@code DataSetParameter}. This is the actual parameter for the routine.
    * @param    dsHandle
    *           A handle to store the reference to the new {@code DataSet} during the scope of this
    *           routine.
    */
   public static void createDynamicDataSet(DataSetParameter dsParam, handle dsHandle)
   {
      createDynamicDataSet(dsParam, dsHandle, ParameterOption.NONE);
   }
   
   /**
    * Dynamically create a {@code DataSet} from the {@code DataSetParameter} provided. The new
    * {@code DataSet} will be stored in {@code dsHandle}. This method is used at the beginning of
    * a method/function in order to properly convert the {@code PARAMETER DATASET-HANDLE}
    * constructs.
    *
    * @param    dsParam
    *           The {@code DataSetParameter}. This is the actual parameter for the routine.
    * @param    dsHandle
    *           A handle to store the reference to the new {@code DataSet} during the scope of this
    *           routine.
    * @param    input
    *           If {@code true} then this is an {@code INPUT} or {@code INPUT-OUTPUT} parameter.
    * @param    output
    *           If {@code true} then this is an {@code OUTPUT} or {@code INPUT-OUTPUT} parameter.
    */
   public static void createDynamicDataSet(DataSetParameter dsParam,
                                           handle           dsHandle,
                                           boolean          input,
                                           boolean          output)
   {
      createDynamicDataSet(dsParam, dsHandle, input, output, ParameterOption.NONE);
   }
   
   /**
    * Dynamically create a {@code DataSet} from the {@code DataSetParameter} provided. The new
    * {@code DataSet} will be stored in {@code dsHandle}. This method is used at the beginning of
    * a method/function in order to properly convert the {@code PARAMETER DATASET-HANDLE}
    * constructs.
    *
    * @param   dsParam
    *          The {@code DataSetParameter}. This is the actual parameter for the routine (src).
    * @param   dsHandle
    *          A handle to store the reference to the new {@code DataSet} during the scope of this
    *          routine (target).
    * @param   input
    *          If {@code true} then this is an {@code INPUT} or {@code INPUT-OUTPUT} parameter.
    * @param   output
    *          If {@code true} then this is an {@code OUTPUT} or {@code INPUT-OUTPUT} parameter.
    * @param   option
    *          Dataset parameter option (APPEND, BY-VALUE, BIND or BY-REFERENCE).
    */
   public static void createDynamicDataSet(DataSetParameter dsParam,
                                           handle           dsHandle,
                                           boolean          input,
                                           boolean          output,
                                           ParameterOption  option)
   {
      dsParam.setInputMode(input);
      dsParam.setOutputMode(output);
      
      createDynamicDataSet(dsParam, dsHandle, option);
   }

   /**
    * Dynamically create a {@code DataSet} from the {@code DataSetParameter} provided. The new
    * {@code DataSet} will be stored in {@code dsHandle}. This method is used at the beginning of
    * a method/function in order to properly convert the {@code PARAMETER DATASET-HANDLE}
    * constructs.
    *
    * @param   dsParam
    *          The {@code DataSetParameter}. This is the actual parameter for the routine (src).
    * @param   dsHandle
    *          A handle to store the reference to the new {@code DataSet} during the scope of this
    *          routine (target).
    * @param   option
    *          Dataset parameter option (APPEND, BY-VALUE, BIND or BY-REFERENCE).
    */
   public static void createDynamicDataSet(DataSetParameter dsParam,
                                           handle           dsHandle,
                                           ParameterOption  option)
   {
      if (FwdServerJMX.JMX_DEBUG)
      {
         DYN_PARAM.timer(() -> createDynamicDataSetImpl(dsParam, dsHandle, option));
      }
      else
      {
         createDynamicDataSetImpl(dsParam, dsHandle, option);
      }
   }
   
   /**
    * Dynamically create a {@code DataSet} from the {@code DataSetParameter} provided. The new
    * {@code DataSet} will be stored in {@code dsHandle}. This method is used at the beginning of
    * a method/function in order to properly convert the {@code PARAMETER DATASET-HANDLE}
    * constructs.
    *
    * @param   dsParam
    *          The {@code DataSetParameter}. This is the actual parameter for the routine (src).
    * @param   dsHandle
    *          A handle to store the reference to the new {@code DataSet} during the scope of this
    *          routine (target).
    * @param   option
    *          Dataset parameter option (APPEND, BY-VALUE, BIND or BY-REFERENCE).
    */
   private static void createDynamicDataSetImpl(DataSetParameter dsParam,
                                                handle           dsHandle,
                                                ParameterOption  option)
   {
      DataSetManager manager = DataSetManager.instance();
      manager._registerScopeable();

      boolean input  = dsParam.isInputMode();
      boolean output = dsParam.isOutputMode();
      boolean append = (option == ParameterOption.APPEND) || dsParam.isAppend();
      boolean bind = dsParam.isBind();

      if (output)
      {
         manager.registerCurrent(dsHandle);
         if (bind && dsParam.getOriginalDataSet() == null)
         {
            ProcedureManager.ProcedureHelper pm = ProcedureManager.getProcedureHelper();
            pm.executeOnReturn(() -> dsParam.getDatasetHandle().assign(dsHandle), WeightFactor.LEVEL_10);
            return;
         }
      }
      
      //if bind, assign source dataset to the new ds handle, 
      //this will ensure that the new dataset has the same data source
      if (bind)
      {
         if (!dsParam.getDataset().referenceOnly)
         {
            dsHandle.assign(dsParam.getDataset());
         }
         
         if (output && dsParam.isOriginalDataSetProxy())
         {
            manager.registerOutputDataSetHandleCopier(dsParam, dsHandle);
         }
         return;
      }
      
      if (dsParam.isByReference())
      {
         dsHandle.assign(dsParam.getDataset());
         
         if (dsHandle._isValid())
         {
            DataSet ds = (DataSet) dsHandle.getResource();
            manager.addByReferenceParameter(ds);
            
            ds.pm.executeOnReturn(() -> manager.removeByReferenceParameter(ds),
                                  WeightFactor.LEVEL_10);
         }
         else if (output)
         {
            manager.registerOutputDataSetHandleCopier(dsParam, dsHandle);
         }
         
         return;
      }
      
      if (input || dsParam.isRemoteParameter())
      {
         if (dsParam.isRemoteParameter() && dsParam.getDatasetWrapper().getXmlDataset() != null)
         {
            String xmlDataset = dsParam.getDatasetWrapper().getXmlDataset();
            readFromXml(xmlDataset, append, dsHandle);
            return;
         }
         
         DataSet newDS = null;
         
         handle srcHandle = dsParam.getDatasetHandle();
         if (srcHandle._isValid())
         {
            if (dsParam.isByReference() || dsParam.isRemoteParameter())
            {
               // check if all tables contain schema.
               if (!dsParam.getDatasetWrapper().getTableDefs().stream().
                        allMatch(DsTableDefinition::isReceivedSchema))
               {
                  // A NO-SCHEMA-MARSHAL table cannot be used as a parameter where the receiving side
                  // does not have a pre-prepared schema
                  ErrorManager.recordOrThrowError(12323);
               }
               // if [dsParam] is remote, the dataset was rehydrated when [dsParam] was created
               // do not copy now for 2nd time. Since it is an [input] param we can use the 
               // dataset handle directly.
               dsHandle.assign(dsParam.get());
            }
            else
            {
               handle newDSh = TypeFactory.handle();
               DataSet.create(newDSh);
               newDS = (DataSet) newDSh.getResource();
               logical res = newDS.copyDatasetImpl(srcHandle, false, false, false, null, false, "");
               if (!res.booleanValue())
               {
                  BlockManager.returnError();
               }
               dsHandle.assign(newDSh);
            }
         }
      }
      
      if (output)
      {
         manager.registerOutputDataSetHandleCopier(dsParam, dsHandle);
      }
   }
   
   /**
    * Constructor to allow building a dataset with no linkage (used for the case of the surrogate
    * resource {@link StaticDataSet}, which delegates the calls to either the default dataset
    * or the bound dataset.
    */
   DataSet()
   {
      super(false, false);
      this.dynamic = false;
      name = "";
   }
   
   /**
    * Initialize this resource by determining its type.
    *
    * @param   legacyName
    *          The legacy name of the {@code DataSet}. Used for lookup.
    * @param   dynamic
    *          Flag indicating if this is a static or dynamic resource.
    * @param   referenceOnly
    *          Flag indicating if this is a REFERENCE-ONLY resource.
    *
    * @throws  IllegalStateException
    *          If the resource type could not be determined from a {@link LegacyResource} annotation, after
    *          checking the interface hierarchy, or more than one interface is annotated.
    */
   DataSet(String legacyName, boolean dynamic, boolean referenceOnly)
   {
      super(dynamic, dynamic);
      this.dynamic = dynamic;
      this.referenceOnly = dynamic ? false : referenceOnly; // dynamic DSes cannot be reference-only 
      name = legacyName; // the name not checked at this time, it was analyzed by the conversion
      if (!dynamic)
      {
         buildInProgress = true;
      }
   }
   
   /**
    * Set the back-reference to the defining static proxy dataset.
    * 
    * @param    proxyDataSet
    *           The defining proxy dataset.
    */
   public void setProxyDataSet(StaticDataSet proxyDataSet)
   {
      this.proxyDataSet = proxyDataSet;
   }
   
   /**
    * Get the defining {@link #proxyDataSet};
    * 
    * @return   See above.
    */
   public StaticDataSet getProxyDataSet()
   {
      return proxyDataSet;
   }
   
   /**
    * Convert this dataset to a handle.
    *
    * @return   See above.
    */
   public handle asHandle()
   {
      return new handle(this);
   }
   
   /**
    * Reset name, nsPrefix and nsURI.
    */
   public void resetNames()
   {
      name = "";
      nsPrefix = null;
      nsUri = "";
   }
   /**
    * Sets the name of this handle. The name cannot be unknown.
    *
    * @param   name
    *          The new name to be set.
    */
   @Override
   public void name(character name)
   {
      if (name == null || name.isUnknown())
      {
         unableToAssignUnknown("NAME", "DATASET widget");
         return;
      }
      
      name(name.getValue());
   }
   
   /**
    * Get the {@code name} attribute of handle.
    *
    * @return See above.
    */
   @Override
   public character name()
   {
      return new character(this.name);
   }
   
   /**
    * Get the <code>name</code> attribute of handle as a string.
    *
    * @return See above.
    */
   @Override
   public String _name()
   {
      return this.name;
   }
   
   /**
    * Sets the {@code name} attribute of the object referred by this handle.
    *
    * @param   name
    *          The new name to be set to target object.
    */
   @Override
   public void name(String name)
   {
      if (!isAlterable())
      {
         ErrorManager.recordOrShowError(12411);
         // Cannot set attribute for a static object.
         ErrorManager.recordOrThrowError(3131, "NAME", "",  LegacyResource.DATASET);
         // Unable to set attribute <attribute name> in widget <widget name> of type <widget-type>.
         return;
      }
      
      if (!validateName(name))
      {
         ErrorManager.recordOrThrowError(3131, "NAME", this.name,  LegacyResource.DATASET);
         // Unable to set attribute <attribute name> in widget <widget name> of type <widget-type>.
         return;
      }
      
      this.name = name;
   }

   /**
    * Dereference a named member from this collection. If this collection is a ProDataSet then 
    * the returned value is a {@link handle} to a named buffer, if the collection is a buffer
    * the returned value is the field value with the respective name. In the case that this 
    * collection does not have a named member with the name specified <code>memberName</code>
    * the unknown value is returned.
    *  
    * @param   index
    *          The subscript to be applied to an extent field.
    * @param   memberName
    *          The name of the member to be extracted.
    *
    * @return  The value of the member or unknown (?) if there is no such member.
    */
   @Override
   public BaseDataType dereference(long index, String memberName)
   {
      UnimplementedFeature.todo("subscripted dereference is not implemented");
      return new unknown();
   }

   /**
    * This overloaded method is used when the dereference is the left-side of an assignment.
    * The implementation will extract the member from the collection and assign it the new value.
    *
    * @param   index
    *          The subscript to be applied to an extent field.
    * @param   memberName
    *          The name of the member to be extracted.
    * @param   value
    *          The new value that will be assigned to the extracted field.
    */
   @Override
   public void dereference(long index, String memberName, Object value)
   {
      UnimplementedFeature.todo("subscripted dereference is not implemented");
   }

   /**
    * Dereference a named member from this collection. If this collection is a ProDataSet then 
    * the returned value is a {@link handle} to a named buffer, if the collection is a buffer
    * the returned value is the field value with the respective name. In the case that this 
    * collection does not have a named member with the name specified <code>memberName</code>
    * the unknown value is returned.
    *  
    * @param   index
    *          The subscript to be applied to an extent field.
    * @param   memberName
    *          The name of the member to be extracted.
    *
    * @return  The value of the member or unknown (?) if there is no such member.
    */
   @Override
   public BaseDataType dereference(NumberType index, String memberName)
   {
      UnimplementedFeature.todo("subscripted dereference is not implemented");
      return new unknown();
   }

   /**
    * This overloaded method is used when the dereference is the left-side of an assignment.
    * The implementation will extract the member from the collection and assign it the new value.
    *
    * @param   index
    *          The subscript to be applied to an extent field.
    * @param   memberName
    *          The name of the member to be extracted.
    * @param   value
    *          The new value that will be assigned to the extracted field.
    */
   @Override
   public void dereference(NumberType index, String memberName, Object value)
   {
      UnimplementedFeature.todo("subscripted dereference is not implemented");
   }
   
   /**
    * Emitted for <code>::</code> operator used as r-value in a dynamic extent assignment, as the conversion
    * rules doesn't know if the target field is extent or scalar.
    * 
    * @param    memberName
    *           The name of the member to be extracted.
    *
    * @return   The value of the member (scalar or extent) or unknown (?) if there is no such member.
    */
   @Override
   public Object derefPoly(String memberName)
   {
      // there are no arrays in a dataset, just independent buffers
      return dereference(memberName);
   }
   
   /**
    * Dereference a named member from this collection. If this collection is a ProDataSet then
    * the returned value is a {@link handle} to a named buffer, if the collection is a buffer
    * the returned value is the field value with the respective name. In the case that this
    * collection does not have a named member with the name specified {@code memberName}
    * the unknown value is returned.
    *  
    * @param   memberName
    *          The name of the member to be extracted.
    *
    * @return  The value of the member or unknown (?) if there is no such member.
    */
   @Override
   public BaseDataType dereference(String memberName)
   {
      handle buffHnd = bufferHandle(memberName);
      if (buffHnd.isUnknown())
      {
         ErrorManager.recordOrShowError(12785, memberName, name().toStringMessage());
         // Table <name> not found in dataset <name>.
         return new handle();
      }
      return buffHnd;
   }
   
   /**
    * Dereference a named member from this collection. If this collection is a ProDataSet then 
    * the returned value is a {@link handle} to a named buffer, if the collection is a buffer
    * the returned value is the field value with the respective name. In the case that this 
    * collection does not have a named member with the name specified {@code memberName}
    * the unknown value is returned.
    * 
    * @param   type
    *          The expected type of the returned value.
    * @param   memberName
    *          The name of the member to be extracted.
    *
    * @return  The value of the member or unknown (?) if there is no such member.
    */
   @Override
   public <T extends BaseDataType> T dereference(Class<T> type, String memberName)
   {
      BaseDataType val = dereference(memberName);
      if (val.isUnknown() || !type.isAssignableFrom(val.getClass()))
      {
         ErrorManager.recordOrShowError(10068);
         // Lead attributes in a chained-attribute expression (a:b:c) must be type HANDLE or a user-defined type and valid (not UNKNOWN).
         return (T) BaseDataType.generateUnknown(type);
      }
      
      return (T) val;
   }
   
   /**
    * This overloaded method is used when the dereference is the left-side of an assignment.
    * The implementation will extract the member from the collection and assign it the new value.
    *
    * @param   memberName
    *          The name of the member to be extracted.
    * @param   value
    *          The new value that will be assigned to the extracted field.
    */
   @Override
   public void dereference(String memberName, Object value)
   {
      ErrorManager.recordOrThrowError(7372);
      // Object of type DATASET widget used where BUFFER-FIELD object required.
   }
   
   /**
    * Get the {@code PREV-SIBLING} attribute of this DataSet. Actually always returns {@code null} because
    * this attribute is not supported by DataSets. Only {@code NEXT-SIBLING} is supported.
    *
    * @return  Always {@code null}.
    */
   @Override
   public handle getPrevSibling()
   {
      handle.invalidAttribute("PREV-SIBLING", false, LegacyResource.DATASET);
      return new handle();
   }
   
   /**
    * Get the {@code NEXT-SIBLING} attribute of this DataSet. Actually always returns the super's 
    * {@code getPrevSibling()} because DataSets are queued in reverse order(!).
    * <p>
    * TODO: investigate other handle resource that manifest the same from the point of NEXT/PREV-SIBLING.
    *
    * @return  Always {@code null}.
    */
   @Override
   public handle getNextSibling()
   {
      return super.getPrevSibling();
   }
   
   /**
    * Returns the resource to it's clear state. The buffers and relations are removed. However,
    * the content of the dataset tables is not dropped (ie, the tables are not cleared and will
    * keep their current data).
    *
    * @return true if operation is successful
    */
   @Override
   public logical clear()
   {
      if (!_dynamic())
      {
         ErrorManager.recordOrShowError(11967);
         // CLEAR may not be used on a static dataset.
         return logical.of(false);
      }
      
      clearImpl(false, true);
      
      // TODO: when is false returned?
      return logical.of(true);
   }
   
   /**
    * Clears the dataset. Deletes the relations, unlinks the buffers and reset all internal data
    * to default values.
    * <p>
    * After being invoked on a dynamic {@code DataSet} the object gets ready to be repopulated.
    * In this case the method can be invoked multiple times.
    * <p>
    * In the case of a static {@code DataSet}s, the method is called only once, when the object is
    * destroyed. It only makes it easy for GC to free the occupied resources.
    *
    * @param   delete
    *          The dataset will be deleted. If there are dynamic buffers associated with it having AUTO-DELETE
    *          attribute set they will be also deleted.
    * @param   resetProperties
    *          If {@code true} all attributes of the dataset are set to unknown values.
    *
    */
   private void clearImpl(boolean delete, boolean resetProperties)
   {
      for (int i = 0; i < relations.size(); i++)
      {
         DataRelation rel = relations.get(i);
         rel.delete();
      }
      relations.clear();
      
      for (int i = 0; i < buffers.size(); i++)
      {
         BufferImpl buf = buffers.get(i);
         // delete dynamic buffers associated with dynamic temp-tables, unless the buffer's AUTO-DELETE 
         // attribute is set to FALSE
         if (delete                          && 
             buf.autoDelete().booleanValue() && 
             buf._dynamic()                  && 
             buf.tableHandleNative().valid() && 
             buf.tableHandleNative()._dynamic())
         {
            AbstractTempTable tt = (AbstractTempTable) buf.tableHandleNative();
            if (tt != null && tt.valid() && tt._dynamic()) 
            {
               // delete the temp-table
               tt.delete();
            }
            // delete the buffer
            buf.delete();
         }
         
         buf.setDataSet(null);
      }
      buffers.clear();
      proxies.clear();
      
      if (resetProperties)
      {
         callbacks = null;
         name = null;
         admData = null;
         nsUri = null;
         nsPrefix = null;
         xmlNodeName = null;
         serializeName = null;
         defaultActive = true;
         error = false;
         rejected = false;
      }
   }
   
   /**
    * Returns {@code true} if the buffer is dynamic.
    * <p>
    * This is not emitted by conversion, is used internally by FWD.
    *
    * @return {@code true} if the resource is dynamic, {@code false} otherwise.
    */
   @Override
   public boolean _dynamic()
   {
      return this.dynamic;
   }
   
   /**
    * Checks  whether the resource is dynamic or static.
    * 
    * @return   {@code true} if the resource is dynamic, {@code false} otherwise. 
    */
   @Override
   public logical dynamic()
   {
      return logical.of(dynamic);
   }

   /**
    * Loads the empty target ({@code DataSet} or {@code TempTable}) object with changed rows from
    * original (source) object.
    *
    * @param   original
    *          Handle to original object ({@code DataSet} or {@code TempTable}) that contains the
    *          changed rows to load into the target object.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical getChanges(handle original)
   {
      return getChanges(original, false);
   }
   
   /**
    * Loads the empty target ({@code DataSet} or {@code TempTable}) object with changed rows from
    * original (source) object.
    *
    * @param   original
    *          Handle to original object ({@code DataSet} or {@code TempTable}) that contains the
    *          changed rows to load into the target object.
    * @param   parentMode
    *          When {@code TRUE} the target gets the changed parent rows of each changed child row.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical getChanges(handle original, logical parentMode)
   {
      return getChanges(original,
                        parentMode != null && !parentMode.isUnknown() && parentMode.getValue());
   }
   
   /**
    * Loads the empty target ({@code DataSet} or {@code TempTable}) object with changed rows from
    * original (source) object.
    *
    * @param   original
    *          Handle to original object ({@code DataSet} or {@code TempTable}) that contains the
    *          changed rows to load into the target object.
    * @param   parentMode
    *          When {@code TRUE} the target gets the changed parent rows of each changed child row.
    *          
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical getChanges(handle original, boolean parentMode)
   {
      // the parameter must be a valid dataset handle:
      if (original == null || original.isUnknown() || !original.isType(LegacyResource.DATASET))
      {
         ErrorManager.recordOrShowError(11953);
         // Dataset GET-CHANGES and MERGE-CHANGES must have a valid dataset handle as the first argument.
         return logical.of(false);
      }
      
      DataSet dsSource = (DataSet) original.getResource();
      // check 'original' to be a compatible dataset handle
      if (!isCompatibleWith(dsSource))
      {
         ErrorManager.recordOrShowError(11953);
         // Dataset GET-CHANGES and MERGE-CHANGES must have a change dataset and an original dataset with the same number of members and the member buffers must have matching schema.
         return logical.of(false);
      }
      
      // this keeps the set of parent jobs that need to be executed after all buffers were
      // processed in non-parentMode, otherwise they risk to be dropped if the parent is processed
      // after the child buffer.
      ArrayList<Object[]> parentJobs = new ArrayList<>();
      
      // processing stops at first non-matching table, returns FALSE, but KEEPS copied/processed tables  
      for (int i = 0; i < buffers.size(); i++)
      {
         BufferImpl src = dsSource.getBuffers().get(i);
         TempTable srcTempTable = TempTableBuilder.asTempTable((Temporary) src);
         
         BufferImpl dst = buffers.get(i);
         logical ok = dst.getChanges(new handle(src));
         if (!ok.getValue())
         {
            return ok;
         }
         
         if (parentMode)
         {
            List<DataRelation> relationSet = dsSource.getRelations(src, false, true, true);
            if (!relationSet.isEmpty())
            {
               // there can be maximum one of this relation because a buffer cannot be child in
               // more then one active relation
               DataRelation dataRelation = relationSet.get(0);
               BufferImpl parentSrc = dataRelation.getParentBufferNative();
               BufferImpl parentDst = null;
               for (int k = 0; k < dsSource.getBuffers().size(); k++)
               {
                  if (dsSource.getBuffers().get(k) == parentSrc.ref())
                  {
                     // get the corresponding buffer
                     parentDst = buffers.get(k);
                  }
               }
               if (parentDst != null) 
               {
                  // check whether the parentDst has an (PRIMARY?) UNIQUE index
                  boolean found = false;
                  int idxNo = 0;
                  TableMapper.LegacyIndexInfo index = TableMapper.getLegacyIndexInfo(parentDst.buffer(), idxNo);
                  while (index != null)
                  {
                     if (index.isUnique()) // and PRIMARY? At least this is implied from error message
                     {
                        found = true;
                        break;
                     }
                     
                     index = TableMapper.getLegacyIndexInfo(parentDst.buffer(), ++idxNo);
                  }
                  if (!found)
                  {
                     ErrorManager.recordOrShowError(11885);
                     // A unique primary index is required in the target table, each field of which is mapped to some source field, in order to do a replace-mode or parent-mode COPY/GET/MERGE/FILL type of operation on a dataset table.
                     ErrorManager.recordOrShowError(11891);
                     // Found error during GET-CHANGES method.
                  }
                  
                  // copy only unchanged records from [parentSrc] to [parentDst] for changed
                  // records from [src.before], using index [index] for iteration. These are
                  // normally skipped when [parentDst] was populated from [parentSrc] but now are
                  // required because of the [parentMode] flag. Only AFTER records are copied:
                  // since they were NOT modified they do not have a BEFORE pair.
                  String whereStr = dataRelation.getWhereString().toJavaType();
                  parentJobs.add(new Object[] { parentSrc, parentDst, src, whereStr});
                  // parentSrc.copyParentUnchangedRecords(parentDst, src, whereStr);
               }
            }
         }
      }
      
      if (!parentJobs.isEmpty())
      {
         for (int i = 0; i < parentJobs.size(); i++)
         {
            Object[] job = parentJobs.get(i);
            BufferImpl parentSrc = (BufferImpl) job[0];
            BufferImpl parentDst = (BufferImpl) job[1];
            BufferImpl childSrc = (BufferImpl) job[2];
            String whereStr = (String) job[3];
            TemporaryBuffer.copyParentUnchangedRecords(parentDst, parentSrc, childSrc, whereStr);
         }
      }
      
      return logical.of(true);
   }
   
   /**
    * Merges the changed rows of all temp-tables or a single temp-table in a source
    * {@code DataSet} object loaded with the {@code GET-CHANGES()} method into the corresponding
    * rows of all temp-tables or a single temp-table (respectively) in the original (target)
    * {@code DataSet} object.
    *
    * @param   original
    *          A handle to the original (target) {@code DataSet} object or the buffer object of a
    *          single target {@code DataSet} temp-table to merge with the changed rows from the
    *          source object.
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical mergeChanges(handle original)
   {
      return mergeChanges(original, false);
   }
   
   /**
    * Merges the changed rows of all temp-tables or a single temp-table in a source
    * {@code DataSet} object loaded with the {@code GET-CHANGES()} method into the corresponding
    * rows of all temp-tables or a single temp-table (respectively) in the original (target)
    * {@code DataSet} object.
    *
    * @param   original
    *          A handle to the original (target) {@code DataSet} object or the buffer object of a
    *          single target {@code DataSet} temp-table to merge with the changed rows from the
    *          source object.
    * @param   allCopyMode
    *          Use {@code TRUE} to indicates that all rows of the source after-image table must be
    *          merged.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical mergeChanges(handle original, logical allCopyMode)
   {
      boolean copyAll = allCopyMode != null && !allCopyMode.isUnknown() && allCopyMode.getValue();
      return mergeChanges(original, copyAll);
   }
   
   /**
    * Merges the changed rows of all temp-tables or a single temp-table in a source
    * {@code DataSet} object loaded with the {@code GET-CHANGES()} method into the corresponding
    * rows of all temp-tables or a single temp-table (respectively) in the original (target)
    * {@code DataSet} object.
    *
    * @param   original
    *          A handle to the original (target) {@code DataSet} object or the buffer object of a
    *          single target {@code DataSet} temp-table to merge with the changed rows from the
    *          source object.
    * @param   allCopyMode
    *          Use {@code TRUE} to indicates that all rows of the source after-image table must be
    *          merged.
    *
    * @return {@code true} if operation is successful.
    */
   @Override
   public logical mergeChanges(handle original, boolean allCopyMode)
   {
      // the parameter must be a valid dataset handle:
      if (original == null || original.isUnknown() || !original.isType(LegacyResource.DATASET))
      {
         ErrorManager.recordOrShowError(11953);
         // Dataset GET-CHANGES and MERGE-CHANGES must have a valid dataset handle as the first argument.
         return logical.of(false);
      }
      
      DataSet dsOriginal = (DataSet) original.getResource();
      // check 'original' to be a compatible dataset handle
      if (!isCompatibleWith(dsOriginal))
      {
         ErrorManager.recordOrShowError(11953);
         // Dataset GET-CHANGES and MERGE-CHANGES must have a change dataset and an original dataset with the same number of members and the member buffers must have matching schema.
         return logical.of(false);
      }
      
      if (buffers.isEmpty())
      {
         return logical.of(true);
      }
      
      // NOTE: this is probably a bug / quirk: if an unrelated (ie. not original) dataset is passed as
      //       parameter, the method returns with success without altering the tables
      //       TODO: prove the last affirmation
      TempTable ttChanges = buffers.get(0).tableHandleNative();
      TempTable ttOriginal = dsOriginal.getBuffers().get(0).tableHandleNative();
      WrappedResource ufo = ttChanges.getOriginTable();
      if (ufo != null && ufo != ttOriginal)
      {
         return logical.of(true);
      }
      
      for (int i = 0; i < buffers.size(); i++)
      {
         BufferImpl myBuf = buffers.get(i);                    // contains the changes
         BufferImpl origBuf = dsOriginal.getBuffers().get(i);  // the target temp-table
         
         logical res = myBuf.mergeChanges(new handle(origBuf), allCopyMode);
         if (!res.getValue())
         {
            return res;
         }
      }
      
      error(false); // clear the ERROR attribute in the process
      return logical.of(true);
   }
   
   /**
    * Duplicates the structure of an existing {@code DataSet}, copying its entire structure.
    * This method is the P2J equivalent of {@code CREATE-LIKE} method of Progress 4GL.
    *
    * @param   hds
    *          A handle to a {@code DataSet} from which to copy the definitions.
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical createLike(handle hds)
   {
      if (hds == null || hds.isUnknown() || !hds.isType(LegacyResource.DATASET))
      {
         ErrorManager.recordOrShowError(11968);
         // First argument to DATASET:CREATE-LIKE must be a valid dataset handle or name".
         return logical.of(false);
      }
      
      return createLikeImpl((DataSet) hds.getResource(), null);
   }
   
   /**
    * Duplicates the structure of an existing {@code DataSet}, copying its entire structure.
    * This method is the P2J equivalent of {@code CREATE-LIKE} method of Progress 4GL.
    *
    * @param   dsName
    *          The name of {@code DataSet} from which to copy the definitions.
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical createLike(String dsName)
   {
      DataSet sourceDS = DataSetManager.instance().byName(dsName);
      if (sourceDS == null || !sourceDS.valid())
      {
         ErrorManager.recordOrShowError(11968);
         // First argument to DATASET:CREATE-LIKE must be a valid dataset handle or name".
         return logical.of(false);
      }
      
      return createLikeImpl(sourceDS, null);
   }
   
   /**
    * Duplicates the structure of an existing {@code DataSet}, copying its entire structure.
    * This method is the P2J equivalent of {@code CREATE-LIKE} method of Progress 4GL.
    *
    * @param   dsName
    *          The name of {@code DataSet} from which to copy the definitions.
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical createLike(character dsName)
   {
      DataSet sourceDS = DataSetManager.instance().byName(dsName.toJavaType());
      if (sourceDS == null || !sourceDS.valid())
      {
         ErrorManager.recordOrShowError(11968);
         // First argument to DATASET:CREATE-LIKE must be a valid dataset handle or name".
         return logical.of(false);
      }
      
      return createLikeImpl(sourceDS, null);
   }
   
   /**
    * Duplicates the structure of an existing {@code DataSet}, copying its entire structure.
    * This method is the P2J equivalent of {@code CREATE-LIKE} method of Progress 4GL.
    *
    * @param   hds
    *          A handle to a {@code DataSet} from which to copy the definitions.
    * @param   prefix
    *          The prefix to prepend to each of the source {@code DataSet} member buffer names,
    *          which creates a new name for each new member buffer.
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical createLike(handle hds, String prefix)
   {
      if (hds == null || hds.isUnknown() || !hds.isType(LegacyResource.DATASET))
      {
         ErrorManager.recordOrShowError(11968);
         // First argument to DATASET:CREATE-LIKE must be a valid dataset handle or name".
         return logical.of(false);
      }
         
      return createLikeImpl((DataSet) hds.getResource(), prefix);
   }
   
   /**
    * Duplicates the structure of an existing {@code DataSet}, copying its entire structure.
    * This method is the P2J equivalent of {@code CREATE-LIKE} method of Progress 4GL.
    *
    * @param   hds
    *          A handle to a {@code DataSet} from which to copy the definitions.
    * @param   prefix
    *          The prefix to prepend to each of the source {@code DataSet} member buffer names,
    *          which creates a new name for each new member buffer.
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical createLike(handle hds, character prefix)
   {
      if (hds == null || hds.isUnknown() || !hds.isType(LegacyResource.DATASET))
      {
         ErrorManager.recordOrShowError(11968);
         // First argument to DATASET:CREATE-LIKE must be a valid dataset handle or name".
         return logical.of(false);
      }
      
      return createLikeImpl((DataSet) hds.getResource(), prefix.toJavaType());
   }
   
   /**
    * Duplicates the structure of an existing {@code DataSet}, copying its entire structure.
    * This method is the P2J equivalent of {@code CREATE-LIKE} method of Progress 4GL.
    *
    * @param   dsName
    *          The name of {@code DataSet} from which to copy the definitions.
    * @param   prefix
    *          The prefix to prepend to each of the source {@code DataSet} member buffer names,
    *          which creates a new name for each new member buffer.
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical createLike(String dsName, String prefix)
   {
      DataSet sourceDS = DataSetManager.instance().byName(dsName);
      if (sourceDS == null || !sourceDS.valid())
      {
         ErrorManager.recordOrShowError(11968);
         // First argument to DATASET:CREATE-LIKE must be a valid dataset handle or name".
         return logical.of(false);
      }
      
      return createLikeImpl(sourceDS, prefix);
   }
   
   /**
    * Duplicates the structure of an existing {@code DataSet}, copying its entire structure.
    * This method is the P2J equivalent of {@code CREATE-LIKE} method of Progress 4GL.
    *
    * @param   dsName
    *          The name of {@code DataSet} from which to copy the definitions.
    * @param   prefix
    *          The prefix to prepend to each of the source {@code DataSet} member buffer names,
    *          which creates a new name for each new member buffer.
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical createLike(character dsName, character prefix)
   {
      DataSet sourceDS = DataSetManager.instance().byName(dsName.toJavaType());
      if (sourceDS == null || !sourceDS.valid())
      {
         ErrorManager.recordOrShowError(11968);
         // First argument to DATASET:CREATE-LIKE must be a valid dataset handle or name".
         return logical.of(false);
      }
      
      return createLikeImpl(sourceDS, prefix.toJavaType());
   }
   
   /**
    * Copies the other TEMP-TABLE. Other must be a TEMP-TABLE handle. By default, all current
    * records are dropped before copy operation.
    * <p>
    * The only reason the {@link TempTableDuplicator} is implemented by {@code DataSet} is that
    * we found it static code (@code DATASET DSet:COPY-TEMP-TABLE()}). If the method was
    * referenced via handle interface, the {@code handle} would automatically unwrap it to an 
    * {@code invalidAttrAccessProxy}. Because the method is accessed directly from the object,
    * however, makes it mandatory that the {@code DataSet} to implement the interface.
    *
    * @param   other
    *          Some other temp-table to copy from.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical copyTempTable(handle other)
   {
      return copyTempTable(other, false, false, false, null);
   }
   
   /**
    * Copies the other TEMP-TABLE. Other must be a TEMP-TABLE handle. By default, all current
    * records are dropped before copy operation.
    * <p>
    * The only reason the {@link TempTableDuplicator} is implemented by {@code DataSet} is that
    * we found it static code (@code DATASET DSet:COPY-TEMP-TABLE()}). If the method was
    * referenced via handle interface, the {@code handle} would automatically unwrap it to an 
    * {@code invalidAttrAccessProxy}. Because the method is accessed directly from the object,
    * however, makes it mandatory that the {@code DataSet} to implement the interface.
    *
    * @param   other
    *          Some other temp-table to copy from.
    * @param   append
    *          Append mode.
    *
    * @return  {@code true}  if operation is successful.
    */
   @Override
   public logical copyTempTable(handle other, logical append)
   {
      return copyTempTable(other, append, logical.of(false), logical.of(false), new character());
   }
   
   /**
    * Copies the other TEMP-TABLE. Other must be a TEMP-TABLE handle. By default, all current
    * records are dropped before copy operation.
    * <p>
    * The only reason the {@link TempTableDuplicator} is implemented by {@code DataSet} is that
    * we found it static code (@code DATASET DSet:COPY-TEMP-TABLE()}). If the method was
    * referenced via handle interface, the {@code handle} would automatically unwrap it to an 
    * {@code invalidAttrAccessProxy}. Because the method is accessed directly from the object,
    * however, makes it mandatory that the {@code DataSet} to implement the interface.
    *
    * @param   other
    *          Some other temp-table to copy from.
    * @param   append
    *          Append mode.
    * @param   replace
    *          Replace mode.
    *
    * @return  {@code true} if operation is successful
    */
   @Override
   public logical copyTempTable(handle other, logical append, logical replace)
   {
      return copyTempTable(other, append, replace, logical.of(false), new character());
   }
   
   /**
    * Copies the other TEMP-TABLE. Other must be a TEMP-TABLE handle. By default, all current
    * records are dropped before copy operation.
    * <p>
    * The only reason the {@link TempTableDuplicator} is implemented by {@code DataSet} is that
    * we found it static code (@code DATASET DSet:COPY-TEMP-TABLE()}). If the method was
    * referenced via handle interface, the {@code handle} would automatically unwrap it to an 
    * {@code invalidAttrAccessProxy}. Because the method is accessed directly from the object,
    * however, makes it mandatory that the {@code DataSet} to implement the interface.
    *
    * @param   other
    *          Some other temp-table to copy from.
    * @param   append
    *          Append mode.
    * @param   replace
    *          Replace mode.
    * @param   looseCopy
    *          Loose copy mode.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical copyTempTable(handle other, logical append, logical replace, logical looseCopy)
   {
      return copyTempTable(other, append, replace, looseCopy, new character());
   }
   
   /**
    * Copies the other TEMP-TABLE. Other must be a TEMP-TABLE handle. By default, all current
    * records are dropped before copy operation.
    * <p>
    * The only reason the {@link TempTableDuplicator} is implemented by {@code DataSet} is that
    * we found it static code (@code DATASET DSet:COPY-TEMP-TABLE()}). If the method was
    * referenced via handle interface, the {@code handle} would automatically unwrap it to an 
    * {@code invalidAttrAccessProxy}. Because the method is accessed directly from the object,
    * however, makes it mandatory that the {@code DataSet} to implement the interface.
    *
    * @param   other
    *          Some other temp-table to copy from.
    * @param   append
    *          Append mode.
    *
    * @return  {@code true}  if operation is successful.
    */
   @Override
   public logical copyTempTable(handle other, boolean append)
   {
      return copyTempTable(other, append, false, false, null);
   }
   
   /**
    * Copies the other TEMP-TABLE. Other must be a TEMP-TABLE handle. By default, all current
    * records are dropped before copy operation.
    * <p>
    * The only reason the {@link TempTableDuplicator} is implemented by {@code DataSet} is that
    * we found it static code (@code DATASET DSet:COPY-TEMP-TABLE()}). If the method was
    * referenced via handle interface, the {@code handle} would automatically unwrap it to an 
    * {@code invalidAttrAccessProxy}. Because the method is accessed directly from the object,
    * however, makes it mandatory that the {@code DataSet} to implement the interface.
    *
    * @param   other
    *          Some other temp-table to copy from.
    * @param   append
    *          Append mode.
    * @param   replace
    *          Replace mode.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical copyTempTable(handle other, boolean append, boolean replace)
   {
      return copyTempTable(other, append, replace, false, null);
   }
   
   /**
    * Copies the other TEMP-TABLE. Other must be a TEMP-TABLE handle. By default, all current
    * records are dropped before copy operation.
    * <p>
    * The only reason the {@link TempTableDuplicator} is implemented by {@code DataSet} is that
    * we found it static code (@code DATASET DSet:COPY-TEMP-TABLE()}). If the method was
    * referenced via handle interface, the {@code handle} would automatically unwrap it to an 
    * {@code invalidAttrAccessProxy}. Because the method is accessed directly from the object,
    * however, makes it mandatory that the {@code DataSet} to implement the interface.
    *
    * @param   other
    *          Some other temp-table to copy from.
    * @param   append
    *          Append mode.
    * @param   replace
    *          Replace mode.
    * @param   looseCopy
    *          Loose copy mode.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical copyTempTable(handle other, boolean append, boolean replace, boolean looseCopy)
   {
      return copyTempTable(other, append, replace, looseCopy, null);
   }
   
   /**
    * Copies the other TEMP-TABLE. Other must be a TEMP-TABLE handle. By default, all current 
    * records are dropped before copy operation. 
    * Only the first parameter is mandatory so this method will probably be heavy overloaded.
    * <p>
    * The only reason the {@link TempTableDuplicator} is implemented by {@code DataSet} is that
    * we found it static code (@code DATASET DSet:COPY-TEMP-TABLE()}). If the method was
    * referenced via handle interface, the {@code handle} would automatically unwrap it to an 
    * {@code invalidAttrAccessProxy}. Because the method is accessed directly from the object,
    * however, makes it mandatory that the {@code DataSet} to implement the interface.
    *
    * @param   other
    *          Some other temp-table to copy from.
    * @param   append
    *          Append mode.
    * @param   replace
    *          Replace mode.
    * @param   looseCopy
    *          Loose copy mode.
    * @param   prefix
    *          Prefix for naming the copied temp-table.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical copyTempTable(handle other,
                                logical append,
                                logical replace,
                                logical looseCopy,
                                character prefix)
   {
      handle.invalidAttribute("COPY-TEMP-TABLE", false, LegacyResource.DATASET);
      return logical.UNKNOWN;
   }
   
   /**
    * Copies the other TEMP-TABLE. Other must be a TEMP-TABLE handle. By default, all current 
    * records are dropped before copy operation. 
    * Only the first parameter is mandatory so this method will probably be heavy overloaded.
    * <p>
    * The only reason the {@link TempTableDuplicator} is implemented by {@code DataSet} is that
    * we found it static code (@code DATASET DSet:COPY-TEMP-TABLE()}). If the method was
    * referenced via handle interface, the {@code handle} would automatically unwrap it to an 
    * {@code invalidAttrAccessProxy}. Because the method is accessed directly from the object,
    * however, makes it mandatory that the {@code DataSet} to implement the interface.
    *
    * @param   other
    *          Some other temp-table to copy from.
    * @param   append
    *          Append mode.
    * @param   replace
    *          Replace mode.
    * @param   looseCopy
    *          Loose copy mode.
    * @param   prefix
    *          Prefix for naming the copied temp-table.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical copyTempTable(handle other,
                                boolean append,
                                boolean replace,
                                boolean looseCopy,
                                String prefix)
   {
      handle.invalidAttribute("COPY-TEMP-TABLE", false, LegacyResource.DATASET);
      return logical.UNKNOWN;
   }
   
   /**
    * Check if the buffer's fill query can be {@link #reuseFillQuery re-used}.
    */
   boolean isReuseFillQuery()
   {
      return reuseFillQuery;
   }
   
   /**
    * Fills a {@code DataSet} object, recursively, based on its defined data sources, data
    * relations, and queries. When invoked on one of its member {@code Buffer} objects a partial
    * fill is performed, by starting at the respective level.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical fill()
   {
      if (!invokeCallback(asHandle(), callbacks == null ? null : callbacks.get(BEFORE_FILL_EVENT)))
      {
         error(true);
         return logical.of(false);
      }
      
      ErrorManager.ErrorHelper errHlp = ErrorManager.getErrorHelper();
      boolean wmOn = errHlp.isWarningMode();
      if (!wmOn)
      {
         errHlp.setWarningMode(true);
      }
      
      try
      {
         this.reuseFillQuery = true;
         
         // start with the current set of top-buffers
         List<Buffer> topBuffers = getTopBufferList();
         for (Buffer crtBuffer : topBuffers)
         {
            // fill this buffer. It will invoke fill for all buffers in parent relation (nesting)
            logical fillOk = crtBuffer.fill();
            if (!fillOk.getValue())
            {
               return fillOk;
            }
         }
      }
      finally
      {
         this.reuseFillQuery = false;
         for (BufferImpl buffer : buffers)
         {
            buffer.clearQuery();
         }
         
         if (!wmOn)
         {
            errHlp.setWarningMode(false);
         }
      }
      
      boolean ok = invokeCallback(asHandle(), callbacks == null ? null : callbacks.get(AFTER_FILL_EVENT));
      error(!ok);
      return logical.of(ok);
   }
   
   
   /**
    * Obtain the mode in which the {@link DataSet#fill()} method fills a {@code DataSet} member buffer. The
    * default mode is {@code MERGE}.
    * <p>
    * <strong>Note:</strong>
    *         Undocumented feature: query all buffers and returns the common fill mode if one exist. Otherwise
    *         returns unknown value.
    *
    * @return  always unknown value.
    */
   @Override
   public character fillMode()
   {
      String mode = null;
      for (int i = 0; i < buffers.size(); i++)
      {
         String crtFillMode = buffers.get(i).fillMode().toJavaType();
         if (i == 0)
         {
            // assume the fill-mode value of first buffer 
            mode = crtFillMode;
         }
         else if (mode == null || !mode.equals(crtFillMode))
         {
            return new character();
         }
      }
      
      return new character(mode);
   }
   
   /**
    * Configures the mode in which the {@link DataSet#fill()} method fills a {@code DataSet} member buffer.
    * The allowed modes are: {@code APPEND}, {@code EMPTY}, {@code MERGE}, {@code NO-FILL}, and
    * {@code REPLACE}.
    * <p>
    * <strong>Note:</strong>
    *         Undocumented feature: set the fill mode on all buffers.
    *
    * @param   mode
    *          The new fill mode for the buffers of this dataset.
    */
   @Override
   public void fillMode(String mode)
   {
      if (mode == null)
      {
         unableToAssignUnknown("FILL-MODE", "DATASET widget");
         return;
      }
      
      String upperMode = mode.toUpperCase();
      switch (upperMode)
      {
         case FM_APPEND:
         case FM_EMPTY:
         case FM_MERGE:
         case FM_NOFILL:
         case FM_REPLACE:
            // one of these is expected
            break;
         default:
            // but this is not. Complain about it.
            ErrorManager.recordOrShowError(11960);
            // FILL-MODE must be one of NO-FILL, MERGE, EMPTY, etc. 
            ErrorManager.recordOrShowError(4058, "FILL-MODE", "DATASET widget", mode);
            // **Attribute <attribute> for the <widget id> has an invalid value of <string>.
      }
      
      // set the fill mode on all buffers
      for (int i = 0; i < buffers.size(); i++)
      {
         BufferImpl buffer = buffers.get(i);
         buffer.fillMode(mode);
      }
   }
   
   /**
    * Configures the mode in which the {@link DataSet#fill()} method fills a {@code DataSet} member buffer.
    * The allowed modes are: {@code APPEND}, {@code EMPTY}, {@code MERGE}, {@code NO-FILL},
    * and {@code REPLACE}.
    * <p>
    * <strong>Note:</strong>
    *         Undocumented feature: set the fill mode on all buffers.
    *
    * @param   mode
    *          the new fill mode for this buffer.
    */
   @Override
   public void fillMode(Text mode)
   {
      fillMode(mode == null ? null : mode.toJavaType());
   }
   
   /**
    * Obtain number of references to a {@code Buffer}, {@code DataSet}, or {@code TempTable}
    * object that is defined as a parameter to which reference-only objects are bound.
    * This corresponds to the read-only {@code NUM-REFERENCES} attribute.
    *
    * @return  number of references to this object.
    */
   @Override
   public integer numReferences()
   {
      return new integer(refs == null ? 0 : refs.size());
   }
   
   /**
    * Checks whether the data in the data source has been modified.
    *
    * @return  {@code true} when the data has been modified.
    */
   @Override
   public logical isDataSourceModified()
   {
      return logical.of(dataSourceModified);
   }
   
   /**
    * Manually mark the data in the target structure as modified.
    *
    * @param   mod
    *          {@code true} when the data is marked as modified and {@code false} otherwise.
    */
   @Override
   public void setDataSourceModified(logical mod)
   {
      if (mod == null || mod.isUnknown())
      {
         unableToAssignUnknown("DATA-SOURCE-MODIFIED", "DATASET widget");
         return;
      }
   
      setDataSourceModified(mod.getValue());
   }
   
   /**
    * Manually mark the data in the target structure as modified.
    *
    * @param   mod
    *          {@code true} when the data is marked as modified and {@code false} otherwise.
    */
   @Override
   public void setDataSourceModified(boolean mod)
   {
      this.dataSourceModified = mod;
   }
   
   /**
    * Accepts changes to the data in target object (a temp-table or all temp-tables in a
    * {@code DataSet}).
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical acceptChanges()
   {
      for (int i = 0; i < buffers.size(); i++)
      {
         BufferImpl buffer = buffers.get(i);
         if (!buffer.isAfterBuffer())
         {
            // skip this buffer
            continue;
         }
         
         logical ok = buffer.beforeBufferNative().acceptChanges();
         if (!ok.booleanValue())
         {
            return ok;
         }
      }
      
      error(false); // clear the ERROR attribute in the process
      return logical.of(true);
   }
   
   /**
    * Rejects changes to the data in target object (a temp-table or all temp-tables in a
    * {@code DataSet}).
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical rejectChanges()
   {
      for (int i = 0; i < buffers.size(); i++)
      {
         BufferImpl buffer = buffers.get(i);
         if (!buffer.isAfterBuffer())
         {
            // skip this buffer
            continue;
         }
         
         logical ok = buffer.beforeBufferNative().rejectChanges();
         if (!ok.booleanValue())
         {
            return ok;
         }
      }
      
      error(false); // clear the ERROR attribute in the process
      return logical.of(true);
   }
   
   /**
    * Applies a callback procedure, which allows execution of a defined event without duplicating
    * the event procedure definition.
    *
    * @param   eventName
    *          The event whose callback will be called.
    *
    * @return   {@code true} if operation is successful.
    */
   @Override
   public logical applyCallback(String eventName)
   {
      if (!isCallbackName(eventName))
      {
         return logical.of(false);
      }
      
      return logical.of(invokeCallback(asHandle(), 
                                        callbacks == null ? null : callbacks.get(eventName.toUpperCase())));
   }
   
   /**
    * Retrieves the name of the internal procedure associated with the ABL callback for the
    * specified event.
    *
    * @param   eventName
    *          The name of the event.
    *
    * @return the name of the internal procedure associated with specified event.
    */
   @Override
   public character getCallbackProcName(String eventName)
   {
      if (!isCallbackName(eventName))
      {
         return new character();
      }
      
      CallbackData cbd = callbacks == null ? null : callbacks.get(eventName.toUpperCase());
      if (cbd == null)
      {
         return new character();
      }
      
      return new character(cbd.getTarget());
   }
   
   /**
    * Retrieves the handle of the procedure that contains the internal procedure associated
    * with the ABL callback for the specified event
    *
    * @param   eventName
    *          The name of the event.
    *
    * @return  a handle to the procedure that contains the callback procedure.
    */
   @Override
   public handle getCallbackProcContext(String eventName)
   {
      if (!isCallbackName(eventName))
      {
         return new handle();
      }
      
      CallbackData cbd = callbacks == null ? null : callbacks.get(eventName.toUpperCase());
      if (cbd == null || cbd.getProcedure() == null)
      {
         return new handle();
      }
   
      return new handle(cbd.getProcedure());
   }
   
   /**
    * Associates an internal procedure with an ABL callback event.
    * <p>
    * This method returns false if the event name is not valid.
    * 
    * @param    event
    *           The event name, must evaluate to one of the events supported by implementing object. 
    * @param    proc
    *           The internal procedure to be executed for this event, which will be executed using
    *           the current THIS-PROCEDURE reference. To remove a callback mapping, set the this
    *           parameter to the empty string.
    *           
    * @return   {@code true} if the event was registered.
    */
   @Override
   public logical setCallbackProcedure(String event, String proc)
   {
      return setCallbackProcedure(event, proc, null);
   }

   /**
    * Associates an internal procedure with an ABL callback event.
    * <p>
    * This method returns false if the event name is not valid.
    *
    * @param   event
    *          The event name.
    * @param   proc
    *          The internal procedure to be executed for this event, which will be executed using
    *          the current THIS-PROCEDURE reference. To remove a callback mapping, set the this
    *          parameter to the empty string.
    *
    * @return   {@code true} if the event was registered.
    */
   @Override
   public logical setCallbackProcedure(String event, character proc)
   {
      return setCallbackProcedure(event, proc.toJavaType(), null);
   }

   /**
    * Associates an internal procedure with an ABL callback event.
    * <p>
    * This method returns false if the event name is not valid.
    *
    * @param   event
    *          The event name.
    * @param   proc
    *          The internal procedure to be executed for this event, which will be executed using
    *          the current THIS-PROCEDURE reference. To remove a callback mapping, set the this
    *          parameter to the empty string.
    *
    * @return   {@code true} if the event was registered.
    */
   @Override
   public logical setCallbackProcedure(character event, character proc)
   {
      return setCallbackProcedure(event.toJavaType(), proc.toJavaType(), null);
   }
   
   /**
    * Associates an internal procedure with an ABL callback event.
    * <p>
    * This method returns false if the event name is not valid or if the procedure's context is
    * not a valid handle.
    * 
    * @param    event
    *           The event name, must evaluate to one of the events supported by implementing object. 
    * @param    proc
    *           The internal procedure to be executed for this event. To remove a callback 
    *           mapping, set the this parameter to the empty string.
    * @param    context
    *           The procedure handle where the internal procedure will be searched and executed.
    *           
    * @return   {@code true} if the event was registered.
    */
   @Override
   public logical setCallbackProcedure(character event, String proc, handle context)
   {
      return setCallbackProcedure(event.toJavaType(), proc, context);
   }

   /**
    * Associates an internal procedure with an ABL callback event.
    * <p>
    * This method returns false if the event name is not valid or if the procedure's context is
    * not a valid handle.
    *
    * @param   event
    *          The event name.
    * @param   proc
    *          The internal procedure to be executed for this event, which will be executed using
    *          the current THIS-PROCEDURE reference. To remove a callback mapping, set the this
    *          parameter to the empty string.
    * @param   context
    *          The procedure handle where the internal procedure will be searched and executed.
    *
    * @return   {@code true} if the event was registered.
    */
   @Override
   public logical setCallbackProcedure(String event, character proc, handle context)
   {
      return setCallbackProcedure(event, proc.toJavaType(), context);
   }
   
   /**
    * Associates an internal procedure with an ABL callback event.
    * <p>
    * This method returns false if the event name is not valid or if the procedure's context is not a valid
    * handle.
    *
    * @param   event
    *          The event name.
    * @param   proc
    *          The internal procedure to be executed for this event, which will be executed using the current
    *          THIS-PROCEDURE reference. To remove a callback mapping, set the this parameter to the empty
    *          string.
    * @param   context
    *          The procedure handle where the internal procedure will be searched and executed.
    *
    * @return   {@code true} if the event was registered.
    */
   @Override
   public logical setCallbackProcedure(character event, character proc, handle context)
   {
      return setCallbackProcedure(event.toJavaType(), proc.toJavaType(), context);
   }
   
   /**
    * Applies a callback procedure, which allows execution of a defined event without duplicating
    * the event procedure definition.
    *
    * @param   eventName
    *          The event whose callback will be called.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical applyCallback(Text eventName)
   {
      return applyCallback(eventName.toJavaType());
   }

   /**
    * Retrieves the name of the internal procedure associated with the ABL callback for the
    * specified event.
    *
    * @param   eventName
    *          The name of the event.
    *
    * @return  the name of the internal procedure associated with specified event.
    */
   @Override
   public character getCallbackProcName(Text eventName)
   {
      return getCallbackProcName(eventName.toJavaType());
   }
   
   /**
    * Retrieves the handle of the procedure that contains the internal procedure associated
    * with the ABL callback for the specified event
    *
    * @param   eventName
    *          The name of the event.
    *
    * @return  a handle to the procedure that contains the callback procedure.
    */
   @Override
   public handle getCallbackProcContext(Text eventName)
   {
      return getCallbackProcContext(eventName.toJavaType());
   }
   
   /**
    * Configures a callback. Creates an association between a method within a class instance or
    * an internal procedure within a persistent procedure, with an ABL callback event.
    *
    * @param   callbackName
    *          The name of a callback.
    * @param   routineName
    *          The name of a method or an internal procedure to be associated.
    * @param   context
    *          The context in which the callback will be executed.
    *
    * @return  {@code true} if operation is successful and {@code false} if the parameters are
    *          invalid.
    */
   @Override
   public logical setCallback(Text callbackName, Text routineName, object<?> context)
   {
      return setCallback(callbackName.toJavaType(), routineName.toJavaType(), context);
   }
   
   /**
    * Configures a callback. Creates an association between a method within a class instance or
    * an internal procedure within a persistent procedure, with an ABL callback event.
    *
    * @param   callbackName
    *          The name of a callback.
    * @param   routineName
    *          The name of a method or an internal procedure to be associated.
    * @param   context
    *          The context in which the callback will be executed.
    *
    * @return  {@code true} if operation is successful and {@code false} if the parameters are
    *          invalid.
    */
   @Override
   public logical setCallback(String callbackName, String routineName, handle context)
   {
      return setCallbackProcedure(callbackName, routineName, context);
   }
   
   /**
    * Configures a callback. Creates an association between a method within a class instance or
    * an internal procedure within a persistent procedure, with an ABL callback event.
    *
    * @param   callbackName
    *          The name of a callback.
    * @param   routineName
    *          The name of a method or an internal procedure to be associated.
    * @param   context
    *          The context in which the callback will be executed.
    *
    * @return  {@code true} if operation is successful and {@code false} if the parameters are
    *          invalid.
    */
   @Override
   public logical setCallback(Text callbackName, Text routineName, handle context)
   {
      return setCallbackProcedure(callbackName.toJavaType(), routineName.toJavaType(), context);
   }
   
   /**
    * Configures a callback. Creates an association between a method within a class instance or
    * an internal procedure within a persistent procedure, with an ABL callback event.
    *
    * @param   callbackName
    *          The name of a callback.
    * @param   routineName
    *          The name of a method or an internal procedure to be associated.
    * @param   context
    *          The context in which the callback will be executed.
    *
    * @return  {@code true} if operation is successful and {@code false} if the parameters are
    *          invalid.
    */
   @Override
   public logical setCallback(String callbackName, Text routineName, handle context)
   {
      return setCallbackProcedure(callbackName, routineName.toJavaType(), context);
   }

   /**
    * Configures a callback. Creates an association between a method within a class instance or
    * an internal procedure within a persistent procedure, with an ABL callback event.
    *
    * @param   callbackName
    *          The name of a callback.
    * @param   routineName
    *          The name of a method or an internal procedure to be associated.
    *
    * @return  {@code true} if operation is successful and {@code false} if the parameters are
    *          invalid.
    */
   @Override
   public logical setCallback(String callbackName, String routineName)
   {
      return setCallback(callbackName, routineName, (object<?>) null);
   }
   
   /**
    * Configures a callback. Creates an association between a method within a class instance or
    * an internal procedure within a persistent procedure, with an ABL callback event.
    *
    * @param   callbackName
    *          The name of a callback.
    * @param   routineName
    *          The name of a method or an internal procedure to be associated.
    *
    * @return  {@code true} if operation is successful and {@code false} if the parameters are
    *          invalid.
    */
   @Override
   public logical setCallback(Text callbackName, Text routineName)
   {
      return setCallback(callbackName.toJavaType(), routineName.toJavaType(), (object<?>) null);
   }
   
   /**
    * Configures a callback. Creates an association between a method within a class instance or
    * an internal procedure within a persistent procedure, with an ABL callback event.
    *
    * @param   callbackName
    *          The name of a callback.
    * @param   routineName
    *          The name of a method or an internal procedure to be associated.
    *
    * @return  {@code true} if operation is successful and {@code false} if the parameters are
    *          invalid.
    */
   @Override
   public logical setCallback(Text callbackName, String routineName)
   {
      return setCallback(callbackName.toJavaType(), routineName);
   }
   
   /**
    * Configures a callback. Creates an association between a method within a class instance or
    * an internal procedure within a persistent procedure, with an ABL callback event.
    *
    * @param   callbackName
    *          The name of a callback.
    * @param   routineName
    *          The name of a method or an internal procedure to be associated.
    *
    * @return  {@code true} if operation is successful and {@code false} if the parameters are
    *          invalid.
    */
   @Override
   public logical setCallback(String callbackName, Text routineName)
   {
      return setCallback(callbackName, routineName.toJavaType());
   }

   /**
    * Configures a callback. Creates an association between a method within a class instance or
    * an internal procedure within a persistent procedure, with an ABL callback event.
    *
    * @param   callbackName
    *          The name of a callback.
    * @param   routineName
    *          The name of a method or an internal procedure to be associated.
    * @param   context
    *          The context in which the callback will be executed.
    *
    * @return  {@code true} if operation is successful and {@code false} if the parameters are
    *          invalid.
    */
   @Override
   public logical setCallback(String callbackName, Text routineName, object<?> context)
   {
      return setCallback(callbackName, routineName.toJavaType(), context);
   }
   
   /**
    * Configures a callback. Creates an association between a method within a class instance or
    * an internal procedure within a persistent procedure, with an ABL callback event.
    *
    * @param   callbackName
    *          The name of a callback.
    * @param   routineName
    *          The name of a method or an internal procedure to be associated.
    * @param   context
    *          The context in which the callback will be executed.
    *
    * @return  {@code true} if operation is successful and {@code false} if the parameters are
    *          invalid.
    */
   @Override
   public logical setCallback(Text callbackName, String routineName, object<?> context)
   {
      return setCallback(callbackName.toJavaType(), routineName, context);
   }
   
   /**
    * Configures a callback. Creates an association between a method within a class instance or
    * an internal procedure within a persistent procedure, with an ABL callback event.
    *
    * @param   callbackName
    *          The name of a callback.
    * @param   routineName
    *          The name of a method or an internal procedure to be associated.
    * @param   context
    *          The context in which the callback will be executed.
    *
    * @return  {@code true} if operation is successful and {@code false} if the parameters are
    *          invalid.
    */
   @Override
   public logical setCallback(Text callbackName, String routineName, handle context)
   {
      return setCallback(callbackName.toJavaType(), routineName, context);
   }
   
   /**
    * Associates an internal procedure with an ABL callback event.
    * <p>
    * This method returns false if the event name is not valid.
    *
    * @param   event
    *          The event name.
    * @param   proc
    *          The internal procedure to be executed for this event, which will be executed using
    *          the current THIS-PROCEDURE reference. To remove a callback mapping, set the this
    *          parameter to the empty string.
    *
    * @return   {@code true} if the event was registered.
    */
   @Override
   public logical setCallbackProcedure(character event, String proc)
   {
      return setCallbackProcedure(event.toJavaType(), proc, null);
   }
   
   /**
    * Configures a callback. Creates an association between a method within a class instance or
    * an internal procedure within a persistent procedure, with an ABL callback event.
    *
    * @param   callbackName
    *          The name of a callback.
    * @param   routineName
    *          The name of a method or an internal procedure to be associated.
    * @param   context
    *          The context in which the callback will be executed.
    *
    * @return  {@code true} if {@code callbackName} is a correct callback name. The rest of
    *          parameters are ignored (note: manual states otherwise).
    */
   @Override
   public logical setCallback(String callbackName, String routineName, object<?> context)
   {
      if (!isCallbackName(callbackName))
      {
         return logical.of(false);
      }
      
      if (routineName == null)
      {
         return logical.of(true);
      }
      
      if (routineName.isEmpty())
      {
         if (callbacks != null)
         {
            callbacks.remove(callbackName.toUpperCase());
         }
         return logical.of(true);
      }
      
      CallbackData cbd;
      if (context == null || context.isUnknown())
      {
         // defaulting to THIS-OBJECT or to THIS-PROCEDURE, depending on the context
         if (ObjectOps.thisObject().isUnknown())
         {
            InvokeConfig callSite = getCallSite(callbackName);
            cbd = new CallbackData(routineName, pm.thisProcedure(), null, callSite);
         }
         else
         {
            cbd = new CallbackData(routineName, null, ObjectOps.thisObject(), null);
         }
      }
      else
      {
         cbd = new CallbackData(routineName, null, context, null);
      }
      
      if (callbacks == null)
      {
         callbacks = new HashMap<>();
      }
      callbacks.put(callbackName.toUpperCase(), cbd);
      
      return logical.of(true);
   }
   
   /**
    * Associates an internal procedure with an ABL callback event.
    *
    * @param   eventName
    *          The eventName of the event.
    * @param   callback
    *          The eventName of the internal procedure associated with the callback event.
    * @param   context
    *          A handle to a procedure that contains the internal procedure specified by
    *          {@code callback}. If not specified, {@code THIS-PROCEDURE} is used as the procedure
    *          context.
    *
    * @return  {@code true} when the method ends with success.
    */
   @Override
   public logical setCallbackProcedure(String eventName, String callback, handle context)
   {
      if (!isCallbackName(eventName))
      {
         return logical.of(false);
      }
      
      if (callback == null)
      {
         return logical.of(true);
      }
      
      if (callback.isEmpty())
      {
         if (callbacks != null)
         {
            callbacks.remove(eventName.toUpperCase());
         }
         return logical.of(true);
      }
      
      if (context == null || context.isUnknown())
      {
         // defaulting to THIS-PROCEDURE
         context = pm.thisProcedure();
      }
      else
      {
         if (!context._isValid() || !(context.getResource() instanceof PersistentProcedure))
         {
            ErrorManager.recordOrShowError(13273);
            // Third argument to SET-CALLBACK must be a valid procedure handle or object reference.
            return logical.of(false);
         }
      }
      
      InvokeConfig callSite = getCallSite(eventName);
      if (callbacks == null)
      {
         callbacks = new HashMap<>();
      }
      callbacks.put(eventName.toUpperCase(), new CallbackData(callback, context, null, callSite));
      return logical.of(true);
   }
   
   /**
    * Returns the value of the the NAMESPACE-URI attribute.
    *
    * @return See above.
    */
   @Override
   public character namespaceURI()
   {
      return new character(nsUri);
   }
   
   /**
    * Setter for NAMESPACE-URI attribute.
    *
    * @param uri The new value of the NAMESPACE-URI attribute.
    */
   @Override
   public void namespaceURI(String uri)
   {
      if (uri == null)
      {
         unableToAssignUnknown("NAMESPACE-URI", "DATASET widget");
         return;
      }
      
      nsUri = uri;
   }
   
   /**
    * Setter for NAMESPACE-URI attribute.
    *
    * @param uri The new value of the NAMESPACE-URI attribute.
    */
   @Override
   public void namespaceURI(character uri)
   {
      namespaceURI(uri == null || uri.isUnknown() ? "" : uri.toJavaType());
   }
   
   /**
    * Obtain the name of the XML element or attribute representing the target object (a
    * {@code DataSet}) name in an XML document. If not set the {@code name} is returned. If the
    * {@code name} is empty {@code "ProDataSet"} is returned.
    *
    * @return  the name of the XML element for this object.
    */
   @Override
   public character getXmlNodeName()
   {
      return new character(getXmlNodeNameInternal());
   }
   
   /**
    * Obtain the name of the XML element or attribute representing the target object. May return
    * {@code null}, unlike the {@code getXmlNodeName()} attribute implementation.
    *
    * @return  the name of the XML element for this object.
    */
   public String getXmlNodeNameInternal()
   {
      return xmlNodeName != null ? xmlNodeName :
             serializeName != null ? serializeName :
             !name.isEmpty() ? name : "ProDataSet";
   }
   
   /**
    * Returns the value of the the NAMESPACE-URI attribute as String.
    *
    * @return See above.
    */
   public String getNsUri()
   {
      return nsUri == null ? "" : nsUri;
   }
   /**
    * Sets the name of the XML element or attribute representing the target object (either a
    * {@code DataSet}, a {@code TempTable}, a {@code TemporaryBuffer}, or a temp-table
    * {@code BufferField}) name in an XML document.
    *
    * @param   name
    *          The new name of the XML element for this object.
    */
   @Override
   public void setXmlNodeName(String name)
   {
      if (name != null)
      {
         xmlNodeName = name;
      }
      // no error is raised otherwise
   }
   
   /**
    * Sets the name of the XML element or attribute representing the target object (either a
    * {@code DataSet}, a {@code TempTable}, a {@code TemporaryBuffer}, or a temp-table
    * {@code BufferField}) name in an XML document.
    *
    * @param   name
    *          The new name of the XML element for this object.
    */
   @Override
   public void setXmlNodeName(Text name)
   {
      setXmlNodeName(name.toJavaType());
   }
   
   /**
    * Implementation of the read access of the {@code SERIALIZE-NAME} attribute.
    *
    * @return the current value of the {@code SERIALIZE-NAME} attribute.
    */
   @Override
   public character getSerializeName()
   {
      String jsName = serializeName != null ? serializeName : name;
      if (jsName == null || jsName.isEmpty())
      {
         jsName = "ProDataSet";
      }
      
      return new character(jsName);
   }
   
   /**
    * Implementation of the write access of the {@code SERIALIZE-NAME} attribute.
    *
    * @param sName the new value for the {@code SERIALIZE-NAME} attribute.
    */
   @Override
   public void setSerializeName(Text sName)
   {
      setSerializeName(sName.toJavaType());
   }
   
   /**
    * Implementation of the write access of the {@code SERIALIZE-NAME} attribute.
    *
    * @param sName the new value for the {@code SERIALIZE-NAME} attribute.
    */
   @Override
   public void setSerializeName(String sName)
   {
      if (sName != null)
      {
         serializeName = sName;
      }
      // no error is raised otherwise
   }
   
   /**
    * Returns the state of the ERROR attribute
    *
    * @return Current state of the ERROR attribute.
    */
   @Override
   public logical error()
   {
      return logical.of(error);
   }
   
   /**
    * Setter for ERROR attribute.
    *
    * @param value The new value of ERROR attribute.
    */
   @Override
   public void error(boolean value)
   {
      this.error = value;
   }
   
   /**
    * Setter for ERROR attribute.
    *
    * @param value The new value of ERROR attribute.
    */
   @Override
   public void error(logical value)
   {
      if (value == null || value.isUnknown())
      {
         unableToAssignUnknown("ERROR", "DATASET widget");
         return;
      }
      error(value.getValue());
   }
   
   /**
    * Returns the state of the REJECTED attribute
    *
    * @return Current state of the REJECTED attribute.
    */
   @Override
   public logical rejected()
   {
      return logical.of(rejected);
   }
   
   /**
    * Setter for REJECTED attribute.
    *
    * @param value The new value of REJECTED attribute.
    */
   @Override
   public void rejected(boolean value)
   {
      this.rejected = value;
   }
   
   /**
    * Setter for REJECTED attribute.
    *
    * @param value The new value of REJECTED attribute.
    */
   @Override
   public void rejected(logical value)
   {
      if (value == null || value.isUnknown())
      {
         unableToAssignUnknown("REJECTED", "DATASET widget");
         return;
      }
      rejected(value.getValue());
   }
   
   /**
    * Reports if this object is valid for use. The {@code DataSet} is valid 
    *
    * @return {@code true} if we are valid (can be used).
    */
   @Override
   public boolean valid()
   {
      return !buildInProgress && !deleted;
   }
   
   /**
    * Get the value of the ADM-DATA attribute.
    *
    * @return See above.
    */
   @Override
   public character getADMData()
   {
      return new character(admData);
   }
   
   /**
    * Set the value of the ADM-DATA attribute.
    *
    * @param value The new value.
    */
   @Override
   public void setADMData(String value)
   {
      this.admData = value;
   }
   
   /**
    * Set the value of the ADM-DATA attribute.
    *
    * @param value The new value.
    */
   @Override
   public void setADMData(character value)
   {
      this.admData = value.toJavaType();
   }
   
   /**
    * Empties this {@code DataSet} object of all records in its associated temp-tables.
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical emptyDataset()
   {
      // TODO: When you invoke this method, the ABL Virtual Machine (AVM) sets the ORIGIN-HANDLE attribute on
      //       any changed temp-tables corresponding to the source temp-tables of this DataSet to the Unknown
      //       value (?).
      //       If TRACKING-CHANGES is true, the AVM raises an error.
      
      for (int i = 0; i < buffers.size(); i++)
      {
         BufferImpl buffer = buffers.get(i);
         buffer.deleteAll();
      }
      
      error(false); // clear the ERROR attribute in the process
      return logical.of(true);
   }
   
   /**
    * Gets the handle of the specified data-relation object.
    *
    * @return  the handle of the specified data-relation object.
    */
   @Override
   public handle getRelation()
   {
      return getRelation(1);
   }
   
   /**
    * Gets the handle of the specified data-relation object.
    *
    * @return  the handle of the specified data-relation object.
    */
   public DataRelation getRelationNative()
   {
      return getRelationNative(1);
   }
   
   /**
    * Gets the handle of the specified data-relation object.
    *
    * @param   index
    *          The index (1-based) of the data-relation to be returned.
    *
    * @return  the handle of the specified data-relation object.
    */
   @Override
   public handle getRelation(NumberType index)
   {
      if (index == null || index.isUnknown())
      {
         return new handle();
      }
      
      return getRelation(index.longValue());
   }
   
   /**
    * Gets the handle of the specified data-relation object.
    *
    * @param   index
    *          The index (1-based) of the data-relation to be returned.
    *
    * @return  the handle of the specified data-relation object.
    */
   @Override
   public handle getRelation(long index)
   {
      return new handle(getRelationNative(index));
   }
   
   /**
    * Gets the handle of the specified data-relation object.
    *
    * @param   index
    *          The index (1-based) of the data-relation to be returned.
    *
    * @return  the handle of the specified data-relation object.
    */
   public DataRelation getRelationNative(long index)
   {
      if (relations.isEmpty())
      {
         return null;
      }
      
      return index - 1 >= relations.size() ? null : relations.get((int) index - 1);
   }
   
   /**
    * Get relations as stream.
    *
    * @return  relations as stream
    */
   public java.util.stream.Stream<DataRelation> getRelations()
   {
      return relations.stream();
   }
   
   /**
    * Gets the handle of the specified data-relation object.
    *
    * @param   name
    *          The name of the data-relation to be returned.
    *
    * @return  the handle of the specified data-relation object.
    */
   @Override
   public handle getRelation(Text name)
   {
      if (name == null || name.isUnknown())
      {
         return new handle();
      }
   
      return getRelation(name.getValue());
   }
   
   /**
    * Gets the handle of the specified data-relation object.
    *
    * @param   name
    *          The name of the data-relation to be returned.
    *
    * @return  the handle of the specified data-relation object.
    */
   @Override
   public handle getRelation(String name)
   {
      for (int i = 0; i < relations.size(); i++)
      {
         DataRelation relation = relations.get(i);
         if (relation.name().toStringMessage().equalsIgnoreCase(name))
         {
            return new handle(relation);
         }
      }
      return new handle();
   }
   
   /**
    * Gets the top-level buffer of this {@code DataSet} object at the specified index position.
    *
    * @return  the top-level buffer at the position 1.
    */
   @Override
   public handle getTopBuffer()
   {
      return getTopBuffer(1);
   }
   
   /**
    * Gets the top-level buffer of this {@code DataSet} object at the specified index position.
    *
    * @param   index
    *          The 1-based index of the top-level buffer.
    *
    * @return  the top-level buffer at the specified index position.
    */
   @Override
   public handle getTopBuffer(long index)
   {
      return new handle(getTopBufferNative(index));
   }

   /**
    * Gets the top-level buffer of this {@code DataSet} object at the specified index position.
    *
    * @param   index
    *          The 1-based index of the top-level buffer.
    *
    * @return  the top-level buffer at the specified index position.
    */
   public BufferImpl getTopBufferNative(long index)
   {
      if (index < 1)
      {
         return null;
      }
      
      List<Buffer> candidates = getTopBufferList();
      if (_getNumTopBuffers() != candidates.size())
      {
         // well, this should NEVER happen!
         throw new RuntimeException("The size of TOP-BUFFER list does not match NUM-TOP-BUFFER");
      }
      
      return index - 1 >= candidates.size() ? null : (BufferImpl) candidates.get((int) (index - 1));
   }

   /**
    * Gets the top-level buffer of this {@code DataSet} object at the specified index position.
    *
    * @param   index
    *          The 1-based index of the top-level buffer.
    *
    * @return  the top-level buffer at the specified index position.
    */
   @Override
   public handle getTopBuffer(NumberType index)
   {
      if (index == null || index.isUnknown())
      {
         return new handle();
      }
      return getTopBuffer(index.longValue());
   }
   
   /**
    * Obtain default navigation query for a top-level buffer of this {@code DataSet} object.
    * This method correspond to the getter side of {@code TOP-NAV-QUERY} ABL attribute. 
    *
    * @return  the default navigation query for a top-level buffer.
    */
   @Override
   public handle getTopNavQuery()
   {
      return getTopNavQuery(1);
   }
   
   /**
    * Obtain default navigation query for a top-level buffer of this {@code DataSet} object.
    * This method correspond to the getter side of {@code TOP-NAV-QUERY} ABL attribute. 
    *
    * @param   index
    *          The 1-based index of the top-level buffer.
    *
    * @return  the default navigation query for a top-level buffer.
    */
   @Override
   public handle getTopNavQuery(long index)
   {
      if (index < 0 || index > _getNumTopBuffers())
      {
         return new handle();
      }
      
      BufferImpl topBuffer = getTopBufferNative(index);
      if (topBuffer == null)
      {
         return new handle();
      }
      return getTopNavQuery(topBuffer);
   }
   
   /**
    * Obtain default navigation query for a top-level buffer of this {@code DataSet} object.
    * This method correspond to the getter side of {@code TOP-NAV-QUERY} ABL attribute. 
    *
    * @param   index
    *          The 1-based index of the top-level buffer.
    *
    * @return  the default navigation query for a top-level buffer.
    */
   @Override
   public handle getTopNavQuery(NumberType index)
   {
      if (index == null || index.isUnknown())
      {
         return new handle();
      }
      
      return getTopBuffer(index.intValue());
   }
   
   /**
    * Obtain default navigation query for a top-level buffer of this {@code DataSet} object.
    * This method correspond to the getter side of {@code TOP-NAV-QUERY} ABL attribute. 
    *
    * @param   bufName
    *          The name of the top-level buffer.
    *
    * @return  the default navigation query for a top-level buffer.
    */
   @Override
   public handle getTopNavQuery(String bufName)
   {
      return getTopNavQuery(getBufferByName(bufName));
   }
   
   /**
    * Obtain default navigation query for a top-level buffer of this {@code DataSet} object.
    * This method correspond to the getter side of {@code TOP-NAV-QUERY} ABL attribute. 
    *
    * @param   bufName
    *          The name of the top-level buffer.
    *
    * @return  the default navigation query for a top-level buffer.
    */
   @Override
   public handle getTopNavQuery(Text bufName)
   {
      if (bufName == null || bufName.isUnknown())
      {
         return new handle();
      }
      
      return getTopNavQuery(bufName.getValue());
   }
   
   /**
    * Configures the default navigation query for a top-level buffer of this {@code DataSet}
    * object. This method correspond to the setter side of {@code TOP-NAV-QUERY} ABL attribute. 
    *
    * @param   queryHandle
    *          The new default navigation query for a top-level buffer.
    */
   @Override
   public void setTopNavQuery(handle queryHandle)
   {
      setTopNavQuery(1, queryHandle);
   }
   
   /**
    * Configures the default navigation query for a top-level buffer of this {@code DataSet}
    * object. This method correspond to the setter side of {@code TOP-NAV-QUERY} ABL attribute. 
    *
    * @param   index
    *          The 1-based index of the top-level buffer.
    * @param   queryHandle
    *          The new default navigation query for a top-level buffer.
    */
   @Override
   public void setTopNavQuery(long index, handle queryHandle)
   {
      if (index < 1 || index > _getNumTopBuffers())
      {
         invalidTopNavQuery(4056, "");
         return;
      }
      
      if (queryHandle.getResource() == null)
      {
         // silently accept invalid values
         return;
      }
      
      BufferImpl topBuffer = getTopBufferNative(index);
      _setTopBufferQuery(queryHandle, topBuffer);
   }
   
   /**
    * Configures the default navigation query for a top-level buffer of this {@code DataSet}
    * object. This method correspond to the setter side of {@code TOP-NAV-QUERY} ABL attribute. 
    *
    * @param   index
    *          The 1-based index of the top-level buffer.
    * @param   queryHandle
    *          The new default navigation query for a top-level buffer.
    */
   @Override
   public void setTopNavQuery(NumberType index, handle queryHandle)
   {
      setTopNavQuery((index == null || index.isUnknown()) ? -1 : index.longValue(), queryHandle);
   }
   
   /**
    * Configures the default navigation query for a top-level buffer of this {@code DataSet}
    * object. This method correspond to the setter side of {@code TOP-NAV-QUERY} ABL attribute. 
    *
    * @param   bufName
    *          The name of the top-level buffer.
    * @param   queryHandle
    *          The new default navigation query for a top-level buffer.
    */
   @Override
   public void setTopNavQuery(String bufName, handle queryHandle)
   {
      if (queryHandle.getResource() == null)
      {
         // silently accept invalid values
         return;
      }
      
      _setTopBufferQuery(queryHandle, getBufferByName(bufName));
   }
   
   /**
    * Configures the default navigation query for a top-level buffer of this {@code DataSet}
    * object. This method correspond to the setter side of {@code TOP-NAV-QUERY} ABL attribute. 
    *
    * @param   bufName
    *          The name of the top-level buffer.
    * @param   queryHandle
    *          The new default navigation query for a top-level buffer.
    */
   @Override
   public void setTopNavQuery(Text bufName, handle queryHandle)
   {
      setTopNavQuery(bufName.toJavaType(), queryHandle);
   }
   
   /**
    * Checks whether <strong>AT LEAST</strong> one data-relation objects in this {@code DataSet} object is
    * active. All data-relation objects in a {@code DataSet} object are active by default.
    *
    * @return  {@code true} when all data-relation objects are active.
    */
   @Override
   public logical areRelationsActive()
   {
      return logical.of(_areRelationsActive());
   }
   
   /**
    * Configures whether all data-relation objects in this {@code DataSet} object to be active or
    * inactive. Setting this attribute to {@code TRUE} will activate all data-relation objects.
    * Setting it to {@code FALSE} will deactivate all data-relation objects instead.
    *
    * @param   active
    *          The new value for {@code RELATIONS-ACTIVE} attribute.
    */
   @Override
   public void setRelationsActive(logical active)
   {
      if (active.isUnknown())
      {
         unableToAssignUnknown("RELATIONS-ACTIVE", "DATASET widget");
         return;
      }
      
      setRelationsActive(active.getValue());
   }
   
   /**
    * Configures whether all data-relation objects in this {@code DataSet} object to be active or
    * inactive. Setting this attribute to {@code TRUE} will activate all data-relation objects.
    * Setting it to {@code FALSE} will deactivate all data-relation objects instead.
    *
    * @param   active
    *          The new value for {@code RELATIONS-ACTIVE} attribute.
    */
   @Override
   public void setRelationsActive(boolean active)
   {
      if (_areRelationsActive() == active)
      {
         // 'nothing' to do.
         // NOTE: this is a bad assumption but is how ABL works!
         return;
      }
      
      if (relations.isEmpty())
      {
         defaultActive = active;
         return;
      }
      
      // check first allowsActiveChild() for all inactive relations
      if (active)
      {
         List<Integer> childBuffers = new LinkedList<>();
         for (int i = 0; i < relations.size(); i++)
         {
            DataRelation rel = relations.get(i);
            Buffer cb = rel.getChildBufferNative();
            if (cb == null)
            {
               continue;
            }
            int uuid = cb.getUniqueID().intValue();
            if (childBuffers.contains(uuid))
            {
               ErrorManager.recordOrShowError(11979, cb.name().toStringMessage());
               // Buffer <name> can have only 1 active parent relation.
               return;
            }
            
            childBuffers.add(uuid);
         }
      }
      
      for (int i = 0; i < relations.size(); i++)
      {
         DataRelation rel = relations.get(i);
         rel.setActive(active);
      }
   }
   
   /**
    * Get the number of data-relation objects of this {@code DataSet} object.
    *
    * @return  the number of data-relation objects of this {@code DataSet} object.
    */
   @Override
   public integer getNumRelations()
   {
      return new integer(relations.size());
   }
   
   /**
    * Obtain the number of top-level buffers in this {@code DataSet} object.
    * 
    * @return  the number of top-level buffers in this {@code DataSet} object.
    */
   @Override
   public integer getNumTopBuffers()
   {
      return new integer(_getNumTopBuffers());
   }
   
   /**
    * Creates a new data-relation object for a pair of parent and child buffers of this {@code DataSet}
    * object. This method applies only to {@code DataSet} objects dynamically created. 
    *
    * @param   parent
    *          The handle to the parent buffer in the data-relation object.
    * @param   child
    *          The handle to the child buffer in the data-relation object.
    * @param   pairs
    *          A comma-delimited list of parent-field, child-field pairs describing the relationship between
    *          parent and child buffers in the new data-relation object.
    *
    * @return  A {@code handle} to the newly created {@code DataRelation}.
    */
   @Override
   public handle addRelation(handle parent, handle child, Text pairs)
   {
      return addRelationImpl(parent, child, pairs, null, null, null, null, null, false);
   }
   
   /**
    * Creates a new data-relation object for a pair of parent and child buffers of this {@code DataSet}
    * object. This method applies only to {@code DataSet} objects dynamically created. 
    *
    * @param   parent
    *          The handle to the parent buffer in the data-relation object.
    * @param   child
    *          The handle to the child buffer in the data-relation object.
    * @param   pairs
    *          A comma-delimited list of parent-field, child-field pairs describing the relationship between
    *          parent and child buffers in the new data-relation object.
    * @param   reposition
    *          The reposition mode. If {@code TRUE}, the relation mode is {@code REPOSITION}.
    *          Otherwise the relation mode is {@code SELECTION}. The default value is {@code SELECTION}.
    *
    * @return  A {@code handle} to the newly created {@code DataRelation}.
    */
   @Override
   public handle addRelation(handle parent, handle child, Text pairs, logical reposition)
   {
      return addRelationImpl(parent, child, pairs, reposition, null, null, null, null, false);
   }
   
   /**
    * Creates a new data-relation object for a pair of parent and child buffers of this {@code DataSet}
    * object. This method applies only to {@code DataSet} objects dynamically created.
    *
    * @param   parent
    *          The handle to the parent buffer in the data-relation object.
    * @param   child
    *          The handle to the child buffer in the data-relation object.
    * @param   pairs
    *          A comma-delimited list of parent-field, child-field pairs describing the relationship between
    *          parent and child buffers in the new data-relation object.
    * @param   reposition
    *          The reposition mode. If {@code TRUE}, the relation mode is {@code REPOSITION}.
    *          Otherwise the relation mode is {@code SELECTION}. The default value is {@code SELECTION}.
    * @param   nested
    *          Use {@code TRUE} to nest child rows of {@code DataSet} buffers within their parent
    *          rows when writing the XML representation of data. When {@code FALSE} is passed, all
    *          child rows are written after all parent rows are written. The default value is {@code FALSE}.
    *
    * @return  A {@code handle} to the newly created {@code DataRelation}.
    */
   @Override
   public handle addRelation(handle parent,
                             handle child,
                             Text pairs,
                             logical reposition,
                             logical nested)
   {
      return addRelationImpl(parent, child, pairs, reposition, nested, null, null, null, false);
   }
   
   /**
    * Creates a new data-relation object for a pair of parent and child buffers of this {@code DataSet}
    * object. This method applies only to {@code DataSet} objects dynamically created.
    *
    * @param   parent
    *          The handle to the parent buffer in the data-relation object.
    * @param   child
    *          The handle to the child buffer in the data-relation object.
    * @param   pairs
    *          A comma-delimited list of parent-field, child-field pairs describing the relationship between
    *          parent and child buffers in the new data-relation object.
    * @param   reposition
    *          The reposition mode. If {@code TRUE}, the relation mode is {@code REPOSITION}.
    *          Otherwise the relation mode is {@code SELECTION}. The default value is {@code SELECTION}.
    * @param   nested
    *          Use {@code TRUE} to nest child rows of {@code DataSet} buffers within their parent
    *          rows when writing the XML representation of data. When {@code FALSE} is passed, all
    *          child rows are written after all parent rows are written. The default value is {@code FALSE}.
    * @param   active
    *          Use {@code false} to make the newly created relation inactive.
    *
    * @return  A {@code handle} to the newly created {@code DataRelation}.
    */
   @Override
   public handle addRelation(handle parent,
                             handle child,
                             Text pairs,
                             logical reposition,
                             logical nested,
                             logical active)
   {
      return addRelationImpl(parent, child, pairs, reposition, nested, active, null, null, false);
   }
   
   /**
    * Creates a new data-relation object for a pair of parent and child buffers of this {@code DataSet}
    * object. This method applies only to {@code DataSet} objects dynamically created.
    *
    * @param   parent
    *          The handle to the parent buffer in the data-relation object.
    * @param   child
    *          The handle to the child buffer in the data-relation object.
    * @param   pairs
    *          A comma-delimited list of parent-field, child-field pairs describing the relationship between
    *          parent and child buffers in the new data-relation object.
    * @param   reposition
    *          The reposition mode. If {@code TRUE}, the relation mode is {@code REPOSITION}.
    *          Otherwise the relation mode is {@code SELECTION}. The default value is {@code SELECTION}.
    * @param   nested
    *          Use {@code TRUE} to nest child rows of {@code DataSet} buffers within their parent
    *          rows when writing the XML representation of data. When {@code FALSE} is passed, all
    *          child rows are written after all parent rows are written. The default value is {@code FALSE}.
    * @param   active
    *          Use {@code false} to make the newly created relation inactive. 
    * @param   recursive
    *          Using {@code TRUE} till causes the data-relation to be filled recursively.
    *
    * @return  A {@code handle} to the newly created {@code DataRelation}.
    */
   @Override
   public handle addRelation(handle parent,
                             handle child,
                             Text pairs,
                             logical reposition,
                             logical nested,
                             logical active,
                             logical recursive)
   {
      return addRelationImpl(parent, child, pairs, reposition, nested, active, recursive, null, false);
   }
   
   /**
    * Creates a new data-relation object for a pair of parent and child buffers of this {@code DataSet}
    * object. This method applies only to {@code DataSet} objects dynamically created.
    * <p>
    * <b>Note:</b><br>
    *    There is a bug/quirk in OE when this is performed. If a relation (for same buffer pair) is added a
    *    second time, it is accepted, leading to two "parallel" relations to be created. FWD will warn this in
    *    log. When/if OE decides to fix the bug/quirk, the handling code should also be removed.
    *
    * @param   parent
    *          The handle to the parent buffer in the data-relation object.
    * @param   child
    *          The handle to the child buffer in the data-relation object.
    * @param   pairs
    *          A comma-delimited list of parent-field, child-field pairs describing the relationship between
    *          parent and child buffers in the new data-relation object.
    * @param   reposition
    *          The reposition mode. If {@code TRUE}, the relation mode is {@code REPOSITION}.
    *          Otherwise the relation mode is {@code SELECTION}. The default value is {@code SELECTION}.
    * @param   nested
    *          Use {@code TRUE} to nest child rows of {@code DataSet} buffers within their parent
    *          rows when writing the XML representation of data. When {@code FALSE} is passed, all
    *          child rows are written after all parent rows are written. The default value is {@code FALSE}.
    * @param   active
    *          Use {@code false} to make the newly created relation inactive. 
    * @param   recursive
    *          Using {@code TRUE} till causes the data-relation to be filled recursively.
    * @param   fkeyHidden
    *          When set to {@code TRUE} the foreign key fields in nested data-relations will be omitted when
    *          writing the XML representation of data. If this option is used, the {@code NESTED} argument
    *          must be {@code TRUE}.
    *
    * @return  A {@code handle} to the newly created {@code DataRelation}.
    */
   @Override
   public handle addRelation(handle parent,
                             handle child,
                             Text pairs,
                             logical reposition,
                             logical nested,
                             logical active,
                             logical recursive,
                             logical fkeyHidden)
   {
      return addRelationImpl(parent, child, pairs, reposition, nested, active, recursive, fkeyHidden, false); 
   }
   
   /**
    * Actual implementation of {@code addRelation}.<p>
    *
    * Creates a new data-relation object for a pair of parent and child buffers of this {@code DataSet}
    * object. This method applies only to {@code DataSet} objects dynamically created.
    * <p>
    * <b>Note:</b><br>
    *    There is a bug/quirk in OE when this is performed. If a relation (for same buffer pair) is added a
    *    second time, it is accepted, leading to two "parallel" relations to be created. FWD will warn this in
    *    log. When/if OE decides to fix the bug/quirk, the handling code should also be removed.
    *
    * @param   parent
    *          The handle to the parent buffer in the data-relation object.
    * @param   child
    *          The handle to the child buffer in the data-relation object.
    * @param   pairs
    *          A comma-delimited list of parent-field, child-field pairs describing the relationship between
    *          parent and child buffers in the new data-relation object.
    * @param   reposition
    *          The reposition mode. If {@code TRUE}, the relation mode is {@code REPOSITION}.
    *          Otherwise the relation mode is {@code SELECTION}. The default value is {@code SELECTION}.
    * @param   nested
    *          Use {@code TRUE} to nest child rows of {@code DataSet} buffers within their parent
    *          rows when writing the XML representation of data. When {@code FALSE} is passed, all
    *          child rows are written after all parent rows are written. The default value is {@code FALSE}.
    * @param   active
    *          Use {@code false} to make the newly created relation inactive. 
    * @param   recursive
    *          Using {@code TRUE} till causes the data-relation to be filled recursively.
    * @param   fkeyHidden
    *          When set to {@code TRUE} the foreign key fields in nested data-relations will be omitted when
    *          writing the XML representation of data. If this option is used, the {@code NESTED} argument
    *          must be {@code TRUE}.
    * @param   nullOnDuplicate
    *          if {@code true} the method will return {@code null} if a duplicate relation is detected.
    *          Otherwise it will throw error condition #11979, eventually returning invalid handle.
    *
    * @return  A {@code handle} to the newly created {@code DataRelation}.
    */
   public handle addRelationImpl(handle parent,
                                 handle child,
                                 Text pairs,
                                 logical reposition,
                                 logical nested,
                                 logical active,
                                 logical recursive,
                                 logical fkeyHidden, 
                                 boolean nullOnDuplicate)
   {
      if (parent == null || parent.isUnknown() || !parent.isType(LegacyResource.BUFFER) ||
          child == null  || child.isUnknown()  || !child.isType(LegacyResource.BUFFER))
      {
         ErrorManager.recordOrShowError(11955);
         // The argument to Dataset-handle:ADD-RELATION method was invalid.
         return new handle();
      }
      
      BufferImpl bufParent = ((BufferImpl) parent.getResource()).ref();
      BufferImpl bufChild = ((BufferImpl) child.getResource()).ref();
      boolean throw11979 = false;
      
      if (!buffers.contains(bufParent) || !buffers.contains(bufChild))
      {
         ErrorManager.recordOrShowError(11961);
         // Buffers in ADD-RELATION must be members of the dataset.
         return new handle();
      }
      
      boolean bActive = _areRelationsActive();
      if (active != null && !active.isUnknown())
      {
         // if this flag is not specified, use the negated value of RELATIONS-ACTIVE 
         bActive = active.getValue();
      }
      
      // check whether a new relation can be added at this moment for the child buffer. If the new relation is
      // inactive it can be added anyway
      if (bActive)
      {
         DataRelation dr = allowsActiveChild(bufChild);
         if (dr != null)
         {
            //Note: There is a bug/quirk in OE when this is performed. If a relation (for same buffer pair) is
            //      added a second time, it is accepted, leading to two "parallel" relations to be created.
            //      FWD will warn this in * log. When/if OE decides to fix the bug/quirk, this code must be
            //      removed or conditioned on emulated OE version
            BufferImpl existingParent = dr.getParentBufferNative();
            if (existingParent != bufParent)
            {
               ErrorManager.recordOrShowError(11979, bufChild.doGetName());
               // Buffer <name> can have only 1 active parent relation.
               return new handle();
            }
            else
            {
               if (nullOnDuplicate)
               {
                  return null;
               }
               throw11979 = true;
            }
         }
      }
      
      if (pairs == null || pairs.isUnknown())
      {
         ErrorManager.recordOrShowError(11964);
         // ADD-RELATION join pairs list was not valid.
         return new handle();
      }
      
      if (bufParent == bufChild)
      {
         if (!recursive.booleanValue())
         {
            ErrorManager.recordOrShowError(11965);
            // Attempt to relate dataset buffer <name> to parent with itself in ancestry.
            return new handle();
         }
      }
      
      String strPairs = pairs.getValue();
      String[] fields = strPairs.split(",");
      if (fields.length == 0 || fields.length == 1 && fields[0].isEmpty())
      {
         ErrorManager.recordOrShowError(11964);
         // ADD-RELATION join pairs list was not valid.
         return new handle();
      }
      else if (fields.length % 2 == 1)
      {
         ErrorManager.recordOrShowError(11962);
         // Odd number of join pairs in ADD-RELATON.
         return new handle();
      }
      
      for (int i = 0; i < fields.length; i += 2)
      {
         // make sure the legacy names are in lowercase (BUT why?)
         String fParent = fields[i];
         String fChild = fields[i + 1];
         String errfld = null;
         
         if (!bufParent.hasField(fParent))
         {
            errfld = fParent;
         }
         else if (!bufChild.hasField(fChild)) 
         {
            errfld = fChild;
         }
         
         if (errfld != null)
         {
            ErrorManager.recordOrShowError(11963, errfld);
            // ADD-RELATION join pairs list entry <name> not valid--list must have unqualified 
            // field names with no embedded blanks, in parent-fld,child-fld order, e.g. 'cust-num,cust-num'.
            return new handle();
         }
      }
      
      boolean bRepos = reposition != null && !reposition.isUnknown() && reposition.getValue();
      boolean bNested = nested != null && !nested.isUnknown() && nested.getValue();
      boolean bRecurs = recursive != null && !recursive.isUnknown() && recursive.getValue();
      boolean bfkHidden = fkeyHidden != null && !fkeyHidden.isUnknown() && fkeyHidden.getValue();
      
      handle ret = addRelationWorker(null, bufParent, bufChild, fields, strPairs,
                                     bRepos, bNested, bActive, bRecurs, bfkHidden, true);
      
      if (throw11979)
      {
         ErrorManager.recordOrShowError(11979, bufChild.doGetName());
         // Buffer <name> can have only 1 active parent relation.
         return new handle();
      }
      
      return ret;
   }
   
   /**
    * Creates a new data-relation object for a pair of parent and child buffers of this {@code DataSet} based
    * on the {@code RECID} of the parent buffer to this {@code DataSet} object. This method applies only to
    * {@code DataSet} objects dynamically created. The newly created object will have the
    * {@code PARENT-ID-RELATION} attribute set to {@code TRUE}.
    *
    * @param   parent
    *          The handle to the parent buffer in the data-relation object.
    * @param   child
    *          The handle to the child buffer in the data-relation object.
    *
    * @return  A handle to the newly created {@link DataRelation} object.
    */
   @Override
   public handle addParentIdRelation(handle parent, handle child)
   {
      return addParentIdRelation(parent, child, null, null, null);
   }
   
   /**
    * Creates a new data-relation object for a pair of parent and child buffers of this {@code DataSet} based
    * on the {@code RECID} of the parent buffer to this {@code DataSet} object. This method applies only to
    * {@code DataSet} objects dynamically created. The newly created object will have the
    * {@code PARENT-ID-RELATION} attribute set to {@code TRUE}.
    *
    * @param   parent
    *          The handle to the parent buffer in the data-relation object.
    * @param   child
    *          The handle to the child buffer in the data-relation object.
    * @param   parentIdField
    *          Denotes the {@code RECID} field in child-buffer-handle on which this
    *          parent-id-relation object is based.
    *
    * @return  A handle to the newly created {@link DataRelation} object.
    */
   @Override
   public handle addParentIdRelation(handle parent, handle child, Text parentIdField)
   {
      return addParentIdRelation(parent, child, parentIdField, null, null);
   }
   
   /**
    * Creates a new data-relation object for a pair of parent and child buffers of this {@code DataSet} based
    * on the {@code RECID} of the parent buffer to this {@code DataSet} object. This method applies only to
    * {@code DataSet} objects dynamically created. The newly created object will have the
    * {@code PARENT-ID-RELATION} attribute set to {@code TRUE}.
    *
    * @param   parent
    *          The handle to the parent buffer in the data-relation object.
    * @param   child
    *          The handle to the child buffer in the data-relation object.
    * @param   parentIdField
    *          Denotes the {@code RECID} field in child-buffer-handle on which this
    *          parent-id-relation object is based.
    * @param   parentFieldsBefore
    *          A list of comma-separated fields from the parent record to be written to XML before
    *          any nested child-buffer-handle records.
    *
    * @return  A handle to the newly created {@link DataRelation} object.
    */
   @Override
   public handle addParentIdRelation(handle parent,
                                     handle child,
                                     Text parentIdField,
                                     Text parentFieldsBefore)
   {
      return addParentIdRelation(parent, child, parentIdField, parentFieldsBefore, null);
   }
   
   /**
    * Creates a new data-relation object for a pair of parent and child buffers of this {@code DataSet} based
    * on the {@code RECID} of the parent buffer to this {@code DataSet} object. This method applies only to
    * {@code DataSet} objects dynamically created. The newly created object will have the
    * {@code PARENT-ID-RELATION} attribute set to {@code TRUE}.
    *
    * @param   parent
    *          The handle to the parent buffer in the data-relation object.
    * @param   child
    *          The handle to the child buffer in the data-relation object.
    * @param   parentIdField
    *          Denotes the {@code RECID} field in child-buffer-handle on which this
    *          parent-id-relation object is based. The default value is the name of the
    *          parent-buffer-handle appended with {@code _id}.
    * @param   parentFieldsBefore
    *          A list of comma-separated fields from the parent record to be written to XML before
    *          any nested child-buffer-handle records.
    * @param   parentFieldsAfter
    *          A list of comma-separated fields from the parent record to be written to XML after
    *          all nested child-buffer-handle records.
    *
    * @return  A handle to the newly created {@link DataRelation} object.
    */
   @Override
   public handle addParentIdRelation(handle parent,
                                     handle child,
                                     Text parentIdField,
                                     Text parentFieldsBefore,
                                     Text parentFieldsAfter)
   {
      // validate buffers:
      if (parent == null || parent.isUnknown() || !parent.isType(LegacyResource.BUFFER) ||
          child == null  || child.isUnknown()  || !child.isType(LegacyResource.BUFFER))
      {
         ErrorManager.recordOrShowError(16025);
         // The argument to Dataset-handle:ADD-PARENT-ID-RELATION method was invalid.
         return new handle();
      }
      
      // validate link to parent:
      BufferImpl bufParent = ((BufferImpl) parent.getResource()).ref();
      BufferImpl bufChild = ((BufferImpl) child.getResource()).ref();
      
      if (!buffers.contains(bufParent) || !buffers.contains(bufChild))
      {
         ErrorManager.recordOrShowError(16027);
         // Buffers in ADD-PARENT-ID-RELATION must be members of the dataset.
         return new handle();
      }
      
//      if (!allowsActiveChild(bufChild))
//      {
//         return new handle();
//      }
      
      String parLink = null;
      if (parentIdField == null || parentIdField.isUnknown() || parentIdField.toJavaType().isEmpty())
      {
         parLink = bufParent._name() + "_id";
      }
      else
      {
         parLink = parentIdField.getValue();
      }
      
      // check if the field exist in the child buffer
      if (!bufChild.hasField(parLink))
      {
         ErrorManager.recordOrShowError(16028, parLink);
         // ADD-PARENT-ID-RELATION parent-id-field <field> is not valid.
         return new handle();
      }
      else
      {
         // and, if so, it is of the proper type (RECID)
         BufferField fparent = bufChild.bufferFieldNative(parLink);
         if (fparent == null || !"RECID".equalsIgnoreCase(fparent.getDataType().getValue()))
         {
            ErrorManager.recordOrShowError(16028, parLink);
            // ADD-PARENT-ID-RELATION parent-id-field <field> is not valid.
            return new handle();
         }
      }
      
      // validate before fields
      List<String> beforeFields = new ArrayList<>();
      if (parentFieldsBefore != null && !parentFieldsBefore.isUnknown())
      {
         String[] beforeStrFields = parentFieldsBefore.getValue().split(",");
         for (String field : beforeStrFields)
         {
            if (!bufParent.hasField(field))
            {
               ErrorManager.recordOrShowError(16029, field);
               // ADD-PARENT-ID-RELATION parent-fields-before entry <field> is not valid.
               return new handle();
            }
            beforeFields.add(field);
         }
      }
      
      List<String> afterFields = new ArrayList<>();
      // validate after fields
      if (parentFieldsAfter!= null && !parentFieldsAfter.isUnknown())
      {
         String[] afterStrFields = parentFieldsAfter.getValue().split(",");
         for (String field : afterStrFields)
         {
            if (!bufParent.hasField(field))
            {
               ErrorManager.recordOrShowError(16030, field);
               // ADD-PARENT-ID-RELATION parent-fields-after entry <field> is not valid.
               return new handle();
            }
            
            if (beforeFields.contains(field))
            {
               ErrorManager.recordOrShowError(16484, field);
               // Duplicate field <field> found in PARENT-FIELDS-BEFORE / PARENT-FIELDS-AFTER.
               return new handle();
            }
            
            afterFields.add(field);
         }
      }
      
      DataRelation dR = new DataRelation(this, bufParent, bufChild, parLink, beforeFields, afterFields, true);
      // the REPOSITION attribute might be set, depending on the OE version
      // dR.setReposition(true);
      dR.name("RELATION" + (relations.size() + 1));
      relations.add(dR);
      return new handle(dR);
   }
   
   /**
    * Copies a source {@code DataSet} object into this {@code DataSet} object.
    *
    * @param   srcDS
    *          The source {@code DataSet}.
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical copyDataset(handle srcDS)
   {
      return copyDatasetImpl(srcDS, false, false, false, null, false, "cpy");
   }
   
   /**
    * Copies a source {@code DataSet} object into this {@code DataSet} object.
    *
    * @param   srcDS
    *          The source {@code DataSet}.
    * @param   append
    *          When {@code true} the source temp-tables are appended to current temp-tables.
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical copyDataset(handle srcDS, logical append)
   {
      boolean isAppend = append != null && !append.isUnknown() && append.getValue();
      return copyDatasetImpl(srcDS, isAppend, false, false, null, false, "cpy");
   }
   
   /**
    * Copies a source {@code DataSet} object into this {@code DataSet} object.
    *
    * @param   srcDS
    *          The source {@code DataSet}.
    * @param   append
    *          When {@code true} the source temp-tables are appended to current temp-tables.
    * @param   replace
    *          When {@code true} the records in this temp-tables are replaced with records from
    *          the temp-tables of source {@code DataSet}. 
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical copyDataset(handle srcDS, logical append, logical replace)
   {
      boolean isAppend = append != null && !append.isUnknown() && append.getValue();
      boolean isReplace = replace != null && !replace.isUnknown() && replace.getValue();
      return copyDatasetImpl(srcDS, isAppend, isReplace, false, null, false, "cpy");
   }
   
   /**
    * Copies a source {@code DataSet} object into this {@code DataSet} object.
    *
    * @param   srcDS
    *          The source {@code DataSet}.
    * @param   append
    *          When {@code true} the source temp-tables are appended to current temp-tables.
    * @param   replace
    *          When {@code true} the records in this temp-tables are replaced with records from
    *          the temp-tables of source {@code DataSet}. 
    * @param   looseCopy
    *          When {@code TRUE}, each temp-table in the source DataSet object is copied to
    *          this {@code DataSet} object based on a field mapping between the source and target
    *          temp-table buffers.
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical copyDataset(handle srcDS, logical append, logical replace, logical looseCopy)
   {
      boolean isAppend = append != null && !append.isUnknown() && append.getValue();
      boolean isReplace = replace != null && !replace.isUnknown() && replace.getValue();
      boolean isLoose = looseCopy != null && !looseCopy.isUnknown() && looseCopy.getValue();
      return copyDatasetImpl(srcDS, isAppend, isReplace, isLoose, null, false, "cpy");
   }
   
   /**
    * Copies a source {@code DataSet} object into this {@code DataSet} object.
    *
    * @param   srcDS
    *          The source {@code DataSet}.
    * @param   append
    *          When {@code true} the source temp-tables are appended to current temp-tables.
    * @param   replace
    *          When {@code true} the records in this temp-tables are replaced with records from
    *          the temp-tables of source {@code DataSet}. 
    * @param   looseCopy
    *          When {@code TRUE}, each temp-table in the source DataSet object is copied to
    *          this {@code DataSet} object based on a field mapping between the source and target
    *          temp-table buffers.
    * @param   pairList
    *          A list of the target and source temp-table PAIRS to be copied, comma-delimited.
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical copyDataset(handle srcDS,
                              logical append,
                              logical replace,
                              logical looseCopy,
                              Text pairList)
   {
      boolean isAppend = append != null && !append.isUnknown() && append.getValue();
      boolean isReplace = replace != null && !replace.isUnknown() && replace.getValue();
      boolean isLoose = looseCopy != null && !looseCopy.isUnknown() && looseCopy.getValue();
      return copyDatasetImpl(
            srcDS, isAppend, isReplace, isLoose, pairList.toJavaType(), false, "cpy");
   }
   
   /**
    * Copies a source {@code DataSet} object into this {@code DataSet} object.
    *
    * @param   srcDS
    *          The source {@code DataSet}.
    * @param   append
    *          When {@code true} the source temp-tables are appended to current temp-tables.
    * @param   replace
    *          When {@code true} the records in this temp-tables are replaced with records from
    *          the temp-tables of source {@code DataSet}. 
    * @param   looseCopy
    *          When {@code TRUE}, each temp-table in the source DataSet object is copied to
    *          this {@code DataSet} object based on a field mapping between the source and target
    *          temp-table buffers.
    * @param   pairList
    *          A list of the target and source temp-table PAIRS to be copied, comma-delimited.
    * @param   currentOnly
    *          Use {@code TRUE} indicates that the AVM copy only the current record from each
    *          temp-table at each level in the source {@code DataSet} object to the target
    *          {@code DataSet} object. The default value is {@code FALSE}.
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical copyDataset(handle srcDS,
                              logical append,
                              logical replace,
                              logical looseCopy,
                              Text pairList,
                              logical currentOnly)
   {
      boolean isAppend = append != null && !append.isUnknown() && append.getValue();
      boolean isReplace = replace != null && !replace.isUnknown() && replace.getValue();
      boolean isLoose = looseCopy != null && !looseCopy.isUnknown() && looseCopy.getValue();
      boolean isCOnly = currentOnly != null && !currentOnly.isUnknown() && currentOnly.getValue();
      return copyDatasetImpl(
            srcDS, isAppend, isReplace, isLoose, pairList.toJavaType(), isCOnly, "cpy");
   }
   
   /**
    * Copies a source {@code DataSet} object into this {@code DataSet} object.
    *
    * @param   srcDS
    *          The source {@code DataSet}.
    * @param   append
    *          When {@code true} the source temp-tables are appended to current temp-tables.
    * @param   replace
    *          When {@code true} the records in this temp-tables are replaced with records from
    *          the temp-tables of source {@code DataSet}. 
    * @param   looseCopy
    *          When {@code TRUE}, each temp-table in the source DataSet object is copied to
    *          this {@code DataSet} object based on a field mapping between the source and target
    *          temp-table buffers.
    * @param   pairList
    *          A list of the target and source temp-table PAIRS to be copied, comma-delimited.
    * @param   currentOnly
    *          Use {@code TRUE} indicates that the AVM copy only the current record from each
    *          temp-table at each level in the source {@code DataSet} object to the target
    *          {@code DataSet} object. The default value is {@code FALSE}.
    * @param   prefix
    *          An expression used as the prefix for (re-)naming the target {@code DataSet}.
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical copyDataset(handle srcDS,
                              logical append,
                              logical replace,
                              logical looseCopy,
                              Text pairList,
                              logical currentOnly,
                              Text prefix)
   {
      boolean isAppend = append != null && !append.isUnknown() && append.getValue();
      boolean isReplace = replace != null && !replace.isUnknown() && replace.getValue();
      boolean isLoose = looseCopy != null && !looseCopy.isUnknown() && looseCopy.getValue();
      boolean isCOnly = currentOnly != null && !currentOnly.isUnknown() && currentOnly.getValue();
      
      return copyDatasetImpl(
            srcDS, isAppend, isReplace, isLoose, pairList.toJavaType(), isCOnly,
            prefix == null || prefix.isUnknown() ? "cpy" : prefix.getValue());
   }
   
   /**
    * Binds all buffers for a dynamic {@code DataSet} object at the same time. Any buffers 
    * previously added to this {@code DataSet} object are removed.
    *
    * @param   hBuffers
    *          Variable argument list of buffer handles to be set. In 4GL, the maximum number of
    *          buffers per query is 18.
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical setBuffers(handle... hBuffers)
   {
      if (!isAlterable())
      {
         ErrorManager.recordOrShowError(11858);
         // Cannot add/set buffers on a static dataset.
         return logical.of(false);
      }
      
      // the list of buffers is cleared even if errors are encountered and method is aborted
      this.buffers.clear();
      this.proxies.clear();
      
      List<BufferImpl> localBuffers = new ArrayList<>(hBuffers.length);
      for (int i = 0; i < hBuffers.length; i++)
      {
         // references must be valid
         if (hBuffers[i].isUnknown())
         {
            ErrorManager.recordOrShowError(7319, Integer.toString(i + 1));
            // ADD/SET-BUFFERS argument <argnumber> was invalid or not found.
            return logical.of(false);
         }
         
         if (hBuffers[i].isType(LegacyResource.TEMP_TABLE))
         {
            hBuffers[i] = hBuffers[i].unwrapTempTable().defaultBufferHandle();
         }
         
         // references must be BUFFER handles (the error number is rather strange!)
         if (!hBuffers[i].isType(LegacyResource.BUFFER))
         {
            ErrorManager.recordOrShowError(11860, Integer.toString(i + 1));
            // Buffer <argnumber> may not appear more than once in any dataset
            return logical.of(false);
         }
         
         BufferImpl proxy = (BufferImpl) hBuffers[i].getResource();
         BufferImpl buffer = proxy.ref();
         
         if (!(buffer instanceof Temporary))
         {
            ErrorManager.recordOrShowError(11884, buffer.doGetName());
            // Dataset buffer <name> must be for a temp-table.
            return logical.of(false);
         }
         
         if (buffer.isBeforeBuffer())
         {
            ErrorManager.recordOrShowError(15008, buffer.doGetName());
            // You may not add before-table buffer <buffer> to a DATASET.
            return logical.of(false);
         }
         
         // check for duplications:
         DataSet ds = buffer._dataSet();
         if (localBuffers.contains(buffer) || (ds != null) && ds.valid() && ds != this)
         {
            ErrorManager.recordOrShowError(11860, buffer.doGetName());
            // Buffer <name> may not appear more than once in any dataset.
            return logical.of(false);
         }
         
         localBuffers.add(buffer);
         proxies.put(buffer, proxy);
      }
      
      // when everything is fine:
      this.buffers.addAll(localBuffers);
      this.buffers.forEach(b -> b.setDataSet(this));
      
      return logical.of(true);
   }
   
   /**
    * Binds all buffers for a dynamic {@code DataSet} object at the same time. Any buffers 
    * previously added to this {@code DataSet} object are removed.
    *
    * @param   bufs
    *          Variable argument list of buffer handles to be set. In 4GL, the maximum number of
    *          buffers per query is 18.
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical setBuffers(Buffer... bufs)
   {
      return logical.of(setBuffersImpl(bufs));
   }
   
   /**
    * Binds all buffers for a dynamic {@code DataSet} object at the same time. Any buffers
    * previously added to this {@code DataSet} object are removed.
    *
    * @param   bufs
    *          Variable argument list of Buffer to be set, a mixed list of {@link Buffer},
    *          {@link handle} or {@link character} instances.
    *
    * @return  {@code true} on success
    */
   @Override
   public logical setBuffers(Object... bufs)
   {
      if (!isAlterable())
      {
         ErrorManager.recordOrShowError(11858);
         // Cannot add/set buffers on a static dataset.
         return logical.of(false);
      }
      
      // the list of buffers is cleared even if errors are encountered and method is aborted
      this.buffers.clear();
      this.proxies.clear();
      
      BufferManager bm = null;
      List<BufferImpl> localBuffers = new ArrayList<>(bufs.length);
      for (int i = 0; i < bufs.length; i++)
      {
         BufferImpl proxy = null;
         if (bufs[i] instanceof Buffer)
         {
            proxy = (BufferImpl) bufs[i]; 
         }
         else if (bufs[i] instanceof handle)
         {
            handle asHandle = (handle) bufs[i]; 
            // references must be valid:
            if (asHandle.isUnknown() || !asHandle.isType(LegacyResource.BUFFER))
            {
               ErrorManager.recordOrShowError(7319, Integer.toString(i + 1));
               // ADD/SET-BUFFERS argument <argnumber> was invalid or not found.
               return logical.of(false);
            }
            proxy = (BufferImpl) asHandle.getResource();
         }
         else if (bufs[i] instanceof character || bufs[i] instanceof String)
         {
            String name = (bufs[i] instanceof String)
                  ? (String) bufs[i] : ((character) bufs[i]).toStringMessage();
            
            name = TextOps.rightTrimNative(name);
            
            if (bm == null)
            {
               bm = BufferManager.get();
            }
            RecordBuffer rb = bm.lookupByName(name);
            if (rb != null)
            {
               proxy = (BufferImpl) rb.getDMOProxy();
            }
            else
            {
               ErrorManager.recordOrShowError(7319, Integer.toString(i + 1));
               // ADD/SET-BUFFERS argument <argnumber> was invalid or not found.
               return logical.of(false);
            }
         }
         else
         {
            // unknown/invalid data type
            ErrorManager.recordOrShowError(7319, Integer.toString(i + 1));
            // ADD/SET-BUFFERS argument <arg-number> was invalid or not found.
            return logical.of(false);
         }
         
         // check for duplications:
         BufferImpl crtBuff = proxy.ref();
         if (crtBuff.isBeforeBuffer())
         {
            ErrorManager.recordOrShowError(15008, crtBuff.doGetName());
            // You may not add before-table buffer <buffer> to a DATASET.
            return logical.of(false);
         }
         
         DataSet ds = crtBuff._dataSet();
         if (localBuffers.contains(crtBuff) || (ds != null) && ds.valid() && ds != this)
         {
            ErrorManager.recordOrShowError(11860, crtBuff.doGetName());
            // Buffer <name> may not appear more than once in any dataset.
            return logical.of(false);
         }
         
         localBuffers.add(crtBuff);
         proxies.put(crtBuff, proxy);
      }
      
      // when everything is fine:
      this.buffers.addAll(localBuffers);
      this.buffers.forEach(b -> b.setDataSet(this));
      
      return logical.of(true);
   }
   
   /**
    * Adds one new buffer to a query object or dynamic {@code DataSet} object, without affecting
    * the other buffers, if any. Use the SET-BUFFERS( ) method to remove all prior buffers and set
    * all buffers for the object at the same time.
    *
    * @param   bufHandle
    *          A buffer handle to add.
    *
    * @return  true on success
    */
   @Override
   public logical addBuffer(handle bufHandle)
   {
      if (!isAlterable()) 
      {
         ErrorManager.recordOrShowError(11858);
         // Cannot add/set buffers on a static dataset.
         return logical.of(false);
      }
      
      if (bufHandle.isUnknown())
      {
         ErrorManager.recordOrShowError(7319, "1");
         // ADD/SET-BUFFERS argument <argnumber> was invalid or not found.
         return logical.of(false);
      }
      
      WrappedResource res = bufHandle.getResource();
      if (res instanceof TempTable)
      {
         res = ((TempTable) res).defaultBufferHandleNative();
      }
      
      if (!(res instanceof Buffer))
      {
         // NOTE: trying to add an invalid handle - this is probably OE bug, should throw other error
         ErrorManager.recordOrShowError(11860, bufHandle.unwrap().name().toStringMessage());
         // Buffer <name> may not appear more than once in any dataset. (11860)
         return logical.of(false);
      }
      
      return addBuffer((Buffer) res);
   }
   
   /**
    * Adds one new buffer to a query object or dynamic {@code DataSet} object, without affecting
    * the other buffers, if any. Use the SET-BUFFERS( ) method to remove all prior buffers and set
    * all buffers for the object at the same time.
    *
    * @param   buf
    *          A buffer to add.
    *
    * @return  true on success
    */
   @Override
   public logical addBuffer(Buffer buf)
   {
      if (!isAlterable())
      {
         ErrorManager.recordOrShowError(11858);
         // Cannot add/set buffers on a static dataset.
         return logical.of(false);
      }
      
      if (!(buf instanceof Temporary))
      {
         ErrorManager.recordOrShowError(11884, buf.name().toStringMessage());
         // Dataset buffer <name> must be for a temp-table.
         return logical.of(false);
      }
      
      BufferImpl proxy = (BufferImpl) buf;
      BufferImpl bufImpl = proxy.ref();
      if (bufImpl.isBeforeBuffer())
      {
         ErrorManager.recordOrShowError(15008, bufImpl.doGetName());
         // You may not add before-table buffer <buffer> to a DATASET.
         return logical.of(false);
      }
      
      DataSet ds = bufImpl._dataSet();
      if (buffers.contains(bufImpl) || (ds != null) && ds.valid() && ds != this)
      {
         ErrorManager.recordOrShowError(11860, bufImpl.doGetName());
         // Buffer <name> may not appear more than once in any dataset.
         return logical.of(false);
      }
      
      buffers.add(bufImpl);
      proxies.put(bufImpl, proxy);
      bufImpl.setDataSet(this);
      
      return logical.of(true);
   }
   
   /**
    * Adds one new buffer to a query object or dynamic {@code DataSet} object, without affecting
    * the other buffers, if any. Use the SET-BUFFERS( ) method to remove all prior buffers and set
    * all buffers for the object at the same time.
    *
    * @param   bufName
    *          Legacy buffer name to add.
    *
    * @return  true on success
    */
   @Override
   public logical addBuffer(String bufName)
   {
      if (!isAlterable())
      {
         ErrorManager.recordOrShowError(11858);
         // Cannot add/set buffers on a static dataset.
         return logical.of(false);
      }
      
      bufName = TextOps.rightTrimNative(bufName);
      BufferManager bm = BufferManager.get();
      RecordBuffer rb = bm.lookupByName(bufName);
      if (rb == null)
      {
         ErrorManager.recordOrShowError(7319, Integer.toString(1));
         // ADD/SET-BUFFERS argument <argnumber> was invalid or not found.
         return logical.of(false);
      }
      
      return addBuffer((Buffer) rb.buffer());
   }
   
   /**
    * Adds one new buffer to a query object or dynamic {@code DataSet} object, without affecting
    * the other buffers, if any. Use the SET-BUFFERS( ) method to remove all prior buffers and set
    * all buffers for the object at the same time.
    *
    * @param   bufName
    *          Legacy buffer name to add.
    *
    * @return  true on success
    */
   @Override
   public logical addBuffer(character bufName)
   {
      if (!isAlterable())
      {
         ErrorManager.recordOrShowError(11858);
         // Cannot add/set buffers on a static dataset.
         return logical.of(false);
      }
      
      if (bufName.isUnknown())
      {
         ErrorManager.recordOrShowError(7319, "1");
         // ADD/SET-BUFFERS argument <argnumber> was invalid or not found.
         return logical.of(false);
      }
      
      return addBuffer(bufName.getValue()); 
   }
   
   /**
    * Obtain the number of buffers in this {@code DataSet}.
    * 
    * @return  the number of buffers in this {@code DataSet}.
    */
   @Override
   public integer numBuffers()
   {
      return integer.of(buffers.size());
   }
   
   /**
    * Obtain the handle to a particular buffer of this {@code DataSet}.
    * 
    * @return  a handle to a particular buffer of this {@code DataSet}.
    */
   @Override
   public handle bufferHandle()
   {
      return bufferHandle(1);
   }
   
   /**
    * Obtain the handle to a particular buffer of this {@code DataSet}.
    * 
    * @param   bufIndex
    *          The sequence number of the desired buffer.
    * 
    * @return  a handle to a particular buffer of this {@code DataSet}.
    */
   @Override
   public handle bufferHandle(int bufIndex)
   {
      return bufferHandle(integer.of(bufIndex));
   }
   
   /**
    * Obtain the handle to a particular buffer of this {@code DataSet}.
    * 
    * @param   bufIndex
    *          The sequence number of the desired buffer.
    * 
    * @return  a handle to a particular buffer of this {@code DataSet}.
    */
   @Override
   public handle bufferHandle(int64 bufIndex)
   {
      int idx = bufIndex.isUnknown() ? 0 : bufIndex.intValue();
      return new handle(getBufferByIndex(idx));
   }
   
   /**
    * Obtain the handle to a particular buffer of this {@code DataSet}.
    * 
    * @param   buffName
    *          The name of the desired buffer.
    * 
    * @return  a handle to a particular buffer of this {@code DataSet}.
    */
   @Override
   public handle bufferHandle(String buffName)
   {
      return new handle(getBufferByName(buffName));
   }
   
   /**
    * Obtain the handle to a particular buffer of this {@code DataSet}.
    * 
    * @param   buffName
    *          The name of the desired buffer.
    * 
    * @return  a handle to a particular buffer of this {@code DataSet}.
    */
   @Override
   public handle bufferHandle(character buffName)
   {
      if (buffName == null || buffName.isUnknown())
      {
         return new handle();
      }
      
      return new handle(getBufferByName(buffName.getValue()));
   }
   
   /**
    * Checks whether fields are written when the temp-table is serialized, for example into
    * JSON or XML.
    * This represents the getter for {@code SERIALIZE-HIDDEN} ABL attribute.
    *
    * @return  {@code true} when the fields are hidden.
    */
   @Override
   public logical getSerializeHidden()
   {
      return logical.of(serializeHidden);
   }
   
   /**
    * Allow to configure whether fields are written when the temp-table is serialized, for example
    * into JSON or XML.
    * This represents the setter for {@code SERIALIZE-HIDDEN} ABL attribute.
    *
    * @param   val
    *          {@code true} when the fields are hidden.
    */
   @Override
   public void setSerializeHidden(logical val)
   {
      if (val.isUnknown())
      {
         unableToAssignUnknown("SERIALIZE-HIDDEN", "DATA-RELATION widget");
         return;
      }
      
      setSerializeHidden(val.booleanValue());
   }
   
   /**
    * Allow to configure whether fields are written when the temp-table is serialized, for example
    * into JSON or XML.
    * This represents the setter for {@code SERIALIZE-HIDDEN} ABL attribute.
    *
    * @param   val
    *          {@code true} when the fields are hidden.
    */
   @Override
   public void setSerializeHidden(boolean val)
   {
      serializeHidden = val;
   }
   
   /**
    * Returns the value of the the NAMESPACE-PREFIX attribute. Only for ELEMENT or ATTRIBUTE,
    * for other attributes return unknown value.
    *
    * @return    See above.
    */
   @Override
   public character namespacePrefix()
   {
      return new character(nsPrefix);
   }
   
   /**
    * Setter for NAMESPACE-PREFIX attribute.
    *
    * @param    prefix
    *           The new value of the NAMESPACE-PREFIX attribute.
    */
   @Override
   public void namespacePrefix(String prefix)
   {
      if (prefix == null)
      {
         unableToAssignUnknown("NAMESPACE-PREFIX", "DATASET widget");
         return;
      }
      
      this.nsPrefix = prefix;
   }
   
   /**
    * Setter for NAMESPACE-PREFIX attribute.
    *
    * @param    prefix
    *           The new value of the NAMESPACE-PREFIX attribute.
    */
   @Override
   public void namespacePrefix(character prefix)
   {
      namespacePrefix(prefix.toJavaType());
   }
   
   /**
    * Implementation of the READ-JSON method. Reads JSON data from the specified data source into
    * this object.
    *
    * @param   source
    *          Data source.
    *
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical readJson(SourceData source)
   {
      return readJson(source, new character("MERGE"));
   }
   
   /**
    * Implementation of the READ-JSON method. Reads JSON data from the specified data source into
    * this object.
    *
    * @param   source
    *          Data source.
    * @param   readMode
    *          Mode in which data is read into buffer. Must evaluate to {@code APPEND}, {@code
    *          EMPTY}, {@code MERGE}, or {@code REPLACE}.
    *
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical readJson(SourceData source, character readMode)
   {
      // normalize parameters
      JsonImport jsonImport = new JsonImport(source, readMode.toJavaType());
      
      try
      {
         // perform import
         return logical.of(jsonImport.readDataset(this));
      }
      catch (PersistenceException exc)
      {
         // TODO: log
         return logical.of(false);
      }
      finally
      {
         // release and flush all buffers
         for (int i = 0; i < buffers.size(); i++)
         {
            buffers.get(i).release();
         }
      }
   }
   
   /**
    * Implementation of the WRITE-JSON method. Writes JSON data from this object into the
    * specified data target.
    *
    * @param   target
    *          Data target.
    *
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical writeJson(TargetData target)
   {
      return writeJson(target, null, null, null, null, null);
   }
   
   /**
    * Implementation of the WRITE-JSON method. Writes JSON data from this object into the
    * specified data target.
    *
    * @param   target
    *          Data target.
    * @param   formatted
    *          {@code True} to format the JSON to make it more readable; {@code false} to write it
    *          as unformatted text. Unknown value is treated as {@code false}.
    *
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical writeJson(TargetData target, logical formatted)
   {
      return writeJson(target, formatted, null, null, null, null);
   }
   
   /**
    * Implementation of the WRITE-JSON method. Writes JSON data from this object into the
    * specified data target.
    *
    * @param   target
    *          Data target.
    * @param   formatted
    *          {@code True} to format the JSON to make it more readable; {@code false} to write it
    *          as unformatted text. Unknown value is treated as {@code false}.
    * @param   encoding
    *          Name of the character encoding to use.
    *
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical writeJson(TargetData target, logical formatted, character encoding)
   {
      return writeJson(target, formatted, encoding, null, null, null);
   }
   
   /**
    * Implementation of the WRITE-JSON method. Writes JSON data from this object into the
    * specified data target.
    *
    * @param   target
    *          Data target.
    * @param   formatted
    *          {@code True} to format the JSON to make it more readable; {@code false} to write it
    *          as unformatted text. Unknown value is treated as {@code false}.
    * @param   encoding
    *          Name of the character encoding to use.
    * @param   omitInitials
    *          {@code True} to omit data for fields whose values match their initial values.
    *
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical writeJson(TargetData target,
                            logical formatted,
                            character encoding,
                            logical omitInitials)
   {
      return writeJson(target, formatted, encoding, omitInitials, null, null);
   }
   
   /**
    * Implementation of the WRITE-JSON method. Writes JSON data from this object into the
    * specified data target.
    *
    * @param   target
    *          Data target.
    * @param   formatted
    *          {@code True} to format the JSON to make it more readable; {@code false} to write it
    *          as unformatted text. Unknown value is treated as {@code false}.
    * @param   encoding
    *          Name of the character encoding to use.
    * @param   omitInitials
    *          {@code True} to omit data for fields whose values match their initial values.
    * @param   omitOuterObject
    *          {@code True} to omit data for the outermost object from the output.
    *
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical writeJson(TargetData target,
                            logical formatted,
                            character encoding,
                            logical omitInitials,
                            logical omitOuterObject)
   {
      return writeJson(target, formatted, encoding, omitInitials, omitOuterObject, null);
   }
   
   /**
    * Implementation of the WRITE-JSON method. Writes JSON data from this object into the
    * specified data target.
    *
    * @param   target
    *          Data target.
    * @param   formatted
    *          {@code True} to format the JSON to make it more readable; {@code false} to write it
    *          as unformatted text. Unknown value is treated as {@code false}.
    * @param   encoding
    *          Name of the character encoding to use.
    * @param   omitInitialValues
    *          {@code True} to omit data for fields whose values match their initial values.
    * @param   omitOuterObject
    *          {@code True} to omit data for the outermost object from the output.
    * @param   writeBeforeImage
    *          {@code True} to write before-image data and error information, else {@code false}.
    *
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical writeJson(TargetData target,
                            logical formatted,
                            character encoding,
                            logical omitInitialValues,
                            logical omitOuterObject,
                            logical writeBeforeImage)
   {
      boolean pretty = formatted != null && !formatted.isUnknown() && formatted.getValue();
      boolean noInit = omitInitialValues != null      && 
                       !omitInitialValues.isUnknown() && 
                       omitInitialValues.getValue();
      boolean noOuter = omitOuterObject != null      &&
                        !omitOuterObject.isUnknown() &&
                        omitOuterObject.getValue();
      boolean beforeImage = writeBeforeImage != null      &&
                            !writeBeforeImage.isUnknown() &&
                            writeBeforeImage.getValue();
      
      boolean ret;
      try
      {
         com.fasterxml.jackson.core.JsonEncoding jsonEncoding = JsonExport.parseEncoding(encoding);
         if (jsonEncoding == null)
         {
            ErrorManager.recordOrShowError(15342, encoding.toStringMessage(), "WRITE-JSON");
            // Invalid encoding '<encoding>' argument for <method>.
            return logical.of(false);
         }
         
         if (!flushAllBuffers())
         {
            return logical.FALSE;
         }

         JsonExport export = new JsonExport();
         ret = export.exportDataSet(this, target, pretty, jsonEncoding, noInit, noOuter, beforeImage);
      }
      catch (PersistenceException exc)
      {
         // TODO: log ?
         ret = false;
      }
      finally
      {
         try
         {
            target.close();
         }
         catch (IOException e)
         {
            // TODO: log ?
            ret = false;
         }
      }
      return logical.of(ret);
   }
   
   /**
    * Obtain the XML Schema data type for the buffer-field object.
    *
    * @return the XML Schema data type for the buffer-field object.
    */
   @Override
   public character getXmlDataType()
   {
      handle.invalidAttribute("XML-DATA-TYPE", false, LegacyResource.DATASET);
      return new character();
   }
   
   /**
    * Configures the XML Schema data type for the buffer-field object.
    *
    * @param newType the new XML Schema data type for the buffer-field object.
    */
   @Override
   public void setXmlDataType(String newType)
   {
      handle.invalidAttribute("XML-DATA-TYPE", true, LegacyResource.DATASET);
   }
   
   /**
    * Configures the XML Schema data type for the buffer-field object.
    *
    * @param newType the new XML Schema data type for the buffer-field object.
    */
   @Override
   public void setXmlDataType(Text newType)
   {
      handle.invalidAttribute("XML-DATA-TYPE", true, LegacyResource.DATASET);
   }
   
   /**
    * Obtain the XML node type of this object.
    * This represents the getter for {@code XML-NODE-TYPE} ABL attribute.
    *
    * @return the XML node type of this object.
    */
   @Override
   public character getXmlNodeType()
   {
      return new character(serializeHidden ? XmlNode.HIDDEN : XmlNode.ELEMENT);
   }
   
   /**
    * Allows to specify how a this object will be represented in XML and XML Schema. Valid XML
    * node types are "ELEMENT" and "HIDDEN". It overlaps the{@code SERIALIZE-HIDDEN} attribute.
    * This represents the setter for {@code XML-NODE-TYPE} ABL attribute.
    *
    * @param   newType
    *          The new type.
    */
   @Override
   public void setXmlNodeType(Text newType)
   {
      setXmlNodeType(newType.toJavaType());
   }
   
   /**
    * Allows to specify how a this object will be represented in XML and XML Schema. Valid XML
    * node types are "ELEMENT" and "HIDDEN". It overlaps the{@code }SERIALIZE-HIDDEN} attribute.
    * This represents the setter for {@code XML-NODE-TYPE} ABL attribute.
    *
    * @param   newType
    *          The new type.
    */
   @Override
   public void setXmlNodeType(String newType)
   {
      if (newType == null)
      {
         unableToAssignUnknown("XML-NODE-TYPE", "DATASET widget");
         return;
      }
      
      String newTypeUp = newType.toUpperCase();
      if (!newTypeUp.equals(XmlNode.HIDDEN) && !newTypeUp.equals(XmlNode.ELEMENT))
      {
         ErrorManager.recordOrShowError(16031, newType, name().toStringMessage());
         // Invalid XML-NODE-TYPE <nodetype> for Dataset <dataset>.
         ErrorManager.recordOrShowError(4058, "XML-NODE-TYPE", "DATASET widget", newType);
         // **Attribute <attribute> for the <widget id> has an invalid value of <string>.
         return;
      }
      
      serializeHidden = newTypeUp.equals(XmlNode.HIDDEN);
   }
   
   /**
    * Implementation of the READ-XML method. Reads XML data from the specified data source into
    * this object.
    * <p>
    * Defaults applied:
    * <ul>
    *    <li>Default field type mappings</li>
    *    <li>LOOSE schema verification mode</li>
    * </ul>
    *
    * @param   source
    *          Data source.
    * @param   readMode
    *          Mode in which data is read into buffer. Must evaluate to {@code APPEND},
    *          {@code EMPTY}, {@code MERGE}, or {@code REPLACE}.
    * @param   schemaLocation
    *          Name of an external XML schema file, or empty string or unknown value to use the
    *          {@code xsi:noNamespaceSchemaLocation} value set in the XML data, if any.
    * @param   overrideDefaultMapping
    *          {@code true} to override the default string-to-binary data type mappings when
    *          creating a temp-table schema from an XML schema.
    *
    * @return  {@code true} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical readXml(SourceData source,
                          character readMode,
                          character schemaLocation,
                          logical overrideDefaultMapping)
   {
      return readXml(source, readMode, schemaLocation, overrideDefaultMapping, 
                     new character(), new character());
   }
   
   /**
    * Implementation of the READ-XML method. Reads XML data from the specified data source into
    * this object.
    *
    * @param   source
    *          Data source.
    * @param   readMode
    *          Mode in which data is read into buffer. Must evaluate to {@code APPEND}, {@code
    *          EMPTY}, {@code MERGE}, or {@code REPLACE}.
    * @param   schemaLocation
    *          Name of an external XML schema file, or empty string or unknown value to use the
    *          {@code xsi:noNamespaceSchemaLocation} value set in the XML data, if any.
    * @param   overrideDefaultMapping
    *          {@code True} to override the default string-to-binary data type mappings when
    *          creating a temp-table schema from an XML schema.
    * @param   fieldTypeMapping
    *          Comma-delimited list of field name and data type name pairs to use to override
    *          data type mappings of specific fields. Specify unknown value to use default
    *          mappings.
    *
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical readXml(SourceData source,
                          character readMode,
                          character schemaLocation,
                          logical overrideDefaultMapping,
                          character fieldTypeMapping)
   {
      return readXml(source,
                     readMode,
                     schemaLocation,
                     overrideDefaultMapping,
                     fieldTypeMapping,
                     new character());
   }
   
   /**
    * Implementation of the READ-XML method. Reads XML data from the specified data source into
    * this object.
    *
    * @param   source
    *          Data source.
    * @param   readMode
    *          Mode in which data is read into buffer. Must evaluate to {@code APPEND}, {@code
    *          EMPTY}, {@code MERGE}, or {@code REPLACE}.
    * @param   schemaLocation
    *          Name of an external XML schema file, or empty string or unknown value to use the
    *          {@code xsi:noNamespaceSchemaLocation} value set in the XML data, if any.
    * @param   overrideDefaultMapping
    *          {@code True} to override the default string-to-binary data type mappings when
    *          creating a temp-table schema from an XML schema.
    * @param   strMapping
    *          Comma-delimited list of field name and data type name pairs to use to override
    *          data type mappings of specific fields. Specify unknown value to use default
    *          mappings.
    * @param   verifySchemaMode
    *          Schema verification mode. Must evaluate to {@code IGNORE}, {@code LOOSE}, or
    *          {@code STRICT}. Default is {@code LOOSE}.
    *
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical readXml(SourceData source,
                          character readMode,
                          character schemaLocation,
                          logical overrideDefaultMapping,
                          character strMapping,
                          character verifySchemaMode)
   {
      Map<String, String> mapping = null;
      if (strMapping != null)
      {
         String javaStrMapping = strMapping.toJavaType();
         if (javaStrMapping != null)
         {
            mapping = new HashMap<>();
            String[] splits = javaStrMapping.split(",");
            int len = splits.length;
            if (len == 0 || len % 2 == 1)
            {
               ErrorManager.recordOrShowError(13200);
               // Odd number of values in field-type-mapping argument.
               return logical.of(false);
            }
            // no other validation at this time
            
            for (int k = 0; k < len; k += 2)
            {
               mapping.putIfAbsent(splits[k], splits[k + 1]); // do not overwrite if field is duplicated
            }
         }
      }
      
      // normalize parameters
      XmlImport xmlImport = new XmlImport(
            source,
            readMode.toJavaType(),
            schemaLocation.toJavaType(),
            !overrideDefaultMapping.isUnknown() && overrideDefaultMapping.booleanValue(),
            mapping,
            verifySchemaMode.toJavaType());
      
      // perform import
      try
      {
         return logical.of(xmlImport.importDataset(this, RecordBuffer::loadRecord));
      }
      finally
      {
         // release and flush all buffers
         for (int i = 0; i < buffers.size(); i++)
         {
            buffers.get(i).release();
         }
      }
   }
   
   /**
    * Implementation of the WRITE-XML method. Writes XML data from this object into the specified
    * data target.
    *
    * @param   target
    *          Data target.
    *
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical writeXml(TargetData target)
   {
      return writeXml(target, null, null, null, null, null, null, null);
   }
   
   /**
    * Implementation of the WRITE-XML method. Writes XML data from this object into the specified
    * data target.
    *
    * @param   target
    *          Data target.
    * @param   formatted
    *          {@code True} to format the XML to make it more readable; {@code false} to write it
    *          as unformatted text. Unknown value is treated as {@code false}.
    *
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical writeXml(TargetData target, logical formatted)
   {
      return writeXml(target, formatted, null, null, null, null, null, null);
   }
   
   /**
    * Implementation of the WRITE-XML method. Writes XML data from this object into the specified
    * data target.
    *
    * @param   target
    *          Data target.
    * @param   formatted
    *          {@code True} to format the XML to make it more readable; {@code false} to write it
    *          as unformatted text. Unknown value is treated as {@code false}.
    * @param   encoding
    *          Name of the character encoding to use.
    *
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical writeXml(TargetData target, logical formatted, character encoding)
   {
      return writeXml(target, formatted, encoding, null, null, null, null, null);
   }
   
   /**
    * Implementation of the WRITE-XML method. Writes XML data from this object into the specified
    * data target.
    *
    * @param   target
    *          Data target.
    * @param   formatted
    *          {@code True} to format the XML to make it more readable; {@code false} to write it
    *          as unformatted text. Unknown value is treated as {@code false}.
    * @param   encoding
    *          Name of the character encoding to use.
    * @param   schemaLocation
    *          Name of an external XML schema file, or empty string or unknown value to set the
    *          {@code xsi:noNamespaceSchemaLocation} to empty string or unknown value.
    *
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical writeXml(TargetData target,
                           logical formatted,
                           character encoding,
                           character schemaLocation)
   {
      return writeXml(target, formatted, encoding, schemaLocation, null, null, null, null);
   }
   
   /**
    * Implementation of the WRITE-XML method. Writes XML data from this object into the specified
    * data target.
    *
    * @param   target
    *          Data target.
    * @param   formatted
    *          {@code True} to format the XML to make it more readable; {@code false} to write it
    *          as unformatted text. Unknown value is treated as {@code false}.
    * @param   encoding
    *          Name of the character encoding to use.
    * @param   schemaLocation
    *          Name of an external XML schema file, or empty string or unknown value to set the
    *          {@code xsi:noNamespaceSchemaLocation} to empty string or unknown value.
    * @param   writeXmlSchema
    *          {@code True} to write XML schema information, else {@code false}.
    *
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical writeXml(TargetData target,
                           logical formatted,
                           character encoding,
                           character schemaLocation,
                           logical writeXmlSchema)
   {
      return writeXml(target,
                      formatted,
                      encoding,
                      schemaLocation,
                      writeXmlSchema,
                      null,
                      null,
                      null);
   }
   
   /**
    * Implementation of the WRITE-XML method. Writes XML data from this object into the specified
    * data target.
    *
    * @param   target
    *          Data target.
    * @param   formatted
    *          {@code True} to format the XML to make it more readable; {@code false} to write it
    *          as unformatted text. Unknown value is treated as {@code false}.
    * @param   encoding
    *          Name of the character encoding to use.
    * @param   schemaLocation
    *          Name of an external XML schema file, or empty string or unknown value to set the
    *          {@code xsi:noNamespaceSchemaLocation} to empty string or unknown value.
    * @param   writeXmlSchema
    *          {@code True} to write XML schema information, else {@code false}.
    * @param   minXmlSchema
    *          {@code True} to write minimal XML schema information, else {@code false}. Must be
    *          {@code false} if {@code writeXmlSchema} is {@code false}.
    *
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical writeXml(TargetData target,
                           logical formatted,
                           character encoding,
                           character schemaLocation,
                           logical writeXmlSchema,
                           logical minXmlSchema)
   {
      return writeXml(target,
                      formatted,
                      encoding,
                      schemaLocation,
                      writeXmlSchema,
                      minXmlSchema,
                      null,
                      null);
   }
   
   /**
    * Implementation of the WRITE-XML method. Writes XML data from this object into the specified
    * data target.
    *
    * @param   target
    *          Data target.
    * @param   formatted
    *          {@code True} to format the XML to make it more readable; {@code false} to write it
    *          as unformatted text. Unknown value is treated as {@code false}.
    * @param   encoding
    *          Name of the character encoding to use.
    * @param   schemaLocation
    *          Name of an external XML schema file, or empty string or unknown value to set the
    *          {@code xsi:noNamespaceSchemaLocation} to empty string or unknown value.
    * @param   writeXmlSchema
    *          {@code True} to write XML schema information, else {@code false}.
    * @param   minXmlSchema
    *          {@code True} to write minimal XML schema information, else {@code false}. Must be
    *          {@code false} if {@code writeXmlSchema} is {@code false}.
    * @param   writeBeforeImage
    *          {@code True} to write before-image data and error information, else {@code false}.
    *
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical writeXml(TargetData target,
                           logical formatted,
                           character encoding,
                           character schemaLocation,
                           logical writeXmlSchema,
                           logical minXmlSchema,
                           logical writeBeforeImage)
   {
      return writeXml(target,
                      formatted,
                      encoding,
                      schemaLocation,
                      writeXmlSchema,
                      minXmlSchema,
                      writeBeforeImage,
                      null);
   }
   
   /**
    * Implementation of the WRITE-XML method. Writes XML data from this object into the specified
    * data target.
    *
    * @param   target
    *          Data target.
    * @param   formatted
    *          {@code True} to format the XML to make it more readable; {@code false} to write it
    *          as unformatted text. Unknown value is treated as {@code false}.
    * @param   encoding
    *          Name of the character encoding to use.
    * @param   schemaLocation
    *          Name of an external XML schema file, or empty string or unknown value to set the
    *          {@code xsi:noNamespaceSchemaLocation} to empty string or unknown value.
    * @param   writeXmlSchema
    *          {@code True} to write XML schema information, else {@code false}.
    * @param   minXmlSchema
    *          {@code True} to write minimal XML schema information, else {@code false}. Must be
    *          {@code false} if {@code writeXmlSchema} is {@code false}.
    * @param   beforeImage
    *          {@code True} to write before-image data and error information, else {@code false}.
    * @param   noInitialVals
    *          {@code True} to omit data for fields whose values match their initial values.
    *
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical writeXml(TargetData target,
                           logical formatted,
                           character encoding,
                           character schemaLocation,
                           logical writeXmlSchema,
                           logical minXmlSchema,
                           logical beforeImage,
                           logical noInitialVals)
   {
      XmlExport export = new XmlExport(
            target,
            formatted != null && !formatted.isUnknown() && formatted.getValue(),
            encoding == null ? null : encoding.toJavaType());
      
      if (!flushAllBuffers())
      {
         return logical.FALSE;
      }
      
      boolean ret;
      try
      {
         ret = export.writeDataset(
               this,
               schemaLocation == null ? null : schemaLocation.toJavaType(),
               writeXmlSchema != null && !writeXmlSchema.isUnknown() && writeXmlSchema.getValue(),
               minXmlSchema != null && !minXmlSchema.isUnknown() && minXmlSchema.getValue(),
               beforeImage != null && !beforeImage.isUnknown() && beforeImage.getValue(),
               noInitialVals != null && !noInitialVals.isUnknown() && noInitialVals.getValue());
      }
      catch(XmlExport.Fatal e) // Emergency exit - for the 4GL compatibility
      {
         // Error was already reported, just stop;
         ret = false;
      }
      finally
      {
         try
         {
            target.close();
         }
         catch (IOException e)
         {
            // TODO: log
            ret = false;
         }
      }
      return logical.of(ret);
   }
   
   /**
    * Writes an XML representation of the definition of this {@code DataSet}.
    *
    * @param   target
    *          Data target.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical writeXmlSchema(TargetData target)
   {
      return writeXmlSchema(target, null, null, null, null);
   }
   
   /**
    * Writes an XML representation of the definition of this {@code DataSet}.
    *
    * @param   target
    *          Data target.
    * @param   formatted
    *          Use {@code true} for a more human-readable document.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical writeXmlSchema(TargetData target, logical formatted)
   {
      return writeXmlSchema(target, formatted, null, null, null);
   }
   
   /**
    * Writes an XML representation of the definition of this {@code DataSet}.
    *
    * @param   target
    *          Data target.
    * @param   formatted
    *          Use {@code true} for a more human-readable document.
    * @param   encoding
    *          The character encoding. The default is UTF-8.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical writeXmlSchema(TargetData target, logical formatted, Text encoding)
   {
      return writeXmlSchema(target, formatted, encoding, null, null);
   }
   
   /**
    * Writes an XML representation of the definition of this {@code DataSet}.
    *
    * @param   target
    *          Data target.
    * @param   formatted
    *          Use {@code true} for a more human-readable document.
    * @param   encoding
    *          The character encoding. The default is UTF-8.
    * @param   minXmlSchema
    *          If {@code true}, the minimum amount of schema for the object will be written.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical writeXmlSchema(TargetData target,
                                 logical formatted,
                                 Text encoding,
                                 logical minXmlSchema)
   {
      return writeXmlSchema(target, formatted, encoding, minXmlSchema, null);
   }
   
   /**
    * Writes an XML representation of the definition of this {@code DataSet}.
    *
    * @param   target
    *          Data target.
    * @param   formatted
    *          Use {@code true} for a more human-readable document.
    * @param   encoding
    *          The character encoding. The default is UTF-8.
    * @param   minXmlSchema
    *          If {@code true}, the minimum amount of schema for the object will be written.
    * @param   omitInitVal
    *          Use {@code true} to omit the initial values.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical writeXmlSchema(TargetData target,
                                 logical formatted,
                                 Text encoding,
                                 logical minXmlSchema,
                                 logical omitInitVal)
   {
      XmlExport export = new XmlExport(
            target,
            formatted != null && !formatted.isUnknown() && formatted.getValue(),
            encoding == null ? null : encoding.toJavaType());
      
      boolean ret;
      try
      {
         ret = export.writeDatasetSchema(
                  this,
                  minXmlSchema != null && !minXmlSchema.isUnknown() && minXmlSchema.getValue(),
                  omitInitVal != null && !omitInitVal.isUnknown() && omitInitVal.getValue());
      }
      finally
      {
         try
         {
            target.close();
         }
         catch (IOException e)
         {
            // TODO: log
            ret = false;
         }
      }
      return logical.of(ret);
   }
   
   /**
    * Reads XML Schema from an XML document and uses that schema to either create a schema for a
    * {@code DataSet} or {@code TempTable} or verify existing schema in a {@code DataSet},
    * {@code TempTable} or temp-table {@code Buffer} object.
    *
    * @param   source
    *          Data source.
    * @param   override
    *          If {@code true} the default mapping between XML Schema string and binary data types
    *          and ABL data types when creating an ABL temp-table schema from an XML Schema will
    *          be overwritten.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical readXmlSchema(SourceData source, logical override)
   {
      return readXmlSchema(source, override, null, null);
   }
   
   /**
    * Reads XML Schema from an XML document and uses that schema to either create a schema for a
    * {@code DataSet} or {@code TempTable} or verify existing schema in a {@code DataSet},
    * {@code TempTable} or temp-table {@code Buffer} object.
    *
    * @param   source
    *          Data source.
    * @param   override
    *          If {@code true} the default mapping between XML Schema string and binary data types
    *          and ABL data types when creating an ABL temp-table schema from an XML Schema will
    *          be overwritten.
    * @param   mapping
    *          A comma-delimited list of field name, data type pairs. This allows to specify the
    *          data type for a specific field.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical readXmlSchema(SourceData source, logical override, Text mapping)
   {
      return readXmlSchema(source, override, mapping, null);
   }
   
   /**
    * Reads XML Schema from an XML document and uses that schema to either create a schema for a
    * {@code DataSet} or {@code TempTable} or verify existing schema in a {@code DataSet},
    * {@code TempTable} or temp-table {@code Buffer} object.
    *
    * @param   source
    *          Data source.
    * @param   override
    *          If {@code true} the default mapping between XML Schema string and binary data types
    *          and ABL data types when creating an ABL temp-table schema from an XML Schema will
    *          be overwritten.
    * @param   strMapping
    *          A comma-delimited list of field name, data type pairs. This allows to specify the
    *          data type for a specific field.
    * @param   verifyMode
    *          Specifies how the XML will be verified against the schema. Valid values: "LOOSE" or
    *          "STRICT".
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical readXmlSchema(SourceData source,
                                logical override,
                                Text strMapping,
                                Text verifyMode)
   {
      Map<String, String> mapping = null;
      if (strMapping != null)
      {
         String javaStrMapping = strMapping.toJavaType();
         if (javaStrMapping != null)
         {
            mapping = new HashMap<>();
            String[] splits = javaStrMapping.split(",");
            int len = splits.length;
            if (len == 0 || len % 2 == 1)
            {
               ErrorManager.recordOrShowError(13200);
               // Odd number of values in field-type-mapping argument.
               return logical.of(false);
            }
            // no other validation at this time
            
            for (int k = 0; k < len; k += 2)
            {
               mapping.putIfAbsent(splits[k], splits[k + 1]); // do not overwrite if field is duplicated
            }
         }
      }
      
      XmlImport xmlImport = new XmlImport(source, null, null,
                                          !override.isUnknown() && override.booleanValue(),
                                          mapping,
                                          verifyMode == null ? null : verifyMode.toJavaType());
      return logical.of(xmlImport.importDatasetSchema(this));
   }
   
   /**
    * Obtain an integer values that uniques identifies this resource.
    *
    * @return  an integer values that uniques identifies this resource.
    */
   @Override
   public integer getUniqueID()
   {
      if (uuid == null)
      {
         uuid = UniqueIdGenerator.getUniqueId(UniqueIdGenerator.IdKind.DATASET);
      }
      return new integer(this.uuid);
   }
   
   /**
    * Set the {@link #postponedDelete} flag.
    *
    * @param    postponed
    *           Flag indicating that delete is postponed.
    */
   public void setPostponedDelete(boolean postponed)
   {
      this.postponedDelete = postponed;
   }
   
   /**
    * Searches whether a buffer is part of the dataset. The before buffers are not taken into
    * consideration so for these arguments the method returns -1.
    * 
    * @param   buffer
    *          The {@code BufferImpl} to be search.
    * 
    * @return  The 0-base index of the buffer if it is part of the {@code DataSet} and -1
    *          otherwise.
    */
   public int getIndexOf(BufferImpl buffer)
   {
      for (int i = 0; i < buffers.size(); i++)
      {
         if (buffer == buffers.get(i))
         {
            return i;
         }
      }
      return -1; // buffer not found
   }
   
   /**
    * Check if the resource can veto the delete or not, before attempting the {@link #delete()} call.
    * 
    * @param    ref
    *           The exact handle holding the reference, on which the delete is attempted.
    * @param    explicit
    *           Flag indicating this is called via <code>DELETE OBJECT</code> from the application.
    * 
    * @return   <code>false</code> if the dataset is registered as a OUTPUT DATASET-HANDLE parameter
    *           or a BY-REFERENCE parameter.
    */
   @Override
   public boolean allowDelete(handle ref, boolean explicit)
   {
      DataSetManager dmgr = DataSetManager.instance();
      
      if (dmgr.isOutputDataSetHandle(this))
      {
         setPostponedDelete(true);
         return false;
      }
      
      if (dmgr.isByReferenceParameter(this))
      {
         if (_dynamic())
         {
            // 12327 error is thrown only for dynamic datasets
            ErrorManager.recordOrThrowError(12327, 
                                            "Cannot delete a BY-REFERENCE PARAMETER " +
                                            "dataset or table in the called procedure",
                                            false, false);
         }
         
         return false;
      }
      
      return true;
   }
   
   /**
    * Delete the resource.
    *
    * @return   {@code true} if the resource was deleted.
    */
   @Override
   protected boolean resourceDelete()
   {
      if (!_dynamic() && !implicitDeletion())
      {
         // TODO: show error ?
         return false;
      }
      
      clearImpl(true, true);
      deleted = true;

      ControlFlowOps.invalidateCallSiteCache(this);

      if (deleteQueries != null)
      {
         for (QueryWrapper query : deleteQueries)
         {
            if (query.valid())
            {
               query.delete();
            }
         }
      }
      
      return true;
   }
   
   /**
    * Allows each resource to declare whether it has some naming constraints.
    *
    * @return  This method always returns {@code false}, requiring that its name should be
    *          validated before assignment.
    *
    * @see  #validateName
    */
   @Override
   protected boolean hasNameConstraints()
   {
      return true;
   }
   
   /**
    * Checks if this {@code REFERENCE-ONLY} {@code DataSet} was bound to a real {@code DataSet}.
    *
    * @return  This object is dynamically built so by convention this method always returns
    *          {@code true} (it is bound to itself).
    */
   @SuppressWarnings("static-method")
   protected boolean isBound()
   {
      return true;
   }
   
   /**
    * Check if this dataset was attempted to be deleted, but was postponed because of the handle
    * being a DATASET-HANDLE BY-REFERENCE.
    * 
    * @return   The {@link #postponedDelete}.
    */
   protected boolean isPostponedDelete()
   {
      return postponedDelete;
   }
   
   /**
    * Test the compatibility of two datasets. The number of buffers and their field number is
    * checked first. The datatype of each one is not (yet).
    *
    * @param   other
    *          The {@code DataSet} to compare with this instance.
    *
    * @return  {@code true} if the two dataset are compatible (ie one can receive data from
    *          another).
    */
   protected boolean isCompatibleWith(DataSet other)
   {
      if (other == null || !other.valid())
      {
         // The use of ErrorManager.recordOrThrowError() is not very effective here because in FWD
         // the bind is done after the called procedure has already started. The ErrorManager will
         // stop only the execution of the called procedure/method. In 4GL the associate / bind
         // operation is performed in caller and, in case of errors the caller is stopped before
         // the invocation of the called method
         TransactionManager.triggerErrorInCaller(12314, ErrorManager.replaceTokens(12314));
         return false;
      }
      int bufSize = buffers.size();
      if (bufSize != other.getBuffers().size())
      {
         TransactionManager.triggerErrorInCaller(12310, ErrorManager.replaceTokens(12310));
         return false;
      }

      RecordBuffer buf1;
      RecordBuffer buf2;

      for (int i = 0; i < bufSize; i++)
      {
         buf1 = buffers.get(i).buffer();
         buf2 = other.getBuffers().get(i).buffer();

         if (buf1.getDMOBufInterface() == buf2.getDMOBufInterface())
         {
            // same DMO, no longer needed to check individual fields.
            continue;
         }

         if (!buf1.getDmoInfo().getSignature().getQuickSignature().equals
             (buf2.getDmoInfo().getSignature().getQuickSignature()))
         {
            TransactionManager.triggerErrorInCaller(12311, 
                                                    ErrorManager.replaceTokens(12311, 
                                                                               buf1.getLegacyName(),
                                                                               buf2.getLegacyName()));
            return false;
         }
      }
      
      // if all validations passed:
      return true;
   }
   
   /**
    * Obtains the original {@code DataSet} definition. This is for internal usage, in cases when
    * that information is needed and the proxy interface will mask and make the information unavailable.
    *
    * @return  the original {@code DataSet} definition.
    */
   public DataSet ref()
   {
      return this;
   }
   
   /**
    * Get the list of {@link #buffers} for the runtime instance of this resource.
    * 
    * @return   The list of buffers.
    */
   public List<BufferImpl> getBuffers()
   {
      return ref().buffers; // NOTE: breaking encapsulation !
   }

   /**
    * Getter for the internal variable {@link #serializedBySOAP}.
    *
    * @return  <code>true</code> if this instance is currently serialized due to a SOAP request,
    *          <code>false</code> otherwise
    */
   public boolean getSerializedBySOAP()
   {
      return serializedBySOAP;
   }

   /**
    * Lookup a buffer by its index.
    *
    * @param   bufIndex
    *          The index to search among the list of buffers.
    *
    * @return  The buffer whose index was specified or {@code null} when there is no match.
    */
   public BufferImpl getBufferByIndex(int bufIndex)
   {
      if (bufIndex < 1 || bufIndex > buffers.size())
      {
         return null;
      }

      BufferImpl src = buffers.get(bufIndex - 1);
      BufferImpl srcProxy = proxies.get(src);
      return (srcProxy != null) ? srcProxy.ref() : src;
   }
   
   /**
    * Copy output data for the linked {@code DataSet} parameter, if any.
    *
    * @param   outputParam
    *          The {@code DataSet} parameter where to copy the data.
    */
   protected void performOutputCopy(DataSetParameter outputParam)
   {
      if (!outputParams.contains(outputParam))
      {
         // was already processed (by i.e. scope close)
         return;
      }
      
      boolean res = true;
      if (outputParam.isRemoteParameter())
      {
         handle pds = outputParam.getDatasetHandle();
         
         DatasetWrapper oldWrapper = outputParam.getDatasetWrapper();

         if (oldWrapper.isAsXml())
         {
            String dsXml = writeToXml(new handle(this), 
                                      oldWrapper.isUseBeforeImage(), 
                                      oldWrapper.isDatasetHandle());
            oldWrapper.setXmlDataset(dsXml);
            return;
         }
         
         // fill result set(s)
         DataSetContainer rs = new DataSetContainer(this,
                                                    oldWrapper.isInput(),
                                                    oldWrapper.isOutput(),
                                                    oldWrapper.isAppend());
         DatasetWrapper wrapper = new DatasetWrapper(rs);
         wrapper.setDatasetName(this.name().getValue());
         outputParam.setDatasetWrapper(wrapper);

         if (pds._isValid())
         {
            // delete it, nothing else to do with it.  done last as the 'pds' can be the same as 'this bound'.
            ((DataSet) pds.getResource()).delete();
         }
      }
      else
      {
         handle hds = outputParam.getDatasetHandle();
         if (hds.isUnknown())
         {
            handle thisHandle = new handle(this);
            DataSet.create(hds);
            DataSet callingDataset = outputParam.getDataset();
            callingDataset.createLike(thisHandle);
            callingDataset.copyDataset(thisHandle);
         }
         else
         {
            DataSet callingDataset = outputParam.getDataset();
            
            // copies all the temp-tables in the order they were defined or added
            // extra temp-tables from both source & destination are ignored 
            int bufCopyCount = Math.min(callingDataset.getBuffers().size(), this.buffers.size());
            for (int i = 0; i < bufCopyCount; i++)
            {
               BufferImpl src = buffers.get(i);
               BufferImpl srcProxy = proxies.get(src);
               src = (srcProxy != null) ? srcProxy.ref() : src; // dynamically compute the .ref()
               
               BufferImpl dst = callingDataset.ref().buffers.get(i);
               BufferImpl dstProxy = callingDataset.ref().proxies.get(dst);
               dst = (dstProxy != null) ? dstProxy.ref() : dst; // dynamically compute the .ref()
               
               rowid srcRec = (referenceOnly || outputParam.isByReference()) && src._available()
                     ? src.rowID()
                     : null;
               
               Temporary srcDMO = (Temporary) src;
               Temporary dstDMO = (Temporary) dst;
               
               res = TemporaryBuffer.copyAllRows(srcRec,
                                                 srcDMO,
                                                 dstDMO,
                                                 outputParam.isAppend(),
                                                 CopyTableMode.OUTPUT_TABLE_PARAM_MODE);
               
               if (!res)
               {
                  break;
               }
            }
         }
      }
      
      if (!res)
      {
         TemporaryBuffer.deferCopyError();
      }
      
      outputParams.remove(outputParam);
   }
   
   /**
    * Obtain the number of top-level buffers in this {@code DataSet} object.
    *
    * @return  the number of top-level buffers in this {@code DataSet} object.
    */
   public int _getNumTopBuffers()
   {
      if (buffers.isEmpty())
      {
         return 0;
      }
      else if (relations.isEmpty())
      {
         return buffers.size();
      }
      
      // initially all buffers are TOP-LEVEL. For each active relation, one buffer becomes a child
      // so it is removed from TOP-LEVEL list. Since a buffer can be a child in not more than a
      // relation, each relation will have unique child buffers. As result, the number of
      // TOP-LEVEL buffer list is the difference between the buffer number and the number of 
      // active relations.
      int topBuffs = buffers.size();
      for (int i = 0; i < relations.size(); i++)
      {
         DataRelation relation = relations.get(i);
         if (relation._isActive() && !relation._isReposition() && !relation.isRecursive().booleanValue())
         {
            topBuffs--;
         }
      }
      // NOTE: because of the #11979 issue described in addRelationImpl() the [topBuffs] may be negative !!!
      return topBuffs;
   }
   
   /**
    * Checks whether a buffer can be at this moment the child side in a relation of this {@code DataSet}. When
    * an existing active relation is detected having the buffer as child the error 11979 is displayed before
    * returning {@code false}.
    * 
    * @param   childBuffer
    *          The child buffer to test.
    *
    * @return  {@code null} if the requested buffer can be a child buffer in a relation in this
    *          {@code DataSet} in current configuration or the {@code DataRelation} that caused the conflict.
    */
   protected DataRelation allowsActiveChild(Buffer childBuffer)
   {
      int childUid = childBuffer.getUniqueID().intValue();
      
      for (int i = 0; i < relations.size(); i++)
      {
         DataRelation relation = relations.get(i);
         if (relation._isActive()      &&
             !relation._isReposition() &&
             relation.getChildBufferNative().getUniqueID().intValue() == childUid)
         {
            return relation;
         }
      }
      
      return null;
   }

   /**
    * Queries the list of {@code DataRelation} s for a buffer. Depending on the requests, only the
    * relations where the buffer is parent, child or any of them is returned.
    *
    * @param   buffer
    *          The buffer to be queried on.
    * @param   parent
    *          If {@code true}, all the relations where the buffer is parent are included.
    * @param   child
    *          If {@code true}, all the relations where the buffer is child are included.
    * @param   active
    *          If {@code true}, ONLY the active relations are included. Otherwise, all relations
    *          that match above constraints will be returned.
    *
    * @return  The set of relations where the requested buffer is part of, depending on the value
    *          of the flags.
    */
   public ArrayList<DataRelation> getRelations(Buffer buffer, boolean parent, boolean child, boolean active)
   {
      return getRelations(buffer, parent, child, active, false);
   }
   
   /**
    * Queries the list of {@code DataRelation} s for a buffer. Depending on the requests, only the
    * relations where the buffer is parent, child or any of them is returned.
    *
    * @param   buffer
    *          The buffer to be queried on.
    * @param   parent
    *          If {@code true}, all the relations where the buffer is parent are included. 
    * @param   child
    *          If {@code true}, all the relations where the buffer is child are included. 
    * @param   active
    *          If {@code true}, ONLY the active relations are included. Otherwise, all relations
    *          that match above constraints will be returned.
    * @param   forFill
    *          A flag indicating that the relations are requested by a fill operation.
    *
    * @return  The set of relations where the requested buffer is part of, depending on the value
    *          of the flags.
    */
   public ArrayList<DataRelation> getRelations(Buffer buffer, 
                                               boolean parent, 
                                               boolean child, 
                                               boolean active,
                                               boolean forFill)
   {
      buffer = ((BufferImpl) buffer).ref();
      
      ArrayList<DataRelation> ret = new ArrayList<>(relations.size());
      for (int i = 0; i < relations.size(); i++)
      {
         DataRelation rel = relations.get(i);
         if (active && !rel._isActive())
         {
            continue;
         }
         // If the relation is reposition and is used for a fill, 
         // ignore the relation and make the child a top-level buffer.
         if (child && rel.getChildBufferNative() == buffer && !(rel._isReposition() && forFill))
         {
            ret.add(rel);
         }
         if (parent && rel.getParentBufferNative() == buffer)
         {
            ret.add(rel);
         }
      }
      return ret;
   }
   
   /**
    * Query the relation by its buffer. 
    *
    * @param   buffer
    *          The buffer to be queried on.
    *
    * @return  The optional relation for the buffer
    */
   public Optional<DataRelation> getRelation(Buffer buffer) 
   {
      return relations.stream().filter(rel -> rel.getChildBuffer().getResource() == buffer).
                       findFirst();
   }

   /**
    * Invokes a callback procedure or a callback method. The callback procedure/method must have
    * an {@code INPUT} parameter of type {@code DATASET} or {@code DATASET-HANDLE}. The passed
    * parameter is this {@code DataSet} object.
    * 
    * @param   cbd
    *          A {@code CallbackData} object holding the data necessary for invocation. May be
    *          {@code null}, in which case this method will silently return {@code yes} even
    *          though nothing is performed.
    * @param   self
    *          The object on which the event is applied (buffer or dataset).
    *          
    * @return  {@code true} if no errors were encountered during execution. 
    */
   protected boolean invokeCallback(handle self, CallbackData cbd)
   {
      if (cbd == null)
      {
         return true; // yep, nothing to do here
      }
      
      ErrorManager.ErrorHelper errHlp = ErrorManager.getErrorHelper();
      boolean wasSilent = errHlp.isSilent();
      try
      {
         if (!wasSilent)
         {
            errHlp.setSilent(true);
         }
         
         SelfManager.pushSelf(self);
         // reference already assumes input-output, do not emit append
         DataSetParameter dsp = new DataSetParameter(this, ParameterOption.BY_REFERENCE);
         if (cbd.getObject() != null)
         {
            // TODO:
            // Method '<methodname>' for callback must be valid public method. (13452)
            
            ObjectOps.invokeStandalone(cbd.getObject(), cbd.getTarget(), "I", dsp);
         }
         else if (cbd.getProcedure() != null && !cbd.getProcedure().isUnknown())
         {
            InvokeConfig callSite = cbd.getCallSite();
            InvokeConfig cfg = callSite == null ? new InvokeConfig() : callSite.clone();
            cfg.setTarget(cbd.getTarget())
               .setInHandle(cbd.getProcedure())
               .setModes("I")
               .setArguments(dsp)
               .executeForResource(this);
         }
         
         // NOTE: sometimes the method return *NO* even if the callback was successfully called
         return !errHlp.isPending(); // !ErrorManager.isError()
      }
      catch (ErrorConditionException e)
      {
         if (e.getProgressErrorCode() == 14457)
         {
            ErrorManager.recordOrShowError(13452, cbd.getTarget());
         }
         return false;
      }
      finally
      {
         SelfManager.popSelf();
         if (!wasSilent)
         {
            errHlp.setSilent(false);
         }
      }
   }
   
   /**
    * Register an output dataset parameter.
    * 
    * @param    dsParam
    *           The dataset parameter.
    */
   protected void addOutputParam(DataSetParameter dsParam)
   {
      if (outputParams == null)
      {
         outputParams = new HashSet<>();
      }
      outputParams.add(dsParam);
   }
   
   /**
    * The implementation of {@code COPY-DATASET} method. Copies all records from all tables of a
    * source {@code DataSet} object into this {@code DataSet} object.
    *
    * @param   hSrcDS
    *          The source {@code DataSet}.
    * @param   append
    *          When {@code true} the source temp-tables are appended to current temp-tables.
    * @param   replace
    *          When {@code true} the records in this temp-tables are replaced with records from
    *          the temp-tables of source {@code DataSet}. 
    * @param   looseCopy
    *          When {@code TRUE}, each temp-table in the source DataSet object is copied to
    *          this {@code DataSet} object based on a field mapping between the source and target
    *          temp-table buffers.
    * @param   pairList
    *          A list of the target and source temp-table PAIRS to be copied, comma-delimited.
    * @param   currentOnly
    *          Use {@code TRUE} indicates that the AVM copy only the current record from each
    *          temp-table at each level in the source {@code DataSet} object to the target
    *          {@code DataSet} object. The default value is {@code FALSE}.
    * @param   prefix
    *          An expression used as the prefix for (re-)naming the target {@code DataSet}.
    *
    * @return  {@code true} on success.
    */
   protected logical copyDatasetImpl(handle hSrcDS,
                                     boolean append,
                                     boolean replace,
                                     boolean looseCopy,
                                     String pairList,
                                     boolean currentOnly,
                                     String prefix)
   {
      if (hSrcDS == null || hSrcDS.isUnknown() || !hSrcDS.isType(LegacyResource.DATASET))
      {
         ErrorManager.recordOrShowError(12362);
         // COPY-DATASET must have a valid dataset handle as first argument.
         return logical.of(false);
      }
      
      String[] pairs = null;
      if (pairList != null && !pairList.isEmpty())
      {
         pairs = pairList.split(",");
         if (pairs.length % 2 == 1)
         {
            ErrorManager.recordOrShowError(12783);
            // Odd number of tables in pairs list parameter in COPY-DATASET.
            return logical.of(false);
         }
      }
      
      DataSet srcDS = (DataSet) hSrcDS.getResource();
      
      // if this is a freshly created DATASET, we copy the structure from the source DS
      if (this.buffers.isEmpty())
      {
         createLikeImpl(srcDS, prefix);
      }
      
      BiFunction<BufferImpl,BufferImpl, Boolean> checkDestB4Buffer = (src, dst) ->
      {
         boolean isEmptyBefore = src.peerBuffer.buffer().isTableDefinitelyEmpty();
         
         if (!isEmptyBefore)
         {
            TempTableBuilder  ttb = ((TempTableBuilder) dst.tableHandleNative());
            if (ttb._dynamic())
            {
               // automatically create implicit before table for this dynamic temp-table
               String tname = name().toJavaType();
               ttb.createBeforeTable("BI" + tname, tname);
            }
            else
            {
               ErrorManager.recordOrShowError(12753, dst.doGetName());
               // In COPY-TEMP-TABLE operation the static target table <name> must have an empty BEFORE-TABLE if the source table has any BEFORE-TABLE records.
               return false;
            }
         }
         return true;
      };
      
      logical res = new logical();
      if (pairs == null)
      {
         // copies all the temp-tables in the order they were defined or added
         // extra temp-tables from both source & destination are ignored 
         int bufCopyCount = Math.min(buffers.size(), srcDS.getBuffers().size());
         for (int i = 0; i < bufCopyCount; i++)
         {
            BufferImpl src = srcDS.getBuffers().get(i);
            BufferImpl dst = buffers.get(i);
            if (src.isAfterBuffer() && !dst.isAfterBuffer() && !checkDestB4Buffer.apply(src, dst))
            {
               res = logical.of(false);
               break; // i-for
            }
            
            if (currentOnly)
            {
               if (!append)
               {
                  dst.deleteAll();
               }
               if (!dst._available())
               {
                  // if destination buffer is empty create a new placeholder for the record to be copied
                  dst.create();
               }
               
               // copy only the current record from the specified temp-table buffers
               res = dst.bufferCopy(src);
               if (!res.getValue())
               {
                  break; // i-for
               }
            }
            else
            {
               // copy the entire AFTER temp-table
               res = dst.copyTempTable(new handle(src), append, replace, looseCopy, prefix);
               if (!res.getValue())
               {
                  break; // i-for
               }
            }
         }
      }
      else
      {
         // copy only the specified/mapped tables
         for (int i = 0; i < pairs.length; i+=2)
         {
            BufferImpl dst = getBufferByName(pairs[i]);
            BufferImpl src = srcDS.getBufferByName(pairs[i + 1]);
            
            if (dst == null && src == null)
            {
               dst = getBufferByName(pairs[i + 1]);
               src = srcDS.getBufferByName(pairs[i]);
            }
            
            if (dst == null || src == null)
            {
               ErrorManager.recordOrShowError(12784, pairs[i], pairs[i + 1]);
               // COPY-DATASET pair <name> and <name> must have a table in each dataset.
               return logical.of(false);
            }
            if (src.isAfterBuffer() && !dst.isAfterBuffer() && !checkDestB4Buffer.apply(src, dst))
            {
               res = logical.of(false);
               break; // i-for
            }
            
            if (currentOnly)
            {
               // copy only the current record from the specified temp-table buffers
               res = src.bufferCopy(dst);
               if (!res.getValue())
               {
                  break; // i-for
               }
            }
            else
            {
               // copy the entire temp-table
               res = dst.copyTempTable(new handle(src), append, replace, looseCopy, prefix);
               if (!res.getValue())
               {
                  break; // i-for
               }
            }
         }
      }
      
      return res;
   }
   
   /**
    * Adds a new reference to this dataset. The {@code NUM-REFERENCES} is updated if needed.
    *
    * @param   staticDataSet
    *          The new {@code DataSet} that references this object.
    */
   final void addReference(StaticDataSet staticDataSet)
   {
      if (refs == null)
      {
         refs = Collections.newSetFromMap(new IdentityHashMap<>());
      }
      refs.add(staticDataSet);
   }
   
   /**
    * Removes a reference to this dataset. The {@code NUM-REFERENCES} is updated if needed.
    *
    * @param   staticDataSet
    *          The new {@code DataSet} that does not references this object any more.
    */
   final void removeReference(StaticDataSet staticDataSet)
   {
      if (refs != null)
      {
         refs.remove(staticDataSet);
      }
   }
   
   /**
    * Write the specified DATASET handle to a XML.
    * <p>
    * This is used when the remote call sent the DATASET as XML and it must receive it in the same format
    * (used by SOAP callers).
    * 
    * @param    dsHandle
    *           The handle referencing the dataset.
    * @param    useBeforeImage
    *           Flag indicating if the before table needs to be serialized.
    * @param    isHandle
    *           Flag indicating that this is used for a DATASET-HANDLE parameter.
    *           
    * @return   The XML representation of this dataset.
    */
   @SuppressWarnings("resource")
   static String writeToXml(handle dsHandle, boolean useBeforeImage, boolean isHandle)
   {
      if (!dsHandle._isValid())
      {
         return null;
      }

      longchar dsXml = new longchar("");
      DataSet ds = (DataSet) dsHandle.getResource();
      ds.serializedBySOAP = true;
      try
      {
         ds.writeXml(new TargetData(Serializator.TYPE_LONGCHAR, dsXml),
                     logical.of(false),  // this must be false
                     null,
                     null,
                     logical.of(isHandle), // schema is sent only for DATASET-HANDLE
                     null,
                     logical.of(useBeforeImage),
                     null);

      }
      finally
      {
         ds.serializedBySOAP = false;
      }
      return dsXml.getValue();
   }
   
   /**
    * Binds all buffers for a dynamic {@code DataSet} object at the same time. Any buffers 
    * previously added to this {@code DataSet} object are removed.
    *
    * @param   bufs
    *          Variable argument list of buffer handles to be set. In 4GL, the maximum number of
    *          buffers per query is 18.
    *
    * @return  {@code true} on success.
    */
   boolean setBuffersImpl(Buffer... bufs)
   {
      if (!isAlterable())
      {
         ErrorManager.recordOrShowError(11858);
         // Cannot add/set buffers on a static dataset.
         return false;
      }
      
      // the list of buffers is cleared even if errors are encountered and method is aborted
      this.buffers.clear();
      
      BufferManager bm = null;
      Class<?> initClass = pm.getInstantiatingExternalProgramClass();
      
      List<BufferImpl> localBuffers = new ArrayList<>(bufs.length);
      for (Buffer buf : bufs)
      {
         if (buf == null)
         {
            continue;
         }
         BufferImpl proxy = (BufferImpl) buf;
         BufferImpl bufRef = proxy.ref();
         if (initClass == null)
         {
            // when initiating a legacy OE class, we don't know the source code where this is
            // happening - but the buffer has the definition class emitted; so force that
            if (bm == null)
            {
               bm = bufRef.buffer().bufferManager;
            }
            Class<?> def = bm.getPendingBufferClass(bufRef.buffer());
            pm.setInstantingExternalProgramClass(def);
         }
         
         if (bufRef.isBeforeBuffer())
         {
            ErrorManager.recordOrShowError(15008, bufRef.doGetName());
            // You may not add before-table buffer <buffer> to a DATASET.
            return false;
         }
         
         // check for duplications:
         DataSet ds = bufRef._dataSet();
         if (localBuffers.contains(bufRef) || (ds != null) && ds.valid() && ds != this)
         {
            ErrorManager.recordOrShowError(11860, bufRef.doGetName());
            // Buffer <name> may not appear more than once in any dataset.
            return false;
         }
         
         localBuffers.add(bufRef);
         proxies.put(bufRef, proxy);
      }
      
      pm.setInstantingExternalProgramClass(initClass);
      
      // when everything is fine:
      this.buffers.addAll(localBuffers);
      localBuffers.forEach(b -> b.setDataSet(this));
      
      return true;
   }

   /**
    * Read the DATASET from XML.
    * <p>
    * This is used when the remote call sent the DATASET as XML and it must receive it in the same format
    * (used by SOAP callers).
    * 
    * @param    xmlDataset
    *           The XML serialized dataset.
    * @param    append
    *           Flag indicating if the read mode is 'append'. Otherwise, is 'empty.
    * @param    ds
    *           The handle where to read the dataset.
    */
   @SuppressWarnings("resource")
   private static void readFromXml(String xmlDataset, boolean append, handle ds)
   {
      character readMode = new character(append ? "APPEND" : "EMPTY");
      
      if (!ds._isValid())
      {
         DataSet.create(ds);
      }
      
      DataSet dsTarget = (DataSet) ds.getResource();
      longchar lc = new longchar();
      longchar.fixCodePage(lc, "utf-8");
      lc.assign(xmlDataset);
      dsTarget.readXml(new SourceData("longchar", lc), 
                       readMode, 
                       new character(), 
                       logical.of(false));
   }
   
   /**
    * Lookup a buffer by its name.
    * 
    * @param   name
    *          The name to search among the list of buffers.
    * 
    * @return  The buffer whose name was specified or {@code null} when there is no match. 
    */
   public BufferImpl getBufferByName(String name)
   {
      List<BufferImpl> proxyBuffers = proxies.values().stream().collect(Collectors.toList());

      for (int i = 0; i < proxyBuffers.size(); i++)
      {
         BufferImpl buf = proxyBuffers.get(i);
         if (buf._name().equalsIgnoreCase(name))
         {
            return buf;
         }
      }
      return null;
   }

   /**
    * Lookup a buffer by its serialize-name.
    * 
    * @param   serializeName
    *          The serialize-name to search among the list of buffers.
    * 
    * @return  The buffer whose serialize-name was specified or {@code null} when there is no match.
    */
   public BufferImpl getBufferBySerializeName(String serializeName)
   {
      if (serializeName == null || serializeName.isEmpty())
      {
         return null; //quick out
      }
      List<BufferImpl> proxyBuffers = proxies.values().stream().collect(Collectors.toList());

      for (int i = 0; i < proxyBuffers.size(); i++)
      {
         BufferImpl buf = proxyBuffers.get(i);
         if (serializeName.equalsIgnoreCase(buf.getSerializeName().toJavaType()))
         {
            return buf;
         }
      }
      return null;
   }

   /**
    * Flush all buffers (including secondary buffers and associated before-buffers) used in this dataset.
    * 
    * @return   {@code true} if all buffers were flushed.
    */
   private boolean flushAllBuffers()
   {
      for (Buffer buf : buffers)
      {
         RecordBuffer buffer = ((BufferImpl) buf).buffer();
         if (!buffer.getParentTable().flushBuffers())
         {
            return false;
         }
      }
      
      return true;
   }
   
   /**
    * Obtain the current {@code TOP-NAV-QUERY} for a specified buffer. Uses lazy initialization:
    * if not exist one is created (and prepared, but not open) now. 
    * 
    * @param   buffer
    *          The buffer whose {@code TOP-NAV-QUERY} is to be returned.
    *
    * @return  A {@code handle} to {@code TOP-NAV-QUERY} for specified buffer.
    */
   private handle getTopNavQuery(BufferImpl buffer)
   {
      buffer = buffer.ref();
      
      QueryWrapper query = topNavQueries == null ? null : topNavQueries.get(buffer);
      if (query == null || !query.valid())
      {
         query = QueryWrapper.createQueryUnnamedPool();
         query.setBuffers(buffer);
         query.prepare("FOR EACH " + buffer._name());
         if (topNavQueries == null)
         {
            topNavQueries = new HashMap<>();
         }
         topNavQueries.put(buffer, query);
         if (deleteQueries == null)
         {
            deleteQueries = Collections.newSetFromMap(new IdentityHashMap<>());
         }
         deleteQueries.add(query);
      }
      
      return new handle(query);
   }
   
   /**
    * Notifies the user about an attempt to set an invalid value for {@code TOP-NAV-QUERY } attribute. Two
    * error messages will be issued, the former being 14293 and the latter the one specified as parameter.
    *
    * @param   errCode
    *          The message code (4056 or 4061).
    * @param   bufName
    *          The buffer's name.
    */
   private static void invalidTopNavQuery(int errCode, String bufName)
   {
      ErrorManager.recordOrShowError(14293, bufName);
      // TOP-NAV-QUERY requires a valid query on top dataset buffer <name>
      ErrorManager.recordOrShowError(errCode, "TOP-NAV-QUERY", "DATASET widget");
      // **Attribute <attribute> for the <widget id> has an invalid value of UNKNOWN. (4056)
      //    or
      // **Attribute  for the <widget id> has an invalid value. (4061)
   }
   
   /**
    * Low level implementation of {@code TOP-NAV-QUERY} method. Does the final parameter checking before
    * mapping the query to the buffer.
    *
    * @param   queryHandle
    *          The query to be set.
    * @param   topBuffer
    *          The top level buffer.
    */
   private void _setTopBufferQuery(handle queryHandle, BufferImpl topBuffer)
   {
      if (queryHandle == null || queryHandle.isUnknown())
      {
         invalidTopNavQuery(4056, topBuffer.name().toStringMessage());
         return;
      }
      
      if (!queryHandle.isType(LegacyResource.QUERY))
      {
         invalidTopNavQuery(4061, topBuffer.name().toStringMessage());
         return;
      }
      
      QueryWrapper qv = (QueryWrapper) queryHandle.getResource();
      if (qv.bufferHandleNative() != topBuffer)
      {
         invalidTopNavQuery(4061, topBuffer.name().toStringMessage());
         return;
      }
      
      if (topNavQueries == null)
      {
         topNavQueries = new HashMap<>();
      }
      topNavQueries.put(topBuffer, qv);
   }
   
   /**
    * Checks whether <strong>AT LEAST</strong> one data-relation objects in this {@code DataSet}
    * object is active. All data-relation objects in a {@code DataSet} object are active by default.
    *
    * @return  {@code true} when all data-relation objects are active.
    */
   boolean _areRelationsActive()
   {
      if (relations.isEmpty() /*|| !dynamic*/)
      {
         // initial testcases suggested that, in case of relations definined in static dataset the [active] is
         // still true although all relations were individually deactivated. It seems that this behaviour is
         // caused by other (combination of) factors
         return defaultActive;
      }
      
      for (int i = 0; i < relations.size(); i++)
      {
         DataRelation rel = relations.get(i);
         if (rel._isActive())
         {
            return true;
         }
      }
      
      // could not find any active relation
      return false;
   }
   
   /**
    * Adds a data-relation object for a pair of parent and child buffers to this {@code DataSet} object.
    *
    * @param   relName
    *          The name of the relation.
    * @param   parent
    *          The handle to the parent buffer in the data-relation object.
    * @param   child
    *          The handle to the child buffer in the data-relation object.
    * @param   fields
    *          An array of (parent-field, child-field) pairs describing the relationship between
    *          parent and child buffers in the new data-relation object.
    * @param   strPairs 
    *          The pair of fields, as string as they appear in the legacy statement.
    * @param   reposition
    *          The reposition mode. If {@code TRUE}, the relation mode is {@code REPOSITION}.
    *          Otherwise the relation mode is {@code SELECTION}. The default value is
    *          {@code SELECTION}.
    * @param   nested
    *          Use {@code TRUE} to nest child rows of {@code DataSet} buffers within their parent
    *          rows when writing the XML representation of data. When {@code FALSE} is passed, all
    *          child rows are written after all parent rows are written. The default value is {@code FALSE}.
    * @param   active
    *          Use {@code true} to make the newly created relation active.
    * @param   recursive
    *          Using {@code TRUE} will cause the data-relation to be filled recursively.
    * @param   fkeyHidden
    *          When set to {@code TRUE} the foreign key fields in nested data-relations will be
    *          omitted when writing the XML representation of data. If this option is used, the
    *          {@code NESTED} argument must be {@code TRUE}.
    * @param   dynamic
    *          Use {@code true} for dynamically created relations.
    *
    * @return  A {@code handle} to the newly created {@code DataRelation}.
    */
   private handle addRelationWorker(String relName,
                                    Buffer parent,
                                    Buffer child,
                                    String[] fields,
                                    String strPairs,
                                    boolean reposition,
                                    boolean nested,
                                    boolean active,
                                    boolean recursive,
                                    boolean fkeyHidden,
                                    boolean dynamic)
   {
      DataRelation dR = new DataRelation(this, parent, child, fields, strPairs, recursive);
      dR.name(relName != null ? relName : "RELATION" + (relations.size() + 1));
      dR.setReposition(reposition);
      dR.setNested(nested);
      dR.setActive(active);
      dR.setForeignKeyHidden(fkeyHidden);
      if (dynamic)
      {
         dR.confirmDynamic();
      }
      
      relations.add(dR);
      return new handle(dR);
   }
   
   /**
    * Checks whether this object can be altered (added buffers and relations). Normally, the
    * static defined {@code DataSet} s are not alterable, but they can 'altered' by their 
    * {@code Builder} while the object is being constructed.
    * 
    * @return  {@code true} if new buffers or relations can be added.
    */
   private boolean isAlterable()
   {
      return _dynamic() || buildInProgress;
   }
   
   /**
    * Test whether a string is a valid CALLBACK name for {@code DataSet}s. This method is responsible of
    * raising the 11950 error if the name specified is unknown or not a name of a known callback for
    * {@code DataSet}.
    * 
    * @param   someName
    *          The name to test.
    *
    * @return  {@code true} only if {@code someName} is equals to "BEFORE-FILL" or "AFTER-FILL",
    *          in ignore-case mode.
    */
   private static boolean isCallbackName(String someName)
   {
      if (someName == null || 
          !BEFORE_FILL_EVENT.equalsIgnoreCase(someName) &&
          !AFTER_FILL_EVENT.equalsIgnoreCase(someName))
      {
         ErrorManager.recordOrShowError(11950, someName != null ? someName : "");
         // Argument <arg> to CALLBACK method must be a valid buffer or dataset event name.
         return false;
      }
      
      return true; 
   }
   
   /**
    * Get the {@link InvokeConfig callsite} for the specified event.
    * 
    * @param    eventName
    *           The event name.
    *
    * @return   see above
    */
   private static InvokeConfig getCallSite(String eventName)
   {
      return (BEFORE_FILL_EVENT.equals(eventName.toUpperCase()) 
                ? BEFORE_FILL_CALL_SITE 
                : AFTER_FILL_CALL_SITE);
   }
   
   /**
    * Obtain the list of top-buffers. From the list of all buffers are removed those buffers that
    * are found as child buffers in an active relation.
    *
    * @return  the list of top-buffers.
    */
   private List<Buffer> getTopBufferList()
   {
      List<Buffer> topBuffers = new LinkedList<>(buffers);
      
      for (int i = 0; i < relations.size(); i++)
      {
         DataRelation relation = relations.get(i);
         if (relation._isActive() && 
             !relation._isReposition() && 
             !relation.isRecursive().booleanValue())
         {
            topBuffers.remove(relation.getChildBufferNative());
         }
      }
      
      return topBuffers;
   }
   
   /**
    * Actual implementation of {@code CREATE-LIKE} method. The {@code sourceDS} structure,
    * including {@code TempTable} and {@code DataRelation} definitions, is copied to the this
    * {@code DataSet}; the source data, however, is not copied.
    * 
    * @param   sourceDS
    *          The source {@code DataSet}.
    * @param   prefix
    *          The name prefix for the newly created  {@code DatSet}.
    *
    * @return  {@code true} on success (and this object is a copy of the {@code sourceDS}), and
    *          {@code false} if the copy operation failed.
    */
   private logical createLikeImpl(DataSet sourceDS, String prefix)
   {
      if (prefix == null)
      {
         prefix = "";
      }
      
      if (!validateName(prefix))
      {
         // no error thrown now, one was already issued by validateName()
         return logical.of(false);
      }
      
      if (!_dynamic())
      {
         ErrorManager.recordOrShowError(11969);
         // Unable to CLEAR target CREATE-LIKE dataset.
         return logical.of(false);
      }
      
      // make sure the old content is gone:
      clearImpl(false, false);
      
      name(prefix + sourceDS.name().getValue());
      
      BufferImpl[] srcBuffers = sourceDS.ref().buffers.toArray(new BufferImpl[sourceDS.getBuffers().size()]);
      BufferImpl[] newBuffers = new BufferImpl[srcBuffers.length];
      for (int i = 0; i < newBuffers.length; i++)
      {
         handle atth = TypeFactory.handle();
         TempTableBuilder.create(atth);
         BufferImpl srcAfterBuff = srcBuffers[i];
         BufferImpl srcB4Buff = srcAfterBuff.beforeBufferNative();
         String beforeName = srcB4Buff != null ? prefix + srcB4Buff._name() : null;

         String tableName = prefix + srcAfterBuff.buffer().getParentTable()._name();
         if (tableName.length() > 32)
         {
            // max length is 32
            tableName = tableName.substring(0, 32);
         }
         
         TempTableBuilder newAtt = (TempTableBuilder) atth.getResource();
         newAtt.createLike(new handle(srcAfterBuff));
         newAtt.tempTablePrepareImpl(tableName, beforeName, false);
         
         // TODO: anything to copy at the before-table?
         copyTempTableOptions((AbstractTempTable) srcAfterBuff.tableHandleNative(), newAtt);
         
         newBuffers[i] = (BufferImpl) newAtt.defaultBufferHandleNative();
      }
      setBuffersImpl(newBuffers);
      // in OE 11.6 the [xmlNodeName] was observed to be copied in a different fashion 
      // if (xmlNodeName == null)
      // {
      //    setXmlNodeName(prefix + sourceDS.getXmlNodeName().toStringMessage());
      // }
      // in OE the attributes are reportedly set as follows:
      nsUri = null;     //unknown
      nsPrefix = null;  //unknown
      xmlNodeName = sourceDS.xmlNodeName;       // copied
      serializeName = sourceDS.serializeName;   // copied
      
      ArrayList<DataRelation> dataRelations = sourceDS.ref().relations;
      for (int i = 0; i < dataRelations.size(); i++)
      {
         DataRelation srcRel = dataRelations.get(i);
         BufferImpl srcChBuffer = srcRel.getChildBufferNative();
         BufferImpl srcParBuffer = srcRel.getParentBufferNative();
         
         BufferImpl bufParent = newBuffers[positionOf(srcBuffers, srcParBuffer)];
         Buffer bufChild = newBuffers[positionOf(srcBuffers, srcChBuffer)];
         if (srcRel.isParentIdRelation().booleanValue())
         {
            DataRelation dR = new DataRelation(this, bufParent, bufChild, 
                        srcRel._getRelationFields(), new ArrayList<>(), new ArrayList<>(), true);
            relations.add(dR);
            
            // in OE 12+ it is reported that the name is not copied 
            //    dR.name(srcRel.name());
            // instead it is generated automatically:
            dR.name("RELATION" + relations.size());
            // TODO: test whether this happens for simple (non parent-id) relations
         }
         else 
         {
            addRelationWorker(srcRel.name().toStringMessage(),
                              bufParent,
                              bufChild,
                              srcRel.getRelationFieldArray(),
                              srcRel._getRelationFields(),
                              srcRel._isReposition(),
                              srcRel.isNested().getValue(),
                              srcRel._isActive(),
                              srcRel.isRecursive().getValue(),
                              srcRel.getForeignKeyHidden().getValue(),
                              true);
         }
      }
      
      // everything is fine:
      return logical.of(true);
   }
   
   /**
    * Copy the temp-table options from the source to the destination.
    * 
    * @param    src
    *           The source table.
    * @param    dst
    *           The destination table.
    */
   private static void copyTempTableOptions(AbstractTempTable src, AbstractTempTable dst)
   {
      // TODO: any other options to copy?
      
      // copy the following from temp-table:
      // XML-NODE-NAME
      // SERIALIZE-NAME
      
      if (!src.getXmlNodeName().isUnknown())
      {
         dst.setXmlNodeName(src.getXmlNodeName());
      }
      if (!src.getSerializeName().isUnknown()) 
      {
         dst.setSerializeName(src.getSerializeName());
      }
      
      // copy the following from fields:
      // SERIALIZE-NAME
      // SERIALIZE-HIDDEN
      // XML-DATA-TYPE
      // XML-NODE-TYPE
      // XML-NODE-NAME
      
      BufferImpl bufSrc = (BufferImpl) src.defaultBuffer().getDMOProxy();
      AbstractTempTable srcTT = (AbstractTempTable) src.defaultBuffer().tableHandleResource();
      BufferImpl bufDst = (BufferImpl) dst.defaultBuffer().getDMOProxy();
      int numFields = bufSrc._numFields();
      Object[] fields =  (Object[]) bufSrc.buffer().getDmoInfo().getExistingFields().toArray();
      for (int i = 0; i < numFields; i++)
      {
         BufferFieldImpl dstField = bufDst.bufferFieldNative(i + 1);
         
         // check if the sources has mutable info which has changed
         TableMapper.MutableFieldInfo srcFieldInfo = srcTT.getMutableInfo(((P2JField) fields[i])
                                                                          .getName().toLowerCase(),
                                                                          false,
                                                                          null);
         if (srcFieldInfo != null)
         {
            // if this has been mutated in the source temp-table, then copy it; otherwise, let the defaults

            SerializeOptions srcSerOpts = srcFieldInfo.getSerializeOptions();
            
            if (srcSerOpts.getSerializeName() != null)
            {
               dstField.setSerializeName(srcSerOpts.getSerializeName());
            }
            dstField.setSerializeHidden(srcSerOpts.isSerializeHidden());
            if (srcSerOpts.getXmlDataType() != null)
            {
               dstField.setXmlDataType(srcSerOpts.getXmlDataType());
            }
            if (srcSerOpts.getXmlNodeType() != null)
            {
               dstField.setXmlNodeType(srcSerOpts.getXmlNodeType());
            }
            if (srcSerOpts.getXmlNodeName() != null)
            {
               dstField.setXmlNodeName(srcSerOpts.getXmlNodeName());
            }
            if (srcFieldInfo.getColumnLabel() != null)
            {
               dstField.setColumnLabel(srcFieldInfo.getColumnLabel());
            }
            if (srcFieldInfo.getHelp() != null)
            {
               dstField.setHelp(srcFieldInfo.getHelp());
            }
            if (srcFieldInfo.getLabel() != null)
            {
               dstField.setLabel(srcFieldInfo.getLabel());
            }
         }
      }
   }
   
   /**
    * Locates a {@code BufferImpl} in an array of objects of same type.
    * 
    * @param   buffers
    *          A buffers of items.
    * @param   item
    *          The item to be located.
    * 
    * @return  the first occurrence of the {@code item} in the {@code buffers} or -1 if not found.
    */
   private static int positionOf(BufferImpl[] buffers, BufferImpl item)
   {
      for (int i = 0; i < buffers.length; i++)
      {
         if (item.ref() == buffers[i].ref())
         {
            return i;
         }
      }
      
      return -1;
   }
   
   /**
    * Declares this static {@code DataSet} as build. This will prohibit some of the setters to be
    * called.
    */
   private void finishBuild()
   {
      buildInProgress = false;
   }
   
   /**
    * Builder class that helps construct a {@code DataSet} object statically, that is, using the
    * {@code DEFINE DATASET} statement. It exposes a set of chaining methods that take each
    * option parameters and set them into the {@code DataSet} being constructed. The chaining must
    * finish with {@link #generate()} to obtain the final {@code DataSet} object. Before
    * returning, the {@code generate()} will do the final checks and register the object to be
    * returned with the {@link DataSetManager}.
    */
   public static class Builder
   {
      /** This is a builder for a shared {@code DataSet}. */
      private final boolean isShared;
      
      /** This is a builder for a shared new {@code DataSet}. */
      private final boolean isNew;
      
      /** The legacy name of the {@code DataSet}. */
      private final String legacyName;
      
      /** This is a reference of a {@code DataSet} defined in another procedure / class. */
      private boolean referenceOnly = false;
      
      /** The build actions to perform. */
      private final ArrayList<Runnable> buildChain = new ArrayList<>(7);
      
      /** The build dataset instance. */
      private DataSet target = null;

      /** The defining type where this dataset is constructed. */
      private Class<?> definingType = null;
      
      /**
       * The constructor. Initialize the private final fields that will be used in the final step
       * of the construction: to parametrize the registration of the {@code DataSet}.
       *
       * @param   legacyName
       *          The legacy name of the {@code DataSet}.
       * @param   isShared
       *          This is a shared {@code DataSet}.
       * @param   isNew
       *          This is a new {@code DataSet}. Only in combination with {@code isShared}.
       */
      public Builder(String legacyName, boolean isShared, boolean isNew)
      {
         this.legacyName = legacyName;
         this.isShared = isShared;
         this.isNew = isNew;
      }
      
      /**
       * Set the defining type where this dataset is constructed.
       * 
       * @param    type
       *           The defining type.
       *           
       * @return   A reference of this object in order to help chaining.
       */
      public Builder setDefiningType(Class<?> type)
      {
         this.definingType = type;
         return this;
      }
      
      /**
       * Sets the namespace attribute. Corresponds to {@code NAMESPACE-URI} option from 
       * {@code DEFINE DATASET} statement.
       * 
       * @param   namespace
       *          The namespace for the {@code DataSet} to be constructed.
       * 
       * @return  A reference of this object in order to help chaining.
       */
      public Builder setNamespaceUri(String namespace)
      {
         buildChain.add(() -> target.namespaceURI(namespace));
         return this;
      }
      
      /**
       * Sets the ns prefix attribute. Corresponds to {@code NAMESPACE-PREFIX} option from 
       * {@code DEFINE DATASET} statement.
       *
       * @param   prefix
       *          The ns prefix for the {@code DataSet} to be constructed.
       *
       * @return  A reference of this object in order to help chaining.
       */
      public Builder setNamespacePrefix(String prefix)
      {
         buildChain.add(() -> target.namespacePrefix(prefix));
         return this;
      }
      
      /**
       * Sets the name of the xml node attribute. Corresponds to {@code XML-NODE-NAME} option from 
       * {@code DEFINE DATASET} statement.
       *
       * @param   nodeName
       *          The xml node name for the {@code DataSet} to be constructed.
       *
       * @return  A reference of this object in order to help chaining.
       */
      public Builder setXmlNodeName(String nodeName)
      {
         buildChain.add(() -> target.setXmlNodeName(nodeName));
         return this;
      }
      
      /**
       * Sets the serialize name attribute. Corresponds to {@code SERIALIZE-NAME} option from 
       * {@code DEFINE DATASET} statement.
       *
       * @param   serName
       *          The serialize name for the {@code DataSet} to be constructed.
       *
       * @return  A reference of this object in order to help chaining.
       */
      public Builder setSerializeName(String serName)
      {
         buildChain.add(() -> target.setSerializeName(serName));
         return this;
      }
      
      /**
       * Sets the xml node type attribute. Corresponds to {@code XML-NODE-TYPE} option from 
       * {@code DEFINE DATASET} statement.
       *
       * @param   nodeType
       *          The node type for the {@code DataSet} to be constructed.
       *
       * @return  A reference of this object in order to help chaining.
       */
      public Builder setXmlNodeType(String nodeType)
      {
         buildChain.add(() -> target.setXmlNodeType(nodeType));
         return this;
      }
      
      /**
       * Mark the DataSet's serialize-hidden attribute. Corresponds to {@code SERIALIZE-HIDDEN}
       * option from {@code DEFINE DATASET} statement.
       *
       * @return  A reference of this object in order to help chaining.
       */
      public Builder setSerializeHidden()
      {
         buildChain.add(() -> target.setSerializeHidden(true));
         return this;
      }
      
      /**
       * Mark the DataSet as reference only. Corresponds to {@code REFERENCE-ONLY} option from
       * {@code DEFINE DATASET} statement.
       *
       * @return  A reference of this object in order to help chaining.
       */
      public Builder setReferenceOnly()
      {
         this.referenceOnly = true;
         return this;
      }
      
      /**
       * Configures the set of buffers for the {@code DataSet}. Corresponds to {@code FOR} option
       * from {@code DEFINE DATASET} statement.
       *
       * @param   bufs
       *          The set buffers for the {@code DataSet} to be constructed.
       *
       * @return  A reference of this object in order to help chaining.
       */
      public Builder forBuffers(Buffer... bufs)
      {
         buildChain.add(() -> target.setBuffersImpl(bufs));
         return this;
      }
      
      /**
       * Configure a data relation. Corresponds to {@code DATA-RELATION} set of options from
       * {@code DEFINE DATASET} statement.
       *
       * @param   relName
       *          The relation name.
       * @param   reposition
       *          Specifies the relation mode as {@code REPOSITION} as opposed to the default
       *          {@code SELECTION} relation mode.
       * @param   nested
       *          If {@code true}, the child rows of a {@code DataSet} buffer are nested within
       *          their parent rows when writing a JSON representation of data or an XML
       *          representation of data or schema.
       * @param   fkHidden
       *          Specifies that foreign key fields in nested data relations are omitted when
       *          writing JSON or XML data, and that foreign key fields are optional in the XML
       *          Schema representation of the child table definition.
       *          If {@code NESTED} option is {@code false}, this option is ignored.
       * @param   inactive
       *          Setting this to {@code true} allows to have two relations between the same two
       *          DataSet temp-table buffers, but only have one relation active at a time.
       * @param   recursive
       *          Allows DataSet {@code FILL} method to load self-referencing elements.
       * @param   parent
       *          The parent buffer in this data-relation. This must be one of the buffers
       *          identified by the buffers added with {@code forBuffers()} above.
       * @param   child
       *          The child buffer in this data-relation. This must be one of the buffers
       *          identified by the buffers added with {@code forBuffers()} above.
       * @param   relationFieldPairs
       *          A set of field pairs (parent, child)*. 
       *
       * @return  A reference of this object in order to help chaining.
       */
      public Builder addDataRelation(String relName,
                                     boolean reposition,
                                     boolean nested,
                                     boolean fkHidden,
                                     boolean inactive,
                                     boolean recursive,
                                     Buffer parent,
                                     Buffer child,
                                     FieldPair... relationFieldPairs)
      {
         logical ok = logical.of(true);
         
         buildChain.add(() -> {
            BufferImpl bufParent = ((BufferImpl) parent).ref();
            BufferImpl bufChild = ((BufferImpl) child).ref();
            if (!target.getBuffers().contains(bufParent) || !target.getBuffers().contains(bufChild))
            {
               ErrorManager.throwError(12384);
               // DATA-RELATION parent and child buffers must also be in the dataset.
               ok.assign(false);
               return;
               // this is a compile-time error, followed by "Could not understand line <number>. (196)
            }
            
            StringBuilder strPairs = new StringBuilder();
            String[] fields = new String[relationFieldPairs.length * 2];
            for (int i = 0; i < relationFieldPairs.length; i++)
            {
               if (strPairs.length() != 0)
               {
                  strPairs.append(",");
               }
               
               String parentField = relationFieldPairs[i].parentField;
               if (StringHelper.startsWithIgnoreCase(parentField, bufParent.doGetName() + "."))
               {
                  parentField = parentField.substring(bufParent.doGetName().length() + 1);
               }
// this will cause an NPE because the buffer scope is not yet open so the parent table is not
// configured
//            if (!bufParent.hasField(parentField))
//            {
//               ErrorManager.throwError(12385);
//               // Pairs in DATA-RELATION RELATION-FIELDS phrase must be in the form: parent-field, child-field, parent-field, child-field etc.
//               return null;
//               // this is a compile-time error, followed by "Could not understand line <number>. (196)
//            }
               
               String childField = relationFieldPairs[i].childField;
               if (StringHelper.startsWithIgnoreCase(childField, bufChild.doGetName() + "."))
               {
                  childField = childField.substring(bufChild.doGetName().length() + 1);
               }
// this will cause an NPE because the buffer scope is not yet open so the parent table is not
// configured
//            if (!bufChild.hasField(childField))
//            {
//               ErrorManager.throwError(12385);
//               // Pairs in DATA-RELATION RELATION-FIELDS phrase must be in the form: parent-field, child-field, parent-field, child-field etc.
//               return null;
//               // this is a compile-time error, followed by "Could not understand line <number>. (196)
//            }
               
               fields[2 * i] = parentField;
               fields[2 * i + 1] = childField;
               strPairs.append(parentField).append(",").append(childField);
            }
            
            target.addRelationWorker(relName, parent, child, fields, strPairs.toString(),
                                     reposition, nested, !inactive, recursive, fkHidden, false);
         });
         
         return ok.getValue() ? this : null;
      }
      
      /**
       * This method chains a {@code PARENT-ID-RELATION} option to the {@code DataSet} builder.
       * 
       * @param   relName
       *          A name for the data relation.
       * @param   parentBuffer
       *          The parent buffer.
       * @param   childBuffer
       *          The child buffer.
       * @param   parentIdField
       *          The field from child buffer of {@code RECID} type that points to a record in
       *          parent buffer.
       * 
       * @return  A reference of this object in order to help chaining.
       */
      public Builder addParentIdRelation(String relName,
                                         Buffer parentBuffer,
                                         Buffer childBuffer,
                                         String parentIdField)
      {
         buildChain.add(() -> {
            DataRelation dR = new DataRelation(target, parentBuffer, childBuffer,
                                               parentIdField, new ArrayList<>(), new ArrayList<>(), false);
            
            dR.name(relName);
            // the REPOSITION attribute might be set, depending on the OE version
            // dR.setReposition(true);
            target.ref().relations.add(dR);
         });
         return this;
      }
      
      /**
       * This method chains a {@code PARENT-ID-RELATION} option to the {@code DataSet} builder.
       * 
       * @param   relName
       *          A name for the data relation.
       * @param   parentBuffer
       *          The parent buffer.
       * @param   childBuffer
       *          The child buffer.
       * @param   parentIdField
       *          The field from child buffer of {@code RECID} type that points to a record in
       *          parent buffer. 
       * @param   parentFieldsBefore
       *          The list of fields from the {@code parentBuffer} temp-table record that are to
       *          be written to XML before any {@code childBuffer} temp-table records.
       * @param   parentFieldsAfter
       *          The list of fields from the {@code parentBuffer} temp-table record that are to
       *          be written to XML after all {@code childBuffer} temp-table records.
       * 
       * @return  A reference of this object in order to help chaining.
       */
      public Builder addParentIdRelation(String relName,
                                         Buffer parentBuffer,
                                         Buffer childBuffer,
                                         String parentIdField,
                                         String[] parentFieldsBefore,
                                         String[] parentFieldsAfter)
      {
         buildChain.add(() -> {
            DataRelation dR = new DataRelation(target,
                                               parentBuffer,
                                               childBuffer,
                                               parentIdField,
                                               Arrays.asList(parentFieldsBefore),
                                               Arrays.asList(parentFieldsAfter),
                                               false);
            dR.name(relName);
            // the REPOSITION attribute might be set, depending on the OE version
            // dR.setReposition(true);
            target.ref().relations.add(dR);
         });
         return this;
      }
      
      /**
       * Validates and registers the {@code DataSet} to {@code DataSetManager}, making it valid.
       * 
       * @return  the parent {@code DataSet}.
       */
      public DataSet generate() 
      {
         target = new DataSet(legacyName, false, referenceOnly);
         
         // the actual build of the dataset
         for (int i = 0; i < buildChain.size(); i++)
         {
            Runnable step = buildChain.get(i);
            step.run();
         }
         
         // prevent further options to be set
         target.finishBuild();
         
         target = StaticDataSet.create(target);
         
         ((StaticDataSet) target).setDefiningType(definingType);
         
         // register with DataSetManager.
         DataSetManager.instance().register(target, legacyName, isShared, isNew);
         
         // end chain by returning the final object 
         return target;
      }
   }
   
   /**
    * Container immutable class that abstracts a field mapping between the parent and child
    * tables/buffers in a data relation.
    */
   public static class FieldPair
   {
      /** The parent field of the relation. */
      private final String parentField;
      
      /** The child field of the relation. */
      private final String childField;
      
      /**
       * The constructor 
       * @param   parentField
       *          The parent field of the relation.
       * @param   childField
       *          The child field of the relation.
       */
      public FieldPair(String parentField, String childField)
      {
         this.parentField = parentField;
         this.childField = childField;
      }
   }
   
   /**
    * Flag for debugging level. When {@code true} the full dataset is returned at the expense of CPU clocks.
    * When {@code false} only the essential dataset structure is returned. Intentionally this is defined as
    * non-final private here and has no accessors so that it can be r/w only at debug time.
    */
   private static boolean fullDebugString = false;
   
   /**
    * Obtain a short description of the dataset used for debugging. If {@code fullDebugString} is {@code true}
    * the full dataset (all rows of all buffers) is dumped as a JSON string. If only the dataset structure
    * is needed, set {@code fullDebugString} to {@code false}.
    * 
    * @return  a short description of the dataset used for debugging.
    */
   @Override
   public String toString()
   {
      return fullDebugString ? getDataAsJson() : getSchema();
   }
   
   /**
    * Obtain a short description of the dataset structure: buffers and relations between them.
    * 
    * @return  a string describing the important elements of the dataset structure.
    */
   private String getSchema()
   {
      StringBuilder sb = new StringBuilder();
      sb.append("{").append(name).append(": ");
      for (int i = 0; i < buffers.size(); i++)
      {
         BufferImpl buffer = buffers.get(i);
         sb.append(buffer.doGetName()).append(" ");
      }
      sb.append("(");
      for (int i = 0; i < relations.size(); i++)
      {
         DataRelation rel = relations.get(i);
         sb.append(rel.name().getValue()).append("=");
         sb.append(rel.getParentBufferNative().doGetName()).append("+");
         sb.append(rel.getChildBufferNative().doGetName()).append(" ");
      }
      sb.append(") }");
      return sb.toString();
   }
   
   /**
    * Get the JSON representation of data of this DATASET. The returned value contains data from all tables of
    * the dataset, but not the relations between them.
    *
    * @return   See above.
    */
   private String getDataAsJson() 
   {
      ObjectMapper objectMapper = new ObjectMapper();
      Map<String, Object> fullResult = new HashMap<>();
      Map<String, Object> datasetResult = new HashMap<>();

      fullResult.put(name, datasetResult);
      for (BufferImpl buffer : buffers)
      {
         String tableName = buffer.buffer().getDmoInfo().legacyTable;
         RecordBufferSerializer recordBufferSerializer = new RecordBufferSerializer(buffer.buffer());
         try
         {
            datasetResult.put(tableName, recordBufferSerializer.sqlTableContent());
         }
         catch (PersistenceException | SQLException e)
         {
            log.severe("Error getting data: " + e.getMessage());
            datasetResult.put("error", "Error getting data: " + e.getMessage());
         }
      }
      try
      {
         return objectMapper.writeValueAsString(fullResult);
      }
      catch (JsonProcessingException e)
      {
         log.severe("JSON processing error: " + e.getMessage());
         return "{\"error\": \"Error generating JSON\"}";
      }
   }
   
   /**
    * Get the SQL representation of data of this DATASET. The returned value contains data from all tables of
    * the dataset, but not the relations between them.
    *
    * @return   See above.
    */
   @SuppressWarnings("unused")
   private String getSqlData()
   {
      StringBuilder sb = new StringBuilder();
      for (int i = 0; i < buffers.size(); i++)
      {
         BufferImpl buffer = buffers.get(i);
         // output the main/after buffer content 
         if (sb.length() != 0)
         {
            sb.append("\n");
         }
         try
         {
            sb.append(buffer.buffer().sqlTableContent(0));
         }
         catch (PersistenceException | SQLException e)
         {
            sb.append("Error getting data: ").append(e.getMessage()).append("\n");
         }
         
         // if exists, output the before buffer content, too
         BufferImpl before = buffer.beforeBufferNative();
         if (before != null && before.valid())
         {
            try
            {
               sb.append("\n").append(before.buffer().sqlTableContent(0));
            }
            catch (PersistenceException | SQLException e)
            {
               sb.append("Error getting data: ").append(e.getMessage()).append("\n");
            }
         }
      }
      return sb.toString();
   }
}