SafeOneWaySimpleTypeAdaptor.java

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

import java.io.Serializable;
import java.lang.reflect.Method;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import nz.co.gregs.dbvolution.datatypes.DBTypeAdaptor;
import nz.co.gregs.dbvolution.exceptions.DBThrownByEndUserCodeException;
import nz.co.gregs.dbvolution.internal.properties.InterfaceInfo.ParameterBounds;
import nz.co.gregs.dbvolution.internal.properties.InterfaceInfo.UnsupportedType;

/**
 * Internal class that wraps one direction of a {@link DBTypeAdaptor} with type
 * checking, meaningful error messages, and automatic casting between number
 * types.
 */
public class SafeOneWaySimpleTypeAdaptor implements Serializable {

	private static final long serialVersionUID = 1l;

	private static final Log log = LogFactory.getLog(SafeOneWaySimpleTypeAdaptor.class);

	/**
	 * Enumerates the possible directions that a QDT Sync operation can have.
	 *
	 */
	public static enum Direction {

		/**
		 * To DBvolution-centric type. toDatabaseValue() method
		 */
		TO_INTERNAL,
		/**
		 * To end-user declared type of field. fromDatabaseValue() method
		 */
		TO_EXTERNAL
	}

	private static final Method TO_EXTERNAL_METHOD;
	private static final Method TO_INTERNAL_METHOD;

	private static final SimpleCast[] SIMPLE_CASTS = {
		new NumberToShortCast(),
		new NumberToIntegerCast(),
		new NumberToLongCast(),
		new NumberToFloatCast(),
		new NumberToDoubleCast(),};

	private String propertyName;
	private Direction direction;

	private Class<?> sourceType;
	private transient SimpleCast sourceCast = null;
	private transient DBTypeAdaptor<Object, Object> typeAdaptor;
	private transient SimpleCast targetCast = null;
	private Class<?> targetType;

