PropertyTypeHandler.java

package nz.co.gregs.dbvolution.internal.properties;

import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.Date;
import nz.co.gregs.dbvolution.annotations.DBAdaptType;
import nz.co.gregs.dbvolution.annotations.DBColumn;
import nz.co.gregs.dbvolution.datatypes.DBBoolean;
import nz.co.gregs.dbvolution.datatypes.DBDate;
import nz.co.gregs.dbvolution.datatypes.DBInteger;
import nz.co.gregs.dbvolution.datatypes.DBNumber;
import nz.co.gregs.dbvolution.datatypes.DBString;
import nz.co.gregs.dbvolution.datatypes.DBStringTrimmed;
import nz.co.gregs.dbvolution.datatypes.DBTypeAdaptor;
import nz.co.gregs.dbvolution.datatypes.DBUnknownDatatype;
import nz.co.gregs.dbvolution.datatypes.QueryableDatatype;
import nz.co.gregs.dbvolution.datatypes.QueryableDatatypeSyncer;
import nz.co.gregs.dbvolution.datatypes.SimpleValueQueryableDatatypeSyncer;
import nz.co.gregs.dbvolution.exceptions.DBRuntimeException;
import nz.co.gregs.dbvolution.exceptions.DBThrownByEndUserCodeException;
import nz.co.gregs.dbvolution.exceptions.InvalidDeclaredTypeException;
import nz.co.gregs.dbvolution.internal.properties.InterfaceInfo.ParameterBounds;
import nz.co.gregs.dbvolution.internal.properties.InterfaceInfo.UnsupportedType;

/**
 * Handles annotation processing, business logic, validation rules, defaulting,
 * and error handling associated with the type of a property. This includes
 * processing of the {@link DBAdaptType} annotation on a property, and type
 * conversion of the property's underlying type.
 *
 * <p>
 * This class handles the majority of the type support logic that is exposed by
 * the {@link PropertyWrapperDefinition} class, which just delegates to this
 * class.
 *
 * <p>
 * This class behaves correctly when no {@link DBAdaptType} property is present.
 *
 * @author Malcolm Lett
 */
class PropertyTypeHandler<BASETYPE> implements Serializable {

	private static final long serialVersionUID = 1l;

	private final JavaProperty<BASETYPE> javaProperty;
	private final Class<BASETYPE> genericPropertyType;
	private final Class<? extends QueryableDatatype<BASETYPE>> dbvPropertyType;
	private transient final DBTypeAdaptor<Object, Object> typeAdaptor;
	private final QueryableDatatypeSyncer internalQdtSyncer;
	private final boolean identityOnly;
	private transient final DBAdaptType dbAdaptTypeAnnotation;
	private transient final DBColumn dbColumnAnnotation;

