/*
* $Id$
*
* Copyright 2007 Glencoe Software, Inc. All rights reserved.
* Use is subject to license terms supplied in LICENSE.txt
*/
package ome.services.blitz.fire;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import Glacier2.CannotCreateSessionException;
import Glacier2.StringSetPrx;
import ome.logic.HardWiredInterceptor;
import ome.security.SecuritySystem;
import ome.services.blitz.impl.ServiceFactoryI;
import ome.services.blitz.util.ConvertToBlitzExceptionMessage;
import ome.services.blitz.util.FindServiceFactoryMessage;
import ome.services.blitz.util.UnregisterServantMessage;
import ome.services.messages.DestroySessionMessage;
import ome.services.sessions.SessionManager;
import ome.services.sessions.events.ChangeSecurityContextEvent;
import ome.services.util.Executor;
import ome.system.OmeroContext;
import ome.system.Principal;
import ome.system.Roles;
import ome.util.messages.InternalMessage;
import ome.util.messages.MessageException;
import omero.ApiUsageException;
import omero.WrappedCreateSessionException;
import omero.api.ClientCallbackPrxHelper;
import omero.api._ServiceFactoryTie;
import omero.cmd.SessionI;
import omero.constants.EVENT;
import omero.constants.GROUP;
import omero.constants.topics.HEARTBEAT;
import omero.util.ServantHolder;
/**
* Central login logic for all OMERO.blitz clients. It is required to create a
* {@link Glacier2.Session} via the {@link Glacier2.SessionManager} in order to
* get through the firewall. The {@link Glacier2.Session} (here a
* {@link ServiceFactoryI} instance) also manages all servants created by the
* client.
*
* @author Josh Moore, josh at glencoesoftware.com
* @since 3.0-Beta2
*/
public final class SessionManagerI extends Glacier2._SessionManagerDisp
implements ApplicationContextAware, ApplicationListener<InternalMessage> {
/**
* "ome.security.basic.BasicSecurityWiring" <em>may</em> be replaced by
* another value at compile time (see blitz/build.xml), but a default value
* is necessary here fore testing.
*/
private final static List<HardWiredInterceptor> CPTORS = HardWiredInterceptor
.parse(new String[] { "ome.security.basic.BasicSecurityWiring" });
private final static Logger log = LoggerFactory.getLogger(SessionManagerI.class);
protected OmeroContext context;
protected final Ice.ObjectAdapter adapter;
protected final SecuritySystem securitySystem;
protected final SessionManager sessionManager;
protected final Executor executor;
protected final Ring ring;
protected final Registry registry;
protected final TopicManager topicManager;
protected final AtomicBoolean loaded = new AtomicBoolean(false);
protected final int servantsPerSession;
/**
* An internal mapping to all {@link ServiceFactoryI} instances for a given
* session since there is no method on {@link Ice.ObjectAdapter} to retrieve
* all servants.
*/
protected final Cache<String, ServantHolder> sessionToHolder = CacheBuilder.newBuilder().build();
public SessionManagerI(Ring ring, Ice.ObjectAdapter adapter,
SecuritySystem secSys, SessionManager sessionManager,
Executor executor, TopicManager topicManager, Registry reg,
int servantsPerSession) {
this.ring = ring;
this.registry = reg;
this.adapter = adapter;
this.executor = executor;
this.securitySystem = secSys;
this.topicManager = topicManager;
this.sessionManager = sessionManager;
this.servantsPerSession = servantsPerSession;
}
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.context = (OmeroContext) applicationContext;
HardWiredInterceptor.configure(CPTORS, context);
loaded.set(true);
}
public Glacier2.SessionPrx create(String userId,
Glacier2.SessionControlPrx control, Ice.Current current)
throws CannotCreateSessionException {
if (!loaded.get()) {
WrappedCreateSessionException wrapped = new WrappedCreateSessionException();
wrapped.backOff = 1000L;
wrapped.concurrency = true;
wrapped.reason = "Server not fully initialized";
wrapped.type = "ApiUsageException";
throw wrapped;
}
try {
// First thing we do is guarantee that the client is giving us
// the required information.
ServiceFactoryI.clientId(current); // throws ApiUsageException
// Before asking the ring, see if we already have the
// session locally.
boolean local = false;
try {
Object o = sessionManager.find(userId);
local = (o != null);
log.info("Found session locally: " + userId);
} catch (Exception e) {
log.debug("Exception while waiting on "
+ "SessionManager.find " + e);
}
// If not, then give the ring a chance to redirect to
// other instances which may already have it.
if (!local) {
Glacier2.SessionPrx sf = ring.getProxyOrNull(userId, control,
current);
if (sf != null) {
return sf; // EARLY EXIT
}
}
// Defaults
Roles roles = securitySystem.getSecurityRoles();
String group = getGroup(current);
if (group == null) {
group = roles.getUserGroupName();
}
String event = getEvent(current);
if (event == null) {
event = "User"; // FIXME This should be in Roles as well.
}
String agent = getAgent(current);
String ip = getIP(current);
// Create the session for this ServiceFactory
Principal p = new Principal(userId, group, event);
final ome.model.meta.Session s = sessionManager.createWithAgent(p, agent, ip);
Principal sp = new Principal(s.getUuid(), group, event);
// Event raised to add to Ring
final boolean needsNewSession = sessionToHolder.getIfPresent(s.getUuid()) == null;
final ServantHolder holder =
sessionToHolder.get(s.getUuid(), new Callable<ServantHolder>() {
@Override
public ServantHolder call() {
return new ServantHolder(s.getUuid(), servantsPerSession);
}
});
// Create the ServiceFactory
ServiceFactoryI session = new ServiceFactoryI(local /* ticket:911 */,
current, holder, control, context, sessionManager, executor,
sp, CPTORS, topicManager, registry, ring.uuid);
Ice.Identity id = session.sessionId();
holder.addClientId(session.clientId);
if (control != null) {
// Not having a control implies that this is an internal
// call, not coming through Glacier, so we can trust it.
StringSetPrx cat = control.categories();
cat.add(new String[]{id.category});
cat.add(new String[]{id.name});
}
_ServiceFactoryTie tie = new _ServiceFactoryTie(session);
Ice.ObjectPrx _prx = current.adapter.add(tie, id); // OK Usage
// Logging & sessionToClientIds addition
if (needsNewSession) {
log.info(String.format("Created session %s for user %s (agent=%s)",
session, userId, agent));
} else {
if (log.isInfoEnabled()) {
log.info(String.format("Rejoining session %s (agent=%s)",
session, agent));
}
}
return Glacier2.SessionPrxHelper.uncheckedCast(_prx);
} catch (Exception t) {
// Then we are good to go.
if (t instanceof CannotCreateSessionException) {
throw (CannotCreateSessionException) t;
}
// These need special handling as well.
else if (t instanceof ome.conditions.ConcurrencyException
|| t instanceof omero.ConcurrencyException) {
// Parse out the back off, then everything is generic.
long backOff = (t instanceof omero.ConcurrencyException) ? ((omero.ConcurrencyException) t).backOff
: ((ome.conditions.ConcurrencyException) t).backOff;
WrappedCreateSessionException wrapped = new WrappedCreateSessionException();
wrapped.backOff = backOff;
wrapped.type = t.getClass().getName();
wrapped.concurrency = true;
wrapped.reason = "ConcurrencyException: " + t.getMessage()
+ "\nPlease retry in " + backOff + "ms. Cause: "
+ t.getMessage();
throw wrapped;
}
ConvertToBlitzExceptionMessage convert = new ConvertToBlitzExceptionMessage(
this, t);
try {
// TODO Possibly provide context.convert(ConversionMsg) methd.
context.publishMessage(convert);
} catch (Throwable t2) {
log.error("Error while converting exception:", t2);
}
if (convert.to instanceof CannotCreateSessionException) {
throw (CannotCreateSessionException) convert.to;
}
// We make an exception for some more or less "expected" exception
// types. Everything else gets logged as an error which we need
// to review.
if (!(t instanceof omero.ApiUsageException
|| t instanceof ome.conditions.ApiUsageException || t instanceof ome.conditions.SecurityViolation)) {
log.error("Error while creating ServiceFactoryI", t);
}
WrappedCreateSessionException wrapped = new WrappedCreateSessionException();
wrapped.backOff = -1;
wrapped.concurrency = false;
wrapped.reason = t.getMessage();
wrapped.type = t.getClass().getName();
wrapped.setStackTrace(t.getStackTrace());
throw wrapped;
}
}
// Listener
// =========================================================================
public void onApplicationEvent(InternalMessage event) {
try {
if (event instanceof UnregisterServantMessage) {
UnregisterServantMessage msg = (UnregisterServantMessage) event;
Ice.Current curr = msg.getCurrent();
ServantHolder holder = msg.getHolder();
// Using static method since we may not have a clientId
// in order to look up the SessionI/ServiceFactoryI
SessionI.unregisterServant(curr.id, adapter, holder);
} else if (event instanceof FindServiceFactoryMessage) {
FindServiceFactoryMessage msg = (FindServiceFactoryMessage) event;
Ice.Identity id = msg.getIdentity();
if (id == null) {
Ice.Current curr = msg.getCurrent();
id = getServiceFactoryIdentity(curr);
}
ServiceFactoryI sf = getServiceFactory(id);
msg.setServiceFactory(id, sf);
} else if (event instanceof DestroySessionMessage) {
DestroySessionMessage msg = (DestroySessionMessage) event;
reapSession(msg.getSessionId());
} else if (event instanceof ChangeSecurityContextEvent) {
ChangeSecurityContextEvent csce = (ChangeSecurityContextEvent) event;
checkStatefulServices(csce);
}
} catch (Throwable t) {
throw new MessageException("SessionManagerI.onApplicationEvent", t);
}
}
/**
* Checks that there are no stateful services active for the session.
*/
void checkStatefulServices(ChangeSecurityContextEvent csce) {
String uuid = csce.getUuid();
final ServantHolder holder = sessionToHolder.getIfPresent(uuid);
if (holder == null) {
return; // Should never happen. Possibly during testing.
}
String servants = holder.getStatefulServiceCount();
if (servants.length() > 0) {
String msg = uuid + " has active stateful services:\n" + servants;
log.debug(msg);
csce.cancel(msg);
}
}
/**
* {@link ServiceFactoryI#doDestroy() Destroys} all the
* {@link ServiceFactoryI} instances based on the given sessionId. Multiple
* clients can be attached to the same session, each with its own
* {@link ServiceFactoryI}
*/
public void requestHeartBeats() {
log.info("Performing requestHeartbeats");
this.context.publishEvent(new TopicManager.TopicMessage(this,
HEARTBEAT.value, new ClientCallbackPrxHelper(),
"requestHeartbeat"));
}
/**
* {@link ServiceFactoryI#doDestroy() Destroys} all the
* {@link ServiceFactoryI} instances based on the given sessionId. Multiple
* clients can be attached to the same session, each with its own
* {@link ServiceFactoryI}
*/
public void reapSession(String sessionId) {
final ServantHolder holder = sessionToHolder.getIfPresent(sessionId);
if (holder == null) {
return;
} else {
sessionToHolder.invalidate(sessionId);
}
Set<String> clientIds = holder.getClientIds();
if (clientIds != null) {
if (clientIds.size() > 0) {
log.info("Reaping " + clientIds.size() + " clients for " + sessionId);
}
for (String clientId : clientIds) {
try {
ServiceFactoryI sf = getServiceFactory(clientId, sessionId);
if (sf != null) {
sf.doDestroy();
Ice.Identity id = sf.sessionId();
log.info("Removing " + sf);
adapter.remove(id); // OK ADAPTER USAGE
}
} catch (Ice.ObjectAdapterDeactivatedException oade) {
// If the object adapter is deactivated, then
// there's basically nothing else we can do.
log.warn("Cannot reap session " + sessionId
+ " from client " + clientId
+ " since adapter is deactivated. Skipping rest");
return;
} catch (Exception e) {
log.error("Error reaping session " + sessionId
+ " from client " + clientId, e);
}
}
List<String> servantIds = holder.getServantList();
if (servantIds.size() > 0) {
log.warn(String.format(
"Reaping all remaining servants for %s: Count=%s",
sessionId, servantIds.size()));
SessionI.cleanServants(true, null, holder, adapter);
}
}
}
// Helpers
// =========================================================================
protected ServiceFactoryI getServiceFactory(String clientId, String sessionId) {
Ice.Identity iid = ServiceFactoryI.sessionId(clientId,
sessionId);
return getServiceFactory(iid);
}
protected ServiceFactoryI getServiceFactory(Ice.Identity iid) {
Ice.Object obj = adapter.find(iid);
if (obj == null) {
log.debug(Ice.Util.identityToString(iid)
+ " already removed.");
return null;
}
if (obj instanceof _ServiceFactoryTie) {
_ServiceFactoryTie tie = (_ServiceFactoryTie) obj;
ServiceFactoryI sf = (ServiceFactoryI) tie.ice_delegate();
return sf;
} else {
log.warn("Not a ServiceFactory: " + obj);
return null;
}
}
protected Ice.Identity getServiceFactoryIdentity(Ice.Current curr) {
Ice.Identity id;
try {
String clientId = ServiceFactoryI.clientId(curr);
id = ServiceFactoryI.sessionId(clientId, curr.id.category);
} catch (ApiUsageException e) {
throw new RuntimeException(
"Cannot create session id for servant:"
+ String.format("\nInfo:\n\tId:%s\n\tOp:%s\n\tCtx:%s",
Ice.Util.identityToString(curr.id),
curr.operation, curr.ctx),
e);
}
return id;
}
protected String getGroup(Ice.Current current) {
if (current.ctx == null) {
return null;
}
return current.ctx.get(GROUP.value);
}
protected String getAgent(Ice.Current current) {
if (current.ctx == null) {
return null;
}
return current.ctx.get("omero.agent");
}
protected String getIP(Ice.Current current) {
if (current.ctx == null) {
return null;
}
return current.ctx.get("omero.ip");
}
protected String getEvent(Ice.Current current) {
if (current.ctx == null) {
return null;
}
return current.ctx.get(EVENT.value);
}
}