Author: bleny Date: 2013-03-14 16:26:30 +0100 (Thu, 14 Mar 2013) New Revision: 249 Url: http://nuiton.org/projects/nuiton-web/repository/revisions/249 Log: fixes #2593: Introduce JpaTransactionFilter Added: trunk/nuiton-web/src/main/java/org/nuiton/web/filter/JpaTransactionFilter.java Modified: trunk/nuiton-web/pom.xml trunk/pom.xml Modified: trunk/nuiton-web/pom.xml =================================================================== --- trunk/nuiton-web/pom.xml 2013-03-14 15:06:26 UTC (rev 248) +++ trunk/nuiton-web/pom.xml 2013-03-14 15:26:30 UTC (rev 249) @@ -45,6 +45,11 @@ </dependency> <dependency> + <groupId>org.hibernate.javax.persistence</groupId> + <artifactId>hibernate-jpa-2.0-api</artifactId> + </dependency> + + <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> Added: trunk/nuiton-web/src/main/java/org/nuiton/web/filter/JpaTransactionFilter.java =================================================================== --- trunk/nuiton-web/src/main/java/org/nuiton/web/filter/JpaTransactionFilter.java (rev 0) +++ trunk/nuiton-web/src/main/java/org/nuiton/web/filter/JpaTransactionFilter.java 2013-03-14 15:26:30 UTC (rev 249) @@ -0,0 +1,383 @@ +/* + * #%L + * Nuiton Web :: Nuiton Web + * + * $Id: TopiaTransactionFilter.java 187 2012-02-20 11:49:09Z tchemit $ + * $HeadURL: http://svn.nuiton.org/svn/nuiton-web/tags/nuiton-web-1.12-beta-2/nuiton-web/... $ + * %% + * 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 javax.persistence.EntityManager; +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 entityManager from + * {@link EntityManager} and deal with the complete lifecycle of a JPA + * entityManager while a request. + * <p/> + * The injected JPA entityManager 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 entityManager. + * When a such method is called on the entityManager 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 entityManager was not still open + * (via a {@link EntityManager#}. + * When a such method is called on the entityManager 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 entityManager</h2> + * The (proxied) entityManager is pushed as an attribute in the servlet request. + * <p/> + * The attribute name is defined by field {@link #requestAttributeName} + * (default value is {@link #JPA_TRANSACTION_REQUEST_ATTRIBUTE}) and can be + * changed. + * <p/> + * A convience method is created here to obtain the entityManager {@link #getTransaction(ServletRequest)} : + * <pre> + * EntityManager em = JpaTransactionFilter.getTransaction(ServletRequest); + * </pre> + * <p/> + * If you prefer to not use this nice method, you can also do this: + * <pre> + * EntityManager em = (EntityManager) request.getAttribute(JpaTransactionFilter#JPA_TRANSACTION_REQUEST_ATTRIBUTE); + * </pre> + * <p/> + * Or + * <pre> + * EntityManager em = (EntityManager) request.getAttribute(modifiedAttributeName); + * </pre> + * + * @author tchemit, bleny + * @since 1.13 + */ +public abstract class JpaTransactionFilter implements Filter { + + public static final String JPA_TRANSACTION_REQUEST_ATTRIBUTE = + "jpaTransaction"; + + public static final String[] DEFAULT_EXCLUDE_METHODS = { + "close" + }; + + public static final String[] DEFAULT_UNUSED_METHODS = { + "toString", + "isOpen", + "clear", + "equals", + "hashCode", + "finalize", + "getClass" + }; + + /** Logger. */ + private static final Log log = + LogFactory.getLog(JpaTransactionFilter.class); + + /** names of methods to forbid access while using proxy. */ + protected Set<String> excludeMethods; + + /** names of methods to by-pass if no entityManager opened on proxy. */ + protected Set<String> unusedMethods; + + /** + * Name of the request attribute where to push the entityManager. + * <p/> + * By default will use value of + * {@link #JPA_TRANSACTION_REQUEST_ATTRIBUTE}. + */ + protected String requestAttributeName = JPA_TRANSACTION_REQUEST_ATTRIBUTE; + + 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 entityManager. + */ + public void setRequestAttributeName(String requestAttributeName) { + this.requestAttributeName = requestAttributeName; + } + + public static EntityManager getTransaction(ServletRequest request) { + EntityManager entityManager = (EntityManager) + request.getAttribute(JPA_TRANSACTION_REQUEST_ATTRIBUTE); + return entityManager; + } + + /** + * Method to open a new entityManager. + * + * @param request incoming request + * @return the new freshly opened entityManager + */ + protected abstract EntityManager createEntityManager(ServletRequest request); + + @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 entityManager + + JpaTransactionProxyInvocationHandler proxyInvocationHandler = + new JpaTransactionProxyInvocationHandler(request); + + EntityManager proxy = (EntityManager) Proxy.newProxyInstance( + getClass().getClassLoader(), + new Class<?>[]{EntityManager.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 entityManager + onCloseTransaction(proxyInvocationHandler.entityManager); + } + } + + /** + * Hook method called when a method with his name in + * {@link #excludeMethods} was invoked on the proxied entityManager. + * + * @param proxy proxied entityManager + * @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 entityManager of the request at the end of + * the request when all filter has been consumed. + * + * @param entityManager the entityManager to close (can be null if entityManager + * was not required while the current request) + */ + protected void onCloseTransaction(EntityManager entityManager) { + if (entityManager == null) { + if (log.isTraceEnabled()) { + log.trace("no entityManager to close"); + } + } else if ( ! entityManager.isOpen()) { + if (log.isTraceEnabled()) { + log.trace("entityManager " + entityManager + " is already closed"); + } + } else { + if (log.isDebugEnabled()) { + log.debug("closing entityManager " + entityManager); + } + entityManager.close(); + } + } + + /** + * Hook method called when a method with his name in + * {@link #unusedMethods} was invoked on the proxied entityManager + * while the underlying entityManager 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 entityManager found + + String methodName = method.getName(); + if (log.isDebugEnabled()) { + log.debug("Skip execution of method " + methodName + + " since no entityManager is created"); + } + + Set<String> methods = getUnusedMethods(); + if (methods.contains("toString")) { + + return "No entityManager opened yet for this proxy"; + } + + if (methods.contains("isOpen")) { + + return true; + } + + if (methods.contains("equals")) { + + return false; + } + + if (methods.contains("hashCode")) { + + return 0; + } + + if (methods.contains("getClass")) { + + return EntityManager.class; + } + + return null; + } + + /** + * Handler of a proxy on a {@link EntityManager}. + * + * @see #excludeMethods + */ + public class JpaTransactionProxyInvocationHandler implements InvocationHandler { + + /** Incoming request that creates this handler. */ + protected final ServletRequest request; + + /** Target to use for the proxy. */ + protected EntityManager entityManager; + + protected JpaTransactionProxyInvocationHandler(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 (entityManager == null) { + + if (log.isTraceEnabled()) { + log.trace("entityManager started due to a call to " + methodName); + } + + if (getUnusedMethods().contains(methodName)) { + + Object result = onUnusedMethod(proxy, method, args); + return result; + } + + // first time entityManager is required, create its + entityManager = createEntityManager(request); + + entityManager.getTransaction().begin(); + + if (log.isDebugEnabled()) { + log.debug("created entityManager " + entityManager); + } + } + + // can invoke the method on the entityManager + try { + Object result = method.invoke(entityManager, args); + return result; + } catch (Exception eee) { + if (log.isErrorEnabled()) { + log.error("Could not execute method " + method.getName(), eee); + } + throw eee; + } + } + } + +} Modified: trunk/pom.xml =================================================================== --- trunk/pom.xml 2013-03-14 15:06:26 UTC (rev 248) +++ trunk/pom.xml 2013-03-14 15:26:30 UTC (rev 249) @@ -146,6 +146,13 @@ <scope>provided</scope> </dependency> + <dependency> + <groupId>org.hibernate.javax.persistence</groupId> + <artifactId>hibernate-jpa-2.0-api</artifactId> + <version>1.0.1.Final</version> + <scope>provided</scope> + </dependency> + <!-- GWT dependencies (from central repo) --> <dependency> <groupId>com.google.gwt</groupId>
participants (1)
-
bleny@users.nuiton.org