	/**
	 *
	 * @param javaProperty the annotated property
	 */
	@SuppressWarnings("unchecked")
	PropertyTypeHandler(JavaProperty<BASETYPE> javaProperty, boolean processIdentityOnly) {
		this.javaProperty = javaProperty;
		this.identityOnly = processIdentityOnly;
		this.dbAdaptTypeAnnotation = javaProperty.getAnnotation(DBAdaptType.class);
		this.dbColumnAnnotation = javaProperty.getAnnotation(DBColumn.class);
		boolean isColumn = (dbColumnAnnotation != null);

		Class<?> typeAdaptorClass = null;
		if (dbAdaptTypeAnnotation != null) {
			typeAdaptorClass = dbAdaptTypeAnnotation.value();
		}
		Class<?> typeAdaptorInternalType = null; // DBv-internal
		Class<?> typeAdaptorExternalType = null;

		// validation: must use type adaptor if java property not a QueryableDataType
		if (isColumn && !QueryableDatatype.class.isAssignableFrom(javaProperty.type())) {
			if (dbAdaptTypeAnnotation == null) {
				throw new InvalidDeclaredTypeException(javaProperty.type().getName() + " is not a supported type on " + javaProperty + ". "
						+ "Use one of the standard DB types, or use the @" + DBAdaptType.class.getSimpleName() + " annotation "
						+ "to adapt from a non-standard type.");
			}
		}

		// validation: type adaptor must implement TypeAdaptor interface if used
		if (typeAdaptorClass != null) {
			if (!DBTypeAdaptor.class.isAssignableFrom(typeAdaptorClass)) {
				throw new InvalidDeclaredTypeException("Type adaptor " + typeAdaptorClass.getName() + " must implement "
						+ DBTypeAdaptor.class.getSimpleName() + ", on " + javaProperty);
			}
		}

		// validation: type adaptor must not be an interface or abstract
		if (typeAdaptorClass != null) {
			if (typeAdaptorClass.isInterface()) {
				throw new InvalidDeclaredTypeException("Type adaptor " + typeAdaptorClass.getName()
						+ " must not be an interface, on " + javaProperty);
			}
			if (Modifier.isAbstract(typeAdaptorClass.getModifiers())) {
				throw new InvalidDeclaredTypeException("Type adaptor " + typeAdaptorClass.getName()
						+ " must not be abstract, on " + javaProperty);
			}
		}

		// validation: type adaptor must use only acceptable styles of generics
		// (note: rule de-activates if InterfaceInfo can't handle the class,
		//   or if other assumptions are broken.
		//   This is intentional to future-proof and because generics of type
		//   hierarchies is tremendously complex and its process very prone to error.)
		if (typeAdaptorClass != null) {
			ParameterBounds[] parameterBounds = null;
			try {
				InterfaceInfo interfaceInfo = new InterfaceInfo(DBTypeAdaptor.class, typeAdaptorClass);
				parameterBounds = interfaceInfo.getInterfaceParameterValueBounds();
			} catch (UnsupportedOperationException dropped) {
				// bumped into generics that can't be handled, so best to give the
				// end-user the benefit of doubt and just skip the validation
//                logger.debug("Cancelled validation on type adaptor " + typeAdaptorClass.getName()
//                        + " due to internal error: " + dropped.getMessage(), dropped);
			}
			if (parameterBounds != null && parameterBounds.length == 2) {
				if (parameterBounds[0].isUpperMulti()) {
					throw new InvalidDeclaredTypeException("Type adaptor " + typeAdaptorClass.getName() + " must not be"
							+ " declared with multiple super types for type variables"
							+ ", on " + javaProperty);
				}
				if (parameterBounds[1].isUpperMulti()) {
					throw new InvalidDeclaredTypeException("Type adaptor " + typeAdaptorClass.getName() + " must not be"
							+ " declared with multiple super types for type variables"
							+ ", on " + javaProperty);
				}

				try {
					typeAdaptorExternalType = parameterBounds[0].upperClass();
				} catch (UnsupportedType e) {
					// rules dependent on this attribute will be disabled
				}

				try {
					typeAdaptorInternalType = parameterBounds[1].upperClass();
				} catch (UnsupportedType e) {
					// rules dependent on this attribute will be disabled
				}
			}
		}

		// validation: Type adaptor's external type must not be a QDT.
		if (typeAdaptorExternalType != null) {
			if (QueryableDatatype.class.isAssignableFrom(typeAdaptorExternalType)) {
				throw new InvalidDeclaredTypeException(
						"Type adaptor's external type must not be a " + QueryableDatatype.class.getSimpleName()
						+ ", on " + javaProperty);
			}
		}

		// validation: Type adaptor's internal type must not be a QDT.
		if (typeAdaptorInternalType != null) {
			if (QueryableDatatype.class.isAssignableFrom(typeAdaptorInternalType)) {
				throw new InvalidDeclaredTypeException(
						"Type adaptor's internal type must not be a " + QueryableDatatype.class.getSimpleName()
						+ ", on " + javaProperty);
			}
		}

		// validation: explicit external type must be a QDT and must not be abstract or an interface
		if (dbAdaptTypeAnnotation != null && explicitTypeOrNullOf(dbAdaptTypeAnnotation) != null) {
			Class<?> explicitQDTType = explicitTypeOrNullOf(dbAdaptTypeAnnotation);
			if (!QueryableDatatype.class.isAssignableFrom(explicitQDTType)) {
				throw new InvalidDeclaredTypeException("@DB" + DBAdaptType.class.getSimpleName() + "(type) on "
						+ javaProperty + " is not a supported type. "
						+ "Use one of the standard DB types.");
			}
			if (Modifier.isAbstract(explicitQDTType.getModifiers()) || Modifier.isInterface(explicitQDTType.getModifiers())) {
				throw new InvalidDeclaredTypeException("@DB" + DBAdaptType.class.getSimpleName()
						+ "(type) must be a concrete type"
						+ ", on " + javaProperty);
			}
		}

		// validation: Type adaptor's external type must be either:
		//   a) castable to the external property type (and not a QDT), or
		//   b) a simple type that is supported by the external property type,
		//      and the external property type must be a QDT
		// (note: in either case can't be a QDT itself due to rules above)
		if (typeAdaptorExternalType != null && !QueryableDatatype.class.isAssignableFrom(javaProperty.type())) {
			if (!javaProperty.type().equals(typeAdaptorExternalType)
					&& SafeOneWaySimpleTypeAdaptor.getSimpleCastFor(javaProperty.type(), typeAdaptorExternalType) == null) {
				throw new InvalidDeclaredTypeException("Type adaptor's external " + typeAdaptorExternalType.getSimpleName()
						+ " type is not compatible with the property type, on " + javaProperty);
			}
		}
		if (typeAdaptorExternalType != null && QueryableDatatype.class.isAssignableFrom(javaProperty.type())) {
			Class<? extends QueryableDatatype<?>> explicitQDTType = (Class<? extends QueryableDatatype<?>>) javaProperty.type();
			Class<?> inferredQDTType = inferredQDTTypeForSimpleType(typeAdaptorExternalType);
			if (inferredQDTType == null) {
				throw new InvalidDeclaredTypeException("Type adaptor's external " + typeAdaptorExternalType.getSimpleName()
						+ " type is not a supported simple type, on " + javaProperty);
			} else if (!isSimpleTypeSupportedByQDT(typeAdaptorExternalType, explicitQDTType)) {
				throw new InvalidDeclaredTypeException("Type adaptor's external " + typeAdaptorExternalType.getSimpleName()
						+ " type is not compatible with a " + explicitQDTType.getSimpleName()
						+ " property, on " + javaProperty);
			}
		}

		// validation: Type adaptor's internal type must be either:
		//   a) a simple type that implies an internal QDT type,
		//      and no explicit QDT type is specified, or
		//   b) a simple type that is supported by the explicit internal QDT type,
		//      and the explicit internal QDT type is specified
		// (note: in either case can't be a QDT itself due to rule above)
		if (typeAdaptorInternalType != null && explicitTypeOrNullOf(dbAdaptTypeAnnotation) == null) {
			Class<?> inferredQDTType = inferredQDTTypeForSimpleType(typeAdaptorInternalType);
			if (inferredQDTType == null) {
				throw new InvalidDeclaredTypeException("Type adaptor's internal " + typeAdaptorInternalType.getSimpleName()
						+ " type is not a supported simple type, on " + javaProperty);
			}
		}
		if (typeAdaptorInternalType != null && explicitTypeOrNullOf(dbAdaptTypeAnnotation) != null) {
			Class<? extends QueryableDatatype<?>> explicitQDTType = explicitTypeOrNullOf(dbAdaptTypeAnnotation);
			Class<?> inferredQDTType = inferredQDTTypeForSimpleType(typeAdaptorInternalType);
			if (inferredQDTType == null) {
				throw new InvalidDeclaredTypeException("Type adaptor's internal " + typeAdaptorInternalType.getSimpleName()
						+ " type is not a supported simple type, on " + javaProperty);
			} else if (!isSimpleTypeSupportedByQDT(typeAdaptorInternalType, explicitQDTType)) {
				throw new InvalidDeclaredTypeException("Type adaptor's internal " + typeAdaptorInternalType.getSimpleName()
						+ " type is not compatible with " + explicitQDTType.getSimpleName()
						+ ", on " + javaProperty);
			}
		}

		// populate everything
		this.genericPropertyType = javaProperty.type();
		if (dbAdaptTypeAnnotation == null) {
			// populate when no annotation
			this.typeAdaptor = null;
			this.dbvPropertyType = (Class<? extends QueryableDatatype<BASETYPE>>) javaProperty.type();
			this.internalQdtSyncer = null;
		} else if (identityOnly) {
			// populate identity-only information when type adaptor declared
			Class<? extends QueryableDatatype<BASETYPE>> type = (Class<? extends QueryableDatatype<BASETYPE>>) explicitTypeOrNullOf(dbAdaptTypeAnnotation);
			if (type == null && typeAdaptorInternalType != null) {
				type = (Class<? extends QueryableDatatype<BASETYPE>>) inferredQDTTypeForSimpleType(typeAdaptorInternalType);
			}
			if (type == null) {
				throw new NullPointerException("null dbvPropertyType, this is an internal bug");
			}
			this.dbvPropertyType = type;

			this.typeAdaptor = null;
			this.internalQdtSyncer = null;
		} else {
			// initialise type adapting
			this.typeAdaptor = newTypeAdaptorInstanceGiven(javaProperty, dbAdaptTypeAnnotation);

			Class<? extends QueryableDatatype<?>> type = explicitTypeOrNullOf(dbAdaptTypeAnnotation);
			if (type == null && typeAdaptorInternalType != null) {
				type = inferredQDTTypeForSimpleType(typeAdaptorInternalType);
			}
			if (type == null) {
				throw new NullPointerException("null dbvPropertyType, this is an internal bug");
			}
			this.dbvPropertyType = (Class<? extends QueryableDatatype<BASETYPE>>) type;

			Class<?> internalLiteralType = literalTypeOf(type);
			Class<?> externalLiteralType;
			if (QueryableDatatype.class.isAssignableFrom(javaProperty.type())) {
				externalLiteralType = literalTypeOf((Class<? extends QueryableDatatype<?>>) javaProperty.type());
			} else {
				externalLiteralType = javaProperty.type();
			}

			if (QueryableDatatype.class.isAssignableFrom(javaProperty.type())) {
				this.internalQdtSyncer = new QueryableDatatypeSyncer(javaProperty.qualifiedName(),
						this.dbvPropertyType, internalLiteralType, externalLiteralType, this.typeAdaptor);
			} else {
				this.internalQdtSyncer = new SimpleValueQueryableDatatypeSyncer(javaProperty.qualifiedName(),
						this.dbvPropertyType, internalLiteralType, externalLiteralType, this.typeAdaptor);
			}
		}
	}

