Skip to content

Instantly share code, notes, and snippets.

@mrg
Last active May 10, 2017 14:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mrg/4dce22b67175c27f4047 to your computer and use it in GitHub Desktop.
Save mrg/4dce22b67175c27f4047 to your computer and use it in GitHub Desktop.
A collection of Cayenne utilities
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.cayenne.CayenneDataObject;
import org.apache.cayenne.DataObjectUtils;
import org.apache.cayenne.DataRow;
import org.apache.cayenne.ObjectContext;
import org.apache.cayenne.PersistenceState;
import org.apache.cayenne.access.DataContext;
import org.apache.cayenne.access.ObjectStore;
import org.apache.cayenne.exp.Expression;
import org.apache.cayenne.exp.ExpressionFactory;
import org.apache.cayenne.map.DbEntity;
import org.apache.cayenne.map.DbJoin;
import org.apache.cayenne.map.DbRelationship;
import org.apache.cayenne.map.ObjAttribute;
import org.apache.cayenne.query.QueryCacheStrategy;
import org.apache.cayenne.query.SelectQuery;
import org.apache.cayenne.util.Util;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Various utilities for Cayenne.
*/
public class CayenneUtils
{
private static final Log log = LogFactory.getLog(CayenneUtils.class);
/**
* No instances required.
*/
private CayenneUtils() { } // NOSONAR
/**
* Cayenne 3.0 -> 3.1 changes the way a DataContext is created. This
* method acts as a bridge between the two versions. All code should
* use this method to ease upgrading to 3.1 in the future.
*
* @return A new DataContext.
*/
public static DataContext createDataContext()
{
return DataContext.createDataContext();
}
/**
* Copies a Cayenne object into a data context.
*
* @param cayenneObject
* The Cayenne object to copy.
* @param dataContext
* The data context to copy cayenneObject into.
* @return A copy of the object associated with the supplied data context.
* Note: The initial state is HOLLOW.
*/
@SuppressWarnings("unchecked")
public static <T extends CayenneDataObject> T copyToContext(T cayenneObject, DataContext dataContext)
{
return (T) dataContext.localObject(cayenneObject.getObjectId(), cayenneObject);
}
/**
* Determines if a Cayenne object is identical to the snapshot data.
*
* @param dataObject The object to compare to the snapshot data.
* @return <tt>true</tt> if identical, <tt>false</tt> otherwise.
*/
public static boolean isIdenticalToSnapshot(CayenneDataObject dataObject)
{
// Need the object store to access the snapshot data.
ObjectStore objectStore = ((DataContext) dataObject.getObjectContext()).getObjectStore();
// Get the snapshot data.
DataRow dataRow = objectStore.getSnapshot(dataObject.getObjectId());
// Loop over all the attributes to compare data.
for (ObjAttribute attribute : dataObject.getObjEntity().getAttributes())
{
String dbAttributeName = attribute.getDbAttributeName();
String objAttributeName = attribute.getName();
// If the snapshot data matches the current data, continue the search.
if (Util.nullSafeEquals(dataRow.get(dbAttributeName), dataObject.readPropertyDirectly(objAttributeName)))
continue;
// The values were not identical to the snapshot.
return false;
}
// All comparisons were identical to the snapshot.
return true;
}
/**
* Determines if a Cayenne object is NOT identical to the snapshot data.
*
* @param dataObject The object to compare to the snapshot data.
* @return <tt>true</tt> if NOT identical, <tt>false</tt> otherwise.
*/
public static boolean isNotIdenticalToSnapshot(CayenneDataObject dataObject)
{
return isIdenticalToSnapshot(dataObject) == false;
}
/**
* Determine if there are in relevant changes in the data context. Does a
* deep introspection instead of the shallow introspection of Cayenne's
* hasChanges() method. Phantom changes are ignored.
*
* @param dataContext
* The data context to check for changes.
* @param allowLooseComparison
* If true, attempts to compensate for web applicators setting
* similar values that show up as dirty, but really aren't, such
* as an empty String and a null String being different.
* @return True if there are changes (the context is dirty), false if the
* context is clean.
*/
public static boolean isDirty(DataContext dataContext, boolean allowLooseComparison)
{
// If there are any new objects, the context is dirty.
if (newObjects(dataContext).size() > 0)
return true;
// If there are any deleted objects, the context is dirty.
if (deletedObjects(dataContext).size() > 0)
return true;
// Need the object store to access the snapshot data.
ObjectStore objectStore = dataContext.getObjectStore();
// Loop over all 'modified' objects and compare snapshot data to current data.
for (CayenneDataObject modifiedObject : modifiedObjects(dataContext))
{
// Get the snapshot data.
DataRow dataRow = objectStore.getSnapshot(modifiedObject.getObjectId());
// Loop over all the attributes to compare data.
for (ObjAttribute attribute : modifiedObject.getObjEntity().getAttributes())
{
// If allowing loose comparisons, include checks for similar-enough changes as non-dirty.
if (allowLooseComparison)
{
Object originalValue = dataRow.get(attribute.getDbAttributeName());
Object currentValue = modifiedObject.readPropertyDirectly(attribute.getName());
// Allow empty String "" and Null to pass as identical.
if (originalValue instanceof String || currentValue instanceof String)
if (StringUtils.isEmpty((String) originalValue))
if (StringUtils.isEmpty((String) currentValue))
continue;
// Allow comparable BigDecimal numbers (such as 1 and 1.000) to pass.
if (originalValue instanceof BigDecimal || currentValue instanceof BigDecimal)
if (BigDecimalUtil.equals((BigDecimal) originalValue, (BigDecimal) currentValue))
continue;
}
// If the snapshot data matches the current data, continue.
if (Util.nullSafeEquals(dataRow.get(attribute.getDbAttributeName()), modifiedObject.readPropertyDirectly(attribute.getName())))
continue;
// Object is dirty, abort all other comparisons.
return true;
}
}
// The context is clean.
return false;
}
/**
* Prints (to standard output) all relevant changes in a data context.
* Useful for debugging.
*
* @param dataContext
* The data context examine to print changes.
*/
public static void printDirty(DataContext dataContext)
{
// Print all new objects.
for (CayenneDataObject newObject : newObjects(dataContext))
log.debug(String.format("Persistent Object [%s]: New Object%n", newObject.getObjectId()));
// Print all deleted objects.
for (CayenneDataObject newObject : deletedObjects(dataContext))
log.debug(String.format("Persistent Object [%s]: Deleted Object%n", newObject.getObjectId()));
// Need the object store to access the snapshot data.
ObjectStore objectStore = dataContext.getObjectStore();
// Loop over all modified objects and print snapshot data versus current data.
for (CayenneDataObject modifiedObject : modifiedObjects(dataContext))
{
// Get the snapshot data.
DataRow dataRow = objectStore.getSnapshot(modifiedObject.getObjectId());
// Loop over all the attributes to compare data.
for (ObjAttribute attribute : modifiedObject.getObjEntity().getAttributes())
{
String dbAttributeName = attribute.getDbAttributeName();
String objAttributeName = attribute.getName();
// If the snapshot data matches the current data, continue.
if (Util.nullSafeEquals(dataRow.get(dbAttributeName), modifiedObject.readPropertyDirectly(objAttributeName)))
continue;
// Print the change.
log.debug(String.format("Persistent Object [%s]: Attribute [%s] changed from [%s] to [%s]%n",
modifiedObject.getObjectId(),
dbAttributeName,
dataRow.get(dbAttributeName),
modifiedObject.readPropertyDirectly(objAttributeName)));
}
}
}
public static void printRuntimeDatabaseRelationships(ObjectContext objectContext)
{
System.out.println("\nRuntime (complimentary) database relationships:\n");
for (DbEntity entity : objectContext.getEntityResolver().getDbEntities())
{
StringBuilder builder = new StringBuilder();
for (DbRelationship relationship : entity.getRelationships())
{
if (relationship.getName().startsWith("runtimeRelationship"))
{
if (builder.length() == 0)
builder.append("Entity: ").append(entity.getName()).append("\n");
builder.append(" Name: ").append(relationship.getName()).append("\n");
for (DbJoin join : relationship.getJoins())
{
builder.append(" Source: ");
builder.append(relationship.getSourceEntity().getName());
builder.append(".");
builder.append(join.getSourceName());
builder.append(", Target: ");
builder.append(relationship.getTargetEntity().getName());
builder.append(".");
builder.append(join.getTargetName());
builder.append("\n");
}
}
}
if (builder.length() > 0)
System.out.println(builder);
}
}
/**
* Helper method to return the collection of new objects as
* CayenneDataObject instead of Object, which is what the standard Cayenne
* API returns.
*
* @param dataContext
* The data context to locate new objects.
* @return A collection of CayenneDataObjects to be inserted.
*/
public static Collection<? extends CayenneDataObject> newObjects(DataContext dataContext)
{
return objectsInState(dataContext, PersistenceState.NEW);
}
/**
* Helper method to return the collection of deleted objects as
* CayenneDataObject instead of Object, which is what the standard Cayenne
* API returns.
*
* @param dataContext
* The data context to locate deleted objects.
* @return A collection of CayenneDataObjects to be deleted.
*/
public static Collection<? extends CayenneDataObject> deletedObjects(DataContext dataContext)
{
return objectsInState(dataContext, PersistenceState.DELETED);
}
/**
* Helper method to return the collection of modified objects as
* CayenneDataObject instead of Object, which is what the standard Cayenne
* API returns.
*
* @param dataContext
* The data context to locate modified objects.
* @return A collection of CayenneDataObjects which are modified (includes
* phantom modifications by default, just like the standard Cayenne
* API).
*/
public static Collection<? extends CayenneDataObject> modifiedObjects(DataContext dataContext)
{
return modifiedObjects(dataContext, true);
}
/**
* Helper method to return the collection of modified objects as
* CayenneDataObject instead of Object, which is what the standard Cayenne
* API returns.
*
* @param dataContext
* The data context to locate modified objects.
* @param includePhantomModifications
* Flag to control if phantom modifications should be included.
* @return A collection of CayenneDataObjects which are modified.
*/
public static Collection<? extends CayenneDataObject> modifiedObjects(DataContext dataContext, boolean includePhantomModifications)
{
// If including phantom modifications, life is simpler.
if (includePhantomModifications)
return objectsInState(dataContext, PersistenceState.MODIFIED);
// Otherwise, evaluate all the "modified" objects and inspect them for
// actual changes, collecting those that are actually modified.
List<CayenneDataObject> modifiedObjects = new ArrayList<CayenneDataObject>();
// Loop over all "modified" objects, filtering out phantom changes.
for (CayenneDataObject modifiedObject : objectsInState(dataContext, PersistenceState.MODIFIED))
{
// If the snapshot data does not match the current object, add the
// current to the list of modified objects.
if (isNotIdenticalToSnapshot(modifiedObject))
modifiedObjects.add(modifiedObject);
}
return modifiedObjects;
}
/**
* Helper method to return a collection of CayenneDataObjects instead of
* Persistent, which is what the standard Cayenne API returns.
*
* @param dataContext
* The data context to locate new objects.
* @param persistenceState
* The state of the objects in question (from PersistenceState).
* @return A collection of CayenneDataObjects matching the given state.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public static Collection<? extends CayenneDataObject> objectsInState(DataContext dataContext, int persistenceState)
{
return new ArrayList(dataContext.getObjectStore().objectsInState(persistenceState));
}
/**
* Find all objects of a certain type (class or interface) in a collection
* of Cayenne objects (such as that returned from newObjects()).
*
* @param searchObjects
* The list of Cayenne objects to search.
* @param type
* The class or interface to find.
* @return A collection of matching objects.
*/
@SuppressWarnings("unchecked")
public static <T> Collection<T> objectsForType(Collection<? extends CayenneDataObject> searchObjects, Class<T> type)
{
List<T> results = new ArrayList<T>();
for (CayenneDataObject searchObject : searchObjects)
if (type.isAssignableFrom(searchObject.getClass()))
results.add((T) searchObject);
return results;
}
/**
* @param objects
* List of Cayenne objects.
* @return A list of integers that are the primary keys of the Cayenne
* objects. Only works for collections of Cayenne objects with a
* single integer primary key.
*/
public static List<Integer> pksForObjects(List<? extends CayenneDataObject> objects)
{
List<Integer> results = new ArrayList<Integer>();
if (objects != null)
for (CayenneDataObject object : objects)
results.add(Integer.valueOf(DataObjectUtils.intPKForObject(object)));
return results;
}
public static <T> T objectForAttribute(ObjectContext objectContext, Class<T> dataObjectClass, String attribute, Object value)
{
T object = null;
Expression expression = ExpressionFactory.matchExp(attribute, value);
SelectQuery query = new SelectQuery(dataObjectClass, expression);
@SuppressWarnings("unchecked")
List<T> objects = objectContext.performQuery(query);
if (objects.size() == 1)
object = objects.get(0);
else if (objects.size() > 1)
throw new IllegalArgumentException("Query matched too many objects (" + objects.size() + ") when only one object is expected.");
return object;
}
@SuppressWarnings("unchecked")
public static <T extends CayenneDataObject> List<T> cachedQuery(ObjectContext context, Class<T> t, Object... propertyValuePairs)
{
SelectQuery query = new SelectQuery(t, ExpressionFactory.matchAllExp(createPropertyValueMap(propertyValuePairs), Expression.EQUAL_TO));
query.setCacheStrategy(QueryCacheStrategy.LOCAL_CACHE);
return context.performQuery(query);
}
public static <T extends CayenneDataObject> T objectForMultipleAttributes(ObjectContext context, Class<T> cayenneDataObjectClass, Object... propertyValuePairs)
{
List<T> resultSet = cachedQuery(context, cayenneDataObjectClass, propertyValuePairs);
if (resultSet.size() == 1)
return resultSet.get(0);
else if (resultSet.size() > 1)
log.warn("The select query returned more than one record for " + cayenneDataObjectClass.getSimpleName() + ", " + ArrayUtils.toString(propertyValuePairs));
return null;
}
public static Map<String, Object> createPropertyValueMap(Object[] propertyValueArray)
{
if (propertyValueArray == null)
return new HashMap<String, Object>(0);
if (propertyValueArray.length % 2 != 0)
throw new IllegalArgumentException("Odd number of entries in property-values array for _createPropertyValueMap. All property-values must come in pairs.");
Map<String, Object> propertyValueMap = new HashMap<String, Object>(propertyValueArray.length / 2);
for (int propertyIndex = 0, valueIndex = propertyValueArray.length / 2; valueIndex < propertyValueArray.length; propertyIndex++, valueIndex++)
{
if (propertyValueArray[propertyIndex] == null)
throw new IllegalArgumentException("Parameter key cannot be null.");
propertyValueMap.put(propertyValueArray[propertyIndex].toString(), propertyValueArray[valueIndex]);
}
return propertyValueMap;
}
/**
* Retrieves the Connection for the very first DataNode defined in the
* model.
*
* @param dataContext
* A DataContext to use to find the DataNode/Connection.
* @return An SQL Connection.
* @throws SQLException
*/
public static Connection getDataNodeConnection(DataContext dataContext) throws SQLException
{
return dataContext.getParentDataDomain().getDataNodes().iterator().next().getDataSource().getConnection();
}
/**
* Retrieves the Connection for the named DataNode defined in the model.
*
* @param dataContext
* A DataContext to use to find the DataNode/Connection.
* @param dataNodeName
* The name of the desired DataNode for the requested Connection.
* @return An SQL Connection.
* @throws SQLException
*/
public static Connection getDataNodeConnection(DataContext dataContext, String dataNodeName) throws SQLException
{
return dataContext.getParentDataDomain().getNode(dataNodeName).getDataSource().getConnection();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment