FileDocCategorySizeDatePackage
OpenConverter.javaAPI DocJava SE 6 API56824Tue Jun 10 00:22:04 BST 2008com.sun.jmx.mbeanserver

OpenConverter.java

/*
 * @(#)OpenConverter.java	1.20 06/06/20
 * 
 * Copyright 2006 Sun Microsystems, Inc. All rights reserved.
 * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */

package com.sun.jmx.mbeanserver;

import static com.sun.jmx.mbeanserver.Util.*;

import com.sun.jmx.remote.util.EnvHelp;

import java.beans.ConstructorProperties;
import java.io.InvalidObjectException;
import java.lang.ref.WeakReference;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.WeakHashMap;
import javax.management.JMX;
import javax.management.ObjectName;
import javax.management.openmbean.ArrayType;
import javax.management.openmbean.CompositeData;
import javax.management.openmbean.CompositeDataInvocationHandler;
import javax.management.openmbean.CompositeDataSupport;
import javax.management.openmbean.CompositeDataView;
import javax.management.openmbean.CompositeType;
import javax.management.openmbean.OpenDataException;
import javax.management.openmbean.OpenType;
import javax.management.openmbean.SimpleType;
import javax.management.openmbean.TabularData;
import javax.management.openmbean.TabularDataSupport;
import javax.management.openmbean.TabularType;
import static javax.management.openmbean.SimpleType.*;

/**
   <p>A converter between Java types and the limited set of classes
   defined by Open MBeans.</p>

   <p>A Java type is an instance of java.lang.reflect.Type.  For our
   purposes, it is either a Class, such as String.class or int.class;
   or a ParameterizedType, such as List<String> or Map<Integer,
   String[]>.  On J2SE 1.4 and earlier, it can only be a Class.</p>

   <p>Each Type is associated with an OpenConverter.  The
   OpenConverter defines an OpenType corresponding to the Type, plus a
   Java class corresponding to the OpenType.  For example:</p>

   <pre>
   Type                     Open class     OpenType
   ----                     ----------     --------
   Integer		    Integer	   SimpleType.INTEGER
   int			    int 	   SimpleType.INTEGER
   Integer[]		    Integer[]	   ArrayType(1, SimpleType.INTEGER)
   int[]		    Integer[]	   ArrayType(SimpleType.INTEGER, true)
   String[][]		    String[][]	   ArrayType(2, SimpleType.STRING)
   List<String>		    String[]	   ArrayType(1, SimpleType.STRING)
   ThreadState (an Enum)    String         SimpleType.STRING
   Map<Integer, String[]>   TabularData	   TabularType(
   			    		     CompositeType(
					       {"key", SimpleType.INTEGER},
					       {"value",
					         ArrayType(1,
						  SimpleType.STRING)}),
					     indexNames={"key"})
   </pre>

   <p>Apart from simple types, arrays, and collections, Java types are
   converted through introspection into CompositeType.  The Java type
   must have at least one getter (method such as "int getSize()" or
   "boolean isBig()"), and we must be able to deduce how to
   reconstruct an instance of the Java class from the values of the
   getters using one of various heuristics.</p>

   @since 1.6
 */
public abstract class OpenConverter {
    private OpenConverter(Type targetType, OpenType openType,
			  Class openClass) {
	this.targetType = targetType;
	this.openType = openType;
	this.openClass = openClass;
    }

    /** <p>Convert an instance of openClass into an instance of targetType. */
    public final Object fromOpenValue(MXBeanLookup lookup, Object value)
            throws InvalidObjectException {
        if (value == null)
            return null;
        else
            return fromNonNullOpenValue(lookup, value);
    }
    
    abstract Object fromNonNullOpenValue(MXBeanLookup lookup, Object value)
            throws InvalidObjectException;

    /** <p>Throw an appropriate InvalidObjectException if we will not be able
        to convert back from the open data to the original Java object.</p> */
    void checkReconstructible() throws InvalidObjectException {
        // subclasses override if action necessary
    }

    /** <p>Convert an instance of targetType into an instance of openClass. */
    final Object toOpenValue(MXBeanLookup lookup, Object value)
            throws OpenDataException {
        if (value == null)
            return null;
        else
            return toNonNullOpenValue(lookup, value);
    }
    
    abstract Object toNonNullOpenValue(MXBeanLookup lookup, Object value)
            throws OpenDataException;

    /** <p>True if and only if this OpenConverter's toOpenValue and fromOpenValue
	methods are the identity function.</p> */
    boolean isIdentity() {
	return false;
    }

    /** <p>True if and only if isIdentity() and even an array of the underlying type
       is transformed as the identity.  This is true for Integer and
       ObjectName, for instance, but not for int.</p> */
    final Type getTargetType() {
	return targetType;
    }

    final OpenType getOpenType() {
	return openType;
    }

    /* The Java class corresponding to getOpenType().  This is the class
       named by getOpenType().getClassName(), except that it may be a
       primitive type or an array of primitive type.  */
    final Class getOpenClass() {
	return openClass;
    }

    private final Type targetType;
    private final OpenType openType;
    private final Class openClass;
    
    private static final class ConverterMap
        extends WeakHashMap<Type, WeakReference<OpenConverter>> {}

    private static final ConverterMap converterMap = new ConverterMap();

    /** Following List simply serves to keep a reference to predefined
        OpenConverters so they don't get garbage collected. */
    private static final List<OpenConverter> permanentConverters = newList();

    private static synchronized OpenConverter getConverter(Type type) {
        WeakReference<OpenConverter> wr = converterMap.get(type);
        return (wr == null) ? null : wr.get();
    }

    private static synchronized void putConverter(Type type,
                                                  OpenConverter conv) {
        WeakReference<OpenConverter> wr =
            new WeakReference<OpenConverter>(conv);
        converterMap.put(type, wr);
    }
    
    private static synchronized void putPermanentConverter(Type type,
                                                           OpenConverter conv) {
        putConverter(type, conv);
        permanentConverters.add(conv);
    }