	/**
	 * Infers the QDT-type that corresponds to the given simple type. Used to
	 * infer the QDT-type that should be used internally, based on the type
	 * supplied by the type adaptor.
	 *
	 * <p>
	 * Make sure to keep this in sync with {@link #literalTypeOf}.
	 *
	 * @return a QDT the should work
	 */
	private static Class<? extends QueryableDatatype<?>> inferredQDTTypeForSimpleType(Class<?> simpleType) {
		if (simpleType.equals(String.class)) {
			return DBString.class;
		} else if (Number.class.isAssignableFrom(simpleType)) {
			if (Integer.class.isAssignableFrom(simpleType) || Long.class.isAssignableFrom(simpleType)) {
				return DBInteger.class;
			}
			if (Float.class.isAssignableFrom(simpleType) || Double.class.isAssignableFrom(simpleType)) {
				return DBNumber.class;
			} else {
				return DBNumber.class;
			}
		} else if (Date.class.isAssignableFrom(simpleType)) {
			return DBDate.class;
		} else if (Boolean.class.isAssignableFrom(simpleType)) {
			return DBBoolean.class;
		}

		// all remaining types require explicit declaration
		return null;
	}

	/**
	 *
	 * <p>
	 * Make sure to keep this in sync with {@link #inferredQDTTypeForSimpleType}.
	 *
	 *
	 * <p style="color: #F90;">Support DBvolution at
	 * <a href="http://patreon.com/dbvolution" target=new>Patreon</a></p>
	 *
	 * @return a standard Java class equivalent to the QDT
	 */
	private static Class<?> literalTypeOf(Class<? extends QueryableDatatype<?>> qdtType) {
		if (qdtType.equals(DBString.class)) {
			return String.class;
		} else if (qdtType.equals(DBStringTrimmed.class)) {
			return String.class;
		} else if (qdtType.equals(DBNumber.class)) {
			return Double.class;
		} else if (qdtType.equals(DBInteger.class)) {
			return Long.class;
		} else if (qdtType.equals(DBDate.class)) {
			return Date.class;
		} else if (qdtType.equals(DBBoolean.class)) {
			return Boolean.class;
		} else {
			throw new RuntimeException("Unrecognised QDT-type " + qdtType.getSimpleName());
		}
	}

