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;
   }   
}