    static {
	/* Set up the mappings for Java types that map to SimpleType.  */

	final OpenType[] simpleTypes = {
	    BIGDECIMAL, BIGINTEGER, BOOLEAN, BYTE, CHARACTER, DATE,
	    DOUBLE, FLOAT, INTEGER, LONG, OBJECTNAME, SHORT, STRING,
	    VOID,
	};

	for (int i = 0; i < simpleTypes.length; i++) {
	    final OpenType t = simpleTypes[i];
	    Class c;
	    try {
		c = Class.forName(t.getClassName(), false,
				  ObjectName.class.getClassLoader());
	    } catch (ClassNotFoundException e) {
		// the classes that these predefined types declare must exist!
		throw new Error(e);
	    }
	    final OpenConverter conv = new IdentityConverter(c, t, c);
            putPermanentConverter(c, conv);

            if (c.getName().startsWith("java.lang.")) {
		try {
		    final Field typeField = c.getField("TYPE");
		    final Class primitiveType = (Class) typeField.get(null);
		    final OpenConverter primitiveConv =
			new IdentityConverter(primitiveType, t, primitiveType);
                    putPermanentConverter(primitiveType,
                                          primitiveConv);
                    if (primitiveType != void.class) {
                        final Class primitiveArrayType =
                            Array.newInstance(primitiveType, 0).getClass();
                        final OpenType primitiveArrayOpenType =
                            ArrayType.getPrimitiveArrayType(primitiveArrayType);
                        final OpenConverter primitiveArrayConv =
                            new IdentityConverter(primitiveArrayType,
                                                  primitiveArrayOpenType,
                                                  primitiveArrayType);
                        putPermanentConverter(primitiveArrayType,
                                              primitiveArrayConv);
                    }
		} catch (NoSuchFieldException e) {
		    // OK: must not be a primitive wrapper
		} catch (IllegalAccessException e) {
		    // Should not reach here
		    assert(false);
		}
	    }
	}
    }

    /** Get the converter for the given Java type, creating it if necessary. */
    public static synchronized OpenConverter toConverter(Type objType)
	    throws OpenDataException {
        
        if (inProgress.containsKey(objType))
            throw new OpenDataException("Recursive data structure");
        
        OpenConverter conv;

	conv = getConverter(objType);
	if (conv != null)
            return conv;
        
        inProgress.put(objType, objType);
        try {
            conv = makeConverter(objType);
        } finally {
            inProgress.remove(objType);
        }

	putConverter(objType, conv);
        return conv;
    }
    
    private static OpenConverter makeConverter(Type objType)
            throws OpenDataException {
        
	/* It's not yet worth formalizing these tests by having for example
	   an array of factory classes, each of which says whether it
	   recognizes the Type (Chain of Responsibility pattern).  */
        if (objType instanceof GenericArrayType) {
            Type componentType =
                ((GenericArrayType) objType).getGenericComponentType();
            return makeArrayOrCollectionConverter(objType, componentType);
        } else if (objType instanceof Class) {
            Class objClass = (Class<?>) objType;
            if (objClass.isEnum()) {
                return makeEnumConverter(objClass);
            } else if (objClass.isArray()) {
                Type componentType = objClass.getComponentType();
                return makeArrayOrCollectionConverter(objClass, componentType);
            } else if (JMX.isMXBeanInterface(objClass)) {
                return makeMXBeanConverter(objClass);
            } else {
                return makeCompositeConverter(objClass);
            }
        } else if (objType instanceof ParameterizedType) {
            return makeParameterizedConverter((ParameterizedType) objType);
        } else
            throw new OpenDataException("Cannot map type: " + objType);
    }

    private static <T extends Enum<T>> OpenConverter
	    makeEnumConverter(Class<T> enumClass) {
	return new EnumConverter<T>(enumClass);
    }

    /* Make the converter for an array type, or a collection such as
     * List<String> or Set<Integer>.  We never see one-dimensional
     * primitive arrays (e.g. int[]) here because they use the identity
     * converter and are registered as such in the static initializer.
     */
    private static OpenConverter
	makeArrayOrCollectionConverter(Type collectionType, Type elementType)
	    throws OpenDataException {

        final OpenConverter elementConverter = toConverter(elementType);
	final OpenType elementOpenType = elementConverter.getOpenType();
	final ArrayType openType = new ArrayType(1, elementOpenType);
	final Class elementOpenClass = elementConverter.getOpenClass();

	final Class openArrayClass;
        final String openArrayClassName;
        if (elementOpenClass.isArray())
            openArrayClassName = "[" + elementOpenClass.getName();
        else
            openArrayClassName = "[L" + elementOpenClass.getName() + ";";
        try {
            openArrayClass = Class.forName(openArrayClassName);
        } catch (ClassNotFoundException e) {
            throw openDataException("Cannot obtain array class", e);
        }

	if (collectionType instanceof ParameterizedType) {
	    return new CollectionConverter(collectionType,
					   openType, openArrayClass,
					   elementConverter);
        } else {
            if (elementConverter.isIdentity()) {
		return new IdentityConverter(collectionType,
                                             openType,
                                             openArrayClass);
	    } else {
		return new ArrayConverter(collectionType,
                                          openType,
                                          openArrayClass,
                                          elementConverter);
	    }
	}
    }

    private static final String[] keyArray = {"key"};
    private static final String[] keyValueArray = {"key", "value"};

    private static OpenConverter
	makeTabularConverter(Type objType, boolean sortedMap,
                             Type keyType, Type valueType)
	    throws OpenDataException {

	final String objTypeName = objType.toString();
	final OpenConverter keyConverter = toConverter(keyType);
	final OpenConverter valueConverter = toConverter(valueType);
	final OpenType keyOpenType = keyConverter.getOpenType();
	final OpenType valueOpenType = valueConverter.getOpenType();
	final CompositeType rowType =
	    new CompositeType(objTypeName,
			      objTypeName,
			      keyValueArray,
			      keyValueArray,
			      new OpenType[] {keyOpenType, valueOpenType});
	final TabularType tabularType =
	    new TabularType(objTypeName, objTypeName, rowType, keyArray);
	return new TabularConverter(objType, sortedMap, tabularType,
				    keyConverter, valueConverter);
    }

