package jeffaschenk.commons.touchpoint.model.dao.resolvers;
import jeffaschenk.commons.parameters.EntityPath;
import jeffaschenk.commons.parameters.EntityPathGroup;
import org.apache.commons.jxpath.JXPathContext;
import org.apache.commons.jxpath.Pointer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Hibernate;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* Object Graph Resolver This will provide utilities to allow for proper
* hydration as requested by the upstream service layer.
*
* @author jeffaschenk@gmail.com
*/
public class ObjectGraphResolver {
/**
* Logging
*/
protected static Log log = LogFactory.getLog(ObjectGraphResolver.class);
/**
* Resolve the Object's DATA while still within session with the underlying
* ORM. This will in essence allow for specific path hydration provided by
* an upstream layer knowledgeable of the object model and the required and
* necessary DATA needed to be presented.
*
* @param object Object whose paths should be resolved.
* @param pathsToBeResolved Object Paths to be resolved.
*/
public static void resolve(Object object, String[] pathsToBeResolved) {
if (object == null) {
throw new IllegalArgumentException(
"Object is null, object must be initially instantiated.");
}
if ((pathsToBeResolved == null) || (pathsToBeResolved.length <= 0)) {
log.warn("No Paths to be Resolved have been specified, ignoring.");
return;
}
for (String path : pathsToBeResolved) {
touchEntityPath(object, path);
} // End of Path For Each Loop.
}
/**
* Resolve the Object's DATA while still within session with the underlying
* ORM. This will in essence allow for specific path hydration provided by
* an upstream layer knowledgeable of the object model and the required and
* necessary DATA needed to be presented.
*
* @param object Object whose paths should be resolved.
* @param pathToBeResolved Object Paths to be resolved.
*/
public static void resolve(Object object, String pathToBeResolved) {
if (object == null) {
throw new IllegalArgumentException(
"Object is null, object must be initially instantiated.");
}
if ((pathToBeResolved == null) || (pathToBeResolved.isEmpty())) {
log.warn("No Paths to be Resolved have been specified, ignoring.");
return;
}
// ******************************
// Touch the Path.
touchEntityPath(object, pathToBeResolved);
}
/**
* Resolve the Object's DATA while still within session with the underlying
* ORM. This will in essence allow for specific path hydration provided by
* an upstream layer knowledgeable of the object model and the required and
* necessary DATA needed to be presented.
*
* @param object Object whose paths should be resolved.
* @param entityPaths Paths Specified in EntityPath wrapper.
*/
public static void resolve(Object object, EntityPath[] entityPaths) {
if (object == null) {
throw new IllegalArgumentException(
"Object is null, object must be initially instantiated.");
}
if ((entityPaths == null) || (entityPaths.length <= 0)) {
log.warn("No Paths to be Resolved have been specified, ignoring.");
return;
}
/**
* Iterate Over the EntityPaths
*/
for (EntityPath entityPath : entityPaths) {
touchEntityPath(object, entityPath.getEntityPath());
} // End of Path Outer For Each Loop.
}
/**
* Resolve the Object's DATA while still within session with the underlying
* ORM. This will in essence allow for specific path hydration provided by
* an upstream layer knowledgeable of the object model and the required and
* necessary DATA needed to be presented.
*
* @param object Object whose paths should be resolved.
* @param entityPath Path Specified in EntityPath wrapper.
*/
public static void resolve(Object object, EntityPath entityPath) {
if (object == null) {
throw new IllegalArgumentException(
"Object is null, object must be initially instantiated.");
}
if ((entityPath == null) || (entityPath.isEmpty())) {
log.warn("No Paths to be Resolved have been specified, ignoring.");
return;
}
// ******************************
// Touch the Path.
touchEntityPath(object, entityPath.getEntityPath());
}
/**
* Resolve the Object's DATA while still within session with the underlying
* ORM. This will in essence allow for specific path hydration provided by
* an upstream layer knowledgeable of the object model and the required and
* necessary DATA needed to be presented.
*
* @param object Object whose paths should be resolved.
* @param entityPathGroups Groups of Paths
*/
public static void resolve(Object object, EntityPathGroup[] entityPathGroups) {
if (object == null) {
throw new IllegalArgumentException(
"Object is null, object must be initially instantiated.");
}
if ((entityPathGroups == null) || (entityPathGroups.length <= 0)) {
log.warn("No Paths to be Resolved have been specified, ignoring.");
return;
}
/**
* Iterate Over all of the Entity Paths within the Group.
*/
for (EntityPathGroup pathGroup : entityPathGroups) {
resolve(object, pathGroup.getEntityPathArray());
} // End of Path For Each Loop.
}
/**
* Resolve the Object's DATA while still within session with the underlying
* ORM. This will in essence allow for specific path hydration provided by
* an upstream layer knowledgeable of the object model and the required and
* necessary DATA needed to be presented.
*
* @param object Object whose paths should be resolved.
* @param entityPathGroup Groups of Paths
*/
public static void resolve(Object object, EntityPathGroup entityPathGroup) {
if (object == null) {
throw new IllegalArgumentException(
"Object is null, object must be initially instantiated.");
}
if ((entityPathGroup == null) || (entityPathGroup.isEmpty())) {
log.warn("No Paths to be Resolved have been specified, ignoring.");
return;
}
// ******************************
// Touch the Path.
for (EntityPath entityPath : entityPathGroup.getEntityPathArray()) {
resolve(object, entityPath.getEntityPath());
} // End of Path For Each Loop.
}
/**
* Resolve the Object's DATA while still within session with the underlying
* ORM. This will in essence allow for specific path hydration provided by
* an upstream layer knowledgeable of the object model and the required and
* necessary DATA needed to be presented.
*
* @param object Object whose paths should be resolved.
* @param optionalParameters Variable arguments passed from upstream layer.
*/
public static void resolve(Object object, Object... optionalParameters) {
if (object == null) {
throw new IllegalArgumentException(
"Object is null, object must be initially instantiated.");
}
if ((optionalParameters == null) || (optionalParameters.length <= 0)) {
log.warn("No Paths to be Resolved have been specified, ignoring.");
return;
}
/**
* Iterate Over all of the Entity Paths within Parameters.
*/
for (Object parameter : optionalParameters) {
if ((parameter.getClass().isArray()) && (EntityPathGroup.class.isInstance(parameter))) {
resolve(object, ((EntityPathGroup[]) parameter));
} else if ( ((parameter.getClass().isArray()) && (EntityPath.class.isInstance(parameter))) ||
(parameter.getClass().getName().equalsIgnoreCase("[L"+EntityPath.class.getName()+";")))
{
resolve(object, ((EntityPath[]) parameter));
} else if ((parameter.getClass().isArray()) && (String.class.isInstance(parameter))) {
resolve(object, ((String[]) parameter));
} else if (EntityPathGroup.class.isInstance(parameter)) {
resolve(object, ((EntityPathGroup) parameter));
} else if (EntityPath.class.isInstance(parameter)) {
resolve(object, ((EntityPath) parameter));
} else if (String.class.isInstance(parameter)) {
resolve(object, ((String) parameter));
} else {
if (log.isWarnEnabled()) {
log.warn("Not Processing this parameter class:[" + parameter.getClass().getName() + "] for Object:["+object.getClass().getName()+"]");
}
}
} // End of Path For Each Loop.
}
/**
* Private Helper Method which Performs the first level touch against an Objects
* Field/Properties using the XPath of the Entity Value.
*
* @param object Object whose paths should be resolved.
* @param entityPathValue Path to be touched with Object Graph.
*/
private static void touchEntityPath(Object object, final String entityPathValue) {
// *********************************************************
// Determine if this entityPath is a full Path?
if (!entityPathValue.contains(".")) {
accessEntityPath(object, entityPathValue);
return;
}
// *********************************************************
// Process nested Path Element
String[] pathNodes = entityPathValue.split("\\.");
Object nodeObject = object; // Set Reference.
for (int nodeObjectIndex = 0; nodeObjectIndex < pathNodes.length; nodeObjectIndex++) {
nodeObject = accessEntityPath(nodeObject, pathNodes[nodeObjectIndex]);
if (nodeObject == null) {
return;
}
if (log.isDebugEnabled())
{ log.debug("Nested Object Class:[" + nodeObject.getClass().getName() + "]"); }
// ***********************************
// Check for Collection, Map or Array
if (((nodeObject instanceof java.util.Collection) ||
(nodeObject instanceof java.util.Map) ||
(nodeObject.getClass().isArray())) &&
(pathNodes.length > nodeObjectIndex + 1)) {
// Pop off this name, since we do not check
// content type, just if that type has a
// method to obtain the next DATA Element.
nodeObjectIndex++;
StringBuffer embeddedPathValue = new StringBuffer();
for (nodeObjectIndex++; nodeObjectIndex < pathNodes.length; nodeObjectIndex++) {
if (embeddedPathValue.length() > 0) {
embeddedPathValue.append(".");
}
embeddedPathValue.append(pathNodes[nodeObjectIndex]);
}
// *******************************************
// Check to ensure we have a Path remaining?
// If not, we are done.
if (embeddedPathValue.length() == 0) {
break;
}
if (log.isDebugEnabled()) {
log.debug("New Embedded Path Value:[" + embeddedPathValue.toString() + "]");
}
// *******************************************
// Now Force the Recursion Against the
// objects contained with the collection.
if (nodeObject instanceof java.util.Collection) {
// Cast
java.util.Collection collection = (java.util.Collection) nodeObject;
if (!collection.isEmpty()) {
for (Object element : collection.toArray()) {
if (log.isDebugEnabled()) {
log.debug("Collection Element Value:[" + element.getClass().getName() + "]");
}
touchEntityPath(element, embeddedPathValue.toString());
}
}
// *******************************************
// Now Force the Recursion Against the
// objects contained with the collection.
} else if (nodeObject instanceof java.util.Map) {
// Cast
java.util.Map map = (java.util.Map) nodeObject;
if (!map.isEmpty()) {
for (Object element : map.values()) {
if (log.isDebugEnabled()) {
log.debug("Map Element Value:[" + element.getClass().getName() + "]");
}
touchEntityPath(element, embeddedPathValue.toString());
}
}
}
// *******************************************
// Ok, now break out of this Loop as we
// should have exhausted our Path.
break;
}
} // End of For Each Recursion for nested Path.
}
/**
* Private Helper Method which Performs the Access against an Objects
* Field/Properties using the XPath of the Entity Value.
*
* @param object Object whose paths should be resolved.
* @param path Path to be touched with Object Graph.
* @return Object resolved from embedded method invocation, if Method Not Found return can be Null.
*/
private static Object accessEntityPath(Object object, final String path) {
// *********************************************************
// Use JXPath to Access the Path within the Object.
try {
JXPathContext context = JXPathContext.newContext(object);
Pointer referencePointer = context.getPointer(path);
if ((referencePointer == null) ||
(referencePointer.getValue() == null)) {
return null;
}
Class<?> valueClass = referencePointer.getValue().getClass();
String referencePath = referencePointer.asPath();
if (log.isDebugEnabled()) {
log.debug("Base Class:[" + object.getClass().getSimpleName() + "], Value Class:[" + valueClass.getName() + "], Path:["
+ referencePath + "]");
}
// *********************************************
// Obtain the method Name.
String methodPrefix;
if (valueClass.getSimpleName().equalsIgnoreCase(
Boolean.class.getSimpleName())) {
methodPrefix = "is";
} else {
methodPrefix = "get";
}
String methodName = methodPrefix
+ path.substring(0, 1)
.toUpperCase()
+ path.substring(1);
// *********************************************
// Obtain the method for this path/field name.
Method method = getMethod(object.getClass(), methodName);
if (method != null) {
// ***************************
// Invoke the Method
return methodInvoker(object, method);
}
} catch (org.apache.commons.jxpath.JXPathNotFoundException e) {
if (object != null) {
log.warn("Specified Path Not Found:["
+ path + "], for Object:["+object.getClass().getName()+"], Ignoring, check your FetchGroup or String Bean Syntax.");
} else {
log.warn("Object is null");
}
} catch (Exception e) {
log.warn("Issue Encountered for Accessing Path:["
+ path + "], for Object:["+object.getClass().getName()+"], Issue:["+e.getMessage()+"], Ignoring.");
}
// *********************
// Nothing to return.
return null;
}
/**
* Private Helper method to invoke a read-only method
* to trigger the underlying ORM Framework to
* load that DATA element.
*
* @param object Object whose paths should be resolved.
* @param method Method to be invoked.
* @return Returned Object from invoked method.
*/
private static Object methodInvoker(Object object, Method method) {
Object resultObject = null;
try {
resultObject = method.invoke(object);
if (resultObject != null) {
Hibernate.initialize(resultObject);
// *************************************
// Does this Object have an IsEmpty
// method? If so, invoke it.
try {
Method isEmptyMethod = resultObject.getClass().getMethod("isEmpty");
if (isEmptyMethod != null) {
isEmptyMethod.invoke(resultObject);
}
} catch (NoSuchMethodException nsme) {
// **************************
// No IsEmpty method,
// so ignore, we have gone
// as far enough here.
}
}
return resultObject;
} catch (IllegalArgumentException e) {
log.debug(e);
} catch (IllegalAccessException e) {
log.debug(e);
} catch (InvocationTargetException e) {
log.debug(e);
} catch (NullPointerException npe) {
// *****************************
// An Null Pointer Exception or
// NPE here is OK, this just
// the getter value was null.
// So we simply Ignore the Benign
// NPE. NPE's are not always
// Bad :)
// *****************************
}
return resultObject;
}
/**
* Private Helper Method to get a Method on a specified Class.
*
* @param clazz Class whose Method is to be found by Name.
* @param methodName Name used to find Method.
* @return Method
*/
private static Method getMethod(Class<?> clazz, String methodName) {
try {
return clazz.getMethod(methodName);
} catch (SecurityException e) {
log.error("Security Exception Accessing " + clazz.getName() + "."
+ methodName + "!");
return null;
} catch (NoSuchMethodException e) {
if (log.isDebugEnabled()) {
log.debug("No Such Method Found for " + clazz.getName() + "."
+ methodName + "!");
}
return null;
}
}
}