Tapestry and O/R persitence services

Eric Everman <[email protected]> Fri, 10 Jan 2003 10:09:49 -0600
Newsgroups gmane.comp.java.tapestry.devel
Message-ID <[email protected]>
Norrman-

I wrote a Tapestry extension for Hibernate sessions, but I have not tried 
it yet in 2.3.  It creates Hibernate sessions on demand with a scope of a 
single page for a single request.  Sessions are automatically committed 
when the page renders or when you switch pages (cycle.setPage()).

My source code is attached - let me know if you have any problems using it 
in 2.3.

Eric Everman
SessionSourceExt.java (text/plain, 10 KB)
package com.preceda.hibertap;

/**
 * @author Eric Everman of Preceda Design
 *
 * A Tapestry Extension that provides Hibernate Sessions on demand.
 * 
 * <p>
 * To use this extension, access it as you would any other Tapetstry Extension
 * via the ApplicationSpecification.
 * 
 * <p>
 * To configure the extension, add something like this to the .application:
 * (Not Javadoc friendly)
 * <extension name="SessionSourceExt" class="com.preceda.hibertap.SessionSourceExt" immediate="yes">
 *   <configure property-name="classList" type="String">
 *     com.preceda.contact.Contact, com.other.OtherClasse, ...
 *   </configure>
 * </extension>
 * 
 * <p>
 * To access the extension within a page or component, use:
 * <code>
 * SessionSourceExt sse = (SessionSourceExt)
 *  (getEngine().getSpecification().getExtension("SessionSourceExt"));
 * </code>
 * 
 * <p>
 * To use the SessionSource extension, there are only three methods that you
 * need to worry about: getSession, commitAndClose, and rollbackAndClose.
 * Each method takes a IRequestCycle as an argument.  A returned Session has
 * a lifetime of a <b>SINGLE PAGE</b> for a <b>SINGLE REQUEST</b>.  At the
 * end of that lifetime, the transaction will automatically be committed
 * via a pageEndRenderer listener.  Thus, unless you need to rollback a
 * transaction, you would normally not need to worry about flushing & committing.
 * 
 * <p>
 * If the page changes during the cycle via
 * <code>cycle.setPage()<code>, the current Session's transaction is
 * committed and the Session is discarded.  Further requests for a Session
 * will be serviced by a new Session.
 * 
 * <p>
 * Sample Usage (pretty easy):
 * 
 * <code>
 * SessionSourceExt sse = (SessionSourceExt)
 *   (getEngine().getSpecification().getExtension("SessionSourceExt"));
 * Session s = sse.getSession(cycle);
 * . . .do some work
 * cycle.setPage("Success");	//Session is auto committed & closed
 * </code>
 * 
 * <p>
 * To Do:
 * <ul>
 * <li>Would a method 'commitAndContinue' be useful?
 * </ul>
 * 
 */

import java.util.StringTokenizer;
import java.io.InputStream;
import java.util.Properties;

import net.sf.tapestry.IPage;
import net.sf.tapestry.IRequestCycle;
import net.sf.tapestry.event.PageEvent;
import net.sf.tapestry.event.PageRenderListener;

import cirrus.hibernate.Session;
import cirrus.hibernate.SessionFactory;
import cirrus.hibernate.Transaction;
import cirrus.hibernate.Datastore;
import cirrus.hibernate.Hibernate;

public class SessionSourceExt {
	
	public final static String SESSION_KEY = "DEFAULT_HIBERNATE_SESSION";
	
	private String classList;	//List of classes to persist
	
	//Full absolute path name to the hibernate.properties file.
	//Defaults to Hibernate default.
	private String propertiesFileName = "/hibernate.properties";
	
	private SessionFactory sessionFactory;	//Used to create all requested Sessions
	
	//Single instance listener used to close & commit sessions
	//when a page completes the redering process.
	private PageRenderListener pageRenderListener;	
	
	
	/**
	 * Default empty constructor
	 */
	public SessionSourceExt() {}
	

	/**
	 * A list of persisted classes.
	 * 
	 * A list of fully qualified class names that Hibernate should persist.
	 * Names can be separated with commas, semi-colons, or spaces.
	 * For each class persisted, Hibernate expects to find a file named
	 * "ClassName.hbm.xml" in the same package location as the class.
	 * 
	 * @param classList String
	 */
	public void setClassList(String classList) {
		this.classList = classList;
	}

	/**
	 * Fully qualified name of the Hibernate Properties file.
	 * 
	 * Optional - defaults to "/hibernate.properties".
	 * 
	 * By Hibernate convention, this file is normally placed in the classpath
	 * root, however it could be placed anywhere.  Note that the name must
	 * be of this form:  "/package/package/name.properties" or if in the
	 * classpath root: "/name.properties".
	 * 
	 * <b>Limitation</b>The properties file must configure Hibernate to supply its
	 * own connections.
	 * 
	 * @param propertiesFileName String
	 */
	public void setPropertiesFileName(String propertiesFileName) {
		this.propertiesFileName = propertiesFileName;
	}
	
	/**
	 * Retrieves an existing Hibernate Session or creates a new one.
	 */
	public Session getSession(IRequestCycle cycle) throws Exception {
		//System.out.print("*** getSession");
		
		SessionWrapper sw = (SessionWrapper)cycle.getAttribute(SESSION_KEY);
		Session sess = null;
		
		if (sw != null) sess = sw.session;
		
		if (sess == null) { 
			
			sess = createSession(cycle);	//No previous session, create new
			
		} else if (! sess.isOpen()) {
			
			closeAndRollback(cycle);			//The user closed the session (do cleanup)
			sess = createSession(cycle);	//Now create a new session
				
		} else if (! sess.isConnected()) {
			
			sess.reconnect();		//The user disconnected the session
		}
		
		return sess;
	}
	