    /* We know how to translate List<E>, Set<E>, SortedSet<E>,
       Map<K,V>, SortedMap<K,V>, and that's it.  We don't accept
       subtypes of those because we wouldn't know how to deserialize
       them.  We don't accept Queue<E> because it is unlikely people
       would use that as a parameter or return type in an MBean.  */
    private static OpenConverter
	makeParameterizedConverter(ParameterizedType objType) throws OpenDataException {

        final Type rawType = objType.getRawType();

        if (rawType instanceof Class) {
            Class c = (Class<?>) rawType;
            if (c == List.class || c == Set.class || c == SortedSet.class) {
                Type[] actuals =
                    ((ParameterizedType) objType).getActualTypeArguments();
                assert(actuals.length == 1);
                if (c == SortedSet.class)
                    mustBeComparable(c, actuals[0]);
                return makeArrayOrCollectionConverter(objType, actuals[0]);
            } else {
                boolean sortedMap = (c == SortedMap.class);
                if (c == Map.class || sortedMap) {
                    Type[] actuals =
                            ((ParameterizedType) objType).getActualTypeArguments();
                    assert(actuals.length == 2);
                    if (sortedMap)
                        mustBeComparable(c, actuals[0]);
                    return makeTabularConverter(objType, sortedMap,
                            actuals[0], actuals[1]);
                }
            }
	}
        throw new OpenDataException("Cannot convert type: " + objType);
    }

    private static OpenConverter makeMXBeanConverter(Type t)
            throws OpenDataException {
        return new MXBeanConverter(t);
    }
    
    private static OpenConverter makeCompositeConverter(Class c)
	    throws OpenDataException {
        
        // For historical reasons GcInfo implements CompositeData but we
        // shouldn't count its CompositeData.getCompositeType() field as
        // an item in the computed CompositeType.
        final boolean gcInfoHack =
            (c.getName().equals("com.sun.management.GcInfo") &&
                c.getClassLoader() == null);

	final List<Method> methods =
                MBeanAnalyzer.eliminateCovariantMethods(c.getMethods());
	final SortedMap<String,Method> getterMap = newSortedMap();

	/* Select public methods that look like "T getX()" or "boolean
	   isX()", where T is not void and X is not the empty
	   string.  Exclude "Class getClass()" inherited from Object.  */
	for (Method method : methods) {
	    final String propertyName = propertyName(method);

	    if (propertyName == null)
		continue;
            if (gcInfoHack && propertyName.equals("CompositeType"))
                continue;

            Method old =
                getterMap.put(decapitalize(propertyName),
                            method);
            if (old != null) {
                final String msg =
                    "Class " + c.getName() + " has method name clash: " +
                    old.getName() + ", " + method.getName();
                throw new OpenDataException(msg);
            }
	}

        final int nitems = getterMap.size();

	if (nitems == 0) {
	    throw new OpenDataException("Can't map " + c.getName() +
					" to an open data type");
	}

        final Method[] getters = new Method[nitems];
        final String[] itemNames = new String[nitems];
        final OpenType[] openTypes = new OpenType[nitems];
        int i = 0;
        for (Map.Entry<String,Method> entry : getterMap.entrySet()) {
            itemNames[i] = entry.getKey();
            final Method getter = entry.getValue();
            getters[i] = getter;
            final Type retType = getter.getGenericReturnType();
            openTypes[i] = toConverter(retType).getOpenType();
            i++;
        }

        CompositeType compositeType =
	    new CompositeType(c.getName(),
			      c.getName(),
			      itemNames, // field names
			      itemNames, // field descriptions
			      openTypes);

	return new CompositeConverter(c,
				      compositeType,
				      itemNames,
				      getters);
    }

    /* Converter for classes where the open data is identical to the
       original data.  This is true for any of the SimpleType types,
       and for an any-dimension array of those.  It is also true for
       primitive types as of JMX 1.3, since an int[] needs to
       can be directly represented by an ArrayType, and an int needs no mapping
       because reflection takes care of it.  */
    private static final class IdentityConverter extends OpenConverter {
	IdentityConverter(Type targetType, OpenType openType,
                          Class openClass) {
	    super(targetType, openType, openClass);
	}

	boolean isIdentity() {
	    return true;
	}

	final Object toNonNullOpenValue(MXBeanLookup lookup, Object value) {
	    return value;
	}

	public final Object fromNonNullOpenValue(MXBeanLookup lookup, Object value) {
	    return value;
	}
    }

    private static final class EnumConverter<T extends Enum<T>>
	    extends OpenConverter {

	EnumConverter(Class<T> enumClass) {
	    super(enumClass, SimpleType.STRING, String.class);
	    this.enumClass = enumClass;
	}

	final Object toNonNullOpenValue(MXBeanLookup lookup, Object value) {
	    return ((Enum) value).name();
	}

        // return type could be T, but after erasure that would be
        // java.lang.Enum, which doesn't exist on J2SE 1.4
	public final Object fromNonNullOpenValue(MXBeanLookup lookup, Object value)
                throws InvalidObjectException {
	    try {
		return Enum.valueOf(enumClass, (String) value);
	    } catch (Exception e) {
		throw invalidObjectException("Cannot convert to enum: " +
                                             value, e);
	    }
	}

	private final Class<T> enumClass;
    }

    private static final class ArrayConverter extends OpenConverter {
	ArrayConverter(Type targetType,
		       ArrayType openArrayType, Class openArrayClass,
		       OpenConverter elementConverter) {
	    super(targetType, openArrayType, openArrayClass);
	    this.elementConverter = elementConverter;
	}

	final Object toNonNullOpenValue(MXBeanLookup lookup, Object value)
                throws OpenDataException {
	    Object[] valueArray = (Object[]) value;
	    final int len = valueArray.length;
            final Object[] openArray = (Object[])
		Array.newInstance(getOpenClass().getComponentType(), len);
	    for (int i = 0; i < len; i++) {
		openArray[i] =
                    elementConverter.toOpenValue(lookup, valueArray[i]);
            }
	    return openArray;
	}