	static {
		try {
			TO_EXTERNAL_METHOD = DBTypeAdaptor.class.getMethod("fromDatabaseValue", Object.class);
		} catch (NoSuchMethodException e) {
			throw new RuntimeException(DBTypeAdaptor.class.getSimpleName() + " does not have a 'fromDatabaseValue' method", e);
		} catch (SecurityException e) {
			throw new RuntimeException(e);
		}

		try {
			TO_INTERNAL_METHOD = DBTypeAdaptor.class.getMethod("toDatabaseValue", Object.class);
		} catch (NoSuchMethodException e) {
			throw new RuntimeException(DBTypeAdaptor.class.getSimpleName() + " does not have a 'toDatabaseValue' method", e);
		} catch (SecurityException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 *
	 * <p>
	 * If {@code sourceType} is provided, then the inherent source type of the
	 * declared type adaptor type is checked against {@code sourceType} for
	 * compatibility.
	 *
	 * <p>
	 * If {@code targetType} is provided, then all conversions are type checked
	 * against {@code targetType} before returning from calls to
	 * {@link #convert(Object)}. {@code targetType} must be compatible with the
	 * target type inherent in the declaration of the type adaptor itself.
	 *
	 * @param propertyName propertyName
	 * @param sourceType type of variable from which input value is retrieved,
	 * optional
	 * @param direction direction
	 * @param typeAdaptor typeAdaptor
	 * @param targetType type of variable to which output value is to be assigned,
	 * optional
	 */
	@SuppressWarnings("unchecked")
	public SafeOneWaySimpleTypeAdaptor(String propertyName, DBTypeAdaptor<?, ?> typeAdaptor, Direction direction, Class<?> sourceType, Class<?> targetType) {
		this.propertyName = propertyName;
		this.direction = direction;
		this.typeAdaptor = (DBTypeAdaptor<Object, Object>) typeAdaptor;

		// infer typeAdaptor's source and target types
		try {
			InterfaceInfo interfaceInfo = new InterfaceInfo(DBTypeAdaptor.class, typeAdaptor);
			ParameterBounds[] parameterBounds = interfaceInfo.getInterfaceParameterValueBounds();

			ParameterBounds sourceBounds;
			ParameterBounds targetBounds;
			if (direction == Direction.TO_EXTERNAL) {
				sourceBounds = parameterBounds[1];
				targetBounds = parameterBounds[0];
			} else {
				sourceBounds = parameterBounds[0];
				targetBounds = parameterBounds[1];
			}

			if (sourceType != null && sourceBounds != null) {
				// sourceType must be at least one of the upper bound classes (if multi)
				boolean matched = false;
				for (Class<?> sourceBoundType : sourceBounds.upperClasses()) {
					SimpleCast cast = getSimpleCastFor(sourceType, sourceBoundType);
					if (cast != null || sourceBoundType.isAssignableFrom(sourceType)) {
						matched = true;
						this.sourceType = sourceType;
						this.sourceCast = cast;
						break;
					}
				}
				if (!matched) {
					throw new IllegalArgumentException("TypeAdaptor " + typeAdaptor.getClass().getSimpleName()
							+ " cannot be used with " + sourceType.getSimpleName() + " values");
				}
			} else if (sourceBounds != null && !sourceBounds.isUpperMulti()) {
				this.sourceType = sourceBounds.upperClass();
				//this.sourceCast = (this.sourceType == null) ? null : getSimpleCastFor(this.sourceType, null);
			}

			if (targetType != null && targetBounds != null) {
				// targetType must be at least one of the upper bound classes (if multi)
				boolean matched = false;
				for (Class<?> targetBoundType : targetBounds.upperClasses()) {
					SimpleCast cast = getSimpleCastFor(targetBoundType, targetType);
					if (cast != null || targetType.isAssignableFrom(targetBoundType)) {
						matched = true;
						this.targetCast = cast;
						this.targetType = targetType;
						break;
					}
				}
				if (!matched) {
					throw new IllegalArgumentException("TypeAdaptor " + typeAdaptor.getClass().getSimpleName()
							+ " cannot be used with " + targetType.getSimpleName() + " values");
				}
			} else if (targetBounds != null && !targetBounds.isUpperMulti()) {
				this.targetType = targetBounds.upperClass();
				//this.targetCast = (this.targetType == null) ? null : getSimpleCastFor(null, this.targetType);
			}

		} catch (UnsupportedType 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);
		} 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);
		}
	}

	@Override
	public String toString() {
		StringBuilder buf = new StringBuilder();
		buf.append(sourceType == null ? "unknown" : sourceType.getSimpleName());
		buf.append("-->");
		if (sourceCast != null) {
			buf.append("(");
			buf.append(sourceCast.getClass().getSimpleName());
			buf.append(")");
			buf.append("-->");
		}
		buf.append(typeAdaptor.getClass().getSimpleName());
		buf.append("-->");
		if (targetCast != null) {
			buf.append("(");
			buf.append(targetCast.getClass().getSimpleName());
			buf.append(")");
			buf.append("-->");
		}
		buf.append(targetType == null ? "unknown" : targetType.getSimpleName());

		return buf.toString();
	}

	/**
	 * Gets the expected type of source values passed to {@link #convert(Object)}.
	 *
	 * <p style="color: #F90;">Support DBvolution at
	 * <a href="http://patreon.com/dbvolution" target=new>Patreon</a></p>
	 *
	 * @return null if not constrained
	 */
	public Class<?> getSourceType() {
		return sourceType;
	}

	/**
	 * Gets the type that values are converted to, possibly including extra
	 * up-casting or down-casting as needed when converting between number types.
	 *
	 * <p style="color: #F90;">Support DBvolution at
	 * <a href="http://patreon.com/dbvolution" target=new>Patreon</a></p>
	 *
	 * @return null if not constrained
	 */
	public Class<?> getTargetType() {
		return targetType;
	}

