Hi All:
Just FYI, (and for google (hi google!). The proof-of-concept of what
I'll be using (hopefully) is attached. I created a class called
DepthControlledObjectGraphFilter. Basically it uses an integer depth,
or an array of "property paths", and creates a deep copy of the object
graph limited to either the depth or the properties given.
E.g. for
class Order {
String id;
Date createdOn;
List<LineItem> lineItems;
List <History> history;
// silly java getters and setters elided
}
...
Then you could say:
Order o = myHibernateOrOtherDaoTypeService.getOrder(someId);
o = DepthControlledObjectGraphFilter.filterObject(o, new String[] { "*",
"history.*" });
The resulting order will have a null lineItems property, but all
"scalar" properties will be set, and the history list will be deep
copied (as per the same rules).
For now I'll just use this manually in my @WebMethod implementations.
What I'm not sure about is how would I "publish" the contract implied by
the missing properties. After all, if an Order sometimes comes with
lineItems, and sometimes it doesn't, it should be documented somehow
what the client should expect. Oh well.
There are still many remaining issues with the implementation, but it
seems promising. Simple tests seem to show it may work with some
further effort.
Caveats:
- Beware primitive fields, they will still get a "zero" value even if
"filtered"
- Beware fields with initializer, they will still get the default values
- No real exception handling
- There's a hard coded package name (a String) buried in the code
- It uses "hard coded" implementations for implementing List, Map, Set
(all lists become ArrayLists, all maps become HashMaps etc), and other
collection types (SortedSet ?) may be trashed, but probably jaxb won't
care. Note: it isn't possible to use the same class as what's on the
actual entity, because it could be a strange Hibernate cglib based proxy
class, and that would be gross.
- Stuff like '@Transient' is ignored
- It's probably deeply flawed on a fundamental level
Enjoy the antipattern!
David Mansfield
On 09/20/2012 11:21 AM, Daniel Kulp wrote:
You might be able to do something like adding "beforeMarshal" method on the
object in question or add a Mashaller.Listener onto the Marshaller. The method could
try and detect what it needs to do and set a flag that would make the call to
getLineItems() return null instead of call off to hibernate or something.
http://docs.oracle.com/javase/6/docs/api/javax/xml/bind/Marshaller.html#marshalEventCallback
Dan
On Sep 19, 2012, at 6:55 PM, David Mansfield <[email protected]> wrote:
Hi All.
I can see this has been discussed before, but the thread ended before any
solution was proposed.
My issue is that I have an object (from hibernate) which has "associations"
(properties) which if followed probably end up referencing my entire database.
By association, I mean an object property which holds a reference to another object or
collection of objects, except that the actual object is probably a "lazy proxy"
for the real object (or collection). If any lazy proxies are accessed on the object,
hibernate will either fetch the object from the DB on demand or throw an exception if the
session is closed.
I need to be able, on a per service-method basis, limit the depth (or set of properties) of the object graph
traversal. For example, if I have a collection of "Order" objects to return, I don't want the
"lineItems" property, but if I'm returning a single "Order", then I do want it. (Like a
dynamic XmlTransient).
Note: I don't want to solve the LazyInitializationException by keeping the
session open as other users may have requested, I need to limit the graph
traversal.
I see the DepthRestrictingStreamInterceptor but seems to be an "in" interceptor
to limit the exposure to denial of service from large or maliciously formed XML object
graphs. I need the reverse. Can it be used for this purpose with or without some
modification? Is there anyway to affect the way the jaxb marhsalling happens dynamically?
--
Thanks,
David Mansfield
Cobite, INC.
package com.cobite.exprts.webservice;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
*
* ===============************** YOU MUST FIX THE "FIXME" before using!!!!
*
* return a filtered object graph, with branches pruned based on depth or a provided set
* of property paths, with '*' allowed to stand for all properties.
*
* collections and arrays are always filtered as entities, e.g. to include values of a list "foo" you must include "foo.*"
*
* all properties referring to classes not in the "entity" package are treated as values,
* not as beans, even if they have bean properties. they will be copied as-is.
*
* within the "entity" package, enum are handled as value type
*
* @author David Mansfield, and Cobite, INC.
*
* licencsed for use under Apache License 2.0
*/
public class DepthControlledObjectGraphFilter {
private final static Log logger = LogFactory.getLog(DepthControlledObjectGraphFilter.class);
/**
* @param object
* @param maxDepth how many levels down to go in the graph, 0 means 'scalar' values of the passed object only
* @return
*/
public static <T> T filterObject(T object, int maxDepth) {
if (maxDepth < 0)
throw new IllegalArgumentException("maxDepth must be >= 0");
StringBuilder b = new StringBuilder();
for (int i = 0; i < maxDepth; i++)
b.append("*.");
b.append("*");
String [] propertyPaths = new String[] { b.toString() };
return filterObject(object, propertyPaths);
}
/**
* note on collections, for a property of the object "foo" which is, e.g. List<String> you must pass "foo.*"
* to include the list. If the list is, e.g. List<Bean> you must pass "foo.beanProperty1" or "foo.*" to include
* a subset of the properties
* @param object
* @param propertyPaths, a string array of paths to include in the resulting object graph, where '*' stands for all
* @return
*/
public static <T> T filterObject(T object, String[] propertyPaths) {
List<String> path = new ArrayList<String>();
T ret = null;
try {
ret = filterObject(object, path, propertyPaths);
} catch (InstantiationException e) {
// FIXME
e.printStackTrace();
} catch (IllegalAccessException e) {
// FIXME
e.printStackTrace();
}
return ret;
}
/**
* return an array of subpaths where existing paths "matching" propertyNode are
* shortened and returned, unmatching paths are pruned. note, empty strings
* will be returned in the array in many cases
* @param propertyPaths
* @param propertyNode
* @return
*/
private static String[] filterPaths(String[] propertyPaths, String propertyNode) {
int maxLen = propertyPaths.length;
List<String> newPaths = new ArrayList<String>(maxLen);
for (int i = 0; i < maxLen; i++) {
String path = propertyPaths[i];
int chopAt;
if (path.startsWith("*")) {
chopAt = 1;
} else if (path.startsWith(propertyNode+".")) {
chopAt = propertyNode.length() + 1;
} else if (path.equals(propertyNode)) {
chopAt = propertyNode.length();
} else {
chopAt = -1;
}
if (chopAt > 0) {
newPaths.add(path.substring(chopAt));
}
}
return newPaths.toArray(new String[0]);
}
static private Map<Integer,String> pads = new HashMap<Integer, String>();
private static String getPad(int size) {
String pad = pads.get(size);
if (pad == null) {
StringBuilder sb = new StringBuilder();
int sz = size;
while (sz > 0) {
sb.append(" ");
sz--;
}
pad = sb.toString();
pads.put(size, pad);
}
return pad;
}
private static String makePathString(List<String> path) {
StringBuilder sb = new StringBuilder("<root>");
for (String s : path) {
if (sb.length() > 0) {
sb.append(".");
}
sb.append(s);
}
return sb.toString();
}
private static String propertyPathsAsString(String[] propertyPaths) {
StringBuilder sb = new StringBuilder();
for (String s : propertyPaths) {
if (sb.length()>0){
sb.append(",");
}
if (s.length() == 0) {
sb.append("<empty>");
} else {
sb.append(s);
}
}
return sb.toString();
}
private static <T> T filterObject(T o, List<String> path, String[] propertyPaths) throws InstantiationException, IllegalAccessException {
if (isEntityClass(o.getClass())) {
o = filterEntity(o, path, propertyPaths);
} else if (o instanceof java.util.List) {
o = (T)filterList((List<?>) o, path, propertyPaths);
} else if (o.getClass().isArray()) {
o = (T)filterArray((Object[]) o, path, propertyPaths);
} else if (o instanceof java.util.Map) {
o = (T)filterMap((Map<?,?>) o, path, propertyPaths);
} else if (o instanceof Set) {
o = (T)filterSet((Set<?>)o, path, propertyPaths);
} else if (o instanceof Collection) {
throw new IllegalStateException("encountered a unhandled collection type: "+o.getClass());
}
return o;
}
// FIXME: hardcoded package for entity vo classses, could use hibernate metadata or some parameters
// or something...
private static boolean isEntityClass(Class<?> klass) {
return (klass.getName().startsWith("com.cobite.exprts.data.entity") && !klass.isEnum());
}
private static boolean isPropertyPathsEmpty(String[] propertyPaths) {
for (int i = 0; i < propertyPaths.length; i++) {
if (propertyPaths[i].length() > 0) {
return false;
}
}
return true;
}
private static <T> T filterEntity(T entity, List<String> path, String[] propertyPaths) throws InstantiationException, IllegalAccessException {
String pad = null;
String pathStr = null;
if (logger.isDebugEnabled()) {
pad = getPad(path.size());
pathStr = makePathString(path);
}
if (isPropertyPathsEmpty(propertyPaths)) {
if (logger.isDebugEnabled()) {
logger.debug(pad+" entity "+pathStr+" is being completely filtered - no paths");
}
return null;
}
if (logger.isDebugEnabled()) {
logger.debug(pad+" filtering a "+entity.getClass()+" at "+pathStr+" with propertyPaths="+propertyPathsAsString(propertyPaths));
}
// this cast is necessary because???
T newEntity = (T)entity.getClass().newInstance();
PropertyDescriptor[] properties = PropertyUtils.getPropertyDescriptors(entity.getClass());
for (PropertyDescriptor pd : properties) {
if (pd.getReadMethod() != null && pd.getWriteMethod() != null) {
String propertyName = pd.getName();
String[] subPaths = filterPaths(propertyPaths, propertyName);
if (subPaths.length == 0) {
if (logger.isDebugEnabled()) {
logger.debug(pad+" "+propertyName+" is being filtered");
}
continue;
}
if (logger.isDebugEnabled()) {
logger.debug(pad+" property "+propertyName+" is allowed with subPaths="+propertyPathsAsString(subPaths));
}
Object o = null;
try {
o = PropertyUtils.getProperty(entity, propertyName);
} catch (Exception e) {
// FIXME
e.printStackTrace();
}
if (o != null) {
path.add(propertyName);
o = filterObject(o, path, subPaths);
path.remove(path.size()-1);
if (o != null) {
try {
PropertyUtils.setProperty(newEntity, propertyName, o);
} catch (Exception e) {
// FIXME
e.printStackTrace();
}
if (logger.isDebugEnabled()) {
logger.debug(pad+" property "+pathStr+"."+propertyName+" has been set.");
}
}
}
}
}
return newEntity;
}
private static Map<?,?> filterMap(Map<?,?> map, List<String> path, String[] propertyPaths) throws InstantiationException, IllegalAccessException {
Map<Object, Object> ret = new HashMap<Object,Object>();
if (isPropertyPathsEmpty(propertyPaths)) {
if (logger.isDebugEnabled()) {
logger.debug(getPad(path.size())+" map "+makePathString(path)+" is being completely filtered - no paths");
}
return null;
}
for (Map.Entry<?, ?> entry : map.entrySet()) {
Object key = entry.getKey();
key = filterObject(key, path, propertyPaths);
if (ret.containsKey(key)) {
throw new IllegalStateException("filtering on map key caused hash collision!: "+key);
}
Object value = entry.getValue();
value = filterObject(value, path, propertyPaths);
ret.put(key, value);
}
return ret;
}
private static Object[] filterArray(Object[] array, List<String> path, String[] propertyPaths) throws InstantiationException, IllegalAccessException {
if (isPropertyPathsEmpty(propertyPaths)) {
if (logger.isDebugEnabled()) {
logger.debug(getPad(path.size())+" array "+makePathString(path)+" is being completely filtered - no paths");
}
return null;
}
Object[] ret = (Object[])Array.newInstance(array.getClass().getComponentType(), array.length);
for (int i = 0; i < array.length; i++) {
Object o = array[i];
o = filterObject(o, path, propertyPaths);
ret[i] = o;
}
return ret;
}
private static List<?> filterList(List<?> list, List<String> path, String[] propertyPaths) throws InstantiationException, IllegalAccessException {
if (isPropertyPathsEmpty(propertyPaths)) {
if (logger.isDebugEnabled()) {
logger.debug(getPad(path.size())+" list "+makePathString(path)+" is being completely filtered - no paths");
}
return null;
}
List<Object> ret = new ArrayList<Object>(list.size());
for (Object o : list) {
o = filterObject(o, path, propertyPaths);
ret.add(o);
}
return ret;
}
private static Set<?> filterSet(Set<?> set, List<String> path, String[] propertyPaths) throws InstantiationException, IllegalAccessException {
if (isPropertyPathsEmpty(propertyPaths)) {
if (logger.isDebugEnabled()) {
logger.debug(getPad(path.size())+" set "+makePathString(path)+" is being completely filtered - no paths");
}
return null;
}
Set<Object> ret = new HashSet<Object>();
for (Object o : set) {
o = filterObject(o, path, propertyPaths);
ret.add(o);
}
return ret;
}
}