/** * Created on Nov 11, 2005 * * $Id: TransactionAwareRepository.java,v 1.4 2008/01/29 12:28:24 coliny Exp $ * $Revision: 1.4 $ */ package org.springmodules.jcr; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import javax.jcr.Credentials; import javax.jcr.Repository; import javax.jcr.Session; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; /** * This FactoryBean exposes a proxy for a target JCR Repository, * returning the current thread-bound Repository (the Spring-managed * transactional Repository or the single OpenPersistenceManagerInView * Repository) on <code>login()</code>, if any. * * <p/>Essentially, <code>login()</code> calls get seamlessly * forwarded to <code>SessionFactoryUtils.getSession</code>. * Furthermore, <code>Session.logout</code> calls get forwarded to * <code>SessionFactoryUtils.releaseSession</code>. * <p/>As the Session returned depends on the workspace and credentials given, * this implementation accepts a JcrSessionFactory as parameter (basically a * wrapper for Repository, Credentials and Workspace properties). The proxy * will check the parameters and proxy the Repository for sessions that are * retrieved with the credentials and workspace name defined on the session factory. * Sessions retrieved with different workspace, credentials are not proxied. * * <p/>The main advantage of this proxy is that it allows DAOs to work with a * plain JCR Repository reference, while still participating in * Spring's (or a J2EE server's) resource and transaction management. DAOs will * only rely on the JCR API in such a scenario, without any Spring dependencies. * * DAOs could seamlessly switch between a JNDI * Repository and this proxy for a local Repository * receiving the reference through Dependency Injection. This will work without * any Spring API dependencies in the DAO code! * * <p/>It is usually preferable to write your JCR-based DAOs with Spring Modules's * JcrTemplate, offering benefits such as consistent data access exceptions * instead of RepositoryExceptions at the DAO layer. However, Spring's resource and * transaction management (and Dependency Injection) will work for DAOs * written against the plain JCR API too. * * <p/>Of course, you can still access the target Repository * even when your DAOs go through this proxy, by defining a bean reference * that points directly at your target Repository bean. * * @author Costin Leau * */ public class TransactionAwareRepository implements InitializingBean, FactoryBean { private JcrSessionFactory sessionFactory; private Repository proxy; private SessionHolderProviderManager sessionHolderProviderManager; /** * allow creation of sessions if none is found on the current thread. */ private boolean allowCreate = true; /** * allow creation of repository even if it doesn't support transactions. */ private boolean allowNonTxRepository = false; /** * @see org.springframework.beans.factory.FactoryBean#getObject() */ public Object getObject() throws Exception { return proxy; } /** * @see org.springframework.beans.factory.FactoryBean#getObjectType() */ public Class getObjectType() { return Repository.class; } /** * @see org.springframework.beans.factory.FactoryBean#isSingleton() */ public boolean isSingleton() { return true; } /** * @return Returns the allowCreate. */ public boolean isAllowCreate() { return allowCreate; } /** * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() */ public void afterPropertiesSet() throws Exception { if (sessionFactory == null) throw new IllegalArgumentException("sessionFactory is required"); // the rest of the properties are set by the setTargetFactory if (!allowNonTxRepository && !JcrUtils.supportsTransactions(sessionFactory.getRepository())) throw new IllegalArgumentException(sessionFactory.toString() + " does NOT support transactions and allowNonTxRepository is false"); } /** * Set whether the Repository proxy is allowed to create * a non-transactional Session when no transactional * Session can be found for the current thread. * <p>Default is "true". Can be turned off to enforce access to * transactional Sessionss, which safely allows for DAOs * written to get a Session without explicit closing * (i.e. a <code>Session.login()</code> * call without corresponding <code>Session.logout()</code> call). * * @see SessionFactoryUtils#getSession(SessionFactory, boolean) * @param allowCreate The allowCreate to set. */ public void setAllowCreate(boolean allowCreate) { this.allowCreate = allowCreate; } /** * @return Returns the allowNonTxRepository. */ public boolean isAllowNonTxRepository() { return allowNonTxRepository; } /** * Set whether the Repository proxy is accepted even if it does not * support transactions which allows the functionality of thread-bound * session but without the tx support. Such an option exists because * transaction support is optional for JSR-170 implementations. * * <p>Default is "true". Can be turned off to enforce only * transactional Sessionss, which safely allows for DAOs * written to get a Session without explicit closing * (i.e. a <code>Session.login()</code> * call without corresponding <code>Session.logout()</code> call). * * @param allowNonTxRepository The allowNonTxRepository to set. */ public void setAllowNonTxRepository(boolean allowNonTxRepository) { this.allowNonTxRepository = allowNonTxRepository; } /** * Set the target JCR Repository that this proxy should * delegate to wrapped in a JcrSessionFactory object along * with the credentials and workspace. * */ public void setTargetFactory(JcrSessionFactory target) { this.sessionFactory = target; this.proxy = (Repository) Proxy.newProxyInstance(Repository.class.getClassLoader(), new Class[] { Repository.class }, new TransactionAwareRepositoryInvocationHandler()); } /** * Return the target JCR Repository that this proxy delegates to. */ public Repository getTargetRepository() { return this.sessionFactory.getRepository(); } /** * Invocation handler that delegates login calls on the Repository proxy to SessionFactoryUtils * for being aware of thread-bound transactions. */ private class TransactionAwareRepositoryInvocationHandler implements InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // Invocation on Repository interface coming in... // check method invocation if (method.getName().equals("login")) { boolean matched = false; // check method signature Class[] paramTypes = method.getParameterTypes(); // a. login() if (paramTypes.length == 0) { // match the sessionFactory definitions matched = (sessionFactory.getWorkspaceName() == null && sessionFactory.getCredentials() == null); } else if (paramTypes.length == 1) { // b. login(java.lang.String workspaceName) if (paramTypes[0] == String.class) matched = ObjectUtils.nullSafeEquals(args[0], sessionFactory.getWorkspaceName()); // c. login(Credentials credentials) if (Credentials.class.isAssignableFrom(paramTypes[0])) matched = ObjectUtils.nullSafeEquals(args[0], sessionFactory.getCredentials()); } else if (paramTypes.length == 2) { // d. login(Credentials credentials, java.lang.String workspaceName) matched = ObjectUtils.nullSafeEquals(args[0], sessionFactory.getCredentials()) && ObjectUtils.nullSafeEquals(args[1], sessionFactory.getWorkspaceName()); } if (matched) { Session session = SessionFactoryUtils.getSession(sessionFactory, isAllowCreate()); Class[] ifcs = ClassUtils.getAllInterfaces(session); return (Session) Proxy.newProxyInstance(getClass().getClassLoader(), ifcs, new TransactionAwareInvocationHandler(session, sessionFactory)); } } else if (method.getName().equals("equals")) { // Only consider equal when proxies are identical. return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE); } else if (method.getName().equals("hashCode")) { // Use hashCode of Repository proxy. return new Integer(hashCode()); } Repository target = getTargetRepository(); // Invoke method on target Repository. try { return method.invoke(target, args); } catch (InvocationTargetException ex) { throw ex.getTargetException(); } } } /** * Invocation handler that delegates close calls on Sessions to * SessionFactoryUtils for being aware of thread-bound transactions. */ private static class TransactionAwareInvocationHandler implements InvocationHandler { private final Session target; private final SessionFactory sessionFactory; public TransactionAwareInvocationHandler(Session target, SessionFactory sessionFactory) { this.target = target; this.sessionFactory = sessionFactory; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // Invocation on Session interface coming in... if (method.getName().equals("equals")) { // Only consider equal when proxies are identical. return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE); } else if (method.getName().equals("hashCode")) { // Use hashCode of Session proxy. return new Integer(hashCode()); } else if (method.getName().equals("logout")) { // Handle close method: only close if not within a transaction. if (this.sessionFactory != null) { SessionFactoryUtils.releaseSession(this.target, this.sessionFactory); } return null; } // Invoke method on target Session. try { return method.invoke(this.target, args); } catch (InvocationTargetException ex) { throw ex.getTargetException(); } } } }