/* * $Id$ * * Copyright 2006 University of Dundee. All rights reserved. * Use is subject to license terms supplied in LICENSE.txt */ package ome.tools.hibernate; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Collections; import java.util.Map; import java.util.WeakHashMap; import javax.sql.DataSource; import ome.api.StatefulServiceInterface; import ome.conditions.ApiUsageException; import ome.conditions.InternalException; import ome.services.messages.RegisterServiceCleanupMessage; import ome.services.util.Executor; import ome.system.OmeroContext; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.hibernate.FlushMode; import org.hibernate.HibernateException; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.orm.hibernate3.HibernateInterceptor; import org.springframework.orm.hibernate3.SessionHolder; import org.springframework.transaction.support.TransactionSynchronizationManager; /** * holder for Hibernate sessions in stateful servics. A count of calls is kept. * * @author Josh Moore      <a * href="mailto:josh.moore@gmx.de">josh.moore@gmx.de</a> * @version 3.0 <small> (<b>Internal version:</b> $Rev$ $Date$) </small> * @since 3.0 */ class SessionStatus { int calls = 0; Session session; SessionStatus(Session session) { if (null == session) { throw new IllegalArgumentException("No null sessions."); } this.session = session; } } /** * interceptor which delegates to * {@link org.springframework.orm.hibernate3.HibernateInterceptor} for stateless * services but which keeps a {@link java.util.WeakHashMap} of sessions keyed by * the stateful service reference. * * original idea from: * http://opensource2.atlassian.com/confluence/spring/pages/viewpage.action?pageId=1447 * * See also: http://sourceforge.net/forum/message.php?msg_id=2455707 * http://forum.springframework.org/archive/index.php/t-10344.html * http://opensource2.atlassian.com/projects/spring/browse/SPR-746 * * and these: http://www.hibernate.org/43.html#A5 * http://www.carbonfive.com/community/archives/2005/07/ive_been_meanin.html * http://www.hibernate.org/377.html * * @author Josh Moore      <a * href="mailto:josh.moore@gmx.de">josh.moore@gmx.de</a> * @version 3.0 <small> (<b>Internal version:</b> $Rev$ $Date$) </small> * @since 3.0 */ public class SessionHandler implements MethodInterceptor, ApplicationContextAware { /** * used by the SessionHandler to test for the end of the stateful service's * life. Using reflection so we get a bit more type safety. */ private final Method close; private final static Logger log = LoggerFactory.getLogger(SessionHandler.class); /** * Access to this collection should only be performed by the protected * methods on this class in order to guarantee the semantics in * {@link #getThis(MethodInvocation)}. */ private final Map<Object, SessionStatus> __sessions = Collections .synchronizedMap(new WeakHashMap<Object, SessionStatus>()); private OmeroContext ctx; private final SessionFactory factory; private final static SessionHolder DUMMY = new EmptySessionHolder(); final private static String CTOR_MSG = "Both arguments to the SessionHandler" + " constructor should be not null."; /** * Constructor taking a {@link SessionFactory}. * A new {@link HibernateInterceptor} will be created. * * @param factory * Not null. */ public SessionHandler(SessionFactory factory) { if (factory == null) { throw new ApiUsageException(CTOR_MSG); } this.factory = factory; try { close = StatefulServiceInterface.class.getMethod("close"); } catch (Exception e) { throw new InternalException( "Can't get StatefulServiceInterface.close method."); } } public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.ctx = (OmeroContext) applicationContext; } // // LOOKUP METHODS // protected Object getThis(MethodInvocation invocation) { Object obj = invocation.getThis(); if (obj instanceof Executor.StatefulWork) { obj = ((Executor.StatefulWork) obj).getThis(); } return obj; } protected void putStatus(MethodInvocation invocation, SessionStatus status) { __sessions.put(getThis(invocation), status); } private SessionStatus getStatus(MethodInvocation invocation) { return __sessions.get(getThis(invocation)); } protected SessionStatus removeStatus(final MethodInvocation invocation) { return __sessions.remove(getThis(invocation)); } // // THREAD METHODS // public void cleanThread() { if (TransactionSynchronizationManager.hasResource(factory)) { SessionHolder holder = (SessionHolder) TransactionSynchronizationManager .getResource(factory); if (holder == null) { throw new IllegalStateException("Can't be null."); } else if (holder == DUMMY) { TransactionSynchronizationManager.unbindResource(factory); } else { throw new IllegalStateException("Thread corrupted."); } } } /** * delegates to {@link HibernateInterceptor} or manages sessions internally, * based on the type of service. */ public Object invoke(final MethodInvocation invocation) throws Throwable { Object obj = getThis(invocation); // Stateless; normal semantics. if (!StatefulServiceInterface.class.isAssignableFrom( obj.getClass())) { throw new InternalException( "Stateless service configured as stateful."); } // Stateful; let's get to work. debug("Performing action in stateful session."); return doStateful(invocation); } private Object doStateful(final MethodInvocation invocation) throws Throwable { Object result = null; SessionStatus status = null; try { // Need to open even if "closing" because the service may need // to perform cleanup in its close() method. status = newOrRestoredSession(invocation); status.session.setFlushMode(FlushMode.COMMIT); // changing MANUAL to COMMIT for ticket:557. the appserver // won't allow us to commit here anyway, and setting to COMMIT // prevents Spring from automatically re-writing the flushMode // as AUTO result = invocation.proceed(); return result; } finally { // TODO do we need to check for disconnected or closed session here? // The newOrRestoredSession method does not attempt to close the // session before throwing the dirty session exception. We must do // it here. try { if (isCloseSession(invocation)) { ctx.publishMessage(new RegisterServiceCleanupMessage(this, invocation.getThis()) { @Override public void close() { SessionStatus status = removeStatus(invocation); status.session.disconnect(); status.session.close(); } }); } else { if (status != null) { // Guarantee that no one has changed the FlushMode status.session.setFlushMode(FlushMode.MANUAL); status.session.disconnect(); status.calls--; } } } catch (Exception e) { log.error("Error while closing/disconnecting session.", e); } finally { try { resetThreadSession(); } catch (Exception e) { log.error("Could not cleanup thread session.", e); throw e; } } } } private SessionStatus newOrRestoredSession(MethodInvocation invocation) throws HibernateException { SessionStatus status = getStatus(invocation); Session previousSession = nullOrSessionBoundToThread(); // a session is currently running. // something has gone wrong (e.g. with cleanup) abort! if (previousSession != null) { String msg = "Dirty Hibernate Session " + previousSession + " found in Thread " + Thread.currentThread(); // If it is closeSession, then this will be handled by // the finally{} block of doStateful if (!isCloseSession(invocation)) { previousSession.close(); } throw new InternalException(msg); } // we may or may not be in a session, but if we haven't yet bound // it to This, then we need to. else if (status == null || !status.session.isOpen()) { Session currentSession = acquireAndBindSession(); status = new SessionStatus(currentSession); putStatus(invocation, status); } // the session bound to This is already currently being called. abort! else if (status.calls > 1) { throw new InternalException( "Hibernate session is not re-entrant.\n" + "Either you have two threads operating on the same " + "stateful object (don't do this)\n or you have a " + "recursive call (recurse on the unwrapped object). "); } // all is fine. else { debug("Binding and reconnecting session."); // TODO doesn't make sense to check, because hibernate always // says "yes" if it has a connectionProvider // if (status.session.isConnected()) // { // throw new InternalException("Session already connected!"); // } bindSession(status.session); // Connection connection = // DataSourceUtils.getConnection(dataSource); // status.session.reconnect(connection); } // It's ready to be used. Increment. status.calls++; return status; } // ~ SESSIONS // ========================================================================= private boolean isCloseSession(MethodInvocation invocation) { return close.getName().equals(invocation.getMethod().getName()); } private Session acquireAndBindSession() throws HibernateException { debug("Opening and binding session."); Session session = factory.openSession(); bindSession(session); return session; } private void bindSession(Session session) { debug("Binding session to thread."); SessionHolder sessionHolder = new SessionHolder(session); sessionHolder.setTransaction(sessionHolder.getSession() .beginTransaction()); // FIXME TODO // If we reach this point, it's ok to bind the new SessionHolder, // however the DUMMY EmptySessionHolder may be present so unbind // just in case. if (TransactionSynchronizationManager.hasResource(factory)) { TransactionSynchronizationManager.unbindResource(factory); } TransactionSynchronizationManager.bindResource(factory, sessionHolder); if (!TransactionSynchronizationManager.isSynchronizationActive()) { throw new InternalException("Synchronization not active for " + "TransactionSynchronizationManager"); } } private Session nullOrSessionBoundToThread() { SessionHolder holder = null; if (TransactionSynchronizationManager.hasResource(factory)) { holder = (SessionHolder) TransactionSynchronizationManager .getResource(factory); // A bit tricky. Works in coordinate with resetThreadSession // since the DUMMY would be replaced anyway. if (holder != null && holder.isEmpty()) { holder = null; } } return holder == null ? null : holder.getSession(); } private boolean isSessionBoundToThread() { return nullOrSessionBoundToThread() != null; } private void resetThreadSession() { if (isSessionBoundToThread()) { debug("Session bound to thread. Reseting."); TransactionSynchronizationManager.unbindResource(factory); TransactionSynchronizationManager.bindResource(factory, DUMMY); } else { debug("Session not bound to thread. No need to reset."); } } private void debug(String message) { if (log.isDebugEnabled()) { log.debug(message); } } } class EmptySessionHolder extends SessionHolder { public EmptySessionHolder() { super((Session) Proxy.newProxyInstance(Session.class.getClassLoader(), new Class[] { Session.class }, new InvocationHandler() { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String name = method.getName(); if (name.equals("toString")) { return "NULL SESSION PROXY"; } else if (name.equals("hashCode")) { return 0; } else if (name.equals("equals")) { return args[0] == null ? false : proxy == args[0]; } else { throw new RuntimeException("No methods allowed"); } } })); } @Override public boolean isEmpty() { return true; } }