# LegacyJavaAppserver.java

    /*
    ** Module   : LegacyJavaAppserver.java
    ** Abstract : Helper APIs to work with legacy JavaOpenClient.
    **
    ** Copyright (c) 2019-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
    ** 001 CA  20190717 Created initial version.
    ** 002 CA  20191119 Added support for before-table.
    **     CA  20191125 Added support for relations.
    ** 003 CA  20200923 API improvements. Added extent support, other data-types.  Allow access to the hidden
    **                  temp-table fields.
    **     CA  20200927 Use IdentityHashMap instead of plain map when the key is a Class.
    **     CA  20201008 Added HANDLE type.  Added addRelation API.
    **     OM  20201120 Added support for xmlNodeName, errorString temp-table attributes.
    **     CA  20210213 Modifying the signature of APIs in this class may result in incompatibilities with other
    **                  existing code, either in TRPL rules (for the proxy generation code), or in 3rd party
    **                  customer apps.  Always leave the previous signature and mark it as deprecated.
    **     CA  20210626 Fixed computeUniqueConstraintsClauses - the dataset name must be null if is not defined.
    **     CA  20210630 If the dataset has a table definition and is in OUTPUT mode, send the table definition.
    **     CA  20210816 rowid values can be passed as string.
    **     CA  20210831 Allow clients to always enable the change logging, as for the .NET client OE uses a
    **                  DataTable, which always track changes.
    **     CA  20210910 Changes to allow support for before-table returned from the remote request via an
    **                  OUTPUT/INPUT-OUTPUT table or dataset.
    **     CA  20210917 Javadoc fixes.
    **     OM  20211020 Added _DATASOURCE_ROWID property.
    **     CA  20210104 Added support for denormalized extent fields, for .NET client compatibility.
    **                  Fixed DATETIME-TZ field problem when the value is a Date, and not a GregorianCalendar.
    **     CA  20220110 Refactored the temp-table's row-meta state to use constants for the indexes in the array
    **                  used for serialization.
    **     CA  20220216 Fixed issues with denormalized extent field names: they can collide with other legacy
    **                  field names so the changes ensure they are unique.
    **     CA  20220615 Added 'getBeforeObjects'.
    **     CA  20220707 'getOutputParameter' must 'endLogging' only if changes are tracked by default.
    **     CA  20220923 Another case of 'getOutputParameter' must 'endLogging' only if changes are tracked
    **                  by default.
    **     CA  20221031 Added JMX instrumentation for table metadata and parameter processing.
    **     CA  20220428 Fixed TABLE, TABLE-HANDLE, MEMPTR and extent parameter support.  Replaced Double with
    **                  BigDecimal and date(time(tz)) with GregorianCalendar.  Fixed problems when invoking from
    **                  outside of FWD server.
    **     CA  20220513 Added 'getTableIndex' helpers.
    **     CA  20220524 Added helpers which work with table metadata, to retrieve field information.
    **     CA  20220527 Added helpers for relation metadata.
    **     CA  20220604 Replaced the FWD proxy with the Java proxy, to allow easy usage from Tomcat.
    **     CA  20220830 Performance improvements for management of the table metadata (especially related to
    **                  computing the SDO types associated with the table).
    **     CA  20220929 Added setNormalizedExtent.
    **     CA  20221006 Small performance improvement, avoid calling 'list.get' multiple times.
    **     CA  20221031 Added JMX instrumentation for table metadata and parameter processing.
    **     CA  20221130 Fixed GregorianCalendar conversion to 'date' or 'datetime' instances - in both cases, the
    **                  timezone from the GregorianCalendar must be used at the constructor.
    **     SVL 20230113 Improved performance by replacing some "for-each" loops with indexed "for" loops.
    **     CA  20230206 Fixed getter/setter APIs used to access a GregorianCalendar field in a DataObject.
    ** 004 CA  20230221 Encapsulated the field and relation metadata in their own classes, to avoid the DataObject
    **                  overhead and increase performance.  Other performance optimizations.
    **                  Added an option to not marshal the table schema - this needs more analysis, as currently
    **                  this flag is per-FWD connection.
    ** 005 CA  20230712 Allow a null argument for the metadata at LegacyJavAppserver.getJavaParameter,  when
    **                  DATASET-HANDLE or TABLE-HANDLE arguments are built.
    ** 006 HC  20240222 Enabled JMX on FWD Client.
    ** 007 CA  20240305 Added namespace URI, namespace prefix and XML node-name for table/graph metadata in
    **                  OpenClient.
    ** 008 GBB 20240423 Precision for decimal output params set to max.
    ** 009 CA  20240426 For OUTPUT dataset/table, if the argument has already the metadata set, re-initialize this
    **                  metadata with the actual response argument, while keeping the metadata references intact.
    ** 010 CA  20240501 A before-table row in ROW-CREATED state will not transfer the full field, but default
    **                  values to the remote side.
    ** 011 AD  20240509 In method getJavaParameter, now copying the dataset relations from the metadata as well.
    ** 012 CA  20240707 Performance improvement when building an output table parameter: use a list to collect the
    **                  rows, and set them only once, as adding them one-by-one is expensive.
    ** 013 SP  20240717 Added safety check in getOutputParameter for TYPE_TABLE case when TableWraper is unknown.
    ** 014 CA  20240809 Skip using JMX timers when JMX_DEBUG flag is not set.
    ** 015 LS  20240807 In getOutputParameter, a map with the name and signature of each table is created
    **                  and sent as parameter to initGraphMetadata.
    **                  Updated DsTableDefinition and TableWrapper to use the table name from metadata.
    **     LS  20240819 Updated getOutputParameter so that, the comparison by name or signature will be done
    **                  based on a flag.
    ** 016 SP  20241007 Fixed toJavaValue(): ensure no trailing zeros are on the right-side of the decimal
    **     CA  20241009 For .NET compatibility, a scale of at least one is guaranteed for decimal values.
    ** 017 AS  20250213 Added high level APIs for index management.
    ** 018 CA  20250424 .NET compatibiltiy still uses normalized extent fields when transferring them.
    ** 019 AP  20250513 Improved createDataGraph method to copy dataset relations.
    */

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

    import java.lang.reflect.*;
    import java.math.*;
    import java.util.*;

    import com.goldencode.p2j.*;
    import com.goldencode.p2j.cfg.*;
    import com.goldencode.p2j.jmx.*;
    import com.goldencode.p2j.persist.*;

    import commonj.sdo.*;

    /**
     * The legacy Java Open Client for appserver relies on Tuscany SDO to create an in-memory DataSet.
     * This class provides helper APIs which do not map on Tuscany SDO existing APIs, and also
     * additional APIs to connect to the FWD appserver and perform calls.
     * <p>
     * This will be extended as more features of Java Open Client are required.
     */
    public class LegacyJavaAppserver
    implements LegacyJavaAppserverApi
    {
       /** Instrumentation for {@link #addTableMetaData}. */
       private static final NanoTimer TABLE_META = NanoTimer.getInstance(FwdServerJMX.TimeStat.AppserverAddTableMetaData);

       /** Instrumentation for {@link #getJavaParameter}. */
       private static final NanoTimer JAVA_PARAM= NanoTimer.getInstance(FwdServerJMX.TimeStat.ApperverGetJavaParameter);

       /** Instrumentation for {@link #getOutputParameter}. */
       private static final NanoTimer OUTPUT_PARAM = NanoTimer.getInstance(FwdServerJMX.TimeStat.AppserverGetOutputParameter);

       /** Mapping of an integer type to the {@link BaseDataType} type. */
       private static final Map<Integer, Class<?>> legacyTypes = new IdentityHashMap<>();

       /** Mapping of a {@link BaseDataType} type to its numeric type. */
       private static final Map<Class<?>, Integer> types2Legacy = new IdentityHashMap<>();

       /** Map of integer modes to their string representation in FWD. */
       private static final Map<Integer, String> paramModes = new IdentityHashMap<>();

       static
       {
          legacyTypes.put(TYPE_HANDLE, handle.class);
          legacyTypes.put(TYPE_INTEGER, integer.class);
          legacyTypes.put(TYPE_INT64, int64.class);
          legacyTypes.put(TYPE_DECIMAL, decimal.class);
          legacyTypes.put(TYPE_MEMPTR, MemoryBuffer.class);
          legacyTypes.put(TYPE_CLOB, clob.class);
          legacyTypes.put(TYPE_BLOB, blob.class);
          legacyTypes.put(TYPE_CHARACTER, character.class);
          legacyTypes.put(TYPE_LONGCHAR, longchar.class);
          legacyTypes.put(TYPE_LOGICAL, logical.class);
          legacyTypes.put(TYPE_DATE, date.class);
          legacyTypes.put(TYPE_DATETIME, datetime.class);
          legacyTypes.put(TYPE_DATETIMETZ, datetimetz.class);
          legacyTypes.put(TYPE_RAW, raw.class);
          legacyTypes.put(TYPE_ROWID, rowid.class);
          legacyTypes.put(TYPE_RECID, recid.class);
          legacyTypes.put(TYPE_UNKNOWN, unknown.class);
          legacyTypes.put(TYPE_DATASET, DataSetContainer.class);
          legacyTypes.put(TYPE_DATASET_HANDLE, DataSetContainer.class);
          legacyTypes.put(TYPE_TABLE_HANDLE, TableWrapper.class);

          paramModes.put(INPUT, "I");
          paramModes.put(OUTPUT, "O");
          paramModes.put(INPUT_OUTPUT, "U");

          for (Integer key : legacyTypes.keySet())
          {
             types2Legacy.put(legacyTypes.get(key), key);
          }
          types2Legacy.put(DataSetContainer.class, TYPE_DATASET);
       }

       /** Flag indicating if the change logging is tracked by default, for all tables created. */
       private boolean defaultTrackChanges = false;

       /** Flag indicating if the extent fields are normalized or denormalized (expanded). */
       private boolean normalizedExtent = true;

       /**
        * Flag indicating that the temp-table schema can be omitted during serialization, as the temp-table is
        * being built from the OpenClient proxy generated code, which is always compatible with the target's table.
        */
       private boolean useSchemaMarshalNone;

       /** Flag indicating that the {@link BigDecimal} are 'forced digits'; required for .NET compatibility. */
       private boolean forceDecimalDigits = false;

       /**
        * Implicit contstructor, initialize a Java-style legacy client (with {@link #normalizedExtent} set to
        * <code>true</code>).
        */
       public LegacyJavaAppserver()
       {
          this(true);
       }

       /**
        * Initialize this client, by specifying the extent mode: normalized (for Java-style) or denormalized
        * (for .NET-style).
        *
        * @param    normalizedExtent
        *           The extent mode.
        */
       public LegacyJavaAppserver(boolean normalizedExtent)
       {
          this.normalizedExtent = normalizedExtent;
       }

       /**
        * Set the flags for .NET compatibility. This includes:
        * <ul>
        * <li>{@link #normalizedExtent} will be set to false, so denormalized fields are used</li>
        * <li>{@link #forceDecimalDigits} will be enabled, so each {@link BigDecimal} will have a scale of at
        * least 1</li>
        * <li>{@link #defaultTrackChanges} will be enabled</li>
        * <li>{@link DataSetSDOHelper#setMergeTables} will enable the 'merge table' behavior for table/dataset
        *     parameters.</li>
        * </ul>
        */
       public void setDotNetCompatibility()
       {
          this.normalizedExtent = false;
          this.defaultTrackChanges = true;
          this.forceDecimalDigits = true;
          LegacyJavaAppserverClientProxy.configureMetadataHelper(true, false);
          LegacyJavaAppserverClient.setMergeTables(true);
       }

       /**
        * Mark this connection to not serialize the temp-table schema, as the source and destination always match,
        * for OpenClient proxy calls.
        */
       public void useSchemaMarshalNone()
       {
          this.useSchemaMarshalNone = true;
       }

       /**
        * Set the {@link #normalizedExtent} flag.
        *
        * @param    normalizedExtent
        *           The extent mode.
        */
       public void setNormalizedExtent(boolean normalizedExtent)
       {
          this.normalizedExtent = normalizedExtent;
       }

       /**
        * Set the {@link #defaultTrackChanges} flag.
        *
        * @param    trackChanges
        *           <code>true</code> if all tables need to {@link #beginLogging}.
        */
       public void setDefaultTrackChanges(boolean trackChanges)
       {
          this.defaultTrackChanges = trackChanges;
       }

       /**
        * Get all the records for the table on the specified index, in the graph metadata.
        *
        * @param    graph
        *           The graph.
        * @param    idx
        *           The table index.
        *
        * @return   The list of rows.
        */
       @Override
       public List<DataObject> getDataObjects(DataGraph graph, int idx)
       {
          return DataSetSDOHelper.getDataObjects(graph, idx);
       }

       /**
        * Get all the records for the table with the specified name, in this graph.
        *
        * @param    graph
        *           The graph.
        * @param    table
        *           The table name.
        *
        * @return   The list of rows.
        */
       @Override
       public List<DataObject> getDataObjects(DataGraph graph, String table)
       {
          return DataSetSDOHelper.getDataObjects(graph, table);
       }

       /**
        * Get all the records for the specified table.
        *
        * @param    tableMeta
        *           The table metadata.
        *
        * @return   The list of rows.
        */
       @Override
       public List<DataObject> getDataObjects(DataObject tableMeta)
       {
          return DataSetSDOHelper.getDataObjects(tableMeta);
       }

       /**
        * Get the before-table rows for this after-table.
        *
        * @param    tableMeta
        *           The after-table metadata.
        *
        * @return   The list of before-table rows.
        */
       @Override
       public List<DataObject> getBeforeObjects(DataObject tableMeta)
       {
          return DataSetSDOHelper.getBeforeObjects(tableMeta);
       }

       /**
        * Get the child rows for this record.
        *
        * @param    row
        *           The row.
        * @param    relation
        *           The child relation.
        *
        * @return   The child rows.
        */
       @Override
       public List<DataObject> getChildRows(DataObject row, String relation)
       {
          return DataSetSDOHelper.getChildRows(row, relation);
       }

       /**
        * Get the row's property on the given index as a {@link GregorianCalendar}.
        *
        * @param    row
        *           The row.
        * @param    idx
        *           The property index.
        *
        * @return   The calendar instance.
        */
       @Override
       public GregorianCalendar getGregorianCalendar(DataObject row, int idx)
       {
          GregorianCalendar gc = (GregorianCalendar) row.get(idx);
          return gc;
       }

       /**
        * Set the field's property on the given index from the {@link GregorianCalendar} instance.
        *
        * @param    row
        *           The row.
        * @param    idx
        *           The property index.
        * @param    date
        *           The property's value.
        */
       @Override
       public void setGregorianCalendar(DataObject row, int idx, GregorianCalendar date)
       {
          row.set(idx, date);
       }

       /**
        * Get the row's property as a {@link GregorianCalendar}.
        *
        * @param    row
        *           The row.
        * @param    name
        *           The property name.
        *
        * @return   The calendar instance.
        */
       @Override
       public GregorianCalendar getGregorianCalendar(DataObject row, String name)
       {
          GregorianCalendar gc = (GregorianCalendar) row.get(name);
          return gc;
       }

       /**
        * Create a new graph to hold a dataset.  It holds just the root <code>graph</code> node.
        *
        * @return   A graph with no metadata.
        */
       @Override
       public DataGraph createDataGraph()
       {
          // tracking-changes are not enabled for this graph - this API is used only for creating a dataset for
          // output mode
          return DataSetSDOHelper.createDataGraph();
       }

       /**
        * Create a new metadata object to hold the table details.
        *
        * @param    metadata
        *           The root graph node.
        *
        * @return   The graph metadata object.
        */
       @Override
       public DataGraph createDataGraph(Object metadata)
       {
          DataObject origMetadata = null;
          if (metadata instanceof DataObject)
          {
             DataObject md = (DataObject) metadata;

             if (md.getContainer() == null)
             {
                DataGraph newGraph = DataSetSDOHelper.createDataGraph((DataObject) metadata);

                if (defaultTrackChanges)
                {
                   DataSetSDOHelper.beginLogging(newGraph);
                }

                return newGraph;
             }

             origMetadata = (DataObject) metadata;
          }
          else
          {
             // this must be a graph
             DataGraph orig = (DataGraph) metadata;
             origMetadata = DataSetSDOHelper.getMetaData(orig);
          }

          metadata = DataSetSDOHelper.createGraphMetaData(origMetadata);
          DataGraph newGraph = DataSetSDOHelper.createDataGraph((DataObject) metadata);

          // copy all tables
          int nt = DataSetSDOHelper.getNumTables(origMetadata);
          for (int i = 0; i < nt; i++)
          {
             DataObject origTable = DataSetSDOHelper.getTableMetaData(origMetadata, i);
             String  name        = origTable.getString("name");
             int     numFields   = origTable.getInt("numFields");
             boolean b4img       = origTable.getBoolean("b4img");
             int     numIndexes  = origTable.getInt("numIndexes");
             String  indexes     = origTable.getString("indexes");
             String  xmlns       = origTable.getString("xmlns");
             String  xmlPrefix   = origTable.getString("xmlPrefix");
             String  xmlNodeName = origTable.getString("xmlNodeName");
             String  errorString = origTable.getString("errorString");
             boolean normalizedExtent = origTable.getBoolean("normalizedExtent");

             DataObject newTable = DataSetSDOHelper.createTableMetaData(name, numFields, b4img,
                                                                        numIndexes, indexes,
                                                                        xmlns, xmlPrefix, xmlNodeName,
                                                                        errorString,
                                                                        normalizedExtent);
             List<LegacyFieldMetaData> origFields = DataSetSDOHelper.getFieldMetaData(origTable);
             List<LegacyFieldMetaData> fieldList = newTable.getList("fields");
             for (int j = 0; j < origFields.size(); j++)
             {
                LegacyFieldMetaData origf = origFields.get(j);
                DataSetSDOHelper.setFieldMetaData(fieldList, newTable, new LegacyFieldMetaData(origf));
             }
             DataSetSDOHelper.addTableMetaData((DataObject) metadata, newTable);
          }

          // copy relations
          LegacyRelationMetaData[] relations = DataSetSDOHelper.getRelations((DataObject) origMetadata);
          for(int i = 0; i < relations.length; i++)
          {
             LegacyRelationMetaData currentRelation = relations[i];

             String name          = currentRelation.getName();
             String parentBuffer  = currentRelation.getParent();
             String childBuffer   = currentRelation.getChild();
             String whereString   = currentRelation.getWhereString();
             boolean active       = currentRelation.isActive();
             boolean parentId     = currentRelation.isParentId();
             String pairs         = currentRelation.getPairs();
             boolean fkeyHidden   = currentRelation.isFkeyHidden();
             boolean recursive    = currentRelation.isRecursive();
             boolean nested       = currentRelation.isNested();
             boolean reposition   = currentRelation.isReposition();

             DataSetSDOHelper.addRelation((DataObject) metadata, name, parentBuffer, childBuffer, whereString,
                                          active, parentId, pairs, fkeyHidden, recursive, nested, reposition);
          }

          if (defaultTrackChanges)
          {
             DataSetSDOHelper.beginLogging(newGraph);
          }

          return newGraph;
       }

       /**
        * Create a new row for the specified table.  This row is not attached to the table - use
        * {@link #addDataObject} to add it.
        *
        * @param    graph
        *           The graph.
        * @param    table
        *           The table name.
        *
        * @return   A new, empty, row.
        */
       @Override
       public DataObject createDataObject(DataGraph graph, String table)
       {
          return DataSetSDOHelper.createDataObject(graph, table);
       }

       /**
        * Create a new row for the specified table.    This row is not attached to the table - use
        * {@link #addDataObject(DataObject, DataObject)} to add it.
        *
        * @param    tableMetaData
        *           The table metadata.
        *
        * @return   The new row.
        */
       @Override
       public DataObject createDataObject(DataObject tableMetaData)
       {
          return DataSetSDOHelper.createDataObject(tableMetaData);
       }

       /**
        * Add the specified row to the graph.
        *
        * @param    graph
        *           The field's
        * @param    row
        *           The row to add to the table.
        */
       @Override
       public void addDataObject(DataGraph graph, DataObject row)
       {
          DataSetSDOHelper.addDataObject(graph, row);
       }

       /**
        * Add the specified row to the graph.
        *
        * @param    tableMetaData
        *           The table metadata.
        * @param    row
        *           The row to add to the table.
        */
       @Override
       public void addDataObject(DataObject tableMetaData, DataObject row)
       {
          DataSetSDOHelper.addDataObject(tableMetaData, row);
       }

       /**
        * Get the table name for a given row or table metadata.
        *
        * @param    value
        *           The table row or table metadata.
        *
        * @return   The table name.
        */
       @Override
       public String getTableName(DataObject value)
       {
          return DataSetSDOHelper.getTableName(value);
       }

       /**
        * Get the table index for a given row or table metadata.
        *
        * @param    value
        *           The table row or table metadata.
        *
        * @return   The table index, 0-based.
        *
        * @throws   NullPointerException
        *           If the row is not attached to a table.
        */
       @Override
       public int getTableIndex(DataObject value)
       {
          return DataSetSDOHelper.getTableIndex(value);
       }

       /**
        * Get the table index for a given table name and data graph.
        *
        * @param    graph
        *           The data graph.
        * @param    name
        *           The table name.
        *
        * @return   The table index, 0-based.
        *
        * @throws   IllegalArgumentException
        *           If the table is not part of this graph.
        */
       @Override
       public int getTableIndex(DataGraph graph, String name)
       {
          return DataSetSDOHelper.getTableIndex(graph, name);
       }

       /**
        * Get the table index for a given table name and data graph metadata.
        *
        * @param    metadata
        *           The graph metadata.
        * @param    name
        *           The table name.
        *
        * @return   The table index, 0-based.
        *
        * @throws   IllegalArgumentException
        *           If the table is not part of this graph.
        */
       @Override
       public int getTableIndex(DataObject metadata, String name)
       {
          return DataSetSDOHelper.getTableIndex(metadata, name);
       }

       /**
        * Get the data graph name for the given table metadata.
        *
        * @param     tableMetadata
        *            The table metadata.
        *
        * @return    The data graph name or <code>null</code> if the table is not part of a graph.
        */
       @Override
       public String getGraphName(DataObject tableMetadata)
       {
          return DataSetSDOHelper.getGraphName(tableMetadata);
       }

       /**
        * Get the legacy field metadata from the given table's metadata.
        *
        * @param    tableMetadata
        *           The table metadata.
        *
        * @return   The metadata for all the table's fields, in their definition order.
        */
       @Override
       public LegacyFieldMetaData[] getTableFields(DataObject tableMetadata)
       {
          List<LegacyFieldMetaData> fields = DataSetSDOHelper.getFieldMetaData(tableMetadata);
          LegacyFieldMetaData[] res = new LegacyFieldMetaData[fields.size()];
          for (int i = 0; i < res.length; i++)
          {
             res[i] = fields.get(i);
          }

          return res;
       }

       /**
        * Get the legacy field metadata from the given row.
        *
        * @param    row
        *           The row.
        *
        * @return   The metadata for all the table's fields, in their definition order.
        */
       @Override
       public LegacyFieldMetaData[] getFields(DataObject row)
       {
          DataObject tableMetadata = row.getContainer();
          return getTableFields(tableMetadata);
       }

       /**
        * Get the number of fields for a given row.
        *
        * @param    row
        *           The table row.
        *
        * @return   The number of fields as specified in the table metadata.
        */
       @Override
       public int getFieldCount(DataObject row)
       {
          return DataSetSDOHelper.getFieldCount(row);
       }

       /**
        * Get the field names for the specified table meta.
        *
        * @param    tableMeta
        *           The table metadata.
        *
        * @return   The field names.
        */
       @Override
       public String[] getTableFieldNames(DataObject tableMeta)
       {
          return DataSetSDOHelper.getTableFieldNames(tableMeta);
       }

       /**
        * Get the number of fields for the specified table meta.
        *
        * @param    tableMeta
        *           The table metadata.
        *
        * @return   The field count.
        */
       @Override
       public int getTableFieldCount(DataObject tableMeta)
       {
          return DataSetSDOHelper.getTableFieldCount(tableMeta);
       }

       /**
        * Get the name of the field on the given index in the metadata.
        *
        * @param    row
        *           The table row.  Must be attached to a table.
        * @param    i
        *           The field index.
        *
        * @return   The field's name.
        */
       @Override
       public String getFieldName(DataObject row, int i)
       {
          return DataSetSDOHelper.getFieldName(row, i);
       }

       /**
        * Get the type of the field on the given index in the metadata.
        *
        * @param    row
        *           The table row.  Must be attached to a table.
        * @param    i
        *           The field index.
        *
        * @return   The field's type.
        */
       @Override
       public int getFieldType(DataObject row, int i)
       {
          return DataSetSDOHelper.getFieldType(row, i);
       }

       /**
        * Check if this table has a field defined with that name.
        *
        * @param    tableMetadata
        *           The table metadata.
        * @param    fieldName
        *           The field name.
        *
        * @return   <code>true</code> if there is a field defined with this name.
        */
       @Override
       public boolean isTableField(DataObject tableMetadata, String fieldName)
       {
          return DataSetSDOHelper.isTableField(tableMetadata, fieldName);
       }

       /**
        * Get the field type.
        *
        * @param    tableMetadata
        *           The table metadata.
        * @param    fieldName
        *           The field name.
        *
        * @return   The field type.
        */
       @Override
       public int getTableFieldType(DataObject tableMetadata, String fieldName)
       {
          return DataSetSDOHelper.getTableFieldType(tableMetadata, fieldName);
       }

       /**
        * Get the field type.
        *
        * @param    tableMeta
        *           The table metadata.
        * @param    fieldIndex
        *           The field index.
        *
        * @return   See above.
        */
       @Override
       public int getTableFieldType(DataObject tableMeta, int fieldIndex)
       {
          return DataSetSDOHelper.getTableFieldType(tableMeta, fieldIndex);
       }

       /**
        * Get all table names from the given graph.
        *
        * @param    graph
        *           The graph.
        *
        * @return   See above.
        */
       @Override
       public String[] getTableNames(DataGraph graph)
       {
          return DataSetSDOHelper.getTableNames(graph);
       }

       /**
        * Get the number of tables for a graph.
        *
        * @param    graph
        *           The graph.
        *
        * @return   See above.
        */
       @Override
       public int getNumTables(DataGraph graph)
       {
          DataObject metadata = getMetaData(graph);
          return metadata == null ? 0 : DataSetSDOHelper.getNumTables(metadata);
       }

       /**
        * Get the number of tables for a graph metadata.
        *
        * @param    metaData
        *           The graph metadata.
        *
        * @return   See above.
        */
       @Override
       public int getNumTables(DataObject metaData)
       {
          return DataSetSDOHelper.getNumTables(metaData);
       }

       /**
        * Get the metadata object for this graph, from the <code>metadata</code> property of the root
        * object.
        *
        * @param    graph
        *           The graph.
        *
        * @return   The metadata object.
        */
       @Override
       public DataObject getMetaData(DataGraph graph)
       {
          return graph == null ? null : DataSetSDOHelper.getMetaData(graph);
       }

       /**
        * Get the metadata for the table registered on the given index in the graph.
        *
        * @param    metaData
        *           The graph metadata.
        * @param    i
        *           The table index.
        *
        * @return   The table metadata.
        */
       @Override
       public DataObject getTableMetaData(DataObject metaData, int i)
       {
          return DataSetSDOHelper.getTableMetaData(metaData, i);
       }

       /**
        * Get the metadata for the specified table.
        *
        * @param    graph
        *           The graph.
        * @param    table
        *           The table name.
        *
        * @return   The table metadata or <code>null</code> if the table is not part of this graph.
        */
       @Override
       public DataObject getTableMetaData(DataGraph graph, String table)
       {
          return DataSetSDOHelper.getTableMetaData(graph, table);
       }

       /**
        * Get the metadata list for all tables.
        *
        * @param    graph
        *           The graph.
        *
        * @return   The metadata for all tables.
        */
       @Override
       public List<DataObject> getTableMetaData(DataGraph graph)
       {
          return DataSetSDOHelper.getTableMetaData(graph);
       }

       /**
        * Create an object with a table metadata.  This object will have the {@code tablemetadata} type and is not
        * attached to the graph.
        *
        * @param   name
        *          The table's name.
        * @param   numFields
        *          The number of fields.
        * @param   b4img
        *          Flag indicating if the table has a BEFORE-IMAGE.
        * @param   numIndexes
        *          The number of indexes.
        * @param   indexes
        *          The index specification (TODO: not validated/implemented at this time).
        * @param   xmlns
        *          The XML namespace (TODO: not validated/implemented at this time).
        * @param   xmlPrefix
        *          The XML prefix (TODO: not validated/implemented at this time).
        *
        * @return   An object with the table metadata.
        */
       @Override
       @Deprecated
       public DataObject createObjectMetaData(String  name,
                                              int     numFields,
                                              boolean b4img,
                                              int     numIndexes,
                                              String  indexes,
                                              String  xmlns,
                                              String  xmlPrefix)
       {
          return createObjectMetaData(name, numFields, b4img, numIndexes, indexes, xmlns, xmlPrefix, null, null);
       }

       /**
        * Create an object with a table metadata. This object will have the {@code tablemetadata} type and is not
        * attached to the graph.
        *
        * @param   name
        *          The table's name.
        * @param   numFields
        *          The number of fields.
        * @param   b4img
        *          Flag indicating if the table has a BEFORE-IMAGE.
        * @param   numIndexes
        *          The number of indexes.
        * @param   indexes
        *          A list o LegacyIndexDefinition containing all the index information.
        * @param   xmlns
        *          The XML namespace (TODO: not validated/implemented at this time).
        * @param   xmlPrefix
        *          The XML prefix (TODO: not validated/implemented at this time).
        *
        * @return   An object with the table metadata.
        */
       public DataObject createObjectMetaData(String  name,
                                              int     numFields,
                                              boolean b4img,
                                              int     numIndexes,
                                              List<LegacyIndexDefinition>  indexes,
                                              String  xmlns,
                                              String  xmlPrefix)
       {
          return createObjectMetaData(name, numFields, b4img, numIndexes, DataSetSDOHelper.toFormattedString(indexes), xmlns, xmlPrefix, null, null);
       }

       /**
        * Create an object with a table metadata.  This object will have the {@code tablemetadata} type and is not
        * attached to the graph.
        *
        * @param   name
        *          The table's name.
        * @param   numFields
        *          The number of fields.
        * @param   b4img
        *          Flag indicating if the table has a BEFORE-IMAGE.
        * @param   numIndexes
        *          The number of indexes.
        * @param   indexes
        *          The index specification (TODO: not validated/implemented at this time).
        * @param   xmlns
        *          The XML namespace (TODO: not validated/implemented at this time).
        * @param   xmlPrefix
        *          The XML prefix (TODO: not validated/implemented at this time).
        * @param   xmlNodeName
        *          The XML node name (TODO: not validated/implemented at this time).
        * @param   errorString
        *          The table's ERROR-STRING attribute  (TODO: not validated/implemented at this time).
        *
        * @return   An object with the table metadata.
        */
       @Override
       public DataObject createObjectMetaData(String  name,
                                              int     numFields,
                                              boolean b4img,
                                              int     numIndexes,
                                              String  indexes,
                                              String  xmlns,
                                              String  xmlPrefix,
                                              String  xmlNodeName,
                                              String  errorString)
       {
          return DataSetSDOHelper.createTableMetaData(name, numFields, b4img,
                                                      numIndexes, indexes,
                                                      xmlns, xmlPrefix, xmlNodeName,
                                                      errorString,
                                                      normalizedExtent);
       }

       /**
        * Create LegacyIndexDefinition objects from the formatted string in table meta
        * containing all index information, components, primary/unique flags etc.
        *
        * @param   tableMeta
        *          The table's metadata object.
        * @return   A list of LegacyIndexDefinition objects holding the metadata for each index
        */
       public List<LegacyIndexDefinition> getLegacyIndexDefinition(DataObject tableMeta)
       {
          return DataSetSDOHelper.getLegacyIndexDefinition(tableMeta);
       }

       /**
        * A method that returns the table's primary unique index.
        *
        * @param   tableMeta
        *          The table's metadata object.
        */
       public LegacyIndexDefinition getPrimaryUniqueIndex(DataObject tableMeta)
       {
          return DataSetSDOHelper.getPrimaryUniqueIndex(tableMeta);
       }

       /**
        * A method that returns the LegacyIndexDefinition that has the specified legacy name.
        * The requested index can be primary or just unique.
        *
        * @param   indexName
        *          The string legacy name of the index.
        * @param   tableMeta
        *          The table's metadata object.
        * @return   The requested LegacyIndexDefinition object.
        */
       public LegacyIndexDefinition getIndex(String indexName, DataObject tableMeta)
       {
          return DataSetSDOHelper.getIndex(indexName, tableMeta);
       }

       /**
        * A method that marks an index as primary unique.
        * The index should already be part of the indexes.
        * The previous primary index will be set as just unique and
        * will be swapped  in the indexes with the newly set primary index.
        *
        * @param   primaryUniqueIndexName
        *          Index legacy name that should be set as primary unique.
        * @param   tableMeta
        *          The table's metadata object.
        * @return   True if the primary unique index was set, or false if one was already present.
        */
       public boolean setPrimaryUniqueIndex(String primaryUniqueIndexName, DataObject tableMeta)
       {
          return DataSetSDOHelper.setPrimaryUniqueIndex(primaryUniqueIndexName, tableMeta);
       }

       /**
        * A method that adds an index to indexes.
        *
        * @param   index
        *          The index to be added. Should not be primary.
        * @param   tableMeta
        *          The table's metadata object.
        * @return   True if the index was added successfully.
        */
       public boolean addIndex(LegacyIndexDefinition index, DataObject tableMeta)
       {
          return DataSetSDOHelper.addIndex(index, tableMeta);
       }

       /**
        * A method that removes any index from the indexes.
        *
        * @param   indexName
        *          Legacy name of the index to be removed.
        * @param   tableMeta
        *          The table's metadata object.
        */
       public void removeIndex(String indexName, DataObject tableMeta)
       {
          DataSetSDOHelper.removeIndex(indexName, tableMeta);
       }

       /**
        * A method that adds a component (field) to the specified index.
        *
        * @param   indexName
        *          Legacy name of the index.
        * @param   field
        *          The field to be added as a new component.
        * @param   tableMeta
        *          The table's metadata object.
        * @return   True if the new component was successfully added.
        */
       public boolean addComponentToIndex(String indexName, String field, DataObject tableMeta)
       {
          return DataSetSDOHelper.addComponentToIndex(indexName, field, tableMeta);
       }

       /**
        * A method that removes a component (field) to the specified index.
        *
        * @param   indexName
        *          Legacy name of the index.
        * @param   field
        *          The field to be removed from the index.
        * @param   tableMeta
        *          The table's metadata object.
        * @return   True if the new component was successfully removed.
        */
       public boolean removeComponentFromIndex(String indexName, String field, DataObject tableMeta)
       {
          return DataSetSDOHelper.removeComponentFromIndex(indexName, field, tableMeta);
       }

       /**
        * A method that converts a List of IndexDefinition
        * objects to single a formatted string represtation.
        *
        * @param   indexes
        *          A list o IndexDefinition containing all the index information.
        * @return   Formatted string that contains all the index information.
        *           The format is specified in progress documentation:
        *           [primeUniqueFlag,primeFld1[,primeFldn]...:primeIdxName.]
        */
       public String toFormattedString(List<LegacyIndexDefinition> indexes)
       {
          return DataSetSDOHelper.toFormattedString(indexes);
       }

       /**
        * Add a new new field with the specified configuration.
        *
        * @param    metaData
        *           The table metadata, of type <code>tablemetadata</code>.  The field will be added
        *           to the <code>fields</code> property.
        * @param    idx
        *           The field's index in the metadata.
        * @param    name
        *           The field's name.
        * @param    extent
        *           The field's extent.
        * @param    type
        *           The legacy type (numeric value based on {@link LegacyJavaAppserver} constants.
        * @param    order
        *           The field's order.
        * @param    xmlMapping
        *           The field's mapping.
        */
       @Override
       public void setFieldMetaData(DataObject metaData,
                                    int        idx,
                                    String     name,
                                    int        extent,
                                    int        type,
                                    int        order,
                                    int        xmlMapping)
       {
          Class<?> cls = legacyTypes.get(type);
          DataSetSDOHelper.setFieldMetaData(metaData, idx, name, extent, cls, type, order, xmlMapping);
       }

       /**
        * Create a metadata object with the specified name.
        *
        * @param    name
        *           The graph name.
        *
        * @return   The metadata object.  Will be attached to a graph via {@link #createDataGraph(Object)}.
        */
       @Override
       public DataObject createGraphMetaData(String name)
       {
          return DataSetSDOHelper.createGraphMetaData(name);
       }

       /**
        * Add the specified table metadata to the graph.  This will create a custom type based on the
        * configured fields, so that each row will be created with this type.
        *
        * @param    metaData
        *           The graph metadata.
        * @param    table
        *           The table metadata.
        */
       @Override
       public void addTableMetaData(DataObject metaData, DataObject table)
       {
          if (FwdServerJMX.JMX_DEBUG)
          {
             TABLE_META.timer(() -> DataSetSDOHelper.addTableMetaData(metaData, table));
          }
          else
          {
             DataSetSDOHelper.addTableMetaData(metaData, table);
          }
       }

       /**
        * Copy the single table from the provided DataGraph to the given graph metadata (i.e. attach a table
        * to a data-set).
        *
        * @param    metaData
        *           The graph metadata.
        * @param    table
        *           The table representation, in a {@link #createDataGraph} format.
        */
       @Override
       public void addTableMetaData(DataObject metaData, DataGraph table)
       {
          String[] tables = getTableNames(table);
          addTableMetaData(metaData, getTableMetaData(table, tables[0]));
       }

       /**
        * Get the graph name from the specified metadata, of type <code>graphmetadata</code>.
        *
        * @param    metadata
        *           The metadata.
        *
        * @return   The graph name.
        */
       @Override
       public String getDataGraphName(DataObject metadata)
       {
          return DataSetSDOHelper.getDataGraphName(metadata);
       }

       /**
        * Get the namespace URI from the given metadata.
        *
        * @param    metadata
        *           The graph or table metadata.
        *
        * @return   See above.
        */
       @Override
       public String getNamespaceURI(DataObject metadata)
       {
          return metadata.getString("xmlns");
       }

       /**
        * Set the namespace URI for the given metadata.
        *
        * @param    metadata
        *           The graph or table metadata.
        * @param    xmlns
        *           The namespace URI.
        */
       @Override
       public void setNamespaceURI(DataObject metadata, String xmlns)
       {
          if (xmlns == null)
          {
             metadata.unset("xmlns");
          }
          else
          {
             metadata.setString("xmlns", xmlns);
          }
       }

       /**
        * Get the namespace prefix from the given metadata.
        *
        * @param    metadata
        *           The graph or table metadata.
        *
        * @return   See above.
        */
       @Override
       public String getNamespacePrefix(DataObject metadata)
       {
          return metadata.getString("xmlPrefix");
       }

       /**
        * Set the namespace prefix for the given metadata.
        *
        * @param    metadata
        *           The graph or table metadata.
        * @param    xmlPrefix
        *           The namespace prefix.
        */
       @Override
       public void setNamespacePrefix(DataObject metadata, String xmlPrefix)
       {
          if (xmlPrefix == null)
          {
             metadata.unset("xmlPrefix");
          }
          else
          {
             metadata.setString("xmlPrefix", xmlPrefix);
          }
       }

       /**
        * Get the XML node-name from the given metadata.
        *
        * @param    metadata
        *           The graph or table metadata.
        *
        * @return   See above.
        */
       @Override
       public String getXmlNodeName(DataObject metadata)
       {
          return metadata.getString("xmlNodeName");
       }

       /**
        * Set the XML node-name for the given metadata.
        *
        * @param    metadata
        *           The graph or table metadata.
        * @param    xmlNodeName
        *           The XML node name.
        */
       @Override
       public void setXmlNodeName(DataObject metadata, String xmlNodeName)
       {
          if (xmlNodeName == null)
          {
             metadata.unset("xmlNodeName");
          }
          else
          {
             metadata.setString("xmlNodeName", xmlNodeName);
          }
       }

       /**
        * Configure a {@link LegacyJavaAppserverParameter} with the given details.
        *
        * @param    mode
        *           The parameter mode.
        * @param    type
        *           The parameter type.
        * @param    value
        *           The parameter value.
        * @param    metadata
        *           The parameter metadata (in case of DataSet).
        *
        * @return   A new parameter instance.
        */
       @Override
       public LegacyJavaAppserverParameter getJavaParameter(int mode, int type, Object value, Object metadata)
       {
          return getJavaParameter(mode, type, SourceNameMapper.NO_EXTENT, value, metadata);
       }

       /**
        * Configure a {@link LegacyJavaAppserverParameter} with the given details.
        *
        * @param    mode
        *           The parameter mode.
        * @param    type
        *           The parameter type.
        * @param    extent
        *           The parameter's extent.
        * @param    pvalue
        *           The parameter value.
        * @param    metadata
        *           The parameter metadata (in case of DataSet).
        *
        * @return   A new parameter instance.
        */
       @Override
       public LegacyJavaAppserverParameter getJavaParameter(int    mode,
                                                            int    type,
                                                            int    extent,
                                                            Object pvalue,
                                                            Object metadata)
       {
          if (FwdServerJMX.JMX_DEBUG)
          {
             LegacyJavaAppserverParameter[] res = new LegacyJavaAppserverParameter[1];

             JAVA_PARAM.timer(() -> res[0] = getJavaParameterImpl(mode, type, extent, pvalue, metadata));
             return res[0];
          }
          else
          {
             return getJavaParameterImpl(mode, type, extent, pvalue, metadata);
          }
       }

       /**
        * Configure a {@link LegacyJavaAppserverParameter} with the given details.
        *
        * @param    mode
        *           The parameter mode.
        * @param    type
        *           The parameter type.
        * @param    extent
        *           The parameter's extent.
        * @param    pvalue
        *           The parameter value.
        * @param    metadata
        *           The parameter metadata (in case of DataSet).
        *
        * @return   A new parameter instance.
        */
       private LegacyJavaAppserverParameter getJavaParameterImpl(int    mode,
                                                                 int    type,
                                                                 int    extent,
                                                                 Object pvalue,
                                                                 Object metadata)
       {
          Object value = pvalue;
          Object vmetadata;
          if (value == null && (type == TYPE_DATASET_HANDLE || type == TYPE_TABLE_HANDLE))
          {
             value = createDataGraph();
             vmetadata = getMetaData((DataGraph) value);
          }
          else
          {
             vmetadata = metadata;
          }
          Object orig = value;

          boolean input = mode == INPUT || mode == INPUT_OUTPUT;
          boolean output = mode == OUTPUT || mode == INPUT_OUTPUT;

          if (type == TYPE_DATASET || type == TYPE_DATASET_HANDLE)
          {
             List<DsTableDefinition> tableDefinitions = new ArrayList<>();
             List<DsRelationDefinition> relationDefinitions = new ArrayList<>();

             DataObject graphMeta = (DataObject) vmetadata;
             String[] tables = graphMeta == null ? new String[0]
                                                 : DataSetSDOHelper.getTableNames(graphMeta);
             for (int i = 0; i < tables.length; i++)
             {
                DataObject tableMeta = DataSetSDOHelper.getTableMetaData(graphMeta, i);

                List<DataObject> rows = input ? DataSetSDOHelper.getDataObjects(tableMeta)
                                              : Collections.emptyList();
                List<LegacyFieldMetaData> fields = DataSetSDOHelper.getFieldMetaData(tableMeta);
                DsTableDefinition dstable = createDsTable(rows, fields, input, output, false);
                if (useSchemaMarshalNone && type == TYPE_DATASET)
                {
                   dstable.setSchemaMarshalLevel(AbstractTempTable.SM_NONE);
                }
                dstable.setName(tables[i]);

                dstable.setIndexes(tableMeta.getInt("numIndexes"), tableMeta.getString("indexes"));
                dstable.setXmlns(tableMeta.getString("xmlns"));
                dstable.setXmlPrefix(tableMeta.getString("xmlPrefix"));
                dstable.setXmlNodeName(tableMeta.getString("xmlNodeName"));
                dstable.setErrorString(tableMeta.getString("errorString"));
                tableDefinitions.add(dstable);

                if (tableMeta.getBoolean("b4img"))
                {
                   List<DataObject> beforeRows = input ? DataSetSDOHelper.getBeforeObjects(tableMeta)
                                                       : Collections.emptyList();
                   DsTableDefinition b4table = createDsTable(beforeRows, fields, input, output, true);
                   if (useSchemaMarshalNone && type == TYPE_DATASET)
                   {
                      b4table.setSchemaMarshalLevel(AbstractTempTable.SM_NONE);
                   }
                   // TODO: here we must not exceed 32 characters...
                   b4table.setName("BI" + tables[i]);

                   dstable.setPeerDef(b4table);
                }
             }

             if (graphMeta != null)
             {
                List<LegacyRelationMetaData> lrmds = graphMeta.getList("relations");
                DsRelationDefinition relationDef;
                LegacyRelationMetaData lrmd;
                for (int i = 0; i < lrmds.size(); i++)
                {
                   lrmd = lrmds.get(i);
                   relationDef = new DsRelationDefinition();
                   relationDef.setActive(lrmd.isActive());
                   relationDef.setChildBuffer(lrmd.getChild());
                   relationDef.setFieldPairs(lrmd.getPairs());
                   relationDef.setFkeyHidden(lrmd.isFkeyHidden());
                   relationDef.setName(lrmd.getName());
                   relationDef.setNested(lrmd.isNested());
                   relationDef.setParentBuffer(lrmd.getParent());
                   relationDef.setParentId(lrmd.isParentId());
                   relationDef.setRecursive(lrmd.isRecursive());
                   relationDef.setReposition(lrmd.isReposition());
                   relationDef.setWhereString(lrmd.getWhereString());
                   relationDefinitions.add(relationDef);
                }
             }

             DataSetContainer dsc = new DataSetContainer(input, output, false);
             String name = graphMeta == null ? null : DataSetSDOHelper.getDataGraphName(graphMeta);
             dsc.setStructureName(name);
             dsc.setTableDefinitions(tableDefinitions);
             dsc.setRelationDefinitions(relationDefinitions.isEmpty() ? null : relationDefinitions);
             if (graphMeta != null)
             {
                dsc.namespaceUri(graphMeta.getString("xmlns"));
                dsc.namespacePrefix(graphMeta.getString("xmlPrefix"));
                dsc.setXmlNodeName(graphMeta.getString("xmlNodeName"));
             }
             value = dsc;
          }
          else if (type == TYPE_TABLE || type == TYPE_TABLE_HANDLE)
          {
             DataGraph graph = (DataGraph) value;
             DataObject graphMeta = DataSetSDOHelper.getMetaData(graph);

             String[] tables = graphMeta == null ? null
                                                 : DataSetSDOHelper.getTableNames(graphMeta);
             if (tables != null)
             {
                DataObject tableMeta = DataSetSDOHelper.getTableMetaData(graphMeta, 0);

                List<DataObject> rows = input ? DataSetSDOHelper.getDataObjects(tableMeta)
                                              : Collections.emptyList();
                List<LegacyFieldMetaData> fields = DataSetSDOHelper.getFieldMetaData(tableMeta);

                TableResultSet ttrs = createTableResultSet(rows, fields, input, output, false);
                ttrs.setStructureName(tables[0]);
                TableWrapper wrapper = new TableWrapper(ttrs);
                if (useSchemaMarshalNone && type == TYPE_TABLE)
                {
                   wrapper.setSchemaMarshalLevel(AbstractTempTable.SM_NONE);
                }
                wrapper.setTableName(tables[0]);
                wrapper.setNumIndexes(tableMeta.getInt("numIndexes"));
                wrapper.setIndexes(tableMeta.getString("indexes"));
                wrapper.setXmlns(tableMeta.getString("xmlns"));
                wrapper.setXmlPrefix(tableMeta.getString("xmlPrefix"));
                wrapper.setXmlNodeName(tableMeta.getString("xmlNodeName"));
                wrapper.setErrorString(tableMeta.getString("errorString"));

                if (tableMeta.getBoolean("b4img"))
                {
                   List<DataObject> beforeRows = input ? DataSetSDOHelper.getBeforeObjects(tableMeta)
                                                       : Collections.emptyList();
                   TableResultSet bttrs = createTableResultSet(beforeRows, fields, input, output, true);
                   TableWrapper b4table = new TableWrapper(bttrs);
                   if (useSchemaMarshalNone && type == TYPE_TABLE)
                   {
                      b4table.setSchemaMarshalLevel(AbstractTempTable.SM_NONE);
                   }
                   b4table.setTableName("BI" + tables[0]);
                   wrapper.setPeerDef(b4table);
                }

                value = wrapper;
             }
             else
             {
                value = new TableWrapper();
             }
          }
          else if (type == TYPE_MEMPTR)
          {
             // this must always be MemoryBuffer
             if (value != null &&
                 !((value instanceof MemoryBuffer ||
                   (value.getClass().isArray() && value.getClass().getComponentType() == MemoryBuffer.class))))
             {
                throw new IllegalArgumentException("memptr must be MemoryBuffer!");
             }
             else if (value == null)
             {
                if (extent == 0 || extent == SourceNameMapper.NO_EXTENT)
                {
                   value = new MemoryBuffer(new byte[0]);
                }
                else if (extent == SourceNameMapper.DYNAMIC_EXTENT)
                {
                   value = new MemoryBuffer[0];
                }
                else
                {
                   value = new MemoryBuffer[extent];
                   for (int i = 0; i < extent; i++)
                   {
                      Array.set(value, i, new MemoryBuffer(new byte[0]));
                   }
                }
             }
          }
          else
          {
             value = fromJavaType(value, type, extent, output);
          }

          return new LegacyJavaAppserverParameter(type, mode, value, orig);
       }

       /**
        * Determine the output parameter value.
        *
        * @param    param
        *           The parameter as received from the remote side.
        *
        * @return   The parameter's output value.
        */
       @Override
       public Object getOutputParameter(LegacyJavaAppserverParameter param)
       {
          if (FwdServerJMX.JMX_DEBUG)
          {
             Object[] res = new Object[1];

             OUTPUT_PARAM.timer(() -> res[0] = getOutputParameterImpl(param));

             return res[0];
          }
          else
          {
             return getOutputParameterImpl(param);
          }
       }

       /**
        * Determine the output parameter value.
        *
        * @param    param
        *           The parameter as received from the remote side.
        *
        * @return   The parameter's output value.
        */
       private Object getOutputParameterImpl(LegacyJavaAppserverParameter param)
       {
          int type = param.getType();
          Object arg = param.getValue();

          switch (type)
          {
             case TYPE_DATASET:
             case TYPE_DATASET_HANDLE:
                DataGraph graph = (DataGraph) param.getOrig();
                DataSetContainer dsc = (DataSetContainer) param.getValue();

                DataObject metadata = DataSetSDOHelper.getMetaData(graph);
                boolean reuseMetadata = (metadata != null);
                if (reuseMetadata)
                {
                   if (dsc.isValid())
                   {
                      if(!DataSetSDOHelper.isMergeTables())
                      {
                         List<String> tableNames = new ArrayList<>();
                         for (DsTableDefinition dst : dsc.getTableDefinitions())
                         {
                            tableNames.add(dst.getName());
                         }
                         // re-build the structures inside the metadata reference
                         DataSetSDOHelper.initGraphMetaData(metadata,
                                                             dsc.getStructureName(),
                                                             dsc.namespacePrefix(),
                                                             dsc.namespaceURI(),
                                                             dsc.getXmlNodeName(),
                                                             tableNames);
                      }
                      else
                      {
                         // create a map with the signature of each table from dataset
                         Map<String, List<Map<String, String>>> tableSignatures = new LinkedHashMap<>();
                         for (DsTableDefinition dst : dsc.getTableDefinitions())
                         {
                            List<Map<String, String>> signature = getSignature(dst.getProperties());
                            tableSignatures.put(dst.getName(), signature);
                         }

                         // re-build the structures inside the metadata reference
                         Map<String, String> namePairs = DataSetSDOHelper.initGraphMetaData(
                                                                            metadata,
                                                                            dsc.getStructureName(),
                                                                            dsc.namespacePrefix(),
                                                                            dsc.namespaceURI(),
                                                                            dsc.getXmlNodeName(),
                                                                            tableSignatures);

                         // update DsTableDefinition to use the table name from metadata
                         if (!namePairs.isEmpty())
                         {
                            for (DsTableDefinition dst : dsc.getTableDefinitions())
                            {
                               String newTableName = namePairs.get(dst.getName());
                               if (newTableName != null)
                               {
                                  dst.setName(newTableName);
                               }
                            }
                         }
                      }
                   }
                   else
                   {
                      // clear the rows
                      DataSetSDOHelper.clearAllRows(metadata);
                   }
                }
                else
                {
                   //create metadata
                   metadata = DataSetSDOHelper.createGraphMetaData(dsc.getStructureName(),
                                                                   dsc.namespacePrefix(),
                                                                   dsc.namespaceURI(),
                                                                   dsc.getXmlNodeName());
                   DataSetSDOHelper.setGraphMetaData(graph, metadata);
                }

                if (dsc.isValid())
                {
                   for (DsRelationDefinition dsr : dsc.getRelationDefinitions())
                   {
                      DataSetSDOHelper.addRelation(metadata,
                                                   dsr.getName(),
                                                   dsr.getParentBuffer(),
                                                   dsr.getChildBuffer(),
                                                   dsr.getWhereString(),
                                                   dsr.isActive(),
                                                   dsr.isParentId(),
                                                   dsr.getFieldPairs(),
                                                   dsr.isFkeyHidden(),
                                                   dsr.isRecursive(),
                                                   dsr.isNested(),
                                                   dsr.isReposition());
                   }
                }

                // end the logging
                if (defaultTrackChanges)
                {
                   DataSetSDOHelper.endLogging(graph);
                }

                if (dsc.isValid())
                {
                   for (DsTableDefinition dst : dsc.getTableDefinitions())
                   {
                      createOutputTable(graph, dst, reuseMetadata, false);
                      DsTableDefinition b4dst = dst.getPeerDef();
                      if (b4dst != null)
                      {
                         createOutputTable(graph, b4dst, reuseMetadata, true);
                      }
                   }
                }

                if (defaultTrackChanges)
                {
                   graph.getChangeSummary().beginLogging();
                }

                return graph;

             case TYPE_TABLE:
             case TYPE_TABLE_HANDLE:
                graph = (DataGraph) param.getOrig();
                TableWrapper wrapper = (TableWrapper) param.getValue();

                metadata = DataSetSDOHelper.getMetaData(graph);
                reuseMetadata = (metadata != null);
                if (reuseMetadata)
                {
                   if (wrapper.isValid())
                   {
                      if (!DataSetSDOHelper.isMergeTables())
                      {
                         List<String> tableNames = new ArrayList<>();
                         tableNames.add(wrapper.getTableName());
                         // re-build the structures inside the metadata reference
                         DataSetSDOHelper.initGraphMetaData(metadata,
                                                             wrapper.getTableName(),
                                                             null,
                                                             null,
                                                             null,
                                                             tableNames);
                      }
                      else
                      {
                         // create a map with the signature of the table
                         Map<String, List<Map<String, String>>> tableSignatures = new LinkedHashMap<>();
                         List<Map<String, String>> signature = getSignature(wrapper.getProperties());
                         tableSignatures.put(wrapper.getTableName(), signature);

                         // re-build the structures inside the metadata reference
                         Map<String, String> namePairs = DataSetSDOHelper.initGraphMetaData(
                                                                            metadata,
                                                                            wrapper.getTableName(),
                                                                            null,
                                                                            null,
                                                                            null,
                                                                            tableSignatures);

                         // update TableWrapper to use the table name from metadata
                         if (!namePairs.isEmpty())
                         {
                            String newTableName = namePairs.get(wrapper.getTableName());
                            if (newTableName != null)
                            {
                               wrapper.setTableName(newTableName);
                            }
                         }
                      }
                   }
                   else
                   {
                      // clear the rows
                      DataSetSDOHelper.clearAllRows(metadata);
                   }
                }
                else
                {
                   //create metadata
                   metadata = DataSetSDOHelper.createGraphMetaData(wrapper.getTableName());
                   DataSetSDOHelper.setGraphMetaData(graph, metadata);
                }

                // end the logging
                if (defaultTrackChanges)
                {
                   DataSetSDOHelper.endLogging(graph);
                }

                if (wrapper.isValid())
                {
                   createOutputTable(graph, wrapper, reuseMetadata, false);
                   TableWrapper b4wrapper = wrapper.getPeerDef();
                   if (b4wrapper != null)
                   {
                      createOutputTable(graph, b4wrapper, reuseMetadata, true);
                   }
                }

                if (defaultTrackChanges)
                {
                   graph.getChangeSummary().beginLogging();
                }

                return graph;
          }

          int extent = SourceNameMapper.NO_EXTENT;
          if (param.getValue().getClass().isArray())
          {
             extent = Array.getLength(param.getValue());
          }

          return toJavaValue(arg, type, false, extent);
       }

       /**
        * Determine the output parameter value on the given index.
        *
        * @param    paramArray
        *           The parameter array.
        * @param    i
        *           The parameter's index.
        *
        * @return   The parameter's output value.
        */
       @Override
       public Object getOutputParameter(LegacyJavaAppserverParameter[] paramArray, int i)
       {
          return getOutputParameter(paramArray[i]);
       }

       /**
        * Get a string representation of the argument's modes.
        *
        * @param    paramArray
        *           The parameter array.
        *
        * @return   The parameter modes.
        */
       @Override
       public String getArgumentModes(LegacyJavaAppserverParameter[] paramArray)
       {
          StringBuilder modes = new StringBuilder();
          for (LegacyJavaAppserverParameter ljcp : paramArray)
          {
             modes.append(paramModes.get(ljcp.getMode()));
          }

          return modes.toString();
       }

       /**
        * Resolve the arguments from the given parameter array.
        *
        * @param    paramArray
        *           The parameter array.
        *
        * @return   The arguments.
        */
       @Override
       public Object[] getArguments(LegacyJavaAppserverParameter[] paramArray)
       {
          Object[] params = new Object[paramArray.length];
          int idx = 0;
          for (LegacyJavaAppserverParameter ljcp : paramArray)
          {
             params[idx++] = ljcp.getValue();
          }

          return params;
       }

       /**
        * Create a memptr corresponding instance.
        *
        * @param    value
        *           The bytes.
        *
        * @return   A {@link MemoryBuffer}.
        */
       @Override
       public Object createMemptr(byte[] value)
       {
          return new MemoryBuffer(value);
       }

       /**
        * Check if the given parameter is a memptr.
        *
        * @param    outputParameter
        *           The parameter.
        *
        * @return   <code>true</code> if the value is a {@link MemoryBuffer}.
        */
       @Override
       public boolean isMemptr(Object outputParameter)
       {
          return outputParameter instanceof MemoryBuffer;
       }

       /**
        * Get the bytes from the given output parameter, which is a {@link MemoryBuffer}.
        *
        * @param    outputParameter
        *           The output parameter.
        *
        * @return   See above.
        */
       @Override
       public byte[] getBytes(Object outputParameter)
       {
          return ((MemoryBuffer) outputParameter).getValue();
       }

       /**
        * Create a new FWd connection with the specified configuration.
        *
        * @param    config
        *           The configuration for the FWD server connection.
        * @param    account
        *           The FWD account to authenticate on the remote side.
        * @param    appServerName
        *           The name of the appserver to which the connection will be established.
        * @param    sessionFree
        *           <code>true</code> to indicate a session-free operating mode, <code>false</code>
        *           to indicate a session-managed operating mode.
        * @param    user
        *           The username passed to the appserver's connect procedure.
        * @param    pwd
        *           The password passed to the appserver's connect procedure.
        * @param    serverInfo
        *           The info passed to the appserver's connect procedure.
        *
        * @return   The helper with the connection result (which was established or not).
        *
        * @throws   NumberedException
        *           If there are connection problems.
        */
       @Override
       public AppServerHelper getConnection(BootstrapConfig   config,
                                            String            account,
                                            String            appServerName,
                                            boolean           sessionFree,
                                            String            user,
                                            String            pwd,
                                            String            serverInfo)
       throws NumberedException
       {
          return AppServerHelper.connect(config, account, appServerName, sessionFree, user, pwd, serverInfo);
       }

       /**
        * Postprocess these arguments - this includes copying state back to the
        * {@link LegacyJavaAppserverParameter} instance, for OUTPUT parameters.
        *
        * @param    paramArray
        *           The parameter array.
        * @param    args
        *           The returned arguments.
        */
       @Override
       public void postProcessArguments(Object paramArray, Object[] args)
       {
          Object[] params = (Object[]) paramArray;
          for (int i = 0; i < params.length; i++)
          {
             LegacyJavaAppserverParameter ljcp = (LegacyJavaAppserverParameter) params[i];
             ljcp.setValue(args[i]);
          }
       }

       /**
        * Get the changes in this change summary, as a graph.
        *
        * @param    changeSummary
        *           The change summary.
        *
        * @return   See above.
        */
       @Override
       public DataGraph getChanges(Object changeSummary)
       {
          return ((ChangeSummary) changeSummary).getDataGraph();
       }

       /**
        * End the logging for changes in this object.
        *
        * @param    changeSummary
        *           The change summary.
        */
       @Override
       public void endLogging(Object changeSummary)
       {
          DataSetSDOHelper.endLogging(((ChangeSummary) changeSummary).getDataGraph());
       }

       /**
        * Begin the logging for changes in this object.
        *
        * @param    changeSummary
        *           The change summary.
        */
       @Override
       public void beginLogging(Object changeSummary)
       {
          DataSetSDOHelper.beginLogging(((ChangeSummary) changeSummary).getDataGraph());
       }

       /**
        * Get the change summary object for this graph.
        *
        * @param    graph
        *           The graph.
        *
        * @return   See above.
        */
       @Override
       public Object getChangeSummary(DataGraph graph)
       {
          return graph.getChangeSummary();
       }

       /**
        * Check if the given object has been deleted.
        *
        * @param    changeSummary
        *           The change summary.
        * @param    object
        *           The object.
        *
        * @return   See above.
        */
       @Override
       public boolean isDeleted(Object changeSummary, DataObject object)
       {
          return ((ChangeSummary) changeSummary).isDeleted(object);
       }

       /**
        * Check if the given object is newly created.
        *
        * @param    changeSummary
        *           The change summary.
        * @param    object
        *           The object.
        *
        * @return   See above.
        */
       @Override
       public boolean isCreated(Object changeSummary, DataObject object)
       {
          return ((ChangeSummary) changeSummary).isCreated(object);
       }

       /**
        * Get all the changed objects in this change summary.
        *
        * @param    changeSummary
        *           The change summary.
        *
        * @return   See above.
        */
       @Override
       public List<DataObject> getChangedDataObjects(Object changeSummary)
       {
          return ((ChangeSummary) changeSummary).getChangedDataObjects();
       }

       /**
        * Retrieve all rows which have an exact match for the specified fields.
        *
        * @param    tableMetadata
        *           The table metadata.
        * @param    fields
        *           The field list.
        * @param    values
        *           The values to be matched.
        *
        * @return   See above.
        */
       @Override
       public List<DataObject> selectRows(DataObject tableMetadata, String[] fields, Object[] values)
       {
          return DataSetSDOHelper.selectRows(tableMetadata, fields, values);
       }

       /**
        * Add a relation to the graph's metadata.
        *
        * @param    metaData
        *           The graph metadata.
        * @param    name
        *           The relation name.
        * @param    parentBuffer
        *           The parent buffer.
        * @param    childBuffer
        *           The child buffer.
        * @param    whereString
        *           The where string.
        * @param    active
        *           Flag indicating if this relation is active.
        * @param    parentId
        *           Flag indicating if parent-id is used.
        * @param    pairs
        *           The field pairs, in <code>parent, child</code> format.
        * @param    fkeyHidden
        *           Flag indicating if fkey-hidden is used.
        * @param    recursive
        *           Flag indicating if recursive is used.
        * @param    nested
        *           Flag indicating if nested is used.
        * @param    reposition
        *           Flag indicating if reposition is used.
        */
       @Override
       public void addRelation(DataObject metaData,
                               String name,
                               String parentBuffer,
                               String childBuffer,
                               String whereString,
                               boolean active,
                               boolean parentId,
                               String pairs,
                               boolean fkeyHidden,
                               boolean recursive,
                               boolean nested,
                               boolean reposition)
       {
          DataSetSDOHelper.addRelation(metaData,
                                       name,
                                       parentBuffer,
                                       childBuffer,
                                       whereString,
                                       active,
                                       parentId,
                                       pairs,
                                       fkeyHidden,
                                       recursive,
                                       nested,
                                       reposition);
       }

       /**
        * Get the relation metadata with the given name.
        *
        * @param    graph
        *           The data graph.
        * @param    name
        *           The relation name.
        *
        * @return   The relation metadata, or <code>null</code> if it does not exist.
        */
       @Override
       public LegacyRelationMetaData getRelation(DataGraph graph, String name)
       {
          return DataSetSDOHelper.getRelation(graph, name);
       }

       /**
        * Get the relation metadata with the given name.
        *
        * @param    metaData
        *           The graph metadata.
        * @param    name
        *           The relation name.
        *
        * @return   The relation metadata, or <code>null</code> if it does not exist.
        */
       @Override
       public LegacyRelationMetaData getRelation(DataObject metaData, String name)
       {
          return DataSetSDOHelper.getRelation(metaData, name);
       }

       /**
        * Get the all relation metadata from the given graph.
        *
        * @param    graph
        *           The data graph.
        *
        * @return   The relations metadata.
        */
       @Override
       public LegacyRelationMetaData[] getRelations(DataGraph graph)
       {
          return DataSetSDOHelper.getRelations(graph);
       }

       /**
        * Get the all relation metadata from the given graph.
        *
        * @param    metaData
        *           The graph metadata.
        *
        * @return   The relations metadata.
        */
       @Override
       public LegacyRelationMetaData[] getRelations(DataObject metaData)
       {
          return DataSetSDOHelper.getRelations(metaData);
       }

       /**
        * Create and populate a table from the results received from the request.
        * <p>
        * If {@link #defaultTrackChanges} is set, then logging is enabled for all rows.
        *
        * @param    graph
        *           The graph where to add the table.
        * @param    wrapper
        *           The table data.
        * @param    reuseMetadata
        *           Flag indicating if the table already exists in the graph.
        * @param    beforeTable
        *           Flag indicating if this is a before table.
        */
       private void createOutputTable(DataGraph    graph,
                                      TableWrapper wrapper,
                                      boolean      reuseMetadata,
                                      boolean      beforeTable)
       {
          DataObject metadata = DataSetSDOHelper.getMetaData(graph);
          ArrayList<PropertyDefinition> props = wrapper.getProperties();
          String tableName = beforeTable ? wrapper.getPeerDef().getTableName() : wrapper.getTableName();

          if (!beforeTable)
          {
             DataObject tableMeta = null;
             if (reuseMetadata)
             {
                // re-built the structures inside the metadata reference
                tableMeta = getTableMetaData(graph, tableName);
                DataSetSDOHelper.initTableMetaData(tableMeta,
                                                   tableName,
                                                   props.size(),
                                                   wrapper.getPeerDef() != null,
                                                   wrapper.getNumIndexes(),
                                                   wrapper.getIndexes(),
                                                   wrapper.getXmlns(),
                                                   wrapper.getXmlPrefix(),
                                                   wrapper.getXmlNodeName(),
                                                   wrapper.getErrorString(),
                                                   normalizedExtent);
             }
             else
             {
                tableMeta = DataSetSDOHelper.createTableMetaData(tableName,
                                                                 props.size(),
                                                                 wrapper.getPeerDef() != null,
                                                                 wrapper.getNumIndexes(),
                                                                 wrapper.getIndexes(),
                                                                 wrapper.getXmlns(),
                                                                 wrapper.getXmlPrefix(),
                                                                 wrapper.getXmlNodeName(),
                                                                 wrapper.getErrorString(),
                                                                 normalizedExtent);
             }

             List<LegacyFieldMetaData> fieldList = tableMeta.getList("fields");
             int idx = 1; // this is 1-based, this is how .xpxg proxies are emitted.
             for (int i = 0; i < props.size(); i++)
             {
                PropertyDefinition prop = props.get(i);
                LegacyFieldMetaData fieldMeta = new LegacyFieldMetaData(idx,
                                                                        prop.getLegacyName(),
                                                                        prop.getExtent(),
                                                                        prop.getType(),
                                                                        types2Legacy.get(prop.getType()),
                                                                        idx,
                                                                        0);
                DataSetSDOHelper.setFieldMetaData(fieldList, tableMeta, fieldMeta);
                idx++;
             }

             // after we have the fields set
             DataSetSDOHelper.addTableMetaData(metadata, tableMeta);
          }
          else
          {
             DataObject table = getTableMetaData(graph, tableName);
             List<DataObject> rows = beforeTable ? DataSetSDOHelper.getBeforeObjects(table)
                                                 : DataSetSDOHelper.getDataObjects(table);

             // clear the rows first
             for (DataObject row : rows)
             {
                row.delete();
             }
          }

          List<Object[]> rowsMeta = wrapper.getRowsMeta();
          List<Object[]> rows = wrapper.getRows();
          List<DataObject> tableRows = new ArrayList<>(rows.size());

          // set rows
          for (int recnum = 0; recnum < rows.size(); recnum++)
          {
             Object[] row = rows.get(recnum);
             Object[] rowMeta = rowsMeta.get(recnum);

             DataObject drow = DataSetSDOHelper.createDataObject(graph, tableName);
             if (!beforeTable && defaultTrackChanges)
             {
                ((FWDDataObjectType) drow).beginLogging();
             }

             try
             {
                ((FWDDataObjectType) drow).setForceNormalizedFields(true);
                // always use normalized fields here
                for (int idx = 0; idx < row.length; idx++)
                {
                   Object rval = row[idx];
                   PropertyDefinition prop = props.get(idx);

                   Class<?> cls = prop.getType();
                   Integer ltype = types2Legacy.get(cls);
                   if (ltype == null)
                   {
                      throw new NullPointerException(
                            "Unknown field type " + cls + " for " + prop.getLegacyName() + " in " + tableName);
                   }

                   Object val = toJavaValue(rval, ltype, true, prop.getExtent());

                   drow.set(idx, val);
                }
             }
             finally
             {
                ((FWDDataObjectType) drow).setForceNormalizedFields(false);
             }

             ((FWDDataObjectType) drow).primaryKey((Long) rowMeta[TempRecord._PK_DATA_INDEX]);
             ((FWDDataObjectType) drow)._rowState((Integer) rowMeta[TempRecord._ROW_STATE_DATA_INDEX]);
             ((FWDDataObjectType) drow)._peerRowid((Long) rowMeta[TempRecord._PEER_ROWID_DATA_INDEX]);
             ((FWDDataObjectType) drow)._originRowid((Long) rowMeta[TempRecord._ORIGIN_ROWID_DATA_INDEX]);
             ((FWDDataObjectType) drow)._datasourceRowid((Long) rowMeta[TempRecord._DATASOURCE_ROWID_INDEX]);
             ((FWDDataObjectType) drow)._errorFlags((Integer) rowMeta[TempRecord._ERROR_FLAG_DATA_INDEX]);
             ((FWDDataObjectType) drow)._errorString((String) rowMeta[TempRecord._ERROR_STRING_DATA_INDEX]);

             tableRows.add(drow);
          }

          if (beforeTable)
          {
             DataSetSDOHelper.setBeforeRows(graph, tableName, tableRows);
          }
          else
          {
             DataSetSDOHelper.setTableRows(graph, tableName, tableRows);
          }
       }

       /**
        * Create and populate a table from the results received from the request.
        * <p>
        * If {@link #defaultTrackChanges} is set, then logging is enabled for all rows.
        *
        * @param    graph
        *           The graph where to add the table.
        * @param    dst
        *           The table data.
        * @param    reuseMetadata
        *           Flag indicating if the table already exists in the graph.
        * @param    beforeTable
        *           Flag indicating if this is a before table.
        */
       private void createOutputTable(DataGraph         graph,
                                      DsTableDefinition dst,
                                      boolean           reuseMetadata,
                                      boolean           beforeTable)
       {
          DataObject metadata = DataSetSDOHelper.getMetaData(graph);
          List<PropertyDefinition> props = dst.getProperties();
          String tableName = beforeTable ? dst.getPeerDef().getName() : dst.getName();

          if (!beforeTable)
          {
             DataObject tableMeta = null;
             if (reuseMetadata)
             {
                // re-built the structures inside the metadata reference
                tableMeta = getTableMetaData(graph, tableName);
                DataSetSDOHelper.initTableMetaData(tableMeta,
                                                   tableName,
                                                   props.size(),
                                                   dst.getPeerDef() != null,
                                                   dst.getNumIndexes(),
                                                   dst.getIndexes(),
                                                   dst.getXmlns(),
                                                   dst.getXmlPrefix(),
                                                   dst.getXmlNodeName(),
                                                   dst.getErrorString(),
                                                   normalizedExtent);
             }
             else
             {
                tableMeta = DataSetSDOHelper.createTableMetaData(tableName,
                                                                 props.size(),
                                                                 dst.getPeerDef() != null,
                                                                 dst.getNumIndexes(),
                                                                 dst.getIndexes(),
                                                                 dst.getXmlns(),
                                                                 dst.getXmlPrefix(),
                                                                 dst.getXmlNodeName(),
                                                                 dst.getErrorString(),
                                                                 normalizedExtent);
             }

             List<LegacyFieldMetaData> fieldList = tableMeta.getList("fields");
             int idx = 1; // this is 1-based, this is how .xpxg proxies are emitted.
             for (PropertyDefinition prop : props)
             {
                LegacyFieldMetaData fieldMeta = new LegacyFieldMetaData(idx,
                                                                        prop.getLegacyName(),
                                                                        prop.getExtent(),
                                                                        prop.getType(),
                                                                        types2Legacy.get(prop.getType()),
                                                                        idx,
                                                                        0);
                DataSetSDOHelper.setFieldMetaData(fieldList, tableMeta, fieldMeta);
                idx++;
             }

             // after we have the fields set
             DataSetSDOHelper.addTableMetaData(metadata, tableMeta);
          }
          else
          {
             DataObject table = getTableMetaData(graph, tableName);
             List<DataObject> rows = beforeTable ? DataSetSDOHelper.getBeforeObjects(table)
                                                 : DataSetSDOHelper.getDataObjects(table);
             // clear the rows first
             for (DataObject row : rows)
             {
                row.delete();
             }
          }

          List<Object[]> rowsMeta = dst.getRowsMeta();
          List<Object[]> rows = dst.getRows();
          List<DataObject> tableRows = new ArrayList<>(rows.size());

          // set rows
          for (int recnum = 0; recnum < rows.size(); recnum++)
          {
             Object[] row = rows.get(recnum);
             Object[] rowMeta = rowsMeta.get(recnum);

             DataObject drow = DataSetSDOHelper.createDataObject(graph, tableName);
             if (!beforeTable && defaultTrackChanges)
             {
                ((FWDDataObjectType) drow).beginLogging();
             }

             try
             {
                // always use normalized fields here
                ((FWDDataObjectType) drow).setForceNormalizedFields(true);
                for (int idx = 0; idx < row.length; idx++)
                {
                   Object rval = row[idx];
                   PropertyDefinition prop = props.get(idx);

                   Class<?> cls = prop.getType();
                   Integer ltype = types2Legacy.get(cls);
                   if (ltype == null)
                   {
                      throw new NullPointerException(
                            "Unknown field type " + cls + " for " + prop.getLegacyName() + " in " + tableName);
                   }

                   Object val = toJavaValue(rval, ltype, true, prop.getExtent());
                   drow.set(idx, val);
                }
             }
             finally
             {
                ((FWDDataObjectType) drow).setForceNormalizedFields(false);
             }

             ((FWDDataObjectType) drow).primaryKey((Long) rowMeta[TempRecord._PK_DATA_INDEX]);
             ((FWDDataObjectType) drow)._rowState((Integer) rowMeta[TempRecord._ROW_STATE_DATA_INDEX]);
             ((FWDDataObjectType) drow)._peerRowid((Long) rowMeta[TempRecord._PEER_ROWID_DATA_INDEX]);
             ((FWDDataObjectType) drow)._originRowid((Long) rowMeta[TempRecord._ORIGIN_ROWID_DATA_INDEX]);
             ((FWDDataObjectType) drow)._datasourceRowid((Long) rowMeta[TempRecord._DATASOURCE_ROWID_INDEX]);
             ((FWDDataObjectType) drow)._errorFlags((Integer) rowMeta[TempRecord._ERROR_FLAG_DATA_INDEX]);
             ((FWDDataObjectType) drow)._errorString((String) rowMeta[TempRecord._ERROR_STRING_DATA_INDEX]);

             tableRows.add(drow);
          }

          if (beforeTable)
          {
             DataSetSDOHelper.setBeforeRows(graph, tableName, tableRows);
          }
          else
          {
             DataSetSDOHelper.setTableRows(graph, tableName, tableRows);
          }
       }

       /**
        * Create a default {@link BaseDataType} value for this field's type.
        *
        * @param    type
        *           The field's type.
        * @param    extent
        *           The field's extent.
        *
        * @return   The default BDT type for this field.
        */
       private Object defaultJavaType(int type, int extent)
       {
          Class<?> cls = legacyTypes.get(type);
          if (extent == 0 || extent == SourceNameMapper.NO_EXTENT)
          {
             return cls == MemoryBuffer.class ? new MemoryBuffer(new byte[0])
                                              : BaseDataType.generateDefault(cls);
          }
          else if (extent == SourceNameMapper.DYNAMIC_EXTENT)
          {
             Object res = Array.newInstance(cls, 0);
             return res;
          }

          Object res = Array.newInstance(cls, extent);
          for (int i = 0; i < extent; i++)
          {
             Array.set(res, i, cls == MemoryBuffer.class ? new MemoryBuffer(new byte[0])
                                                         : BaseDataType.generateDefault(cls));
          }

          return res;
       }

       /**
        * Convert the given value to a {@link BaseDataType} compatible instance.
        *
        * @param    value
        *           The Java value.
        * @param    type
        *           The value's type.
        * @param    extent
        *           The extent value.
        * @param    output
        *           Flag indicating OUTPUT mode.
        *
        * @return   See above.
        */
       private Object fromJavaType(Object value, int type, int extent, boolean output)
       {
          if (value == null)
          {
             Class<?> cls = legacyTypes.get(type);
             if (extent == 0 || extent == SourceNameMapper.NO_EXTENT)
             {
                return cls == MemoryBuffer.class ? new MemoryBuffer(new byte[0])
                                                 : BaseDataType.generateUnknown(cls);
             }
             else if (extent == SourceNameMapper.DYNAMIC_EXTENT)
             {
                Object res = Array.newInstance(cls, 0);
                return res;
             }
             else
             {
                Object res = Array.newInstance(cls, extent);
                for (int i = 0; i < extent; i++)
                {
                   Array.set(res, i, cls == MemoryBuffer.class ? new MemoryBuffer(new byte[0])
                                                               : BaseDataType.generateUnknown(cls));
                }

                return res;
             }
          }

          if (extent != 0 && extent != SourceNameMapper.NO_EXTENT)
          {
             Class<?> elType = legacyTypes.get(type);

             if (value instanceof List)
             {
                int length = ((List) value).size();
                Object res = Array.newInstance(elType, length);

                for (int i = 0; i < length; i++)
                {
                   Array.set(res, i, fromJavaType(((List) value).get(i), type, SourceNameMapper.NO_EXTENT, output));
                }

                return res;
             }
             else if (value.getClass().isArray())
             {
                int length = Array.getLength(value);
                Object res = Array.newInstance(elType, length);

                for (int i = 0; i < length; i++)
                {
                   Array.set(res, i, fromJavaType(Array.get(value, i), type, SourceNameMapper.NO_EXTENT, output));
                }

                return res;
             }

             // TODO: throw error
          }
          switch (type)
          {
             case TYPE_LOGICAL:
                return new logical((Boolean) value);

             case TYPE_CHARACTER:
                return new character((String) value);

             case TYPE_HANDLE:
                return handle.fromResourceId((Long) value);

             case TYPE_LONGCHAR:
                return new longchar((String) value);

             case TYPE_INTEGER:
                return new integer((Number) value);

             case TYPE_INT64:
                return new int64((Number) value);

             case TYPE_DECIMAL:
                decimal decimalValue = new decimal((Number) value);

                if (output)
                {
                   // should accommodate for any new value for output params
                   // otherwise precision is determined by the default 0 value
                   decimalValue.setPrecision(decimal.MAX_SCALE);
                }
                return decimalValue;

             case TYPE_BLOB:
                if (value.getClass().isArray() && value.getClass().getComponentType() == byte.class)
                {
                   return new blob(((byte[]) value));
                }
                else if (value instanceof MemoryBuffer)
                {
                   return new blob(((MemoryBuffer) value).getValue());
                }

                throw new IllegalStateException("Can not determine the blob value from " + value);

             case TYPE_CLOB:
                return new clob((String) value);

             case TYPE_RAW:
                return new raw((byte[]) value);

             case TYPE_RECID:
                return new recid((long) value);

             case TYPE_ROWID:
                if (value instanceof String)
                {
                   String s = (String) value;
                   s = s.trim();
                   if (s.isEmpty())
                   {
                      return new rowid();
                   }
                   value = Long.parseLong(s);
                }
                return new rowid((long) value);

             case TYPE_UNKNOWN:
                return unknown.UNKNOWN;

             case TYPE_MEMPTR:
                if (value.getClass().isArray() && value.getClass().getComponentType() == byte.class)
                {
                   return new MemoryBuffer(((byte[]) value));
                }
                else if (value instanceof MemoryBuffer)
                {
                   return value;
                }

                throw new IllegalStateException("Can not determine the blob value from " + value);

             case TYPE_DATE:
                if (value instanceof GregorianCalendar)
                {
                   GregorianCalendar gc = (GregorianCalendar) value;
                   value = Date.from(gc.toZonedDateTime().toInstant());

                   return new date((Date) value, gc.getTimeZone());
                }

                return new date((Date) value);

             case TYPE_DATETIME:
                if (value instanceof GregorianCalendar)
                {
                   GregorianCalendar gc = (GregorianCalendar) value;
                   value = Date.from(gc.toZonedDateTime().toInstant());

                   return new datetime((Date) value, gc.getTimeZone());
                }

                return new datetime((Date) value);

             case TYPE_DATETIMETZ:
                if (value instanceof GregorianCalendar)
                {
                   GregorianCalendar gc = (GregorianCalendar) value;
                   return new datetimetz(gc.toZonedDateTime().toOffsetDateTime());
                }
                else
                {
                   return new datetimetz((Date) value);
                }
          }

          throw new IllegalArgumentException("Can not convert FWD value of type " + type);
       }

       /**
        * Convert a {@link BaseDataType} instance to a Java value.
        *
        * @param    arg
        *           The value.
        * @param    type
        *           The value's type.
        * @param    field
        *           Flag indicating that this call is for a field value.
        * @param    extent
        *           The extent value.
        *
        * @return   See above.
        */
       private Object toJavaValue(Object arg, int type, boolean field, int extent)
       {
          if (arg instanceof BaseDataType && ((BaseDataType) arg).isUnknown())
          {
             return type == memptr() ? new MemoryBuffer() : null;
          }

          if (extent != 0 && extent != SourceNameMapper.NO_EXTENT)
          {
             Class<?> ctype = null;
             switch (type)
             {
                case TYPE_LOGICAL:
                   ctype = Boolean.class;
                   break;

                case TYPE_CHARACTER:
                case TYPE_LONGCHAR:
                case TYPE_CLOB:
                   ctype = String.class;
                   break;

                case TYPE_INTEGER:
                   ctype = Integer.class;
                   break;

                case TYPE_DECIMAL:
                   ctype = BigDecimal.class;
                   break;

                case TYPE_RAW:
                case TYPE_BLOB:
                   ctype = byte[].class;
                   break;

                case TYPE_HANDLE:
                case TYPE_RECID:
                case TYPE_INT64:
                case TYPE_ROWID:
                   ctype = Long.class;
                   break;

                case TYPE_MEMPTR:
                   ctype = MemoryBuffer.class;
                   break;

                case TYPE_DATE:
                case TYPE_DATETIME:
                case TYPE_DATETIMETZ:
                   ctype = GregorianCalendar.class;
                   break;
             }

             int length = Array.getLength(arg);
             Object res = field ? new ArrayList<>() : Array.newInstance(ctype, length);
             for (int i = 0; i < length; i++)
             {
                Object elem = toJavaValue(Array.get(arg, i), type, field, SourceNameMapper.NO_EXTENT);

                if (field)
                {
                   ((List) res).add(elem);
                }
                else
                {
                   Array.set(res, i, elem);
                }
             }

             return res;
          }

          switch (type)
          {
             case TYPE_LOGICAL:
                return ((logical) arg).booleanValue();

             case TYPE_CHARACTER:
                return ((character) arg).toStringMessage();

             case TYPE_HANDLE:
                return handle.resourceId(((handle) arg).getResource());

             case TYPE_LONGCHAR:
                return ((longchar) arg).toStringMessage();

             case TYPE_INTEGER:
                return ((integer) arg).intValue();

             case TYPE_INT64:
                return ((int64) arg).longValue();

             case TYPE_DECIMAL:
                BigDecimal val = ((decimal) arg).toBigDecimal();
                if (val.scale() > 0)
                {
                   BigDecimal val2 = val.stripTrailingZeros();
                   // a negative scale means scientific notation, so use only if there are decimal digits
                   // for .NET client, we need at least a decimal digit, otherwise it can be treated as 'integer'
                   // instead of 'decimal', for i.e. JSON serialization
                   return val2.scale() <= 0 ? val2.setScale(forceDecimalDigits ? 1 : 0) : val2;
                }
                else
                {
                   return val;
                }

             case TYPE_BLOB:
                return ((blob) arg).getByteArray();

             case TYPE_CLOB:
                return ((clob) arg).toJavaType();

             case TYPE_RAW:
                return ((raw) arg).getByteArray();

             case TYPE_RECID:
                return ((recid) arg).longValue();

             case TYPE_ROWID:
                return ((rowid) arg).getValue();

             case TYPE_UNKNOWN:
                return null;

             case TYPE_MEMPTR:
                return (MemoryBuffer) arg;

             case TYPE_DATE:
             case TYPE_DATETIME:
                GregorianCalendar gc = new GregorianCalendar();
                gc.setTime(((date) arg).dateValue());
                return gc;

             case TYPE_DATETIMETZ:
                return ((datetimetz) arg).calendarValue();
          }

          throw new IllegalArgumentException("Could not resolve java value of type " + type);
       }

       /**
        * Create a table definition part of a dataset parameter.
        *
        * @param    rows
        *           The table rows.
        * @param    fields
        *           The table fields.
        * @param    input
        *           Flag indicating if this parameter is in input or input-output mode.
        * @param    output
        *           Flag indicating if this parameter is in output or input-output mode.
        * @param    before
        *           Flag indicating if this table is a before-table.
        *
        * @return   A {@link DsTableDefinition} instance with the specified metadata and records.
        */
       private DsTableDefinition createDsTable(List<DataObject> rows,
                                               List<LegacyFieldMetaData> fields,
                                               boolean input,
                                               boolean output,
                                               boolean before)
       {
          Iterator<LegacyFieldMetaData> fieldIter = fields.iterator();
          Iterator<DataObject> rowIter = rows.iterator();
          DsTableDefinition dstable = new DsTableDefinition(new TableResultSet(input, output, false)
          {
             DataObject nextRow;

             @Override
             public Object[] nextRow()
             {
                nextRow = rowIter.next();

                int numFields = fields.size();
                Object[] data = new Object[numFields];
                ((FWDDataObjectType) nextRow).setForceNormalizedFields(true);
                for (int i = 0; i < data.length; i++)
                {
                   LegacyFieldMetaData field = fields.get(i);
                   int ftype = field.getLegacyType();
                   int extent = field.getExtent();
                   Object fval = nextRow.get(i);

                   if (before && ((FWDDataObjectType) nextRow)._rowState() == Buffer.ROW_CREATED)
                   {
                      data[i] = defaultJavaType(ftype, extent);
                   }
                   else
                   {
                      data[i] = fromJavaType(fval, ftype, extent, output);
                   }
                }
                ((FWDDataObjectType) nextRow).setForceNormalizedFields(false);

                return data;
             }

             @Override
             public Object[] nextRowMeta()
             {
                FWDDataObjectType row = (FWDDataObjectType) nextRow;

                return row.toArray();
             }

             @Override
             public PropertyDefinition nextProperty()
             {
                LegacyFieldMetaData fieldMeta = fieldIter.next();
                String name = fieldMeta.getName();
                Class<?> type = fieldMeta.getType();
                int extent = fieldMeta.getExtent();

                PropertyDefinition prop = new PropertyDefinition(name, type, extent, name);

                return prop;
             }

             @Override
             public boolean hasMoreRows()
             {
                return rowIter.hasNext();
             }

             @Override
             public boolean hasMoreProperties()
             {
                return fieldIter.hasNext();
             }
          });

          return dstable;
       }

       /**
        * Serialize the rows and fields in a {@link TableResultSet}, to be sent to the remote side.
        *
        * @param    rows
        *           The rows.
        * @param    fields
        *           The fields.
        * @param    input
        *           Flag indicating INPUT mode.
        * @param    output
        *           Flag indicating OUTPUT mode.
        *
        * @return   The wrapped rows.
        */
       private TableResultSet createTableResultSet(List<DataObject> rows,
                                                   List<LegacyFieldMetaData> fields,
                                                   boolean input,
                                                   boolean output,
                                                   boolean before)
       {
          Iterator<LegacyFieldMetaData> fieldIter = fields.iterator();
          Iterator<DataObject> rowIter = rows.iterator();
          TableResultSet ttrs = new TableResultSet(input, output, false)
          {
             DataObject nextRow;

             @Override
             public Object[] nextRow()
             {
                nextRow = rowIter.next();

                int numFields = fields.size();
                Object[] data = new Object[numFields];
                ((FWDDataObjectType) nextRow).setForceNormalizedFields(true);
                for (int i = 0; i < data.length; i++)
                {
                   LegacyFieldMetaData field = fields.get(i);
                   int ftype = field.getLegacyType();
                   int extent = field.getExtent();
                   Object fval = nextRow.get(i);

                   if (before && ((FWDDataObjectType) nextRow)._rowState() == Buffer.ROW_CREATED)
                   {
                      data[i] = defaultJavaType(ftype, extent);
                   }
                   else
                   {
                      data[i] = fromJavaType(fval, ftype, extent, output);
                   }
                }
                ((FWDDataObjectType) nextRow).setForceNormalizedFields(false);

                return data;
             }

             @Override
             public Object[] nextRowMeta()
             {
                FWDDataObjectType row = (FWDDataObjectType) nextRow;

                return row.toArray();
             }

             @Override
             public PropertyDefinition nextProperty()
             {
                LegacyFieldMetaData fieldMeta = fieldIter.next();
                String name = fieldMeta.getName();
                Class<?> type = fieldMeta.getType();
                int extent = fieldMeta.getExtent();

                return new PropertyDefinition(name, type, extent, name);
             }

             @Override
             public boolean hasMoreRows()
             {
                return rowIter.hasNext();
             }

             @Override
             public boolean hasMoreProperties()
             {
                return fieldIter.hasNext();
             }
          };

          return ttrs;
       }

       /**
        * Get the signature of a table based on the properties list
        *
        * @param    properties
        *           The properties of the table
        *
        * @return   A list containing each field with its type and extent
        */
       private List<Map<String, String>> getSignature(List<PropertyDefinition> properties)
       {
          List<Map<String, String>> signature = new LinkedList<>();

          for (int i = 0; i < properties.size(); i++)
          {
             Map<String, String> field = new LinkedHashMap<>();
             PropertyDefinition property = properties.get(i);
             int extent = property.getExtent() == -2 ? 0 : property.getExtent();
             field.put("type", property.getType().getName());
             field.put("extent", String.valueOf(extent));
             signature.add(field);
          }

          return signature;
       }
    }