/*
* Copyright 2008 Glencoe Software, Inc. All rights reserved.
* Use is subject to license terms supplied in LICENSE.txt
*/
package ome.services.util;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import ome.conditions.InternalException;
import ome.security.SecuritySystem;
import ome.security.basic.CurrentDetails;
import ome.system.EventContext;
import ome.system.OmeroContext;
import ome.system.Principal;
import ome.system.ServiceFactory;
import ome.tools.hibernate.SessionFactory;
import ome.tools.spring.InternalServiceFactory;
import ome.util.SqlAction;
import org.aopalliance.aop.Advice;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.hibernate.Session;
import org.hibernate.StatelessSession;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionCallback;
/**
* Simple execution/work interface which can be used for <em>internal</em> tasks
* which need to have a full working implementation. The
* {@link Executor#execute(Principal, ome.services.util.Executor.Work)} method
* ensures that {@link SecuritySystem#login(Principal)} is called before the
* task, that a {@link TransactionCallback} and a {@link HibernateCallback}
* surround the call, and that subsequently {@link SecuritySystem#logout()} is
* called.
*
* @author Josh Moore, josh at glencoesoftware.com
* @since 3.0-Beta3
*/
public interface Executor extends ApplicationContextAware {
public enum Priority {
/**
* Uses a non-limited thread pool.
*/
SYSTEM,
/**
* Uses the limited thread pool configured via etc/omero.properties
* with omero.threads.max_threads, etc.
*/
USER;
}
/**
* Provides access to the context for Work-API consumers who need to publish
* events, etc.
*/
public OmeroContext getContext();
/**
* Returns a {@link Principal} representing your current session or null,
* if none is active.
*/
public Principal principal();
/**
* Call {@link #execute(Map, Principal, Work)} with
* a null call context.
*/
public Object execute(final Principal p, final Work work);
/**
* Executes a {@link Work} instance wrapped in two layers of AOP. The first
* is intended to acquire the proper arguments for
* {@link Work#doWork(Session, ServiceFactory)} from the
* {@link OmeroContext}, and the second performs all the standard service
* actions for any normal method call.
*
* If the {@link Map} argument is not null, then additionally,
* setContext will be called in a try/finally block. The first login
* within this thread will then pick up this delayed context.
*
* If the {@link Principal} argument is not null, then additionally, a
* login/logout sequence will be performed in a try/finally block.
*
* {@link Work} implementation must be annotated with {@link Transactional}
* in order to properly specify isolation, read-only status, etc.
*
* @param callContext
* Possibly null.
* @param p
* Possibly null.
* @param work
* Not null.
* @return See above.
*/
public Object execute(final Map<String, String> callContext,
final Principal p, final Work work);
/**
* Call {@link #submit(Map, Callable)} with a null callContext.
* @param callable
* @return See above.
*/
public <T> Future<T> submit(final Callable<T> callable);
/**
* Simple submission method which can be used in conjunction with a call to
* {@link #execute(Principal, Work)} to overcome the no-multiple-login rule.
*
* @param <T>
* @param callContext Possibly null. See {@link CurrentDetails#setContext(Map)}
* @param callable Not null. Action to be taken.
* @return See above.
*/
public <T> Future<T> submit(final Map<String, String> callContext,
final Callable<T> callable);
/**
* Simple submission method with a {@link Priority}.
*
* @param prio Possibly null. See {@link #submit(Priority, Map, Callable)}
* @param callable Not null. Action to be taken.
* @return See above.
*/
public <T> Future<T> submit(final Priority prio,
final Callable<T> callable);
/**
* Like {@link #submit(Map, Callable)} but provide info to which priority
* queue should be used.
*
* @param prio Possibly null. Priority for execution. Default: {@link Priority#USER}
* @param callContext Possibly null. See {@link CurrentDetails#setContext(Map)}
* @param callable Not null. Action to be taken.
* @return See above.
*/
public <T> Future<T> submit(Priority prio,
final Map<String, String> callContext,
final Callable<T> callable);
/**
* Helper method to perform {@link Future#get()} and properly unwrap the
* exceptions. Any {@link RuntimeException} which was thrown during
* execution will be rethrown. All other exceptions will be wrapped in an
* {@link InternalException}.
*/
public <T> T get(final Future<T> future);
/**
* Returns the {@link ExecutorService} assigned to this instance.
* Used by {@link #submit(Callable)} and {@link #get(Future)}.
*/
public ExecutorService getService();
/**
* Executes a {@link SqlWork} wrapped with a transaction. Since
* {@link StatelessSession} does not return proxies, there is less concern
* about returned values, but this method <em>completely</em> overrides
* OMERO security, and should be used <b>very</em> carefully.
*
* As with {@link #execute(Principal, Work)} the {@link SqlWork}
* instance must be properly marked with an {@link Transactional}
* annotation.
*
* @param work
* Non-null.
* @return See above.
*/
public Object executeSql(final SqlWork work);
/**
* Work SPI to perform actions within the server as if they were fully
* wrapped in our service logic. Note: any results which are coming from
* Hibernate <em>may <b>not</b></em> be assigned directly to a field, rather
* must be returned as an {@link Object} so that Hibernate proxies can be
* properly handled.
*/
public interface Work<X> {
/**
* Returns a description of what this work will be doing for logging
* purposes.
*/
String description();
/**
* Work method. Must return all results coming from Hibernate via the
* {@link Object} return method.
*
* @param session
* non null.
* @param sf
* non null.
* @return Any results which will be used by non-wrapped code.
*/
X doWork(Session session, ServiceFactory sf);
}
/**
* In the case of a stateful work order, the bean itself needs to be
* associated with the sessionHandler and thread rather than the word
* instance itself. If SessionHandler sees a StatefulWork instance
* it will ask for the inner "this" to be used.
*/
public interface StatefulWork {
Object getThis();
}
/**
* Work SPI to perform actions related to
* {@link org.hibernate.SessionFactory#openStatelessSession() stateless}
* sessions. This overrides <em>ALL</em> security in the server and should
* only be used as a last resort. Currently accept locations are:
* <ul>
* <li>In the {@link ome.services.sessions.SessionManager} to boot strap a
* {@link ome.model.meta.Session session}
* <li>In the {@link ome.security.basic.EventHandler} to save
* {@link ome.model.meta.EventLog event logs}
* </ul>
*
* Before the JTA fixes of 4.0, this interface provided a
* {@link org.hibernate.StatelessSession}. However, as mentioned in
* <a href="http://jira.springframework.org/browse/SPR-2495">jira:SPR-2495</a>,
* that interface is not
* currently supported in Spring's transaction management.
*/
public interface SqlWork {
/**
* Return a description of what this work will be doing for logging
* purposes.
*/
String description();
Object doWork(SqlAction sql);
}
public abstract class Descriptive {
final protected String description;
public Descriptive(Object o, String method, Object...params) {
this(o.getClass().getName(), method, params);
}
public Descriptive(String name, String method, Object...params) {
StringBuilder sb = new StringBuilder();
sb.append(name);
sb.append(".");
sb.append(method);
sb.append(ServiceHandler.getResultsString(params,
new IdentityHashMap<Object, String>()));
this.description = sb.toString();
}
public String description() {
return description;
}
}
/**
* Simple adapter which takes a String for {@link #description}
*/
public abstract class SimpleWork extends Descriptive implements Work {
/**
* Member field set by the {@link Executor} instance before
* invoking {@link #doWork(Session, ServiceFactory)}. This
* was introduced to prevent strange contortions trying to
* get access to JDBC directly since the methods on Session
* are no longer usable. It was introduced as a setter-injection
* to prevent wide-scale changes to the code-base. It could
* equally have been added to the interface method as an argument.
*
* @see ticket:73
*/
private /*final*/ SqlAction sql;
public SimpleWork(Object o, String method, Object...params) {
super(o, method, params);
}
public synchronized void setSqlAction(SqlAction sql) {
if (this.sql != null) {
throw new InternalException("Can only set SqlAction once!");
}
this.sql = sql;
}
public SqlAction getSqlAction() {
return sql;
}
}
/**
* Simple adapter which takes a String for {@link #description}
*/
public abstract class SimpleSqlWork extends Descriptive implements SqlWork {
public SimpleSqlWork(Object o, String method, Object...params) {
super(o, method, params);
}
}
public class Impl implements Executor {
private final static Logger log = LoggerFactory.getLogger(Executor.class);
protected OmeroContext context;
protected InternalServiceFactory isf;
final protected List<Advice> advices = new ArrayList<Advice>();
final protected CurrentDetails principalHolder;
final protected String[] proxyNames;
final protected SessionFactory factory;
final protected SqlAction sqlAction;
final protected ExecutorService service;
final protected ExecutorService systemService;
public Impl(CurrentDetails principalHolder, SessionFactory factory,
SqlAction sqlAction, String[] proxyNames) {
this(principalHolder, factory, sqlAction, proxyNames,
java.util.concurrent.Executors.newCachedThreadPool());
}
public Impl(CurrentDetails principalHolder, SessionFactory factory,
SqlAction sqlAction, String[] proxyNames,
ExecutorService service) {
this.sqlAction = sqlAction;
this.factory = factory;
this.principalHolder = principalHolder;
this.proxyNames = proxyNames;
this.service = service;
// Allowed to create more threads.
this.systemService = Executors.newCachedThreadPool();
}
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.context = (OmeroContext) applicationContext;
this.isf = new InternalServiceFactory(this.context);
for (String name : proxyNames) {
advices.add((Advice) this.context.getBean(name));
}
}
public OmeroContext getContext() {
return this.context;
}
public Principal principal() {
if (principalHolder.size() == 0) {
return null;
} else {
EventContext ec = principalHolder.getCurrentEventContext();
String session = ec.getCurrentSessionUuid();
return new Principal(session);
}
}
/**
* Call {@link #execute(Map, Principal, Work)}
* with a null call context.
*/
public Object execute(final Principal p, final Work work) {
return execute(null, p, work);
}
/**
* Executes a {@link Work} instance wrapped in two layers of AOP. The
* first is intended to acquire the proper arguments for
* {@link Work#doWork(Session, ServiceFactory)} for
* the {@link OmeroContext}, and the second performs all the standard
* service actions for any normal method call.
*
* If the {@link Principal} argument is not null, then additionally, a
* login/logout sequence will be performed in a try/finally block.
*
* @param callContext Possibly null key-value map. See #3529
* @param p
* @param work
*/
public Object execute(final Map<String, String> callContext,
final Principal p, final Work work) {
if (work instanceof SimpleWork) {
((SimpleWork) work).setSqlAction(sqlAction);
}
Interceptor i = new Interceptor(factory);
ProxyFactory factory = new ProxyFactory();
factory.setTarget(work);
factory.setInterfaces(new Class[] { Work.class });
for (Advice advice : advices) {
factory.addAdvice(advice);
}
factory.addAdvice(i);
Work wrapper = (Work) factory.getProxy();
// First we guarantee that this will cause one and only
// login to take place.
if (p == null && principalHolder.size() == 0) {
throw new IllegalStateException("Must provide principal");
} else if (p != null && principalHolder.size() > 0) {
throw new IllegalStateException(
"Already logged in. Use Executor.submit() and .get().");
}
// Don't need to worry about the login stack below since
// already checked.
if (p != null) {
this.principalHolder.login(p);
}
if (callContext != null) {
this.principalHolder.setContext(callContext);
}
try {
// Arguments will be replaced after hibernate is in effect
return wrapper.doWork(null, isf);
} finally {
if (callContext != null) {
this.principalHolder.setContext(null);
}
if (p != null) {
int left = this.principalHolder.logout();
if (left > 0) {
log.warn("Logins left: " + left);
for (int j = 0; j < left; j++) {
this.principalHolder.logout();
}
}
}
}
}
public <T> Future<T> submit(final Callable<T> callable) {
return submit(null, null, callable);
}
public <T> Future<T> submit(final Map<String, String> callContext,
final Callable<T> callable) {
return submit(null, callContext, callable);
}
public <T> Future<T> submit(final Priority prio,
final Callable<T> callable) {
return submit(prio, null, callable);
}
public <T> Future<T> submit(final Priority prio,
final Map<String, String> callContext,
final Callable<T> callable) {
Callable<T> wrapper = callable;
if (callContext != null) {
wrapper = new Callable<T>() {
public T call() throws Exception {
principalHolder.setContext(callContext);
try {
return callable.call();
} finally {
principalHolder.setContext(null);
}
}
};
}
if (prio == null || prio == Priority.USER) {
return service.submit(wrapper);
} else if (prio == Priority.SYSTEM) {
return systemService.submit(wrapper);
} else {
throw new InternalException("Unknown priority: " + prio);
}
}
public <T> T get(final Future<T> future) {
try {
return future.get();
} catch (InterruptedException e1) {
throw new InternalException("Future.get interrupted:"
+ e1.getMessage());
} catch (ExecutionException e1) {
if (e1.getCause() instanceof RuntimeException) {
throw (RuntimeException) e1.getCause();
} else {
throw new InternalException(
"Caught exception thrown by Future.get:"
+ e1.getMessage());
}
}
}
public ExecutorService getService() {
return service;
}
/**
* Executes a {@link SqlWork} in transaction.
*
* @param work
* Non-null.
* @return See above.
*/
public Object executeSql(final SqlWork work) {
if (principalHolder.size() > 0) {
throw new IllegalStateException(
"Currently logged in. \n"
+ "JDBC will then take part in transaction directly. \n"
+ "Please have the proper JDBC or data source injected.");
}
ProxyFactory factory = new ProxyFactory();
factory.setTarget(work);
factory.setInterfaces(new Class[] { SqlWork.class });
factory.addAdvice(advices.get(2)); // TX FIXME
SqlWork wrapper = (SqlWork) factory.getProxy();
return wrapper.doWork(this.sqlAction);
}
/**
* Interceptor class which properly lookups and injects the session
* objects in the
* {@link Work#doWork(TransactionStatus, Session, ServiceFactory)}
* method.
*/
static class Interceptor implements MethodInterceptor {
private final SessionFactory factory;
public Interceptor(SessionFactory sf) {
this.factory = sf;
}
public Object invoke(final MethodInvocation mi) throws Throwable {
final Object[] args = mi.getArguments();
args[0] = factory.getSession();
return mi.proceed();
}
}
}
}