Transaction Handling in Tapestry5 with Jboss-5.1.0GA

edit: This works with JBoss-7.0.2, too

In a session bean every method has set the transaction attribute REQUIRED by default, i.e., calling it without an active transaction results in the creation of a new transaction. In a tapestry application you may inject session beans and call methods on them. Unfortunately, every call will open up a new transaction being ‘commited’ as the method returns. This has the disadvantage, that the first level cache of the transaction will be cleared after every call, for example. In this post, you will learn how to set up a transaction before a page or component render request and commit it right after the request is completely processed.

A session bean can be injected into a page via @Inject private DummyController dummyController, by introducing them to tapestry in the AppModule like this:

    /**
     * Looks up for the {@link DummyController} via JNDI. If it couldn't be
     * found this method returns {@code null}.
     *
     * @return the {@link DummyController} or {@code null} if not found.
     */
    @Scope(ScopeConstants.PERTHREAD)
    public DummyController buildSystemController(Request request, RequestGlobals globals) throws LoginException {
    	InitialContext ctx = null;
    	try {
    		final Properties env = new Properties();
    		env.setProperty("java.naming.factory.initial","org.jboss.naming.NamingContextFactory");
    		env.setProperty("java.naming.factory.url.pkgs","org.jboss.naming:org.jnp.interfaces");
    		// create a new initial context in order to have access to the JNDI of the jboss.
    		ctx = new InitialContext(env);
    		Object controller;
    		try {
    			// Whether my tapestry client is embedded in the jboss or started externally
			if (Boolean.parseBoolean(System.getProperty("myapp.external")) ) {
    				// lookup the controller.
    				controller = ctx.lookup(String.format(CONTROLLER_JNDI_NAME_REMOTE, "DummyBean"));
    			}
			else {
    				// lookup the controller.
    				controller = ctx.lookup(String.format(CONTROLLER_JNDI_NAME_LOCAL, "DummyBean"));
    			}
    			return controller;
    		}
		finally {
    			ctx.close();
    		}
    	}
	catch (NamingException e) {
    		log.warn(type + "Controller couldn't be built " + e.toString());
    	}
    	return null;
    }

The transaction handling is introduced by a PageRenderRequestFilter and a ComponentRenderRequestFilter being registered in the AppModule:

    public void contributePageRenderRequestHandler(OrderedConfiguration<PageRenderRequestFilter> configuration,
            @InjectService("PageTransactionFilter")
            PageRenderRequestFilter pageTransactionFilter) {
        // The filter is registered if the tapestry application is embedded in the jboss, only.
        if(!Boolean.parseBoolean(System.getProperty("myapp.external"))) {
        	// register the transaction filter.
        	configuration.add("pageTransactionFilter", pageTransactionFilter);
        }
    }

    public void contributeComponentEventRequestHandler(OrderedConfiguration<ComponentEventRequestFilter> configuration,
    		@InjectService("ComponentTransactionFilter")
            ComponentEventRequestFilter componentTransactionFilter) {
    	// The filter is registered if the tapestry application is embedded in the jboss, only.
    	if(!Boolean.parseBoolean(System.getProperty("myapp.external"))) {
    		// register the transaction filter.
    		configuration.add("componentTransactionFilter", componentTransactionFilter);
    	}
    }

The filters are implemented as follows:

    public PageRenderRequestFilter buildPageTransactionFilter(final Logger log) {
    	return new PageRenderRequestFilter() {
   		@Override
    		public void handle(PageRenderRequestParameters parameters,
    				PageRenderRequestHandler handler) throws IOException {
    			// begin transaction, process page render request and commit the transaction.
    			// rollback the transaction, if an exception is thrown.
    			try {
    				UserTransaction tx = ControllerUtil.beginTransaction();
    				try {
    					handler.handle(parameters);
    					ControllerUtil.commitTransaction(tx);
    				}
    				catch(RuntimeException e) {
    					ControllerUtil.rollbackTransaction(tx);
    					throw e;
    				}
    			}
    			catch(Exception e) {
    				throw new IllegalStateException(e);
    			}
    		}
		};
    }

    public ComponentEventRequestFilter buildComponentTransactionFilter(final Logger log) {
    	return new ComponentEventRequestFilter() {
		@Override
		public void handle(ComponentEventRequestParameters parameters,
				ComponentEventRequestHandler handler) throws IOException {
    			try {
    				// begin transaction, process page render request and commit the transaction.
    				// rollback the transaction, if an exception is thrown.
    				UserTransaction tx = ControllerUtil.beginTransaction();
    				try {
    					handler.handle(parameters);
    					ControllerUtil.commitTransaction(tx);
    				}
    				catch(RuntimeException e) {
    					ControllerUtil.rollbackTransaction(tx);
    					throw e;
    				}
    			}
    			catch(Exception e) {
    				throw new IllegalStateException(e);
    			}
		}
	};
    }

The util class ControllerUtil looks like this:

	public static UserTransaction beginTransaction() throws
			NotSupportedException, SystemException, NamingException  {
		UserTransaction tx = (UserTransaction) new InitialContext().lookup("java:comp/UserTransaction");
		tx.begin();
		return tx;
	}

	public static void commitTransaction(UserTransaction tx) throws
			IllegalStateException, RollbackException, HeuristicMixedException,
			HeuristicRollbackException, SystemException {
        if(tx != null) {
			tx.commit();
		}
	}

	public static void rollbackTransaction(UserTransaction tx) throws
			IllegalStateException, SystemException {
        if(tx != null) {
			tx.rollback();
		}
	}

See “Transaction Handling for Ajax Components in Tapestry-5.x” for adding Ajax support.

Leave a Reply

Your email address will not be published. Required fields are marked *