	/**
	 * Create a new session and store it as an attribute of the RequestCycle.
	 * 
	 * This method also adds a pageEndRender listener on the current page so
	 * that the session can be automatically committed when the page has
	 * completed rendering.
	 */
	private Session createSession(IRequestCycle cycle) throws Exception {
		//System.out.print("*** Create Session");
		
		if (sessionFactory == null) init();
		
		//Create a Session & begin a transaction on it
		Session session = sessionFactory.openSession();
		Transaction trans = session.beginTransaction();
		
		//Store Session & Transaction to a SessionWrapper & store as cycle attrib
		SessionWrapper sw = new SessionWrapper();
		sw.session = session;
		sw.transaction = trans;
		cycle.setAttribute(SESSION_KEY, sw);
		
		//Add pageRenderListener to allow auto commits
		IPage page = cycle.getPage();
		page.addPageRenderListener(pageRenderListener);
	
		
		return session;
	}
	
	/**
	 * Close and commits the session contained in the passed cycle
	 * 
	 * After calling this method, the current Session will be defunct and future
	 * calls to getSession will be servied by a new Session.
	 */
	public void closeAndCommit(IRequestCycle cycle) {
		//System.out.print("*** Close And Commit Session");
		
		SessionWrapper sw = (SessionWrapper)cycle.getAttribute(SESSION_KEY);
		
		if (sw != null) {
			
			Session session = sw.session;
			Transaction trans = sw.transaction;
			
			if (session != null && session.isOpen()) {


				try {
					
					try {
						
						trans.commit();
						//System.out.print("*** Session Closed OK");
						
					} catch (Exception ee) {
						
						System.out.print("*** Error during session close");
						trans.rollback();
						
					} finally {
						
						session.close();
						
					}
					
				} catch (Exception e) {
					//Can't do anything at this point - rollback or close failed	
				}
				
			} else {

				System.out.print("*** Session is closed (!!)");
				
			}
			
			//Cleanup
			cycle.removeAttribute(SESSION_KEY);
			cycle.getPage().removePageRenderListener(pageRenderListener);
		
		} else {

			System.out.print("*** There is no SessionWrapper!!!");
			
		}
		
			
	}
	
	/**
	 * Rolls back the session's transaction and closes the session.
	 * 
	 * After calling this method, the current Session will be defunct and future
	 * calls to getSession will be servied by a new Session.
	 */
	public void closeAndRollback(IRequestCycle cycle) {
		//System.out.print("*** Rollback");
		
		
		SessionWrapper sw = (SessionWrapper)cycle.getAttribute(SESSION_KEY);
		
		if (sw != null) {
			
			Session sess = sw.session;
			Transaction trans = sw.transaction;
			
			if (trans != null) { 
				
				try {
					trans.rollback();
				} catch (Exception e) { /* Not much we can do */ }
				
				try {
					sess.close();
				} catch (Exception e) { /* Not much we can do */ }
				
			}
				
				
			//Cleanup
			cycle.removeAttribute(SESSION_KEY);
			cycle.getPage().removePageRenderListener(pageRenderListener);
			
		} else {
			System.out.print("*** You are trying to rollback a cycle with no Session!!");
		}

	}
	
	private void init() throws Exception {

		if (classList == null || classList.length() == 0) {
			
			throw new Exception(
				"The Required init param classNames was not set or was empty.");
				
		} else {
			
			pageRenderListener = new PageRenderListener() {
				public void pageBeginRender(PageEvent event) { /*Don't need this event*/ }
				
				public void pageEndRender(PageEvent event) {
					
					//System.out.print("*** PageEndRender Event");
					closeAndCommit(event.getRequestCycle());
						
					event.getRequestCycle().removeAttribute(SESSION_KEY);
					event.getPage().removePageRenderListener(pageRenderListener);
				}
			};
			
			StringTokenizer st = new StringTokenizer(classList, ",; ");
			Datastore ds = Hibernate.createDatastore();
			
			try {
				
				while (st.hasMoreElements()) {
					ds.storeClass(Class.forName(st.nextToken()));	
				}
				
			} catch (Exception e) {
				
				throw new Exception(
					"An error occured while building Hibernate's persistant class list.  " +
					"Most likely, one of the class names is not on the classpath.",
					e);
					
			}
			

			
			//Load Hibernate Properties from properties file
			Properties props = new Properties();
			InputStream iStream = null;
			
			try {
				
				// "/" causes the loader to not convert dots to slashes (ie abs path).
				iStream = this.getClass().getResourceAsStream(propertiesFileName);
				props.load(iStream);
				iStream.close();
				
			} catch (Exception e) {
				
				throw new Exception(
					"The properties file '" + propertiesFileName + "' could not be loaded.", e);
				
			}
			

			sessionFactory = ds.buildSessionFactory(props);	//Build session factory


			try {
				
				//Test session
				Session s = sessionFactory.openSession();
				Transaction t = s.beginTransaction();
				t.commit();
				s.close();
				
			} catch (Exception e) {
				throw new Exception(
					"An error occured while testing the Hibernate SessionFactory.",
					e);	
			}
			
			
		}
	}
	
	
	/**
	 * Inner class used to encapsilate a session with its transaction
	 */
	static class SessionWrapper {
		protected Session session;
		protected Transaction transaction;
	}



}