# DataSetSDOHelper.java

    /*
    ** Module   : DataSetSDOHelper.java
    ** Abstract : Helper APIs to work with DataSet in Tuscany.
    **
    ** 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 OM  20200120 Small typo fix.
    ** 004 CA  20200923 API improvements. Added extent support, other data-types.  Allow access to the hidden
    **                  temp-table fields.
    **     OM  20201120 Added support for marshaling "xmlNodeName" and "errorString" attributes.
    **     CA  20210831 Track the created record in the before-table, if change logging is enabled.
    **     CA  20210910 Changes to allow support for before-table returned from the remote request via an
    **                  OUTPUT/INPUT-OUTPUT table or dataset.
    **     CA  20220104 Added support for denormalized extent fields, for .NET client compatibility.
    **     CA  20220215 When creating a row, populate the extent fields with NULL values.
    **     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  20220428 BigDecimal and GregorianCalendar are correct data-types for decimal and date(time(tz)).
    **     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  20220930 Rows can be added multiple times, tuscany SDO will take care of not re-adding an existing
    **                  row.
    **     CA  20221006 Use a copy of the row instances associated with a meta field, when caching.
    **     CA  20221116 Added 'unknownValuesForFields', a flag which when set, will force the DataObject.get to
    **                  return null even for Java native types - without this flag, DataObject.get will return
    **                  i.e. 0 or false for int/boolean native types.
    ** 005 CA  20230221 Encapsulated the field and relation metadata in their own classes, to avoid the DataObject
    **                  overhead and increase performance.  Other performance optimizations.
    ** 006 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
    ** 007 CA  20230511 Backed out the HELPER_CONTEXT singleton, as is not thread-safe.
    ** 008 CA  20240305 Added namespace URI, namespace prefix and XML node-name for table/graph metadata in
    **                  OpenClient.
    ** 009 CA  20240331 Use logical constants for TRUE, FALSE, UNKNOWN, within internal FWD runtime, so logical
    **                  type checks must include sub-classes, too.
    ** 010 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.
    ** 011 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.
    ** 012 LS  20240807 Updated initGraphMeta so that the comparison between the tables is made based on the
    **                  structure and not on the name.
    **     LS  20240819 Added 'mergeTables', a flag which when set, will merge the response instead of replacing
    **                  it in the dataset.
    ** 013 SP  20240926 createBeforeRow() should not set the beforeRecord._peerRowId if state is ROW_DELETED.
    ** 014 CA  20241107 Added character constant support.
    ** 015 AS  20250213 Added APIs for index management.
    ** 016 AS  20250217 Resolve field names case insensitively for data object get/set/unset operations.
    ** 017 ICP 20250123 Added support for integer and int64 constants, fixed javadoc.
    ** 018 AP  20250411 Added more tests and improved support for denormalized extents.
    **     CA  20250424 'forcedNormalized' is used by FWD runtime, so normalized access needs to be allowed even
    **                  when using denormalized extents.
    **     CA  20250424 .NET compatibiltiy still uses normalized extent fields when transferring them.
    ** 019 AS  20250507 Added field caching for DataObjects.
    ** 020 AS  20250604 Made HelperContext scope a thread local class member.
    */

    /*
    ** 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.InvocationHandler;
    import java.lang.reflect.Proxy;
    import java.math.*;
    import java.util.*;
    import java.util.logging.*;

    import com.goldencode.p2j.util.logging.*;
    import com.goldencode.util.*;

    import org.apache.tuscany.sdo.api.*;

    import com.goldencode.p2j.persist.*;

    import commonj.sdo.*;
    import commonj.sdo.helper.*;

    /**
     * Defines APIs to load DATASETs as Tuscany SDO.  This allows compatibility with the legacy Java Open client.
     * <p>
     * The {@link com.goldencode.p2j.persist.DataSet} is constructed as a {@link DataGraph}, for which
     * the metadata has the following structure (each graph node is a {@link DataObject}):
     * <ul>
     *   <li>the root object has a <code>graph</code> type, with two properties:
     *    <ol>
     *       <li><code>scope</code>, which holds the {@link HelperContext} used to build the metadata.
     *       </li>
     *       <li><code>metadata</code>, having the custom <code>graphmetadata</code> type.</li>
     *    </ol>
     *   </li>
     *   <li><code>graphmetadata</code> defines the details about the tables and relations, in these properties:
     *    <ol>
     *       <li><code>scope</code>, which helps the {@link HelperContext} used to build the metadata</li>
     *       <li><code>name</code>, a String with the DataSet name</li>
     *       <li><code>xmlns</code>, a String with the XML namespace.</li>
     *       <li><code>xmlPrefix</code>, a String with the XML prefix.</li>
     *       <li><code>xmlNodeName</code>, a String with the XML node name.</li>
     *       <li><code>idSequence</code>, the internal record ID sequence</li>
     *       <li><code>tables</code>, a list of <code>tablemetadata</code> holding the defined tables</li>
     *       <li><code>relations</code>, a list of {@link LegacyRelationMetaData} holding the defined relations</li>
     *    </ol>
     *   </li>
     *   <li><code>tablemetadata</code> defines the details about a table:
     *    <ol>
     *       <li><code>idx</code>, an int with the table index in the datagraph's table list.</li>
     *       <li><code>name</code>, a String with the legacy table name.</li>
     *       <li><code>numFields</code>, an int with the number of defined fields.</li>
     *       <li><code>b4img</code>, a boolean flag identifying if a table as a BEFORE-IMAGE.</li>
     *       <li><code>numIndexes</code>, an int identifying the number of defined indexes.</li>
     *       <li><code>indexes</code>, a String with the index definitions.</li>
     *       <li><code>xmlns</code>, a String with the XML namespace.</li>
     *       <li><code>xmlPrefix</code>, a String with the XML prefix.</li>
     *       <li><code>xmlNodeName</code>, a String with the XML node name.</li>
     *       <li><code>errorString</code>, a String with the table's ERROR-STRING attribute value.</li>
     *       <li><code>tabletype</code>, a {@link Type} defining this table (used to add rows to the table). This
     *           type MUST extend the <code>baserowtype</code>, so that the rows know their container.
     *       </li>
     *       <li><code>fields</code>, a list with the field definitions, of {@link LegacyFieldMetaData} type.</li>
     *       <li><code>fieldMap</code>, a case-insensitive map with a cache of {@link LegacyFieldMetaData} for
     *           each defined field</li>
     *       <li><code>extent</code>, a boolean flag indicating that at least one field is extent</li>
     *       <li><code>rows</code>, a list with the table's rows (they have as type a custom type
     *           build based on the table's<code>fields</code>.  The base type is
     *           <code>baserowtype</code>, an empty type.
     *        </li>
     *       <li><code>beforeRows</code>, a list with the before-table's rows, with the same record
     *           type as the <code>rows</code> list.  These are created/managed only when logging is enabled.
     *       </li>
     *       <li><code>normalizedExtent</code>, flag indicating if the extent fields are kept in a single field
     *          (normalized, as for Java clients) or denormalized, with individual fields for each index (as for
     *          .NET clients).
     *       </li>
     *    </ol>
     *   </li>
     *   <li>{@link LegacyRelationMetaData} defines the details about a relation:
     *    <ol>
     *       <li><code>name</code>, a String with the relation name.</li>
     *       <li><code>parentBuffer</code>, a String with the name of the parent table.</li>
     *       <li><code>childBuffer</code>, a String with the name of the child table.</li>
     *       <li><code>whereString</code>, a String with the WHERE clause of this relation.</li>
     *       <li><code>active</code>, a Boolean with the relation's active state.</li>
     *       <li><code>parentId</code>, a Boolean with the parent-id state.</li>
     *       <li><code>pairs</code>, a String with <code>parent, child</code> field pairs, comma delimited.</li>
     *       <li><code>fkeyHidden</code>, a Boolean with the FOREIGN-KEY-HIDDEN status.</li>
     *       <li><code>recursive</code>, a Boolean with the recursive status.</li>
     *       <li><code>nested</code>, a Boolean with the NESTED status.</li>
     *       <li><code>reposition</code>, a Boolean with Rthe REPOSITION status.</li>
     *    </ol>
     *   </li>
     *   <li> {@link LegacyFieldMetaData} defines the details about a field:
     *    <ol>
     *       <li><code>idx</code>, an int with the field's position in the table's <code>fields</code></li>
     *       <li><code>name</code>, a String with the legacy field name; indexed for denormalized extent.</li>
     *       <li><code>extent</code>, an int with the field's extent.</li>
     *       <li><code>extentIndex</code>, an int with the extent field's index, for denormalized extent.</li>
     *       <li><code>extentField</code>, the legacy name of the extent field (non-indexed).</li>
     *       <li><code>type</code>, a {@link BaseDataType} class with the field's type.</li>
     *       <li><code>legacytype</code>, an int with the field's type, as specified by
     *           {@link LegacyJavaAppserver} constants.</li>
     *       <li><code>order</code>, an int with the fields order.</li>
     *       <li><code>xmlMapping</code>, a int with the XML name describing this field.</li>
     *    </ol>
     *   </li>
     * </ul>
     * TODO: The XML related metadata is NOT used now in {@link LegacyJavaAppserver}.
     */
    class DataSetSDOHelper
    {
       /** The URI for the custom types. */
       public static final String FWD_URI = "goldencode.com";

       /** The URI for defining known types (as int, String, etc). */
       public static final String SDO_API_URI = "commonj.sdo";

       /** A prefix added to denormalized extent fields. */
       public static final String DENORMALIZED_PREFIX = "__fwd_denormalized__";

       /** The suffix for a table row.*/
       private static final String FWD_TABLE_TYPE_SUFFIX = "---FWD-type" + (char) 2;

       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(DataSetSDOHelper.class);

       /**
        * Flag indicating any unset field will return null, instead of its default value (like 0 and false for
        * Java native types).
        */
       private static volatile boolean unknownValuesForFields = false;

       /**
        * Flag indicating the response will be merged, instead of being replaced in the dataset.
        */
       private static volatile boolean mergeTables = false;

       /**
        * Thread-local helper context for the data object.
        */
       private static final ThreadLocal<HelperContext> threadLocalScope = ThreadLocal.withInitial(() ->
       {
          HelperContext context = SDOUtil.createHelperContext();
          createGraphMetaDataTypes(context);
          return context;
       });

       /**
        * Set the {@link #unknownValuesForFields} flag.
        *
        * @param    state
        *           <code>true</code> to return null if the field is not set.
        */
       public static void setUnknownValuesForFields(boolean state)
       {
          unknownValuesForFields = state;
       }

       /**
        * Get the {@link #unknownValuesForFields} flag.
        *
        * @return   See above.
        */
       public static boolean isUnknownValuesForFields()
       {
          return unknownValuesForFields;
       }

       /**
        * Set the {@link #mergeTables} flag.
        *
        * @param    state
        *           <code>true</code> to merge the response.
        */
       public static void setMergeTables(boolean state)
       {
          mergeTables = state;
       }

       /**
        * Get the {@link #mergeTables} flag.
        *
        * @return   See above.
        */
       public static boolean isMergeTables()
       {
          return mergeTables;
       }

       /**
        * Begin the logging for changes in this graph.
        *
        * @param    graph
        *           The graph to track changes.
        */
       public static void beginLogging(DataGraph graph)
       {
          ChangeSummary cs = graph.getChangeSummary();
          cs.beginLogging();

          // clean up before-table records and start logging

          String[] tables = getTableNames(graph);
          for (String table : tables)
          {
             List<DataObject> rows = getDataObjects(graph, table);
             for (DataObject row : rows)
             {
                FWDDataObjectType fwdRow = (FWDDataObjectType) row;
                fwdRow.beginLogging();
             }

             List<DataObject> beforeRows = getBeforeObjects(graph, table);
             for (DataObject row : beforeRows)
             {
                row.delete();
             }
          }
       }

       /**
        * End the logging for changes in this graph.
        *
        * @param    graph
        *           The graph to track changes.
        */
       public static void endLogging(DataGraph graph)
       {
          ChangeSummary cs = graph.getChangeSummary();
          cs.endLogging();
       }

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

       /**
        * 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.
        */
       public static DataObject getMetaData(DataGraph graph)
       {
          return graph.getRootObject().getDataObject("metadata");
       }

       /**
        * Add a new new field with the specified configuration.
        *
        * @param    fields
        *           The table's fields list, as retrieved from the {@link DataObject}.
        * @param    metaData
        *           The table metadata, of type <code>tablemetadata</code>.  The field will be added
        *           to the <code>fields</code> property.
        * @param    field
        *           The field meta.
        *
        * @return   The created field metadata.
        */
       public static LegacyFieldMetaData setFieldMetaData(List<LegacyFieldMetaData>     fields,
                                                              DataObject                metaData,
                                                              LegacyFieldMetaData       field)
       {
          clearMap("fieldMap", metaData);
          if (field.extent > 0)
          {
             metaData.setBoolean("extent", true);
             boolean normalizedExtent = metaData.getBoolean("normalizedExtent");
             if (normalizedExtent)
             {
                // nothing to do for normalized extent (Java-style clients)
                fields.add(field);
                return field;
             }

             // for denormalized extent (.NET-style clients), create a field for each index
             for (int fidx = 1; fidx <= field.extent; fidx++)
             {
                String fname = buildDenormalizedFieldName(field.name, fidx);

                LegacyFieldMetaData extFieldMeta = setFieldMetaData(metaData,
                                                                    field.idx,
                                                                    fname,
                                                                    0,
                                                                    field.type,
                                                                    field.legacyType,
                                                                    field.order,
                                                                    field.xmlMapping);
                extFieldMeta.extentIndex = fidx;
                extFieldMeta.extentField = field.name;
             }
          }
          else
          {
             fields.add(field);
          }

          return field;
       }

       /**
        * 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    cls
        *           The fields {@link BaseDataType} class.
        * @param    legacyType
        *           The legacy type (numeric value based on {@link LegacyJavaAppserver} constants.
        * @param    order
        *           The field's order.
        * @param    xmlMapping
        *           The field's mapping.
        *
        * @return   The created field metadata.
        */
       public static LegacyFieldMetaData setFieldMetaData(DataObject metaData,
                                                          int        idx,
                                                          String     name,
                                                          int        extent,
                                                          Class<?>   cls,
                                                          int        legacyType,
                                                          int        order,
                                                          int        xmlMapping)
       {
          if (extent > 0)
          {
             metaData.setBoolean("extent", true);
          }
          // check if the field name is unique
          List<LegacyFieldMetaData> fields = metaData.getList("fields");
          for (int i = 0; i < fields.size(); i++)
          {
             LegacyFieldMetaData f = fields.get(i);
             if (name.equalsIgnoreCase(f.name))
             {
                String tname = getTableName(metaData);
                throw new IllegalStateException("Field " + name + " already exists in table " + tname + "!");
             }
          };

          LegacyFieldMetaData fieldMeta =
             new LegacyFieldMetaData(idx, name, extent, cls, legacyType, order, xmlMapping);
          return setFieldMetaData(fields, metaData, fieldMeta);
       }

       /**
        * 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.
        */
       public static LegacyRelationMetaData getRelation(DataGraph graph, String name)
       {
          return getRelation(getMetaData(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.
        */
       public static LegacyRelationMetaData getRelation(DataObject metaData, String name)
       {
          List<LegacyRelationMetaData> relations = metaData.getList("relations");
          for (LegacyRelationMetaData rel : relations)
          {
             if (rel.getName().equalsIgnoreCase(name))
             {
                return rel;
             }
          }

          return null;
       }

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

       /**
        * Get the all relation metadata from the given graph.
        *
        * @param    metaData
        *           The graph metadata.
        *
        * @return   The relations metadata.
        */
       public static LegacyRelationMetaData[] getRelations(DataObject metaData)
       {
          List<LegacyRelationMetaData> relations = metaData.getList("relations");
          LegacyRelationMetaData[] res = new LegacyRelationMetaData[relations.size()];

          for (int i = 0; i < relations.size(); i++)
          {
             res[i] = relations.get(i);
          }

          return res;
       }

       /**
        * 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.
        */
       public static 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)
       {
          LegacyRelationMetaData rel = new LegacyRelationMetaData(name,
                                                                  parentBuffer,
                                                                  childBuffer,
                                                                  whereString,
                                                                  active,
                                                                  parentId,
                                                                  pairs,
                                                                  fkeyHidden,
                                                                  recursive,
                                                                  nested,
                                                                  reposition);
          metaData.getList("relations").add(rel);
       }

       /**
        * 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).
        * @param   normalizedExtent
        *          Flag indicating if the extents are normalized or individual fields will be created for each
        *          index.
        *
        * @return   An object with the table metadata.
        */
       public static DataObject createTableMetaData(String   name,
                                                    int      numFields,
                                                    boolean  b4img,
                                                    int      numIndexes,
                                                    String   indexes,
                                                    String   xmlns,
                                                    String   xmlPrefix,
                                                    String   xmlNodeName,
                                                    String   errorString,
                                                    boolean  normalizedExtent)
       {
          HelperContext scope = getHelperContext();

          DataFactory dataFactory = scope.getDataFactory();
          DataObject tableMeta = dataFactory.create(FWD_URI, "tablemetadata");
          initTableMetaData(tableMeta,
                            name,
                            numFields,
                            b4img,
                            numIndexes,
                            indexes,
                            xmlns,
                            xmlPrefix,
                            xmlNodeName,
                            errorString,
                            normalizedExtent);
          return tableMeta;
       }

       /**
        * Initialize the given table metadata.
        *
        * @param   tableMeta
        *          The table's metadata object.
        * @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).
        * @param   normalizedExtent
        *          Flag indicating if the extents are normalized or individual fields will be created for each
        *          index.
        */
       public static void initTableMetaData(DataObject tableMeta,
                                            String     name,
                                            int        numFields,
                                            boolean    b4img,
                                            int        numIndexes,
                                            String     indexes,
                                            String     xmlns,
                                            String     xmlPrefix,
                                            String     xmlNodeName,
                                            String     errorString,
                                            boolean    normalizedExtent)
       {
          tableMeta.setString("name", name);
          tableMeta.setInt("numFields", numFields);
          tableMeta.setBoolean("b4img", b4img);
          tableMeta.setInt("numIndexes", numIndexes);
          tableMeta.setString("indexes", indexes);
          tableMeta.setString("xmlns", xmlns);
          tableMeta.setString("xmlPrefix", xmlPrefix);
          tableMeta.setString("xmlNodeName", xmlNodeName);
          tableMeta.setString("errorString", errorString);
          tableMeta.setBoolean("normalizedExtent", normalizedExtent);
          tableMeta.getList("fields").clear();
          tableMeta.getList("rows").clear();
          tableMeta.getList("beforeRows").clear();
          tableMeta.setBoolean("extent", false);
          clearMap("fieldMap", tableMeta);
       }

       /**
        * 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 static List<LegacyIndexDefinition> getLegacyIndexDefinition(DataObject tableMeta)
       {
          String indexes = tableMeta.getString("indexes");

          if(indexes == null || indexes.isEmpty())
          {
             throw new IllegalArgumentException("Null or empty indexes string is not allowed.");
          }

          List<LegacyIndexDefinition> indexList = new ArrayList<>();
          indexList.add(new LegacyIndexDefinition());
          StringBuilder sb = new StringBuilder();
          List<String> components = new ArrayList<>();
          components.add(new String());
          int i = 2;
          if(indexes.charAt(0) == '1')
          {
             indexList.get(0).setPrimaryUnique(true);
          }
          else if (indexes.charAt(0) == '0')
          {
             indexList.get(0).setPrimaryUnique(false);
          }
          else
          {
             i = 0;
          }

          int componentIndex = 0;
          int indexPosition = 0;
          int length = indexes.length();

          for (; i < length; i++)
          {
             if(indexes.charAt(i) == ' ')
             {
                continue;
             }
             if (indexes.charAt(i) == '.' || i == length - 1)
             {
                if(i == length - 1)
                {
                   sb.append(indexes.charAt(i));
                }
                indexList.get(indexPosition).setLegacyName(sb.toString());
                if (i != length - 1)
                {
                   components.add(new String());
                   indexList.add(new LegacyIndexDefinition());
                   indexPosition++;
                   sb.setLength(0);
                   continue;
                }
             }
             if (indexes.charAt(i) == ',')
             {
                components.set(componentIndex, sb.toString());
                components.add(new String());
                sb.setLength(0);
                componentIndex++;
                continue;
             }
             if (indexes.charAt(i) == ':')
             {
                components.set(componentIndex, sb.toString());
                indexList.get(indexPosition).setComponents(new ArrayList<>(components));
                components.clear();
                componentIndex = 0;
                sb.setLength(0);
                continue;
             }
             sb.append(indexes.charAt(i));
          }
          return indexList;
       }

       /**
        * A method that returns the table's primary unique index.
        *
        * @param tableMeta
        *        The table's metadata object.
        */
       public static LegacyIndexDefinition getPrimaryUniqueIndex(DataObject tableMeta)
       {
          String indexes = tableMeta.getString("indexes");
          LegacyIndexDefinition led = getLegacyIndexDefinition(tableMeta).get(0);
          if (led.isPrimaryUnique())
          {
             return led;
          }
          return null;
       }

       /**
        * 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 static LegacyIndexDefinition getIndex(String indexName, DataObject tableMeta)
       {
          if (indexName == null || indexName.isEmpty() || tableMeta == null)
          {
             throw new IllegalArgumentException("Null/empty primary unique index or " +
                     "data object meta data not allowed.");
          }
          List<LegacyIndexDefinition> indexes = getLegacyIndexDefinition(tableMeta);

          for (LegacyIndexDefinition index : indexes)
          {
             if (index.getLegacyName().equalsIgnoreCase(indexName))
             {
                return index;
             }
          }
          return null;
       }

       /**
        * 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 static boolean setPrimaryUniqueIndex(String primaryUniqueIndexName, DataObject tableMeta)
       {
          if (primaryUniqueIndexName == null || tableMeta == null)
          {
             throw new IllegalArgumentException("Null primary unique index or " +
                     "data object meta data not allowed.");
          }

          List<LegacyIndexDefinition> indexes = getLegacyIndexDefinition(tableMeta);
          LegacyIndexDefinition primaryUniqueIndex = getIndex(primaryUniqueIndexName, tableMeta);
          int existingIndexPosition = -1;
          existingIndexPosition = indexes.indexOf(primaryUniqueIndex);
          if (existingIndexPosition >= 0)
          {
             if (primaryUniqueIndex.isPrimaryUnique())
             {
                return false;
             }
             else
             {
                 indexes.get(0).setPrimaryUnique(false);
                 Collections.swap(indexes, 0, existingIndexPosition);
                 indexes.get(0).setPrimaryUnique(true);
             }
          }
          else
          {
             throw new IllegalArgumentException("Index not found. To set an index as primary unique, " +
                     "it should be present in indexes.");
          }
          tableMeta.setString("indexes", toFormattedString(indexes));
          return true;
       }

       /**
        * A method that adds an index to indexes.
        *
        * @param   index
        *          The index to be added. Must not be primary!
        * @param   tableMeta
        *          The table's metadata object.
        * @return   True if the index was added successfully.
        */
       public static boolean addIndex(LegacyIndexDefinition index, DataObject tableMeta)
       {
          if (index == null || tableMeta == null)
          {
             throw new IllegalArgumentException("Null unique index or " +
                                                "data object meta data not allowed.");
          }
          if (index.isPrimaryUnique())
          {
             throw new IllegalArgumentException("Adding primary indexes not allowed.");
          }
          List<LegacyIndexDefinition> indexes = getLegacyIndexDefinition(tableMeta);
          if (indexes.contains(index))
          {
             return false;
          }
          else
          {
             indexes.add(index);
             tableMeta.setString("indexes", toFormattedString(indexes));
             return true;
          }
       }

       /**
        * 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 static void removeIndex(String indexName, DataObject tableMeta)
       {
          if (indexName == null || indexName.isEmpty() || tableMeta == null)
          {
             throw new IllegalArgumentException("Null/empty unique index or " +
                                                "data object meta data not allowed.");
          }
          List<LegacyIndexDefinition> indexes = getLegacyIndexDefinition(tableMeta);
          for (LegacyIndexDefinition index : indexes)
          {
             if (index.getLegacyName().equalsIgnoreCase(indexName))
             {
                indexes.remove(index);
                tableMeta.setString("indexes", toFormattedString(indexes));
                return;
             }
          }
       }

       /**
        * 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 static boolean addComponentToIndex(String indexName, String field, DataObject tableMeta)
       {
          if (indexName == null   ||
              indexName.isEmpty() ||
              tableMeta == null   ||
              field == null       ||
              field.isEmpty())
          {
             throw new IllegalArgumentException("Null/empty arguments not allowed.");
          }

          if (!isTableField(tableMeta, field))
          {
             throw new IllegalArgumentException("Field " + field + " not found in table " +
                                                 tableMeta.getString("name"));
          }

          List<LegacyIndexDefinition> indexes = getLegacyIndexDefinition(tableMeta);
          for (LegacyIndexDefinition index : indexes)
          {
             if (index.getLegacyName().equalsIgnoreCase(indexName))
             {
                index.getComponents().add(field);
                tableMeta.setString("indexes", toFormattedString(indexes));
                return true;
             }
          }
          return false;
       }

       /**
        * 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 static boolean removeComponentFromIndex(String indexName, String field, DataObject tableMeta)
       {
          if (indexName == null  ||
             indexName.isEmpty() ||
             tableMeta == null   ||
             field == null       ||
             field.isEmpty())
          {
             throw new IllegalArgumentException("Null/empty arguments not allowed.");
          }

          if (!isTableField(tableMeta, field))
          {
             throw new IllegalArgumentException("Field " + field + " not found in table " +
                                                 tableMeta.getString("name"));
          }

          List<LegacyIndexDefinition> indexes = getLegacyIndexDefinition(tableMeta);
          for (LegacyIndexDefinition index : indexes)
          {
             if (index.getLegacyName().equalsIgnoreCase(indexName))
             {
                boolean removed = index.getComponents().remove(field);
                tableMeta.setString("indexes", toFormattedString(indexes));
                return removed;
             }
          }
          return false;
       }

       /**
        * 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 static String toFormattedString(List<LegacyIndexDefinition> indexes)
       {
          StringBuilder sb = new StringBuilder();
          for (int i = 0; i < indexes.size(); i++)
          {
             if (indexes.get(i).isPrimaryUnique() && i == 0)
             {
                sb.append("1,");
             }
             else if (i == 0)
             {
                sb.append("0,");
             }

             for (String component: indexes.get(i).getComponents())
             {
                sb.append(component);
                sb.append(',');
             }
             sb.setCharAt(sb.length() - 1, ':');
             sb.append(indexes.get(i).getLegacyName());
             sb.append('.');
          }
          sb.setLength(sb.length() - 1);
          return sb.toString();
       }

       /**
        * 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.
        */
       public static void addTableMetaData(DataObject metaData, DataObject table)
       {
          boolean sameGraph = table.getContainer() == metaData;
          if (table.getContainer() != null && !sameGraph)
          {
             List<LegacyFieldMetaData> fields = getFieldMetaData(table);

             String  name        = table.getString("name");
             int     numFields   = table.getInt("numFields");
             boolean b4img       = table.getBoolean("b4img");
             int     numIndexes  = table.getInt("numIndexes");
             String  indexes     = table.getString("indexes");
             String  xmlns       = table.getString("xmlns");
             String  xmlPrefix   = table.getString("xmlPrefix");
             String  xmlNodeName = table.getString("xmlNodeName");
             String  errorString = table.getString("errorString");
             boolean normalizedExtent = table.getBoolean("normalizedExtent");

             table = createTableMetaData(name, numFields, b4img, numIndexes, indexes, xmlns, xmlPrefix,
                                         xmlNodeName, errorString, normalizedExtent);
             List<LegacyFieldMetaData> fieldList = table.getList("fields");

             for (int i = 0; i < fields.size(); i++)
             {
                LegacyFieldMetaData fieldMeta = fields.get(i);
                setFieldMetaData(fieldList, table, fieldMeta);
             }
          }

          HelperContext scope = (HelperContext) metaData.get("scope");
          TypeHelper typeHelper = scope.getTypeHelper();

          List<LegacyFieldMetaData> fields = table.getList("fields"); // direct access, all fields are needed
          if (fields.isEmpty())
          {
             return;
          }

          String tableName = table.getString("name");
          if (!sameGraph)
          {
             // set the table index
             table.setInt("idx", metaData.getList("tables").size());

             metaData.getList("tables").add(table);
          }

          String tableTypeName = tableTypeName(tableName, fields);
          Type type = typeHelper.getType(FWD_URI, tableTypeName);
          if (type == null)
          {
             Type stringType  = typeHelper.getType(SDO_API_URI, "String");
             Type booleanType = typeHelper.getType(SDO_API_URI, "Boolean");
             Type intType     = typeHelper.getType(SDO_API_URI, "Int");
             Type longType    = typeHelper.getType(SDO_API_URI, "Long");
             Type doubleType  = typeHelper.getType(SDO_API_URI, "Double");
             Type dateType    = typeHelper.getType(SDO_API_URI, "Date");
             Type objectType  = typeHelper.getType(SDO_API_URI, "Object");
             Type bytesType   = typeHelper.getType(SDO_API_URI, "Bytes");

             DataObject tableType = createTypeDescription(scope, FWD_URI, tableTypeName);
             // the table must be derived from 'baserowtype' - otherwise the rows will not know the
             // container, and will not be able to access the table metadata
             Type baseRowType = typeHelper.getType(FWD_URI, "baserowtype");
             tableType.set("baseType", Arrays.asList(baseRowType));

             for (int i = 0; i < fields.size(); i++)
             {
                LegacyFieldMetaData field = fields.get(i);

                Class<?> cls = field.type;
                Type ftype = objectType;
                if (cls == integer.class || cls == integer.integerConstant.class)
                {
                   ftype = intType;
                }
                else if (cls == int64.class || cls == int64.int64Constant.class)
                {
                   ftype = longType;
                }
                else if (cls == decimal.class)
                {
                   ftype = objectType;
                }
                else if (cls == logical.class || cls == logical.logicalConstant.class)
                {
                   ftype = booleanType;
                }
                else if (cls == character.class  ||
                         cls == longchar.class   ||
                         cls == character.characterConstant.class)
                {
                   ftype = stringType;
                }
                else if (cls == date.class  || cls == datetime.class || cls == datetimetz.class)
                {
                   ftype = objectType;
                }
                else if (cls == memptr.class || cls == blob.class)
                {
                   ftype = bytesType;
                }

                DataObject prop = addPropertyDescription(tableType, ftype, field.name);

                int extent = field.extent;
                if (extent != 0 && extent != SourceNameMapper.NO_EXTENT)
                {
                   prop.setBoolean("many", true);
                }
             }

             type = typeHelper.define(tableType);
          }

          table.set("tabletype", type);
       }

       /**
        * Get the metadata for all fields in the specified table.
        * <p>
        * This is always in normalized mode, even if the extents are denormalized.
        *
        * @param    tableMeta
        *           The table metadata.
        *
        * @return   A list of field metadata objects.
        */
       public static List<LegacyFieldMetaData> getFieldMetaData(DataObject tableMeta)
       {
          return getFieldMetaData(tableMeta, true);
       }

       /**
        * Get the metadata for all fields in the specified table.
        *
        * @param    tableMeta
        *           The table metadata.
        * @param    normalized
        *           If the fields are required in normalized or denormalized mode.
        *
        * @return   A list of field metadata objects.
        */
       public static List<LegacyFieldMetaData> getFieldMetaData(DataObject tableMeta, boolean normalized)
       {
          List<LegacyFieldMetaData> allFields = tableMeta.getList("fields");
          boolean isExtent = tableMeta.getBoolean("extent");

          if (normalized && isExtent)
          {
             // do not include synthetic extent fields for denormalized extent
    		   List<LegacyFieldMetaData> legacyFields = new ArrayList<>(allFields.size());
    		   for (int i = 0; i < allFields.size(); i++)
    		   {
    		      LegacyFieldMetaData field = allFields.get(i);
    		      if (field.extentField == null)
    		      {
    		         legacyFields.add(field);
    		      }
    		      else
    		      {
    		         int extent = 0;
    		         while (i < allFields.size() && field.extentField.equalsIgnoreCase(allFields.get(i).extentField))
    		         {
    		            i = i + 1;
    		            extent = extent + 1;
    		         }
    		         i = i - 1;
                   LegacyFieldMetaData fext = new LegacyFieldMetaData(field.idx,
                                                                      field.extentField,
                                                                      extent,
                                                                      field.type,
                                                                      field.legacyType,
                                                                      field.order,
                                                                      field.xmlMapping);
                   legacyFields.add(fext);
    		      }
    		   }

    		   return legacyFields;
          }
          else
          {
             return new ArrayList<>(allFields);
          }
       }

       /**
        * Get the field metadata for the specified property.
        *
        * @param    tableMeta
        *           The table metadata.
        * @param    fname
        *           The field name.
        * @param    forceNormalizedFields
        *           When this flag is set, only normalized extent fields are looked.
        *
        * @return   The found field's metadata.
        *
        * @throws   IllegalStateException
        *           If the field can't be resolved.
        */
       public static LegacyFieldMetaData getFieldMetaData(DataObject tableMeta,
                                                          String     fname,
                                                          boolean    forceNormalizedFields)
       {
          return getFieldMetaData(tableMeta, fname, forceNormalizedFields, true);
       }

       /**
        * Get the field metadata for the specified property.
        *
        * @param    tableMeta
        *           The table metadata.
        * @param    fname
        *           The field name. The comparison is case insensitive.
        * @param    forceNormalizedFields
        *           When this flag is set, only normalized extent fields are looked.
        * @param    failNotFound
        *           Throw an {@link IllegalStateException} if the field can't be resolved.
        *
        * @return   The found field's metadata.
        *
        * @throws   IllegalStateException
        *           If the field can't be resolved and <code>failNotFound</code> flag is set.
        */
       public static LegacyFieldMetaData getFieldMetaData(DataObject tableMeta,
                                                          String     fname,
                                                          boolean    forceNormalizedFields,
                                                          boolean    failNotFound)
       {
          Map<String, LegacyFieldMetaData> fieldMap = (Map<String, LegacyFieldMetaData>)
                                                      tableMeta.get("fieldMap");
          if (fieldMap.isEmpty())
          {
             List<LegacyFieldMetaData> fields = tableMeta.getList("fields");
             for (LegacyFieldMetaData field : fields)
             {
                fieldMap.put(field.getName(), field);
             }
          }

          boolean normalizedExtent = tableMeta.getBoolean("normalizedExtent");
          boolean extent = tableMeta.getBoolean("extent");
          if (extent && !normalizedExtent && !forceNormalizedFields)
          {
             // if denormalized extent access is done outside of the FWD runtime,
             // then the lookup for denormalized extent fields has priority
             LegacyFieldMetaData f = fieldMap.get(DENORMALIZED_PREFIX + fname);
             if (f != null && f.extentIndex != null)
             {
                return f;
             }
          }

          // look for actual name match
          LegacyFieldMetaData f = fieldMap.get(fname);
          if (f != null)
          {
             return f;
          }

          String tname = getTableName(tableMeta);
          String msg = "Could not resolve field " + fname + " in table " + tname + "!";

          if (failNotFound)
          {
             throw new IllegalStateException(msg);
          }
          else if (LOG.isLoggable(Level.WARNING))
          {
             LOG.log(Level.WARNING, msg);
          }

          return null;
       }

       /**
        * 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.
        */
       public static DataObject getTableMetaData(DataObject metaData, int i)
       {
          return (DataObject) metaData.getList("tables").get(i);
       }

       /**
        * Get the metadata for the table registered with the given name in the graph.
        *
        * @param    metaData
        *           The graph metadata.
        * @param    name
        *           The table name.
        *
        * @return   The table metadata.
        */
       public static DataObject getTableMetaData(DataObject metaData, String name)
       {
          List<DataObject> tables = metaData.getList("tables");

          for (int i = 0; i < tables.size(); i++)
          {
             DataObject tableMeta = tables.get(i);
             String tname = tableMeta.getString("name");
             if (tname.equalsIgnoreCase(name))
             {
                return tableMeta;
             }
          }

          return null;
       }

       /**
        * 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.
        */
       public static DataObject getTableMetaData(DataGraph graph, String table)
       {
          return getTableMetaData(getMetaData(graph), table);
       }

       /**
        * Get the metadata list for all tables.
        *
        * @param    graph
        *           The graph.
        *
        * @return   The metadata for all tables.
        */
       public static List<DataObject> getTableMetaData(DataGraph graph)
       {
          List<DataObject> tables = new ArrayList<>();
          String[] tableNames = getTableNames(graph);
          for (String table : tableNames)
          {
             tables.add(getTableMetaData(graph, table));
          }
          return tables;
       }

       /**
        * Create a new graph to hold a dataset.  It holds just the root <code>graph</code> node.
        *
        * @return   A graph with no metadata.
        */
       public static DataGraph createDataGraph()
       {
          HelperContext scope = getHelperContext();
          TypeHelper typeHelper = scope.getTypeHelper();

          DataGraph graph = SDOUtil.createDataGraph();

          DataObject graphRoot = graph.createRootObject(typeHelper.getType(FWD_URI, "graph"));
          graphRoot.set("scope", scope);

          return graph;
       }

       /**
        * Create a new metadata object to hold the table details.
        *
        * @param    metadata
        *           The root graph node.
        *
        * @return   The graph metadata object.
        */
       public static DataGraph createDataGraph(DataObject metadata)
       {
          if (metadata.getContainer() != null)
          {
             throw new IllegalStateException("Metadata is already attached!");
          }

          HelperContext scope = (HelperContext) metadata.get("scope");
          TypeHelper typeHelper = scope.getTypeHelper();

          DataGraph graph = SDOUtil.createDataGraph();

          DataObject graphRoot = graph.createRootObject(typeHelper.getType(FWD_URI, "graph"));
          graphRoot.setDataObject("metadata", metadata);

          return graph;
       }

       /**
        * Set the metadata to the specified graph.
        *
        * @param    graph
        *           The graph object.
        * @param    metadata
        *           The metadata object which will be set to the graph's <code>metadata</code>
        *           property.
        */
       public static void setGraphMetaData(DataGraph graph, DataObject metadata)
       {
          if (metadata.getContainer() != null)
          {
             throw new IllegalStateException("Metadata is already attached!");
          }

          DataObject graphRoot = graph.getRootObject();
          graphRoot.setDataObject("metadata", metadata);
       }

       /**
        * 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 #setGraphMetaData}.
        */
       public static DataObject createGraphMetaData(String name)
       {
          return createGraphMetaData(name, null, null, null);
       }

       /**
        * Create a metadata object with the specified details.
        *
        * @param    name
        *           The graph name.
        * @param    xmlns
        *           The namespace URI.
        * @param    xmlPrefix
        *           The namespace prefix.
        * @param    xmlNodeName
        *           The XML node-name.
        *
        * @return   The metadata object.  Will be attached to a graph via {@link #setGraphMetaData}.
        */
       public static DataObject createGraphMetaData(String name,
                                                    String xmlns,
                                                    String xmlPrefix,
                                                    String xmlNodeName)
       {
          HelperContext scope = getHelperContext();
          DataFactory dataFactory = scope.getDataFactory();

          DataObject metadata = dataFactory.create(FWD_URI, "graphmetadata");
          metadata.set("scope", scope);
          initGraphMetaData(metadata, name, xmlns, xmlPrefix, xmlNodeName, (List<String>) null);

          return metadata;
       }

       /**
        * Initialize this graph metadata.
        *
        * @param    metadata
        *           The graph metadata.
        * @param    name
        *           The graph name.
        * @param    xmlns
        *           The namespace URI.
        * @param    xmlPrefix
        *           The namespace prefix.
        * @param    xmlNodeName
        *           The XML node-name.
        * @param    tableNames
        *           The table names to keep in this metadata.  May be {@code null}.
        */
       public static void initGraphMetaData(DataObject   metadata,
                                            String       name,
                                            String       xmlns,
                                            String       xmlPrefix,
                                            String       xmlNodeName,
                                            List<String> tableNames)
       {
          metadata.setString("name", name);
          if (xmlns != null)
          {
             metadata.setString("xmlns", xmlns);
          }
          else
          {
             metadata.unset("xmlns");
          }
          if (xmlPrefix != null)
          {
             metadata.setString("xmlPrefix", xmlPrefix);
          }
          else
          {
             metadata.unset("xmlPrefix");
          }
          if (xmlNodeName != null)
          {
             metadata.setString("xmlNodeName", xmlNodeName);
          }
          else
          {
             metadata.unset("xmlPrefix");
          }
          metadata.setLong("idSequence", 0);
          metadata.getList("relations").clear();
          if (tableNames == null)
          {
             metadata.getList("tables").clear();
          }
          else
          {
             HelperContext scope = getHelperContext();
             DataFactory dataFactory = scope.getDataFactory();

             List<DataObject> tables = metadata.getList("tables");
             List<DataObject> argumentTables = new ArrayList<>(tables);

             // clear and fix the tables
             tables.clear();

             for (int i = 0; i < tableNames.size(); i++)
             {
                String tableName = tableNames.get(i);

                // find the DataObject for this table
                DataObject table = null;
                for (int j = 0; j < argumentTables.size(); j++)
                {
                   DataObject argTable = argumentTables.get(j);
                   String argTableName = argTable.getString("name");
                   if (argTableName.equalsIgnoreCase(tableName))
                   {
                      table = argTable;
                      break;
                   }
                }

                if (table == null)
                {
                   // build a new metadata and set its name
                   table = dataFactory.create(FWD_URI, "tablemetadata");
                   table.setString("name", tableName);
                }

                // reset the metadata
                table.setInt("idx", tables.size());
                table.getList("fields").clear();
                clearMap("fieldMap", table);
                table.setBoolean("extent", false);
                table.getList("rows").clear();
                table.getList("beforeRows").clear();

                // add it to the graph
                tables.add(table);
             }
          }
       }

       /**
        * Initialize this graph metadata.
        *
        * @param    metadata
        *           The graph metadata.
        * @param    name
        *           The graph name.
        * @param    xmlns
        *           The namespace URI.
        * @param    xmlPrefix
        *           The namespace prefix.
        * @param    xmlNodeName
        *           The XML node-name.
        * @param    tableSignatures
        *           The table names and signatures to keep in this metadata.  May be {@code null}.
        *
        * @return   A map with old and new name pairs.
        */
       public static Map<String, String> initGraphMetaData(DataObject   metadata,
                                                           String       name,
                                                           String       xmlns,
                                                           String       xmlPrefix,
                                                           String       xmlNodeName,
                                                           Map<String, List<Map<String, String>>> tableSignatures)
       {
          Map<String, String> namePairs = new LinkedHashMap<>();
          metadata.setString("name", name);
          if (xmlns != null)
          {
             metadata.setString("xmlns", xmlns);
          }
          else
          {
             metadata.unset("xmlns");
          }
          if (xmlPrefix != null)
          {
             metadata.setString("xmlPrefix", xmlPrefix);
          }
          else
          {
             metadata.unset("xmlPrefix");
          }
          if (xmlNodeName != null)
          {
             metadata.setString("xmlNodeName", xmlNodeName);
          }
          else
          {
             metadata.unset("xmlPrefix");
          }
          metadata.setLong("idSequence", 0);
          metadata.getList("relations").clear();
          if (tableSignatures == null)
          {
             metadata.getList("tables").clear();
          }
          else
          {
             HelperContext scope = getHelperContext();
             DataFactory dataFactory = scope.getDataFactory();

             List<DataObject> tables = metadata.getList("tables");
             List<DataObject> argumentTables = new ArrayList<>(tables);

             // clear and fix the tables
             tables.clear();

             for (int i = 0; i < argumentTables.size(); i++)
             {
                DataObject argTable = argumentTables.get(i);
                List<LegacyFieldMetaData> fields = argTable.getList("fields");
                List<Map<String, String>> argTableSignature = new LinkedList<>();

                //create the signature for current table
                for (int j = 0; j < fields.size(); j++)
                {
                   LegacyFieldMetaData fieldMetaData = fields.get(j);
                   String extent = String.valueOf(fieldMetaData.getExtent());
                   String type = fieldMetaData.getType().getTypeName();
                   Map<String, String> currentField = new LinkedHashMap<>();
                   currentField.put("type", type);
                   currentField.put("extent", extent);
                   argTableSignature.add(currentField);
                }

                // check if there is a table that matches the signature of the argTable
                // and save the new name
                DataObject table = null;
                for (Map.Entry<String, List<Map<String, String>>> tableSignature : tableSignatures.entrySet())
                {
                   if (tableSignature.getValue().equals(argTableSignature))
                   {
                      String argTableName = argTable.getString("name");
                      String tableName = tableSignature.getKey();
                      if (!argTableName.equals(tableName))
                      {
                         namePairs.put(tableName, argTableName);
                      }
                      table = argTable;
                      tableSignatures.remove(tableName);
                      break;
                   }
                }

                if (table == null)
                {
                   // build a new metadata and set its name
                   table = dataFactory.create(FWD_URI, "tablemetadata");
                   table.setString("name", argTable.getString("name"));
                }

                // reset the metadata
                table.setInt("idx", tables.size());
                table.getList("fields").clear();
                clearMap("fieldMap", table);
                table.setBoolean("extent", false);
                table.getList("rows").clear();
                table.getList("beforeRows").clear();

                // add it to the graph
                tables.add(table);
             }
          }
          return namePairs;
       }

       /**
        * A method that clears a map property, if not set, instantiates one and sets it.
        * This method emulates the behavior of 'getList' for maps.
        *
        * @param   map
        *          The name of the map property.
        * @param   table
        *          Table metadata object.
        */
       private static void clearMap(String map, DataObject table)
       {
          Object value = table.get(map);
          if (value == null)
          {
             Map<String, Object> newMap = new CaseInsensitiveHashMap<>(100);
             table.set(map, newMap);
          }
          else if (value instanceof Map)
          {
             ((Map<?, ?>) value).clear();
          }
       }

       /**
        * Create a metadata object identical with the given metadata (except tables).
        *
        * @param    ori
        *           The source metadata.
        *
        * @return   The metadata object.  Will be attached to a graph via {@link #setGraphMetaData}.
        */
       public static DataObject createGraphMetaData(DataObject ori)
       {
          HelperContext scope = getHelperContext();
          DataFactory dataFactory = scope.getDataFactory();

          DataObject metadata = dataFactory.create(FWD_URI, "graphmetadata");
          metadata.set("scope", scope);
          metadata.setString("name", ori.getString("name"));
          if (ori.isSet("xmlns"))
          {
             metadata.setString("xmlns", ori.getString("xmlns"));
          }
          if (ori.isSet("xmlPrefix"))
          {
             metadata.setString("xmlPrefix", ori.getString("xmlPrefix"));
          }
          if (ori.isSet("xmlNodeName"))
          {
             metadata.setString("xmlNodeName", ori.getString("xmlNodeName"));
          }
          metadata.setLong("idSequence", 0);

          return metadata;
       }

       /**
        * Create the metadata types.
        *
        * @param    scope
        *           The helper context where the types will be defined.
        */
       public static void createGraphMetaDataTypes(HelperContext scope)
       {
          TypeHelper typeHelper = scope.getTypeHelper();
          Type stringType  = typeHelper.getType(SDO_API_URI, "String");
          Type booleanType = typeHelper.getType(SDO_API_URI, "Boolean");
          Type intType     = typeHelper.getType(SDO_API_URI, "Int");
          Type longType    = typeHelper.getType(SDO_API_URI, "Long");
          Type objectType  = typeHelper.getType(SDO_API_URI, "Object");

          /*
          graph:
             scope: HelperContext
             metadata: graphmetadata

          graphmetadata:
             scope: HelperContext
             name: String
             xmlns: String
             xmlPrefix: String
             xmlNodeName: String
             tables[]: tablemetadata
             relations[]: relationmetadata

          tablemetadata:
             idx: int
             name: string
             numFields: int
             b4img: boolean
             numIndexes: int
             indexes: String
             xmlns: String
             xmlPrefix: String
             xmlNodeName: String
             errorString: String
             tabletype: object
             fields[]: fieldmetadata
             fieldMap: CaseInsensitiveHashMap<String, LegacyFieldMetadata>
             extent: boolean
             rows[]: baserowtype
             beforeRows[]: baserowtype
             normalizedExtent: boolean

          relationmetadata:
             name: string
             parentBuffer: string
             childBuffer: string
             whereString: string
             active: boolean
             parentId: boolean
             pairs: string
             fkeyHidden: boolean
             recursive: boolean
             nested: boolean
             reposition: boolean

          fieldmetadata:
             idx: int
             name: String
             extent: int
             extentIndex: int
             extentField: String
             type: Class
             legacytype: type
             order: int
             xmlMapping: int
          */

          DataObject baseRowType = createTypeDescription(scope, FWD_URI, "baserowtype");
          typeHelper.define(baseRowType);

          DataObject tableMeta = createTypeDescription(scope, FWD_URI, "tablemetadata");
          addPropertyDescription(tableMeta, intType,     "idx");
          addPropertyDescription(tableMeta, stringType,  "name");
          addPropertyDescription(tableMeta, intType,     "numFields");
          addPropertyDescription(tableMeta, booleanType, "b4img");
          addPropertyDescription(tableMeta, intType,     "numIndexes");
          addPropertyDescription(tableMeta, stringType,  "indexes");
          addPropertyDescription(tableMeta, stringType,  "xmlns");
          addPropertyDescription(tableMeta, stringType,  "xmlPrefix");
          addPropertyDescription(tableMeta, stringType,  "xmlNodeName");
          addPropertyDescription(tableMeta, stringType,  "errorString");
          addPropertyDescription(tableMeta, booleanType, "normalizedExtent");
          addPropertyDescription(tableMeta, booleanType, "extent");
          addPropertyDescription(tableMeta, objectType,  "fieldMap");
          addPropertyDescription(tableMeta, objectType,  "tabletype");
          DataObject fields = addPropertyDescription(tableMeta, objectType, "fields");
          fields.setBoolean("many", true);
          DataObject rows = addPropertyDescription(tableMeta, baseRowType, "rows");
          rows.setBoolean("many", true);
          DataObject beforeRows = addPropertyDescription(tableMeta, baseRowType, "beforeRows");
          beforeRows.setBoolean("many", true);
          typeHelper.define(tableMeta);

          DataObject graphMeta = createTypeDescription(scope, FWD_URI, "graphmetadata");
          addPropertyDescription(graphMeta, objectType, "scope");
          addPropertyDescription(graphMeta, stringType, "name");
          addPropertyDescription(graphMeta, stringType,  "xmlns");
          addPropertyDescription(graphMeta, stringType,  "xmlPrefix");
          addPropertyDescription(graphMeta, stringType,  "xmlNodeName");
          addPropertyDescription(graphMeta, longType, "idSequence");
          DataObject tables = addPropertyDescription(graphMeta, tableMeta, "tables");
          tables.setBoolean("many", true);
          DataObject relations = addPropertyDescription(graphMeta, objectType, "relations");
          relations.setBoolean("many", true);
          typeHelper.define(graphMeta);

          DataObject graphType = createTypeDescription(scope, FWD_URI, "graph");
          addPropertyDescription(graphType, objectType, "scope");
          addPropertyDescription(graphType, graphMeta, "metadata");
          typeHelper.define(graphType);
       }

       /**
        * Clear the rows in the entire dataset.
        *
        * @param    metadata
        *           The metadata for this dataset.
        */
       public static void clearAllRows(DataObject metadata)
       {
          List<DataObject> tables = metadata.getList("tables");
          for (int i = 0; i < tables.size(); i++)
          {
             DataObject table = tables.get(i);
             table.getList("rows").clear();
             table.getList("beforeRows").clear();
          }
       }

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

       /**
        * Get all table names from the given graph metadata.
        *
        * @param    metadata
        *           The graph metadata.
        *
        * @return   See above.
        */
       public static String[] getTableNames(DataObject metadata)
       {
          List<DataObject> tables = metadata.getList("tables");
          String[] names = new String[tables.size()];

          for (int i = 0; i < names.length; i++)
          {
             names[i] = tables.get(i).getString("name");
          }

          return names;
       }

       /**
        * Get the number of tables for a graph metadata.
        *
        * @param    metadata
        *           The graph metadata.
        *
        * @return   See above.
        */
       public static int getNumTables(DataObject metadata)
       {
          List<DataObject> tables = metadata.getList("tables");
          return tables.size();
       }

       /**
        * Get the table name for a given row or table metadata.
        *
        * @param    value
        *           The table row or table metadata.
        *
        * @return   The table name.
        */
       public static String getTableName(DataObject value)
       {
          Type type = value.getType();

          if (type != null && type.getName().indexOf(FWD_TABLE_TYPE_SUFFIX) > 0)
          {
             // must be a row
             return tableFromType(type);
          }

          // must be the table metadata
          return value.getString("name");
       }

       /**
        * 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.
        */
       public static int getTableIndex(DataObject value)
       {
          Type type = value.getType();

          if (type != null && type.getName().indexOf(FWD_TABLE_TYPE_SUFFIX) > 0)
          {
             // must be a row
             value = value.getContainer();

             if (value == null)
             {
                throw new NullPointerException("The row is not attached to a table - can't resolve the table index.");
             }
          }

          // must be the table metadata
          return value.getInt("idx");
       }

       /**
        * 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.
        */
       public static int getTableIndex(DataGraph graph, String name)
       {
          String[] tables = getTableNames(graph);
          for (int i = 0; i < tables.length; i++)
          {
             if (tables[i].equalsIgnoreCase(name))
             {
                return i;
             }
          }

          String gname = getDataGraphName(getMetaData(graph));
          throw new IllegalArgumentException("Table " + name  + " does not exist in graph " + gname);
       }

       /**
        * 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.
        */
       public static int getTableIndex(DataObject metadata, String name)
       {
          String[] tables = getTableNames(metadata);
          for (int i = 0; i < tables.length; i++)
          {
             if (tables[i].equalsIgnoreCase(name))
             {
                return i;
             }
          }

          String gname = getDataGraphName(metadata);
          throw new IllegalArgumentException("Table " + name  + " does not exist in graph " + gname);
       }

       /**
        * 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.
        */
       public static String getGraphName(DataObject tableMetadata)
       {
          if (tableMetadata.getContainer() != null)
          {
             return getDataGraphName(tableMetadata.getContainer());
          }

          // TODO: tables without datasets?
          throw new IllegalArgumentException("The table is not part of a dataset!");
       }

       /**
        * 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.
        */
       public static int getFieldCount(DataObject row)
       {
          DataObject tableMeta = row.getContainer();

          return tableMeta.getInt("numFields");
       }

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

       /**
        * Get the field names for the specified table meta.
        *
        * @param    tableMeta
        *           The table metadata.
        *
        * @return   The field names.
        */
       public static String[] getTableFieldNames(DataObject tableMeta)
       {
          List<LegacyFieldMetaData> fields = getFieldMetaData(tableMeta);
          String[] res = new String[fields.size()];

          for (int i = 0; i < fields.size(); i++)
          {
             res[i] = fields.get(i).name;
          }

          return res;
       }

       /**
        * 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.
        */
       public static String getFieldName(DataObject row, int i)
       {
          DataObject tableMeta = row.getContainer();
          List<LegacyFieldMetaData> fields = getFieldMetaData(tableMeta);
          LegacyFieldMetaData field = fields.get(i);

          return field.name;
       }

       /**
        * 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.
        */
       public static int getFieldType(DataObject row, int i)
       {
          DataObject tableMeta = row.getContainer();
          List<LegacyFieldMetaData> fields = getFieldMetaData(tableMeta);
          LegacyFieldMetaData field = fields.get(i);

          return field.legacyType;
       }

       /**
        * 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.
        */
       public static boolean isTableField(DataObject tableMetadata, String fieldName)
       {
          List<LegacyFieldMetaData> fields = tableMetadata.getList("fields");
          for (int i = 0; i < fields.size(); i++)
          {
             LegacyFieldMetaData field = fields.get(i);
             if (fieldName.equalsIgnoreCase(field.name))
             {
                return true;
             }
          }

          return false;
       }

       /**
        * Get the field type.
        *
        * @param    tableMetadata
        *           The table metadata.
        * @param    fieldName
        *           The field name.
        *
        * @return   The field type.
        */
       public static int getTableFieldType(DataObject tableMetadata, String fieldName)
       {
          List<LegacyFieldMetaData> fields = tableMetadata.getList("fields");
          for (int i = 0; i < fields.size(); i++)
          {
             LegacyFieldMetaData field = fields.get(i);
             if (fieldName.equalsIgnoreCase(field.name))
             {
                return field.legacyType;
             }
          }

          throw new IllegalArgumentException("Field " + fieldName + " does not exist in " +
                                             getTableName(tableMetadata));
       }

       /**
        * Get the field type.
        *
        * @param    tableMetadata
        *           The table metadata.
        * @param    fieldIndex
        *           The field index.
        *
        * @return   See above.
        */
       public static int getTableFieldType(DataObject tableMetadata, int fieldIndex)
       {
          List<LegacyFieldMetaData> fields = tableMetadata.getList("fields");
          return fields.get(fieldIndex).legacyType;
       }

       /**
        * 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.
        */
       public static DataObject createDataObject(DataGraph graph, String table)
       {
          DataObject graphMeta = getMetaData(graph);
          DataObject tableMeta = getTableMetaData(graphMeta, table);

          return createDataObject(tableMeta);
       }

       /**
        * 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.
        */
       public static DataObject createDataObject(DataObject tableMetaData)
       {
          if (tableMetaData.getContainer() != null)
          {
             DataObject graphMeta = tableMetaData.getContainer();
             String table = getTableName(tableMetaData);

             HelperContext scope = (HelperContext) graphMeta.get("scope");
             DataObject tableMeta = getTableMetaData(graphMeta, table);
             Type type = (Type) tableMeta.get("tabletype");

             DataObject row = scope.getDataFactory().create(FWD_URI, type.getName());
             Set<Class<?>> ifaces = new HashSet<>();
             Class<?> parent = row.getClass();
             while (parent != null)
             {
                for (Class<?> iface : parent.getInterfaces())
                {
                   ifaces.add(iface);
                }
                parent = parent.getSuperclass();
             }
             ifaces.add(TempTableRecord.class);
             ifaces.add(FWDDataObjectType.class);

             Class<?>[] iface = ifaces.toArray(new Class[0]);
             InvocationHandler handler = new FWDDataObject();
             ClassLoader clsLoader = DataSetSDOHelper.class.getClassLoader();
             FWDDataObjectType fwdRow = (FWDDataObjectType) Proxy.newProxyInstance(clsLoader, iface, handler);
             fwdRow.setRow(row);
             fwdRow.setTableMetaData(tableMetaData);
             fwdRow.primaryKey(getNextId(graphMeta));

             List<LegacyFieldMetaData> fields = DataSetSDOHelper.getFieldMetaData(tableMetaData, false);
             boolean normalizedExtent = tableMetaData.getBoolean("normalizedExtent");
             if (normalizedExtent)
             {
                for (int i = 0; i < fields.size(); i++)
                {
                   LegacyFieldMetaData field = fields.get(i);
                   if (field.extent > 0)
                   {
                      List<?> data = new ArrayList<>();
                      for (int j = 0; j < field.extent; j++)
                      {
                         data.add(null);
                      }

                      row.setList(field.name, data);
                   }
                }
             }

             return (DataObject) fwdRow;
          }

          // TODO: table without datasets?
          throw new IllegalArgumentException("The table is not part of a dataset!");
       }

       /**
        * Set the table rows for the specified table.
        *
        * @param    graph
        *           The graph.
        * @param    tableName
        *           The table name.
        * @param    rows
        *           The list of rows.
        */
       public static void setTableRows(DataGraph graph, String tableName, List<DataObject> rows)
       {
          DataObject table = getTableMetaData(graph, tableName);
          table.setList("rows", rows);
       }

       /**
        * Set the before-table rows for the specified table.
        *
        * @param    graph
        *           The graph.
        * @param    tableName
        *           The table name.
        * @param    rows
        *           The list of rows.
        */
       public static void setBeforeRows(DataGraph graph, String tableName, List<DataObject> rows)
       {
          DataObject table = getTableMetaData(graph, tableName);
          table.setList("beforeRows", rows);
       }

       /**
        * Add the specified row to the graph.
        *
        * @param    graph
        *           The graph.
        * @param    row
        *           The row to add to the table.
        */
       public static void addDataObject(DataGraph graph, DataObject row)
       {
          DataObject graphMeta = getMetaData(graph);
          DataObject tableMeta = getTableMetaData(graphMeta, tableFromType(row.getType()));

          if (row.getContainer() != null && row.getContainer() != tableMeta)
          {
             row.getContainer().getList("rows").remove(row);
          }

          boolean added = tableMeta.getList("rows").add(row);

          if (added && graph.getChangeSummary().isLogging())
          {
             ((FWDDataObjectType) row).beginLogging();
             ((FWDDataObjectType) row).rowCreated();
          }
       }
       /**
        * Add the specified row to the graph.
        *
        * @param    tableMetaData
        *           The table metadata.
        * @param    row
        *           The row to add to the table.
        */
       public static void addDataObject(DataObject tableMetaData, DataObject row)
       {
          if (row.getContainer() != null && row.getContainer() != tableMetaData)
          {
             row.getContainer().getList("rows").remove(row);
          }

          boolean added = tableMetaData.getList("rows").add(row);

          if (added)
          {
             DataObject graphMeta = tableMetaData.getContainer();
             DataGraph graph = graphMeta.getDataGraph();

             if (graph.getChangeSummary().isLogging())
             {
                ((FWDDataObjectType) row).beginLogging();
                ((FWDDataObjectType) row).rowCreated();
             }
          }
       }

       /**
        * 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.
        */
       public static List<DataObject> getDataObjects(DataGraph graph, int idx)
       {
          DataObject graphMeta = getMetaData(graph);
          DataObject table = getTableMetaData(graphMeta, idx);

          return new ArrayList<>(table.getList("rows"));
       }

       /**
        * Get all the records for this table.
        *
        * @param    table
        *           The table metadata.
        *
        * @return   The list of rows.
        */
       public static List<DataObject> getDataObjects(DataObject table)
       {
          return new ArrayList<>(table.getList("rows"));
       }

       /**
        * Get the before-table rows for this after-table.
        *
        * @param    table
        *           The after-table.
        *
        * @return   The list of before-table rows.
        */
       public static List<DataObject> getBeforeObjects(DataObject table)
       {
          return new ArrayList<>(table.getList("beforeRows"));
       }

       /**
        * Get all the records for the table with the given name, in the graph metadata.
        *
        * @param    graph
        *           The graph.
        * @param    name
        *           The table name.
        *
        * @return   The list of rows.
        */
       public static List<DataObject> getDataObjects(DataGraph graph, String name)
       {
          DataObject graphMeta = getMetaData(graph);
          return getDataObjects(graphMeta, name);
       }

       /**
        * Get all the records for the table with the given name, in the graph metadata.
        *
        * @param    graphMeta
        *           The graph metadata.
        * @param    name
        *           The table name.
        *
        * @return   The list of rows.
        */
       public static List<DataObject> getDataObjects(DataObject graphMeta, String name)
       {
          DataObject table = getTableMetaData(graphMeta, name);

          return new ArrayList<>(table.getList("rows"));
       }

       /**
        * Get the before-table rows for the specified after-table.
        *
        * @param    graph
        *           The graph.
        * @param    name
        *           The table name.
        *
        * @return   The list of before-table rows.
        */
       public static List<DataObject> getBeforeObjects(DataGraph graph, String name)
       {
          DataObject graphMeta = getMetaData(graph);
          DataObject table = getTableMetaData(graphMeta, name);

          return new ArrayList<>(table.getList("beforeRows"));
       }

       /**
        * 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.
        */
       public static List<DataObject> selectRows(DataObject tableMetadata, String[] fields, Object[] values)
       {
          List<DataObject> res = new ArrayList<>();

          List<DataObject> rows = tableMetadata.getList("rows");
          for (DataObject row : rows)
          {
             boolean allEqual = true;
             for (int i = 0; i < fields.length; i++)
             {
                Object val = row.get(fields[i]);
                if (!((val == null && values[i] == null) || (val != null && val.equals(values[i]))))
                {
                   allEqual = false;
                   break;
                }
             }

             if (allEqual)
             {
                res.add(row);
             }
          }

          return res;
       }

       /**
        * Get the child rows for this record.
        *
        * @param    row
        *           The row.
        * @param    relation
        *           The child relation.
        *
        * @return   The child rows.
        */
       public static List<DataObject> getChildRows(DataObject row, String relation)
       {
          DataObject tableMeta = row.getContainer();
          DataObject graphMeta = tableMeta.getContainer();
          List<LegacyRelationMetaData> relations = graphMeta.getList("relations");
          LegacyRelationMetaData found = null;
          for (LegacyRelationMetaData rel : relations)
          {
             String name = rel.getName();
             if (name.equalsIgnoreCase(relation))
             {
                found = rel;
                break;
             }
          }

          if (found == null)
          {
             // TODO: error?
             return Collections.emptyList();
          }

          String child = found.getChild();
          String[] pairs = found.getPairs().split(","); // parent, child
          List<DataObject> childRows = getDataObjects(graphMeta, child);

          Object[] parentValues = new Object[pairs.length / 2];
          for (int i = 0, idx = 0; i < pairs.length; i += 2, idx++)
          {
             parentValues[idx] = row.get(pairs[i]);
          }
          List<DataObject> res = new ArrayList<>();
          l1: for (DataObject childRow : childRows)
          {
             for (int i = 1, idx = 0; i < pairs.length; i += 2, idx++)
             {
                if (!parentValues[idx].equals(childRow.get(pairs[i])))
                {
                   continue l1;
                }
             }

             // all match, get it
             res.add(childRow);
          }
          return res;
       }

       /**
        * Create a new type description.
        *
        * @param    scope
        *           The context.
        * @param    uri
        *           The type URI.
        * @param    name
        *           The type name.
        *
        * @return   A new object defining this type.
        */
       protected static DataObject createTypeDescription(HelperContext scope, String uri, String name)
       {
          DataObject typeDesc = scope.getDataFactory().create(SDO_API_URI, "Type");
          typeDesc.set("name", name);
          typeDesc.set("uri", uri);
          return typeDesc;
       }

       /**
        * Create a new property for the specified type.
        *
        * @param    containerType
        *           The object type.
        * @param    propertyType
        *           The property type.
        * @param    propertyName
        *           The property name.
        *
        * @return   The created property.
        */
       protected static DataObject addPropertyDescription(DataObject containerType,
                                                          Object     propertyType,
                                                          String     propertyName)
       {
          DataObject property = containerType.createDataObject("property");
          property.set("type", propertyType);
          property.setString("name", propertyName);
          property.setBoolean("containment", true);
          return property;
       }

       /**
        * Compute the denormalized field name.  This name will start with the {@link #DENORMALIZED_PREFIX} and
        * end with the field's index.
        *
        * @param    fname
        *           The legacy extent field name.
        * @param    idx
        *           The field's index.
        *
        * @return   See above.
        */
       static String buildDenormalizedFieldName(String fname, int idx)
       {
          return DENORMALIZED_PREFIX + fname + idx;
       }

       /**
        * Mark this record as being changed, in the before-table.
        *
        * @param    record
        *           The changed record.
        */
       static void logChanged(FWDDataObjectType record)
       {
          record._rowState(Buffer.ROW_MODIFIED);
          createBeforeRow(record, Buffer.ROW_MODIFIED);
       }

       /**
        * Mark this record as being deleted, in the before-table.
        *
        * @param    record
        *           The delete record.
        */
       static void logDeleted(FWDDataObjectType record)
       {
          record._rowState(Buffer.ROW_DELETED);
          createBeforeRow(record, Buffer.ROW_DELETED);
       }

       /**
        * Mark this record as being created, in the before-table.
        *
        * @param    record
        *           The created record.
        */
       static void logCreated(FWDDataObjectType record)
       {
          record._rowState(Buffer.ROW_CREATED);
          createBeforeRow(record, Buffer.ROW_CREATED);
       }

       /**
        * Add a new before-table row.
        *
        * @param    graph
        *           The graph.
        * @param    row
        *           The row to add.
        */
       static void addBeforeObject(DataGraph graph, DataObject row)
       {
          if (row.getContainer() != null)
          {
             throw new IllegalStateException("Row is already attached!");
          }

          DataObject graphMeta = getMetaData(graph);
          DataObject tableMeta = getTableMetaData(graphMeta, tableFromType(row.getType()));

          tableMeta.getList("beforeRows").add(row);
       }

       /**
        * Create a before-table row with the specified state.
        *
        * @param    record
        *           The after-table row.
        * @param    state
        *           The record state.
        */
       private static void createBeforeRow(FWDDataObjectType record, int state)
       {
          DataObject theRow = record.getRow();
          String table = getTableName(theRow);

          DataGraph graph = theRow.getDataGraph();
          List<DataObject> beforeRows = getBeforeObjects(graph, table);
          for (DataObject row : beforeRows)
          {
             Long peerRowId = ((FWDDataObjectType) row)._peerRowid();
             if (peerRowId != null && peerRowId.equals(record.primaryKey()))
             {
                // already logged, ensure proper state
                ((FWDDataObjectType) row)._rowState(state);
                return;
             }
          }

          // create a copy of the record
          DataObject beforeRow = createDataObject(graph, table);
          List<Property> properties = theRow.getInstanceProperties();
          try
          {
             ((FWDDataObjectType) beforeRow).setForceNormalizedFields(true);
             // always use normalized fields here
             for (Property prop : properties)
             {
                beforeRow.set(prop, theRow.get(prop));
             }
          }
          finally
          {
             ((FWDDataObjectType) beforeRow).setForceNormalizedFields(false);
          }
          addBeforeObject(graph, beforeRow);

          FWDDataObjectType beforeRecord = (FWDDataObjectType) beforeRow;
          beforeRecord._rowState(state);
          record._peerRowid(beforeRecord.primaryKey());
          if (state == Buffer.ROW_DELETED)
          {
             // don't set the before record peer rowId because the record will be deleted anyway.
             return;
          }
          beforeRecord._peerRowid(record.primaryKey());
       }

       /**
        * Construct the custom type to hold records for this table.
        *
        * @param    table
        *           The table name.
        * @param    fields
        *           The table's fields.
        *
        * @return   The custom type name.
        */
       private static String tableTypeName(String table, List<LegacyFieldMetaData> fields)
       {
          String key = table + FWD_TABLE_TYPE_SUFFIX + getFieldListKey(fields);

          return key;
       }

       /**
        * Get the key associated with a field list.  This is used to compute the {@link #tableTypeName table name}.
        *
        * @param    fields
        *           The field list.
        *
        * @return   The key.
        */
       private static String getFieldListKey(List<LegacyFieldMetaData> fields)
       {
          StringBuilder key = new StringBuilder(fields.size() * 64);

          char delim1 = (char) 1;
          char delim3 = (char) 3;

          for (int i = 0; i < fields.size(); i++)
          {
             LegacyFieldMetaData field = fields.get(i);

             if (field.extentIndex != null)
             {
                // skip the denormalized fields from the key
                continue;
             }

             key.append(field.idx).append(delim1);
             key.append(field.name).append(delim1);
             key.append(field.extent).append(delim1);
             key.append(field.legacyType).append(delim1);
             key.append(field.order).append(delim1);
             key.append(field.xmlMapping).append(delim1);
             key.append(delim3);
          }

          return key.toString();
       }

       /**
        * Compute the table name from the custom type of the table's records.
        *
        * @param    type
        *           The table's record type.
        *
        * @return   The table name.
        */
       private static String tableFromType(Type type)
       {
          String name = type.getName();
          return name.substring(0, name.indexOf(FWD_TABLE_TYPE_SUFFIX));
       }

       /**
        * Get next internal record ID for this graph.
        *
        * @param    graph
        *           The graph.
        *
        * @return   The next record ID.
        */
       private static long getNextId(DataGraph graph)
       {
          return getNextId(getMetaData(graph));
       }

       /**
        * Get next internal record ID for this graph.
        *
        * @param    graphMeta
        *           The graph metadata.
        *
        * @return   The next record ID.
        */
       private static long getNextId(DataObject graphMeta)
       {
          long nextId = graphMeta.getLong("idSequence");
          nextId = nextId + 1;
          graphMeta.setLong("idSequence", nextId);

          return nextId;
       }

       /**
        * Get the {@link HelperContext} instance.
        *
        * @return   See above.
        */
       private static HelperContext getHelperContext()
       {
          return threadLocalScope.get();
       }

       /**
        * A simple test for the Tuscany SDO APIs.
        */
       public static void main(String[] args)
       {
          boolean normalizedExtentForTt1 = false;
          boolean normalizedExtentForTt2 = true;
          DataObject metadata = createGraphMetaData("ds1");
          DataGraph graph = createDataGraph(metadata);
          DataObject tableTt1 = createTableMetaData("tt1", 6, false, 0, null, null, null, null, null,
                                                    normalizedExtentForTt1);
          DataObject tableTt2 = createTableMetaData("tt2", 2, false, 0, null, null, null, null, null,
                                                    normalizedExtentForTt2);

          // field index is always 1-based
          setFieldMetaData(tableTt1, 1, "f1", 0, integer.class, LegacyJavaAppserver.TYPE_INTEGER, 0, 0);
          setFieldMetaData(tableTt1, 2, "f2", 0, character.class, LegacyJavaAppserver.TYPE_CHARACTER, 1, 0);
          setFieldMetaData(tableTt1, 3, "f3", 0, memptr.class, LegacyJavaAppserver.TYPE_MEMPTR, 2, 0);
          setFieldMetaData(tableTt1, 4, "f4", 0, decimal.class, LegacyJavaAppserver.TYPE_DECIMAL, 3, 0);
          setFieldMetaData(tableTt1, 5, "f5", 0, logical.class, LegacyJavaAppserver.TYPE_LOGICAL, 4, 0);
          setFieldMetaData(tableTt1, 6, "f6", 2, decimal.class, LegacyJavaAppserver.TYPE_DECIMAL, 5, 0);
          setFieldMetaData(tableTt1, 7, "f7", 2, decimal.class, LegacyJavaAppserver.TYPE_DECIMAL, 6, 0);

          setFieldMetaData(tableTt2, 1, "f1", 0, integer.class, LegacyJavaAppserver.TYPE_INTEGER, 0, 0);
          setFieldMetaData(tableTt2, 2, "f2", 2, decimal.class, LegacyJavaAppserver.TYPE_DECIMAL, 1, 0);
          setFieldMetaData(tableTt2, 3, "f3", 0, date.class, LegacyJavaAppserver.TYPE_DATE, 2, 0);
          setFieldMetaData(tableTt2, 4, "f4", 3, decimal.class, LegacyJavaAppserver.TYPE_DECIMAL, 3, 0);

          // TODO: relations?

          addTableMetaData(metadata, tableTt1);
          addTableMetaData(metadata, tableTt2);

          addRelation(metadata, "r1", "tt1", "tt2", "", true, false, "f1,f1", false, false, true, false);

          LegacyRelationMetaData[] relations = getRelations(graph);
          relations = getRelations(metadata);
          for (LegacyRelationMetaData rel : relations)
          {
             String name = rel.getName();
             String parent = rel.getParent();
             String child = rel.getChild();

             System.out.println("relation: " + name + " " + parent + "/" + child);
          }

          DataObject metadata2 = getMetaData(graph);
          if (metadata != metadata2)
          {
             LOG.warning("false!");
          }

          System.out.println(getDataGraphName(metadata));
          String[] names = getTableNames(graph);
          for (String nm : names)
          {
             System.out.println(nm);
          }

          DataObject rtt1 = createDataObject(graph, "tt1");

          // test 'forceNormalized
          ((FWDDataObjectType) rtt1).setForceNormalizedFields(true);
          rtt1.set(3, -1.0);
          rtt1.set(5, Arrays.asList(1.0, 2.0));
          rtt1.set(6, Arrays.asList(3.0, 4.0));
          double f4 = (double) rtt1.get(3);
          List l6 = (List) rtt1.get(5);
          List l7 = (List) rtt1.get(6);
          assert f4 == -1;
          assert (double) l6.get(0) == 1.0 && (double) l6.get(1) == 2.0;
          assert (double) l7.get(0) == 3.0 && (double) l7.get(1) == 4.0;
          assert rtt1.isSet(3);
          assert rtt1.isSet(5);
          assert rtt1.isSet(6);
          rtt1.unset(3);
          rtt1.unset(5);
          rtt1.unset(6);
          assert !rtt1.isSet(3);
          assert !rtt1.isSet(5);
          assert !rtt1.isSet(6);

          rtt1.set(3, -2.0);
          rtt1.setList(5, Arrays.asList(10.0, 20.0));
          rtt1.setList(6, Arrays.asList(30.0, 40.0));
          f4 = rtt1.getDouble(3);
          l6 = rtt1.getList(5);
          l7 = rtt1.getList(6);
          assert f4 == -2;
          assert (double) l6.get(0) == 10.0 && (double) l6.get(1) == 20.0;
          assert (double) l7.get(0) == 30.0 && (double) l7.get(1) == 40.0;

          rtt1.set("f4", -3.0);
          rtt1.set("f6", Arrays.asList(11.0, 12.0));
          rtt1.set("f7", Arrays.asList(13.0, 14.0));
          f4 = (double) rtt1.get("f4");
          l6 = (List) rtt1.get("f6");
          l7 = (List) rtt1.get("f7");
          assert f4 == -3;
          assert (double) l6.get(0) == 11.0 && (double) l6.get(1) == 12.0;
          assert (double) l7.get(0) == 13.0 && (double) l7.get(1) == 14.0;
          assert rtt1.isSet("f4");
          assert rtt1.isSet("f6");
          assert rtt1.isSet("f7");
          rtt1.unset(3);
          rtt1.unset(5);
          rtt1.unset(6);
          assert !rtt1.isSet("f4");
          assert !rtt1.isSet("f6");
          assert !rtt1.isSet("f7");

          rtt1.setDouble("f4", -4.0);
          rtt1.setList("f6", Arrays.asList(110.0, 120.0));
          rtt1.setList("f7", Arrays.asList(130.0, 140.0));
          f4 = rtt1.getDouble("f4");
          l6 = rtt1.getList("f6");
          l7 = rtt1.getList("f7");
          assert f4 == -4;
          assert (double) l6.get(0) == 110.0 && (double) l6.get(1) == 120.0;
          assert (double) l7.get(0) == 130.0 && (double) l7.get(1) == 140.0;

          ((FWDDataObjectType) rtt1).setForceNormalizedFields(false);

          rtt1.set(3, -1.0);
          rtt1.set(5, 1.0);
          rtt1.set(6, 2.0);
          rtt1.set(7, 3.0);
          rtt1.set(8, 4.0);
          assert (double) rtt1.get(3) == -1;
          assert (double) rtt1.get(5) == 1;
          assert (double) rtt1.get(6) == 2;
          assert (double) rtt1.get(7) == 3;
          assert (double) rtt1.get(8) == 4;
          assert rtt1.isSet(3);
          assert rtt1.isSet(5);
          assert rtt1.isSet(6);
          assert rtt1.isSet(7);
          assert rtt1.isSet(8);
          rtt1.unset(3);
          rtt1.unset(5);
          rtt1.unset(6);
          rtt1.unset(7);
          rtt1.unset(8);
          assert !rtt1.isSet(3);
          assert !rtt1.isSet(5);
          assert !rtt1.isSet(6);
          assert !rtt1.isSet(7);
          assert !rtt1.isSet(8);

          rtt1.setDouble(3, -2.0);
          rtt1.setDouble(5, 10.0);
          rtt1.setDouble(6, 20.0);
          rtt1.setDouble(7, 30.0);
          rtt1.setDouble(8, 40.0);
          assert (double) rtt1.getDouble(3) == -2;
          assert (double) rtt1.getDouble(5) == 10;
          assert (double) rtt1.getDouble(6) == 20;
          assert (double) rtt1.getDouble(7) == 30;
          assert (double) rtt1.getDouble(8) == 40;

          rtt1.set("f4", -3.0);
          rtt1.set("f61", 11.0);
          rtt1.set("f62", 12.0);
          rtt1.set("f71", 13.0);
          rtt1.set("f72", 14.0);
          assert (double) rtt1.get("f4") == -3;
          assert (double) rtt1.get("f61") == 11;
          assert (double) rtt1.get("f62") == 12;
          assert (double) rtt1.get("f71") == 13;
          assert (double) rtt1.get("f72") == 14;
          assert rtt1.isSet("f4");
          assert rtt1.isSet("f61");
          assert rtt1.isSet("f62");
          assert rtt1.isSet("f71");
          assert rtt1.isSet("f72");
          rtt1.unset("f4");
          rtt1.unset("f61");
          rtt1.unset("f62");
          rtt1.unset("f71");
          rtt1.unset("f72");
          assert !rtt1.isSet("f4");
          assert !rtt1.isSet("f61");
          assert !rtt1.isSet("f62");
          assert !rtt1.isSet("f71");
          assert !rtt1.isSet("f72");

          rtt1.setDouble("f4", -4.0);
          rtt1.setDouble("f61", 110.0);
          rtt1.setDouble("f62", 120.0);
          rtt1.setDouble("f71", 130.0);
          rtt1.setDouble("f72", 140.0);
          assert (double) rtt1.getDouble("f4") == -4;
          assert (double) rtt1.getDouble("f61") == 110;
          assert (double) rtt1.getDouble("f62") == 120;
          assert (double) rtt1.getDouble("f71") == 130;
          assert (double) rtt1.getDouble("f72") == 140;

          rtt1.setInt("F1", 1234);
          assert rtt1.getInt("F1") == 1234;

          rtt1.setString("f2", "abcd");
          assert rtt1.getString("f2").equals("abcd");

          rtt1.setBytes("f3", "890890".getBytes());

          assert Arrays.equals(rtt1.getBytes(rtt1.getInstanceProperty("f3")), "890890".getBytes());

          rtt1.setBigDecimal("f4", new BigDecimal("124124512512512515.125125125"));
          rtt1.setBoolean("f5", true);

          rtt1.set(5, 1.0);
          rtt1.set(6, 2.0);

          assert rtt1.getDouble(5) == 1.0;
          assert rtt1.getDouble(6) == 2.0;

          rtt1.unset(5);
          assert !rtt1.isSet(5);
          assert rtt1.isSet(6);

          rtt1.set("__fwd_denormalized__f61", 4.0);
          assert rtt1.getDouble("__FWD_denormalized__F61") == 4.0;

          rtt1.set("__fwd_denormalized__f71", 6.0);
          rtt1.set("__fWD_denormalized__f72", 7.0);
          assert rtt1.getDouble("f71") == 6.0;
          assert rtt1.getDouble("F72") == 7.0;
          addDataObject(graph, rtt1);

          DataObject rtt2 = createDataObject(graph, "tt2");
          rtt2.setInt("F1", 1234);
          rtt2.setList("f2", Arrays.asList(1.0, 3.0));
          rtt2.set("f4", Arrays.asList(1.0, 10.0, 100.0));

          assert rtt2.getInt("f1") == 1234;
          assert rtt2.getInt(0) == 1234;
          assert rtt2.getList(1).equals(Arrays.asList(1.0, 3.0));
          assert rtt2.getList("f2").equals(rtt2.getList(1));
          assert rtt2.getList("F2").equals(rtt2.getList("f2"));
          assert rtt2.getList("F4").equals(Arrays.asList(1.0, 10.0, 100.0));
          assert rtt2.getList(3).equals(rtt2.getList("F4"));
          assert rtt2.getList(rtt2.getInstanceProperty("F4")).equals(rtt2.getList("F4"));

          assert rtt2.isSet("f4");
          rtt2.unset("f4");
          assert !rtt2.isSet("f4");

          addDataObject(graph, rtt2);
          System.out.println("Index for table tt1: " + getTableIndex(rtt1));
          System.out.println("Index for table tt2: " + getTableIndex(tableTt2));
          System.out.println("Index for table tt2: " + getTableIndex(graph, "tt2"));

          rtt1 = createDataObject(graph, "tt1");
          rtt1.setInt("f1", 1234);
          rtt1.setInt("F1", 1234);
          rtt1.setString("f2", "abcd");
          rtt1.setString("F2", "abcd");
          rtt1.setBytes("f3", "890890".getBytes());
          rtt1.setBytes("F3", "890890".getBytes());
          rtt1.setBigDecimal("f4", new BigDecimal("124124512512512515.125125125"));
          rtt1.setBigDecimal("F4", new BigDecimal("124124512512512515.125125125"));
          rtt1.setBoolean("f5", true);
          rtt1.setBoolean("F5", true);

          addDataObject(graph, rtt1);

          List<DataObject> rows1 = getDataObjects(graph, "tt1");
          Iterator<DataObject> iter1 = rows1.iterator();
          while (iter1.hasNext())
          {
             DataObject row = iter1.next();
             List f6 = normalizedExtentForTt1 ? (List) row.get("F6") : null;
             if (!normalizedExtentForTt1)
             {
                row.unset("F62");
                //row.unset("f62");
                System.out.println(row.isSet("F61") + " " + row.isSet("f62"));
             }

             System.out.println(getTableName(row));
             System.out.println(((FWDDataObjectType) row).primaryKey() + " " +
                                   row.getInt("f1") + " " +
                                   row.getString("f2") + " " +
                                   new String(row.getBytes("f3")) + " " +
                                   row.getBigDecimal("f4") + " " +
                                   row.getBoolean("f5") + " " +
                                   (normalizedExtentForTt1 ? row.getList("f6").get(0) + " " + f6.get(1)
                                      : row.getDouble("F61") + " " + row.getDouble("f62")));

             int fieldCount = getFieldCount(row);
             for (int i = 0; i < fieldCount; i++)
             {
                System.out.print(getFieldName(row, i) + " = " + row.get(i) + ";");
             }

             System.out.println();
          }

          List<DataObject> rows2 = getDataObjects(graph, 1);
          Iterator<DataObject> iter2 = rows2.iterator();
          while (iter2.hasNext())
          {
             DataObject row = iter2.next();
             System.out.println(getTableName(row));
             System.out.println(((FWDDataObjectType) row).primaryKey() + " " + row.getInt("F1") + " " + row.getDate("f3"));
          }

          List<DataObject> rrows1 = getChildRows(rtt1, "r1");
          for (DataObject row : rrows1)
          {
             System.out.println(getTableName(row));
             System.out.println(((FWDDataObjectType) row).primaryKey() + " " + row.getInt("f1") + " " + row.getDate("f3"));
          }

          // test the change summary/logging
          beginLogging(graph);
          // add
          DataObject rtt3 = createDataObject(graph, "tt2");
          rtt3.setInt("f1", 991234);
          rtt3.setDate("F3", new Date());
          addDataObject(graph, rtt3);
          // delete
          rtt2 = getDataObjects(tableTt2).get(0);
          rtt2.delete();
          // update
          rtt1 = getDataObjects(tableTt1).get(0);
          rtt1.setInt("f1", 77777);
          ChangeSummary summary = graph.getChangeSummary();
          System.out.println(summary.isCreated(rtt3));
          System.out.println(summary.isDeleted(rtt2));
          System.out.println(summary.isModified(rtt1));
          endLogging(graph);
          System.out.println(summary.isCreated(rtt3));
          System.out.println(summary.isDeleted(rtt2));
          System.out.println(summary.isModified(rtt1));

          List<DataObject> changes = summary.getChangedDataObjects();
          for (DataObject d : changes)
          {
             System.out.println(d.get("f1") + " " +
                                   summary.isCreated(d) + " " +
                                   summary.isDeleted(d) + " " +
                                   summary.isModified(d));
          }
          System.out.println(changes);

          rows2 = getDataObjects(graph, 1);
          iter2 = rows2.iterator();
          while (iter2.hasNext())
          {
             DataObject row = iter2.next();
             System.out.println(getTableName(row));
             System.out.println(((FWDDataObjectType) row).primaryKey() + " " +
                                   row.getInt("f1") + " " + row.getDate("f3"));
          }

          rows2 = getBeforeObjects(graph, "tt1");
          iter2 = rows2.iterator();
          while (iter2.hasNext())
          {
             DataObject row = iter2.next();
             System.out.println(getTableName(row));
             System.out.println(((FWDDataObjectType) row).primaryKey() + " " + row.getInt("f1"));
          }

          rows2 = getBeforeObjects(graph, "tt2");
          iter2 = rows2.iterator();
          while (iter2.hasNext())
          {
             DataObject row = iter2.next();
             System.out.println(getTableName(row));
             System.out.println(((FWDDataObjectType) row).primaryKey() + " " +
                                   row.getInt("f1") + " " + row.getDate("f3"));
          }
       }
    }