/*
* Copyright 2011 Glencoe Software, Inc. All rights reserved.
* Use is subject to license terms supplied in LICENSE.txt
*/
package omero.cmd;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import net.sf.ehcache.Ehcache;
import net.sf.ehcache.Element;
import ome.conditions.SessionException;
import ome.logic.HardWiredInterceptor;
import ome.services.sessions.SessionManager;
import ome.services.util.Executor;
import ome.system.OmeroContext;
import ome.system.Principal;
import omero.ApiUsageException;
import omero.InternalException;
import omero.ServerError;
import omero.ShutdownInProgress;
import omero.api.ClientCallbackPrx;
import omero.api.ClientCallbackPrxHelper;
import omero.api._StatefulServiceInterfaceOperations;
import omero.constants.CLIENTUUID;
import omero.util.CloseableServant;
import omero.util.IceMapper;
import omero.util.ServantHolder;
import omero.util.TieAware;
import org.aopalliance.aop.Advice;
import org.aopalliance.intercept.MethodInterceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.hibernate.Session;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import Ice.ConnectTimeoutException;
import Ice.ConnectionLostException;
import Ice.ConnectionRefusedException;
import Ice.Current;
/**
*
* @author Josh Moore, josh at glencoesoftware.com
* @since 4.3.2
*/
public class SessionI implements _SessionOperations {
// STATIC
// ===========
private final static Logger log = LoggerFactory.getLogger(SessionI.class);
// PRIVATE STATE
// =================
// These fields are special for this instance of SF alone. It represents
// a single clients use of a session.
public final String clientId;
public final Glacier2.SessionControlPrx control;
protected final AtomicBoolean reusedSession; // See #3202, modifiable as of 4.3
protected boolean doClose = true;
protected ClientCallbackPrx callback;
// SHARED STATE
// ===================
// The following elements will all be the same or at least equivalent
// in different instances of SF attached to the same session.
public final ServantHolder holder;
public final SessionManager sessionManager;
/**
* {@link Executor} to be used by servant implementations which do not
* delegate to the server package where all instances are wrapped with AOP
* for dealing with Hibernate.
*/
public final Executor executor;
public final Principal principal;
public final OmeroContext context;
public final Ice.ObjectAdapter adapter;
/** Token in the form of a UUID for securing method invocations. */
public final String token;
public SessionI(boolean reusedSession, Ice.Current current,
ServantHolder holder, Glacier2.SessionControlPrx control,
OmeroContext context, SessionManager sessionManager,
Executor executor, Principal principal, String token) throws ApiUsageException {
this.clientId = clientId(current);
this.adapter = current.adapter;
this.control = control;
this.sessionManager = sessionManager;
this.context = context;
this.executor = executor;
this.principal = principal;
this.token = token;
this.reusedSession = new AtomicBoolean(reusedSession);
// Setting up in memory store.
Ehcache cache = sessionManager.inMemoryCache(principal.getName());
String key = "servantHolder";
this.holder = holder;
if (!cache.isKeyInCache(key)) {
cache.put(new Element(key, holder));
} else {
// Check that SessionManagerI has given us the same instance.
ServantHolder oldHolder = (ServantHolder) cache.get(key).getObjectValue();
assert oldHolder == holder;
}
}
public Ice.ObjectAdapter getAdapter() {
return this.adapter;
}
public Principal getPrincipal() {
return this.principal;
}
public Executor getExecutor() {
return this.executor;
}
// ~ Command API
// =========================================================================
public void submit_async(AMD_Session_submit __cb, omero.cmd.Request req,
Ice.Current current) {
try {
if (req == null || !IRequest.class.isAssignableFrom(req.getClass())) {
log.info("Non-IRequest found:" + req);
__cb.ice_response(null);
return; // EARLY EXIT
}
Ice.Object servant = null;
for (String key : Arrays.asList(req.ice_id(),
_HandleTie.ice_staticId())) {
try {
servant = createServantDelegate(key);
if (servant != null && servant instanceof IHandle) {
break;
}
} catch (Exception e) {
log.debug(e.getClass().getName() + " on lookup of " + key);
}
}
IHandle handle = null;
if (servant != null) {
if (servant instanceof Ice.TieBase) {
Ice.TieBase tie = (Ice.TieBase) servant;
Object delegate = tie.ice_delegate();
if (IHandle.class.isAssignableFrom(delegate.getClass())) {
handle = (IHandle) delegate;
}
} else if (servant instanceof IHandle) {
handle = (IHandle) servant;
}
}
if (handle == null) {
log.info("No handle found for " + req);
InternalException ie = new InternalException();
ie.message = "No handle found for " + req;
__cb.ice_exception(ie);
return; // EARLY EXIT
}
// ID
Ice.Identity id = holder.getIdentity("IHandle" + UUID.randomUUID().toString());
// Tie
_HandleTie tie = (_HandleTie) servant;
HandlePrx prx = HandlePrxHelper.checkedCast(registerServant(id, tie));
// Init
try {
handle.initialize(id, (IRequest) req, current.ctx);
executor.submit(current.ctx, Executors.callable(handle));
__cb.ice_response(prx);
} catch (Throwable e) {
log.error("Exception on startup; removing handle " + id, e);
unregisterServant(id);
throw e;
}
} catch (Exception e) {
log.error("Exception on " + req);
__cb.ice_exception(e);
} catch (Throwable t) {
log.error("Throwable on " + req);
RuntimeException rt = new RuntimeException("Throwable raised on "
+ req);
rt.initCause(t);
throw rt;
}
}
// ~ Glacier2 API
// =========================================================================
/**
* Destruction simply decrements the reference count for a session to allow
* reconnecting to it. This means that the Glacier timeout property is
* fairly unimportant. If a Glacier connection times out or is otherwise
* destroyed, a client can attempt to reconnect.
*
* However, in the case of only one reference to the session, if the
* Glacier2 timeout is greater than the session timeout, exceptions can be
* thrown when this method tries to clean up the session. Therefore all
* session access must be guarded by a try/finally block.
*/
public void destroy(Ice.Current current) {
Ice.Identity sessionId = sessionId();
log.debug("destroy(" + this + ")");
// Remove this instance from the adapter: 1) to prevent
// further remote calls on it, and 2) to reduce the number
// of calls to destroy() from SessionManagerI.reapSession()
// If an exception if thrown, there's not much we can do,
// and it's important to continue cleaning up resources!
try {
adapter.remove(sessionId); // OK ADAPTER USAGE
// If not in adapter, then SessionManagerI won't be able to find it.
holder.removeClientId(clientId);
} catch (Ice.NotRegisteredException nre) {
// It's possible that another thread tried to remove
// this session first. Logging the fact, but we will
// continue with the closing which should be safe
// to call multiple times.
log.warn("NotRegisteredException: "
+ Ice.Util.identityToString(sessionId()));
} catch (Ice.ObjectAdapterDeactivatedException oade) {
log.warn("Adapter already deactivated. Cannot remove: "
+ sessionId());
} catch (Throwable t) {
log.error("Can't remove service factory", t);
}
int ref;
try {
// First detach and get the reference count.
ref = sessionManager.detach(this.principal.getName());
} catch (SessionException rse) {
// If the session has already been removed or has timed out,
// then we should do everything we can to clean up.
log.info("Session already removed. Cleaning up blitz state.");
ref = 0;
doClose = true;
}
// If we are supposed to close, do only so if the ref count
// is < 1.
if (doClose && ref < 1) {
// First call back to the client to prevent any further access
// We do so one way though to prevent hanging this method. We
// also take steps to not fall into a recursive loop.
ClientCallbackPrx copy = callback;
callback = null;
if (copy != null) {
try {
Ice.ObjectPrx prx = copy.ice_oneway();
ClientCallbackPrx oneway = ClientCallbackPrxHelper
.uncheckedCast(prx);
oneway.sessionClosed();
} catch (Exception e) {
handleCallbackException(e);
}
}
// Must check all session access in this method too.
doDestroy();
try {
ref = sessionManager.close(this.principal.getName());
} catch (SessionException se) {
// An exception could still theoretically be thrown here
// if the timeout/removal happened since the last call.
// Therefore, we'll just let another exception be thrown
// since the time for shutdown is not overly critical.
}
} else {
cleanupSelf();
}
}
/**
* Reusable method for logging an exception that occurs on one-way
* communication with clients during callback notification.
*/
public void handleCallbackException(Exception e) {
if (e instanceof Ice.NotRegisteredException) {
log.warn(clientId + "'s callback not registered -"
+ " perhaps wrong proxy?");
} else if (e instanceof ConnectionRefusedException) {
log.warn(clientId + "'s callback refused connection -"
+ " did the client die?");
} else if (e instanceof ConnectionLostException) {
log.debug(clientId + "'s connection lost as expected");
} else if (e instanceof ConnectTimeoutException) {
log.warn("ConnectTimeoutException on callback:" + clientId);
} else if (e instanceof Ice.SocketException ) {
log.warn("SocketException on callback: " + clientId);
} else {
log.error(
"Unknown error on callback method for client: " + clientId, e);
}
}
/**
* Called if this isn't a kill-everything event.
* @see <a href="http://trac.openmicroscopy.org/ome/ticket/9330">Trac ticket #9330</a>
*/
public void cleanupSelf() {
if (log.isInfoEnabled()) {
log.info(String.format("cleanupSelf(%s).", this));
}
cleanServants(false);
}
/**
* Performs the actual cleanup operation on all the resources shared between
* this and other {@link ome.services.blitz.impl.ServiceFactoryI} instances in the
* same {@link Session}. Since {@link #destroy(Current)} is called regardless by the
* router, even when a client has just died, we have this internal method
* for handling the actual closing of resources.
*
* This method must take precautions to not throw a {@link SessionException}
* . See {@link #destroy(Current)} for more information.
*
* When {@link #destroy(Current)} is called but it isn't time to close down
* the entire instance, then we should still close down the stateless
* services for this instance as well as remove ourselves from the adapter.
*
* @see <a href="http://trac.openmicroscopy.org/ome/ticket/9330">Trac ticket #9330</a>
*/
public void doDestroy() {
if (log.isInfoEnabled()) {
log.info(String.format("doDestroy(%s)", this));
}
cleanServants(true);
}
/**
* Use {@link #cleanServants(boolean, String, ServantHolder, Ice.ObjectAdapter)}
* to clean up.
*
* @param all if stateful sessions should be shut down such that other clients cannot reattach (the servant is cleaned up)
*/
public void cleanServants(boolean all) {
cleanServants(all, clientId, holder, adapter);
}
/**
* Clean all servants that are in the current holder and remove them from
* the adapter. If all is true, then all servants that are non-null will be
* processed. Otherwise, only servants that a) belong to this clientId and
* b) are not stateful (since stateful services may be used by other clients).
*
* @param all if stateful sessions should be shut down such that other clients cannot reattach (the servant is cleaned up)
* @param clientId Only used if all is false.
* @param holder {@link ServantHolder} to be cleaned.
* @param adapter from which the servants should be removed.
*/
public static void cleanServants(boolean all, String clientId,
ServantHolder holder, Ice.ObjectAdapter adapter) {
// Cleaning up resources
// =================================================
holder.acquireLock("*"); // Protects all the servants on destruction
try {
List<String> servants = holder.getServantList();
for (final String idName : servants) {
final Ice.Identity id = holder.getIdentity(idName);
final Object servant = holder.getUntied(id);
if (servant == null) {
log.warn("Servant already removed: " + idName);
// But calling unregister just in case
unregisterServant(id, adapter, holder);
continue; // LOOP.
}
if (!all) {
if (!idName.contains(clientId)) {
// If this doesn't belong to the current client, then
// do nothing.
continue; // LOOP
} else if (servant instanceof _StatefulServiceInterfaceOperations) {
// Allowing stateful sessions to persist even after this
// particular client is shutdown. This allows another client
// to re-attach.
log.info("Leaving StatefulService alive:" + idName);
continue; // LOOP
}
}
// All errors are ignored within the loop.
try {
// Now that we have the servant instance, we do what we can
// to clean it up. Our AmdServants must use a message
// to have the servant removed. InteractiveProcessors must
// be stopped and unregistered. Stateless must only be
// unregistered.
//
if (servant instanceof CloseableServant) {
CloseableServant cs = (CloseableServant) servant;
cs.close(newCurrent(id, "close", adapter, clientId));
}
// Now ignoring all non-CloseableServants, since
// that is *the* interface that should be used
// for any cleanup. See #11378
} catch (Exception e) {
log.error("Error destroying servant: " + idName + "="
+ servant, e);
} finally {
// Now we will again try to remove the servant, which may
// have already been done, after the method call, though, it
// is guaranteed to no longer be active.
unregisterServant(id, adapter, holder);
log.info("Removed servant from adapter: " + idName);
}
}
} finally {
holder.releaseLock("*");
}
}
// ~ Helpers
// =========================================================================
protected void internalServantConfig(Object obj) throws ServerError {
if (obj instanceof SessionAware) {
((SessionAware) obj).setSession(this);
}
}
public static Ice.Current newCurrent(Ice.Identity id, String method,
Ice.ObjectAdapter adapter, String clientId) {
final Ice.Current __curr = new Ice.Current();
__curr.id = id;
__curr.adapter = adapter;
__curr.operation = method;
__curr.ctx = new HashMap<String, String>();
__curr.ctx.put(CLIENTUUID.value, clientId);
return __curr;
}
public Ice.Current newCurrent(Ice.Identity id, String method) {
return newCurrent(id, method, adapter, clientId);
}
public void allow(Ice.ObjectPrx prx) {
if (prx != null && control != null) {
control.identities().add(
new Ice.Identity[] { prx.ice_getIdentity() });
}
}
/**
* Creates a proxy according to the servant definition for the given
* name. Injects the instance for this session so that all
* services are linked to a single session.
*
* Creates an ome.api.* service (mostly managed by Spring), wraps it with
* the {@link HardWiredInterceptor interceptors} which are in effect, and
* stores the instance away in the cache.
*
* Note: Since {@link HardWiredInterceptor} implements
* {@link MethodInterceptor}, all the {@link Advice} instances will be
* wrapped in {@link Advisor} instances and will be returned by
* {@link Advised#getAdvisors()}.
*/
protected Ice.Object createServantDelegate(String name) throws ServerError {
Ice.Object servant = null;
try {
servant = (Ice.Object) context.getBean(name);
configureServant(servant);
return servant;
} catch (ClassCastException cce) {
InternalException ie = new InternalException();
IceMapper.fillServerError(ie, cce);
ie.message = "Could not cast to Ice.Object:[" + name + "]";
throw ie;
} catch (NoSuchBeanDefinitionException nosuch) {
ApiUsageException aue = new ApiUsageException();
aue.message = name
+ " is an unknown service. Please check Constants.ice or the documentation for valid strings.";
throw aue;
} catch (Exception e) {
log.warn("Uncaught exception in createServantDelegate. ", e);
throw new InternalException(null, e.getClass().getName(),
e.getMessage());
}
}
/**
* Apply proper AOP and call setters for any known injection properties.
* @param servant the servant
* @throws ServerError if the configuration or tying failed
*/
public void configureServant(Ice.Object servant) throws ServerError {
// Now setup the servant
// ---------------------------------------------------------------------
internalServantConfig(servant);
Object real = servant;
if (servant instanceof Ice.TieBase) {
Ice.TieBase tie = (Ice.TieBase) servant;
real = tie.ice_delegate();
internalServantConfig(real);
if (real instanceof TieAware) {
((TieAware) real).setTie(tie);
}
}
}
/**
* Registers the servant with the adapter (or throws an exception if one is
* already registered) as well as configures the servant in any post-Spring
* way necessary, based on the type of the servant.
*/
public Ice.ObjectPrx registerServant(Ice.Identity id, Ice.Object servant)
throws ServerError {
Ice.ObjectPrx prx = null;
try {
servant = callContextWrapper(servant);
Ice.Object already = adapter.find(id);
if (null == already) {
adapter.add(servant, id); // OK ADAPTER USAGE
prx = adapter.createDirectProxy(id);
if (log.isInfoEnabled()) {
log.info("Added servant to adapter: "
+ servantString(id, servant));
}
} else {
throw new omero.InternalException(null, null,
"Servant already registered: "
+ servantString(id, servant));
}
} catch (Exception e) {
if (e instanceof omero.InternalException) {
throw (omero.InternalException) e;
} else if (e instanceof Ice.ObjectAdapterDeactivatedException) {
// ticket:1251
ShutdownInProgress sip = new ShutdownInProgress(null, null,
"ObjectAdapter deactivated");
IceMapper.fillServerError(sip, e);
throw sip;
} else {
omero.InternalException ie = new omero.InternalException();
IceMapper.fillServerError(ie, e);
throw ie;
}
}
// Alright to register this servant now.
// Using just the name because the category essentially == this
// holder
holder.put(id, servant);
return prx;
}
protected Ice.Object callContextWrapper(Ice.Object servant) {
// If this isn't a tie, then we can't do any wrapping.
if (!(Ice.TieBase.class.isAssignableFrom(servant.getClass()))) {
return servant;
}
Ice.TieBase tie = (Ice.TieBase) servant;
Object delegate = tie.ice_delegate();
ProxyFactory wrapper = new ProxyFactory(delegate);
wrapper.addAdvice(0, new CallContext(context, token));
tie.ice_delegate(wrapper.getProxy());
return servant;
}
/**
* Calls {@link #unregisterServant(Ice.Identity, Ice.ObjectAdapter, ServantHolder)}
*/
public void unregisterServant(Ice.Identity id) {
unregisterServant(id, adapter, holder);
}
/**
* Reverts all the additions made by
* {@link #registerServant(Ice.Identity, Ice.Object)}
*
* Now called by {@link ome.services.blitz.fire.SessionManagerI} in response
* to an {@link ome.services.blitz.util.UnregisterServantMessage}.
*/
public static void unregisterServant(Ice.Identity id, Ice.ObjectAdapter adapter,
ServantHolder holder) {
// If this is not found ignore.
if (null == adapter.find(id)) {
return; // EARLY EXIT!
}
// Here we assume that if the "close()" call is required, that it has
// already been made, either by a user or by the SF.close() method in
// which case unregisterServant() is being closed via
// onApplicationEvent().
// Otherwise, it is being called directly by SF.close().
final Ice.Object obj = adapter.remove(id); // OK ADAPTER USAGE
final String str = servantString(id, obj);
if (holder == null) {
log.warn("Holder is null for " + str);
} else {
Object removed = holder.remove(id);
if (removed == null) {
log.error("Adapter and active servants out of sync.");
}
}
if (log.isInfoEnabled()) {
log.info("Unregistered servant:" + str);
}
}
private static String servantString(Ice.Identity id, Object obj) {
StringBuilder sb = new StringBuilder(Ice.Util.identityToString(id));
sb.append("(");
sb.append(obj);
sb.append(")");
return sb.toString();
}
// Id Helpers
// =========================================================================
// Used for naming service factory instances and creating Ice.Identities
// from Ice.Currents, etc.
public Ice.Identity getIdentity(String name) {
return holder.getIdentity(name);
}
/**
* Definition of session ids: {@code "session-<CLIENTID>/<UUID>"}
*/
public static Ice.Identity sessionId(String clientId, String uuid) {
Ice.Identity id = new Ice.Identity();
id.category = "session-" + clientId;
id.name = uuid;
return id;
}
/**
* Returns the {@link Ice.Identity} for this instance as defined by
* {@link #sessionId(String, String)}
*/
public Ice.Identity sessionId() {
return sessionId(clientId, principal.getName());
}
/**
* Helpers method to extract the {@link CLIENTUUID} out of the given
* Ice.Current. Throws an {@link ApiUsageException} if none is present,
* since it is each client's responsibility to set this value.
*
* (Typically done in our SDKs)
*/
public static String clientId(Ice.Current current) throws ApiUsageException {
String clientId = null;
if (current.ctx != null) {
clientId = current.ctx.get(omero.constants.CLIENTUUID.value);
}
if (clientId == null) {
throw new ApiUsageException(null, null, "No "
+ omero.constants.CLIENTUUID.value
+ " key provided in context.");
}
return clientId;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append("(");
sb.append(Ice.Util.identityToString(sessionId()));
sb.append(")");
return sb.toString();
}
}