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