All Downloads are FREE. Search and download functionalities are using the official Maven repository.

nz.co.gregs.dbvolution.internal.properties.RowDefinitionClassWrapper Maven / Gradle / Ivy

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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import nz.co.gregs.dbvolution.databases.DBDatabase;
import nz.co.gregs.dbvolution.annotations.DBTableName;
import nz.co.gregs.dbvolution.exceptions.DBRuntimeException;
import nz.co.gregs.dbvolution.exceptions.ReferenceToUndefinedPrimaryKeyException;
import nz.co.gregs.dbvolution.internal.properties.JavaPropertyFinder.PropertyType;
import nz.co.gregs.dbvolution.internal.properties.JavaPropertyFinder.Visibility;
import nz.co.gregs.dbvolution.query.RowDefinition;

/**
 * Wraps the class-type of an end-user's data model object. Generally it's
 * expected that the class is annotated with DBvolution annotations to mark the
 * table name and the fields or bean properties that map to columns, however
 * this class will work against any class type.
 *
 * 

* To wrap a target object instance, use the * {@link #instanceWrapperFor(nz.co.gregs.dbvolution.query.RowDefinition) } * method. * *

* Note: instances of this class are expensive to create, and are intended to be * cached and kept long-term. Instances can be safely shared between DBDatabase * instances for different database types. * *

* Instances of this class are thread-safe. * *

Support DBvolution at * Patreon

* * @author Malcolm Lett */ public class RowDefinitionClassWrapper { private final Class adapteeClass; private final boolean identityOnly; private final TableHandler tableHandler; /** * The property that forms the primary key, null if none. */ private final PropertyWrapperDefinition[] primaryKeyProperties; /** * All properties of which DBvolution is aware, ordered as first encountered. * Properties are only included if they are columns. */ private final List columnProperties; private final List autoFillingProperties; private final List allProperties; /** * Column names with original case for doing lookups on case-sensitive * databases. If column names duplicated, stores only the first encountered of * each column name. Assumes validation is done elsewhere in this class. Note: * doesn't need to be synchronized because it's never modified once created. */ private final Map columnPropertiesByCaseSensitiveColumnName; /** * Column names normalized to upper case for doing lookups on case-insensitive * databases. If column names duplicated, stores only the first encountered of * each column name. Assumes validation is done elsewhere in this class. Note: * doesn't need to be synchronized because it's never modified once created. */ private final Map columnPropertiesByUpperCaseColumnName; /** * Lists of properties that would have duplicated columns if-and-only-if using * a case-insensitive database. For each duplicate upper case column name, * lists all properties that have that same upper case column name. * *

* We don't know in advance whether the database in use is case-insensitive or * not. So we give case-different duplicates the benefit of doubt and just * record until later. If this class is accessed for use on a case-insensitive * database the exception will be thrown then, on first access to this class. */ private final Map> duplicatedColumnPropertiesByUpperCaseColumnName; /** * Indexed by java property name. */ private final Map columnPropertiesByPropertyName; /** * Fully constructs a wrapper for the given class, including performing all * validations that can be performed up front. * * @param clazz the {@code DBRow} class to wrap */ public RowDefinitionClassWrapper(Class clazz) { this(clazz, false); } /** * Internal constructor only. Pass {@code processIdentityOnly=true} when * processing a referenced class. * *

* When processing identity only, only the primary key properties are * identified. * * * @param processIdentityOnly pass {@code true} to only process the set of * columns and primary keys, and to ensure that the primary key columns are * valid, but to exclude all other validations on non-primary key columns and * types etc. */ RowDefinitionClassWrapper(Class clazz, boolean processIdentityOnly) { adapteeClass = clazz; identityOnly = processIdentityOnly; // annotation handlers tableHandler = new TableHandler(clazz); // pre-calculate properties list // (note: skip if processing identity only, in order to avoid // all the per-property validation) columnProperties = new ArrayList(); autoFillingProperties = new ArrayList(); allProperties = new ArrayList(); if (processIdentityOnly) { // identity-only: extract only primary key properties JavaPropertyFinder propertyFinder = getColumnPropertyFinder(); for (JavaProperty javaProperty : propertyFinder.getPropertiesOf(clazz)) { ColumnHandler column = new ColumnHandler(javaProperty); if (column.isColumn() && column.isPrimaryKey()) { PropertyWrapperDefinition property = new PropertyWrapperDefinition(this, javaProperty, processIdentityOnly); columnProperties.add(property); allProperties.add(property); } } } else { // extract all column properties int columnIndex = 0; JavaPropertyFinder propertyFinder = getColumnOrAutoFillablePropertyFinder(); for (JavaProperty javaProperty : propertyFinder.getPropertiesOf(clazz)) { PropertyWrapperDefinition property = new PropertyWrapperDefinition(this, javaProperty, processIdentityOnly); if (property.isColumn()) { columnIndex++; property.setColumnIndex(columnIndex); columnProperties.add(property); allProperties.add(property); } else { autoFillingProperties.add(property); allProperties.add(property); } } } // pre-calculate primary key List pkProperties = new ArrayList(); for (PropertyWrapperDefinition property : columnProperties) { if (property.isPrimaryKey()) { pkProperties.add(property); } } this.primaryKeyProperties = pkProperties.toArray(new PropertyWrapperDefinition[]{}); // if (primaryKeyProperties.size() > 1) { // throw new UnsupportedOperationException("Multi-Column Primary Keys are not yet supported: Please remove the excess @PrimaryKey statements from " + clazz.getSimpleName()); // } else { // this.primaryKeyProperty = primaryKeyProperties.isEmpty() ? null : primaryKeyProperties.get(0); // } // pre-calculate properties index columnPropertiesByCaseSensitiveColumnName = new HashMap(); columnPropertiesByUpperCaseColumnName = new HashMap(); columnPropertiesByPropertyName = new HashMap(); duplicatedColumnPropertiesByUpperCaseColumnName = new HashMap>(); for (PropertyWrapperDefinition property : allProperties) { // add unique values for case-insensitive lookups // (defer erroring until actually know database is case insensitive) if (property.isColumn()) { columnPropertiesByPropertyName.put(property.javaName(), property); // add unique values for case-sensitive lookups // (error immediately on collisions) if (columnPropertiesByCaseSensitiveColumnName.containsKey(property.getColumnName())) { if (!processIdentityOnly) { throw new ReferenceToUndefinedPrimaryKeyException("Class " + clazz.getName() + " has multiple properties for column " + property.getColumnName()); } } else { columnPropertiesByCaseSensitiveColumnName.put(property.getColumnName(), property); } if (columnPropertiesByUpperCaseColumnName.containsKey(property.getColumnName().toUpperCase())) { if (!processIdentityOnly) { List list = duplicatedColumnPropertiesByUpperCaseColumnName.get(property.getColumnName().toUpperCase()); if (list == null) { list = new ArrayList(); list.add(columnPropertiesByUpperCaseColumnName.get(property.getColumnName().toUpperCase())); } list.add(property); duplicatedColumnPropertiesByUpperCaseColumnName.put(property.getColumnName().toUpperCase(), list); } } else { columnPropertiesByUpperCaseColumnName.put(property.getColumnName().toUpperCase(), property); } } } } /** * Gets a new instance of the java property finder, configured as required * *

Support DBvolution at * Patreon

* * @return A new JavePropertyFinder for all fields, public methods, that have * DBColumn annotation, and are fields or beans. */ private static JavaPropertyFinder getColumnPropertyFinder() { return new JavaPropertyFinder( Visibility.PRIVATE, Visibility.PUBLIC, JavaPropertyFilter.COLUMN_PROPERTY_FILTER, PropertyType.FIELD, PropertyType.BEAN_PROPERTY); } private static JavaPropertyFinder getColumnOrAutoFillablePropertyFinder() { return new JavaPropertyFinder( Visibility.PRIVATE, Visibility.PUBLIC, JavaPropertyFilter.COLUMN_OR_AUTOFILLABLE_PROPERTY_FILTER, PropertyType.FIELD, PropertyType.BEAN_PROPERTY); } /** * Checks for errors that can't be known in advance without knowing the * database being accessed. * * @param database active database */ private void checkForRemainingErrorsOnAcccess(DBDatabase database) { // check for case-differing duplicate columns if (database.getDefinition().isColumnNamesCaseSensitive()) { if (!duplicatedColumnPropertiesByUpperCaseColumnName.isEmpty()) { StringBuilder buf = new StringBuilder(); for (List props : duplicatedColumnPropertiesByUpperCaseColumnName.values()) { for (PropertyWrapperDefinition property : props) { if (buf.length() > 0) { buf.append(", "); } buf.append(property.getColumnName()); } } throw new DBRuntimeException("The following columns are referenced multiple times on case-insensitive databases: " + buf.toString()); } } } /** * Gets an object wrapper instance for the given target object * * @param target the {@code DBRow} instance *

Support DBvolution at * Patreon

* @return A RowDefinitionInstanceWrapper for the supplied target. */ public RowDefinitionInstanceWrapper instanceWrapperFor(RowDefinition target) { if (identityOnly) { throw new AssertionError("Attempt to access non-identity information of identity-only DBRow class wrapper"); } // checkForRemainingErrorsOnAcccess(database); return new RowDefinitionInstanceWrapper(this, target); } /** * Gets a string representation suitable for debugging. * *

Support DBvolution at * Patreon

* * @return a string representation of this object. */ @Override public String toString() { if (isTable()) { return getClass().getSimpleName() + "<" + tableName() + ":" + adapteeClass.getName() + ">"; } else { return getClass().getSimpleName() + ""; } } /** * Two {@code RowDefinitionClassWrappers} are equal if they wrap the same * classes. * * @param obj obj *

Support DBvolution at * Patreon

* @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 RowDefinitionClassWrapper)) { return false; } RowDefinitionClassWrapper other = (RowDefinitionClassWrapper) obj; if (adapteeClass == null) { if (other.adapteeClass != null) { return false; } } else if (!adapteeClass.equals(other.adapteeClass)) { return false; } return true; } /** * Calculates the hash-code based on the hash-code of the wrapped class. * *

Support DBvolution at * Patreon

* * @return the hash-code */ @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((adapteeClass == null) ? 0 : adapteeClass.hashCode()); return result; } /** * Gets the underlying wrapped class. * *

Support DBvolution at * Patreon

* * @return the DBRow or Object wrapped by this instance. */ public Class adapteeClass() { return adapteeClass; } /** * Gets the simple name of the class being wrapped by this adaptor. *

* Use {@link #tableName()} for the name of the table mapped to this class. * *

* Equivalent to {@code this.adaptee().getSimpleName();} * *

Support DBvolution at * Patreon

* * @return the SimpleName of the class being wrapped. */ public String javaName() { return adapteeClass.getSimpleName(); } /** * Gets the fully qualified name of the class being wrapped by this adaptor. *

* Use {@link #tableName()} for the name of the table mapped to this class. * *

Support DBvolution at * Patreon

* * @return the fully qualified name of the class being wrapped. */ public String qualifiedJavaName() { return adapteeClass.getName(); } /** * Indicates whether this class maps to a database table. * *

Support DBvolution at * Patreon

* * @return TRUE if this RowDefinitionClassWrapper represents a database table * or view, otherwise FALSE. */ public boolean isTable() { return tableHandler.isTable(); } /** * Gets the indicated table name. Applies defaulting if the * {@link DBTableName} annotation is present but doesn't provide an explicit * table name. * *

* If the {@link DBTableName} annotation is missing, this method returns * {@code null}. * *

* Use {@link TableHandler#getDBTableNameAnnotation() } for low level access. * *

Support DBvolution at * Patreon

* * @return the table name, if specified explicitly or implicitly. */ public String tableName() { return tableHandler.getTableName(); } /** * *

Support DBvolution at * Patreon

* * @return the table name, if specified explicitly or implicitly. */ public String selectQuery() { return tableHandler.getSelectQuery(); } /** * Gets the property that is the primary key, if one is marked. Note: * multi-column primary key tables are not yet supported. * *

Support DBvolution at * Patreon

* * @return the primary key property or null if no primary key */ public PropertyWrapperDefinition[] primaryKeyDefinitions() { return Arrays.copyOf(primaryKeyProperties, primaryKeyProperties.length); } /** * Gets the property associated with the given column. If multiple properties * are annotated for the same column, this method will return only the first. * *

* Only provides access to properties annotated with {@code DBColumn}. * *

* Assumes validation is applied elsewhere to prohibit duplication of column * names. * * @param database active database * @param columnName columnName columnName * *

Support DBvolution at * Patreon

* @return the PropertyWrapperDefinition for the column name supplied. Null if * no such column is found. * @throws AssertionError if called when in {@code identityOnly} mode. */ public PropertyWrapperDefinition getPropertyDefinitionByColumn(DBDatabase database, String columnName) { if (identityOnly) { throw new AssertionError("Attempt to access non-identity information of identity-only DBRow class wrapper"); } checkForRemainingErrorsOnAcccess(database); if (database.getDefinition().isColumnNamesCaseSensitive()) { return columnPropertiesByUpperCaseColumnName.get(columnName.toUpperCase()); } else { return columnPropertiesByCaseSensitiveColumnName.get(columnName); } } /** * Like {@link #getPropertyDefinitionByColumn(DBDatabase, String)} except that * handles the case where the database definition is not yet known, and thus * returns all possible matching properties by column name. * *

* Assumes working in "identity-only" mode. * * *

Support DBvolution at * Patreon

* * @return the non-null list of matching property definitions, with only * identity information available, empty if no such properties found */ List getPropertyDefinitionIdentitiesByColumnNameCaseInsensitive(String columnName) { List list = new ArrayList(); JavaPropertyFinder propertyFinder = getColumnPropertyFinder(); for (JavaProperty javaProperty : propertyFinder.getPropertiesOf(adapteeClass)) { ColumnHandler column = new ColumnHandler(javaProperty); if (column.isColumn() && column.getColumnName().equalsIgnoreCase(columnName)) { PropertyWrapperDefinition property = new PropertyWrapperDefinition(this, javaProperty, true); list.add(property); } } return list; } /** * Gets the property by its java property name. *

* Only provides access to properties annotated with {@code DBColumn}. * *

* It's legal for a field and bean-property to have the same name, and to both * be annotated, but for different columns. This method doesn't handle that * well and returns only the first one it sees. * * @param propertyName propertyName *

Support DBvolution at * Patreon

* @return the PropertyWrapperDefinition for the named object property Null if * no such property is found. * @throws AssertionError if called when in {@code identityOnly} mode. */ public PropertyWrapperDefinition getPropertyDefinitionByName(String propertyName) { if (identityOnly) { throw new AssertionError("Attempt to access non-identity information of identity-only DBRow class wrapper"); } return columnPropertiesByPropertyName.get(propertyName); } /** * Gets all properties annotated with {@code DBColumn}. * *

Support DBvolution at * Patreon

* * @return a List of all PropertyWrapperDefinitions for the wrapped class. */ public List getColumnPropertyDefinitions() { if (identityOnly) { throw new AssertionError("Attempt to access non-identity information of identity-only DBRow class wrapper"); } return columnProperties; } /** * Gets all properties NOT annotated with {@code DBColumn}. * *

Support DBvolution at * Patreon

* * @return a List of all PropertyWrapperDefinitions for the wrapped class. */ public List getAutoFillingPropertyDefinitions() { if (identityOnly) { throw new AssertionError("Attempt to access non-identity information of identity-only DBRow class wrapper"); } return autoFillingProperties; } /** * Gets all foreign key properties. * *

Support DBvolution at * Patreon

* * @return a list of ProperyWrapperDefinitions for all the foreign keys * defined in the wrapped object */ public List getForeignKeyPropertyDefinitions() { if (identityOnly) { throw new AssertionError("Attempt to access non-identity information of identity-only DBRow class wrapper"); } List list = new ArrayList(); for (PropertyWrapperDefinition property : columnProperties) { if (property.isColumn() && property.isForeignKey()) { list.add(property); } } return list; } /** * Gets all primary key properties. * *

Support DBvolution at * Patreon

* * @return a list of ProperyWrapperDefinitions for all the foreign keys * defined in the wrapped object */ public List getPrimaryKeyPropertyDefinitions() { if (identityOnly) { throw new AssertionError("Attempt to access non-identity information of identity-only DBRow class wrapper"); } List list = new ArrayList(); for (PropertyWrapperDefinition property : columnProperties) { if (property.isColumn() && property.isPrimaryKey()) { list.add(property); } } return list; } String schemaName() { return tableHandler.getSchemaName(); } boolean isRequiredTable() { return tableHandler.isRequiredTable(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy