/*
* Copyright 2006-2014 University of Dundee. All rights reserved.
* Use is subject to license terms supplied in LICENSE.txt
*/
package ome.tools.hibernate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import ome.api.StatefulServiceInterface;
import ome.model.IObject;
import ome.model.internal.Details;
import ome.model.meta.Node;
import ome.model.meta.Session;
import ome.model.meta.Share;
import ome.security.ACLVoter;
import ome.security.basic.CurrentDetails;
import ome.system.EventContext;
import ome.util.ContextFilter;
import ome.util.Filterable;
import ome.util.Utils;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.hibernate.Hibernate;
import org.hibernate.collection.AbstractPersistentCollection;
/**
* removes all proxies from a return graph to prevent ClassCastExceptions and
* Session Closed exceptions. You need to be careful with printing. Calling
* toString() on an unitialized object will break before filtering is complete.
*
* Note: we aren't setting the filtered collections here because it's "either
* null/unloaded or filtered". We will definitiely filter here, so it would just
* increase bandwidth.
*
* As of 4.4.0, this class is also responsible for various security-based
* modifications to returned {@link Details} objects.
*
* @author Josh Moore, josh at glencoesoftware.com
* @since 1.0
* @see <a href="http://trac.openmicroscopy.org/ome/ticket/8277">Trac ticket #8277</a>
*/
public class ProxyCleanupFilter extends ContextFilter {
protected Map unloadedObjectCache = new IdentityHashMap();
protected final ACLVoter acl;
protected final CurrentDetails current;
/**
* Passes {@code null}s to {@link ProxyCleanupFilter#ProxyCleanupFilter(ACLVoter, CurrentDetails)}
* such that all restricted objects will be unloaded.
*/
public ProxyCleanupFilter() {
this(null, null);
}
/**
* Construct a proxy cleanup filter that checks
* the security restrictions on certain objects.
* @param acl the ACL voter
* @param current the current thread's security context
*/
public ProxyCleanupFilter(ACLVoter acl, CurrentDetails current) {
this.acl = acl;
this.current = current;
}
@Override
public Filterable filter(String fieldId, Filterable f) {
if (f == null) {
return null;
}
if (unloadedObjectCache.containsKey(f)) {
return (IObject) unloadedObjectCache.get(f);
}
// Filtering all node objects for security reasons
if (f instanceof Node) {
return new Node((((Node) f).getId()), false);
}
// A proxy; send over the wire in altered form.
if (!Hibernate.isInitialized(f)) {
if (f instanceof IObject) {
IObject proxy = (IObject) f;
IObject unloaded = Utils.trueInstance(f.getClass().asSubclass(IObject.class));
unloaded.setId(proxy.getId());
unloaded.unload();
unloadedObjectCache.put(f, unloaded);
return unloaded;
} else if (f instanceof Details) {
// Currently Details is only "known" non-IObject Filterable
// Unlikely that this is ever reached.
Details d = (Details) f;
d = (Details) super.filter(fieldId, d.shallowCopy());
return d;
} else {
// TODO Here there's not much we can do. copy constructor?
throw new RuntimeException(
"Bailing out. Don't want to set to a value to null.");
}
// Not a proxy; it will be serialized and sent over the wire.
} else {
if (f instanceof IObject) {
if (acl != null) {
// When acl is null, assume this is for internal use
// and therefore the object will not be passed out.
// See ticket:8794 and OmeroMetadata.java
acl.postProcess((IObject) f);
}
}
// Also for security reasons, we're now checking ownership
// of sessions.
if (f instanceof Share) {
// ticket:2733
Share share = (Share) f;
if ( ! share.isLoaded()) {
return share;
} else if (share.retrieve("#2733") == null) {
return new Share(share.getId(), false);
}
} else if (f instanceof Session) {
Session session = (Session) f;
if ( ! session.isLoaded()) {
return session;
} else if (acl == null) {
return new Session(session.getId(), false);
} else {
EventContext ec = current.getCurrentEventContext();
if (!ec.isCurrentUserAdmin()) {
Long uid = session.getOwner().getId();
if (!ec.getCurrentUserId().equals(uid)) {
return new Session(session.getId(), false);
}
}
}
}
// Any clean up here.
return super.filter(fieldId, f);
}
}
@Override
public Collection filter(String fieldId, Collection c) {
// is a proxy. null it. will be refilled by
// MergeEventListener on re-entry.
if (null == c || !Hibernate.isInitialized(c)) {
return null;
}
Collection retVal = super.filter(fieldId, c);
// ticket:61 : preventing Hibernate collection types from escaping.
if (retVal instanceof AbstractPersistentCollection) {
if (retVal instanceof Set) {
retVal = new HashSet(retVal);
} else if (retVal instanceof List) {
retVal = new ArrayList(retVal);
}
} // end ticket:61
return retVal;
}
@Override
public Map filter(String fieldId, Map m) {
if (null == m || !Hibernate.isInitialized(m)) {
return null;
}
Map retVal = super.filter(fieldId, m);
// ticket:61 : preventing Hibernate collection types from escaping.
if (retVal instanceof AbstractPersistentCollection) {
retVal = new HashMap(retVal);
} // end ticket:61
return retVal;
}
// TODO FIXME need to further test this.
@Override
protected void doFilter(String arg0, Object arg1) {
if (arg1 instanceof Object[]) {
Object[] arr = (Object[]) arg1;
for (int i = 0; i < arr.length; i++) {
arr[i] = this.filter(arg0, arr[i]);
}
}
}
/** wraps a filter for each invocation */
public static class Interceptor implements MethodInterceptor {
private final SessionHandler sessions;
private final ACLVoter acl;
private final CurrentDetails current;
public Interceptor(ACLVoter acl, SessionHandler sessions,
CurrentDetails current) {
this.acl = acl;
this.sessions = sessions;
this.current = current;
}
private final ThreadLocal<Integer> depth = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return Integer.valueOf(0);
}
};
public Object invoke(MethodInvocation arg0) throws Throwable {
int d = depth.get().intValue();
if (d == 0) {
sessions.cleanThread();
}
Object result;
d++;
depth.set(d);
try {
result = arg0.proceed();
if (!StatefulServiceInterface.class.isAssignableFrom(arg0
.getThis().getClass())) {
result = new ProxyCleanupFilter(acl, current)
.filter(null, result);
}
} finally {
d--;
depth.set(d);
}
return result;
}
}
}