	public final Object fromNonNullOpenValue(MXBeanLookup lookup, Object openValue)
                throws InvalidObjectException {
	    final Object[] openArray = (Object[]) openValue;
	    final Type targetType = getTargetType();
	    final Object[] valueArray;
            final Type componentType;
            if (targetType instanceof GenericArrayType) {
                componentType =
                    ((GenericArrayType) targetType).getGenericComponentType();
            } else if (targetType instanceof Class &&
                       ((Class<?>) targetType).isArray()) {
                componentType = ((Class<?>) targetType).getComponentType();
            } else {
                throw new IllegalArgumentException("Not an array: " +
                                                   targetType);
            }
            valueArray = (Object[]) Array.newInstance((Class<?>) componentType,
                                                      openArray.length);
	    for (int i = 0; i < openArray.length; i++) {
		valueArray[i] =
		    elementConverter.fromOpenValue(lookup, openArray[i]);
	    }
	    return valueArray;
	}

        void checkReconstructible() throws InvalidObjectException {
            elementConverter.checkReconstructible();
        }

	/** OpenConverter for the elements of this array.  If this is an
	    array of arrays, the converter converts the second-level arrays,
	    not the deepest elements.  */
	private final OpenConverter elementConverter;
    }

    private static final class CollectionConverter extends OpenConverter {
	CollectionConverter(Type targetType,
			    ArrayType openArrayType,
			    Class openArrayClass,
			    OpenConverter elementConverter) {
	    super(targetType, openArrayType, openArrayClass);
	    this.elementConverter = elementConverter;

	    /* Determine the concrete class to be used when converting
	       back to this Java type.  We convert all Lists to ArrayList
	       and all Sets to TreeSet.  (TreeSet because it is a SortedSet,
	       so works for both Set and SortedSet.)  */
            Type raw = ((ParameterizedType) targetType).getRawType();
            Class c = (Class<?>) raw;
	    if (c == List.class)
		collectionClass = ArrayList.class;
	    else if (c == Set.class)
                collectionClass = HashSet.class;
            else if (c == SortedSet.class)
		collectionClass = TreeSet.class;
	    else { // can't happen
		assert(false);
		collectionClass = null;
	    }
	}

	final Object toNonNullOpenValue(MXBeanLookup lookup, Object value)
                throws OpenDataException {
	    final Collection valueCollection = (Collection) value;
            if (valueCollection instanceof SortedSet) {
                Comparator comparator =
                    ((SortedSet) valueCollection).comparator();
                if (comparator != null) {
                    final String msg =
                        "Cannot convert SortedSet with non-null comparator: " +
                        comparator;
                    throw new OpenDataException(msg);
                }
            }
	    final Object[] openArray = (Object[])
		Array.newInstance(getOpenClass().getComponentType(),
				  valueCollection.size());
	    int i = 0;
	    for (Object o : valueCollection)
		openArray[i++] = elementConverter.toOpenValue(lookup, o);
	    return openArray;
	}

	public final Object fromNonNullOpenValue(MXBeanLookup lookup, Object openValue)
                throws InvalidObjectException {
	    final Object[] openArray = (Object[]) openValue;
	    final Collection<Object> valueCollection;
	    try {
		valueCollection = collectionClass.newInstance();
	    } catch (Exception e) {
                throw invalidObjectException("Cannot create collection", e);
	    }
	    for (Object o : openArray) {
                Object value = elementConverter.fromOpenValue(lookup, o);
		if (!valueCollection.add(value)) {
		    final String msg =
			"Could not add " + o + " to " +
			collectionClass.getName() +
			" (duplicate set element?)";
		    throw new InvalidObjectException(msg);
		}
	    }
	    return valueCollection;
	}

        void checkReconstructible() throws InvalidObjectException {
            elementConverter.checkReconstructible();
        }

	private final Class<? extends Collection> collectionClass;
	private final OpenConverter elementConverter;
    }

    private static final class MXBeanConverter extends OpenConverter {
        MXBeanConverter(Type intf) {
            super(intf, SimpleType.OBJECTNAME, ObjectName.class);
        }

        final Object toNonNullOpenValue(MXBeanLookup lookup, Object value)
                throws OpenDataException {
            lookupNotNull(lookup, OpenDataException.class);
            ObjectName name = lookup.mxbeanToObjectName(value);
            if (name == null)
                throw new OpenDataException("No name for object: " + value);
            return name;
        }

        public final Object fromNonNullOpenValue(MXBeanLookup lookup, Object value)
                throws InvalidObjectException {
            lookupNotNull(lookup, InvalidObjectException.class);
            ObjectName name = (ObjectName) value;
            Object mxbean =
                lookup.objectNameToMXBean(name, (Class<?>) getTargetType());
            if (mxbean == null) {
                final String msg =
                    "No MXBean for name: " + name;
                throw new InvalidObjectException(msg);
            }
            return mxbean;
        }
        
        private <T extends Exception> void
            lookupNotNull(MXBeanLookup lookup, Class<T> excClass)
                throws T {
            if (lookup == null) {
                final String msg =
                    "Cannot convert MXBean interface in this context";
                T exc;
                try {
                    Constructor<T> con = excClass.getConstructor(String.class);
                    exc = con.newInstance(msg);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
                throw exc;
            }
        }
    }
    
    private static final class TabularConverter extends OpenConverter {
	TabularConverter(Type targetType,
                         boolean sortedMap,
			 TabularType tabularType,
			 OpenConverter keyConverter,
			 OpenConverter valueConverter) {
	    super(targetType, tabularType, TabularData.class);
            this.sortedMap = sortedMap;
	    this.keyConverter = keyConverter;
	    this.valueConverter = valueConverter;
	}