	/**
	 * Uses the type adaptor to convert in the configured direction.
	 *
	 * @param value value
	 * <p style="color: #F90;">Support DBvolution at
	 * <a href="http://patreon.com/dbvolution" target=new>Patreon</a></p>
	 * @return the value supplied converted by the type adaptor
	 * @throws ClassCastException on type conversion failure
	 * @throws DBThrownByEndUserCodeException if the type adaptor throws an
	 * exception
	 */
	public Object convert(Object value) {
		String valStr = (value == null) ? "null" : value.getClass().getSimpleName() + "[" + value + "]";
		try {
			Object result = convertInternal(value);

			String resultStr = (result == null) ? "null" : result.getClass().getSimpleName() + "[" + result + "]";
			log.debug(this + " converting " + valStr + " ==> " + resultStr);
			return result;
		} catch (RuntimeException e) {
			log.debug(this + " converting " + valStr + " ==> " + e.getClass().getSimpleName());
			throw e;
		}
	}

	private Object convertInternal(Object incomingValue) {
		Object value = incomingValue;
		// validate source
		if (sourceCast != null && value != null) {
			if (!sourceCast.acceptsSource(value)) {
				throw new ClassCastException("Cannot pass " + value.getClass().getSimpleName()
						+ " to " + methodName()
						+ ", on property " + propertyName);
			}
		} else if (sourceType != null && value != null) {
			if (!sourceType.isInstance(value)) {
				throw new ClassCastException("Cannot pass " + value.getClass().getSimpleName()
						+ " to " + methodName()
						+ ", on property " + propertyName);
			}
		}

		// cast
		if (sourceCast != null) {
			value = sourceCast.cast(value);
		}

		// convert via type adaptor
		Object result;
		if (direction == Direction.TO_EXTERNAL) {
			try {
				result = typeAdaptor.fromDatabaseValue(value);
			} catch (NullPointerException e) {
				String msg = (e.getLocalizedMessage() == null) ? "" : ": " + e.getLocalizedMessage();
				throw new DBThrownByEndUserCodeException("Type adaptor " + typeAdaptor.getClass().getSimpleName() + " threw " + e.getClass().getSimpleName()
						+ " when getting property " + propertyName + msg + ": Please ensure that the fromDatabaseValue method handles database NULLs as well as normal values.", e);
			} catch (RuntimeException e) {
				String msg = (e.getLocalizedMessage() == null) ? "" : ": " + e.getLocalizedMessage();
				throw new DBThrownByEndUserCodeException("Type adaptor threw " + e.getClass().getSimpleName()
						+ " when getting property " + propertyName + msg, e);
			}
		} else {
			try {
				result = typeAdaptor.toDatabaseValue(value);
			} catch (NullPointerException e) {
				String msg = (e.getLocalizedMessage() == null) ? "" : ": " + e.getLocalizedMessage();
				throw new DBThrownByEndUserCodeException("Type adaptor " + typeAdaptor.getClass().getSimpleName() + " threw " + e.getClass().getSimpleName()
						+ " when setting property " + propertyName + msg + ": Please ensure that the toDatabaseValue method handles database NULLs as well as normal values.", e);
			} catch (RuntimeException e) {
				String msg = (e.getLocalizedMessage() == null) ? "" : ": " + e.getLocalizedMessage();
				throw new DBThrownByEndUserCodeException("Type adaptor threw " + e.getClass().getSimpleName()
						+ " when setting property " + propertyName + msg, e);
			}
		}

		// cast
		if (targetCast != null) {
			result = targetCast.cast(result);
		}

		// validate result
		if (targetType != null && result != null) {
			if (!targetType.isInstance(result)) {
				throw new ClassCastException("Cannot cast " + result.getClass().getSimpleName()
						+ " to " + targetType.getSimpleName()
						+ ", on property " + propertyName);
			}
		}
		return result;
	}

	private String methodName() {
		if (direction == Direction.TO_EXTERNAL) {
			return typeAdaptor.getClass().getSimpleName() + "." + TO_EXTERNAL_METHOD.getName() + "()";
		} else {
			return typeAdaptor.getClass().getSimpleName() + "." + TO_INTERNAL_METHOD.getName() + "()";
		}
	}

