Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.techempower.data.EntityGroup Maven / Gradle / Ivy
/*******************************************************************************
* Copyright (c) 2018, TechEmpower, Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name TechEmpower, Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL TECHEMPOWER, INC. BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/
package com.techempower.data;
import gnu.trove.map.*;
import gnu.trove.map.hash.*;
import java.lang.reflect.*;
import java.sql.*;
import java.time.*;
import java.util.*;
import java.util.Date;
import java.util.concurrent.*;
import com.esotericsoftware.reflectasm.*;
import com.techempower.cache.*;
import com.techempower.collection.*;
import com.techempower.data.mapping.*;
import com.techempower.helper.*;
import com.techempower.reflect.*;
import com.techempower.util.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Manages interactions with a SQL database to store and retrieve entities of a
* given type.
*
* Example usage in EntityStore#initialize():
*
*
* public void initialize()
* {
* //
* // Foo objects are stored in a table named "foo" with id column "id".
* // They are sorted by id and don't write to a log.
* //
* register(EntityGroup.of(Foo.class));
* //
* // Bar objects are stored in a table named "bars" with id column "barid".
* // They are sorted by name and don't write to a log.
* //
* register(EntityGroup.of(Bar.class)
* .table("bars")
* .id("barid")
* .comparator(new Comparator<Bar>() {
* public int compare(Bar o1, Bar o2)
* {
* return ObjectHelper.compare(o1.getName(), o2.getName());
* }
* }));
* //
* // Baz objects are stored in a table named "the_baz" with id column "id".
* // They are sorted by id. They don't have a zero-argument constructor so
* // a factory method is supplied.
* //
* register(EntityGroup.of(Baz.class)
* .table("the_baz")
* .maker(new EntityMaker<Baz>() {
* public Baz make()
* {
* return new Baz(System.currentTimeMillis());
* }
* });
* }
*
* @param The type of entities managed by this object.
*/
public class EntityGroup
{
private static final Class>[] NO_PARAMETERS = new Class[0];
private static final Object[] NO_VALUES = new Object[0];
//
// Constants.
//
/**
* Compares entities by id. This is the default comparator used for sorting
* objects if no other is provided.
*/
private static final Comparator ID_COMPARATOR =
new Comparator() {
@Override
public int compare(Identifiable o1, Identifiable o2)
{
return Long.compare(o1.getId(), o2.getId());
}
};
/**
* Skips sorting entirely. This is the comparator to specify if your application
* prefers to avoid the performance cost (both in synchronization locks and data
* structure updates) of maintaining a sorted list of these entities.
*/
public static final Comparator NO_COMPARATOR =
new Comparator() {
@Override
public int compare(Identifiable o1, Identifiable o2)
{
// This should never be called.
throw new UnsupportedOperationException();
}
};
/**
* Gets a suitable default Comparator for the group, using the natural order
* if the type is Comparable, and the IDs if not.
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public static Comparator super C> defaultComparator(Class type)
{
if (Comparable.class.isAssignableFrom(type))
{
Comparator comparator = Comparator.naturalOrder();
return (Comparator)comparator;
}
else
{
return ID_COMPARATOR;
}
}
/**
* This is used to indicate that a field does not have a custom type adapter.
*/
private static final TypeAdapter NO_ADAPTER
= new TypeAdapter() {
@Override
public Object write(Object value)
{
throw new UnsupportedOperationException();
}
@Override
public Object read(Object value)
{
throw new UnsupportedOperationException();
}
};
//
// Protected fields.
//
private final EntityStore entityStore;
private final ConnectorFactory cf;
private final Class type;
private final String table;
private final String id;
private final String where;
private final String[] whereArguments;
private final EntityMaker maker;
private final Comparator super T> comparator;
private final MethodAccess access;
private final Logger log = LoggerFactory.getLogger(getClass());
private final String quotedTable;
private final String quotedIdField;
private final String getSingleQuery;
private final String deleteSingleQuery;
private final boolean readOnly;
private final boolean distribute;
private DataFieldToMethodMap[] setMethods = null;
private DataFieldToMethodMap[] getMethods = null;
private DataFieldToMethodMap[] getMethodsWithoutId = null;
private String fieldPartsForUpdate = null;
/**
* This maps fields to type adapters. If a field does not exist as a key in
* this map, it is unknown whether it has a type adapter. If a field is in
* this map and its value is {@link #NO_ADAPTER}, this the field does not have
* a type adapter.
*/
private final Map> typeAdaptersByFieldName
= new ConcurrentHashMap<>();
/**
* A unique identifier for this cache group to be assigned by the entity
* store as the group is registered.
*/
private int groupNumber;
//
// Protected constructor.
//
/**
* Returns a new entity group of the given type. This constructor is
* non-public because users should only instantiate this class by way of a
* {@link Builder}, which can be obtained from a call to
* {@link EntityGroup#of(Class)}.
*
* @param entityStore The EntityStore that manages this group.
* @param type The type of the entities.
* @param table The name of the database table that stores the entities.
* @param id The name of the database column that holds the identities of the
* entities.
* @param maker The generator to use when creating entities of this type.
* @param comparator The comparator to use when sorting entities of this type.
* @param where An optional WHERE clause (not including the "WHERE" keyword)
* in PreparedStatement form.
* @param whereArguments The arguments to insert into the WHERE clause.
*/
@SuppressWarnings("unchecked")
protected EntityGroup(EntityStore entityStore,
Class type,
String table,
String id,
EntityMaker maker,
Comparator super T> comparator,
String where,
String[] whereArguments,
boolean readOnly,
boolean distribute)
{
Objects.requireNonNull(entityStore, "EntityStore cannot be null.");
//
// Required fields.
//
this.type = type;
this.access = MethodAccess.get(this.type);
this.entityStore = entityStore;
this.cf = entityStore.getConnectorFactory();
this.readOnly = readOnly;
this.distribute = distribute;
//
// Optional fields.
//
this.maker = (maker == null)
? new EntityMaker() {
@Override
public T make() {
try
{
return EntityGroup.this.type.getConstructor(NO_PARAMETERS)
.newInstance();
}
catch (InstantiationException
| IllegalAccessException
| NoSuchMethodException
| InvocationTargetException e)
{
// Do nothing. We'll be returning null.
}
return null;
}
}
: maker;
this.table = (table == null)
? type.getSimpleName().toLowerCase()
: table;
this.id = (id == null)
? "id"
: id;
this.where = where;
this.whereArguments = (whereArguments != null ? whereArguments.clone() : null);
this.comparator = (Comparator super T>)((comparator == null)
? defaultComparator(type)
: comparator);
//
// SQL Queries.
//
this.quotedIdField = enquote(this.id);
this.quotedTable = enquote(this.table);
this.getSingleQuery = "SELECT * FROM " + quotedTable
+ " WHERE " + quotedIdField + " = ?";
this.deleteSingleQuery = "DELETE FROM " + quotedTable
+ " WHERE " + quotedIdField + " = ?";
}
//
// Field getter methods.
//
/**
* Returns the type of the entities.
*/
public Class type()
{
return this.type;
}
/**
* Returns the simple name of the type of the entities.
*/
public String name()
{
return this.type.getSimpleName();
}
/**
* Returns the name of the database table that stores the entities.
*/
public String table()
{
return this.table;
}
/**
* Returns the read-only state of the group.
*/
public boolean readOnly()
{
return this.readOnly;
}
/**
* Returns whether updates should be sent to DistributionListeners.
*/
public boolean distribute()
{
return this.distribute;
}
/**
* Returns the name of the database column that holds the identities of the
* entities.
*/
public String id()
{
return this.id;
}
/**
* Returns the generator to use when creating entities of this type.
*/
public EntityMaker maker()
{
return this.maker;
}
/**
* Resets this group of entities. In the base class, this doesn't do
* anything, but subclasses such as CacheGroup act differently.
*/
public void reset()
{
// Does nothing here.
}
/**
* Synchronously resets and re-initializes this group of entities. In
* the base class, this doesn't do anything, but subclasses such as
* CacheGroup act differently.
*/
public void resetSynchronous()
{
// Call reset() at least, for subclasses besides CacheGroup.
reset();
}
/**
* Returns the comparator to use when sorting entities of this type.
*/
public Comparator super T> comparator()
{
return this.comparator;
}
/**
* Gets the unique group number assigned by the entity store.
*
* @return the groupNumber
*/
public int getGroupNumber()
{
return this.groupNumber;
}
/**
* Allows the entity store to set the unique group number.
*
* @param groupNumber the groupNumber to set
*/
public void setGroupNumber(int groupNumber)
{
this.groupNumber = groupNumber;
}
/**
* Gets a reference to the type (Class) managed by this group.
*/
public Class getType()
{
return this.type;
}
//
// Database operations.
//
/**
* Returns the object with the given id, or null if there is no such object.
*/
public T get(long idToGet)
{
return rawGet(idToGet);
}
/**
* For use by subclasses. Not intended for use by client code.
*/
protected T rawGet(long idToGet)
{
try (
ConnectionMonitor monitor = this.cf.getConnectionMonitor();
PreparedStatement statement = monitor.getConnection().prepareStatement(
this.getSingleQuery + getWhereClause(" AND ") + ";",
ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_READ_ONLY)
)
{
statement.setLong(1, idToGet);
attachWhereArguments(2, statement);
try (ResultSet resultSet = statement.executeQuery())
{
if (resultSet.next())
{
return make(resultSet);
}
}
}
catch (Exception e)
{
throw new EntityException(this.name() + " Exception during SELECT.", e);
}
return null;
}
/**
* Put an object into the data store (and cache if applicable). This will
* always persist the object to the data store; caching is only applicable
* when using the CacheGroup subclass or something similar. Sets the
* initialized flag on the entity as a precaution, in case the calling
* code did not do so.
*
* @param object The object to put ("put" means persist to disk and update in
* cache if this is a cache group.)
* @return The JDBC Statement.executeUpdate() return value.
*/
public int put(T object)
{
if (readOnly)
{
throw new EntityException("EntityGroup for " + name()
+ " is read-only. The \"put\" method is not permitted.");
}
// Call the initialize method to set the initialized flag as a precaution
// if the calling code did not do so.
if (object instanceof Initializable)
{
final Initializable initializable = (Initializable)object;
if (!initializable.isInitialized())
{
initializable.initialize();
}
}
if (isPersisted(object))
{
return update(object);
}
else
{
return insert(object);
}
}
/**
* Put objects into the data store (and cache if applicable). This will
* always persist the objects to the data store; caching is only applicable
* when using the CacheGroup subclass or something similar.
*
* @param objects The objects to put ("put" means persist to disk and update
* in cache if this is a cache group.)
* @return Sum of the positive JDBC Statement.executeUpdate() return values.
*/
@SafeVarargs
public final int putAll(T... objects)
{
return putAll(CollectionHelper.toList(objects));
}
/**
* Put objects into the data store (and cache if applicable). This will
* always persist the objects to the data store; caching is only applicable
* when using the CacheGroup subclass or something similar.
*
* @param objects The objects to put ("put" means persist to disk and update
* in cache if this is a cache group.)
* @return Sum of the positive JDBC Statement.executeUpdate() return values.
*/
public int putAll(Collection objects)
{
if (readOnly)
{
throw new EntityException("EntityGroup for " + name()
+ " is read-only. The \"putAll\" method is not permitted.");
}
if ( (objects == null)
|| (objects.isEmpty())
)
{
return 0;
}
int rowsUpdated = 0;
// If the size of the collection is fewer than 100, just call put()
// in a loop. In testing, we've not observed a benefit to using batch
// updates (as provided by updateAll).
if (objects.size() < 100)
{
for (T object : objects)
{
int c = put(object);
if (c > 0)
{
// Only accumulate positive values;
rowsUpdated += c;
}
}
}
else
{
List persisted = null;
List nonPersisted = null;
for (T object : objects)
{
if (isPersisted(object))
{
if (persisted == null)
{
persisted = new ArrayList<>(objects.size());
}
persisted.add(object);
}
else
{
if (nonPersisted == null)
{
nonPersisted = new ArrayList<>(objects.size());
}
nonPersisted.add(object);
}
}
rowsUpdated += updateAll(persisted);
rowsUpdated += insertAll(nonPersisted);
}
return rowsUpdated;
}
/**
* Remove an entity from the database (and cache if applicable).
*/
public void remove(long idToRemove)
{
if (readOnly)
{
throw new EntityException("EntityGroup for " + name()
+ " is read-only. The \"remove\" method is not permitted.");
}
try (
ConnectionMonitor monitor = this.cf.getConnectionMonitor();
PreparedStatement statement = monitor.getConnection().prepareStatement(
this.deleteSingleQuery + getWhereClause(" AND ") + ";")
)
{
statement.setLong(1, idToRemove);
attachWhereArguments(2, statement);
//this.log.debug(statement.toString());
statement.executeUpdate();
}
catch (Exception e)
{
throw new EntityException(this.name() + " Exception during DELETE.", e);
}
}
/**
* Remove the given entities from the database (and cache if applicable).
*/
public void removeAll(Collection ids)
{
if (readOnly)
{
throw new EntityException("EntityGroup for " + name()
+ " is read-only. The \"removeAll\" method is not permitted.");
}
if (ids.isEmpty())
{
return;
}
try (
ConnectionMonitor monitor = this.cf.getConnectionMonitor();
PreparedStatement statement = monitor.getConnection().prepareStatement(
"DELETE FROM " + quotedTable
+ " WHERE " + quotedIdField + " IN ("
+ StringHelper.join(",", Collections.nCopies(ids.size(), "?"))
+ ")" + getWhereClause(" AND ") + ";")
)
{
int i = 0;
for (long longToDelete : ids)
{
statement.setLong(++i, longToDelete);
}
attachWhereArguments(ids.size() + 1, statement);
//this.log.debug(statement.toString());
statement.executeUpdate();
}
catch (Exception e)
{
throw new EntityException(this.name() + " Exception during DELETE (removeAll).", e);
}
}
/**
* Returns the current size of the entity group.
*/
public int size()
{
return rawSize();
}
/**
* For use by subclasses. Not intended for use by client code.
*/
protected int rawSize()
{
try (
ConnectionMonitor monitor = this.cf.getConnectionMonitor();
PreparedStatement statement = monitor.getConnection().prepareStatement(
"SELECT COUNT(*) FROM " + quotedTable
+ getWhereClause(" WHERE ")
+ ";",
ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_READ_ONLY)
)
{
attachWhereArguments(1, statement);
//this.log.debug(statement.toString());
try (ResultSet resultSet = statement.executeQuery())
{
resultSet.next();
return resultSet.getInt(1);
}
}
catch (Exception e)
{
throw new EntityException(this.name() + " Exception during SELECT (size).", e);
}
}
/**
* Returns a sorted list of all objects in the database (or cache if
* applicable).
*/
public List list()
{
return rawList();
}
/**
* For use by subclasses. Not intended for use by client code.
*/
protected List rawList()
{
final List objects = new ArrayList<>();
try (
ConnectionMonitor monitor = this.cf.getConnectionMonitor();
PreparedStatement statement = monitor.getConnection().prepareStatement(
"SELECT * FROM " + quotedTable
+ getWhereClause(" WHERE ")
+ ";",
ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_READ_ONLY)
)
{
attachWhereArguments(1, statement);
//this.log.debug(statement.toString());
try (ResultSet resultSet = statement.executeQuery())
{
while (resultSet.next())
{
T object = make(resultSet);
objects.add(object);
}
}
}
catch (Exception e)
{
throw new EntityException(this.name() + " Exception during SELECT (list).", e);
}
// Skip sorting if not desired.
if (this.comparator != NO_COMPARATOR)
{
Collections.sort(objects, this.comparator);
}
return objects;
}
/**
* Returns a list of objects with the given ids. The objects are in the
* order specified by the given ids. The returned list will not include
* nulls.
*/
public List list(Collection ids)
{
return rawList(ids);
}
/**
* For use by subclasses. Not intended for use by client code.
*/
protected List rawList(Collection ids)
{
final TLongObjectMap map = map(ids);
final List list = new ArrayList<>(ids.size());
for (long idToList : ids)
{
final T object = map.get(idToList);
if (object != null)
{
list.add(object);
}
}
return list;
}
/**
* Returns a map of all objects in the database (or cache if applicable),
* mapped by id.
*/
public TLongObjectMap map()
{
return rawMap();
}
/**
* For use by subclasses. Not intended for use by client code.
*/
protected TLongObjectMap rawMap()
{
final TLongObjectMap objects = new TLongObjectHashMap<>();
try (
ConnectionMonitor monitor = this.cf.getConnectionMonitor();
PreparedStatement statement = monitor.getConnection().prepareStatement(
"SELECT * FROM " + quotedTable
+ getWhereClause(" WHERE ")
+ ";",
ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_READ_ONLY)
)
{
attachWhereArguments(1, statement);
//this.log.debug(statement.toString());
try (ResultSet resultSet = statement.executeQuery())
{
while (resultSet.next())
{
T object = make(resultSet);
objects.put(object.getId(), object);
}
}
}
catch (Exception e)
{
throw new EntityException(this.name() + " Exception during SELECT (map).", e);
}
return objects;
}
/**
* Returns a map of objects with the given ids.
*/
public TLongObjectMap map(Collection ids)
{
return rawMap(ids);
}
/**
* For use by subclasses. Not intended for use by client code.
*/
protected TLongObjectMap rawMap(Collection ids)
{
if (ids.isEmpty())
{
return new TLongObjectHashMap<>(0);
}
final TLongObjectMap objects = new TLongObjectHashMap<>(ids.size());
try (
ConnectionMonitor monitor = this.cf.getConnectionMonitor();
PreparedStatement statement = monitor.getConnection().prepareStatement(
"SELECT * FROM " + quotedTable
+ " WHERE " + quotedIdField + " IN ("
+ StringHelper.join(",", Collections.nCopies(ids.size(), "?"))
+ ")" + getWhereClause(" AND ") + ";",
ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_READ_ONLY)
)
{
int i = 0;
for (long idToSet : ids)
{
statement.setLong(++i, idToSet);
}
attachWhereArguments(ids.size() + 1, statement);
//this.log.debug(statement.toString());
try (ResultSet resultSet = statement.executeQuery())
{
while (resultSet.next())
{
T object = make(resultSet);
objects.put(object.getId(), object);
}
}
}
catch (Exception e)
{
throw new EntityException(this.name() + " Exception during SELECT (map).", e);
}
return objects;
}
/**
* Returns the lowest identity assigned to an entity. Returns 0 if no
* result can be computed.
*/
public long lowest()
{
return rawLowest();
}
/**
* For use by subclasses. Not intended for use by client code.
*/
protected long rawLowest()
{
return identityAggregate("MIN");
}
/**
* Returns the highest identity assigned to an entity. Returns 0 if no
* result can be computed.
*/
public long highest()
{
return rawHighest();
}
/**
* For use by subclasses. Not intended for use by client code.
*/
protected long rawHighest()
{
return identityAggregate("MAX");
}
/**
* Runs an arbitrary SQL query that must return a resultset that is
* exactly comparable to the standard resultsets used by the list() method,
* including the order of the columns. ResultSet indexes rather than field
* names are used to deserialize results.
*
* The results are captured into a List and returned. Does not use the
* usual comparator to sort the results.
*
* Use this method at your own risk since its usage is considered non-
* standard.
*
* @param query Any old SQL query. Can use "?" marks in place of values.
* @param arguments The values to substitute for the "?" marks in the query.
* @return A list of entities, hopefully.
* @throws SQLException If you messed up.
*/
public List query(String query, Object... arguments) throws SQLException
{
final List objects = new ArrayList<>();
try (
ConnectionMonitor monitor = this.cf.getConnectionMonitor();
PreparedStatement statement = monitor.getConnection().prepareStatement(query)
)
{
attachArguments(statement, arguments);
try (ResultSet resultSet = statement.executeQuery())
{
while (resultSet.next())
{
T object = make(resultSet);
objects.add(object);
}
}
}
return objects;
}
/**
* Runs an arbitrary SQL query that must return a resultset that is
* exactly comparable to the standard resultsets used by the list() method,
* including the order of the columns. ResultSet indexes rather than field
* names are used to deserialize results.
*
* The first result is captured and returned, assuming a result is present
* at all.
*
* Use this method at your own risk since its usage is considered non-
* standard.
*
* @param query Any old SQL query. Can use "?" marks in place of values.
* @param arguments The values to substitute for the "?" marks in the query.
* @return A single entity, assuming a compatible resultset with at least
* one row is returned by the query.
* @throws SQLException If you messed up.
*/
public T querySingle(String query, Object... arguments) throws SQLException
{
T object = null;
try (
ConnectionMonitor monitor = this.cf.getConnectionMonitor();
PreparedStatement statement = monitor.getConnection().prepareStatement(query)
)
{
attachArguments(statement, arguments);
try (ResultSet resultSet = statement.executeQuery())
{
if (resultSet.next())
{
object = make(resultSet);
}
}
}
return object;
}
/**
* Attach an arbitrary list of arguments to a PreparedStatement.
*/
private void attachArguments(PreparedStatement statement, Object... arguments)
throws SQLException
{
int index = 1;
for (Object argument : arguments)
{
if (argument instanceof Date)
{
statement.setDate(index++, new java.sql.Date(((Date)argument).getTime()));
}
else
{
statement.setObject(index++, argument);
}
}
}
/**
* Called by put(object) to insert the object into the database. If its
* identity is zero, it is assumed the database will generate and return an
* auto-incremented id. If its identity is greater than zero, the object
* will be inserted with that id.
* @return The number of affected rows returned by the JDBC connection.
* @return The JDBC Statement.executeUpdate() return value.
*/
protected int insert(T object)
{
// Include the ID field if it has been specified already by the object.
final DataFieldToMethodMap[] fields = (object.getId() > 0)
? getGetMethodMappingCache()
: getGetMethodMappingCacheWithoutId();
final StringList fieldsPart = new StringList(", ");
for (DataFieldToMethodMap field : fields)
{
fieldsPart.add(enquote(field.getFieldName()));
}
try (
ConnectionMonitor monitor = this.cf.getConnectionMonitor();
PreparedStatement statement = monitor.getConnection().prepareStatement(
"INSERT INTO " + quotedTable + " ("
+ fieldsPart.toString() + ") VALUES ("
+ StringHelper.join(", ", Collections.nCopies(fields.length, "?"))
+ ");",
Statement.RETURN_GENERATED_KEYS)
)
{
int index = 1;
for (DataFieldToMethodMap field : fields)
{
final Object value = readValueForUpdate(object, field);
applyValueToStatement(field, value, statement, index++);
}
//this.log.debug(statement.toString());
int rowsUpdated = statement.executeUpdate();
// If the entity is persistence aware, let's inform it that it has been
// persisted.
if (object instanceof PersistenceAware)
{
((PersistenceAware)object).setPersisted(true);
}
if (object.getId() <= 0)
{
// Gather the new identity from the Statement.
try (ResultSet resultSet = statement.getGeneratedKeys())
{
if (resultSet.next())
{
object.setId(resultSet.getLong(1));
}
else
{
throw new EntityException(this.name() + " Identity not returned from INSERT.");
}
}
}
return rowsUpdated;
}
catch (SQLException e)
{
throw new EntityException(this.name() + " Exception during INSERT.", e);
}
}
/**
* Called by putAll(objects) to insert the objects into the database. If a
* given object's identity is zero, it is assumed the database will generate
* and return an auto-incremented id. If its identity is greater than zero,
* the object will be inserted with that id.
* @return Sum of the positive JDBC Statement.executeUpdate() return values.
*/
protected int insertAll(Collection objects)
{
if ( (objects == null)
|| (objects.isEmpty())
)
{
return 0;
}
// First, subdivide this into objects with id and those without. Different
// logic will be used to insert each.
List objectsWithId = new ArrayList<>(objects.size());
List objectsWithoutId = new ArrayList<>(objects.size());
int rowsUpdated = 0;
for (T object : objects)
{
if (object.getId() > 0)
{
objectsWithId.add(object);
}
else
{
objectsWithoutId.add(object);
}
}
DataFieldToMethodMap[] cache = getGetMethodMappingCache();
// Find the list of fields to be included in the update. The id will be
// included if it's greater than zero.
List fieldsWithId = new ArrayList<>();
List fieldsWithoutId = new ArrayList<>();
for (DataFieldToMethodMap field : cache)
{
fieldsWithId.add(field);
if (!field.getFieldName().equalsIgnoreCase(this.id))
{
fieldsWithoutId.add(field);
}
}
StringList fieldsPartWithId = new StringList(", ");
for (DataFieldToMethodMap field : fieldsWithId)
{
fieldsPartWithId.add(enquote(field.getFieldName()));
}
StringList fieldsPartWithoutId = new StringList(", ");
for (DataFieldToMethodMap field : fieldsWithoutId)
{
fieldsPartWithoutId.add(enquote(field.getFieldName()));
}
try (ConnectionMonitor monitor = this.cf.getConnectionMonitor())
{
try (PreparedStatement statementWithId = monitor.getConnection().prepareStatement(
"INSERT INTO " + quotedTable + " ("
+ fieldsPartWithId.toString() + ") VALUES ("
+ StringHelper.join(", ", Collections.nCopies(fieldsWithId.size(), "?"))
+ ");"))
{
try (PreparedStatement statementWithoutId = monitor.getConnection().prepareStatement(
"INSERT INTO " + quotedTable + " ("
+ fieldsPartWithoutId.toString() + ") VALUES ("
+ StringHelper.join(", ", Collections.nCopies(fieldsWithoutId.size(), "?"))
+ ");",
Statement.RETURN_GENERATED_KEYS))
{
for (T object : objectsWithId)
{
int index = 1;
for (DataFieldToMethodMap field : fieldsWithId)
{
Object value = readValueForUpdate(object, field);
applyValueToStatement(
field,
value,
statementWithId,
index++);
}
statementWithId.addBatch();
}
for (T object : objectsWithoutId)
{
int index = 1;
for (DataFieldToMethodMap field : fieldsWithoutId)
{
Object value = readValueForUpdate(object, field);
applyValueToStatement(
field,
value,
statementWithoutId,
index++);
}
statementWithoutId.addBatch();
}
if (!objectsWithId.isEmpty())
{
//this.log.debug(statementWithId.toString());
rowsUpdated += accumulatePositiveValues(statementWithId.executeBatch());
}
if (!objectsWithoutId.isEmpty())
{
//this.log.debug(statementWithoutId.toString());
rowsUpdated += accumulatePositiveValues(statementWithoutId.executeBatch());
// Gather the new ids from the Statement.
try (ResultSet resultSet = statementWithoutId.getGeneratedKeys())
{
int i = 0;
while (resultSet.next())
{
long identity = resultSet.getLong(1);
objectsWithoutId.get(i++).setId(identity);
}
if (i != objectsWithoutId.size())
{
throw new EntityException(this.name() + " One or more identities not returned after INSERT.");
}
}
}
for (T object : objects)
{
// If the entity is persistence aware, let's inform it that it has been
// persisted.
if (object instanceof PersistenceAware)
{
((PersistenceAware)object).setPersisted(true);
}
}
}
}
return rowsUpdated;
}
catch (SQLException e)
{
throw new EntityException(this.name() + " Exception during INSERT.", e);
}
}
/**
* Called by put(object) to update the object in the database and returns
* its id.
* @return The JDBC Statement.executeUpdate() return value.
*/
protected int update(T object)
{
// Include every field in the update except the id.
final DataFieldToMethodMap[] fields = getGetMethodMappingCacheWithoutId();
final String fieldParts = getFieldPartsForUpdate();
try (
ConnectionMonitor monitor = this.cf.getConnectionMonitor();
final PreparedStatement statement = monitor.getConnection().prepareStatement(
"UPDATE " + quotedTable + " SET " + fieldParts
+ " WHERE " + quotedIdField + " = ?"
+ getWhereClause(" AND ") + ";")
)
{
statement.setLong(fields.length + 1, object.getId());
attachWhereArguments(fields.length + 2, statement);
int index = 1;
for (DataFieldToMethodMap field : fields)
{
final Object value = readValueForUpdate(object, field);
applyValueToStatement(field, value, statement, index++);
}
//this.log.debug(statement.toString());
return statement.executeUpdate();
}
catch (SQLException e)
{
throw new EntityException(this.name() + " Exception during UPDATE.", e);
}
}
/**
* Called by put(objects) to update the objects in the database.
* @return Sum of the positive JDBC Statement.executeUpdate() return values.
*/
protected int updateAll(Collection objects)
{
if ( (objects == null)
|| (objects.isEmpty())
)
{
return 0;
}
final DataFieldToMethodMap[] fields = getGetMethodMappingCacheWithoutId();
final String fieldParts = getFieldPartsForUpdate();
try (
ConnectionMonitor monitor = this.cf.getConnectionMonitor();
PreparedStatement statement = monitor.getConnection().prepareStatement(
"UPDATE " + quotedTable + " SET " + fieldParts
+ " WHERE " + quotedIdField + " = ?"
+ getWhereClause(" AND ") + ";")
)
{
for (T object : objects)
{
statement.setLong(fields.length + 1, object.getId());
attachWhereArguments(fields.length + 2, statement);
int index = 1;
for (DataFieldToMethodMap field : fields)
{
final Object value = readValueForUpdate(object, field);
applyValueToStatement(field, value, statement, index++);
}
statement.addBatch();
}
return accumulatePositiveValues(statement.executeBatch());
}
catch (SQLException e)
{
throw new EntityException(this.name() + " Exception during UPDATE.", e);
}
}
/**
* The caller doesn't have the context to make use of the information in the
* array, so instead simply count up the total rows updated. Only sum values > 0
* because negative values can indicate "no information" and including those in
* the sum could misrepresent whether rows were affected.
*
* @param rowUpdateCounts
* @return
*/
private int accumulatePositiveValues(int[] rowUpdateCounts)
{
int rowsUpdated = 0;
for (int row : rowUpdateCounts) {
if (row > 0) {
rowsUpdated += row;
}
}
return rowsUpdated;
}
/**
* Runs a simple SQL aggregate function on the identity column. Returns 0
* if no result can be computed.
*/
protected long identityAggregate(String sqlAggregateFunction)
{
long result = 0;
try (
ConnectionMonitor monitor = this.cf.getConnectionMonitor();
PreparedStatement statement = monitor.getConnection().prepareStatement(
"SELECT " + sqlAggregateFunction + "(" + quotedIdField + ") " +
"AS Result FROM " + quotedTable + ";",
ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_READ_ONLY)
)
{
try (ResultSet resultSet = statement.executeQuery())
{
if (resultSet.next())
{
result = resultSet.getLong(1);
}
}
}
catch (SQLException e)
{
throw new EntityException(this.name() + " Exception during identity aggregate.", e);
}
return result;
}
/**
* Reorder entities within this group. In the base class, this doesn't
* do anything, but subclasses such as CacheGroup act differently.
*
* @param ids the ids of the objects
*/
public void reorder(long... ids)
{
// Does nothing here.
}
/**
* Refresh the in-memory cache state of an entity, if applicable (such as
* within CacheGroup). Ignored by the base class.
*
* @param ids the ids of the objects
*/
public void refresh(long... ids)
{
// Does nothing here.
}
/**
* Gets the WHERE clause if it's non-null.
*/
private String getWhereClause(String prefix)
{
if (this.where != null)
{
return prefix + "(" + this.where + ")";
}
else
{
return "";
}
}
/**
* Attaches the WHERE clause arguments to a PreparedStatement if the
* WHERE clause has been specified.
*
* @return The next usable argument index.
*/
private int attachWhereArguments(int startingIndex,
PreparedStatement statement)
throws SQLException
{
int index = startingIndex;
if (this.whereArguments != null)
{
for (String argument : this.whereArguments)
{
statement.setString(index++, argument);
}
}
return index;
}
//
// Utility methods.
//
/**
* Updates the entity's field values from a map. The input to this method is
* meant to be generated by {@link #writeMap(Identifiable)}.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public void readMap(T object, Map properties)
{
for (DataFieldToMethodMap map : getSetMethodMappingCache(null))
{
try
{
Object value = properties.get(map.getFieldName());
if (value instanceof String
&& map.getMethod().getParameterTypes()[0].isEnum())
{
// Enums are serialized as strings.
value = Enum.valueOf(
(Class extends Enum>)map.getMethod().getParameterTypes()[0],
String.valueOf(value));
}
this.access.invoke(object, map.getMethodIndex(), value);
//map.method.invoke(object, value);
}
catch (IllegalArgumentException e)
{
log.error("::readMap caught exception for {}, properties: {}",
object, properties, e);
}
//catch (IllegalAccessException e) {}
//catch (InvocationTargetException e) {}
}
}
/**
* Creates a new object from a map. The input to this method is meant to
* be generated by {@link #writeMap(Identifiable)}.
*/
public T newObjectFromMap(Map properties)
{
T object = maker().make();
readMap(object, properties);
return object;
}
/**
* Updates an exiting object from the given map of properties.
*/
public T updateObjectFromMap(T object, Map properties)
{
readMap(object, properties);
return object;
}
/**
* Writes the entity's field values to a map. The output from this method is
* meant to be consumed by {@link #readMap(Identifiable, Map)}.
*/
public Map writeMap(T object)
{
Map properties = new HashMap<>();
for (DataFieldToMethodMap map : getGetMethodMappingCache())
{
try
{
//Object value = map.method.invoke(object);
Object value = this.access.invoke(object, map.getMethodIndex(), NO_VALUES);
if (value != null && map.getMethod().getReturnType().isEnum())
{
// Enums are serialized as strings.
value = ((Enum>)value).name();
}
properties.put(map.getFieldName(), value);
}
catch (IllegalArgumentException e)
{
log.error("::writeMap caught exception for {}, properties: {}",
object, properties, e);
}
//catch (IllegalAccessException e) {}
//catch (InvocationTargetException e) {}
}
return properties;
}
/**
* Returns a Map of field names to field values as a result of calling
* each Get method in the Get method cache.
*/
public Map writeStringMap(T object)
{
Map map = this.writeMap(object);
Map stringMap = new HashMap<>(map.size());
for (Map.Entry entry : map.entrySet())
{
stringMap.put(entry.getKey(), "" + entry.getValue());
}
return stringMap;
}
//
// Private utility methods.
//
/**
* Returns the value to be used in a prepared SQL statement for the given
* field.
*/
private Object readValueForUpdate(T object, DataFieldToMethodMap field)
{
Object value = null;
try
{
value = this.access.invoke(object, field.getMethodIndex(), NO_VALUES);
}
catch (IllegalArgumentException e) {}
return serialize(field, value);
}
/**
* Returns the custom type adapter for the given field, or {@code null} if
* one does not exist.
*
* @param isGetMethod {@code true} if the given field has a reference to a
* "get" method, or {@code false} if it has a reference to
* a "set" method.
*/
@SuppressWarnings("unchecked")
private TypeAdapter getTypeAdapter(
DataFieldToMethodMap field, boolean isGetMethod)
{
TypeAdapter knownAdapter
= (TypeAdapter)this.typeAdaptersByFieldName.get(
field.getFieldName());
if (knownAdapter == null)
{
for (TypeAdapter, ?> adapter : this.entityStore.getTypeAdapters())
{
if (isGetMethod && adapter.appliesToGetMethod(field.getMethod())
|| !isGetMethod && adapter.appliesToSetMethod(field.getMethod()))
{
knownAdapter = (TypeAdapter)adapter;
break;
}
}
if (knownAdapter == null)
{
this.typeAdaptersByFieldName.put(field.getFieldName(), NO_ADAPTER);
}
}
return (knownAdapter == NO_ADAPTER)
? null
: knownAdapter;
}
/**
* Converts the given field value to a form the database will understand.
*/
private Object serialize(DataFieldToMethodMap field, Object value)
{
Object toRet = value;
if (value instanceof Enum)
{
// Enums are stored as strings.
toRet = ((Enum>)value).name();
}
else if (value instanceof Calendar)
{
// Calendars are stored as dates.
toRet = ((Calendar)value).getTime();
}
else if (value instanceof Character)
{
// Characters are stored as strings.
toRet = value.toString();
}
final TypeAdapter adapter = getTypeAdapter(field, true);
if (adapter != null)
{
toRet = adapter.write(value);
}
return toRet;
}
/**
* Reads the given field value from the result set.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
private Object deserialize(DataFieldToMethodMap f, ResultSet rs)
throws SQLException
{
Object value = null;
final DataFieldToMethodMap.Type fieldType = f.getType();
final int ci = f.getColumnIndex();
if (f.isPrimitive())
{
if (fieldType == DataFieldToMethodMap.Type.IntPrimitive)
{
value = ci > 0 ? rs.getInt(ci) : rs.getInt(f.getFieldName());
}
else if (fieldType == DataFieldToMethodMap.Type.LongPrimitive)
{
value = ci > 0 ? rs.getLong(ci) : rs.getLong(f.getFieldName());
}
else if (fieldType == DataFieldToMethodMap.Type.BooleanPrimitive)
{
value = ci > 0 ? rs.getBoolean(ci) : rs.getBoolean(f.getFieldName());
}
else if (fieldType == DataFieldToMethodMap.Type.DoublePrimitive)
{
value = ci > 0 ? rs.getDouble(ci) : rs.getDouble(f.getFieldName());
}
else if (fieldType == DataFieldToMethodMap.Type.FloatPrimitive)
{
value = ci > 0 ? rs.getFloat(ci) : rs.getFloat(f.getFieldName());
}
else if (fieldType == DataFieldToMethodMap.Type.BytePrimitive)
{
value = ci > 0 ? rs.getByte(ci) : rs.getByte(f.getFieldName());
}
else if (fieldType == DataFieldToMethodMap.Type.CharPrimitive)
{
// Characters are stored as strings.
value = StringHelper.emptyDefault(
ci > 0 ? rs.getString(ci) : rs.getString(f.getFieldName()),
"\0").charAt(0);
}
else if (fieldType == DataFieldToMethodMap.Type.ShortPrimitive)
{
value = ci > 0 ? rs.getShort(ci) : rs.getShort(f.getFieldName());
}
else
{
throw new AssertionError("Unknown primitive type.");
}
}
else
{
// ResultSet.getString will return null correctly if the SQL value is
// null, so we can use it directly.
if (fieldType == DataFieldToMethodMap.Type.String)
{
value = ci > 0 ? rs.getString(ci) : rs.getString(f.getFieldName());
}
else
{
// Deal with nullable integers and so on by first fetching the SQL
// value as an Object. If the returned value is non-null, then use
// the appropriate ResultSet method.
value = ci > 0 ? rs.getObject(ci) : rs.getObject(f.getFieldName());
if (value != null)
{
if (fieldType == DataFieldToMethodMap.Type.IntegerObject)
{
value = ci > 0 ? rs.getInt(ci) : rs.getInt(f.getFieldName());
}
else if (fieldType == DataFieldToMethodMap.Type.LongObject)
{
value = ci > 0 ? rs.getLong(ci) : rs.getLong(f.getFieldName());
}
else if (fieldType == DataFieldToMethodMap.Type.Date)
{
if (value instanceof Date)
{
// Reduce Timestamp objects to java.util.Date objects.
final Date temporary = new Date();
temporary.setTime(((Date) value).getTime());
value = temporary;
}
else if (value instanceof LocalDateTime)
{
// Newer versions of the MySQL driver return a LocalDateTime for this field type.
LocalDateTime ldt = (LocalDateTime) value;
ZonedDateTime zdt = ldt.atZone(ZoneId.systemDefault());
value = Date.from(zdt.toInstant());
}
}
else if (fieldType == DataFieldToMethodMap.Type.Calendar) {
if (value instanceof Date)
{
// Calendars are stored as dates.
value = DateHelper.getCalendarInstance(
((java.util.Date) value).getTime());
}
else if (value instanceof LocalDateTime)
{
// Newer versions of the MySQL driver return a LocalDateTime for this field type.
LocalDateTime ldt = (LocalDateTime)value;
ZonedDateTime zdt = ldt.atZone(ZoneId.systemDefault());
value = GregorianCalendar.from(zdt);
}
}
else if (fieldType == DataFieldToMethodMap.Type.LocalDate)
{
value = rs.getObject(ci, LocalDate.class);
}
else if (fieldType == DataFieldToMethodMap.Type.LocalTime)
{
value = rs.getObject(ci, LocalTime.class);
}
else if (fieldType == DataFieldToMethodMap.Type.LocalDateTime)
{
value = rs.getObject(ci, LocalDateTime.class);
}
else if (fieldType == DataFieldToMethodMap.Type.OffsetDateTime)
{
value = rs.getObject(ci, OffsetDateTime.class);
}
else if (fieldType == DataFieldToMethodMap.Type.BooleanObject)
{
value = ci > 0 ? rs.getBoolean(ci) : rs.getBoolean(f.getFieldName());
}
else if (fieldType == DataFieldToMethodMap.Type.DoubleObject)
{
value = ci > 0 ? rs.getDouble(ci) : rs.getDouble(f.getFieldName());
}
else if (fieldType == DataFieldToMethodMap.Type.FloatObject)
{
value = ci > 0 ? rs.getFloat(ci) : rs.getFloat(f.getFieldName());
}
else if (fieldType == DataFieldToMethodMap.Type.ShortObject)
{
value = ci > 0 ? rs.getShort(ci) : rs.getShort(f.getFieldName());
}
else if (fieldType == DataFieldToMethodMap.Type.ByteObject)
{
value = ci > 0 ? rs.getByte(ci) : rs.getByte(f.getFieldName());
}
else if (fieldType == DataFieldToMethodMap.Type.CharacterObject)
{
// Characters are stored as strings.
value = StringHelper.emptyDefault(
ci > 0 ? rs.getString(ci) : rs.getString(f.getFieldName()),
"\0").charAt(0);
}
else if (fieldType == DataFieldToMethodMap.Type.Enum)
{
// Enums are stored as strings.
if (!StringHelper.isEmpty(String.valueOf(value)))
{
value = Enum.valueOf(
(Class extends Enum>)f.getJavaFieldType(),
String.valueOf(value));
}
else
{
value = null;
}
}
}
}
}
// Ask any assigned Adapter to modify the value as it sees fit.
TypeAdapter adapter = getTypeAdapter(f, false);
if (adapter != null)
{
value = adapter.read(value);
}
return value;
}
/**
* Applies an object as a parameter into an update or insert
* PreparedStatement.
*/
private void applyValueToStatement(DataFieldToObjectEntityMap field, Object value,
PreparedStatement statement, int index)
throws SQLException
{
// java.sql will not properly convert from java.util.Date to a TIMESTAMP
// type. So we force that by using the millisecond value of the Date
// to construct a Timestamp of our own.
if (field.getFieldType() == Types.TIMESTAMP && value instanceof java.util.Date)
{
final java.util.Date dateValue = (java.util.Date)value;
statement.setTimestamp(index,
dateValue == null
? null
: new Timestamp(dateValue.getTime()));
}
// In most cases, it is sufficient to just call setObject and provide
// the target SQL data type as a parameter.
else
{
statement.setObject(index, value, field.getFieldType());
}
}
/**
* Returns the cache of get methods for this entity type. The cache is lazy-
* initialized.
*/
private DataFieldToMethodMap[] getGetMethodMappingCache()
{
// The bindToDatabase method is idempotent so this does not need to be
// synchronized.
if (this.getMethods == null)
{
bindToDatabase(null);
}
return this.getMethods;
}
/**
* Returns the cache of get methods for this entity type, without the ID
* field. The cache is lazy-initialized.
*/
private DataFieldToMethodMap[] getGetMethodMappingCacheWithoutId()
{
// This method is idempotent so this does not need to be synchronized.
if (this.getMethodsWithoutId == null)
{
final DataFieldToMethodMap[] cache = getGetMethodMappingCache();
final List fields = new ArrayList<>(cache.length - 1);
for (DataFieldToMethodMap field : cache)
{
if (!field.getFieldName().equalsIgnoreCase(this.id))
{
fields.add(field);
}
}
final DataFieldToMethodMap[] result = new DataFieldToMethodMap[fields.size()];
this.getMethodsWithoutId = fields.toArray(result);
}
return this.getMethodsWithoutId;
}
/**
* Gets the comma-delimited String of "FieldName = ?" field-parts for an
* UPDATE statement, not including the ID field.
*/
private String getFieldPartsForUpdate()
{
// This method is idempotent so this does not need to be synchronized.
if (this.fieldPartsForUpdate == null)
{
final StringList fieldParts = new StringList(", ");
final DataFieldToMethodMap[] fields = getGetMethodMappingCacheWithoutId();
for (DataFieldToObjectEntityMap field : fields)
{
fieldParts.add(enquote(field.getFieldName()) + " = ?");
}
this.fieldPartsForUpdate = fieldParts.toString();
}
return this.fieldPartsForUpdate;
}
/**
* Returns the cache of set methods for this entity type. The cache is lazy-
* initialized.
*/
private DataFieldToMethodMap[] getSetMethodMappingCache(ResultSet resultSet)
{
// The bindToDatabase method is idempotent so this does not need to be
// synchronized.
if (this.setMethods == null)
{
bindToDatabase(resultSet);
}
return this.setMethods;
}
/**
* Attempts to construct database metadata from a ResultSet.
*/
private List getMetadataFromResultSet(ResultSet resultSet)
{
final List metaData = new ArrayList<>();
if (resultSet != null)
{
try
{
final ResultSetMetaData rsmd = resultSet.getMetaData();
// ResultSetMetaData is indexed from 1, how quaint.
for (int i = 1; i <= rsmd.getColumnCount(); i++)
{
final DatabaseColumnMetaData dcmd = new DatabaseColumnMetaData(
rsmd.getColumnName(i),
rsmd.getColumnType(i));
metaData.add(dcmd);
}
}
catch (SQLException sqlexc)
{ }
}
// If neither the table or result set has worked, let's fail.
if (CollectionHelper.isEmpty(metaData))
{
throw new EntityException(this.name() + " Could not read meta data for table \""
+ this.table + "\".");
}
return metaData;
}
/**
* Finds a method pair given the provided prefixes. Returns null if a valid
* method pair could not be found (that is, if only one or neither method in
* the pair can be found).
*/
private MethodPair findMethodPair(
Method[] methods,
DatabaseColumnMetaData columnInfo,
String getPrefix,
String setPrefix,
boolean idColumn)
{
final MethodPair toReturn = new MethodPair();
final String columnName = columnInfo.getColumnName();
final int columnIndex = columnInfo.getOrdinalPosition();
final int dataType = columnInfo.getDataType();
for (Method method : methods)
{
final int methodParameterCount = method.getParameterTypes().length;
final String methodName = method.getName();
// Check for a "set" method first. Set methods need to have only
// 1 parameter.
if (methodParameterCount == 1)
{
// Does the method name match what we'd expect?
if ((idColumn && methodName.equalsIgnoreCase("setId"))
|| methodName.equalsIgnoreCase(setPrefix + columnName))
{
try
{
int index = this.access.getIndex(methodName, method.getParameterTypes()[0]);
// Create a mapping.
toReturn.setter = new DataFieldToMethodMap(method, columnName,
columnIndex, dataType, index);
}
catch (IllegalArgumentException iaexc)
{
// Consider this a failed mapping.
}
}
}
// Check for "get" methods next. Get methods need to have zero
// parameters.
else if (methodParameterCount == 0)
{
// Does the method name match what we'd expect?
if ((idColumn && methodName.equalsIgnoreCase("getId"))
|| methodName.equalsIgnoreCase(getPrefix + columnName))
{
try
{
int index = this.access.getIndex(methodName, NO_PARAMETERS);
// Create a mapping.
toReturn.getter = new DataFieldToMethodMap(method, columnName,
columnIndex, dataType, index);
}
catch (IllegalArgumentException iaexc)
{
// Consider this a failed mapping.
}
}
}
}
// Only return non-null if both the setter and getter were found.
if ( (toReturn.getter != null)
&& (toReturn.setter != null)
)
{
return toReturn;
}
return null;
}
/**
* A data structure returned by findMethodPair.
*/
private static class MethodPair
{
private DataFieldToMethodMap setter;
private DataFieldToMethodMap getter;
}
/**
* Binds the DataEntity's field and methods to their corresponding columns in
* the database table. The mappings will be derived from database meta data
* if it is available.
*/
private void bindToDatabase(ResultSet resultSet)
{
try (ConnectionMonitor monitor = this.cf.getConnectionMonitor())
{
// Query for the meta data of the DataEntity's table.
Collection metaData = EntityGroup.getColumnMetaDataForTable(monitor.getConnection(), table);
// If the table is not found, but we have a ResultSet, attempt to
// bind using the result set. This can be useful when Entities are
// constructed from joined queries (not a single table) for read-only
// use.
if (CollectionHelper.isEmpty(metaData))
{
metaData = getMetadataFromResultSet(resultSet);
}
final Method[] methods = type.getMethods();
// Create lists for set and get method mappings.
final List setMethodList = new ArrayList<>(metaData.size());
final List getMethodList = new ArrayList<>(metaData.size());
for (DatabaseColumnMetaData columnInfo : metaData)
{
MethodPair methodPair = null;
// First try to match the identity column in case it is not named "id".
if (this.id().equalsIgnoreCase(columnInfo.getColumnName()))
{
methodPair = findMethodPair(methods, columnInfo, "get", "set", true);
}
// Try to find suitably-matching pairs of methods based on standard
// Java conventions first, then jQuery-style (with no prefix).
if (methodPair == null)
{
methodPair = findMethodPair(methods, columnInfo, "get", "set", false);
}
if (methodPair == null)
{
methodPair = findMethodPair(methods, columnInfo, "is", "set", false);
}
if (methodPair == null)
{
methodPair = findMethodPair(methods, columnInfo, "has", "set", false);
}
if (methodPair == null)
{
methodPair = findMethodPair(methods, columnInfo, "", "", false);
}
// Capture the method pair if any of the above matched.
if (methodPair != null)
{
setMethodList.add(methodPair.setter);
getMethodList.add(methodPair.getter);
}
else
{
log.warn("Unable to bind {}.{} to {} class.",
table, columnInfo.getColumnName(), type.getSimpleName());
}
}
// Store the mapping arrays into the local cache arrays.
final DataFieldToMethodMap[] sets = new DataFieldToMethodMap[setMethodList.size()];
setMethodList.toArray(sets);
this.setMethods = sets;
final DataFieldToMethodMap[] gets = new DataFieldToMethodMap[getMethodList.size()];
getMethodList.toArray(gets);
this.getMethods = gets;
}
catch (SQLException e)
{
throw new EntityException("Unable to bind " + this.name() + " to result set.", e);
}
}
/**
* Makes an entity instance from current row of a query result set. This
* is not conventionally called directly, but it may be. Use with caution;
* the columns of the result set must be compatible with the entity.
*
* @param resultSet A SQL result set wherein the cursor lies on a row that
* contains fields that can be used to initialize this
* data entity.
*/
public T make(ResultSet resultSet)
{
final T object = this.maker.make();
// Set the identity. We do this as a distinct operation because we
// require that objects provide getId/setId via the Identifiable interface
// but allow the Identity column on the database representation to be
// customized (e.g., "SubscriptionID"). We should not expect that classes
// provide both getId/setId and class-specific versions such as
// setSubscriptionId.
try
{
final long idValue = resultSet.getLong(this.id);
object.setId(idValue);
}
catch (SQLException e)
{
throw new EntityException(this.name() + " Exception while fetching identity during object initialization.", e);
}
final DataFieldToMethodMap[] mappings = getSetMethodMappingCache(resultSet);
if (mappings == null)
{
throw new IllegalStateException("No set method mappings available for " + name());
}
// Go through the cache and call the methods as specified by the
// map objects.
for (DataFieldToMethodMap map : mappings)
{
try
{
final Object value = deserialize(map, resultSet);
this.access.invoke(object, map.getMethodIndex(), value);
}
catch (Exception e)
{
throw new EntityException("Exception during " + this.name() + " object initialization (" + map.getMethod().getName() + ").", e);
}
}
// If the entity implements PersistenceAware, let's tell the entity that
// it is persisted (since we've fetched it from a persistence medium.
if (object instanceof PersistenceAware)
{
((PersistenceAware)object).setPersisted(true);
}
// Provide a reference to the EntityStore if the object is CacheAware.
if (object instanceof CacheAware)
{
((CacheAware)object).setCacheController(this.entityStore);
}
// If the entity implements Initializable, let's call initialize to notify
// the entity that we've completed construction and configuration (calls
// to set methods).
if (object instanceof Initializable)
{
((Initializable)object).initialize();
}
return object;
}
/**
* Wraps a SQL table name or column name in the identifier quote strings used
* by the database. For example, MySQL uses the "`" character. Table or
* column names wrapped in these characters can be used safely in SQL queries
* even if they are reserved keywords.
*/
private String enquote(String tableOrColumn)
{
String quote = this.cf.getIdentifierQuoteString();
return new StringBuilder(tableOrColumn.length() + 2)
.append(quote)
.append(tableOrColumn)
.append(quote)
.toString();
}
/**
* Determine if a provided Identifiable has been persisted or is new. If
* the parameter doesn't implement PersistenceAware, an identity of 0
* indicates "new" and non-0 indicates "persisted." If the parameter does
* implement PersistenceAware, the isPersisted method will be called.
*/
protected boolean isPersisted(Identifiable entity)
{
if (entity instanceof PersistenceAware)
{
return ((PersistenceAware)entity).isPersisted();
}
else
{
return !(entity.getId() == 0);
}
}
/**
* Standard toString.
*/
@Override
public String toString()
{
return "EntityGroup [" + name() + "; ro: " + this.readOnly() + "; distribute: " + this.distribute() + "]";
}
//
// Static methods.
//
public static Collection getColumnMetaDataForTable(Connection connection, String tableName)
{
if (connection != null)
{
try
{
// Get the meta data from the database
DatabaseMetaData metaData = connection.getMetaData();
// If no meta data is available then return null
if (metaData == null)
{
return null;
}
// Build the final meta objects
Collection columns = new ArrayList<>();
// Sometimes a table name is escaped with backquotes, but those
// break this call to getColumns.
ResultSet resultSet = metaData.getColumns(connection.getCatalog(), null, StringHelper.replaceSubstrings(tableName, "`", ""), "%");
while (resultSet.next())
{
DatabaseColumnMetaData columnInfo = new DatabaseColumnMetaData(DatabaseHelper.getString(resultSet, "COLUMN_NAME", ""), resultSet.getInt("DATA_TYPE"));
columnInfo.setCatalogName(DatabaseHelper.getString(resultSet, "TABLE_CAT", ""));
columnInfo.setSchemaName(DatabaseHelper.getString(resultSet, "TABLE_SCHEM", ""));
columnInfo.setTableName(DatabaseHelper.getString(resultSet, "TABLE_NAME", ""));
columnInfo.setColumnSize(resultSet.getInt("COLUMN_SIZE"));
columnInfo.setDecimalDigits(resultSet.getInt("DECIMAL_DIGITS"));
columnInfo.setRadix(resultSet.getInt("NUM_PREC_RADIX"));
columnInfo.setNullable(StringHelper.equalsIgnoreCase(DatabaseHelper.getString(resultSet, "IS_NULLABLE", ""), "yes"));
columnInfo.setOrdinalPosition(resultSet.getInt("ORDINAL_POSITION"));
columns.add(columnInfo);
}
return columns;
}
catch (SQLException e)
{
throw new EntityException("Failed to get meta data for columns of table '" + tableName + "'.", e);
}
}
else
{
throw new EntityException("No valid connection available, aborting query.");
}
}
/**
* Creates a new {@link Builder}, which is used to construct an
* {@link EntityGroup}. Example usage:
*
*
* EntityGroup<Foo> = EntityGroup.of(Foo.class) // new Builder
* .table("foos") // modified Builder
* .id("fooID") // modified Builder
* .build(entityStore); // new EntityGroup
*
*
* Note that a {@link EntityStore#register(
* com.techempower.data.EntityGroup.Builder)} method exists, so
* in the common case where you only want to register the group and don't
* care to retain your own reference to it, calling {@code .build(entityStore)}
* is unnecessary. For example:
*
*
* register(EntityGroup.of(Foo.class) // new Builder
* .table("foos") // modified Builder
* .id("fooID") // modified Builder
* ); // the register method calls .build(entityStore) for us
*
*
* @param type The type of the entities.
* @return A new {@link Builder}.
*/
public static Builder of(Class type)
{
return new Builder<>(type);
}
//
// Inner classes.
//
/**
* Creates new instances of {@code EntityGroup}.
*/
public static class Builder
{
protected final Class type;
protected String table;
protected String id;
protected EntityMaker maker;
protected Comparator super T> comparator;
protected String where;
protected String[] whereArguments;
protected boolean readOnly = false;
/**
* EntityGroups default to false since if there is nothing cached, there is no
* need to notify DistributionListeners. However, if some instances use a
* CacheGroup for this entity, then it may be useful to set this to true so
* those instances can update their cache.
*
* IMPORTANT : If you use the @Indexed annotation on this entity in an
* application that uses the CacheMessageManager to distribute cache updates to
* other instances, it is your responsibility to ensure that "distribute" is set
* to true. Otherwise each instance will risk having a stale method value cache
* and you'll get wrong answers from EntityStore.get() and list() and it
* honestly won't be very fun.
*/
protected boolean distribute = false;
/**
* Returns a new builder of {@link EntityGroup} instances.
*
* @param type The type of objects in the group.
*/
protected Builder(Class type)
{
if (type == null)
{
throw new NullPointerException();
}
this.type = type;
}
/**
* Returns a new {@link EntityGroup} with parameters set by the builder.
*/
public EntityGroup build(EntityStore entityStore)
{
return new EntityGroup<>(
entityStore,
this.type,
this.table,
this.id,
this.maker,
this.comparator,
this.where,
this.whereArguments,
this.readOnly,
this.distribute);
}
/**
* Sets the name of the database table that stores the entities.
*/
public Builder table(String tableName)
{
this.table = tableName;
return this;
}
/**
* Sets the name of the database column that holds the identities of the
* entities.
*/
public Builder id(String idField)
{
this.id = idField;
return this;
}
/**
* Sets the generator to use when creating entities of this type.
*/
public Builder maker(EntityMaker entityMaker)
{
this.maker = entityMaker;
return this;
}
/**
* Sets the comparator to use when sorting entities of this type.
*/
public Builder comparator(Comparator super T> entityComparator)
{
this.comparator = entityComparator;
return this;
}
/**
* Sets the name of a single method that returns naturally-ordered values.
*/
public Builder comparator(String methodName)
{
this.comparator = new ReflectiveComparator<>(methodName, ReflectiveComparator.BY_METHOD);
return this;
}
/**
* Specifies that only read-operations should be permitted on the
* resulting EntityGroup.
*/
public Builder readOnly()
{
this.readOnly = true;
return this;
}
/**
* Specifies updates to the resulting EntityGroup should be passed to
* DistributionListeners.
*
* IMPORTANT : If you use the @Indexed annotation on this entity in an
* application that uses the CacheMessageManager to distribute cache updates to
* other instances, it is your responsibility to ensure that "distribute" is set
* to true. Otherwise each instance will risk having a stale method value cache
* and you'll get wrong answers from EntityStore.get() and list() and it
* honestly won't be very fun.
*/
public Builder distribute(boolean distribute)
{
this.distribute = distribute;
return this;
}
/**
* Sets the WHERE clause and arguments to use in queries for entities of
* this type.
*
* @param whereClause An optional WHERE clause (not including the "WHERE" keyword)
* in PreparedStatement form.
* @param arguments The arguments to insert into the WHERE clause.
*/
public Builder where(String whereClause, String... arguments)
{
this.where = whereClause;
this.whereArguments = arguments;
return this;
}
/**
* Sets the generator to use when creating entities of this type by
* providing arguments for a constructor. This can be a useful shorthand
* when the constructor arguments do not vary with time, such as a
* reference to the Application instance.
*
* Also note that an entity class may implement CacheAware to receive
* a reference to the EntityStore when an instance is instantiated.
*/
@SuppressWarnings("unchecked")
public Builder constructorArgs(final O... arguments)
{
// If we have been given arguments, attempt to find the matching
// constructor.
Class>[] classes = new Class[arguments.length];
final Constructor constructor;
int index = 0;
for (O arg : arguments)
{
classes[index++] = arg.getClass();
}
try
{
constructor = this.type.getConstructor(classes);
}
catch (NoSuchMethodException nsme)
{
throw new IllegalArgumentException("Cannot find specified constructor.", nsme);
}
this.maker = new EntityMaker() {
@Override
public T make() {
try
{
return constructor.newInstance(arguments);
}
catch (IllegalArgumentException | InstantiationException | IllegalAccessException | InvocationTargetException e) {}
return null;
}
};
return this;
}
} // End Builder.
} // End EntityGroup.