	final Object toNonNullOpenValue(MXBeanLookup lookup, Object value)
                throws OpenDataException {
	    final Map<Object, Object> valueMap = (Map<Object, Object>) value;
            if (valueMap instanceof SortedMap) {
                Comparator comparator = ((SortedMap) valueMap).comparator();
                if (comparator != null) {
                    final String msg =
                        "Cannot convert SortedMap with non-null comparator: " +
                        comparator;
                    throw new OpenDataException(msg);
                }
            }
	    final TabularType tabularType = (TabularType) getOpenType();
	    final TabularData table = new TabularDataSupport(tabularType);
	    final CompositeType rowType = tabularType.getRowType();
	    for (Map.Entry entry : valueMap.entrySet()) {
		final Object openKey =
		    keyConverter.toOpenValue(lookup, entry.getKey());
		final Object openValue =
		    valueConverter.toOpenValue(lookup, entry.getValue());
		final CompositeData row;
                row =
                    new CompositeDataSupport(rowType, keyValueArray,
                                             new Object[] {openKey,
                                                           openValue});
		table.put(row);
	    }
	    return table;
	}

	public final Object fromNonNullOpenValue(MXBeanLookup lookup, Object openValue)
                throws InvalidObjectException {
	    final TabularData table = (TabularData) openValue;
	    final Collection<CompositeData> rows =
		(Collection<CompositeData>) table.values();
            final Map<Object, Object> valueMap =
                sortedMap ? newSortedMap() : newMap();
	    for (CompositeData row : rows) {
		final Object key =
		    keyConverter.fromOpenValue(lookup, row.get("key"));
		final Object value =
		    valueConverter.fromOpenValue(lookup, row.get("value"));
		if (valueMap.put(key, value) != null) {
		    final String msg =
			"Duplicate entry in TabularData: key=" + key;
		    throw new InvalidObjectException(msg);
		}
	    }
	    return valueMap;
	}

        void checkReconstructible() throws InvalidObjectException {
            keyConverter.checkReconstructible();
            valueConverter.checkReconstructible();
        }

	private final boolean sortedMap;
        private final OpenConverter keyConverter;
	private final OpenConverter valueConverter;
    }

    private static final class CompositeConverter extends OpenConverter {
	CompositeConverter(Class targetClass,
			   CompositeType compositeType,
			   String[] itemNames,
			   Method[] getters) throws OpenDataException {
	    super(targetClass, compositeType, CompositeData.class);

	    assert(itemNames.length == getters.length);

	    this.itemNames = itemNames;
	    this.getters = getters;
	    this.getterConverters = new OpenConverter[getters.length];
	    for (int i = 0; i < getters.length; i++) {
		Type retType = getters[i].getGenericReturnType();
		getterConverters[i] = OpenConverter.toConverter(retType);
	    }
	}

	final Object toNonNullOpenValue(MXBeanLookup lookup, Object value)
                throws OpenDataException {
            CompositeType ct = (CompositeType) getOpenType();
	    if (value instanceof CompositeDataView)
		return ((CompositeDataView) value).toCompositeData(ct);
            if (value == null)
                return null;

	    Object[] values = new Object[getters.length];
	    for (int i = 0; i < getters.length; i++) {
		try {
		    Object got = getters[i].invoke(value, (Object[]) null);
		    values[i] = getterConverters[i].toOpenValue(lookup, got);
		} catch (Exception e) {
		    throw openDataException("Error calling getter for " +
                                            itemNames[i] + ": " + e, e);
		}
	    }
	    return new CompositeDataSupport(ct, itemNames, values);
	}

	/** Determine how to convert back from the CompositeData into
	    the original Java type.  For a type that is not reconstructible,
            this method will fail every time, and will throw the right
            exception. */
	private synchronized void makeCompositeBuilder()
		throws InvalidObjectException {
	    if (compositeBuilder != null)
		return;

	    Class targetClass = (Class<?>) getTargetType();
            /* In this 2D array, each subarray is a set of builders where
               there is no point in consulting the ones after the first if
               the first refuses.  */
	    CompositeBuilder[][] builders = {
                {
                    new CompositeBuilderViaFrom(targetClass, itemNames),
                },
                {
                    new CompositeBuilderViaConstructor(targetClass, itemNames),
                },
                {
                    new CompositeBuilderCheckGetters(targetClass, itemNames,
                                                     getterConverters),
                    new CompositeBuilderViaSetters(targetClass, itemNames),
                    new CompositeBuilderViaProxy(targetClass, itemNames),
                },
	    };
	    CompositeBuilder foundBuilder = null;
	    /* We try to make a meaningful exception message by
	       concatenating each Builder's explanation of why it
	       isn't applicable.  */
	    StringBuffer whyNots = new StringBuffer();
        find:
	    for (CompositeBuilder[] relatedBuilders : builders) {
                for (int i = 0; i < relatedBuilders.length; i++) {
                    CompositeBuilder builder = relatedBuilders[i];
                    String whyNot = builder.applicable(getters);
                    if (whyNot == null) {
                        foundBuilder = builder;
                        break find;
                    }
                    if (whyNot.length() > 0) {
                        if (whyNots.length() > 0)
                            whyNots.append("; ");
                        whyNots.append(whyNot);
                        if (i == 0)
                           break; // skip other builders in this group
                    }
                }
	    }
	    if (foundBuilder == null) {
		final String msg =
		    "Do not know how to make a " + targetClass.getName() +
		    " from a CompositeData: " + whyNots;
		throw new InvalidObjectException(msg);
	    }
	    compositeBuilder = foundBuilder;
	}

	void checkReconstructible() throws InvalidObjectException {
            makeCompositeBuilder();
        }

        public final Object fromNonNullOpenValue(MXBeanLookup lookup, Object value)
                throws InvalidObjectException {
	    makeCompositeBuilder();
	    return compositeBuilder.fromCompositeData(lookup,
                                                      (CompositeData) value,
                                                      itemNames,
                                                      getterConverters);
	}

	private final String[] itemNames;
	private final Method[] getters;
	private final OpenConverter[] getterConverters;
	private CompositeBuilder compositeBuilder;
    }