	/**
	 * Gets the appropriate simple cast or null if one doesn't exist
	 *
	 * @param sourceType the required source type, null to select by targetType
	 * only
	 * @param targetType the required target type, null to select by sourceType
	 * only
	 */
	static SimpleCast getSimpleCastFor(Class<?> sourceType, Class<?> targetType) {
		if (sourceType == null && targetType == null) {
			throw new NullPointerException("at least one of sourceType or targetType must be specified");
		}
		for (SimpleCast cast : SIMPLE_CASTS) {
			if ((sourceType == null || cast.sourceType().isAssignableFrom(sourceType))
					&& (targetType == null || targetType.isAssignableFrom(cast.targetType()))) {
				return cast;
			}
		}
		return null;
	}

	/**
	 * Used internally to handle automatic casting
	 */
	static interface SimpleCast {

		public boolean acceptsSource(Object value);

		public boolean acceptsSource(Class<?> type);

		public Object cast(Object value);

		public Class<?> sourceType();

		public Class<?> targetType();
	}

	private abstract static class BaseSimpleCast<S, T> implements SimpleCast {

		private Class<?> sourceType;
		private Class<?> targetType;

		public BaseSimpleCast() {
			InterfaceInfo interfaceInfo = new InterfaceInfo(BaseSimpleCast.class, this);
			ParameterBounds[] parameterBounds = interfaceInfo.getInterfaceParameterValueBounds();
			try {
				sourceType = parameterBounds[0].upperClass();
				targetType = parameterBounds[1].upperClass();
			} catch (UnsupportedType unexpected) {
				// not ever expecting this to occur
				throw new RuntimeException(unexpected);
			}
		}

		@Override
		public String toString() {
//			StringBuilder buf = new StringBuilder();
//			buf.append(sourceType == null ? "null" : sourceType.getSimpleName());
//			buf.append("-->");
//			buf.append(targetType == null ? "null" : targetType.getSimpleName());
//			return buf.toString();
			return getClass().getSimpleName();
		}

		@Override
		public Class<?> sourceType() {
			return sourceType;
		}

		@Override
		public Class<?> targetType() {
			return targetType;
		}

		@Override
		public boolean acceptsSource(Object value) {
			if (value == null) {
				return true;
			}
			return acceptsSource(value.getClass());
		}

		@Override
		public boolean acceptsSource(Class<?> type) {
			return sourceType.isAssignableFrom(type);
		}

		@Override
		public final Object cast(Object value) {
			if (value == null) {
				return null;
			}
			if (!sourceType().isInstance(value)) {
				throw new ClassCastException("Cannot cast " + value.getClass().getSimpleName() + " to " + sourceType().getSimpleName());
			}

			@SuppressWarnings("unchecked")
			T result = safeNonNullCast((S) value);

			if (!targetType().isInstance(result)) {
				throw new ClassCastException("Cannot cast " + result.getClass().getSimpleName() + " to " + targetType().getSimpleName());
			}
			return result;
		}

		protected abstract T safeNonNullCast(S value);
	}

	static class NumberToShortCast extends BaseSimpleCast<Number, Short> {

		@Override
		protected Short safeNonNullCast(Number value) {
			return value.shortValue();
		}
	}

	static class NumberToIntegerCast extends BaseSimpleCast<Number, Integer> {

		@Override
		protected Integer safeNonNullCast(Number value) {
			return value.intValue();
		}
	}

	static class NumberToLongCast extends BaseSimpleCast<Number, Long> {

		@Override
		protected Long safeNonNullCast(Number value) {
			return value.longValue();
		}
	}

	static class NumberToFloatCast extends BaseSimpleCast<Number, Float> {

		@Override
		protected Float safeNonNullCast(Number value) {
			return value.floatValue();
		}
	}

	static class NumberToDoubleCast extends BaseSimpleCast<Number, Double> {

		@Override
		protected Double safeNonNullCast(Number value) {
			return value.doubleValue();
		}
	}
}