/** * Copyright 2009 the original author or authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.extensions.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 Extensions'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 * @author Sergio Bossa * @author Salvatore Incandela */ public class TransactionAwareRepository implements InitializingBean, FactoryBean { private JcrSessionFactory sessionFactory; private Repository proxy; /** * 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 Sessions, 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. * @param target */ 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. * @return */ 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 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 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(); } } } }