    /** Converts from a CompositeData to an instance of the targetClass.  */
    private static abstract class CompositeBuilder {
	CompositeBuilder(Class targetClass, String[] itemNames) {
	    this.targetClass = targetClass;
            this.itemNames = itemNames;
	}

	Class getTargetClass() {
	    return targetClass;
	}

        String[] getItemNames() {
            return itemNames;
        }

	/** If the subclass is appropriate for targetClass, then the
	    method returns null.  If the subclass is not appropriate,
	    then the method returns an explanation of why not.  If the
	    subclass should be appropriate but there is a problem,
	    then the method throws InvalidObjectException.  */
	abstract String applicable(Method[] getters)
                throws InvalidObjectException;

        abstract Object fromCompositeData(MXBeanLookup lookup, CompositeData cd,
                                          String[] itemNames,
                                          OpenConverter[] converters)
                throws InvalidObjectException;

	private final Class targetClass;
        private final String[] itemNames;
    }

    /** Builder for when the target class has a method "public static
	from(CompositeData)".  */
    private static final class CompositeBuilderViaFrom
	    extends CompositeBuilder {

	CompositeBuilderViaFrom(Class targetClass, String[] itemNames) {
	    super(targetClass, itemNames);
	}

	String applicable(Method[] getters) throws InvalidObjectException {
	    // See if it has a method "T from(CompositeData)"
	    // as is conventional for a CompositeDataView
	    Class targetClass = getTargetClass();
	    try {
		Method fromMethod =
		    targetClass.getMethod("from",
					  new Class[] {CompositeData.class});

		if (!Modifier.isStatic(fromMethod.getModifiers())) {
		    final String msg =
			"Method from(CompositeData) is not static";
		    throw new InvalidObjectException(msg);
		}

		if (fromMethod.getReturnType() != getTargetClass()) {
		    final String msg =
			"Method from(CompositeData) returns " +
			fromMethod.getReturnType().getName() +
			" not " + targetClass.getName();
		    throw new InvalidObjectException(msg);
		}

		this.fromMethod = fromMethod;
		return null; // success!
	    } catch (InvalidObjectException e) {
		throw e;
	    } catch (Exception e) {
		// OK: it doesn't have the method
		return "no method from(CompositeData)";
	    }
	}

	final Object fromCompositeData(MXBeanLookup lookup, CompositeData cd,
                                 String[] itemNames,
                                 OpenConverter[] converters)
                throws InvalidObjectException {
	    try {
		return fromMethod.invoke(null, cd);
	    } catch (Exception e) {
		final String msg = "Failed to invoke from(CompositeData)";
                throw invalidObjectException(msg, e);
	    }
	}

	private Method fromMethod;
    }

    /** This builder never actually returns success.  It simply serves
        to check whether the other builders in the same group have any
        chance of success.  If any getter in the targetClass returns
        a type that we don't know how to reconstruct, then we will
        not be able to make a builder, and there is no point in repeating
        the error about the problematic getter as many times as there are
        candidate builders.  Instead, the "applicable" method will return
        an explanatory string, and the other builders will be skipped.
        If all the getters are OK, then the "applicable" method will return
        an empty string and the other builders will be tried.  */
    private static class CompositeBuilderCheckGetters extends CompositeBuilder {
        CompositeBuilderCheckGetters(Class targetClass, String[] itemNames,
                                     OpenConverter[] getterConverters) {
            super(targetClass, itemNames);
            this.getterConverters = getterConverters;
        }

        String applicable(Method[] getters) {
            for (int i = 0; i < getters.length; i++) {
                try {
                    getterConverters[i].checkReconstructible();
                } catch (InvalidObjectException e) {
                    return "method " + getters[i].getName() + " returns type " +
                        "that cannot be mapped back from OpenData";
                }
            }
            return "";
        }
        
        final Object fromCompositeData(MXBeanLookup lookup, CompositeData cd,
                                       String[] itemNames,
                                       OpenConverter[] converters) {
            throw new Error();
        }
        
        private final OpenConverter[] getterConverters;
    }

    /** Builder for when the target class has a setter for every getter. */
    private static class CompositeBuilderViaSetters extends CompositeBuilder {

        CompositeBuilderViaSetters(Class targetClass, String[] itemNames) {
            super(targetClass, itemNames);
        }

        String applicable(Method[] getters) {
            try {
                Constructor c = getTargetClass().getConstructor((Class[]) null);
            } catch (Exception e) {
                return "does not have a public no-arg constructor";
            }

            Method[] setters = new Method[getters.length];
            for (int i = 0; i < getters.length; i++) {
                Method getter = getters[i];
                Class returnType = getter.getReturnType();
                String name = propertyName(getter);
                String setterName = "set" + name;
                Method setter;
                try {
                    setter = getTargetClass().getMethod(setterName, returnType);
                    if (setter.getReturnType() != void.class)
                        throw new Exception();
                } catch (Exception e) {
                    return "not all getters have corresponding setters " +
                           "(" + getter + ")";
                }
                setters[i] = setter;
            }
            this.setters = setters;
            return null;
        }

        Object fromCompositeData(MXBeanLookup lookup, CompositeData cd,
                                 String[] itemNames,
                                 OpenConverter[] converters)
                throws InvalidObjectException {
            Object o;
            try {
                o = getTargetClass().newInstance();
                for (int i = 0; i < itemNames.length; i++) {
		    if (cd.containsKey(itemNames[i])) {
			Object openItem = cd.get(itemNames[i]);
			Object javaItem =
			    converters[i].fromOpenValue(lookup, openItem);
			setters[i].invoke(o, javaItem);
		    }
                }
            } catch (Exception e) {
                throw invalidObjectException(e);
            }
            return o;
        }

        private Method[] setters;
    }

