Author: tchemit Date: 2013-12-20 01:00:38 +0100 (Fri, 20 Dec 2013) New Revision: 291 Url: http://nuiton.org/projects/nuiton-web/repository/revisions/291 Log: fixes #2975: Add a new filter to deal with typed persistence context Added: trunk/nuiton-web/src/main/java/org/nuiton/web/filter/TypedTopiaTransactionFilter.java Modified: trunk/nuiton-web/src/main/java/org/nuiton/web/filter/TopiaTransactionFilter.java Modified: trunk/nuiton-web/src/main/java/org/nuiton/web/filter/TopiaTransactionFilter.java =================================================================== --- trunk/nuiton-web/src/main/java/org/nuiton/web/filter/TopiaTransactionFilter.java 2013-12-12 10:41:25 UTC (rev 290) +++ trunk/nuiton-web/src/main/java/org/nuiton/web/filter/TopiaTransactionFilter.java 2013-12-20 00:00:38 UTC (rev 291) @@ -24,381 +24,19 @@ */ package org.nuiton.web.filter; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.hibernate.Transaction; -import org.nuiton.topia.AbstractTopiaPersistenceContext; -import org.nuiton.topia.TopiaException; import org.nuiton.topia.TopiaPersistenceContext; -import org.nuiton.topia.TopiaTransaction; -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import java.io.IOException; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - /** - * <h2>Purpose of this filter</h2> - * This filter purpose is to inject in the request a transaction from - * {@link org.nuiton.topia.TopiaPersistenceContext} and deal with the complete lifecycle of a topia - * transaction while a request. - * <p/> - * The injected transaction will be closed (if was really opened) at the end of - * the request. - * <h2>Configuration of the filter</h2> - * The filter accepts two configuration parameters: - * <ul> - * <li>{@code excludeMethods}: This parameters configure a set of method names - * which should never be called on the proxied transaction. - * When a such method is called on the transaction then the filter will pass in - * the hook {@link #onExcludeMethod(Object, Method, Object[])}. - * <p/> - * Default implementation of this hook is to throw an exception. - * </li> - * <li>{@code unusedMethods}: This parameters configure a set of method names - * which should be by-pass when the proxied transaction was not still open (via a - * {@link org.nuiton.topia.AbstractTopiaApplicationContext#newPersistenceContext()} ()}. - * When a such method is called on the transaction then the filter will pass in - * the hook {@link #onUnusedMethod(Object, Method, Object[])}. - * <p/> - * Default implementation of this hook is to not return null values. - * </li> - * </ul> - * <h2>Obtain the transaction</h2> - * The (proxied) transaction is pushed as an attribute in the servlet request. - * <p/> - * The attribute name is defined by field {@link #requestAttributeName} - * (default value is {@link #TOPIA_TRANSACTION_REQUEST_ATTRIBUTE}) and can be - * changed. - * <p/> - * A convience method is created here to obtain the transaction {@link #getTransaction(ServletRequest)} : - * <pre> - * TopiaContext tx = TopiaTransactionFilter.getTransaction(ServletRequest); - * </pre> - * <p/> - * If you prefer to not use this nice method, you can also do this: - * <pre> - * TopiaContext tx = (TopiaContext) request.getAttribute(TopiaTransactionFilter#TOPIA_TRANSACTION_REQUEST_ATTRIBUTE); - * </pre> - * <p/> - * Or - * <pre> - * TopiaContext tx = (TopiaContext) request.getAttribute(modifiedAttributeName); - * </pre> + * Implementation of the {@link TypedTopiaTransactionFilter} using the + * {@link TopiaPersistenceContext} as type. * * @author tchemit <chemit@codelutin.com> * @since 1.6 */ -public abstract class TopiaTransactionFilter implements Filter { +public abstract class TopiaTransactionFilter extends TypedTopiaTransactionFilter<TopiaPersistenceContext> { - public static final String TOPIA_TRANSACTION_REQUEST_ATTRIBUTE = - "topiaTransaction"; - - public static final String[] DEFAULT_EXCLUDE_METHODS = { - "beginTransaction", - "closeContext", - "clear" - }; - - public static final String[] DEFAULT_UNUSED_METHODS = { - "toString", - "isClosed", - "closeContext", - "clear", - "equals", - "hashCode", - "finalize", - "getClass" - }; - - /** Logger. */ - private static final Log log = - LogFactory.getLog(TopiaTransactionFilter.class); - - /** names of methods to forbid access while using proxy. */ - protected Set<String> excludeMethods; - - /** names of methods to by-pass if no transaction opened on proxy. */ - protected Set<String> unusedMethods; - - /** - * Name of the request attribute where to push the transaction. - * <p/> - * By default will use value of - * {@link #TOPIA_TRANSACTION_REQUEST_ATTRIBUTE}. - * - * @since 1.10 - */ - protected String requestAttributeName = TOPIA_TRANSACTION_REQUEST_ATTRIBUTE; - - public Set<String> getExcludeMethods() { - return excludeMethods; + public TopiaTransactionFilter() { + super(TopiaPersistenceContext.class); } - public Set<String> getUnusedMethods() { - return unusedMethods; - } - - /** - * to change the {@link #requestAttributeName}. - * - * @param requestAttributeName new name of the request attribute - * where to push the transaction. - * @since 1.10 - */ - public void setRequestAttributeName(String requestAttributeName) { - this.requestAttributeName = requestAttributeName; - } - - public static TopiaPersistenceContext getTransaction(ServletRequest request) { - TopiaPersistenceContext topiaContext = (TopiaPersistenceContext) - request.getAttribute(TOPIA_TRANSACTION_REQUEST_ATTRIBUTE); - return topiaContext; - } - - /** - * Method to open a new transaction. - * - * @param request incoming request - * @return the new freshly opened transaction - * @throws TopiaException if any problem while opening a new transaction - */ - protected abstract TopiaPersistenceContext beginTransaction(ServletRequest request) throws TopiaException; - - @Override - public void destroy() { - } - - @Override - public void init(FilterConfig filterConfig) throws ServletException { - String methodsFromConfig; - - methodsFromConfig = filterConfig.getInitParameter("excludeMethods"); - String[] methods; - if (StringUtils.isNotEmpty(methodsFromConfig)) { - methods = methodsFromConfig.split(","); - } else { - methods = DEFAULT_EXCLUDE_METHODS; - } - excludeMethods = new HashSet<String>(Arrays.asList(methods)); - - methodsFromConfig = filterConfig.getInitParameter("unusedMethods"); - if (StringUtils.isNotEmpty(methodsFromConfig)) { - methods = methodsFromConfig.split(","); - } else { - methods = DEFAULT_UNUSED_METHODS; - } - unusedMethods = new HashSet<String>(Arrays.asList(methods)); - } - - @Override - public void doFilter(ServletRequest request, - ServletResponse response, - FilterChain chain) throws IOException, ServletException { - - // creates a proxy of a lazy transaction - - TopiaTransactionProxyInvocationHandler proxyInvocationHandler = - new TopiaTransactionProxyInvocationHandler(request); - - TopiaPersistenceContext proxy = (TopiaPersistenceContext) Proxy.newProxyInstance( - getClass().getClassLoader(), - new Class<?>[]{TopiaPersistenceContext.class, - TopiaTransaction.class}, - proxyInvocationHandler - ); - - // push it in request as an attribute - request.setAttribute(requestAttributeName, proxy); - try { - - // chain to next filter - chain.doFilter(request, response); - } finally { - - // close the real transaction - onCloseTransaction(proxyInvocationHandler.transaction); - } - } - - /** - * Hook method called when a method with his name in - * {@link #excludeMethods} was invoked on the proxied transaction. - * - * @param proxy proxied transaction - * @param method method invoked - * @param args arguments of the invoked method - * @return the return code of the method - * @throws Throwable if any error to do. - */ - protected Object onExcludeMethod(Object proxy, - Method method, - Object[] args) throws Throwable { - - // not authorized - throw new IllegalAccessException( - "Not allowed to access method " + method.getName() + " on " + - proxy); - } - - /** - * Hook method to close the topia transaction of the request at the end of - * the request when all filter has been consumed. - * - * @param transaction the transaction to close (can be null if transaction - * was not required while the current request) - * @since 1.9.1 - */ - protected void onCloseTransaction(TopiaPersistenceContext transaction) { - if (transaction == null) { - if (log.isTraceEnabled()) { - log.trace("no transaction to close"); - } - } else if (transaction.isClosed()) { - if (log.isTraceEnabled()) { - log.trace("transaction " + transaction + " is already closed"); - } - } else { - if (log.isDebugEnabled()) { - log.debug("closing transaction " + transaction); - } - // let's rollback transaction if the transaction was not rollbacked nor committed - // as the topia context close context does not affect hibernate transaction - // so if something bad happen then we will always have a - Transaction tx = ((AbstractTopiaPersistenceContext) transaction).getHibernateSupport().getHibernateSession().getTransaction(); - if (!tx.wasCommitted() && !tx.wasRolledBack()) { - if (log.isDebugEnabled()) { - log.debug("rollback transaction!"); - } - tx.rollback(); - } - transaction.closeContext(); - } - } - - /** - * Hook method called when a method with his name in - * {@link #unusedMethods} was invoked on the proxied transaction - * while the underlying transaction is still not opened. - * - * @param proxy the proxy itself - * @param method method invoked - * @param args arguments of the invoked method - * @return the return code of the method - * @throws Throwable if any error to do. - */ - protected Object onUnusedMethod(Object proxy, - Method method, - Object[] args) throws Throwable { - - // by-pass method since no transaction found - - String methodName = method.getName(); - if (log.isDebugEnabled()) { - log.debug("Skip execution of method " + methodName + - " since no transaction is instanciated."); - } - - Set<String> methods = getUnusedMethods(); - if (methods.contains("toString")) { - - return "No transaction opened yet for this proxy"; - } - - if (methods.contains("isClosed")) { - - return false; - } - - if (methods.contains("equals")) { - - return false; - } - - if (methods.contains("hashCode")) { - - return 0; - } - - if (methods.contains("getClass")) { - - return TopiaPersistenceContext.class; - } - - return null; - } - - /** - * Handler of a proxy on a {@link TopiaPersistenceContext}. - * - * @see #excludeMethods - */ - public class TopiaTransactionProxyInvocationHandler implements InvocationHandler { - - /** Incoming request that creates this handler. */ - protected final ServletRequest request; - - /** Target to use for the proxy. */ - protected TopiaPersistenceContext transaction; - - protected TopiaTransactionProxyInvocationHandler(ServletRequest request) { - this.request = request; - } - - @Override - public Object invoke(Object proxy, - Method method, - Object[] args) throws Throwable { - - String methodName = method.getName(); - - if (getExcludeMethods().contains(methodName)) { - - Object result = onExcludeMethod(proxy, method, args); - return result; - } - - if (transaction == null) { - - if (log.isTraceEnabled()) { - log.trace("transaction started due to a call to " + methodName); - } - - if (getUnusedMethods().contains(methodName)) { - - Object result = onUnusedMethod(proxy, method, args); - return result; - } - - // first time transaction is required, create its - transaction = beginTransaction(request); - - if (log.isDebugEnabled()) { - log.debug("Open transaction " + transaction); - } - } - - // can invoke the method on the transaction - try { - Object result = method.invoke(transaction, args); - return result; - } catch (Exception eee) { - if (log.isErrorEnabled()) { - log.error("Could not execute method " + method.getName(), eee); - } - throw eee; - } - } - } - } Copied: trunk/nuiton-web/src/main/java/org/nuiton/web/filter/TypedTopiaTransactionFilter.java (from rev 290, trunk/nuiton-web/src/main/java/org/nuiton/web/filter/TopiaTransactionFilter.java) =================================================================== --- trunk/nuiton-web/src/main/java/org/nuiton/web/filter/TypedTopiaTransactionFilter.java (rev 0) +++ trunk/nuiton-web/src/main/java/org/nuiton/web/filter/TypedTopiaTransactionFilter.java 2013-12-20 00:00:38 UTC (rev 291) @@ -0,0 +1,425 @@ +/* + * #%L + * Nuiton Web :: Nuiton Web + * + * $Id$ + * $HeadURL$ + * %% + * Copyright (C) 2011 CodeLutin + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/lgpl-3.0.html>. + * #L% + */ +package org.nuiton.web.filter; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.hibernate.Transaction; +import org.nuiton.topia.AbstractTopiaPersistenceContext; +import org.nuiton.topia.TopiaException; +import org.nuiton.topia.TopiaPersistenceContext; +import org.nuiton.topia.TopiaTransaction; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import java.io.IOException; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * <h2>Purpose of this filter</h2> + * This filter purpose is to inject in the request a transaction from + * {@link TopiaPersistenceContext} and deal with the complete lifecycle of a topia + * transaction while a request. + * <p/> + * The injected transaction will be closed (if was really opened) at the end of + * the request. + * <h2>Configuration of the filter</h2> + * The filter accepts two configuration parameters: + * <ul> + * <li>{@code excludeMethods}: This parameters configure a set of method names + * which should never be called on the proxied transaction. + * When a such method is called on the transaction then the filter will pass in + * the hook {@link #onExcludeMethod(Object, Method, Object[])}. + * <p/> + * Default implementation of this hook is to throw an exception. + * </li> + * <li>{@code unusedMethods}: This parameters configure a set of method names + * which should be by-pass when the proxied transaction was not still open (via a + * {@link org.nuiton.topia.AbstractTopiaApplicationContext#newPersistenceContext()} ()}. + * When a such method is called on the transaction then the filter will pass in + * the hook {@link #onUnusedMethod(Object, Method, Object[])}. + * <p/> + * Default implementation of this hook is to not return null values. + * </li> + * </ul> + * <h2>Obtain the transaction</h2> + * The (proxied) transaction is pushed as an attribute in the servlet request. + * <p/> + * The attribute name is defined by field {@link #requestAttributeName} + * (default value is {@link #TOPIA_TRANSACTION_REQUEST_ATTRIBUTE}) and can be + * changed. + * <p/> + * A convience method is created here to obtain the transaction {@link #getPersistenceContext(ServletRequest)} : + * <pre> + * PersistenceContext tx = TypedTopiaTransactionFilter.getPersistenceContext(ServletRequest); + * </pre> + * <p/> + * If you prefer to not use this nice method, you can also do this: + * <pre> + * PersistenceContext tx = (PersistenceContext) request.getAttribute(TopiaTransactionFilter#TOPIA_TRANSACTION_REQUEST_ATTRIBUTE); + * </pre> + * <p/> + * Or + * <pre> + * PersistenceContext tx = (PersistenceContext) request.getAttribute(modifiedAttributeName); + * </pre> + * + * @author tchemit <chemit@codelutin.com> + * @since 1.15 + */ +public abstract class TypedTopiaTransactionFilter<PersistenceContext extends TopiaPersistenceContext> implements Filter { + + public static final String TOPIA_TRANSACTION_REQUEST_ATTRIBUTE = + "topiaTransaction"; + + public static final String[] DEFAULT_EXCLUDE_METHODS = { + "beginTransaction", + "closeContext", + "clear" + }; + + public static final String[] DEFAULT_UNUSED_METHODS = { + "toString", + "isClosed", + "closeContext", + "clear", + "equals", + "hashCode", + "finalize", + "getClass" + }; + + /** Logger. */ + private static final Log log = + LogFactory.getLog(TypedTopiaTransactionFilter.class); + + public static <PersistenceContext extends TopiaPersistenceContext> PersistenceContext getPersistenceContext(ServletRequest request) { + PersistenceContext topiaContext = (PersistenceContext) + request.getAttribute(TOPIA_TRANSACTION_REQUEST_ATTRIBUTE); + return topiaContext; + } + + /** + * @deprecated since 1.16, use now {@link #getPersistenceContext(ServletRequest)} + */ + @Deprecated + public static <PersistenceContext extends TopiaPersistenceContext> PersistenceContext getTransaction(ServletRequest request) { + PersistenceContext topiaContext = getPersistenceContext(request); + return topiaContext; + } + + /** names of methods to forbid access while using proxy. */ + protected Set<String> excludeMethods; + + /** names of methods to by-pass if no transaction opened on proxy. */ + protected Set<String> unusedMethods; + + protected final Class<PersistenceContext> persistenceContextType; + + /** + * Name of the request attribute where to push the transaction. + * <p/> + * By default will use value of + * {@link #TOPIA_TRANSACTION_REQUEST_ATTRIBUTE}. + * + * @since 1.10 + */ + protected String requestAttributeName = TOPIA_TRANSACTION_REQUEST_ATTRIBUTE; + + protected TypedTopiaTransactionFilter(Class<PersistenceContext> persistenceContextType) { + if (persistenceContextType==null) { + throw new NullPointerException("persistenceContextType can't be null"); + } + if (!persistenceContextType.isInterface()) { + throw new IllegalArgumentException("persistenceContextType must be an interface"); + } + this.persistenceContextType = persistenceContextType; + } + + public Set<String> getExcludeMethods() { + return excludeMethods; + } + + public Set<String> getUnusedMethods() { + return unusedMethods; + } + + /** + * to change the {@link #requestAttributeName}. + * + * @param requestAttributeName new name of the request attribute + * where to push the transaction. + * @since 1.10 + */ + public void setRequestAttributeName(String requestAttributeName) { + this.requestAttributeName = requestAttributeName; + } + + /** + * Method to open a new transaction. + * + * @param request incoming request + * @return the new freshly opened transaction + * @throws TopiaException if any problem while opening a new transaction + */ + protected abstract PersistenceContext beginTransaction(ServletRequest request) throws TopiaException; + + @Override + public void destroy() { + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + String methodsFromConfig; + + methodsFromConfig = filterConfig.getInitParameter("excludeMethods"); + String[] methods; + if (StringUtils.isNotEmpty(methodsFromConfig)) { + methods = methodsFromConfig.split(","); + } else { + methods = DEFAULT_EXCLUDE_METHODS; + } + excludeMethods = new HashSet<String>(Arrays.asList(methods)); + + methodsFromConfig = filterConfig.getInitParameter("unusedMethods"); + if (StringUtils.isNotEmpty(methodsFromConfig)) { + methods = methodsFromConfig.split(","); + } else { + methods = DEFAULT_UNUSED_METHODS; + } + unusedMethods = new HashSet<String>(Arrays.asList(methods)); + } + + @Override + public void doFilter(ServletRequest request, + ServletResponse response, + FilterChain chain) throws IOException, ServletException { + + // creates a proxy of a lazy transaction + + TopiaTransactionProxyInvocationHandler proxyInvocationHandler = + new TopiaTransactionProxyInvocationHandler(request); + + PersistenceContext proxy = (PersistenceContext) Proxy.newProxyInstance( + getClass().getClassLoader(), + new Class<?>[]{persistenceContextType, + TopiaTransaction.class}, + proxyInvocationHandler + ); + + // push it in request as an attribute + request.setAttribute(requestAttributeName, proxy); + try { + + // chain to next filter + chain.doFilter(request, response); + } finally { + + // close the real transaction + onCloseTransaction(proxyInvocationHandler.transaction); + } + } + + /** + * Hook method called when a method with his name in + * {@link #excludeMethods} was invoked on the proxied transaction. + * + * @param proxy proxied transaction + * @param method method invoked + * @param args arguments of the invoked method + * @return the return code of the method + * @throws Throwable if any error to do. + */ + protected Object onExcludeMethod(Object proxy, + Method method, + Object[] args) throws Throwable { + + // not authorized + throw new IllegalAccessException( + "Not allowed to access method " + method.getName() + " on " + + proxy); + } + + /** + * Hook method to close the topia transaction of the request at the end of + * the request when all filter has been consumed. + * + * @param transaction the transaction to close (can be null if transaction + * was not required while the current request) + * @since 1.9.1 + */ + protected void onCloseTransaction(PersistenceContext transaction) { + if (transaction == null) { + if (log.isTraceEnabled()) { + log.trace("no transaction to close"); + } + } else if (transaction.isClosed()) { + if (log.isTraceEnabled()) { + log.trace("transaction " + transaction + " is already closed"); + } + } else { + if (log.isDebugEnabled()) { + log.debug("closing transaction " + transaction); + } + // let's rollback transaction if the transaction was not rollbacked nor committed + // as the topia context close context does not affect hibernate transaction + // so if something bad happen then we will always have a + Transaction tx = ((AbstractTopiaPersistenceContext) transaction).getHibernateSupport().getHibernateSession().getTransaction(); + if (!tx.wasCommitted() && !tx.wasRolledBack()) { + if (log.isDebugEnabled()) { + log.debug("rollback transaction!"); + } + tx.rollback(); + } + transaction.closeContext(); + } + } + + /** + * Hook method called when a method with his name in + * {@link #unusedMethods} was invoked on the proxied transaction + * while the underlying transaction is still not opened. + * + * @param proxy the proxy itself + * @param method method invoked + * @param args arguments of the invoked method + * @return the return code of the method + * @throws Throwable if any error to do. + */ + protected Object onUnusedMethod(Object proxy, + Method method, + Object[] args) throws Throwable { + + // by-pass method since no transaction found + + String methodName = method.getName(); + if (log.isDebugEnabled()) { + log.debug("Skip execution of method " + methodName + + " since no transaction is instanciated."); + } + + Set<String> methods = getUnusedMethods(); + if (methods.contains("toString")) { + + return "No transaction opened yet for this proxy"; + } + + if (methods.contains("isClosed")) { + + return false; + } + + if (methods.contains("equals")) { + + return false; + } + + if (methods.contains("hashCode")) { + + return 0; + } + + if (methods.contains("getClass")) { + + return persistenceContextType; + } + + return null; + } + + /** + * Handler of a proxy on a {@link TopiaPersistenceContext}. + * + * @see #excludeMethods + */ + public class TopiaTransactionProxyInvocationHandler implements InvocationHandler { + + /** Incoming request that creates this handler. */ + protected final ServletRequest request; + + /** Target to use for the proxy. */ + protected PersistenceContext transaction; + + protected TopiaTransactionProxyInvocationHandler(ServletRequest request) { + this.request = request; + } + + @Override + public Object invoke(Object proxy, + Method method, + Object[] args) throws Throwable { + + String methodName = method.getName(); + + if (getExcludeMethods().contains(methodName)) { + + Object result = onExcludeMethod(proxy, method, args); + return result; + } + + if (transaction == null) { + + if (log.isTraceEnabled()) { + log.trace("transaction started due to a call to " + methodName); + } + + if (getUnusedMethods().contains(methodName)) { + + Object result = onUnusedMethod(proxy, method, args); + return result; + } + + // first time transaction is required, create its + transaction = beginTransaction(request); + + if (log.isDebugEnabled()) { + log.debug("Open transaction " + transaction); + } + } + + // can invoke the method on the transaction + try { + Object result = method.invoke(transaction, args); + return result; + } catch (Exception eee) { + if (log.isErrorEnabled()) { + log.error("Could not execute method " + method.getName(), eee); + } + throw eee; + } + } + } + +}