package nxm.sys.test.junit.lib;

import static org.junit.Assert.assertNotEquals;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.DoubleAccumulator;
import java.util.concurrent.atomic.DoubleAdder;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.concurrent.atomic.LongAdder;

import org.junit.After;
import org.junit.Before;

import nxm.sys.inc.AsciiMap;
import nxm.sys.inc.Keyable;
import nxm.sys.lib.Data;
import nxm.sys.lib.KeyVector;
import nxm.sys.lib.Midas;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.Shell;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.Table;
import nxm.sys.lib.TextFile;
import nxm.sys.lib.Time;
import nxm.sys.test.junit.NMTestCase;

/** This will test the Table class.

    @author  J. Czechowski IV
    @since   NeXtMidas 2.1.0
 */
public final class TableTest extends NMTestCase {
  /** public constructor to allow running this test case directly with JUnit. */
  public TableTest (String methodName) { this(methodName, null); }

  /** Creates a new instance of the class.
      @param methodName The name of the method being called.
      @param midas      The midas context to use when running the tests.
   */
  public TableTest (String methodName, Midas midas) {
    super(methodName, midas);
  }

  /** Save the default Table flags in order to be able to restore them
   *  at the end of this test, so follow-on tests do not fail even if
   *  one of these tests fails
   *  @since 3.8.0 for NXM-3120
   */
  @Before
  public void saveDefaultTableflags () {
    origTableFlags = Table.getDefaultFlagsInt();
  }

  /** Restore the default Table flags , so follow-on tests do not fail even if
   *  one of these tests fails
   *  @since 3.8.0 for NXM-3120
   */
  @After
  public void restoreDefaultTableflags () {
    Table.setDefaultFlags(origTableFlags);
  }

  private int origTableFlags;

  private static final int NONE   = 0; // Table ordering: unordered
  private static final int INSERT = 1; // Table ordering: insertion order
  private static final int SORT   = 2; // Table ordering: sorted (alphabetical)

  private static final TableMode[] MODES
    = { new TableMode(Table.FB,   "FromBytes",                 "FB",   Table.FB,  false, NONE  ),
        new TableMode(Table.KV,   "KeyVector",                 "KV",   Table.KV,  false, INSERT),
        new TableMode(Table.HT,   "Hashtable",                 "HT",   Table.MAP, false, NONE  ),
        new TableMode(Table.HM,   "HashMap",                   "HM",   Table.MAP, false, NONE  ),
        new TableMode(Table.LHM,  "LinkedHashMap",             "LHM",  Table.MAP, false, INSERT),
        new TableMode(Table.SLHM, "SynchronizedLinkedHashMap", "SLHM", Table.MAP, false, INSERT),
        new TableMode(Table.TM,   "TreeMap",                   "TM",   Table.MAP, false, SORT  ),
        new TableMode(Table.STM,  "SynchronizedTreeMap",       "STM",  Table.MAP, false, SORT  ),
        new TableMode(Table.MAP,  "Map",                       "MAP",  Table.MAP, true,  NONE  ),
        new TableMode(Table.KEY,  "Keyable",                   "KEY",  Table.KEY, true,  NONE  ),
        new TableMode(Table.CHM,  "ConcurrentHashMap",         "CHM",  Table.CHM, true,  NONE  ),
      };

  private static final int[] MODES_KEEPING_INSERTION_ORDER= { Table.KV, Table.LHM, Table.SLHM };

  private static final Integer       ZERO               = Integer.valueOf(0);
  private static final Integer       ONE                = Integer.valueOf(1);
  private static final Integer       TWO                = Integer.valueOf(2);
  private static final Integer       THREE              = Integer.valueOf(3);
  private static final Integer       FOUR               = Integer.valueOf(4);
  //Add values that are not preloaded by the integer class. The range of preloaded is -128 though 127
  private static final Integer       FOUR_HUNDRED       = Integer.valueOf(400);
  private static final Integer       FOUR_HUNDRED_ONE   = Integer.valueOf(401);
  private static final Integer       FOUR_HUNDRED_TWO   = Integer.valueOf(402);
  private static final Integer       FOUR_HUNDRED_THREE = Integer.valueOf(403);
  private static final Integer       FOUR_HUNDRED_FOUR  = Integer.valueOf(404);
  private static final Byte          BYTE               = Byte.valueOf(Byte.MAX_VALUE);
  private static final Short         SHORT              = Short.valueOf(Short.MIN_VALUE);
  private static final Integer       INTEGER            = Integer.valueOf(Integer.MAX_VALUE);
  private static final Long          LONG               = Long.valueOf(Long.MIN_VALUE);
  private static final Float         FLOAT              = Float.valueOf(Float.MAX_VALUE);
  private static final Double        DOUBLE             = Double.valueOf(Double.MIN_VALUE);
  private static final BigInteger    BIGINTEGER         = new BigInteger(Long.MAX_VALUE+"999");
  private static final BigDecimal    BIGDECIMAL         = new BigDecimal("10.1234567890123456789");
  private static final AtomicInteger ATOMICINTEGER      = new AtomicInteger(Integer.MIN_VALUE);
  private static final AtomicLong    ATOMICLONG         = new AtomicLong(Long.MAX_VALUE);
  private static final Data          DATA_X             = new Data(21L);
  private static final Data          DATA_L             = new Data(21);
  private static final Data          DATA_I             = new Data((short)21);
  private static final Data          DATA_B             = new Data((byte)21);
  private static final Data          DATA_D             = new Data(21.0);
  private static final Data          DATA_F             = new Data(21.0F);
  private static final Data          DATA_S             = new Data("TableTest");


  /* format is array of [ <String:key1>,<Object:value1>, <key2>,<value2>, ... <keyN>,<valueN>] */
  private static final Object[] data_for_sample_table = {
    "ZERO",          ZERO,
    "ONE" ,          ONE,
    "TWO",           TWO,
    "THREE",         THREE,
    "FOUR",          FOUR,
    "BYTE",          BYTE,
    "SHORT",         SHORT,
    "INTEGER",       INTEGER,
    "LONG",          LONG,
    "FLOAT",         FLOAT,
    "DOUBLE",        DOUBLE,
    "BIGINTEGER",    BIGINTEGER,
    "BIGDECIMAL",    BIGDECIMAL,
    "ATOMICINTEGER", ATOMICINTEGER,
    "ATOMICLONG",    ATOMICLONG,
    "DATA_X",        DATA_X,
    "DATA_L",        DATA_L,
    "DATA_I",        DATA_I,
    "DATA_B",        DATA_B,
    "DATA_D",        DATA_D,
    "DATA_F",        DATA_F,
  };

  /* format is array of [ <String:key1>,<Object:value1>, <key2>,<value2>, ... <keyN>,<valueN>] */
  private static final Object[] data_for_java_number_table = {
    "ZERO",          ZERO,
    "ONE" ,          ONE,
    "TWO",           TWO,
    "THREE",         THREE,
    "FOUR",          FOUR,
    "BYTE",          BYTE,
    "SHORT",         SHORT,
    "INTEGER",       INTEGER,
    "LONG",          LONG,
    "FLOAT",         FLOAT,
    "DOUBLE",       DOUBLE,
  };

  private static int size_sample_table;
  private static String[] keys_for_sample_table;
  private static String[] keys_for_java_number_table;
  private boolean haveListener = false;
  private String currentOptions = getMidas().io.getOptions();

  /**
   * Creates a sample table to use.
   * @param tm The mode of the table
   * @return A sample table to use
   */
  private Table makeSampleTable(TableMode tm) {
    return makeSampleTable(tm.modeNum, 0);
  }

  /** Creates a sample table to use.
      @return A sample table to use.
   */
  private Table makeSampleTable(int mode, int flags) {
    Table table = new Table(mode, flags);

    assert((data_for_sample_table.length%2) == 0); // make sure we have even number of items in sample data
    size_sample_table = data_for_sample_table.length/2;
    keys_for_sample_table = new String[size_sample_table];
    for (int ii=0, jj=0; ii < data_for_sample_table.length-1; ii+=2, jj++) {
      String key = (String) data_for_sample_table[ii];
      Object val = data_for_sample_table[ii+1];
      table.put(key, val);
      keys_for_sample_table[jj] = key;
    }
    return table;
  }

  /**
   * Creates a sample table to use.
   * @param tm The mode of the table
   * @return A sample table to use
   */
  private Table makeJavaNumberTable(TableMode tm) {
    return makeJavaNumberTable(tm.modeNum, 0);
  }

  /** Creates a sample table that has Java Numbers to use.
      @return A sample table to use.
   */
  private Table makeJavaNumberTable(int mode, int flags) {
    Table table = new Table(mode, flags);
    table.setFlags("+KeepJavaNumericType");

    assert((data_for_java_number_table.length%2) == 0); // make sure we have even number of items in sample data
    size_sample_table = data_for_java_number_table.length/2;
    keys_for_java_number_table = new String[size_sample_table];
    for (int ii=0, jj=0; ii < data_for_java_number_table.length-1; ii+=2, jj++) {
      String key = (String) data_for_java_number_table[ii];
      Object val = data_for_java_number_table[ii+1];
      table.put(key, val);
      keys_for_java_number_table[jj] = key;
    }
    return table;
  }

  /** Creates a ReadOnly Table with KEY="TESTKEY",VALUE="TESTVALUE" */
  private Table makeReadOnlyTable() {
    Table table = new Table();
    String key = "TESTKEY";
    String value = "TESTVALUE";
    table.put(key, value);

    table.setFlags(Table.READONLY);
    return table;
  }

  private void insertSampleTestData(Table tab, Hashtable<String,Object> ht) {
    // matches Table created in makeSampleTable()
    for (int ii = 0; ii < data_for_sample_table.length-1; ii+=2) {
      String key = (String) data_for_sample_table[ii];
      Object val = data_for_sample_table[ii+1];
      tab.put(key, val);
      ht.put(key, val);
    }
    final Integer fourZeroFour = Integer.valueOf(404);
    tab.put("FOUR_HUNDRED_FOUR",  fourZeroFour);
    ht.put("FOUR_HUNDRED_FOUR",  fourZeroFour);
  }