    /** Builder for when the target class has a constructor that is
	annotated with @ConstructorProperties so we can see the correspondence
	to getters.  */
    private static final class CompositeBuilderViaConstructor
	    extends CompositeBuilder {

	CompositeBuilderViaConstructor(Class targetClass, String[] itemNames) {
	    super(targetClass, itemNames);
	}

	String applicable(Method[] getters) throws InvalidObjectException {
            
            final Class<ConstructorProperties> propertyNamesClass = ConstructorProperties.class;
            
	    Class targetClass = getTargetClass();
	    Constructor[] constrs = targetClass.getConstructors();
            
            // Applicable if and only if there are any annotated constructors
            List<Constructor> annotatedConstrList = newList();
            for (Constructor constr : constrs) {
                if (Modifier.isPublic(constr.getModifiers())
                        && constr.getAnnotation(propertyNamesClass) != null)
                    annotatedConstrList.add(constr);
            }

	    if (annotatedConstrList.isEmpty())
		return "no constructor has @ConstructorProperties annotation";
            
            annotatedConstructors = newList();

            // Now check that all the annotated constructors are valid
            // and throw an exception if not.

            // First link the itemNames to their getter indexes.
            Map<String, Integer> getterMap = newMap();
            String[] itemNames = getItemNames();
            for (int i = 0; i < itemNames.length; i++)
                getterMap.put(itemNames[i], i);
            
            // Run through the constructors making the checks in the spec.
            // For each constructor, remember the correspondence between its
            // parameters and the items.  The int[] for a constructor says
            // what parameter index should get what item.  For example,
            // if element 0 is 2 then that means that item 0 in the
            // CompositeData goes to parameter 2 of the constructor.  If an
            // element is -1, that item isn't given to the constructor.
            // Also remember the set of properties in that constructor
            // so we can test unambiguity.
            Set<BitSet> getterIndexSets = newSet();
            for (Constructor constr : annotatedConstrList) {
                String[] propertyNames =
                    constr.getAnnotation(propertyNamesClass).value();
                
                Set<String> propertyNameSet =
                    newSet(Arrays.asList(propertyNames));

                Type[] paramTypes = constr.getGenericParameterTypes();
                if (paramTypes.length != propertyNames.length) {
                    final String msg =
                        "Number of constructor params does not match " +
                        "@ConstructorProperties annotation: " + constr;
                    throw new InvalidObjectException(msg);
                }
                
                int[] paramIndexes = new int[getters.length];
                for (int i = 0; i < getters.length; i++)
                    paramIndexes[i] = -1;
                BitSet present = new BitSet();

                for (int i = 0; i < propertyNames.length; i++) {
                    String propertyName = propertyNames[i];
                    if (!getterMap.containsKey(propertyName)) {
                        final String msg =
                            "@ConstructorProperties includes name " + propertyName +
                            " which does not correspond to a property: " +
                            constr;
                        throw new InvalidObjectException(msg);
                    }
                    int getterIndex = getterMap.get(propertyName);
                    paramIndexes[getterIndex] = i;
                    if (present.get(getterIndex)) {
                        final String msg =
                            "@ConstructorProperties contains property " +
                            propertyName + " more than once: " + constr;
                        throw new InvalidObjectException(msg);
                    }
                    present.set(getterIndex);
                    Method getter = getters[getterIndex];
                    Type propertyType = getter.getGenericReturnType();
                    if (!propertyType.equals(paramTypes[i])) {
                        final String msg =
                            "@ConstructorProperties gives property " + propertyName +
                            " of type " + propertyType + " for parameter " +
                            " of type " + paramTypes[i] + ": " + constr;
                        throw new InvalidObjectException(msg);
                    }
                }

                if (!getterIndexSets.add(present)) {
                    final String msg =
                        "More than one constructor has a @ConstructorProperties " +
                        "annotation with this set of names: " +
                        Arrays.toString(propertyNames);
                    throw new InvalidObjectException(msg);
                }
                
                Constr c = new Constr(constr, paramIndexes, present);                
                annotatedConstructors.add(c);
            }

            /* Check that no possible set of items could lead to an ambiguous
             * choice of constructor (spec requires this check).  For any
             * pair of constructors, their union would be the minimal
             * ambiguous set.  If this set itself corresponds to a constructor,
             * there is no ambiguity for that pair.  In the usual case, one
             * of the constructors is a superset of the other so the union is
             * just the bigger constuctor.
	     *
	     * The algorithm here is quadratic in the number of constructors
	     * with a @ConstructorProperties annotation.  Typically this corresponds
	     * to the number of versions of the class there have been.  Ten
	     * would already be a large number, so although it's probably
	     * possible to have an O(n lg n) algorithm it wouldn't be
	     * worth the complexity.
             */
            for (BitSet a : getterIndexSets) {
                boolean seen = false;
                for (BitSet b : getterIndexSets) {
                    if (a == b)
                        seen = true;
                    else if (seen) {
                        BitSet u = new BitSet();
                        u.or(a); u.or(b);
                        if (!getterIndexSets.contains(u)) {
                            Set<String> names = new TreeSet<String>();
                            for (int i = u.nextSetBit(0); i >= 0;
                                 i = u.nextSetBit(i+1))
                                names.add(itemNames[i]);
                            final String msg =
                                "Constructors with @ConstructorProperties annotation " +
                                " would be ambiguous for these items: " +
                                names;
                            throw new InvalidObjectException(msg);
                        }
                    }
                }
            }

	    return null; // success!
	}

        Object fromCompositeData(MXBeanLookup lookup, CompositeData cd,
                                 String[] itemNames,
                                 OpenConverter[] converters)
                throws InvalidObjectException {
            // The CompositeData might come from an earlier version where
            // not all the items were present.  We look for a constructor
            // that accepts just the items that are present.  Because of
            // the ambiguity check in applicable(), we know there must be
            // at most one maximally applicable constructor.
            CompositeType ct = cd.getCompositeType();
            BitSet present = new BitSet();
            for (int i = 0; i < itemNames.length; i++) {
                if (ct.getType(itemNames[i]) != null)
                    present.set(i);
            }
            
            Constr max = null;
            for (Constr constr : annotatedConstructors) {
                if (subset(constr.presentParams, present) &&
                        (max == null ||
                         subset(max.presentParams, constr.presentParams)))
                    max = constr;
            }
            
            if (max == null) {
                final String msg =
                    "No constructor has a @ConstructorProperties for this set of " +
                    "items: " + ct.keySet();
                throw new InvalidObjectException(msg);
            }
            
            Object[] params = new Object[max.presentParams.cardinality()];
            for (int i = 0; i < itemNames.length; i++) {
                if (!max.presentParams.get(i))
                    continue;
                Object openItem = cd.get(itemNames[i]);
                Object javaItem = converters[i].fromOpenValue(lookup, openItem);
                int index = max.paramIndexes[i];
                if (index >= 0)
                    params[index] = javaItem;
            }

	    try {
		return max.constructor.newInstance(params);
	    } catch (Exception e) {
		final String msg =
		    "Exception constructing " + getTargetClass().getName();
                throw invalidObjectException(msg, e);
	    }
        }
        
        private static boolean subset(BitSet sub, BitSet sup) {
            BitSet subcopy = (BitSet) sub.clone();
            subcopy.andNot(sup);
            return subcopy.isEmpty();
        }
        
        private static class Constr {
            final Constructor constructor;
            final int[] paramIndexes;
            final BitSet presentParams;
            Constr(Constructor constructor, int[] paramIndexes,
                   BitSet presentParams) {
                this.constructor = constructor;
                this.paramIndexes = paramIndexes;
                this.presentParams = presentParams;
            }
        }
        
        private List<Constr> annotatedConstructors;
    }

