jdk/test/javax/management/mxbean/MXBeanTest.java
changeset 2 90ce3da70b43
child 831 50f701930577
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/javax/management/mxbean/MXBeanTest.java	Sat Dec 01 00:00:00 2007 +0000
@@ -0,0 +1,707 @@
+/*
+ * Copyright 2005-2006 Sun Microsystems, Inc.  All Rights Reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
+ * CA 95054 USA or visit www.sun.com if you need additional information or
+ * have any questions.
+ */
+
+/*
+ * @test
+ * @bug 6175517 6278707 6318827 6305746 6392303
+ * @summary General MXBean test.
+ * @author Eamonn McManus
+ * @run clean MXBeanTest MerlinMXBean TigerMXBean
+ * @run build MXBeanTest MerlinMXBean TigerMXBean
+ * @run main MXBeanTest
+ */
+
+import java.lang.reflect.Array;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import javax.management.Attribute;
+import javax.management.JMX;
+import javax.management.MBeanAttributeInfo;
+import javax.management.MBeanInfo;
+import javax.management.MBeanOperationInfo;
+import javax.management.MBeanParameterInfo;
+import javax.management.MBeanServer;
+import javax.management.MBeanServerConnection;
+import javax.management.MBeanServerFactory;
+import javax.management.MBeanServerInvocationHandler;
+import javax.management.ObjectName;
+import javax.management.StandardMBean;
+import javax.management.openmbean.ArrayType;
+import javax.management.openmbean.CompositeData;
+import javax.management.openmbean.CompositeDataInvocationHandler;
+import javax.management.openmbean.OpenMBeanAttributeInfo;
+import javax.management.openmbean.OpenMBeanInfo;
+import javax.management.openmbean.OpenMBeanOperationInfo;
+import javax.management.openmbean.OpenMBeanParameterInfo;
+import javax.management.openmbean.OpenType;
+import javax.management.openmbean.SimpleType;
+import javax.management.openmbean.TabularData;
+import javax.management.openmbean.TabularType;
+import javax.management.remote.JMXConnector;
+import javax.management.remote.JMXConnectorFactory;
+import javax.management.remote.JMXConnectorServer;
+import javax.management.remote.JMXConnectorServerFactory;
+import javax.management.remote.JMXServiceURL;
+
+public class MXBeanTest {
+    public static void main(String[] args) throws Exception {
+        testInterface(MerlinMXBean.class, false);
+        testInterface(TigerMXBean.class, false);
+        testInterface(MerlinMXBean.class, true);
+        testInterface(TigerMXBean.class, true);
+        testExplicitMXBean();
+        testSubclassMXBean();
+        testIndirectMXBean();
+
+        if (failures == 0)
+            System.out.println("Test passed");
+        else {
+            System.out.println("TEST FAILURES: " + failures);
+            System.exit(1);
+        }
+    }
+
+    private static int failures = 0;
+
+    public static interface ExplicitMXBean {
+        public int[] getInts();
+    }
+    public static class Explicit implements ExplicitMXBean {
+        public int[] getInts() {
+            return new int[] {1, 2, 3};
+        }
+    }
+    public static class Subclass
+        extends StandardMBean
+        implements ExplicitMXBean {
+        public Subclass() {
+            super(ExplicitMXBean.class, true);
+        }
+
+        public int[] getInts() {
+            return new int[] {1, 2, 3};
+        }
+    }
+    public static interface IndirectInterface extends ExplicitMXBean {}
+    public static class Indirect implements IndirectInterface {
+        public int[] getInts() {
+            return new int[] {1, 2, 3};
+        }
+    }
+
+    private static void testExplicitMXBean() throws Exception {
+        System.out.println("Explicit MXBean test...");
+        MBeanServer mbs = MBeanServerFactory.newMBeanServer();
+        ObjectName on = new ObjectName("test:type=Explicit");
+        Explicit explicit = new Explicit();
+        mbs.registerMBean(explicit, on);
+        testMXBean(mbs, on);
+    }
+
+    private static void testSubclassMXBean() throws Exception {
+        System.out.println("Subclass MXBean test...");
+        MBeanServer mbs = MBeanServerFactory.newMBeanServer();
+        ObjectName on = new ObjectName("test:type=Subclass");
+        Subclass subclass = new Subclass();
+        mbs.registerMBean(subclass, on);
+        testMXBean(mbs, on);
+    }
+
+    private static void testIndirectMXBean() throws Exception {
+        System.out.println("Indirect MXBean test...");
+        MBeanServer mbs = MBeanServerFactory.newMBeanServer();
+        ObjectName on = new ObjectName("test:type=Indirect");
+        Indirect indirect = new Indirect();
+        mbs.registerMBean(indirect, on);
+        testMXBean(mbs, on);
+    }
+
+    private static void testMXBean(MBeanServer mbs, ObjectName on)
+            throws Exception {
+        MBeanInfo mbi = mbs.getMBeanInfo(on);
+        MBeanAttributeInfo[] attrs = mbi.getAttributes();
+        int nattrs = attrs.length;
+        if (mbi.getAttributes().length != 1)
+            failure("wrong number of attributes: " + attrs);
+        else {
+            MBeanAttributeInfo mbai = attrs[0];
+            if (mbai.getName().equals("Ints")
+                && mbai.isReadable() && !mbai.isWritable()
+                && mbai.getDescriptor().getFieldValue("openType")
+                    .equals(new ArrayType(SimpleType.INTEGER, true))
+                && attrs[0].getType().equals("[I"))
+                success("MBeanAttributeInfo");
+            else
+                failure("MBeanAttributeInfo: " + mbai);
+        }
+
+        int[] ints = (int[]) mbs.getAttribute(on, "Ints");
+        if (equal(ints, new int[] {1, 2, 3}, null))
+            success("getAttribute");
+        else
+            failure("getAttribute: " + Arrays.toString(ints));
+
+        ExplicitMXBean proxy =
+            JMX.newMXBeanProxy(mbs, on, ExplicitMXBean.class);
+        int[] pints = proxy.getInts();
+        if (equal(pints, new int[] {1, 2, 3}, null))
+            success("getAttribute through proxy");
+        else
+            failure("getAttribute through proxy: " + Arrays.toString(pints));
+    }
+
+    private static class NamedMXBeans extends HashMap<ObjectName, Object> {
+        private static final long serialVersionUID = 0;
+
+        NamedMXBeans(MBeanServerConnection mbsc) {
+            this.mbsc = mbsc;
+        }
+
+        MBeanServerConnection getMBeanServerConnection() {
+            return mbsc;
+        }
+
+        private final MBeanServerConnection mbsc;
+    }
+
+    /* This is the core of the test.  Given the MXBean interface c, we
+       make an MXBean object that implements that interface by
+       constructing a dynamic proxy.  If the interface defines an
+       attribute Foo (with getFoo and setFoo methods), then it must
+       also contain a field (constant) Foo of the same type, and a
+       field (constant) FooType that is an OpenType.  The field Foo is
+       a reference value for this case.  We check that the attribute
+       does indeed have the given OpenType.  The dynamically-created
+       MXBean will return the reference value from the getFoo()
+       method, and we check that that value survives the mapping to
+       open values and back when the attribute is accessed through an
+       MXBean proxy.  The MXBean will also check in its setFoo method
+       that the value being set is equal to the reference value, which
+       tests that the mapping and unmapping also works in the other
+       direction.  The interface should define an operation opFoo with
+       two parameters and a return value all of the same type as the
+       attribute.  The MXBean will check that the two parameters are
+       equal to the reference value, and will return that value.  The
+       test checks that calling the operation through an MXBean proxy
+       returns the reference value, again after mapping to and back
+       from open values.
+
+       If any field (constant) in the MXBean interface has a name that
+       ends with ObjectName, say FooObjectName, then its value must be
+       a String containing an ObjectName value.  There must be a field
+       (constant) called Foo that is a valid MXBean, and that MXBean
+       will be registered in the MBean Server with the given name before
+       the test starts.  This enables us to test that inter-MXBean
+       references are correctly converted to ObjectNames and back.
+     */
+    private static <T> void testInterface(Class<T> c, boolean nullTest)
+            throws Exception {
+
+        System.out.println("Testing " + c.getName() +
+                           (nullTest ? " for null values" : "") + "...");
+
+        MBeanServer mbs = MBeanServerFactory.newMBeanServer();
+
+        JMXServiceURL url = new JMXServiceURL("rmi", null, 0);
+        JMXConnectorServer cs =
+            JMXConnectorServerFactory.newJMXConnectorServer(url, null, mbs);
+        cs.start();
+        JMXServiceURL addr = cs.getAddress();
+        JMXConnector cc = JMXConnectorFactory.connect(addr);
+        MBeanServerConnection mbsc = cc.getMBeanServerConnection();
+
+        NamedMXBeans namedMXBeans = new NamedMXBeans(mbsc);
+        InvocationHandler ih =
+            nullTest ? new MXBeanNullImplInvocationHandler(c, namedMXBeans) :
+                       new MXBeanImplInvocationHandler(c, namedMXBeans);
+        T impl = c.cast(Proxy.newProxyInstance(c.getClassLoader(),
+                                               new Class[] {c},
+                                               ih));
+        ObjectName on = new ObjectName("test:type=" + c.getName());
+        mbs.registerMBean(impl, on);
+
+        System.out.println("Register any MXBeans...");
+
+        Field[] fields = c.getFields();
+        for (Field field : fields) {
+            String n = field.getName();
+            if (n.endsWith("ObjectName")) {
+                String objectNameString = (String) field.get(null);
+                String base = n.substring(0, n.length() - 10);
+                Field f = c.getField(base);
+                Object mxbean = f.get(null);
+                ObjectName objectName =
+                    ObjectName.getInstance(objectNameString);
+                mbs.registerMBean(mxbean, objectName);
+                namedMXBeans.put(objectName, mxbean);
+            }
+        }
+
+        try {
+            testInterface(c, mbsc, on, namedMXBeans, nullTest);
+        } finally {
+            try {
+                cc.close();
+            } finally {
+                cs.stop();
+            }
+        }
+    }
+
+    private static <T> void testInterface(Class<T> c,
+                                          MBeanServerConnection mbsc,
+                                          ObjectName on,
+                                          NamedMXBeans namedMXBeans,
+                                          boolean nullTest)
+            throws Exception {
+
+        System.out.println("Type check...");
+
+        MBeanInfo mbi = mbsc.getMBeanInfo(on);
+        MBeanAttributeInfo[] mbais = mbi.getAttributes();
+        for (int i = 0; i < mbais.length; i++) {
+            MBeanAttributeInfo mbai = mbais[i];
+            String name = mbai.getName();
+            Field typeField = c.getField(name + "Type");
+            OpenType typeValue = (OpenType) typeField.get(null);
+            OpenType openType =
+                (OpenType) mbai.getDescriptor().getFieldValue("openType");
+            if (typeValue.equals(openType))
+                success("attribute " + name);
+            else {
+                final String msg =
+                    "Wrong type attribute " + name + ": " +
+                    openType + " should be " + typeValue;
+                failure(msg);
+            }
+        }
+
+        MBeanOperationInfo[] mbois = mbi.getOperations();
+        for (int i = 0; i < mbois.length; i++) {
+            MBeanOperationInfo mboi = mbois[i];
+            String oname = mboi.getName();
+            if (!oname.startsWith("op"))
+                throw new Error();
+            OpenType retType =
+                (OpenType) mboi.getDescriptor().getFieldValue("openType");
+            MBeanParameterInfo[] params = mboi.getSignature();
+            MBeanParameterInfo p1i = params[0];
+            MBeanParameterInfo p2i = params[1];
+            OpenType p1Type =
+                (OpenType) p1i.getDescriptor().getFieldValue("openType");
+            OpenType p2Type =
+                (OpenType) p2i.getDescriptor().getFieldValue("openType");
+            if (!retType.equals(p1Type) || !p1Type.equals(p2Type)) {
+                final String msg =
+                    "Parameter and return open types should all be same " +
+                    "but are not: " + retType + " " + oname + "(" + p1Type +
+                    ", " + p2Type + ")";
+                failure(msg);
+                continue;
+            }
+            String name = oname.substring(2);
+            Field typeField = c.getField(name + "Type");
+            OpenType typeValue = (OpenType) typeField.get(null);
+            if (typeValue.equals(retType))
+                success("operation " + oname);
+            else {
+                final String msg =
+                    "Wrong type operation " + oname + ": " +
+                    retType + " should be " + typeValue;
+                failure(msg);
+            }
+        }
+
+
+        System.out.println("Mapping check...");
+
+        Object proxy =
+            JMX.newMXBeanProxy(mbsc, on, c);
+
+        Method[] methods = c.getMethods();
+        for (int i = 0; i < methods.length; i++) {
+            final Method method = methods[i];
+            if (method.getDeclaringClass() != c)
+                continue; // skip hashCode() etc inherited from Object
+            final String mname = method.getName();
+            final int what = getType(method);
+            final String name = getName(method);
+            final Field refField = c.getField(name);
+            if (nullTest && refField.getType().isPrimitive())
+                continue;
+            final Field openTypeField = c.getField(name + "Type");
+            final OpenType openType = (OpenType) openTypeField.get(null);
+            final Object refValue = nullTest ? null : refField.get(null);
+            Object setValue = refValue;
+            try {
+                Field onField = c.getField(name + "ObjectName");
+                String refName = (String) onField.get(null);
+                ObjectName refObjName = ObjectName.getInstance(refName);
+                Class<?> mxbeanInterface = refField.getType();
+                setValue = nullTest ? null :
+                    JMX.newMXBeanProxy(mbsc, refObjName, mxbeanInterface);
+            } catch (Exception e) {
+                // no xObjectName field, setValue == refValue
+            }
+            boolean ok = true;
+            try {
+                switch (what) {
+                case GET:
+                    final Object gotOpen = mbsc.getAttribute(on, name);
+                    if (nullTest) {
+                        if (gotOpen != null) {
+                            failure(mname + " got non-null value " +
+                                    gotOpen);
+                            ok = false;
+                        }
+                    } else if (!openType.isValue(gotOpen)) {
+                        if (gotOpen instanceof TabularData) {
+                            // detail the mismatch
+                            TabularData gotTabular = (TabularData) gotOpen;
+                            compareTabularType((TabularType) openType,
+                                               gotTabular.getTabularType());
+                        }
+                        failure(mname + " got open data " + gotOpen +
+                                " not valid for open type " + openType);
+                        ok = false;
+                    }
+                    final Object got = method.invoke(proxy, (Object[]) null);
+                    if (!equal(refValue, got, namedMXBeans)) {
+                        failure(mname + " got " + string(got) +
+                                ", should be " + string(refValue));
+                        ok = false;
+                    }
+                    break;
+
+                case SET:
+                    method.invoke(proxy, new Object[] {setValue});
+                    break;
+
+                case OP:
+                    final Object opped =
+                        method.invoke(proxy, new Object[] {setValue, setValue});
+                    if (!equal(refValue, opped, namedMXBeans)) {
+                        failure(
+                                mname + " got " + string(opped) +
+                                ", should be " + string(refValue)
+                                );
+                        ok = false;
+                    }
+                    break;
+
+                default:
+                    throw new Error();
+                }
+
+                if (ok)
+                    success(mname);
+
+            } catch (Exception e) {
+                failure(mname, e);
+            }
+        }
+    }
+
+
+    private static void success(String what) {
+        System.out.println("OK: " + what);
+    }
+
+    private static void failure(String what) {
+        System.out.println("FAILED: " + what);
+        failures++;
+    }
+
+    private static void failure(String what, Exception e) {
+        System.out.println("FAILED WITH EXCEPTION: " + what);
+        e.printStackTrace(System.out);
+        failures++;
+    }
+
+    private static class MXBeanImplInvocationHandler
+            implements InvocationHandler {
+        MXBeanImplInvocationHandler(Class intf, NamedMXBeans namedMXBeans) {
+            this.intf = intf;
+            this.namedMXBeans = namedMXBeans;
+        }
+
+        public Object invoke(Object proxy, Method method, Object[] args)
+                throws Throwable {
+            final String mname = method.getName();
+            final int what = getType(method);
+            final String name = getName(method);
+            final Field refField = intf.getField(name);
+            final Object refValue = getRefValue(refField);
+
+            switch (what) {
+            case GET:
+                assert args == null;
+                return refValue;
+
+            case SET:
+                assert args.length == 1;
+                Object setValue = args[0];
+                if (!equal(refValue, setValue, namedMXBeans)) {
+                    final String msg =
+                        mname + "(" + string(setValue) +
+                        ") does not match ref: " + string(refValue);
+                    throw new IllegalArgumentException(msg);
+                }
+                return null;
+
+            case OP:
+                assert args.length == 2;
+                Object arg1 = args[0];
+                Object arg2 = args[1];
+                if (!equal(arg1, arg2, namedMXBeans)) {
+                    final String msg =
+                        mname + "(" + string(arg1) + ", " + string(arg2) +
+                        "): args not equal";
+                    throw new IllegalArgumentException(msg);
+                }
+                if (!equal(refValue, arg1, namedMXBeans)) {
+                    final String msg =
+                        mname + "(" + string(arg1) + ", " + string(arg2) +
+                        "): args do not match ref: " + string(refValue);
+                    throw new IllegalArgumentException(msg);
+                }
+                return refValue;
+            default:
+                throw new Error();
+            }
+        }
+
+        Object getRefValue(Field refField) throws Exception {
+            return refField.get(null);
+        }
+
+        private final Class intf;
+        private final NamedMXBeans namedMXBeans;
+    }
+
+    private static class MXBeanNullImplInvocationHandler
+            extends MXBeanImplInvocationHandler {
+        MXBeanNullImplInvocationHandler(Class intf, NamedMXBeans namedMXBeans) {
+            super(intf, namedMXBeans);
+        }
+
+        @Override
+        Object getRefValue(Field refField) throws Exception {
+            Class<?> type = refField.getType();
+            if (type.isPrimitive())
+                return super.getRefValue(refField);
+            else
+                return null;
+        }
+    }
+
+
+    private static final String[] prefixes = {
+        "get", "set", "op",
+    };
+    private static final int GET = 0, SET = 1, OP = 2;
+
+    private static String getName(Method m) {
+        return getName(m.getName());
+    }
+
+    private static String getName(String n) {
+        for (int i = 0; i < prefixes.length; i++) {
+            if (n.startsWith(prefixes[i]))
+                return n.substring(prefixes[i].length());
+        }
+        throw new Error();
+    }
+
+    private static int getType(Method m) {
+        return getType(m.getName());
+    }
+
+    private static int getType(String n) {
+        for (int i = 0; i < prefixes.length; i++) {
+            if (n.startsWith(prefixes[i]))
+                return i;
+        }
+        throw new Error();
+    }
+
+    static boolean equal(Object o1, Object o2, NamedMXBeans namedMXBeans) {
+        if (o1 == o2)
+            return true;
+        if (o1 == null || o2 == null)
+            return false;
+        if (o1.getClass().isArray()) {
+            if (!o2.getClass().isArray())
+                return false;
+            return deepEqual(o1, o2, namedMXBeans);
+        }
+        if (o1 instanceof CompositeData && o2 instanceof CompositeData) {
+            return compositeDataEqual((CompositeData) o1, (CompositeData) o2,
+                                      namedMXBeans);
+        }
+        if (Proxy.isProxyClass(o1.getClass())) {
+            if (Proxy.isProxyClass(o2.getClass()))
+                return proxyEqual(o1, o2, namedMXBeans);
+            InvocationHandler ih = Proxy.getInvocationHandler(o1);
+//            if (ih instanceof MXBeanInvocationHandler) {
+//                return proxyEqualsObject((MXBeanInvocationHandler) ih,
+//                                         o2, namedMXBeans);
+            if (ih instanceof MBeanServerInvocationHandler) {
+                return true;
+            } else if (ih instanceof CompositeDataInvocationHandler) {
+                return o2.equals(o1);
+                // We assume the other object has a reasonable equals method
+            }
+        } else if (Proxy.isProxyClass(o2.getClass()))
+            return equal(o2, o1, namedMXBeans);
+        return o1.equals(o2);
+    }
+
+    // We'd use Arrays.deepEquals except we want the test to work on 1.4
+    // Note this code assumes no selfreferential arrays
+    // (as does Arrays.deepEquals)
+    private static boolean deepEqual(Object a1, Object a2,
+                                     NamedMXBeans namedMXBeans) {
+        int len = Array.getLength(a1);
+        if (len != Array.getLength(a2))
+            return false;
+        for (int i = 0; i < len; i++) {
+            Object e1 = Array.get(a1, i);
+            Object e2 = Array.get(a2, i);
+            if (!equal(e1, e2, namedMXBeans))
+                return false;
+        }
+        return true;
+    }
+
+    // This is needed to work around a bug (5095277)
+    // in CompositeDataSupport.equals
+    private static boolean compositeDataEqual(CompositeData cd1,
+                                              CompositeData cd2,
+                                              NamedMXBeans namedMXBeans) {
+        if (cd1 == cd2)
+            return true;
+        if (!cd1.getCompositeType().equals(cd2.getCompositeType()))
+            return false;
+        Collection v1 = cd1.values();
+        Collection v2 = cd2.values();
+        if (v1.size() != v2.size())
+            return false; // should not happen
+        for (Iterator i1 = v1.iterator(), i2 = v2.iterator();
+             i1.hasNext(); ) {
+            if (!equal(i1.next(), i2.next(), namedMXBeans))
+                return false;
+        }
+        return true;
+    }
+
+    // Also needed for 5095277
+    private static boolean proxyEqual(Object proxy1, Object proxy2,
+                                      NamedMXBeans namedMXBeans) {
+        if (proxy1.getClass() != proxy2.getClass())
+            return proxy1.equals(proxy2);
+        InvocationHandler ih1 = Proxy.getInvocationHandler(proxy1);
+        InvocationHandler ih2 = Proxy.getInvocationHandler(proxy2);
+        if (!(ih1 instanceof CompositeDataInvocationHandler)
+            || !(ih2 instanceof CompositeDataInvocationHandler))
+            return proxy1.equals(proxy2);
+        CompositeData cd1 =
+            ((CompositeDataInvocationHandler) ih1).getCompositeData();
+        CompositeData cd2 =
+            ((CompositeDataInvocationHandler) ih2).getCompositeData();
+        return compositeDataEqual(cd1, cd2, namedMXBeans);
+    }
+
+//    private static boolean proxyEqualsObject(MXBeanInvocationHandler ih,
+//                                             Object o,
+//                                             NamedMXBeans namedMXBeans) {
+//        if (namedMXBeans.getMBeanServerConnection() !=
+//            ih.getMBeanServerConnection())
+//            return false;
+//
+//        ObjectName on = ih.getObjectName();
+//        Object named = namedMXBeans.get(on);
+//        if (named == null)
+//            return false;
+//        return (o == named && ih.getMXBeanInterface().isInstance(named));
+//    }
+
+    /* I wanted to call this method toString(Object), but oddly enough
+       this meant that I couldn't call it from the inner class
+       MXBeanImplInvocationHandler, because the inherited Object.toString()
+       prevented that.  Surprising behaviour.  */
+    static String string(Object o) {
+        if (o == null)
+            return "null";
+        if (o instanceof String)
+            return '"' + (String) o + '"';
+        if (o instanceof Collection)
+            return deepToString((Collection) o);
+        if (o.getClass().isArray())
+            return deepToString(o);
+        return o.toString();
+    }
+
+    private static String deepToString(Object o) {
+        StringBuffer buf = new StringBuffer();
+        buf.append("[");
+        int len = Array.getLength(o);
+        for (int i = 0; i < len; i++) {
+            if (i > 0)
+                buf.append(", ");
+            Object e = Array.get(o, i);
+            buf.append(string(e));
+        }
+        buf.append("]");
+        return buf.toString();
+    }
+
+    private static String deepToString(Collection c) {
+        return deepToString(c.toArray());
+    }
+
+    private static void compareTabularType(TabularType t1, TabularType t2) {
+        if (t1.equals(t2)) {
+            System.out.println("same tabular type");
+            return;
+        }
+        if (t1.getClassName().equals(t2.getClassName()))
+            System.out.println("same class name");
+        if (t1.getDescription().equals(t2.getDescription()))
+            System.out.println("same description");
+        else {
+            System.out.println("t1 description: " + t1.getDescription());
+            System.out.println("t2 description: " + t2.getDescription());
+        }
+        if (t1.getIndexNames().equals(t2.getIndexNames()))
+            System.out.println("same index names");
+        if (t1.getRowType().equals(t2.getRowType()))
+            System.out.println("same row type");
+    }
+}