  /** Creates a really simple sample table to use.
   *   @return A sample table to use.
   */
  private Table makeSimpleSampleTable (TableMode tm, int flags) {
    Table table = new Table(tm.modeNum, flags);

    table.put("ZERO",  ZERO);
    table.put("ONE",   ONE);
    table.put("TWO",   TWO);
    table.put("THREE", THREE);
    table.put("FOUR",  FOUR);
    return table;
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // TEST METHODS (in alphabetical order)
  //////////////////////////////////////////////////////////////////////////////////////////////////

  /** Test both variants of the Table.addIfNotPresent(..) APIs added in NeXtMidas 3.1.0.
      @see Table#addIfNotPresent(String, Object)
      @see Table#addIfNotPresent(Map)
      @since NeXtMidas 3.1.0
   */
  public void testAddIfNotPresent () {
    final String EXISTING_KEY = "ZERO";      // existing KEY in table
    final Object VAL_AT_EXISTING_KEY = ZERO; // value at EXISTING_KEY in table
    final String NONEXISTENT_KEY = "TESTKEY_BUG_2518"; // non-existent KEY in table
    final Object NEW_VALUE = "the Value for BUG 2518"; // value to add at NONEXISTENT_KEY
    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);

      boolean result1 = table.addIfNotPresent(EXISTING_KEY, NEW_VALUE);
      assertFalse(mode+": add to existing key/value result should be false", result1);
      assertEquals(mode+": value at existing key should NOT have changed!", VAL_AT_EXISTING_KEY, table.getKey(EXISTING_KEY));

      boolean result2 = table.addIfNotPresent(NONEXISTENT_KEY, NEW_VALUE);
      assertTrue(mode+": add to non-existing key/value result", result2);
      assertEquals(mode+": value at new key", NEW_VALUE, table.getKey(NONEXISTENT_KEY));
    }
  }

  /**
   * Test the ability to create a table.
   * @see Table#addTable(String)
   */
  public void testAddTable () {
    final String EXISTING_KEY = "ZERO";      // existing KEY in table
    final Object VAL_AT_EXISTING_KEY = ZERO; // value at EXISTING_KEY in table
    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);

      Object val = table.addTable("MYTABLE");

      assertTrue(mode+": return value of addTable is a Table", val instanceof Table);
      assertTrue(mode+": getKey('MYTABLE') is a Table", (table.getKey("MYTABLE") instanceof Table));
      assertEquals(mode+": getKey('MYTABLE') has size 0", 0, ((Table)table.getKey("MYTABLE")).size());

      Object valAtKey = table.addTable(EXISTING_KEY);
      assertEquals(mode+": addTable(existingKey) == existing value entry", VAL_AT_EXISTING_KEY, valAtKey);
      assertEquals(mode+": getKey(existingKey) has NOT changed after addTable(existingKey)", VAL_AT_EXISTING_KEY, table.getKey(EXISTING_KEY));
    }
  }

  /**
   * Test appending values/overwriting values to an already existing table.
   * @see Table#append(String)
   */
  public void testAppend() {
    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);
      int expSize = table.getSize() + 2; // below append adds two entries and overrides "ONE"
      table.append("{A=\"A\",B=\"B\",ONE=\"one\"}");

      assertEquals(""+mode+": Total entries",  expSize,  table.getSize());
      assertEquals(""+mode+": getKey('ZERO')",    ZERO,  table.getKey("ZERO"));
      assertEquals(""+mode+": getKey('ONE')",     "one", table.getKey("ONE"));
      assertEquals(""+mode+": getKey('TWO')",     TWO,   table.getKey("TWO"));
      assertEquals(""+mode+": getKey('THREE')",   THREE, table.getKey("THREE"));
      assertEquals(""+mode+": getKey('FOUR')",    FOUR,  table.getKey("FOUR"));
      assertEquals(""+mode+": getKey('A')",       "A",   table.getKey("A"));
      assertEquals(""+mode+": getKey('B')",       "B",   table.getKey("B"));
    }
  }

  /**
   * Tests the ability to clear a table
   * @see Table#clear()
   */
  public void testClear() {
    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);

      table.clear();

      assertEquals(""+mode+": Table has size 0",  0, table.size());
      assertEquals(""+mode+": Table has no keys", 0, table.getKeys().length);
    }
  }

  /**
   * Test the ability to check if a key is in a table.
   * @see Table#containsKey(Object)
   * @see Table#containsKey(String)
   */
  public void testContainsKey() {
    StringBuffer  one = new StringBuffer("ONE");
    StringBuilder two = new StringBuilder("TWO");
    StringBuilder ten = new StringBuilder("TEN");

    Table subTableA = new Table(Table.HT); // for testing BUG 2334
    subTableA.fromBytes("{B=23,C={D=32,E=13}}".getBytes());

    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);

      /* Use containsKey(String) */
      assertTrue(mode+": containsKey('ZERO')",  table.containsKey("ZERO"));
      assertTrue(mode+": containsKey('ONE')",   table.containsKey("ONE"));
      assertTrue(mode+": containsKey('TWO')",   table.containsKey("TWO"));
      assertTrue(mode+": containsKey('THREE')", table.containsKey("THREE"));
      assertTrue(mode+": containsKey('FOUR')",  table.containsKey("FOUR"));
      assertFalse(mode+": containsKey('FIVE')", table.containsKey("FIVE"));
      assertFalse(mode+": containsKey('SIX')",  table.containsKey("SIX"));

      /* Use containsKey(Object) which should convert the objects to a string. */
      assertTrue(mode+": containsKey(one)",     table.containsKey(one));
      assertTrue(mode+": containsKey(two)",     table.containsKey(two));
      assertFalse(mode+": containsKey('SIX')",  table.containsKey(ten));

      // test cases for BUG 2334 - to drill down to sub-Tables (but NOT value's properties/methods)
      table.put("A", subTableA);
      assertTrue (mode+": containsKey(A)",            table.containsKey("A"));
      assertTrue (mode+": containsKey(A.B)",          table.containsKey("A.B"));
      assertFalse(mode+": containsKey(A.B.C)",        table.containsKey("A.B.C"));
      assertFalse(mode+": containsKey(A.B.getValue)", table.containsKey("A.B.getValue"));
      assertTrue (mode+": containsKey(A.C)",          table.containsKey("A.C"));
      assertTrue (mode+": containsKey(A.C.D)",        table.containsKey("A.C.D"));
      assertFalse(mode+": containsKey(A.C.GETSIZE)",  table.containsKey("A.C.GETSIZE"));
    }
  }

  /** test old behavior containsKey(String)  which is now in containsKeyx(String) method
      which DOES NOT support the dot syntax to drill down into sub-Tables.
      @since NeXtMidas 2.9.2
   */
  public void testContainsKeyx() {
    Table subTableA = new Table(Table.HT);
    subTableA.fromBytes("{B=23,C={D=32,E=13}}".getBytes());

    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);

      /* Use containsKey(String) */
      assertTrue(mode+": containsKeyx('ZERO')",  table.containsKeyx("ZERO"));
      assertTrue(mode+": containsKeyx('ONE')",   table.containsKeyx("ONE"));
      assertTrue(mode+": containsKeyx('TWO')",   table.containsKeyx("TWO"));
      assertTrue(mode+": containsKeyx('THREE')", table.containsKeyx("THREE"));
      assertTrue(mode+": containsKeyx('FOUR')",  table.containsKeyx("FOUR"));
      assertFalse(mode+": containsKeyx('FIVE')", table.containsKeyx("FIVE"));
      assertFalse(mode+": containsKeyx('SIX')",  table.containsKeyx("SIX"));

      // test cases for BUG 2334 - to drill down to sub-Tables (but NOT value's properties/methods)
      table.put("A", subTableA);
      assertTrue (mode+": containsKeyx(A)",            table.containsKeyx("A"));
      assertFalse(mode+": containsKeyx(A.B)",          table.containsKeyx("A.B"));
      assertFalse(mode+": containsKeyx(A.B.C)",        table.containsKeyx("A.B.C"));
      assertFalse(mode+": containsKeyx(A.B.getValue)", table.containsKeyx("A.B.getValue"));
      assertFalse(mode+": containsKeyx(A.C)",          table.containsKeyx("A.C"));
      assertFalse(mode+": containsKeyx(A.C.D)",        table.containsKeyx("A.C.D"));
      assertFalse(mode+": containsKeyx(A.C.GETSIZE)",  table.containsKeyx("A.C.GETSIZE"));
    }
  }

  /**
   * Test that values are stored in the table.
   */
  public void testContainsValue() {
    // Need to use large values. The Integer class precaches all integers between -128 and 127.
    // By using valued that don't fall into that range, the objects are created rather than pointed to previously
    // created objects. When using == on an object, it will fail because the reference is different.
    // Using .equals() will pass because it is by value.
    Integer fourZeroOne  = Integer.valueOf(401);
    Integer fourZeroTwo  = Integer.valueOf(402);
    Integer fourZeroSix  = Integer.valueOf(406);

    for (TableMode mode : MODES) {
      Table   table = makeSampleTable(mode);
      //This is a fix for the class constructors being deprecated in java 9. Making the test values larger than 127 will make them create new objects
      table.put("FOUR_HUNDRED",  FOUR_HUNDRED);
      table.put("FOUR_HUNDRED_ONE",  FOUR_HUNDRED_ONE);
      table.put("FOUR_HUNDRED_TWO",  FOUR_HUNDRED_TWO);
      table.put("FOUR_HUNDRED_THREE",  FOUR_HUNDRED_THREE);
      table.put("FOUR_HUNDRED_FOUR",  FOUR_HUNDRED_FOUR);

      boolean ident = table.isMode(Table.KV); // value check uses "==" comparison rather than .equals(..)

      assertTrue(""+mode+": containsValue(FOUR_HUNDRED)",       table.containsValue(FOUR_HUNDRED));
      assertTrue(""+mode+": containsValue(FOUR_HUNDRED_ONE)",   table.containsValue(FOUR_HUNDRED_ONE));
      assertTrue(""+mode+": containsValue(FOUR_HUNDRED_TWO)",   table.containsValue(FOUR_HUNDRED_TWO));
      assertTrue(""+mode+": containsValue(FOUR_HUNDRED_THREE)", table.containsValue(FOUR_HUNDRED_THREE));
      assertTrue(""+mode+": containsValue(FOUR_HUNDRED_FOUR)",  table.containsValue(FOUR_HUNDRED_FOUR));
      assertEquals(""+mode+": containsValue(fourZeroOne)",      !ident, table.containsValue(fourZeroOne));
      assertEquals(""+mode+": containsValue(fourZeroTwo)",      !ident, table.containsValue(fourZeroTwo));
      assertFalse(""+mode+": containsValue(fourZeroSix)",       table.containsValue(fourZeroSix));

      table.put("FOUR_HUNDRED_ONE",  fourZeroOne);
      table.put("FOUR_HUNDRED_TWO",  fourZeroTwo);
      table.put("FOURZEROSIX",  fourZeroSix);

      assertTrue(""+mode+": containsValue(FOUR_HUNDRED)",       table.containsValue(FOUR_HUNDRED));
      assertEquals(""+mode+": containsValue(FOUR_HUNDRED_ONE)", !ident, table.containsValue(FOUR_HUNDRED_ONE));
      assertEquals(""+mode+": containsValue(FOUR_ZERO_TWO)",    !ident, table.containsValue(FOUR_HUNDRED_TWO));
      assertTrue(""+mode+": containsValue(FOUR_HUNDRED_TWO)",   table.containsValue(FOUR_HUNDRED_THREE));
      assertTrue(""+mode+": containsValue(FOUR_HUNDRED_FOUR)",  table.containsValue(FOUR_HUNDRED_FOUR));
      assertTrue(""+mode+": containsValue(fourZeroOne)",        table.containsValue(fourZeroOne));
      assertTrue(""+mode+": containsValue(fourZeroTwo)",        table.containsValue(fourZeroTwo));
      assertTrue(""+mode+": containsValue(fourZeroSix)",        table.containsValue(fourZeroSix));
    }
  }

  /**
   * Test copying a table.
   * @see Table#copy()
   */
  public void testCopy () {
    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);
      Table copy  = table.copy();

      assertNotSame("copy() with "+mode+" (not same)", table, copy);
      assertEquals("copy() with "+mode+" (equals)", table, copy);
    }
  }

  /**
   * Test the setting of default flags
   * @since NeXtMidas 3.7.0
   */
  public void testSetDefaultFlags() {
    int prevflags = Table.getDefaultFlagsInt();

    // Test setting the default flags to one flag at a time
    String flist = Table.flagsList;
    String[] tokens = flist.split(",");
    for (String t : tokens){
      Table.setDefaultFlags(t);  // adds "|" around strings
      assertEquals("The default flag is set: ", "|"+t+"|", Table.getDefaultFlags());
    }

    // Test setting the default flags to contradictory PERMIT_NULL and EXCEPTION_ON_NULL
    // (since 3.7.3 for NXM-3122)
    try {
      Table.setDefaultFlags("ExceptionOnNull|PermitNull");
      fail("Table.setDefaultFlags should not allow contradictory null related flags");
    }
    catch (IllegalArgumentException iae) {
      // pass
    }

    // Restore default flags
    Table.setDefaultFlags(prevflags);
  }

  /**
   * Test setting default mode of Table.
   * @since NeXtMidas 3.7.0
   */
  public void testSetDefaultMode() {
    String prevmode = Table.getDefaultMode();
    for (TableMode mode : MODES) {
      if (mode.modeNum == Table.FB) {
        assertNotSame("The Default String cannot be FB. ", mode.modeNum, Table.getDefaultModeInt());
      }
      else {
        Table.setDefaultMode(mode.modeStr);
        assertEquals("The Default String is set. ", mode.modeNum, Table.getDefaultModeInt());
      }
    }
    Table.setDefaultMode(prevmode);
  }

  //public void testDump() { } // no applicable test case

  public void testEmptyTabl() {
    Table tab = new Table();
    assertTrue("String Form of Empty Table", tab.toString().equals("{}"));
  }

  public void testEntrySet() {
    for (TableMode mode : MODES) {
      Table table = makeSimpleSampleTable(mode,0);
      String[] keys;   // The expected keys
      Object[] values; // The expected values

      if (mode.ordered == SORT) {
        keys   = new String[] { "FOUR", "ONE", "THREE", "TWO", "ZERO" };
        values = new Object[] {  FOUR,   ONE,   THREE,   TWO,   ZERO  };
      } else {
        values = new Object[] {  ZERO,   ONE,   TWO,   THREE,   FOUR  };
        keys   = new String[] { "ZERO", "ONE", "TWO", "THREE", "FOUR" };
      }

      if (mode.ordered == SORT) Arrays.sort(keys);
      checkArrays(mode, true, keys, values, table.entrySet());

      // Test modifications:
      // Remove "ONE"
      // Change "TWO" to "two"
      java.util.Set<Map.Entry<String,Object>> set  = table.entrySet();
      java.util.Iterator<Map.Entry<String,Object>> iter = set.iterator();

      Map.Entry<String,Object> one = null;
      Map.Entry<String,Object> two = null;

      while (iter.hasNext()) {
        Map.Entry<String,Object> entry = iter.next();
        Object    key   = entry.getKey();

        if (key.equals("ONE")) { one = entry; }
        if (key.equals("TWO")) { two = entry; }
      }

      assertNotNull("Found 'ONE'", one);
      assertNotNull("Found 'TWO'", two);

      set.remove(one);
      assertFalse("'ONE' was removed from Table", table.containsKey("ONE"));

      two.setValue("two");
      assertEquals("'TWO' was changed to 'two'", "two", table.get("TWO"));
    }
  }

  /**
   * Test that tables are equal.
   * @see Table#equals(Object)
   */
  public void testEquals() {
    Integer fourZeroFour = Integer.valueOf(404);

    for (TableMode mode : MODES) {
      Table t0 = makeSampleTable(mode);
      Table t1 = t0;
      Table t2 = makeSampleTable(mode);
      Table t3 = makeSampleTable(mode);
      Table t4 = new Table();
      Table t5 = makeSampleTable(mode);

      Hashtable<String,Object> ht = new Hashtable<>();
      insertSampleTestData(t4, ht);


      t3.put("FOUR_HUNDRED_FOUR",  fourZeroFour);
      t5.put("FOUR_HUNDRED_FOUR", Integer.valueOf(404));

      assertTrue(""+mode+": t0.equals(t1)", t0.equals(t0));
      assertTrue(""+mode+": t0.equals(t1)", t0.equals(t1));
      assertTrue(""+mode+": t0.equals(t2)", t0.equals(t2));
      assertTrue(""+mode+": t1.equals(t2)", t1.equals(t2));
      assertTrue(""+mode+": t2.equals(t1)", t2.equals(t1));

      assertTrue(""+mode+": t3.equals(t4)", t3.equals(t4));
      assertTrue(""+mode+": t4.equals(t3)", t4.equals(t3));
      assertTrue(""+mode+": t3.equals(t5)", t3.equals(t5));
      assertTrue(""+mode+": t5.equals(t3)", t5.equals(t3));

      assertFalse(""+mode+": t2.equals(t3)", t2.equals(t3));
      assertFalse(""+mode+": t2.equals(t4)", t2.equals(t4));

      assertTrue(""+mode+": t4.equals(ht)", t4.equals(ht));
      assertTrue(""+mode+": ht.equals(t4)", ht.equals(t4));

      // BUG 1358 - test equality to null and non Table objects
      assertFalse(""+mode+": t1.equals(null)", t1.equals(null));
      assertFalse(""+mode+": t1.equals(babh)", t1.equals("babh"));
      assertFalse(""+mode+": t1.equals(ONE)", t1.equals("ONE"));
    }
  }

  /**
   * Test creating a table from bytes.
   * @see Table#fromBytes(byte[])
   * @see Table#Table(byte[])
   */
  public void testFromBytes() {
    Table tableFromBytes = new Table();
    Table tableFromByteJavaNum = new Table();
    tableFromByteJavaNum.setFlags("+KeepJavaNumericType");
    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);
      Table tableJN = makeJavaNumberTable(mode);

      // test toBytes vs Constructor
      byte[] tableAsBytes  = table.toBytes();  // this table has Java Objects (Numbers)
      Table tableConstructorFromBytes = new Table(tableAsBytes);
      byte[] tableAsBytes1 = tableConstructorFromBytes.toBytes();
      assertArrayEquals("toBytes(..) and Table(byte[]) byte[]", tableAsBytes, tableAsBytes1);

      // test table is handling new Java as Numbers correctly
      byte[] tableJNAsBytes  = tableJN.toBytes();
      tableFromByteJavaNum.fromBytes(tableJNAsBytes);
      assertEquals("fromBytes(..) and Table(byte[]) Table", tableJN, tableFromByteJavaNum); // 3.9.1 add test since we can serialize/deserialize Java Numbers

      // test fromBytes vs Constructor
      tableFromBytes.fromBytes(tableAsBytes);
      byte[] fromBytes = tableFromBytes.toBytes();
      Table tableConstructorFromBytes2 = new Table(fromBytes);
      byte[] tableAsBytes2 = tableConstructorFromBytes2.toBytes();
      assertEquals("fromBytes(..) and Table(byte[]) Table", tableFromBytes, tableConstructorFromBytes2);
      assertArrayEquals("toBytes(..) and fromBytes(byte[]) byte[]", fromBytes, tableAsBytes2);

      // test Constructor using byte[] and offset=0 (for Bug 1291)
      Table tableConstructorFromBytes3 = new Table(tableAsBytes2,0,fromBytes.length);
      byte[] tableAsBytes3 = tableConstructorFromBytes3.toBytes();
      assertEquals("toBytes(..) and Table(byte[],start=0,len) Table", tableConstructorFromBytes2, tableConstructorFromBytes3);
      assertArrayEquals("toBytes(..) and Table(byte[],start=0,len) byte[]", tableAsBytes2, tableAsBytes3);

      // test Constructor using byte[] and offset=2 (for Bug 1291)
      final int startpos   = 2;
      byte[] bytearray     = new byte[tableAsBytes2.length+startpos+3];
      System.arraycopy(tableAsBytes2, 0, bytearray, startpos, tableAsBytes2.length);
      Table tableConstructorFromBytes4 = new Table(bytearray,startpos,tableAsBytes2.length);
      byte[] tableAsBytes4 = tableConstructorFromBytes4.toBytes();
      assertEquals("toBytes(..) and Table(byte[],start="+startpos+",len) Table", tableConstructorFromBytes2, tableConstructorFromBytes4);
      assertArrayEquals("toBytes(..) and Table(byte[],start="+startpos+",len) byte[]", tableAsBytes2, tableAsBytes4);

    } // end for loop over MODES (TableMode[])

    // === BEGIN tests for CheckMalformed flag (Bug 85) ===
    tableFromBytes.setFlags("+CheckMalformed");
    checkBadTable(tableFromBytes, "no_open_brace}", "Malformed Table", "missing open");
    checkBadTable(tableFromBytes, "{no_close_brace", "Malformed Table", "missing close");
    checkBadTable(tableFromBytes, "no_open_or_close_braces", "Malformed Table", "missing open", "missing close");
    checkBadTable(tableFromBytes, "{A=\"missing_closing_quote}", "Malformed Table", "quote");
    try {
      byte[] malformedBytes = "XXX_{no_close_brace_YYY".getBytes();
      tableFromBytes.fromBytes(malformedBytes, 4, 19); // skip pass XXX_ and end before _YYY
      fail("{no_close_brace_ should cause Exception for Malformed table");
    } catch (MidasException e) {
      String emsg = e.getMessage();
      assertTrue("Exception contains: Malformed Table ", emsg.contains("Malformed Table"));
      assertTrue("Exception contains: missing close ",   emsg.contains("missing close"));
      assertFalse("Exception should not contain XXX_: " + emsg, emsg.contains("XXX_"));
      assertFalse("Exception should not contain _YYY: " + emsg, emsg.contains("_YYY"));
    }
    // == END tests for CheckMalformed flag (Bug 85) ===

    // === BEGIN Test for un-escaping of backslash escape sequence (Bug 2292) ===
    String tblFlags = "-DisableEscapeSequences|+CheckMalformed";
    checkTableFromString("1. backslash backslash esc-seq", "{A=\"bsbs\\\\\",KEY2=Val2}", tblFlags,
        "A","bsbs\\", "KEY2","Val2");    // bsbs\\  ==>  bsbs\
    checkTableFromString("2. backslash quote esc-seq", "{A=\"bsqu\\\"\",KEY2=\"Val2\"}", tblFlags,
        "A","bsqu\"", "KEY2","Val2");    // bsqu\"  ==>  bsqu"
    checkTableFromString("3. eq backslash backslash esc-seq", "{A=\"eqbsbs=\\\\\",KEY2=Val2}", tblFlags,
        "A","eqbsbs=\\", "KEY2","Val2"); // eqbsbs=\\  ==>  eqbsbs=\
    checkTableFromString("4. eq backslash quote esc-seq", "{A=\"eqbsqu=\\\"\",KEY2=\"Val2\"}", tblFlags,
        "A","eqbsqu=\"", "KEY2","Val2"); // eqbsqu=\"  ==>  eqbsqu="
    checkTableFromString("5. 4 backslashes esc-seq", "{A=\"4bs\\\\\\\\\",KEY2=Val2}", tblFlags,
        "A","4bs\\\\", "KEY2","Val2");   // 4bs\\\\  ==>  4bs\\
    checkTableFromString("6. 3 backslashes quote esc-seq", "{A=\"3bsqu\\\\\\\"\",KEY2=Val2}", tblFlags,
        "A","3bsqu\\\"", "KEY2","Val2"); // 3bsqu\\\"  ==>  3bsqu\"
    checkTableFromString("7. backslash u HHHH esc-seq", "{A=\"bsuHHHH\\u007E\",KEY2=Val2}", tblFlags,
        "A","bsuHHHH\u007E", "KEY2","Val2"); // bsuHHHH\u007F  ==>  bsuHHHH~
    checkTableFromString("8. backslash x HH esc-seq", "{A=\"bsxHH\\x7E\",KEY2=Val2}", tblFlags,
        "A","bsxHH\u007E", "KEY2","Val2");   // bsxHH\x7F  ==>  bsxHH~
    checkTableFromString("9. Windows dir", "{A=\"c:\\\\DOS\\\\path\\\\\",KEY2=Val2}", tblFlags,
        "A","c:\\DOS\\path\\", "KEY2","Val2"); // c:\\DOS\\path\\  ==>  c:\DOS\path\

    String testEscape  = "{A={B=\"Quotes\\\"__Tab\\t__Newline\\n__BSlash\\\\__UCode\\u0128__LBrace\t{RBrace}\"}}";
    int origFlags = tableFromBytes.getFlagsInt();
    tableFromBytes.setFlags("-DisableEscapeSequences");
    tableFromBytes.fromBytes(testEscape.getBytes());
    String converted   = tableFromBytes.toString();
    boolean hasQuotes  = converted.charAt(13)==AsciiMap.QUOTE;
    boolean hasTab     = converted.charAt(19)==AsciiMap.HTAB;
    boolean hasNewLine = converted.charAt(29)==AsciiMap.LF;
    boolean hasBSlash  = converted.charAt(38)==AsciiMap.BSLASH;
    boolean hasUCode   = converted.charAt(46)==0x128;
    //System.out.println("TableTest converted="+converted);
    assertEquals("Escaped quotes: ",hasQuotes, true);
    assertEquals("Escaped Tab: ",hasTab, true);
    assertEquals("Escaped NewLine: ",hasNewLine, true);
    assertEquals("Escaped BackSlash: ",hasBSlash, true);
    assertEquals("Escaped UniCode: ",hasUCode, true);

    // Test for adding the escape chars back in (Bug 2292)
    byte[] bytes = tableFromBytes.toBytes();
    char bs = AsciiMap.BSLASH;
    hasBSlash = bytes[13]==bs && bytes[20]==bs && bytes[31]==bs && bytes[41]==bs;
    //System.out.println("TableTest w/Escapes="+new String(bytes));
    assertEquals("BackSlashes put back in: ",hasBSlash, true);

    // Test for turning off the escapes (Bug 2292)
    String testNoEscapes = "{A=\"A simple tab \\t. \"}";
    tableFromBytes.setFlags("+DisableEscapeSequences");
    tableFromBytes.fromBytes(testNoEscapes.getBytes());
    String unconverted = tableFromBytes.toString();
    //System.out.println("Unconverted = "+unconverted);
    hasBSlash    = unconverted.charAt(17)==AsciiMap.BSLASH;
    boolean hasT = unconverted.charAt(18)=='t';
    assertEquals("Did not translate escape: ",(hasBSlash && hasT), true);
    tableFromBytes.setFlags(origFlags); // set back

    // test case for BUG 2095 - spaces before/after open/close braces
    String tblstr = "{A=1, B =  2  }"; // also test spaces between key/value pairs
    String spacesBeforeNAfter = "\t   "+tblstr+" ";
    Table expectedtbl = new Table(tblstr);
    tableFromBytes.fromBytes(spacesBeforeNAfter.getBytes());
    assertEquals("Table parsing - Spaces before/after {}", expectedtbl, tableFromBytes);

    _testTableSpaces();
  }

  /**
   *  Test for Bug 2949: intr.Table.loadOpalTable moved to lib.Table.fromOpalFile()
   *  New methods created fromOpalFile (MidasReference ref, Object filename, boolean keepCase)
   *  and fromOpalFile (String[] lines, boolean keepCase)
   *  @since NeXtMidas 3.7.0
   */
  public void testFromOpalFile () {
    // used sys.test.test_table.mm procedure test_load_opal  as an example: assert opalCS.1234 eq 3.14
    String filename = urlSysTest+"test_table_opal.tbl";
    Table testOpal = new Table();
    testOpal = Table.fromOpalFile(null,  filename , true);

    Data d314  = new Data(3.14);  // defaults to Double
    Data d420  = new Data(42.0);
    Data d123  = new Data(1.23);
    Data int1 = new Data(1);
    Data int2 = new Data(2);
    Data int3 = new Data(3);
    Data d10   = new Data(1.0);
    Data d20   = new Data(2.0);
    Data Tbyte = new Data((byte)100);
    Data Tshort = new Data((short)12);
    Data Tfloat = new Data(2.345f);
    Time Ttime = new Time("2015:01:23::22:57:35.956423789012");

    assertEquals("Table size is 14. ", 14, testOpal.getSize());

    assertEquals("testOpal.TYPE_B = 100 ", Tbyte, testOpal.getKey("TYPE_B"));
    assertEquals("testOpal.TYPE_I = 12 ", Tshort, testOpal.getKey("TYPE_I"));
    assertEquals("testOpal.TYPE_F = 2.345 ", Tfloat, testOpal.getKey("TYPE_F"));
    assertEquals("testOpal.TVAL = 2015:01:23::22:57:35.956423789012 ", Ttime, testOpal.getKey("TVAL"));
    assertEquals("testOpal.1234 = 3.14 ", d314, testOpal.getKey("1234"));
    assertEquals("testOpal.ABCD = 42.0 ", d420, testOpal.getKey("ABCD"));
    assertEquals("testOpal.321 = 1.23 ", d123, testOpal.getKey("321"));
    assertEquals("testOpal.abcd = testOpal.ABCD = 42.0 ", d420, testOpal.getKey("abcd"));
    assertEquals("testOpal.efgh = testOpal.ABCD = 42.0 ", d420, testOpal.getKey("efgh"));
    assertEquals("testOpal.A = 1 ", int1, testOpal.getKey("A"));
    assertEquals("testOpal.B = 2 ", int2, testOpal.getKey("B"));
    assertEquals("testOpal.C = 3 ", int3, testOpal.getKey("C"));
    assertEquals("testOpal.ZZZ = String with // in it, but not a comment ", "String with // in it, but not a comment", testOpal.getKey("ZZZ"));
    assertEquals("testOpal.SUB.FOO = 1.0 ", d10, testOpal.getTable("SUB").getKey("FOO"));
    assertEquals("testOpal.SUB.BAR = 2.0 ", d20, testOpal.getTable("SUB").getKey("BAR"));
  }

  /** utility method to check a bad table */
  private void checkBadTable (Table tableFromBytes, String badTableFromString, String ... errorContents) {
    try {
      tableFromBytes.fromBytes(badTableFromString.getBytes());
      fail(badTableFromString+" should cause Exception for Malformed table");
    } catch (MidasException e) {
      String emsg = e.getMessage();
      for (String errContent : errorContents)
        assertTrue("Exception contains: "+errContent, emsg.contains(errContent));
    }
  }

  /** utility method check fromBytes(..) */
  private void checkTableFromString (String msg, String tableFromString, String tabFlags, String ...keyValPairs) {
    Table tableFromBytes = new Table();
    tableFromBytes.setFlags(tabFlags);
    tableFromBytes.fromBytes(tableFromString.getBytes());
    for (int i=0; (i+1)<keyValPairs.length; i+=2) {
      String key    = keyValPairs[i];
      String expVal = keyValPairs[i+1];
      assertEquals(msg+" at key="+key, expVal, tableFromBytes.getS(key));
    }
    if ((keyValPairs.length%2) != 0) {
      throw new MidasException("keyValPairs parameter not in pairs of twos");
    }
  }

  /**
   * Test the ability to add all data from another table.
   */
  @SuppressWarnings("deprecation")
  public void testFromTable() {
    Table t0 = new Table();

    Hashtable<String,Object> ht = new Hashtable<>();

    ht.put("ZERO",  ZERO);
    ht.put("ONE",   ONE);
    ht.put("TWO",   TWO);
    ht.put("THREE", THREE);
    ht.put("FOUR",  FOUR);

    t0.fromTable(ht);

    assertTrue("t0.equals(ht)", t0.equals(ht));
  }

  /**
   * Placeholder test. This is partially testing in testPut()
   * @see #testPut()
   */
  public void testFromTextFile() { } // partially tested in testPut ()

  public void testGet() {
    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);

      assertSame(""+mode+": get('ZERO')",  ZERO,  table.get("ZERO"));
      assertSame(""+mode+": get('ONE')",   ONE,   table.get("ONE"));
      assertSame(""+mode+": get('TWO')",   TWO,   table.get("TWO"));
      assertSame(""+mode+": get('THREE')", THREE, table.get("THREE"));
      assertSame(""+mode+": get('FOUR')",  FOUR,  table.get("FOUR"));

      assertNull(""+mode+": get('FIVE')",  table.get("FIVE"));
      assertNull(""+mode+": get('SIX')",   table.get("SIX"));
    }

    getKeysWithDots();
  }

  private void getKeysWithDots () {
    Table dotKeys = new Table("{FIRST={SECOND={NEXT={FOURTH=HI!}}}}");
    dotKeys.setFlags("+AllowDotsInKeys");
    Table innerDot = new Table();
    innerDot.setFlags("+AllowDotsInKeys");
    innerDot.putx("THIRD.FOURTH", "HELLO!");
    dotKeys.putx("FIRST.SECOND", innerDot);
    dotKeys.putx("FIRST.NEXT", "Greetings!");


    // NOTE: GET will give priority to nesting
    //       GETX only honors direct Key names
    // The decision to have GET give priority to nesting was based on the awkwardness
    // of having to try each key option starting with the full "key" string and then
    // backing up one dot at a time, when getx is available
    Object getVal1 = dotKeys.get("FIRST");
    Object getxVal1 = dotKeys.getx("FIRST");
    Object getVal12 = dotKeys.get("FIRST.SECOND");
    Object getxVal12 = dotKeys.getx("FIRST.SECOND");
    Object getVal12alt = dotKeys.get("FIRST.NEXT");
    Object getVal1234 = dotKeys.get("FIRST.SECOND.NEXT.FOURTH");
    Object getInvalid = dotKeys.get("FIRST.SECOND.THIRD.FOURTH");
    Object getxVal1234 = ((Table)dotKeys.getx("FIRST.SECOND")).get("THIRD.FOURTH");
    assertEquals("Table +AllowDotsInKeys get no dot", "{SECOND={NEXT={FOURTH=\"HI!\"}}}", getVal1.toString());
    assertEquals("Table +AllowDotsInKeys getx no dot", "{SECOND={NEXT={FOURTH=\"HI!\"}}}", getxVal1.toString());
    assertEquals("Table +AllowDotsInKeys get prefers dot mean nested", "{NEXT={FOURTH=\"HI!\"}}", getVal12.toString());
    assertEquals("Table +AllowDotsInKeys getx assumes dot part of name", "{THIRD.FOURTH=\"HELLO!\"}", getxVal12.toString());
    assertEquals("Table +AllowDotsInKeys get dot can drill down", "HI!", getVal1234);
    assertEquals("Table +AllowDotsInKeys dot in keys multiple getx to drill down", "HELLO!", getxVal1234);
    assertEquals("Table +AllowDotsInKeys dot in keys invalid to use get to drill down", null, getInvalid);
    assertEquals("Table +AllowDotsInKeys get falls back to full key if drilling down fails", "Greetings!", getVal12alt);
    // removal does honor the full name if available and then switches to trying
    // the name up to the first dot
    dotKeys.remove("FIRST.SECOND");
    dotKeys.remove("FIRST.NEXT");
    assertEquals("Table +AllowDotsInKeys remove prefers full key name",
        "{FIRST={SECOND={NEXT={FOURTH=\"HI!\"}}}}", dotKeys.toString());
  }

  public void testGetB() {
    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);

      assertEquals(""+mode+": getB('ZERO')",  (byte)0, table.getB("ZERO"));
      assertEquals(""+mode+": getB('ONE')",   (byte)1, table.getB("ONE"));
      assertEquals(""+mode+": getB('TWO')",   (byte)2, table.getB("TWO"));
      assertEquals(""+mode+": getB('THREE')", (byte)3, table.getB("THREE"));
      assertEquals(""+mode+": getB('FOUR')",  (byte)4, table.getB("FOUR"));

      assertEquals(""+mode+": getB('FIVE')",  (byte)0, table.getB("FIVE"));
      assertEquals(""+mode+": getB('SIX')",   (byte)0, table.getB("SIX"));

      assertEquals(""+mode+": getB('THREE', 3)", (byte)3, table.getB("THREE", (byte)3));
      assertEquals(""+mode+": getB('FOUR',  4)", (byte)4, table.getB("FOUR",  (byte)4));
      assertEquals(""+mode+": getB('FIVE',  5)", (byte)5, table.getB("FIVE",  (byte)5));
      assertEquals(""+mode+": getB('SIX',   6)", (byte)6, table.getB("SIX",   (byte)6));
    }
  }

  public void testGetD() {
    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);

      assertEquals(""+mode+": getD('ZERO')",  0.0d, table.getD("ZERO"));
      assertEquals(""+mode+": getD('ONE')",   1.0d, table.getD("ONE"));
      assertEquals(""+mode+": getD('TWO')",   2.0d, table.getD("TWO"));
      assertEquals(""+mode+": getD('THREE')", 3.0d, table.getD("THREE"));
      assertEquals(""+mode+": getD('FOUR')",  4.0d, table.getD("FOUR"));

      assertEquals(""+mode+": getD('FIVE')",  0.0d, table.getD("FIVE"));
      assertEquals(""+mode+": getD('SIX')",   0.0d, table.getD("SIX"));

      assertEquals(""+mode+": getD('THREE', 3.0d)", 3.0d, table.getD("THREE", 3.0d));
      assertEquals(""+mode+": getD('FOUR',  4.0d)", 4.0d, table.getD("FOUR",  4.0d));
      assertEquals(""+mode+": getD('FIVE',  5.0d)", 5.0d, table.getD("FIVE",  5.0d));
      assertEquals(""+mode+": getD('SIX',   6.0d)", 6.0d, table.getD("SIX",   6.0d));
    }
  }

