PropertyWrapperDefinition.java

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

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import nz.co.gregs.dbvolution.DBRow;
import nz.co.gregs.dbvolution.annotations.AutoFillDuringQueryIfPossible;
import nz.co.gregs.dbvolution.annotations.DBForeignKey;
import nz.co.gregs.dbvolution.databases.definitions.DBDefinition;
import nz.co.gregs.dbvolution.datatypes.DBEnumValue;
import nz.co.gregs.dbvolution.datatypes.DBLargeObject;
import nz.co.gregs.dbvolution.datatypes.InternalQueryableDatatypeProxy;
import nz.co.gregs.dbvolution.datatypes.QueryableDatatype;
import nz.co.gregs.dbvolution.exceptions.DBThrownByEndUserCodeException;
import nz.co.gregs.dbvolution.expressions.DBExpression;
import nz.co.gregs.dbvolution.query.RowDefinition;
import nz.co.gregs.dbvolution.results.Spatial2DResult;

/**
 * Abstracts a java field or bean-property as a DBvolution-centric property,
 * which contains values from a specific column in a database
 * table.Transparently handles all annotations associated with the property,
 * including type adaption.<p>
 * Provides access to the meta-data defined on a single java property of a
 * class, and provides methods for reading and writing the value of the property
 * on target objects. Instances of this class are not bound to specific target
 * objects, nor are they bound to specific database definitions.
 *
 * <p>
 * For binding to specific target objects and database definitions, use the
 * {@link PropertyWrapper} class.
 *
 * <p>
 * DB properties can be seen to have the types and values in the table that
 * follows. This class provides a virtual view over the property whereby the
 * DBv-centric type and value are easily accessible via the
 * {@link #getQueryableDatatype(nz.co.gregs.dbvolution.query.RowDefinition)  value()}
 * and
 * {@link #setQueryableDatatype(nz.co.gregs.dbvolution.query.RowDefinition, nz.co.gregs.dbvolution.datatypes.QueryableDatatype)  setValue()}
 * methods.
 * <ul>
 * <li> rawType/rawValue - the type and value actually stored on the declared
 * java property
 * <li> dbvType/dbvValue - the type and value used within DBv (a
 * QueryableDataType)
 * <li> databaseType/databaseValue - the type and value of the database column
 * itself (this class doesn't deal with these)
 * </ul>
 *
 * <p>
 * Note: instances of this class are expensive to create and should be cached.
 *
 * <p>
 * This class is <i>thread-safe</i>.
 *
 * <p>
 * This class is not serializable. References to it within serializable classes
 * should be marked as {@code transient}.
 *
 * @param <ROW> the class of the DBRow (probably) that this instance wraps
 * @param <BASETYPE> the class of the field (probably) that this instance wraps
 */
public class PropertyWrapperDefinition<ROW extends RowDefinition, BASETYPE extends Object> implements Serializable {

	private static final long serialVersionUID = 1l;

	private final RowDefinitionClassWrapper<ROW> classWrapper;
	private final JavaProperty<BASETYPE> javaProperty;

	private final ColumnHandler<BASETYPE> columnHandler;
	private final PropertyTypeHandler<BASETYPE> typeHandler;
	private final ForeignKeyHandler<?, BASETYPE> foreignKeyHandler;
	private final EnumTypeHandler<BASETYPE> enumTypeHandler;
	private boolean checkedForColumnExpression = false;
	private Integer columnIndex = null;
	private DBExpression[] columnExpression = new DBExpression[]{}; // empty if not present on propertyf
	public ArrayList<ColumnAspects> allColumnAspects = null;

	PropertyWrapperDefinition(RowDefinitionClassWrapper<ROW> classWrapper, JavaProperty<BASETYPE> javaProperty, boolean processIdentityOnly) {
		this.classWrapper = classWrapper;
		this.javaProperty = javaProperty;

		// handlers
		this.columnHandler = new ColumnHandler<>(javaProperty);
		this.typeHandler = new PropertyTypeHandler<>(javaProperty, processIdentityOnly);
		this.foreignKeyHandler = new ForeignKeyHandler<>(javaProperty, processIdentityOnly);
		this.enumTypeHandler = new EnumTypeHandler<>(javaProperty, this.columnHandler);
	}

	JavaProperty<BASETYPE> getRawJavaProperty() {
		return javaProperty;
	}

