Last active
May 10, 2017 14:15
-
-
Save mrg/4dce22b67175c27f4047 to your computer and use it in GitHub Desktop.
A collection of Cayenne utilities
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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