//  public void testGetData() {
//    Table table = makeSampleTable();
//
//    final Data one = new Data(1);
//    final Data two = new Data(2);
//    final Data six = new Data(6);
//    final Data ten = new Data(10);
//
//    table.put("ONE", one);
//    table.put("TWO", two);
//    table.put("SIX", six);
//    table.put("TEN", ten);
//
//    assertEquals("getData('ZERO')",  null, table.getData("ZERO"));
//    assertEquals("getData('ONE')",   one,  table.getData("ONE"));
//    assertEquals("getData('TWO')",   two,  table.getData("TWO"));
//    assertEquals("getData('THREE')", null, table.getData("THREE"));
//    assertEquals("getData('FOUR')",  null, table.getData("FOUR"));
//    assertEquals("getData('FIVE')",  null, table.getData("FIVE"));
//    assertEquals("getData('SIX')",   six,  table.getData("SIX"));
//    assertEquals("getData('SEVEN')", null, table.getData("SEVEN"));
//    assertEquals("getData('EIGHT')", null, table.getData("EIGHT"));
//    assertEquals("getData('NINE')",  null, table.getData("NINE"));
//    assertEquals("getData('TEN')",   ten,  table.getData("TEN"));
// }

//  public void testGetDataFile() { }

  public void testGetDefaultFlags () {
    Table tbl = new Table();
    assertEquals("New table has default flags (int)",    tbl.getFlagsInt(), Table.getDefaultFlagsInt());
    assertEquals("New table has default flags (String)", tbl.getFlags(),    Table.getDefaultFlags());
    assertEquals("New table has default mode (int)",     tbl.getModeInt(),  Table.getDefaultModeInt());
    assertEquals("New table has default mode (String)",  tbl.getMode(),     Table.getDefaultMode());
    assertEquals("New table has default mode (name)",    tbl.getModeName(), Table.getDefaultModeName());
  }

  public void testGetDefaultFlagsInt () { } // tested in testGetDefaultFlags
  public void testGetDefaultMode     () { } // tested in testGetDefaultFlags
  public void testGetDefaultModeInt  () { } // tested in testGetDefaultFlags
  public void testGetDefaultModeName () { } // tested in testGetDefaultFlags

  public void testGetF() {
    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);

      assertEquals(""+mode+": getF('ZERO')",  0.0f, table.getF("ZERO"));
      assertEquals(""+mode+": getF('ONE')",   1.0f, table.getF("ONE"));
      assertEquals(""+mode+": getF('TWO')",   2.0f, table.getF("TWO"));
      assertEquals(""+mode+": getF('THREE')", 3.0f, table.getF("THREE"));
      assertEquals(""+mode+": getF('FOUR')",  4.0f, table.getF("FOUR"));

      assertEquals(""+mode+": getF('FIVE')",  0.0f, table.getF("FIVE"));
      assertEquals(""+mode+": getF('SIX')",   0.0f, table.getF("SIX"));

      assertEquals(""+mode+": getF('THREE', 3.0f)", 3.0f, table.getF("THREE", 3.0f));
      assertEquals(""+mode+": getF('FOUR',  4.0f)", 4.0f, table.getF("FOUR",  4.0f));
      assertEquals(""+mode+": getF('FIVE',  5.0f)", 5.0f, table.getF("FIVE",  5.0f));
      assertEquals(""+mode+": getF('SIX',   6.0f)", 6.0f, table.getF("SIX",   6.0f));
    }
  }

  public void testGetFlags           () { } // tested in testGetDefaultFlags
  public void testGetFlagsInt        () { } // tested in testGetDefaultFlags

  @SuppressWarnings("deprecation")
  public void testGetHashtable() {
    Table t0 = new Table();

    Hashtable<String, Object> ht = t0.getHashtable();

    assertTrue("t0.equals(ht)", t0.equals(ht));
  }

  public void testGetI() {
    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);

      assertEquals(""+mode+": getI('ZERO')",  (short)0, table.getI("ZERO"));
      assertEquals(""+mode+": getI('ONE')",   (short)1, table.getI("ONE"));
      assertEquals(""+mode+": getI('TWO')",   (short)2, table.getI("TWO"));
      assertEquals(""+mode+": getI('THREE')", (short)3, table.getI("THREE"));
      assertEquals(""+mode+": getI('FOUR')",  (short)4, table.getI("FOUR"));

      assertEquals(""+mode+": getI('FIVE')",  (short)0, table.getI("FIVE"));
      assertEquals(""+mode+": getI('SIX')",   (short)0, table.getI("SIX"));

      assertEquals(""+mode+": getI('THREE', 3)", (short)3, table.getI("THREE", (short)3));
      assertEquals(""+mode+": getI('FOUR',  4)", (short)4, table.getI("FOUR",  (short)4));
      assertEquals(""+mode+": getI('FIVE',  5)", (short)5, table.getI("FIVE",  (short)5));
      assertEquals(""+mode+": getI('SIX',   6)", (short)6, table.getI("SIX",   (short)6));
    }
  }

  public void testGetKey() {
    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);
      String size = Integer.toString(table.getSize());
      String keyList = table.getKeyList();
      String[] keys = table.getKeys();
      String keysSize = Integer.toString(keys.length);
      String[] passedKeys = (String[]) table.getKey("GETKEYS");
      String passedSize = Integer.toString(passedKeys.length);

      Arrays.sort(keys);

      for (int ii = 0; ii < keys.length; ii++) {
        if (keys[ii] != null) {
          boolean found = (Arrays.binarySearch(keys,passedKeys[ii]) >= 0)? true: false;
          assertTrue("getKey('GETKEYS') --> contains "+passedKeys[ii], found);
        }
      }

      assertEquals("getKey('GETKEYS') count", keysSize, passedSize);
      assertNull  ("getKey('GETPREV')",       table.getKey("GETPREV"));
      assertEquals("getKey('GETSIZE')",       size,     table.getKey("GETSIZE").toString());
      assertEquals("getKey('GETLIST')",       keyList,  table.getKey("GETLIST").toString());

      assertSame(""+mode+": getKey('ZERO')",  ZERO,  table.getKey("ZERO"));
      assertSame(""+mode+": getKey('ONE')",   ONE,   table.getKey("ONE"));
      assertSame(""+mode+": getKey('TWO')",   TWO,   table.getKey("TWO"));
      assertSame(""+mode+": getKey('THREE')", THREE, table.getKey("THREE"));
      assertSame(""+mode+": getKey('FOUR')",  FOUR,  table.getKey("FOUR"));

      assertNull(""+mode+": getKey('FIVE')",  table.getKey("FIVE"));
      assertNull(""+mode+": getKey('SIX')",   table.getKey("SIX"));
    }
  }

  public void testGetKeyList() {
    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);

      String expected = StringUtil.join(table.getKeys(),",");
      String actual   = table.getKeyList();

      assertEquals(""+mode+": Got correct key list", expected, actual);
    }
  }

  public void testGetKeys() {
    for (TableMode mode : MODES) {
      // Table table = makeSimpleSampleTable(mode,0);
      Table table = makeSampleTable(mode);

//    String[] keys   = { "ZERO", "ONE", "TWO", "THREE", "FOUR" }; // The expected keys
      String[] keys   = keys_for_sample_table; // The expected keys
      String[] actual = table.getKeys();       // The actual keys

      if (mode.ordered == SORT) Arrays.sort(keys);
      checkArrays(mode, true, keys, actual, null, null);
    }
  }

  public void testGetL() {
    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);

      assertEquals(""+mode+": getL('ZERO')",  0, table.getL("ZERO"));
      assertEquals(""+mode+": getL('ONE')",   1, table.getL("ONE"));
      assertEquals(""+mode+": getL('TWO')",   2, table.getL("TWO"));
      assertEquals(""+mode+": getL('THREE')", 3, table.getL("THREE"));
      assertEquals(""+mode+": getL('FOUR')",  4, table.getL("FOUR"));

      assertEquals(""+mode+": getL('FIVE')",  0, table.getL("FIVE"));
      assertEquals(""+mode+": getL('SIX')",   0, table.getL("SIX"));

      assertEquals(""+mode+": getL('THREE', 3)", 3, table.getL("THREE", 3));
      assertEquals(""+mode+": getL('FOUR',  4)", 4, table.getL("FOUR",  4));
      assertEquals(""+mode+": getL('FIVE',  5)", 5, table.getL("FIVE",  5));
      assertEquals(""+mode+": getL('SIX',   6)", 6, table.getL("SIX",   6));
    }
  }

  public void testGetMode() {
    for (TableMode mode : MODES) {
      if (!mode.modeInt) {
        Table table = new Table(mode.modeNum);
        assertEquals(""+mode+": getMode()", mode.modeStr, table.getMode());
      }
    }
  }

  public void testGetModeInt() {
    for (TableMode mode : MODES) {
      if (!mode.modeInt) {
        Table table = new Table(mode.modeNum);
        assertEquals(""+mode+": getMode()", mode.modeNum, table.getModeInt());
      }
    }
  }

  public void testGetModeName() {
    for (TableMode mode : MODES) {
      if (!mode.modeInt) {
        Table table = new Table(mode.modeNum);
        assertEquals(""+mode+": getMode()", mode.modeAbr, table.getModeName());
      }
    }
  }

  public void testGetNoState() {
    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);

      table.put("T",   "TRUE");
      table.put("F",   "FALSE");
      table.put("Y",   "YES");
      table.put("N",   "NO");
      table.put("POS", "100");
      table.put("NEG", "-10");

      assertEquals(""+mode+": getNoState('ZERO')", true,  table.getNoState("ZERO"));
      assertEquals(""+mode+": getNoState('ONE')",  false, table.getNoState("ONE"));
      assertEquals(""+mode+": getNoState('TWO')",  false, table.getNoState("TWO"));
      assertEquals(""+mode+": getNoState('T')",    false, table.getNoState("T"));
      assertEquals(""+mode+": getNoState('F')",    true,  table.getNoState("F"));
      assertEquals(""+mode+": getNoState('Y')",    false, table.getNoState("Y"));
      assertEquals(""+mode+": getNoState('N')",    true,  table.getNoState("N"));
      assertEquals(""+mode+": getNoState('POS')",  false, table.getNoState("POS"));
      assertEquals(""+mode+": getNoState('NEG')",  true,  table.getNoState("NEG"));
      assertEquals(""+mode+": getNoState('A')",    false, table.getNoState("A"));

      assertEquals(""+mode+": getNoState('T', true)",  false, table.getNoState("T", true));
      assertEquals(""+mode+": getNoState('T', false)", false, table.getNoState("T", false));
      assertEquals(""+mode+": getNoState('A', true)",  true,  table.getNoState("A", true));
      assertEquals(""+mode+": getNoState('A', false)", false, table.getNoState("A", false));
    }
  }

  public void testGetO() {
    Integer five = Integer.valueOf(5);
    Integer six  = Integer.valueOf(6);

    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);
      //Tests that the values are in the table
      assertSame(""+mode+": getO('ZERO')",  ZERO,  table.getO("ZERO"));
      assertSame(""+mode+": getO('ONE')",   ONE,   table.getO("ONE"));
      assertSame(""+mode+": getO('TWO')",   TWO,   table.getO("TWO"));
      assertSame(""+mode+": getO('THREE')", THREE, table.getO("THREE"));
      assertSame(""+mode+": getO('FOUR')",  FOUR,  table.getO("FOUR"));
      //test that the values are not in the table
      assertNull(""+mode+": getO('FIVE')",  table.getO("FIVE"));
      assertNull(""+mode+": getO('SIX')",   table.getO("SIX"));
      //Tests that if the value is not in the table to add the default value. First two do not add a value because they key is already in the table.
      assertSame(""+mode+": getO('ONE',  five)", ONE,  table.getO("ONE",  five));
      assertSame(""+mode+": getO('TWO',  six)",  TWO,  table.getO("TWO",  six));
      assertSame(""+mode+": getO('FIVE', five)", five, table.getO("FIVE", five));
      assertSame(""+mode+": getO('SIX',  six)",  six,  table.getO("SIX",  six));
    }
  }

  public void testGetPrevious() { }

  public void testGetRoot() { }

  public void testGetS() {
    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);

      assertEquals(""+mode+": getS('ZERO')",  "0", table.getS("ZERO"));
      assertEquals(""+mode+": getS('ONE')",   "1", table.getS("ONE"));
      assertEquals(""+mode+": getS('TWO')",   "2", table.getS("TWO"));
      assertEquals(""+mode+": getS('THREE')", "3", table.getS("THREE"));
      assertEquals(""+mode+": getS('FOUR')",  "4", table.getS("FOUR"));

      assertNull(""+mode+": getS('FIVE')",  table.getS("FIVE"));
      assertNull(""+mode+": getS('SIX')",   table.getS("SIX"));

      assertEquals(""+mode+": getS('THREE', 'three')", "3",    table.getS("THREE", "three"));
      assertEquals(""+mode+": getS('FOUR',  'four' )", "4",    table.getS("FOUR",  "four"));
      assertEquals(""+mode+": getS('FIVE',  'five' )", "five", table.getS("FIVE",  "five"));
      assertEquals(""+mode+": getS('SIX',   'six'  )", "six",  table.getS("SIX",   "six"));
    }
  }

  public void testGetSize() {
    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);
      int expSize = size_sample_table;

      assertEquals(""+mode+": Table size()",    expSize, table.size());
      assertEquals(""+mode+": Table getSize()", expSize, table.getSize());
      table.clear();
      assertEquals(""+mode+": Clear Table, size is 0", 0, table.size());
      assertEquals(""+mode+": Clear Table, size is 0", 0, table.getSize());
      assertEquals(""+mode+": New Table, size is 0",   0, (new Table()).size());
      assertEquals(""+mode+": New Table, size is 0",   0, (new Table()).getSize());
    }
  }

  public void testGetState() {
    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);

      table.put("T",   "TRUE");
      table.put("F",   "FALSE");
      table.put("Y",   "YES");
      table.put("N",   "NO");
      table.put("POS", "100");
      table.put("NEG", "-10");

      assertEquals(""+mode+": getState('ZERO')", false, table.getState("ZERO"));
      assertEquals(""+mode+": getState('ONE')",  true,  table.getState("ONE"));
      assertEquals(""+mode+": getState('TWO')",  true,  table.getState("TWO"));
      assertEquals(""+mode+": getState('T')",    true,  table.getState("T"));
      assertEquals(""+mode+": getState('F')",    false, table.getState("F"));
      assertEquals(""+mode+": getState('Y')",    true,  table.getState("Y"));
      assertEquals(""+mode+": getState('N')",    false, table.getState("N"));
      assertEquals(""+mode+": getState('POS')",  true,  table.getState("POS"));
      assertEquals(""+mode+": getState('NEG')",  false, table.getState("NEG"));
      assertEquals(""+mode+": getState('A')",    false, table.getState("A"));

      assertEquals(""+mode+": getState('T', true)",  true,  table.getState("T", true));
      assertEquals(""+mode+": getState('T', false)", true,  table.getState("T", false));
      assertEquals(""+mode+": getState('A', true)",  true,  table.getState("A", true));
      assertEquals(""+mode+": getState('A', false)", false, table.getState("A", false));
    }
  }

  public void testGetString() {
    String one = "one";
    String two = "two";
    String six = "six";
    String ten = "ten";

    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);

      table.put("ONE", one);
      table.put("TWO", two);
      table.put("SIX", six);
      table.put("TEN", ten);

      assertEquals(""+mode+": getString('ZERO')",  null, table.getString("ZERO"));
      assertEquals(""+mode+": getString('ONE')",   one,  table.getString("ONE"));
      assertEquals(""+mode+": getString('TWO')",   two,  table.getString("TWO"));
      assertEquals(""+mode+": getString('THREE')", null, table.getString("THREE"));
      assertEquals(""+mode+": getString('FOUR')",  null, table.getString("FOUR"));
      assertEquals(""+mode+": getString('FIVE')",  null, table.getString("FIVE"));
      assertEquals(""+mode+": getString('SIX')",   six,  table.getString("SIX"));
      assertEquals(""+mode+": getString('SEVEN')", null, table.getString("SEVEN"));
      assertEquals(""+mode+": getString('EIGHT')", null, table.getString("EIGHT"));
      assertEquals(""+mode+": getString('NINE')",  null, table.getString("NINE"));
      assertEquals(""+mode+": getString('TEN')",   ten,  table.getString("TEN"));
    }
  }

  public void testGetTable() {
    Table t1 = new Table();
    Table t2 = new Table();
    Table t3 = new Table();

    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);
      table.put("T1", t1);
      table.put("T2", t2);
      table.put("T3", t3);

      assertEquals(mode+": getTable('ZERO')",  null, table.getTable("ZERO"));
      assertEquals(mode+": getTable('ONE')",   null, table.getTable("ONE"));
      assertEquals(mode+": getTable('TWO')",   null, table.getTable("TWO"));
      assertEquals(mode+": getTable('THREE')", null, table.getTable("THREE"));
      assertEquals(mode+": getTable('FOUR')",  null, table.getTable("FOUR"));
      assertEquals(mode+": getTable('FIVE')",  null, table.getTable("FIVE"));
      assertEquals(mode+": getTable('SIX')",   null, table.getTable("SIX"));
      assertEquals(mode+": getTable('T1')",    t1,   table.getTable("T1"));
      assertEquals(mode+": getTable('T2')",    t2,   table.getTable("T2"));
      assertEquals(mode+": getTable('T3')",    t3,   table.getTable("T3"));
    }

    // tests for BUG 2333 - no exceptions thrown, defValue should be returned for getTable(String, Table defValue)
    //                      when sub-key not found; i.e. does NOT drill down into object's properties/methods.
    Table t = new Table();
    Table d = new Table();
    Table z = new Table();
    t.put("D", d);
    t.put("D.C", 2);
    t.put("D.Z", z);
    t.put("A", 1);
    t.put("E", "Hello");
    Table[] defTables = {null, new Table()};
    for (Table defTab : defTables) {
      assertEquals("getTable('B', defTab)==defTab",     defTab, t.getTable("B",     defTab));
      assertEquals("getTable('B.C', defTab)==defTab",   defTab, t.getTable("B.C",   defTab));
      assertEquals("getTable('B.D.D', defTab)==defTab", defTab, t.getTable("B.D.D", defTab));
      assertEquals("getTable('D', defTab)== D",              d, t.getTable("D",     defTab));
      assertEquals("getTable('D.Z', defTab)==Z",             z, t.getTable("D.Z",   defTab));
      assertEquals("getTable('D.C', defTab)==defTab",   defTab, t.getTable("D.C",   defTab));
      assertEquals("getTable('E', defTab)==defTab",     defTab, t.getTable("E",     defTab));
      assertEquals("getTable('A', defTab)==defTab",     defTab, t.getTable("A",     defTab));
    } // end for loop
  }

  public void testGetTableOrNull() {
    // tests for BUG 2333 (no exceptions thrown, nulls should be returned)
    Table t = new Table();
    Table d = new Table();
    Table z = new Table();
    t.put("D", d);
    t.put("D.C", 2);
    t.put("D.Z", z);
    t.put("A", 1);
    t.put("E", "Hello");
    assertEquals("getTableOrNull('B', null)",     null, t.getTableOrNull("B"));
    assertEquals("getTableOrNull('B.C', null)",   null, t.getTableOrNull("B.C"));
    assertEquals("getTableOrNull('B.D.D', null)", null, t.getTableOrNull("B.D.D"));
    assertEquals("getTableOrNull('D', null)== D",    d, t.getTableOrNull("D"));
    assertEquals("getTableOrNull('D.Z', null)==Z",   z, t.getTableOrNull("D.Z"));
    assertEquals("getTableOrNull('D.C', null)",   null, t.getTableOrNull("D.C"));
    assertEquals("getTableOrNull('E', null)",     null, t.getTableOrNull("E"));
    assertEquals("getTableOrNull('A', null)",     null, t.getTableOrNull("A"));
  }

  public void testGetUniqueKey() {
    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);

      assertEquals(""+mode+": getUniqueKey('AKEY') #0",  "AKEY",  table.getUniqueKey("AKEY"));
      table.put("AKEY", "VAL");
      assertEquals(""+mode+": getUniqueKey('AKEY') #1",  "AKEY1", table.getUniqueKey("AKEY"));
      table.put("AKEY1", "VAL");
      assertEquals(""+mode+": getUniqueKey('AKEY') #2",  "AKEY2", table.getUniqueKey("AKEY"));
      table.put("AKEY2", "VAL");
      assertEquals(""+mode+": getUniqueKey('AKEY') #3",  "AKEY3", table.getUniqueKey("AKEY"));


      assertEquals(""+mode+": getUniqueKey('MYKEY','_no') #0", "MYKEY",     table.getUniqueKey("MYKEY","_no"));
      table.put("MYKEY", "VAL");
      assertEquals(""+mode+": getUniqueKey('MYKEY','_no') #1", "MYKEY_no1", table.getUniqueKey("MYKEY","_no"));
      table.put("MYKEY_no1", "VAL");
      assertEquals(""+mode+": getUniqueKey('MYKEY','_no') #2", "MYKEY_no2", table.getUniqueKey("MYKEY","_no"));
      table.put("MYKEY_no2", "VAL");
      assertEquals(""+mode+": getUniqueKey('MYKEY','_no') #3", "MYKEY_no3", table.getUniqueKey("MYKEY","_no"));
    }
  }

  public void testGetx() { }

  public void testGetX() {
    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);

      assertEquals(""+mode+": getX('ZERO')",  0L, table.getX("ZERO"));
      assertEquals(""+mode+": getX('ONE')",   1L, table.getX("ONE"));
      assertEquals(""+mode+": getX('TWO')",   2L, table.getX("TWO"));
      assertEquals(""+mode+": getX('THREE')", 3L, table.getX("THREE"));
      assertEquals(""+mode+": getX('FOUR')",  4L, table.getX("FOUR"));

      assertEquals(""+mode+": getX('FIVE')",  0L, table.getX("FIVE"));
      assertEquals(""+mode+": getX('SIX')",   0L, table.getX("SIX"));

      assertEquals(""+mode+": getX('THREE', 3L)", 3L, table.getX("THREE", 3L));
      assertEquals(""+mode+": getX('FOUR',  4L)", 4L, table.getX("FOUR",  4L));
      assertEquals(""+mode+": getX('FIVE',  5L)", 5L, table.getX("FIVE",  5L));
      assertEquals(""+mode+": getX('SIX',   6L)", 6L, table.getX("SIX",   6L));


      // Test larger and smaller values (Bug 1599)
      for (long val = Integer.MAX_VALUE - 8L; val <= Integer.MAX_VALUE + 8L; val++) {
        String key = "POS"+(val);
        table.put(key, val);
        assertEquals(""+mode+": getX('"+key+"', "+val+")", val, table.getX(key));
      }
      for (long val = Integer.MIN_VALUE - 8L; val <= Integer.MIN_VALUE + 8L; val++) {
        String key = "NEG"+(-val);
        table.put(key, val);
        assertEquals(""+mode+": getX('"+key+"', "+val+")", val, table.getX(key));
      }
    }
  }

  public void testHashCode() {
    for (TableMode mode : MODES) {
      Table t0 = makeSampleTable(mode);
      Table t1 = t0;
      Table t2 = makeSampleTable(mode);
      Table t3 = makeSampleTable(mode);
      Table t4 = new Table();
      Table t5 = makeSampleTable(mode);

      Hashtable<String,Object> ht = new Hashtable<>();
      final Integer fourZeroFour = Integer.valueOf(404);

      t3.put("FOUR_HUNDRED_FOUR",  fourZeroFour);
      t5.put("FOUR_HUNDRED_FOUR", Integer.valueOf(404));
      insertSampleTestData(t4, ht);

      assertEquals(""+mode+": t0.hashCode() == t1.hashCode()", t0.hashCode(), t1.hashCode());
      assertEquals(""+mode+": t0.hashCode() == t2.hashCode()", t0.hashCode(), t2.hashCode());

      assertEquals(""+mode+": t3.hashCode() == t5.hashCode()", t3.hashCode(), t5.hashCode());
      assertEquals(""+mode+": t3.hashCode() == t4.hashCode()", t3.hashCode(), t4.hashCode());
      assertEquals(""+mode+": t4.hashCode() == ht.hashCode()", t4.hashCode(), ht.hashCode());
    }
  }

  public void testIsEmpty() {
    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);

      assertFalse(""+mode+": Table size is not empty", table.isEmpty());
      table.clear();
      assertTrue(""+mode+": Clear Table, is empty", table.isEmpty());
      assertTrue(""+mode+": New Table, is empty",   (new Table()).isEmpty());
    }
  }

  public void testIsMode() {
    for (TableMode mode : MODES) {
      Table table = new Table(mode.modeNum);
      assertTrue(""+mode+": isMode("+mode.modeNum+")", table.isMode(mode.modeNum));
      assertTrue(""+mode+": isMode("+mode.modeStr+")", table.isMode(mode.modeStr));
      assertTrue(""+mode+": isMode("+mode.modeAbr+")", table.isMode(mode.modeAbr));
    }
  }

  public void testIterator() {
    int ii;
    for (int mode : MODES_KEEPING_INSERTION_ORDER) {
      Table table = makeSampleTable(mode, 0);
      Table.Iterator iter = table.iterator();
      for (ii=0; ii<size_sample_table && iter.hasNext(); ii++) {
        String key = iter.next();
        Object val = iter.getValue();
        assertEquals(""+mode+": iterator() key    at "+ii, keys_for_sample_table[ii], key);
        assertEquals(""+mode+": iterator() vallue at "+ii, table.get(key), val);
      }

      // since 3.3.1 tests for Table implementing Iterable<String> interface (BUG 2699)
      ii = 0;
      for (String key : table) {
        Object val = table.get(key);
        assertEquals(""+mode+": Iterable - key    at "+ii, keys_for_sample_table[ii], key);
        assertEquals(""+mode+": Iterable - vallue at "+ii, table.get(key), val);
        ii++;
      }

      // since 3.5.4 test Table.Iterator implementing Iterable<String> interface and reset() method (BUG 2907)
      iter.reset();
      ii = 0;
      for (String key : iter) {
        Object val = iter.getValue();
        assertEquals(mode+": Table.iterator() key    at "+ii, keys_for_sample_table[ii], key);
        assertEquals(mode+": Table.iterator() vallue at "+ii, table.get(key), val);
        ii++;
      }

    } // end outer for loop

    // more tests for Table implementing Iterable<String> interface (BUG 2699)
    Table expResults = makeSampleTable(Table.LHM, 0);
    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);
      ii = 0;
      for (String key : table) {
        Object val = table.get(key);
        assertEquals(""+mode+": Iterable - value at key: "+key, expResults.get(key), val);
        if (ii++ % 2 == 0) {
          table.remove(key); // test removing value at specified key on table
          assertFalse(""+mode+": Iterable - remove key: "+key, table.containsKey(key));
        }
      }
    }
  }

  public void testKeys() {
    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);

      String[] keys = keys_for_sample_table;                        // The expected keys
      Enumeration<String> enumKeys = table.keys();                        // The actual keys

      if (mode.ordered == SORT) Arrays.sort(keys);
      checkArrays(mode, true, keys, toArray(enumKeys), null, null);
    }
  }

  public void testKeySet() {
    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);

      String[] keys = keys_for_sample_table;                        // The expected keys

      Set<String> col = table.keySet();
      Iterator<String> iter = col.iterator();

      if (mode.ordered == SORT) Arrays.sort(keys);
      checkArrays(mode, true, keys, toArray(iter), null, null);
    }
  }

  /** Tests that Tables with Keys with characters '(' and ')' can parse correctly
   *  and there is an option for quoting keys when serializing to handle
   *  even '{', "}', and '=' within keys NXM-3715
   *  @since NeXtMidas 4.0.0
   */
  public void testKeyWithSpecialCharacters () {
    // Case 1: TEST(...) can be a key and is used with LIST2 templates
    // test_list2_template_colors "Range"
    String strTableWithTestKey = "{COLORMAP={_DEFAULT_=RED,TEST(X,LT,3)=YELLOW}}}";
    Table tableWithTestKey = new Table(strTableWithTestKey);
    assertEquals("Table with 'TEST(X,LT,3)' handles ','s properly","_DEFAULT_,TEST(X,LT,3)",((Table)tableWithTestKey.getKey("COLORMAP")).getKeyList());
    // Case 2: random ')' '(' in key

    Table parenKeysTbl = new Table("{he)(o=hello,1) keys={with ) or (=should,be=reserializable}}",Table.CASESENSITIVE);
    String serializedTbl = parenKeysTbl.toString();
    Table parentKeysTblCopy = new Table(serializedTbl,Table.CASESENSITIVE);
    assertEquals("Table with keys with '(' or ')' reserialize",parenKeysTbl,parentKeysTblCopy);
    Table curlyKeysTbl = new Table(Table.HT,Table.CASESENSITIVE|Table.QUOTED_KEYS_SERIALIZATION);
    curlyKeysTbl.put("my key with } {","needs quoting");
    Table innerCurlyKeysTbl = new Table("{initial key=simple}",Table.CASESENSITIVE|Table.QUOTED_KEYS_SERIALIZATION);
    innerCurlyKeysTbl.put("added key", curlyKeysTbl);
    String serializedTbl2 = innerCurlyKeysTbl.toString();
    Table innerCurlyKeysTblCpy = new Table(serializedTbl2,Table.CASESENSITIVE|Table.QUOTED_KEYS_SERIALIZATION);
    String curlyKeyValue = null;
    Object innerTable = innerCurlyKeysTblCpy.get("added key");
    if (innerTable instanceof Table) curlyKeyValue = String.valueOf(((Table)innerTable).get("my key with } {"));
    assertEquals("Table with quoted keys with '{' or '}' reserializes","needs quoting",curlyKeyValue);
    Table equalInKey = new Table(Table.HT,Table.CASESENSITIVE|Table.QUOTED_KEYS_SERIALIZATION);
    Data vector = Data.fromString("(1,1,1)", 'D', null);
    equalInKey.put("D:1=0x1 = 1l", vector);
    String serializedEqualKey = equalInKey.toString();
    Table equalInKeyCpy = new Table(serializedEqualKey,Table.CASESENSITIVE|Table.QUOTED_KEYS_SERIALIZATION);
    Object cpyVector = equalInKeyCpy.get("1=0x1 = 1l");
    assertEquals("Table with quoted keys with '=' reserializes",vector,cpyVector);
    // Case 2: with '(' and ')' as part of key name support disabled
    Table.setDoNotAllowRandomParen(true);
    Table parentKeysTblReverted = new Table(serializedTbl,Table.CASESENSITIVE);
    String revertedExpectedKeys = "he,(o=hello,1) keys";
    assertEquals("Table with keys with '(' or ')' reserialize",revertedExpectedKeys,parentKeysTblReverted.getKeyList());
    //String revertedBehavior = parenKeysTbl.toString();
    Table.setDoNotAllowRandomParen(false);
  }

  public void testMatch() { }

  public void testMerge () {
    final String A = "A";    final String ABC = "ABC";
    final String B = "B";    final String DEF = "DEF";
    final String C = "C";    final String GHI = "GHI";

    Table leftSub  = new Table();
    Table rightSub = new Table();
    Table mergeSub = new Table();
    leftSub.put("A", A);                               mergeSub.put("A", A);
    leftSub.put("B", B);    rightSub.put("B", TWO);    mergeSub.put("B", TWO);
    leftSub.put("C", C);    rightSub.put("C", THREE);  mergeSub.put("C", THREE);
                            rightSub.put("D", FOUR);   mergeSub.put("D", FOUR);

    Table left  = new Table();
    Table right = new Table();
    Table flat  = new Table();
    Table merge = new Table();
    left.put("ZERO",  ZERO);                                    flat.put("ZERO",  ZERO);      merge.put("ZERO",  ZERO);
    left.put("ONE",   ONE);      right.put("ONE",   ABC);       flat.put("ONE",   ABC);       merge.put("ONE",   ABC);
    left.put("TWO",   TWO);      right.put("TWO",   DEF);       flat.put("TWO",   DEF);       merge.put("TWO",   DEF);
                                 right.put("THREE", GHI);       flat.put("THREE", GHI);       merge.put("THREE", GHI);
    left.put("TABLE", leftSub);  right.put("TABLE", rightSub);  flat.put("TABLE", rightSub);  merge.put("TABLE", mergeSub);

    final KeyVector kv = new KeyVector(); kv.merge(right);
    final Keyable   kb = new Keyable () {
      public String[] getKeys ()                  { return kv.getKeys();   }
      public Object   getKey (String k)           { return kv.getKey(k);   }
      public Object   setKey (String k, Object v) { return kv.setKey(k,v); }
    };

    for (TableMode mode : MODES) {
      // Static merge method
      Table t1 = left.copy();
      Table t2 = right.copy();
      Table f  = Table.merge(t1, t2, false);
      Table m  = Table.merge(t1, t2, true);

      assertEquals("merge(Table,Table,boolean) t1 not changed ("+mode+")",  left,  t1);
      assertEquals("merge(Table,Table,boolean) t2 not changed ("+mode+")",  right, t2);
      assertEquals("merge(Table,Table,boolean) flat merge ("+mode+")",      flat,  f);
      assertEquals("merge(Table,Table,boolean) recursive merge ("+mode+")", merge, m);

      // Instance merge method
      Table tblTable     = t1.copy();   tblTable.merge(t2);
      Table tblKeyVector = t1.copy();   tblKeyVector.merge(kv);
      Table tblKeyable   = t1.copy();   tblKeyable.merge(kb);

      assertEquals("merge(Keyable) with Table ("+mode+")",     flat, tblTable);
      assertEquals("merge(Keyable) with KeyVector ("+mode+")", flat, tblKeyVector);
      assertEquals("merge(Keyable) with Keyable ("+mode+")",   flat, tblKeyable);
    }
  }

  public void testMore() { }

  public void testNext() { }

  /**
   * Test the ability to put data in a table.
   */
  public void testPut() {
    final String one = "one";
    final String two = "two";
    final String six = "six";
    final String ten = "ten";

    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);
      int expSize = table.getSize() + 2; // below puts will add two entries

      Object prevOne = table.put("ONE", one);
      Object prevTwo = table.put("TWO", two);

      Object prevSix = table.put("SIX", six);
      Object prevTen = table.put("TEN", ten);

      assertEquals("Table size",    expSize,     table.size());

      assertSame(mode+": getKey('ZERO')",  ZERO,  table.getKey("ZERO"));
      assertSame(mode+": getKey('ONE')",   one,   table.getKey("ONE"));
      assertSame(mode+": getKey('TWO')",   two,   table.getKey("TWO"));
      assertSame(mode+": getKey('THREE')", THREE, table.getKey("THREE"));
      assertSame(mode+": getKey('FOUR')",  FOUR,  table.getKey("FOUR"));
      assertSame(mode+": getKey('SIX')",   six,   table.getKey("SIX"));
      assertSame(mode+": getKey('TEN')",   ten,   table.getKey("TEN"));

      assertNull(mode+": getKey('FIVE')",  table.getKey("FIVE"));
      assertNull(mode+": getKey('SEVEN')", table.getKey("SEVEN"));
      assertNull(mode+": getKey('EIGHT')", table.getKey("EIGHT"));

      // since 3.3.3 test put(..) return previous value at key (Bug 2699)
      assertSame(mode+": put('ONE') return previous value", ONE, prevOne);
      assertSame(mode+": put('TWO') return previous value", TWO, prevTwo);
      assertNull(mode+": put('SIX') return previous value", prevSix);
      assertNull(mode+": put('TEN') return previous value", prevTen);

      // Test null stored as "NULL".
      Table nullIsNULL  = new Table(mode.modeNum, 0);
      // Test EXCEPTION_ON_NULL flag.
      Table nullIsError = new Table(mode.modeNum, Table.EXCEPTION_ON_NULL);

      nullIsNULL.put("ONE", ONE);  nullIsError.put("ONE", ONE);
      nullIsNULL.put("TWO", TWO);  nullIsError.put("TWO", TWO);
      nullIsNULL.put("TEN", null);

      try {
        nullIsError.put("TEN", null);
        assertTrue(mode+": Insert of null with EXCEPTION_ON_NULL throws NullPointerException", false);
      } catch (NullPointerException e) {
        assertTrue(mode+": Insert of null with EXCEPTION_ON_NULL throws NullPointerException", true);
      }

      assertSame(mode+": getKey('ONE')", ONE, nullIsNULL.getKey("ONE"));
      assertSame(mode+": getKey('TWO')", TWO, nullIsNULL.getKey("TWO"));
      assertSame(mode+": getKey('ONE')", ONE, nullIsError.getKey("ONE"));
      assertSame(mode+": getKey('TWO')", TWO, nullIsError.getKey("TWO"));

      assertEquals(mode+": getKey('TEN')", "NULL", nullIsNULL.getKey("TEN"));

      // Test PERMIT_NULL flag- since 3.7.3 for NXM-3122
      if ( (mode.modeNum != Table.HT) && (mode.modeNum != Table.CHM)) {
        // verify that the key with a null value is stored in the table
        Table nullIsNull  = new Table(mode.modeNum, Table.PERMIT_NULL);
        nullIsNull.put("ONE", ONE);
        nullIsNull.put("TWO", TWO);
        String nullString = null;
        nullIsNull.put("NULLVALUE", nullString);
        assertTrue(mode+" entry with null value added",nullIsNull.keySet().contains("NULLVALUE"));
        assertEquals(mode+": getKey('NULLVALUE')"+nullIsNull, nullString  , nullIsNull.getKey("NULLVALUE"));
        // verify that the null is converted to "null" when saved to a file
        TextFile tf = new TextFile(getMidas(), "ram:table_with_null.tbl");
        nullIsNull.toTextFile(tf);
        Table copyNull = new Table(mode.modeNum, Table.PERMIT_NULL);
        copyNull.fromTextFile(tf);
        assertEquals(mode+": getKey('NULLVALUE')"+copyNull, "null"  , copyNull.getKey("NULLVALUE"));
      }
      else {
        // Trying to create a Map or HashTable Table with PERMIT_NULL should
        // give a NPE, since they do not null values
        try {
          Table nullNPE = new Table(mode.modeNum, Table.PERMIT_NULL);
          nullNPE.put("NULLVALUE", null);
          fail("Inserting null value results in NPE with Table Mode "+mode);
        }
        catch (NullPointerException npe) {
          // pass
        }
      }
    }

    // Test PUT with ADDROOT flag (Bug 2929)
    String value        = "Hello World!"; // use String not Number since so type doesn't convert to Data
    String value2       = "Hi Earth";
    Table  tblNoAddRoot = new Table();
    Table  tblAddRoot   = new Table(); tblAddRoot.setFlags("+AddRoot");

    assertEquals("tblNoAddRoot has AddRoot set", false, (tblNoAddRoot.getFlagsInt() & Table.ADDROOT)!=0);
    assertEquals("tblAddRoot   has AddRoot set", true,  (tblAddRoot.getFlagsInt()   & Table.ADDROOT)!=0);

    try {
      tblNoAddRoot.put("A.B.C", value); // <-- This should throw exception
      assertEquals("tblNoAddRoot put of A.B.C fails", true, false);
    }
    catch (RuntimeException e) {
      assertEquals("tblNoAddRoot put of A.B.C fails", true, true);
    }

    try {
      tblNoAddRoot.put("X.Y.Z", value2); // <-- This should throw exception
      assertEquals("tblNoAddRoot put of X.Y.Z fails", true, false);
    }
    catch (RuntimeException e) {
      assertEquals("tblNoAddRoot put of X.Y.Z fails", true, true);
    }

    try {
      tblAddRoot.put("A.B.C", value); // <-- This should NOT throw exception
      assertEquals("tblAddRoot put of A.B.C fails", false, false);
    }
    catch (RuntimeException e) {
      assertEquals("tblAddRoot put of A.B.C fails", false, true);
    }

    try {
      tblAddRoot.put("X.Y.Z", value2); // <-- This should NOT throw exception
      assertEquals("tblAddRoot put of X.Y.Z fails", false, false);
    }
    catch (RuntimeException e) {
      assertEquals("tblAddRoot put of X.Y.Z fails", false, true);
    }

    // Do the gets one-at-a-time to better-trap errors
    Object objA = tblAddRoot.get("A");      assertEquals("tblAddRoot get of A     is Table",  true, objA instanceof Table);
    Object objB = tblAddRoot.get("A.B");    assertEquals("tblAddRoot get of A.B   is Table",  true, objB instanceof Table);
    Object objC = tblAddRoot.get("A.B.C");  assertEquals("tblAddRoot get of A.B.C is String", true, objC instanceof String);

    Object objX = tblAddRoot.get("X");      assertEquals("tblAddRoot get of X     is Table",  true, objX instanceof Table);
    Object objY = tblAddRoot.get("X.Y");    assertEquals("tblAddRoot get of X.Y   is Table",  true, objY instanceof Table);
    Object objZ = tblAddRoot.get("X.Y.Z");  assertEquals("tblAddRoot get of X.Y.Z is String", true, objZ instanceof String);

    Object objTbl = tblAddRoot; assertEquals("tblAddRoot get of A.B.C, X.Y.Z is Table", true, objTbl instanceof Table);

    Table tblA = (Table)objA;
    Table tblB = (Table)objB;
    assertEquals("tblAddRoot get of A     has proper type",  tblAddRoot.getMode(),  tblA.getMode());
    assertEquals("tblAddRoot get of A     has proper flags", tblAddRoot.getFlags(), tblA.getFlags());
    assertEquals("tblAddRoot get of A.B   has proper type",  tblAddRoot.getMode(),  tblB.getMode());
    assertEquals("tblAddRoot get of A.B   has proper flags", tblAddRoot.getFlags(), tblB.getFlags());
    assertEquals("tblAddRoot get of A.B.C has proper value", value,                 objC);

    Table tblX = (Table)objX;
    Table tblY = (Table)objY;
    assertEquals("tblAddRoot get of X     has proper type",  tblAddRoot.getMode(),  tblX.getMode());
    assertEquals("tblAddRoot get of X     has proper flags", tblAddRoot.getFlags(), tblX.getFlags());
    assertEquals("tblAddRoot get of X.Y   has proper type",  tblAddRoot.getMode(),  tblY.getMode());
    assertEquals("tblAddRoot get of X.Y   has proper flags", tblAddRoot.getFlags(), tblY.getFlags());
    assertEquals("tblAddRoot get of X.Y.Z has proper value", value2,                objZ);


    //********************************************************************************************
    // Tests for +KeepJavaNumericType flag - since 3.8.0 for NXM-3120
    //********************************************************************************************

    // save flags
    int prevFlags = Table.getDefaultFlagsInt();
    Table.setDefaultFlags(0);  // do not get type from previous

    //*** Confirm Default results in a Table of Data Objects with expected Data type ***************
    Table defTable = new Table(); // Table with default flags
    defTable.put("DATA_D",1.0);
    defTable.put("DATA_F",2.0f);
    defTable.put("DATA_X",3L);
    defTable.put("DATA_L",4);
    defTable.put("DATA_I",(short)5);
    defTable.put("DATA_B",(byte)6);
    defTable.put("DATA__L", new Data(7));

    // check that by default all values were saved as Data Objects of the correct type
    checkValuesAreExpectedClassAndType(defTable);


    //** +KeepJavaNumericType results in primitive numeric types being stored in the matching **
    //**      Java wrapper class. Data objects continue to be stored as Data objects.         **
    //** Serialization of this table will result in lower case type letters for Java Numbers  **
    Table numTable = new Table();                // Table with PutDataAsNumber flag
    numTable.setFlags("+KeepJavaNumericType|+CaseSensitive");
    numTable.put("NUM_d",1.0);
    numTable.put("NUM_f",2.0f);
    numTable.put("NUM_x",3L);
    numTable.put("NUM_l",4);
    numTable.put("NUM_i",(short)5);
    numTable.put("NUM_b",(byte)6);
    numTable.put("NUM__L", new Data(7));
    checkValuesAreExpectedClassAndType(numTable);

    //** DEFAULT Re-serialization of Numbers serialized with +KeepJavaNumericType **
    //**        results in values being re-serialized as Number Objects           **
    //**           (Java Numbers are given lower case type letters)               **
    //**        The original Table and the re-serialized Table are EQUAL          **
    Table.setDefaultFlags("+CaseSensitive"); // needed as default flag so used in re-serialization
    Table defaultCopy = new Table(numTable.toString()); // re-serialized Table
    checkValuesAreExpectedClassAndType(defaultCopy);
    assertEquals("Table with Java Numbers can be check for equality with reserialized",numTable,defaultCopy);

    //**     The +KeepJavaNumericType flag has no effect on re-serialization      **
    //**  (still lower case types become Numbers & upper case types Data objects) **
    //**     The original Table and the re-serialized Table are EQUAL          **
    Table.setDefaultFlags("+KeepJavaNumericType");
    Table copyNumTable = new Table(numTable.toString()); // re-serialized Table
    checkValuesAreExpectedClassAndType(copyNumTable);
    assertEquals("Table with Java Numbers can be check for equality with reserialized",numTable,copyNumTable);

    // restore original flags
    Table.setDefaultFlags(prevFlags);

    // Execute tests that use the CoreIO option FavorJavaBehaviorOverTraditionalMidasBehavior
    putJavaObjectsTest();
  }

  public void testModifyKeys () {
    /*
     * Restore dots
     */
    // Replace underscores with dots in an underscore-key table
    Table underscoreTbl = new Table("{FIRST_SECOND={THIRD_FOURTH={ONE_ONE=1.1,GREETING=HELLO}},FIRST=1ST,SECOND=2ND,FIFTH=5TH,"+
        "FIFTH_SIXTH={FIFTH_SIXTH={SIX_SIX=6.6,GREETING=HI}},RELEASE_SERIES={SERIES_NUMBER=4.0.1,TYPE=STABLE}}");
    underscoreTbl.modifyKeys("_",".");
    String expected = "{THIRD.FOURTH={ONE.ONE=D:1.1,GREETING=\"HELLO\"}}";
    assertEquals("Table.restoreDotsInKeys FIRST.SECOND table from underscores",expected,underscoreTbl.getx("FIRST.SECOND").toString());
    expected = "5TH";
    assertEquals("Table.restoreDotsInKeys FIFTH table from underscores",expected,underscoreTbl.getx("FIFTH").toString());
    expected = "{FIFTH.SIXTH={SIX.SIX=D:6.6,GREETING=\"HI\"}}";
    assertEquals("Table.restoreDotsInKeys FIFTH.SIXTH table from underscores",expected,underscoreTbl.getx("FIFTH.SIXTH").toString());

    // Replace dot-brackets (<DOT>) with dots in a dot-bracket-key table
    Table dotBracketTbl = new Table("{FIRST<DOT>SECOND={THIRD<DOT>FOURTH={ONE<DOT>ONE=1.1,GREETING=HELLO}},FIRST=1ST,SECOND=2ND,FIFTH=5TH,"+
        "FIFTH<DOT>SIXTH={FIFTH<DOT>SIXTH={SIX<DOT>SIX=6.6,GREETING=HI}},RELEASE<DOT>SERIES={SERIES<DOT>NUMBER=4.0.1,TYPE=STABLE}}");
    dotBracketTbl.modifyKeys("<DOT>",".");
    expected = "{THIRD.FOURTH={ONE.ONE=D:1.1,GREETING=\"HELLO\"}}";
    assertEquals("Table.restoreDotsInKeys from <DOT>",expected,dotBracketTbl.getx("FIRST.SECOND").toString());
    expected = "5TH";
    assertEquals("Table.restoreDotsInKeys FIFTH table from <DOT>",expected,dotBracketTbl.getx("FIFTH").toString());
    expected = "{FIFTH.SIXTH={SIX.SIX=D:6.6,GREETING=\"HI\"}}";
    assertEquals("Table.restoreDotsInKeys FIFTH.SIXTH table from <DOT>",expected,dotBracketTbl.getx("FIFTH.SIXTH").toString());

    /*
     * Replace dots
     */
    // Dot-key table represented as: {FIRST.SECOND={THIRD.FOURTH={ONE.ONE=1.1,GREETING=HELLO}},FIRST=1ST,SECOND=2ND,FIFTH=5TH",
    //    "FIFTH.SIXTH={FIFTH.SIXTH={SIX.SIX=6.6,GREETING=HI}},RELEASE.SERIES={SERIES.NUMBER=4.0.1,TYPE=STABLE}}

    // Replace dots with underscores in a dot-key table
    Table dotKeyToUnderscoreTbl = makeDotKeyTable();
    dotKeyToUnderscoreTbl.modifyKeys("\\.","_");
    expected = "{THIRD_FOURTH={ONE_ONE=1.1,GREETING=\"HELLO\"}}";
    assertEquals("Table.replaceDotsInKeys FIRST_SECOND table",expected,dotKeyToUnderscoreTbl.getx("FIRST_SECOND").toString());
    expected = "5TH";
    assertEquals("Table.replaceDotsInKeys FIFTH table",expected,dotKeyToUnderscoreTbl.getx("FIFTH").toString());
    expected = "{FIFTH_SIXTH={SIX_SIX=6.6,GREETING=\"HI\"}}";
    assertEquals("Table.replaceDotsInKeys FIFTH_SIXTH table",expected,dotKeyToUnderscoreTbl.getx("FIFTH_SIXTH").toString());

    // Replace dots with <DOT> in a dot-key table
    Table dotKeyToDotBracketTbl = makeDotKeyTable();
    dotKeyToDotBracketTbl.modifyKeys("\\.","<DOT>");
    expected = "{THIRD<DOT>FOURTH={ONE<DOT>ONE=1.1,GREETING=\"HELLO\"}}";
    assertEquals("Table.replaceDotsInKeys FIRST<DOT>SECOND table",expected,dotKeyToDotBracketTbl.getx("FIRST<DOT>SECOND").toString());
    expected = "5TH";
    assertEquals("Table.replaceDotsInKeys FIFTH table",expected,dotKeyToDotBracketTbl.getx("FIFTH").toString());
    expected = "{FIFTH<DOT>SIXTH={SIX<DOT>SIX=6.6,GREETING=\"HI\"}}";
    assertEquals("Table.replaceDotsInKeys FIFTH<DOT>SIXTH table",expected,dotKeyToDotBracketTbl.getx("FIFTH<DOT>SIXTH").toString());
  }

  /** Makes and returns a dot-key Table
   *  @return the dot-key Table
   *  @since NeXtMidas 4.0.1
   */
  private Table makeDotKeyTable() {
    // Make table represented as: {FIRST.SECOND={THIRD.FOURTH={ONE.ONE=1.1,GREETING=HELLO}},FIRST=1ST,SECOND=2ND,FIFTH=5TH",
    //    "FIFTH.SIXTH={FIFTH.SIXTH={SIX.SIX=6.6,GREETING=HI}},RELEASE.SERIES={SERIES.NUMBER=4.0.1,TYPE=STABLE}}
    Table dotKeyTbl = new Table();
    dotKeyTbl.setFlags(Table.ALLOW_DOTS_IN_KEYS);
    Table firstSecond = new Table();
    firstSecond.setFlags(Table.ALLOW_DOTS_IN_KEYS);
    Table thirdFourth = new Table();
    thirdFourth.setFlags(Table.ALLOW_DOTS_IN_KEYS);
    thirdFourth.putx("ONE.ONE", 1.1);
    thirdFourth.putx("GREETING", "HELLO");
    firstSecond.putx("THIRD.FOURTH", thirdFourth);
    dotKeyTbl.putx("FIRST.SECOND",firstSecond);
    dotKeyTbl.putx("FIRST","1ST");
    dotKeyTbl.putx("SECOND","2ND");
    dotKeyTbl.putx("FIFTH","5TH");
    Table fifthSixthOuter = new Table();
    fifthSixthOuter.setFlags(Table.ALLOW_DOTS_IN_KEYS);
    Table fifthSixthInner = new Table();
    fifthSixthInner.setFlags(Table.ALLOW_DOTS_IN_KEYS);
    fifthSixthInner.putx("SIX.SIX", 6.6);
    fifthSixthInner.putx("GREETING", "HI");
    fifthSixthOuter.putx("FIFTH.SIXTH", fifthSixthInner);
    dotKeyTbl.putx("FIFTH.SIXTH", fifthSixthOuter);
    Table releaseSeries = new Table();
    releaseSeries.setFlags(Table.ALLOW_DOTS_IN_KEYS);
    releaseSeries.putx("SERIES.NUMBER", "4.0.1");
    releaseSeries.putx("TYPE","STABLE");
    dotKeyTbl.putx("RELEASE.SERIES", releaseSeries);
    return dotKeyTbl;
  }

  /**
   * Will test that the object types that are put in are correct when the CoreIO option
   * of FavorJavaBehaviorOverTraditionalMidasBehavior is set and when it is not.
   * When it is set, the object should match the type that is sent in.
   * If it is not set, then the object will be of type Data.class
   */
  private void putJavaObjectsTest() {
    // save off current options to reset after test
    String currentOptions = getMidas().io.getOptions();
    // run without favorjavabehavior
    getMidas().io.setOptions("-FavorJavaBehaviorOverTraditionalMidasBehavior");
    Table tbl = new Table();
    String temp = "Test";
    double[] dbls = {1.1,2.2,3.3,4.4};
    float[] floats = {1.00010f,2.000000003f,3.0000020f,4.00001f};
    int[] ints = {10000,200000,300000,400000};
    long[] longs = {Long.MAX_VALUE,Long.MIN_VALUE,9999999999999999l,-9999999999999999l};
    short[] shorts = {1,2,3,4};

    byte[] testStringAsBytes = temp.getBytes();
    // First test should return a data object for adding byte
    for (byte b : testStringAsBytes) {
      tbl.put(String.valueOf((char)b), b);
    }
    for(char letter : temp.toCharArray()) {
      assertEquals("The object should be an instance of Data.class", Data.class, tbl.get(String.valueOf(letter)).getClass());
    }
    // test should return a data object for adding double
    for(int i = 0;i<dbls.length;i++) {
      tbl.put("dbls"+i, dbls[i]);
      assertEquals("The object should be an instance of Data.class", Data.class, tbl.get("dbls"+i).getClass());
      assertEquals("The data object should be equal", new Data(dbls[i]), tbl.get("dbls"+i));
    }

    // test should return a data object for adding float
    for(int i = 0;i<floats.length;i++) {
      tbl.put("floats"+i, floats[i]);
      assertEquals("The object should be an instance of Data.class", Data.class, tbl.get("floats"+i).getClass());
      assertEquals("The data object should be equal", new Data(floats[i]), tbl.get("floats"+i));
    }

    // test should return a data object for adding int
    for(int i = 0;i<ints.length;i++) {
      tbl.put("ints"+i, ints[i]);
      assertEquals("The object should be an instance of Data.class", Data.class, tbl.get("ints"+i).getClass());
      assertEquals("The data object should be equal", new Data(ints[i]), tbl.get("ints"+i));
    }

    // test should return a data object for adding long
    for(int i = 0;i<longs.length;i++) {
      tbl.put("longs"+i, longs[i]);
      assertEquals("The object should be an instance of Data.class", Data.class, tbl.get(String.valueOf("longs"+i)).getClass());
      assertEquals("The data object should be equal", new Data(longs[i]), tbl.get("longs"+i));
    }

    // test should return a data object for adding short
    for(int i = 0;i<shorts.length;i++) {
      tbl.put("shorts"+i, shorts[i]);
      assertEquals("The object shoujld be an instance of Data.class", Data.class, tbl.get("shorts"+i).getClass());
      assertEquals("The data object should be equal", new Data(shorts[i]), tbl.get("shorts"+i));
    }

    Midas M = Shell.getMidasContext();
    if (Shell.isGradleBuild()) {
      M.warning("nxm.sys.junit.lib.ArgsTest skipping testGetD() because fails in Gradle build");
      return;
    }
    // Now run the same tests with +FavorJavaBehaviorOverTraditionalMidasBehavior
    getMidas().io.setOptions("+FavorJavaBehaviorOverTraditionalMidasBehavior");
    tbl = new Table();

    // Test that the object is a byte when added.
    for (byte b : testStringAsBytes) {
      tbl.put(String.valueOf((char)b), b);
    }
    for(char letter : temp.toCharArray()) {
      assertEquals("The class should an instance of Byte.class", Byte.class, tbl.get(String.valueOf(letter)).getClass());
    }
    for(int i = 0;i<dbls.length;i++) {
      tbl.put("dbl"+i, dbls[i]);
      assertEquals("The object should be an instance of Double.class", Double.class, tbl.get("dbl"+i).getClass());
      assertEquals("The value should be equal to what was inserted", dbls[i], tbl.get("dbl"+i));

    }
    // test should return a float object for adding float
    for(int i = 0;i<floats.length;i++) {
      tbl.put("floats"+i, floats[i]);
      assertEquals("The object should be an instance of Float.class", Float.class, tbl.get("floats"+i).getClass());
      assertEquals("The value should be equal to what was inserted", floats[i], tbl.get("floats"+i));
    }
    // test should return a data object for adding int
    for(int i = 0;i<ints.length;i++) {
      tbl.put("ints"+i, ints[i]);
      assertEquals("The object should be an instance of Integer.class", Integer.class, tbl.get("ints"+i).getClass());
      assertEquals("The value should be equal to what was inserted", ints[i], tbl.get("ints"+i));

    }
    // test should return a data object for adding long
    for(int i = 0;i<longs.length;i++) {
      tbl.put("longs"+i, longs[i]);
      assertEquals("The object should be an instance of Long.class", Long.class, tbl.get(String.valueOf("longs"+i)).getClass());
      assertEquals("The value should be equal to what was inserted", longs[i], tbl.get("longs"+i));
    }
    // test should return a data object for adding short
    for(int i = 0;i<shorts.length;i++) {
      tbl.put("shorts"+i, shorts[i]);
      assertEquals("The object shoujld be an instance of Short.class", Short.class, tbl.get("shorts"+i).getClass());
      assertEquals("The value should be equal to what was inserted", shorts[i], tbl.get("shorts"+i));
    }
    // Reset the io options
    getMidas().io.setOptions(currentOptions);

  }

  /**
   * Check that the values in the Table are of the expected class
   * (based on last letter of case sensitive key name)
   * If the expected class is a Data Object, then also check type
   * @param tbl table to be checked
   */
  private static void checkValuesAreExpectedClassAndType (Table tbl) {
    for (String key : tbl) {
      char expType = key.charAt(key.length() - 1);
      Class<?> expClass = Data.class;
      switch (expType) {
        case 'd': expClass = Double.class;  break;
        case 'f': expClass = Float.class;   break;
        case 'x': expClass = Long.class;    break;
        case 'l': expClass = Integer.class; break;
        case 'i': expClass = Short.class;   break;
        case 'b': expClass = Byte.class;    break;
      }
      assertEquals("Table value is of expected Class:"+key, expClass, tbl.get(key).getClass());
      if (expClass.equals(Data.class)) {
        Data dataValue = (Data)tbl.get(key);
        assertEquals("Data value in Table has expected type for key:"+key, expType, dataValue.getFormatTypeChar());
      }
    }
  }

  /**
   * Test the ability to put all data from one table into another table.
   * @see Table#putAll(Map)
   */
  public void testPutAll() {
    for (TableMode mode : MODES) {
      Table t0 = makeSampleTable(mode);
      Table t1 = makeSampleTable(mode);

      Table t4 = makeSampleTable(mode);
      Hashtable<String,Object> ht = new Hashtable<>();

      insertSampleTestData(t4, ht);

      t0.putAll(t4);
      t1.putAll(ht);

      assertTrue(""+mode+": t0.equals(t4)", t0.equals(t4));
      assertTrue(""+mode+": t1.equals(ht)", t0.equals(ht));
    }
  }

  public void testPutx() { }  // tested via testPutAll()

  public void testReadExternal() { }

  /** test case for Bug 1343 for methods (put,remove,clear) on ReadOnly Table requiring FORCE flag. */
  public void testReadOnlyTable() {
    Table readOnlyTable = makeReadOnlyTable();
    int size            = readOnlyTable.size();
    // pre-conditions
    assertTrue ("ReadOnly Table - precond: should contain KEY=TESTKEY",   readOnlyTable.containsKey("TESTKEY"));
    assertFalse("ReadOnly Table - precond: should not contain KEY=NEWKEY",readOnlyTable.containsKey("NEWKEY"));

    // --- test case for Bug 1343 for READONLY tables ---
    info("!!!!! It is ok to see several warning below for Table read only unit tests... !!!!!");
    readOnlyTable.put("NEWKEY", "NEWVALUE"); // this should NOT get put into the ReadOnly table
    assertFalse("ReadOnly Table - put('NEWKEY')",readOnlyTable.containsKey("NEWKEY"));

    readOnlyTable.remove("TESTKEY");         // this should NOT remove key from the ReadOnly table
    assertTrue("ReadOnly Table - remove('TESTKEY')",readOnlyTable.containsKey("TESTKEY"));

    readOnlyTable.clear();                   // this should NOT clear the ReadOnly table
    assertEquals("ReadOnly Table - clear()", size, readOnlyTable.size());

    // use FORCE flags to put, remove, clear from ReadOnly Table
    readOnlyTable.putx("NEWKEY", "NEWVALUE", Table.FORCE);
    assertTrue("ReadOnly Table - put('NEWKEY',FORCE)",readOnlyTable.containsKey("NEWKEY"));

    readOnlyTable.remove("TESTKEY", Table.FORCE);
    assertFalse("ReadOnly Table - remove('TESTKEY',FORCE)",readOnlyTable.containsKey("TESTKEY"));

    readOnlyTable.clear(Table.FORCE);
    assertEquals("ReadOnly Table - clear(FORCE)", 0, readOnlyTable.size());
  }

  /**
   * Test the ability to remove a key/value from a table.
   * @see Table#remove(Object)
   * @see Table#remove(String)
   * @see Table#remove(String, int)
   */
  public void testRemove() {
    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);
      int expSize = table.size() - 2; // below removes two entries

      table.remove("ONE");
      table.remove("TWO");
      table.remove("SIX");
      table.remove("TEN");

      assertEquals("Table size", expSize, table.size());

      assertEquals(""+mode+": getKey('ZERO')",  ZERO,  table.getKey("ZERO"));
      assertEquals(""+mode+": getKey('ONE')",   null,  table.getKey("ONE"));
      assertEquals(""+mode+": getKey('TWO')",   null,  table.getKey("TWO"));
      assertEquals(""+mode+": getKey('THREE')", THREE, table.getKey("THREE"));
      assertEquals(""+mode+": getKey('FOUR')",  FOUR,  table.getKey("FOUR"));
      assertEquals(""+mode+": getKey('SIX')",   null,  table.getKey("SIX"));
      assertEquals(""+mode+": getKey('TEN')",   null,  table.getKey("TEN"));

      /* Test remove(Object) */
      table = makeSampleTable(mode);
      expSize = table.size() - 2; // below removes two entries

      Object oneVal = table.remove("ONE");  // test for 2455
      Object twoVal = table.remove("TWO");  // test for 2455

      assertEquals(""+mode+": remove('ONE')", ONE,  oneVal);
      assertEquals(""+mode+": remove('TWO')", TWO,  twoVal);
      assertEquals(""+mode+": remove('SIX')", null, table.remove((Object)"SIX"));
      assertEquals(""+mode+": remove('TEN')", null, table.remove((Object)"TEN"));

      assertEquals(""+mode+": Table size", expSize, table.size());

      assertEquals(""+mode+": getKey('ZERO')",  ZERO,  table.getKey("ZERO"));
      assertEquals(""+mode+": getKey('ONE')",   null,  table.getKey("ONE"));
      assertEquals(""+mode+": getKey('TWO')",   null,  table.getKey("TWO"));
      assertEquals(""+mode+": getKey('THREE')", THREE, table.getKey("THREE"));
      assertEquals(""+mode+": getKey('FOUR')",  FOUR,  table.getKey("FOUR"));
      assertEquals(""+mode+": getKey('SIX')",   null,  table.getKey("SIX"));
      assertEquals(""+mode+": getKey('TEN')",   null,  table.getKey("TEN"));

      // Bug 1791 Table remove when iterating using Table.enumeration(), Table.more(), Table.next()
      Table tbl = makeSampleTable(mode);
      tbl.enumeration();
      while (tbl.more()) {
        String key = tbl.next();
        tbl.remove(key);
      }
      if (tbl.getModeInt()==Table.KV) {
        warn("SKIPPING "+mode+": Table (enumeration->more->next->remove) KNOWN ISSUE: size="
            + tbl.size() + " expect size=0");
        continue; // NTN: skip test for known issue with KeyVector backed Tables
      }
      assertEquals(""+mode+": Table (enumeration->more->next->remove) size", 0, tbl.size());
    }
  }

  /**
   * Bug 2988 remove Table.Listener test
   * Table.Listener is an interface with putAction and getAction methods.  These methods are called by
   * Table.get() and Table.put() methods.  Global boolean variable, haveListener, is initially set to false
   * indicating no Listener interface has been added to the table and is used to verify the existence or
   * removal of the Listener interface.  To fail the test, set haveListener to false after addListener().
   */
  public void testRemoveListener () {
    Table tbl = new Table();

    Table.Listener listener = new Table.Listener() {
      String testput = null;
      String testget = null;

      @Override
      public boolean putAction (String key, Object value) {
        testput = "putAction() called on key=" + key + " value=" + value;
        assertTrue("testRemoveListener: Table Listener expected. " + testput + " ", haveListener);
        if (key.equalsIgnoreCase("test_has_table_listener")) {
          return true;
        } else {
          // force Table.putx() method to add key/value pair
          return false;
        }
      }

      @Override
      public boolean getAction (String key, Object value) {
        testget = "getAction() called on key= " + key + " value= " + value;
        assertTrue("testRemoveListener: Table Listener expected. " + testput + " ", haveListener);
        return !testget.isEmpty();
      }
    };

    tbl.addListener(listener);
    haveListener=true;
    tbl.putx("test_has_table_listener", "this_listener");
    tbl.get("test_has_table_listener");
    assertNull("testRemoveListener: expected Table Listener to NOT add key/value pair. ", tbl.getKey("test_has_table_listener"));
    tbl.putx("test_false_return",  "value_added");
    assertNotNull("testRemoveListener: expected Table Listener to add key/value pair. ", tbl.getKey("test_false_return"));

    tbl.removeListener(listener);
    haveListener = false;
    tbl.putx("test_no_table_listener", "no_listener");
    assertNotNull("testRemoveListener: expected Table Listener to add key/value pair. ", tbl.getKey("test_no_table_listener"));
    tbl.get("test_no_table_listener");
  }

  /**
   * Test the ability to rename key in table.
   * @see Table#rename(String, String)
   */
  public void testRename () {
    String[] names  = { "ZERO", "ONE", "TWO", "THREE", "FOUR" }; // The initial names
    Object[] values = {  ZERO,   ONE,   TWO,   THREE,   FOUR  }; // The expected values
    String[] keys   = { "ZERO", "UNO", "TWO", "THREE",  "IV"  }; // The new names

    for (TableMode mode : MODES) {
      Table table = makeSimpleSampleTable(mode,0);
      for (int j = 0; j < names.length; j++) {
        table.put(names[j], values[j]);
      }
      table.rename("ONE",  "UNO");
      table.rename("FOUR", "4");
      table.rename("4",    "IV");
      table.rename("ERR",  "OK"); // rename key that does not exist (should be NOOP)

      assertEquals(""+mode+": Table.rename: table size", names.length, table.size());

      for (int j = 0; j < keys.length; j++) {
        assertEquals(""+mode+": Table.rename: value of "+keys[j], values[j], table.getKey(keys[j]));
      }
    }
  }

  /**
   * Tests the ability to set flags on a table
   */
  public void testSetFlags () {
    // test for Bug 1839 to switch Table to KeyVector mode when ADD flag is set
    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);
      table.setFlags("+ADD");
      int expSize = table.size() + 2; // below two setKeys should add two entries (with ADD flag)
      table.setKey("ONE", "value1");
      table.setKey("ONE", "value2");
      assertEquals("Table size after adding duplicate keys ", expSize, table.size());
    }

    // Test setting table flags to contradictory PERMIT_NULL and EXCEPTION_ON_NULL
    // (since 3.7.3 for NXM-3122)
    try {
      Table table = new Table();
      table.setFlags("ExceptionOnNull|PermitNull");
      fail("Table.setFlags should not allow contradictory null related flags");
    }
    catch (IllegalArgumentException iae) {
      // pass
    }

  }

  /**
   * This test the ability to put a key/value in a table.
   * @see Table#setKey(String, Object)
   */
  public void testSetKey () {
    String one = "one";
    String two = "two";
    String six = "six";
    String ten = "ten";

    for (TableMode mode : MODES) {
      Table table = makeSampleTable(mode);
      int expSize = table.size() + 2; // below setKeys should add two entries
      table.setKey("ONE", one);
      table.setKey("TWO", two);
      table.setKey("SIX", six);
      table.setKey("TEN", ten);

      assertEquals("Table size", expSize, table.size());

      assertSame(""+mode+": getKey('ZERO')",  ZERO,  table.getKey("ZERO"));
      assertSame(""+mode+": getKey('ONE')",   one,   table.getKey("ONE"));
      assertSame(""+mode+": getKey('TWO')",   two,   table.getKey("TWO"));
      assertSame(""+mode+": getKey('THREE')", THREE, table.getKey("THREE"));
      assertSame(""+mode+": getKey('FOUR')",  FOUR,  table.getKey("FOUR"));
      assertSame(""+mode+": getKey('SIX')",   six,   table.getKey("SIX"));
      assertSame(""+mode+": getKey('TEN')",   ten,   table.getKey("TEN"));

      assertNull(""+mode+": getKey('FIVE')",  table.getKey("FIVE"));
      assertNull(""+mode+": getKey('SEVEN')", table.getKey("SEVEN"));
      assertNull(""+mode+": getKey('EIGHT')", table.getKey("EIGHT"));
    }
  }

  /**
   * Tests the ability to set the mode on a table.
   * @see Table#setMode(int)
   */
  public void testSetMode () {
    for (TableMode mode1 : MODES) {
      for (TableMode mode2 : MODES) {
        String str;
        Table  table;

        str   = "Changed from "+mode1+" to "+mode2+" setMode("+mode2.modeNum+"),";

        if (mode1.modeNum == Table.FB) {
          table = new Table("{A=1,B=2,C=3}");
        }
        else {
          table = makeSampleTable(mode1);
        }

        table.setMode(mode2.modeNum);
        assertTrue(str+" isMode("+mode2.modeNum+")", table.isMode(mode2.modeNum));
        assertTrue(str+" isMode("+mode2.modeStr+")", table.isMode(mode2.modeStr));
        assertTrue(str+" isMode("+mode2.modeAbr+")", table.isMode(mode2.modeAbr));

        str   = "Changed from "+mode1+" to "+mode2+" setMode("+mode2.modeStr+"),";
        table = makeSampleTable(mode1);
        table.setMode(mode2.modeStr);
        assertTrue(str+" isMode("+mode2.modeNum+")", table.isMode(mode2.modeNum));
        assertTrue(str+" isMode("+mode2.modeStr+")", table.isMode(mode2.modeStr));
        assertTrue(str+" isMode("+mode2.modeAbr+")", table.isMode(mode2.modeAbr));

        str   = "Changed from "+mode1+" to "+mode2+" setMode("+mode2.modeAbr+"),";
        table = makeSampleTable(mode1);
        table.setMode(mode2.modeAbr);
        assertTrue(str+" isMode("+mode2.modeNum+")", table.isMode(mode2.modeNum));
        assertTrue(str+" isMode("+mode2.modeStr+")", table.isMode(mode2.modeStr));
        assertTrue(str+" isMode("+mode2.modeAbr+")", table.isMode(mode2.modeAbr));
      }
    }
    // Test support of setting mode to DEFAULT - since 3.7.3 for NXM-3122
    Table defaultModeTbl = new Table("DEFAULT",null);
    assertEquals("Table mode can be set to default using 'DEFAULT'",Table.SLHM,defaultModeTbl.getModeInt());
    Table defModeTbl = new Table("DEF",null);
    assertEquals("Table mode can be set to default using 'DEF'",Table.SLHM,defModeTbl.getModeInt());
  }

  public void testSetPrevious () { }

  /** Tests that serialization and deserialization works as expected */
  public void testSerializable () {
    Table simple = new Table("{K1=D:1.23,K2=\"TWO\"}");
    testSerializeDeserialize(simple,"Table with String constructor");
    simple.setFlags("+CASESENSITIVE");
    testSerializeDeserialize(simple,"Table with Flag set");
  }

  private void testSerializeDeserialize (Table table, String message) {
    File ramFile = null;
    try {
      ramFile = File.createTempFile("tableserializationtest", "temp");
      FileOutputStream fos = new FileOutputStream(ramFile);
      ObjectOutputStream oos = new ObjectOutputStream(fos);
      oos.writeObject(table);
      oos.flush();
      fos.close();
      FileInputStream fis = new FileInputStream(ramFile);
      ObjectInputStream ois = new ObjectInputStream(fis);
      Table  sfTable    = (Table)ois.readObject();
      fis.close();
      assertEquals("table serialization restores basic table:",table.toString(), sfTable.toString());
      assertEquals("table serialization restores flags:",table.getFlags(), sfTable.getFlags());
    }
    catch (Exception e) {
      fail("Unexpected Exception during serialization:"+e);
    }
    finally {
      ramFile.delete();
    }
  }

  /**
   * Test the ability to get the size of the table correctly.
   * @see Table#size()
   */
  public void testSize() {
    for (TableMode mode : MODES) {
      Table t1 = makeSampleTable(mode);
      Table t2 = new Table(mode.modeNum);
      Table t3 = makeSampleTable(mode);
      Table t4 = makeSampleTable(mode);
      int expSize_t1 = size_sample_table;
      int expSize_t3 = size_sample_table + 2; // below set keys increase size by 2


      t3.setKey("ONE", "one");
      t3.setKey("TWO", "two");
      t3.setKey("SIX", "six");
      t3.setKey("TEN", "ten");

      t4.clear();

      assertEquals(""+mode+": Table 1 size", expSize_t1, t1.size());
      assertEquals(""+mode+": Table 2 size is 0", 0, t2.size());
      assertEquals(""+mode+": Table 3 size", expSize_t3, t3.size());
      assertEquals(""+mode+": Table 4 size is 0", 0, t4.size());
    }
  }

  /**
   * @see #testFromBytes() for test case
   *
   */
  public void testToBytes () { }

  /**
   * The toString method is also tested in testFromBytes(), so this is just
   * adding testing for toString (and toStringBufffer() which toString calls)
   * with and without the SerializeTimeAsTColon flag set
   * @Since 3.5.3 Bug NXM-2876, NXM-2883
   */
  public void testToString () {
    // create table with a table with a Time object value
    Table table = new Table();
    Time timeVal = new Time("1950:01:01::12:30:33.456");
    table.put("KEYTIME", timeVal);
    // check toString with SerializeTimeAsTColon flag true
    table.setFlags("+SerializeTimeAsTColon");
    String tabStringTColon = table.toString();
    String expectedtc = "{KEYTIME=T:1950:01:01::12:30:33.456000000000}"; // FULL_STD with 12 decimal places
    assertEquals("with SerializeTimeAsTColon true:",expectedtc,tabStringTColon);
    // check toString with SerializeTimeAsTColon flag false
    table.setFlags("-SerializeTimeAsTColon");
    String tabString = table.toString();
    String expected = "{KEYTIME=\"12:30:33.456\"}"; // abbreviated to HH:mm:SS.SSS since less than 1 day
    assertEquals("with SerializeTimeAsTColon true:",expected,tabString);

    // Test serialization and deserialization of Tables with BigDecimal and BigInteger,
    // as well as AtomicInteger, AtomicLong, DoubleAccumulator, DoubleAdder,
    // LongAccumulator, and LongAdder NXM-4001
    Table sampleTable = makeSampleTable(MODES[5]);
    sampleTable.put("LARGE_BIGDECIMAL", new BigDecimal("100518E500"));
    sampleTable.put("SMALL_BIGINTEGER", BigInteger.valueOf(100518));
    sampleTable.put("DOUBLEACCUMULATOR", new DoubleAccumulator(Double::sum,Double.MAX_VALUE));
    DoubleAdder doubleAdder = new DoubleAdder(); doubleAdder.add(Double.MAX_VALUE);
    sampleTable.put("DOUBLEADDER", doubleAdder);
    sampleTable.put("LONGACCUMULATOR", new LongAccumulator(Long::sum,Long.MAX_VALUE));
    LongAdder longAdder = new LongAdder(); longAdder.add(Long.MAX_VALUE);
    sampleTable.put("LONGADDER", longAdder);
    String expectedDefault = "{ZERO=0,ONE=1,TWO=2,THREE=3,FOUR=4,BYTE=127,SHORT=-32768,"+
          "INTEGER=2147483647,LONG=-9223372036854775808,FLOAT=3.4028235E38,DOUBLE=4.9E-324,"+
           "BIGINTEGER=9223372036854775807999,BIGDECIMAL=10.1234567890123456789,"+
           "ATOMICINTEGER=-2147483648,ATOMICLONG=9223372036854775807,DATA_X=X:21,DATA_L=L:21,"+
           "DATA_I=I:21,DATA_B=B:21,DATA_D=D:21.0,DATA_F=F:21.0,"+
           "LARGE_BIGDECIMAL=1.00518E+505,SMALL_BIGINTEGER=100518,"+
           "DOUBLEACCUMULATOR=1.7976931348623157E308,DOUBLEADDER=1.7976931348623157E308,"+
           "LONGACCUMULATOR=9223372036854775807,LONGADDER=9223372036854775807}";
    String expectedKeepNumeric = "{ZERO=l:0,ONE=l:1,TWO=l:2,THREE=l:3,FOUR=l:4,BYTE=b:127,SHORT=i:-32768,"+
           "INTEGER=l:2147483647,LONG=x:-9223372036854775808,FLOAT=f:3.4028235E38,DOUBLE=d:4.9E-324,"+
           "BIGINTEGER=n:9223372036854775807999,BIGDECIMAL=d:10.123456789012346,"+
           "ATOMICINTEGER=l:-2147483648,ATOMICLONG=x:9223372036854775807,DATA_X=X:21,DATA_L=L:21,"+
           "DATA_I=I:21,DATA_B=B:21,DATA_D=D:21.0,DATA_F=F:21.0,"+
           "LARGE_BIGDECIMAL=n:1.00518E+505,SMALL_BIGINTEGER=x:100518,"+
           "DOUBLEACCUMULATOR=d:1.7976931348623157E308,DOUBLEADDER=d:1.7976931348623157E308,"+
           "LONGACCUMULATOR=x:9223372036854775807,LONGADDER=x:9223372036854775807}";
    //System.out.println("default table:"+sampleTable);
    assertEquals("default handling BigDecimal & BigInteger:",expectedDefault,sampleTable.toString());
    sampleTable.setFlags("+KeepJavaNumericType");
    String strSampleTable = sampleTable.toString();
    //System.out.println("JavaNumeric table:"+strSampleTable);
    assertEquals("Java Numeric handling BigDecimal & BigInteger:",expectedKeepNumeric,strSampleTable);
    Table newTable = new Table(strSampleTable,Table.KEEP_JAVA_NUMERIC_TYPE);
    Object largeBD = newTable.get("LARGE_BIGDECIMAL");
    Object largeBI = newTable.get("BIGINTEGER");
    //System.out.println("large BigDecimal reserialized:"+largeBD.getClass());
    assertEquals("Java Numeric reserialized large BigDecimal class:",BigDecimal.class,largeBD.getClass());
    //System.out.println("large BigInteger reserialized:"+largeBI.getClass());
    assertEquals("Java Numeric reserialized large BigInteger class:",BigDecimal.class,largeBI.getClass());

  }

  //public void testToStringAbbr() { } // no applicable test

  /** This is heavily tested in testToString
   *  Additional tests added here for:
   *  Bug 2983: if key in hashtable  is NOT a string then a ClassCastException
   *            is thrown in the toStringBuffer method.
   *  NXM-3119: Table.toStringBuffer properly handle File and Path Objects
   * @since NeXtMidas 3.7.0
   */
  public void testToStringBuffer () {
    // test for bug 2938 ClassCastException when key is not a String
     Time t = new Time("2015-09-30::10:20:30");
    Hashtable<Object, String> ht = new Hashtable<>();
    ht.put(t, "time object");
    ht.put("key2", "string key");
    Table tb = new Table();
    tb.put("ok",ht);
    try {
      tb.toStringBuffer();
    } catch (ClassCastException e) {
      fail("Table.toStringBuffer error: " + e);
    }

    // Test for Windows file paths (when Table contains File or Path)
    // (Since 3.7.3 for NXM-3119)
    // adding linux to confirm both platofrms work
    File winFile = new File("C:\\Users\\nxm\\myFile.txt");
    Path winPath = Paths.get("C:\\Users\\nxm\\myPath.txt");
    File linuxFile = new File("/home/nxm/myFile.txt");
    Path linuxPath = Paths.get("/home/nxm/myFile.txt");
    Table tbWin = new Table();
    Table tbLinux = new Table();
    tbWin.put("FILE", winFile);
    tbWin.put("PATH", winPath);
    tbLinux.put("FILE", linuxFile);
    tbLinux.put("PATH", linuxPath);
    String tableStr = tbWin.toStringBuffer().toString();
    Table  tableCopy = new Table(tableStr);
    Table linuxTableCopy = new Table(tbLinux.toStringBuffer().toString());

    // test that File serialization/re-serialization was successful - Do not replace the \\ with / for the file that is in the table. This action should already be completed.
    try {
      File fileCopy = new File((String)tableCopy.get("FILE"));
      assertEquals("Table.toStringBuffer Windows File serialization/deserialization",
          winFile.toString().replace('\\','/'),
          fileCopy.toString().replace('\\','/'));
      assertEquals("Table.toStringBuffer Windows File serialization/deserialization for Linux OS", linuxFile.toString(), tbLinux.get("FILE").toString());
    }
    catch (MidasException me) {
      fail("Table.toStringBuffer Windows File serialization/deserialization:"+me.getMessage());
    }

    // test that Path serialization/re-serialization was successful
    try {
      Path pathCopy = Paths.get((String)tableCopy.get("PATH"));
      assertEquals("Table.toStringBuffer Windows Path serialization/deserialization",
          winPath.toString().replace('\\','/'),
          pathCopy.toString().replace('\\','/'));
      assertEquals("Table.toStringBuffer Windows Path serialization/deserialization",
          linuxPath.toString(),
          Paths.get((String)linuxTableCopy.get("PATH")).toString());
    }
    catch (MidasException me) {
      fail("Table.toStringBuffer Windows Path serialization/deserialization:"+me.getMessage());
    }

    // Test that Table can be serialized with original flags
    // (Since 3.8.0 for NXM-3117)
    Table orig = new Table();
    orig.setFlags("+KeepJavaNumericType");
    orig.put("JAVA_LONG",12345678l);          // value will be added as Java Long since KeepJavaNumericType is set
    orig.put("JAVA_STRING", "line1\nline2");  // value is String with escape character
    // == default behavior is to add DISABLE_ESCAPE_SEQUENCES to Table flags for serialization ==
    String tableStringDisableEscape = "{JAVA_LONG=x:12345678,JAVA_STRING=\"line1\nline2\"}";
    assertEquals("Table.toString() deafault adds disableEscapeSequences to uses original flags",tableStringDisableEscape,orig.toString());
    // ===== use SERIALIZE_WITH_ORIGINAL_FLAGS to force serialization with original flags =======
    orig.setFlags("+SerializeWithOriginalFlags");
    String tableStringOrigFlags = "{JAVA_LONG=x:12345678,JAVA_STRING=\"line1\\nline2\"}";
    assertEquals("Table.toString() +SerializeWithOriginalFlags uses original flags",tableStringOrigFlags,orig.toString());

  }

  public void testToTextFile () { } // partially tested in testPut()

  /**
   * Test the integrity of the values in the Table.
   * This test will create a table and check them against a copy of the table.
   * If they are equal it will pass, if it is not, it will fail.
   */
  public void testValues () {
    Object[] values = {  ZERO,   ONE,   TWO,   THREE,   FOUR  }; // The expected values

    for (TableMode mode : MODES) {
      Table table = makeSimpleSampleTable(mode,0);

      Collection<Object> col  = table.values();
      Iterator<Object>   iter = col.iterator();

      checkArrays(mode, false, null, null, values, toArray(iter));
    }
  }

  //public void testWriteExternal() { }

  /** Tests Jeremy's RMIF scenario where he wants Java Numeric and all caps types */
  public void testJeremyRMIFCase () {
    Midas M = Shell.getMidasContext();
    if (Shell.isGradleBuild()) {
      M.warning("nxm.sys.junit.lib.ArgsTest skipping testGetD() because fails in Gradle build");
      return;
    }
    // save off current options to reset after test
    String currentOptions = getMidas().io.getOptions();
    // run without favorjavabehavior
    getMidas().io.setOptions("FavorJavaBehaviorOverTraditionalMidasBehavior");

    Table table = new Table();
    table.put("DI_ZERO",  new Data (ZERO));
    table.put("NI_ONE",   ONE);
    table.put("DD_TWO",   new Data(2.0));
    table.put("ND_THREE", Double.valueOf(3.0));
    String expectedSerialization = "{DI_ZERO=L:0,NI_ONE=L:1,DD_TWO=D:2.0,ND_THREE=D:3.0}";

    assertEquals("Table.toString() with FavorJavaBehaviorOverTraditionalMidasBehavior:",expectedSerialization,table.toString());

    byte[] tableBytes = table.toBytes();
    Table reserializedTable = new Table();
    reserializedTable.fromBytes(tableBytes);

    String[] expectedClasses = {Integer.class.toString(),Integer.class.toString(),
                                Double.class.toString(),Double.class.toString()};
    String[] actualClasses = {reserializedTable.get("DI_ZERO").getClass().toString(),
                             reserializedTable.get("NI_ONE").getClass().toString(),
                             reserializedTable.get("DD_TWO").getClass().toString(),
                             reserializedTable.get("ND_THREE").getClass().toString()};

    assertEquals("Table reserialization with FavorJavaBehaviorOverTraditionalMidasBehavior all Number classes",
                 Arrays.toString(expectedClasses), Arrays.toString(actualClasses));

    // Reset the io options
    getMidas().io.setOptions(currentOptions);
  }


  //************************************************************************************************
  // Private helper methods
  //************************************************************************************************

  /**
   *  Tests for +DisableConsistentHandlingOfWhiteSpace and -DisableConsistentHandlingOfWhiteSpace
   *  +DisableConsistentHandlingOfWhiteSpace - will do the old behavior that is broken
   *  -DisableConsistentHandlingOfWhiteSpace - will do the new behavior that is fixed
   *  @since NeXtMidas 3.8.1 for NXM-3143
   */
  private void _testTableSpaces () {
    String origFlags = Table.getDefaultFlags();
    Table.setDefaultFlags("-DisableConsistentHandlingOfWhiteSpace");
    String tbl1Val = "{ KEY1= \"FIRST VALUE\", KEY2 =  \"SECOND\", KEY3 = THIRD, KEY4 = 4}";
    String tbl2Val = "{ KEY1 = " +
        "  \"FIRST VALUE\", KEY2 =  " +
        "  \"SECOND\", KEY3 = THIRD, KEY4 = " +
        "  4}";
    String tbl3Val = "{	KEY1	= \"FIRST VALUE\",	KEY2 =	\"SECOND\", KEY3	= THIRD,	KEY4	= 4}";
    String tbl4Val = "{ KEY1 = \"FIRST VALUE\" , KEY2 =  \"SECOND\", KEY3 = THIRD , KEY4 = 4}";

    // Expected table, everything is setup for the base case of a table string.
    String expTableVal = "{KEY1=\"FIRST VALUE\",KEY2=\"SECOND\",KEY3=THIRD,KEY4=4}";

    Table tbl1 = new Table(tbl1Val);
    Table tbl2 = new Table(tbl2Val);
    Table tbl3 = new Table(tbl3Val);
    Table tbl4 = new Table(tbl4Val);
    Table expTable = new Table(expTableVal);
    assertEquals("tbl1 spaces after equals are parsing correctly", expTable, tbl1);
    assertEquals("tbl2 new lines are parsing correctly", expTable, tbl2);
    assertEquals("tbl3 tabs are parsing correctly", expTable, tbl3);
    assertEquals("tbl4 space after value and before comma parsing correctly", expTable, tbl4);

    Table.setDefaultFlags("+DisableConsistentHandlingOfWhiteSpace");
    expTableVal = "{KEY1=\"FIRST VALUE\",KEY2=\"SECOND\",KEY3=THIRD,KEY4=4}";
    tbl1 = new Table(tbl1Val);
    tbl2 = new Table(tbl2Val);
    tbl3 = new Table(tbl3Val);
    tbl4 = new Table(tbl4Val);
    assertNotEquals("tbl1 spaces after equals are parsing incorrectly", expTable, tbl1);
    assertNotEquals("tbl2 new lines are parsing incorrectly", expTable, tbl2);
    assertNotEquals("tbl3 tabs are parsing incorrectly", expTable, tbl3);
    assertNotEquals("tbl4 space after value and before comma parsing incorrectly", expTable, tbl4);
    assertEquals("tbl1 value is old and incorrect value"," \"FIRST VALUE\"", tbl1.get("KEY1"));
    assertEquals("tbl4 value is old and incorrect value"," \"FIRST VALUE\" ", tbl4.get("KEY1")); // This behavior is different than if it was created from the command line or macro


    String nestedTable = "{AUX=RAM,FG=PRM,LAYER={LT=2D},TAG=PENNY}";
    Table nestTab = new Table(nestedTable);
    System.out.println(nestTab.get("LAYER"));
    String expected = "{LT=\"2D\"}";
    assertEquals("get nested table",expected,((Table)nestTab.get("LAYER")).toString());
    Table.setDefaultFlags(origFlags);

  }

  /** Checks arrays of keys and values. */
  private void checkArrays (TableMode tm, boolean ordered, Object[] eKeys, Object[] eVals,
                            java.util.Set<Map.Entry<String,Object>> entrySet) {
    Object[] aKeys = new Object[entrySet.size()];
    Object[] aVals = new Object[entrySet.size()];

    java.util.Iterator<Map.Entry<String,Object>> iter = entrySet.iterator();

    for (int i = 0; i < aKeys.length; i++) {
      Map.Entry<String,Object> e = iter.next();
      aKeys[i] = e.getKey();
      aVals[i] = e.getValue();
    }

    checkArrays(tm, ordered, eKeys, aKeys, eVals, aVals);
  }

  /**
   * Checks arrays of keys and values.
   * @param tm       table mode
   * @param ordered  ordered (NONE, INSERT, SORT)
   * @param eKeys    expected keys
   * @param aKeys    actual keys
   * @param eVals    expected values
   * @param aVals    actual values
   */
  private void checkArrays (TableMode tm, boolean ordered, Object[] eKeys, Object[] aKeys,
                                                           Object[] eVals, Object[] aVals) {
    int eSize = (eKeys != null)? eKeys.length : eVals.length;
    int aSize = (aKeys != null)? aKeys.length : aVals.length;

    assertEquals(""+tm+": actual size equals expected size", eSize, aSize);

    if (!ordered || (tm.ordered == NONE)) {
      boolean[] found = new boolean[eSize];

      if (eKeys != null) {
        for (int i = 0; i < eSize; i++) {
          int index = find(eKeys, aKeys[i]);
          assertTrue(""+tm+": found key "+aKeys[i], (index >= 0));
          assertTrue(""+tm+": unique entry for "+aKeys[i], !found[index]);
          found[index] = true;
          if (eVals != null) {
            assertEquals(""+tm+": correct value for "+aKeys[i], eVals[index], aVals[i]);
          }
        }
      }
      else {
        for (int i = 0; i < eSize; i++) {
          int index = find(eVals, aVals[i]);
          assertTrue(""+tm+": found value "+aVals[i], (index >= 0));
          assertTrue(""+tm+": unique entry for "+aVals[i], !found[index]);
          found[index] = true;
        }
      }
    }
    else {
      for (int i = 0; i < eSize; i++) {
        if (eKeys != null) assertEquals(""+tm+": found key "+aKeys[i]+" in correct order", eKeys[i], aKeys[i]);
        if (eVals != null) assertEquals(""+tm+": found value "+aVals[i]+" in correct order", eVals[i], aVals[i]);
      }
    }
  }

  private int find (Object[] array, Object val) {
    for (int i = 0; i < array.length; i++) {
      if (val.equals(array[i])) return i;
    }
    return -1;
  }

  private Object[] toArray (java.util.Iterator<?> iter) {
    ArrayList<Object> list = new ArrayList<>();

    while (iter.hasNext()) {
      list.add(iter.next());
    }
    return list.toArray();
  }

  private Object[] toArray (java.util.Enumeration<String> e) {
    return Collections.list(e).toArray();
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** Defines a table mode. */
  private static class TableMode {
    final int     modeNum;    // Mode number (e.g. Table.HM)
    final String  modeStr;    // Mode string (e.g. "HashMap")
    final String  modeAbr;    // Mode abbr.  (e.g. "HM")
    final int     modeGen;    // Generic mode
    final boolean modeInt;    // Mode is an interface
    final int     ordered;    // Ordered (NONE, INSERT, SORT)

    public TableMode (int modeNum, String modeStr, String modeAbr, int modeGen, boolean modeInt,
                      int ordered) {
      this.modeNum = modeNum;
      this.modeStr = modeStr;
      this.modeAbr = modeAbr;
      this.modeGen = modeGen;
      this.modeInt = modeInt;
      this.ordered = ordered;
    }

    public String toString () {
      return "mode="+modeAbr;
    }
  }

  @After
  public void tearDown () {
    getMidas().io.setOptions(currentOptions);
  }

}