	/**
	 * Tests whether the simpleType is supported by the given QDT-type. A simple
	 * type is supported by the QDT type iff the simple type implies a QDT-type,
	 * and:
	 * <ul>
	 * <li> the implied QDT-type is exactly the same as the given QDT-type, or
	 * <li> the implied QDT-type (eg: DBInteger) is instance-of assignable to the
	 * given QDT-type (eg: DBNumber), or
	 * <li> the implied QDT-type (eg: DBDate) is a super-class of the given given
	 * QDT-type (eg: DBSpecialDate).
	 * </ul>
	 *
	 *
	 *
	 * <p style="color: #F90;">Support DBvolution at
	 * <a href="http://patreon.com/dbvolution" target=new>Patreon</a></p>
	 *
	 * @return TRUE if the simple type can be replaced by a QDT
	 */
	private static boolean isSimpleTypeSupportedByQDT(Class<?> simpleType,
			Class<? extends QueryableDatatype<?>> qdtType) {
		Class<?> inferredQDTType = inferredQDTTypeForSimpleType(simpleType);
		if (inferredQDTType != null) {
			if (qdtType.isAssignableFrom(inferredQDTType) || inferredQDTType.isAssignableFrom(qdtType)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Internal helper to support the way annotation attribute defaulting works.
	 *
	 *
	 * <p style="color: #F90;">Support DBvolution at
	 * <a href="http://patreon.com/dbvolution" target=new>Patreon</a></p>
	 *
	 * @return A QDT Class
	 */
	private static Class<? extends QueryableDatatype<?>> explicitTypeOrNullOf(DBAdaptType annotation) {
		if (annotation == null) {
			return null;
		}

		// detect default
		if (annotation.type().equals(DBUnknownDatatype.class)) {
			return null;
		}

		// return value
		return annotation.type();
	}

	/**
	 * Gets the DBv-centric type of the property, possibly after type adaption.
	 */
	public Class<? extends QueryableDatatype<?>> getQueryableDatatypeClass() {
		return dbvPropertyType;
	}

	/**
	 * Gets the type of the property, possibly after type adaption.
	 */
	public Class<?> getGenericClass() {
		return genericPropertyType;
	}

	/**
	 * Indicates whether the property's type is adapted by an explicit or implicit
	 * type adaptor. (Note: at present there is no support for implicit type
	 * adaptors)
	 *
	 * <p style="color: #F90;">Support DBvolution at
	 * <a href="http://patreon.com/dbvolution" target=new>Patreon</a></p>
	 *
	 * @return TRUE if the property is type adapted
	 */
	public boolean isTypeAdapted() {
		return (dbAdaptTypeAnnotation != null);
	}

	/**
	 * Gets the DBv-centric value from the underlying java property, converting if
	 * needed. This method behaves correctly regardless of whether an
	 * {@link DBAdaptType} annotation is present.
	 *
	 * @param target object containing the property
	 * <p style="color: #F90;">Support DBvolution at
	 * <a href="http://patreon.com/dbvolution" target=new>Patreon</a></p>
	 * @return the DBv-centric property value
	 * @throws DBThrownByEndUserCodeException if any user code throws an exception
	 * @throws IllegalStateException if the underlying java property is not
	 * readable
	 */
	@SuppressWarnings("unchecked")
	public QueryableDatatype<BASETYPE> getJavaPropertyAsQueryableDatatype(Object target) {
		if (identityOnly) {
			throw new AssertionError("Attempt to read value from identity-only property");
		}

		// get via type adaptor and simple-type java property
		if (typeAdaptor != null && internalQdtSyncer instanceof SimpleValueQueryableDatatypeSyncer) {
			SimpleValueQueryableDatatypeSyncer syncer = (SimpleValueQueryableDatatypeSyncer) internalQdtSyncer;
			Object externalValue = javaProperty.get(target);

			// convert
			return (QueryableDatatype<BASETYPE>) syncer.setInternalQDTFromExternalSimpleValue(externalValue);
		} // get via type adaptor and QDT java property
		else if (typeAdaptor != null) {
			Object externalValue = javaProperty.get(target);

			// this should be completely safe by now
			QueryableDatatype<?> externalQdt = (QueryableDatatype<?>) externalValue;

			// convert
			return (QueryableDatatype<BASETYPE>) internalQdtSyncer.setInternalQDTFromExternalQDT(externalQdt);
		} // get directly without type adaptor
		// (note: type checking was performed at creation time)
		else {
			return (QueryableDatatype) javaProperty.get(target);
		}
	}

	/**
	 * Sets the underlying java property according to the given DBv-centric value.
	 * This method behaves correctly regardless of whether an {@link DBAdaptType}
	 * annotation is present.
	 *
	 * @param target object containing the property
	 *
	 * @throws DBThrownByEndUserCodeException if any user code throws an exception
	 * @throws IllegalStateException if the underlying java property is not
	 * writable
	 */
	public void setJavaPropertyAsQueryableDatatype(Object target, QueryableDatatype<?> dbvValue) {
		if (identityOnly) {
			throw new AssertionError("Attempt to write value to identity-only property");
		}

		// set via type adaptor and simple-type java property
		if (typeAdaptor != null && internalQdtSyncer instanceof SimpleValueQueryableDatatypeSyncer) {
			SimpleValueQueryableDatatypeSyncer syncer = (SimpleValueQueryableDatatypeSyncer) internalQdtSyncer;
			syncer.setInternalQueryableDatatype(dbvValue);
			Object externalValue = syncer.getExternalSimpleValueFromInternalQDT();

			javaProperty.set(target, externalValue);
		} // set via type adaptor and QDT java property
		else if (typeAdaptor != null) {
			Object externalValue = javaProperty.get(target);

			// this should be completely safe by now
			QueryableDatatype<?> externalQdt = (QueryableDatatype<?>) externalValue;

			// convert
			internalQdtSyncer.setInternalQueryableDatatype(dbvValue);
			externalQdt = internalQdtSyncer.setExternalFromInternalQDT(externalQdt);
			if (externalQdt == null && externalValue != null) {
				javaProperty.set(target, null);
			}
		} // set directly without type adaptor
		// (note: type checking was performed at creation time)
		else {
			javaProperty.set(target, dbvValue);
		}
	}

	/**
	 * Constructs a new instance of the type adaptor referenced by the given
	 * annotation instance. Handles all exceptions and throws them as the
	 * appropriate runtime exceptions
	 *
	 *
	 *
	 * <p style="color: #F90;">Support DBvolution at
	 * <a href="http://patreon.com/dbvolution" target=new>Patreon</a></p>
	 *
	 * @return a DBTypeAdaptor
	 * @throws DBRuntimeException on unexpected internal errors, and
	 * @throws InvalidDeclaredTypeException on errors with the end-user supplied
	 * code
	 */
	private static DBTypeAdaptor<Object, Object> newTypeAdaptorInstanceGiven(JavaProperty<?> property, DBAdaptType annotation) {
		Class<? extends DBTypeAdaptor<?, ?>> adaptorClass = annotation.value();
		if (adaptorClass == null) {
			// shouldn't be possible
			throw new DBRuntimeException("Encountered unexpected null " + DBAdaptType.class.getSimpleName()
					+ ".adptor() (probably a bug in DBvolution)");
		}

		if (adaptorClass.isInterface()) {
			throw new InvalidDeclaredTypeException("TypeAdaptor cannot be an interface (" + adaptorClass.getSimpleName()
					+ "), on property " + property.qualifiedName());
		}
		if (Modifier.isAbstract(adaptorClass.getModifiers())) {
			throw new InvalidDeclaredTypeException("TypeAdaptor cannot be an abstract class (" + adaptorClass.getSimpleName()
					+ "), on property " + property.qualifiedName());
		}

		try {
			adaptorClass.getConstructor().newInstance();
		} catch (InstantiationException | IllegalAccessException | NoSuchMethodException | SecurityException | IllegalArgumentException | InvocationTargetException e) {
			throw new InvalidDeclaredTypeException("Type adaptor " + adaptorClass.getName()
					+ " could not be constructed, on property "
					+ property.qualifiedName() + ": " + e.getMessage(), e);
		}

		// get default constructor
		Constructor<? extends DBTypeAdaptor<?, ?>> constructor;
		try {
			constructor = adaptorClass.getConstructor();
		} catch (NoSuchMethodException e) {
			throw new InvalidDeclaredTypeException("Type adaptor " + adaptorClass.getName()
					+ " has no default constructor, on property "
					+ property.qualifiedName(), e);
		} catch (SecurityException e) {
			// caused by a Java security manager or an attempt to access a non-visible field
			// without first making it visible
			throw new DBRuntimeException("Java security error retrieving constructor for " + adaptorClass.getName()
					+ ", referenced by property " + property.qualifiedName() + ": " + e.getLocalizedMessage(), e);
		}

		// construct adaptor instance
		DBTypeAdaptor<?, ?> instance;
		try {
			instance = constructor.newInstance();
		} catch (InstantiationException e) {
			throw new InvalidDeclaredTypeException(adaptorClass.getName() + " cannot be constructed (it is probably abstract), referenced by property "
					+ property.qualifiedName(), e);
		} catch (IllegalAccessException e) {
			// caused by a Java security manager or an attempt to access a non-visible field
			// without first making it visible
			throw new DBRuntimeException("Java security error instantiating " + adaptorClass.getName()
					+ ", referenced by property " + property.qualifiedName() + ": " + e.getLocalizedMessage(), e);
		} catch (IllegalArgumentException e) {
			// expected, so probably represents a bug
			throw new IllegalArgumentException("Internal error instantiating "
					+ adaptorClass.getName() + ", referenced by property " + property.qualifiedName() + ": " + e.getLocalizedMessage(), e);
		} catch (InvocationTargetException e) {
			// any checked or runtime exception thrown by the setter method itself
			Throwable cause = (e.getCause() == null) ? e : e.getCause();
			String msg = (cause.getLocalizedMessage() == null) ? "" : ": " + cause.getLocalizedMessage();
			throw new DBThrownByEndUserCodeException("Constructor threw " + cause.getClass().getSimpleName() + " when instantiating "
					+ adaptorClass.getName() + ", referenced by property " + property.qualifiedName() + msg, cause);
		}

		// downcast
		// (technically the instance is for <?,? extends QueryableDataType> but
		//  that can't be used reflectively when all we know is Object and QueryableDataType)
		@SuppressWarnings("unchecked")
		DBTypeAdaptor<Object, Object> result = (DBTypeAdaptor<Object, Object>) instance;
		return result;
	}
}