    /** Builder for when the target class is an interface and contains
	no methods other than getters.  Then we can make an instance
	using a dynamic proxy that forwards the getters to the source
	CompositeData.  */
    private static final class CompositeBuilderViaProxy
	    extends CompositeBuilder {

	CompositeBuilderViaProxy(Class targetClass, String[] itemNames) {
	    super(targetClass, itemNames);
	}

	String applicable(Method[] getters) {
	    Class targetClass = getTargetClass();
	    if (!targetClass.isInterface())
		return "not an interface";
            Set<Method> methods =
                newSet(Arrays.asList(targetClass.getMethods()));
            methods.removeAll(Arrays.asList(getters));
            /* If the interface has any methods left over, they better be
             * public methods that are already present in java.lang.Object.
             */
            String bad = null;
            for (Method m : methods) {
                String mname = m.getName();
                Class[] mparams = m.getParameterTypes();
                try {
                    Method om = Object.class.getMethod(mname, mparams);
                    if (!Modifier.isPublic(om.getModifiers()))
                        bad = mname;
                } catch (NoSuchMethodException e) {
                    bad = mname;
                }
                /* We don't catch SecurityException since it shouldn't
                 * happen for a method in Object and if it does we would
                 * like to know about it rather than mysteriously complaining.
                 */
            }
	    if (bad != null)
		return "contains methods other than getters (" + bad + ")";
	    return null; // success!
	}

	final Object fromCompositeData(MXBeanLookup lookup, CompositeData cd,
                                 String[] itemNames,
                                 OpenConverter[] converters) {
	    final Class targetClass = getTargetClass();
	    return
		Proxy.newProxyInstance(targetClass.getClassLoader(),
				       new Class[] {targetClass},
				       new CompositeDataInvocationHandler(cd));
	}
    }

    static InvalidObjectException invalidObjectException(String msg,
                                                         Throwable cause) {
        return EnvHelp.initCause(new InvalidObjectException(msg), cause);
    }
    
    static InvalidObjectException invalidObjectException(Throwable cause) {
        return invalidObjectException(cause.getMessage(), cause);
    }
    
    static OpenDataException openDataException(String msg, Throwable cause) {
        return EnvHelp.initCause(new OpenDataException(msg), cause);
    }
    
    static OpenDataException openDataException(Throwable cause) {
        return openDataException(cause.getMessage(), cause);
    }
    
    static void mustBeComparable(Class collection, Type element)
            throws OpenDataException {
        if (!(element instanceof Class)
            || !Comparable.class.isAssignableFrom((Class<?>) element)) {
            final String msg =
                "Parameter class " + element + " of " +
                collection.getName() + " does not implement " +
                Comparable.class.getName();
            throw new OpenDataException(msg);
        }
    }

    /**
     * Utility method to take a string and convert it to normal Java variable
     * name capitalization.  This normally means converting the first
     * character from upper case to lower case, but in the (unusual) special
     * case when there is more than one character and both the first and
     * second characters are upper case, we leave it alone.
     * <p>
     * Thus "FooBah" becomes "fooBah" and "X" becomes "x", but "URL" stays
     * as "URL".
     *
     * @param  name The string to be decapitalized.
     * @return  The decapitalized version of the string.
     */
    public static String decapitalize(String name) {
        if (name == null || name.length() == 0) {
            return name;
        }
        int offset1 = Character.offsetByCodePoints(name, 0, 1);
        // Should be name.offsetByCodePoints but 6242664 makes this fail
        if (offset1 < name.length() &&
                Character.isUpperCase(name.codePointAt(offset1)))
            return name;
        return name.substring(0, offset1).toLowerCase() +
               name.substring(offset1);
    }

    /**
     * Reverse operation for java.beans.Introspector.decapitalize.  For any s,
     * capitalize(decapitalize(s)).equals(s).  The reverse is not true:
     * e.g. capitalize("uRL") produces "URL" which is unchanged by
     * decapitalize.
     */
    static String capitalize(String name) {
        if (name == null || name.length() == 0)
            return name;
        int offset1 = name.offsetByCodePoints(0, 1);
        return name.substring(0, offset1).toUpperCase() +
               name.substring(offset1);
    }

    public static String propertyName(Method m) {
	String rest = null;
	String name = m.getName();
	if (name.startsWith("get"))
	    rest = name.substring(3);
	else if (name.startsWith("is") && m.getReturnType() == boolean.class)
	    rest = name.substring(2);
	if (rest == null || rest.length() == 0
	    || m.getParameterTypes().length > 0
	    || m.getReturnType() == void.class
	    || name.equals("getClass"))
	    return null;
	return rest;
    }

    private final static Map<Type, Type> inProgress = newIdentityHashMap();
    // really an IdentityHashSet but that doesn't exist
}