	/**
	 * Gets a string representation of the wrapped property, suitable for
	 * debugging and logging.
	 *
	 * @return a string representation of this object
	 */
	@Override
	public String toString() {
		StringBuilder buf = new StringBuilder();
		buf.append(type().getSimpleName());
		buf.append(" ");
		buf.append(qualifiedJavaName());
		if (!javaName().equalsIgnoreCase(getColumnName())) {
			buf.append("<").append(getColumnName()).append(">");
		}

		if (isTypeAdapted()) {
			buf.append(" (");
			buf.append(getRawJavaType().getSimpleName());
			buf.append(")");
		}
		return buf.toString();
	}

	/**
	 * Generates a hash-code of this property wrapper definition, based entirely
	 * on the java property it wraps.
	 *
	 * @return a hash-code.
	 */
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((javaProperty == null) ? 0 : javaProperty.hashCode());
		return result;
	}

	/**
	 * Equality of this property wrapper definition, based on the java property it
	 * wraps in a specific class. Two instances are identical if they wrap the
	 * same java property (field or bean-property) in the same class and the same
	 * class-loader.
	 *
	 * @param obj the other object to compare to. @return {@code true} if the two
	 * objects are equal, {@code false} otherwise.
	 */
	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (!(obj instanceof PropertyWrapperDefinition)) {
			return false;
		}
		var other = (PropertyWrapperDefinition) obj;
		if (javaProperty == null) {
			if (other.javaProperty != null) {
				return false;
			}
		} else if (!javaProperty.equals(other.javaProperty)) {
			return false;
		}
		return true;
	}

	/**
	 * Gets the name of the java property, without the containing class name.
	 * Mainly used within error messages. eg: {@code "uid"}
	 *
	 * <p>
	 * Use {@link #getColumnName()} to determine column name.
	 *
	 * @return a String of the Java field name for this property
	 */
	public String javaName() {
		return javaProperty.name();
	}

	/**
	 * Gets the partially qualified name of the underlying java property, using
	 * the short-name of the containing class. Mainly used within logging and
	 * error messages. eg: {@code "Customer.uid"}
	 *
	 * <p>
	 * Use {@link #getColumnName()} to determine column name.
	 *
	 * @return a String of the short name of the declared class of this property
	 */
	public String shortQualifiedJavaName() {
		return javaProperty.shortQualifiedName();
	}

	/**
	 * Gets the fully qualified name of the underlying java property, including
	 * the fully qualified name of the containing class. Mainly used within
	 * logging and error messages. eg:
	 * {@code "nz.co.mycompany.myproject.Customer.uid"}
	 *
	 * <p>
	 * Use {@link #getColumnName()} to determine column name.
	 *
	 * @return a String of the full name of the class of this property
	 */
	public String qualifiedJavaName() {
		return javaProperty.qualifiedName();
	}

	/**
	 * Gets the DBvolution-centric type of the property. If a type adaptor is
	 * present, then this is the type after conversion from the target object's
	 * actual property type.
	 *
	 * <p>
	 * Use {@link #getRawJavaType()} in the rare case that you need to know the
	 * underlying java property type.
	 *
	 * @return the Class of the internal QueryableDatatype used by this property
	 */
	@SuppressWarnings("unchecked")
	public Class<QueryableDatatype<?>> type() {
		return (Class<QueryableDatatype<?>>) typeHandler.getQueryableDatatypeClass();
	}

	/**
	 * Convenience method for testing the type.Equivalent to
	 * {@code refType.isAssignableFrom(this.type())}
	 *
	 * @param refType	refType @return TRUE if the supplied type is assignable from
	 * the internal QueryableDatatype, FALSE otherwise.
	 * @return true if refType will accept the wrapped field as a valid value
	 */
	public boolean isInstanceOf(Class<? extends QueryableDatatype<?>> refType) {
		return refType.isAssignableFrom(type());
	}

	/**
	 * Convenience method for testing the type. Equivalent to
	 * {@code this.type().isAssignableFrom(DBLargeObject.class)}.
	 *
	 * @return TRUE if a DBLargeObject is assignable from the internal
	 * QueryableDatatype, FALSE otherwise.
	 */
	public boolean isInstanceOfLargeObject() {
		return DBLargeObject.class.isAssignableFrom(type());
	}

	/**
	 * Gets the annotated table name of the table this property belongs to.
	 * Equivalent to {@code getRowDefinitionClassWrapper().tableName()}.
	 *
	 * @return a String of the table name containing this property.
	 */
	public String tableName() {
		return classWrapper.tableName();
	}

	/**
	 * Gets the annotated column name. Applies defaulting if the {@code DBColumn}
	 * annotation is present but does not explicitly specify the column name.
	 *
	 * <p>
	 * If the {@code DBColumn} annotation is missing, this method returns
	 * {@code null}.
	 *
	 * @return the column name, if specified explicitly or implicitly
	 */
	public String getColumnName() {
		return columnHandler.getColumnName();
	}

	/**
	 * Indicates whether this property is a column.
	 *
	 * @return {@code true} if this property is a column
	 */
	public boolean isColumn() {
		return columnHandler.isColumn();
	}

	/**
	 * Indicates whether this property is a primary key.
	 *
	 * @return {@code true} if this property is a primary key
	 */
	public boolean isPrimaryKey() {
		return columnHandler.isPrimaryKey();
	}

	/**
	 * Indicates whether this property is a foreign key.
	 *
	 * @return {@code true} if this property is a foreign key
	 */
	public boolean isForeignKey() {
		return foreignKeyHandler.isForeignKey();
	}

	/**
	 * Gets the class referenced by this property, if this property is a foreign
	 * key.
	 *
	 * @return the referenced class if this property is a foreign key; null if not
	 * a foreign key
	 */
	public Class<? extends DBRow> referencedClass() {
		return foreignKeyHandler.getReferencedClass();
	}

	/**
	 * Gets the table referenced by this property, if this property is a foreign
	 * key.
	 *
	 * @return the referenced table name if this property is a foreign key; null
	 * if not a foreign key
	 */
	public String referencedTableName() {
		return foreignKeyHandler.getReferencedTableName();
	}

	/**
	 * Gets the column name in the foreign table referenced by this property. The
	 * referenced column is either explicitly indicated by use of the
	 * {@link DBForeignKey#column()} attribute, or it is implicitly the single
	 * primary key of the referenced table if the {@link DBForeignKey#column()}
	 * attribute is unset.
	 *
	 * @return the referenced column name if this property is a foreign key; null
	 * if not a foreign key
	 */
	public String referencedColumnName() {
		return foreignKeyHandler.getReferencedColumnName();
	}

	/**
	 * Gets identity information for the referenced property in the referenced
	 * table. The referenced property is either explicitly indicated by use of the
	 * {@link DBForeignKey#column()} attribute, or it is implicitly the single
	 * primary key of the referenced table.
	 *
	 * <p>
	 * Note that the property definition returned provides identity of the
	 * property only. It provides access to the property's: java name, column
	 * name, type, and identity information about the table it belongs to (ie:
	 * table name). Attempts to get or set its value or get the type adaptor
	 * instance will result in an internal exception.
	 *
	 * @return the referenced property if this property is a foreign key; null if
	 * not a foreign key
	 */
	public PropertyWrapperDefinition<?, ?> referencedPropertyDefinitionIdentity() {
		return foreignKeyHandler.getReferencedPropertyDefinitionIdentity();
	}

	/**
	 * Gets the enum type, or null if not appropriate
	 *
	 * @return the enum type, which may also implement {@link DBEnumValue}
	 */
	public Class<? extends Enum<?>> getEnumType() {
		return enumTypeHandler.getEnumType();
	}

	/**
	 * Gets the type of the code supplied by enum values. This is derived from the
	 * {@link DBEnumValue} implementation in the enum.
	 *
	 * @return null if not known or not appropriate
	 */
	public Class<?> getEnumCodeType() {
		return enumTypeHandler.getEnumLiteralValueType();
	}

	/**
	 * Indicates whether the value of the property can be retrieved. Bean
	 * properties which are missing a 'getter' can not be read, but may be able to
	 * be set.
	 *
	 * @return TRUE if the property is readable, FALSE otherwise.
	 */
	public boolean isReadable() {
		return javaProperty.isReadable();
	}

	/**
	 * Indicates whether the value of the property can be modified. Bean
	 * properties which are missing a 'setter' can not be written to, but may be
	 * able to be read.
	 *
	 * @return TRUE if the property can be set, FALSE otherwise
	 */
	public boolean isWritable() {
		return javaProperty.isWritable();
	}

	/**
	 * 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)
	 *
	 * @return {@code true} if a type adaptor is being used
	 */
	public boolean isTypeAdapted() {
		return typeHandler.isTypeAdapted();
	}

	/**
	 * Gets the DBvolution-centric value of the property.The value returned may
	 * have undergone type conversion from the target object's actual property
	 * type, if a type adaptor is present.<p>
	 * Use {@link #isReadable()} beforehand to check whether the property can be
	 * read.
	 *
	 * @param target object instance containing this property @return the
	 * QueryableDatatype used internally.
	 * @return the appropriate QDT for this field
	 * @throws IllegalStateException if not readable (you should have called
	 * isReadable() first)
	 * @throws DBThrownByEndUserCodeException if any user code throws an exception
	 */
	public QueryableDatatype<BASETYPE> getQueryableDatatype(RowDefinition target) {
		QueryableDatatype<BASETYPE> qdt = typeHandler.getJavaPropertyAsQueryableDatatype(target);
		new InternalQueryableDatatypeProxy<BASETYPE>(qdt).setPropertyWrapper(this);
		return qdt;
	}

	/**
	 * Sets the DBvolution-centric value of the property. The value set may have
	 * undergone type conversion to the target object's actual property type, if a
	 * type adaptor is present.
	 *
	 * <p>
	 * Use {@link #isWritable()} beforehand to check whether the property can be
	 * modified.
	 *
	 * @param target object instance containing this property
	 * @param value value value
	 *
	 * @throws IllegalStateException if not writable (you should have called
	 * isWritable() first)
	 * @throws DBThrownByEndUserCodeException if any user code throws an exception
	 */
	public void setQueryableDatatype(RowDefinition target, QueryableDatatype<BASETYPE> value) {
		new InternalQueryableDatatypeProxy<BASETYPE>(value).setPropertyWrapper(this);
		typeHandler.setJavaPropertyAsQueryableDatatype(target, value);
	}

	/**
	 * Gets the value of the declared property in the end-user's target object,
	 * prior to type conversion to the DBvolution-centric type.
	 *
	 * <p>
	 * In most cases you will not need to call this method, as type conversion is
	 * done transparently via the {@link #getQueryableDatatype(nz.co.gregs.dbvolution.query.RowDefinition)
	 * } and
	 * {@link #setQueryableDatatype(nz.co.gregs.dbvolution.query.RowDefinition, nz.co.gregs.dbvolution.datatypes.QueryableDatatype) }
	 * methods.
	 *
	 * <p>
	 * Use {@link #isReadable()} beforehand to check whether the property can be
	 * read.
	 *
	 * @param target object instance containing this property @return value
	 * @return the field this wrapper wraps
	 * @throws IllegalStateException if not readable (you should have called
	 * isReadable() first)
	 * @throws DBThrownByEndUserCodeException if any user code throws an exception
	 */
	public Object rawJavaValue(Object target) {
		return javaProperty.get(target);
	}

	/**
	 * Set the value of the declared property in the end-user's target object,
	 * without type conversion to/from the DBvolution-centric type.
	 *
	 * <p>
	 * In most cases you will not need to call this method, as type conversion is
	 * done transparently via the {@link #getQueryableDatatype(nz.co.gregs.dbvolution.query.RowDefinition)
	 * } and
	 * {@link #setQueryableDatatype(nz.co.gregs.dbvolution.query.RowDefinition, nz.co.gregs.dbvolution.datatypes.QueryableDatatype) }
	 * methods.
	 *
	 * <p>
	 * Use {@link #isWritable()} beforehand to check whether the property can be
	 * modified.
	 *
	 * @param target object instance containing this property
	 * @param value new value
	 * @throws IllegalStateException if not writable (you should have called
	 * isWritable() first)
	 * @throws DBThrownByEndUserCodeException if any user code throws an exception
	 */
	public void setRawJavaValue(Object target, Object value) {
		javaProperty.set(target, value);
	}

	/**
	 * Gets the declared type of the property in the end-user's target object,
	 * prior to type conversion to the DBvolution-centric type.
	 *
	 * <p>
	 * In most cases you will not need to call this method, as type conversion is
	 * done transparently via the {@link #getQueryableDatatype(nz.co.gregs.dbvolution.query.RowDefinition)
	 * } and
	 * {@link #setQueryableDatatype(nz.co.gregs.dbvolution.query.RowDefinition, nz.co.gregs.dbvolution.datatypes.QueryableDatatype) }
	 * methods. Use the {@link #type()} method to get the DBv-centric property
	 * type, after type conversion.
	 *
	 * @return the declared class of the property
	 */
	public Class<?> getRawJavaType() {
		return javaProperty.type();
	}

	/**
	 * Gets the wrapper for the RowDefinition (DBRow or DBReport) subclass
	 * containing this property.
	 *
	 * @return the RowDefinitionClassWrapper representing the enclosing object of
	 * this property
	 */
	public RowDefinitionClassWrapper<ROW> getRowDefinitionClassWrapper() {
		return classWrapper;
	}

	/**
	 * @return the columnExpression
	 */
	public DBExpression[] getColumnExpression() {
		return Arrays.copyOf(columnExpression, columnExpression.length);
	}

	void setColumnExpression(DBExpression... expression) {
		columnExpression = Arrays.copyOf(expression, expression.length);
	}

	boolean hasColumnExpression() {
		return getColumnExpression().length > 0;
	}

	public synchronized List<ColumnAspects> getColumnAspects(DBDefinition defn, RowDefinition actualRow) {
		allColumnAspects = new ArrayList<ColumnAspects>();
		checkForColumnExpression(actualRow);
		if (hasColumnExpression()) {
			DBExpression[] columnExpression1 = getColumnExpression();
			for (DBExpression dBExpression : columnExpression1) {
				if (dBExpression != null) {
					allColumnAspects.add(new ColumnAspects(
							defn.transformToSelectableType(dBExpression).toSQLString(defn),
							defn.formatForColumnAlias(String.valueOf(dBExpression.hashCode())),
							dBExpression)
					);
				}
			}
		} else {
			allColumnAspects.add(new ColumnAspects(
					defn.formatTableAliasAndColumnName(actualRow, getColumnName()),
					defn.formatColumnNameForDBQueryResultSet(actualRow, getColumnName())
			));
		}
		return allColumnAspects;
	}

	String[] getColumnAlias(DBDefinition defn, RowDefinition actualRow) {
		checkForColumnExpression(actualRow);
		if (hasColumnExpression()) {
			ArrayList<String> strList = new ArrayList<String>();
			DBExpression[] columnExpression1 = getColumnExpression();
			for (DBExpression dBExpression : columnExpression1) {
				if (dBExpression != null) {
					final String formattedForColumnAlias = defn.formatForColumnAlias(String.valueOf(dBExpression.hashCode()));
					strList.add(formattedForColumnAlias);
				}
			}
			return strList.toArray(new String[]{});
		} else {
			return new String[]{defn.formatColumnNameForDBQueryResultSet(actualRow, getColumnName())};
		}
	}

	void checkForColumnExpression(RowDefinition actualRow) {
		if (!checkedForColumnExpression && !hasColumnExpression()) {
			Object value = this.getRawJavaProperty().get(actualRow);
			if (value != null && QueryableDatatype.class.isAssignableFrom(value.getClass())) {
				QueryableDatatype<?> qdt = (QueryableDatatype) value;
				if (qdt.hasColumnExpression()) {
					this.setColumnExpression(qdt.getColumnExpression());
				}
			}
			checkedForColumnExpression = true;
		}
	}

	boolean isForeignKeyTo(RowDefinition table) {
		return foreignKeyHandler.isForeignKeyTo(table.getClass());
	}

	boolean isRecursiveForeignKey() {
		return foreignKeyHandler.isForeignKeyTo(classWrapper.adapteeClass());
	}

	void setColumnIndex(int columnIndex) {
		this.columnIndex = columnIndex;
	}

	/**
	 * @return the columnIndex
	 */
	public Integer getColumnIndex() {
		return columnIndex;
	}

	boolean isAutoIncrementColumn() {
		return columnHandler.isAutoIncrement();
	}

	boolean isSpatial2DType() {
		Class<? extends QueryableDatatype<?>> qdt = type();
		return (Spatial2DResult.class.isAssignableFrom(qdt));
	}

	boolean isAutoFilling() {
		return this.javaProperty.isAnnotationPresent(AutoFillDuringQueryIfPossible.class);
	}

	Class<?> getAutoFillingClass() {
		return this.javaProperty.getAnnotation(AutoFillDuringQueryIfPossible.class).requiredClass();
	}

	boolean isLargeObject() {
		return DBLargeObject.class.isAssignableFrom(type());
	}

	public <R extends DBRow, A> int compareBetweenRows(R o1, R o2) {
		@SuppressWarnings("unchecked")
		QueryableDatatype<A> v1 = (QueryableDatatype<A>) getQueryableDatatype(o1);
		@SuppressWarnings("unchecked")
		QueryableDatatype<A> v2 = (QueryableDatatype<A>) getQueryableDatatype(o2);
		return v1.compareTo(v2);
	}
}