Saturday, 20 December 2008

Dao Facade pattern

For a few months I was programming PHP applications using Symfony framework. I don't know Ruby On Rails that much, I suppose though, it's resembling RoR in some aspects. There was one piece of that platform I would like to focus on. It's ORM based on Propel. It's not as powerful as many JPA based frameworks, it has some nice features though.
One of those features was an ease of building simple CRUD applications really fast. In my opinion, the major concepts allowing building CRUD so easy are:

* Automatic Dao objects generation
* Provided availability of the most important methods like: retrieve by PK, select by criteria, selection with limited results
* Criteria abstraction layer

The funny stuff is that about year ago I have started working on a private project and I was missing this features, I couldn't name them though.

Let's start with automatic Dao generation first. I don't like the idea of code generation. I'm a fan of meta-data and stuff like that, but that's it. I have focused on a flexible DaoAdapter object. This object could serve as a parent to any dao implementation providing the most important methods.
The interface of my Dao is:

 
import java.util.List;

import torfox.dao.helper.FilterCriteria;
import torfox.dao.helper.SortCriteria;


/**
* Basic interface for data access objects
*
* @author Bartosz Jankiewicz
*
*/
public interface Dao
{
public final static String ID = "id";

/**
* Saves object to database, creating new record when necessary.
*
* @param object Object being saved
* @throws DataAccessException
*/
public E saveObject( E object) throws DataAccessException;

/**
* Returns object that corresponds given identifier
*
* @param id Object identifier
* @return Entity that matches identifier
* @throws DataAccessException
*/
public E retrieveById( Integer id) throws DataAccessException;

/**
* Returns object that corresponds given identifier. It's for String
* based identity columns.
*
* @param id Object identifier
* @return Matching object or null.
* @throws DataAccessException
*/
public E retrieveById( String id) throws DataAccessException;

/**
* Removes object from database.
*
* @param object
* @throws DataAccessException
*/
public void removeObject( E object) throws DataAccessException;

/**
* Usuwa z bazy obiekt o określonym identyfikatorze i typie na podstawie generic
*
* @param id
*/
public void removeObject( Integer id) throws DataAccessException;

/**
* Loading objects basing on the given criteria parameter.
*
* @param page Page number - used for paging results
* @param count Number of results per page - used in conjunction with page
* @param filter Filter object
* @param criteria Optional sorting
* @return List of objects
*/
public List doSelectPaged( final int page, final int count, final FilterCriteria filter, final SortCriteria ... criteria) throws DataAccessException;

/**
* Counts objects matchign criteria. Usually used for paging
*
* @param filterCriteria Yes, that's criteria
* @return
*/
public int getObjectCount( FilterCriteria filterCriteria) throws DataAccessException;

/**
* Retrieves a single entity matching the criteria. The implementation
* could throw an exception if 0 or more than 1 objects are matching the
* criteria.
*
* @param filterCriteria Criteria object
* @return Matching entity
*/
public E getSingleEntity( FilterCriteria filterCriteria) throws DataAccessException;

/**
* Selects all objects - no paging is provided by this method.
*
*/
public List doSelect( FilterCriteria filter, SortCriteria ... criteria) throws DataAccessException;

}


FilterCriteria and SortCriteria implementations is pretty simple. I could also use JPA (the final spec. wasn't ready when I was working on that) or Hibernate DetachedCriteria implementation, but I wanted the implementation to be totally independent.

I have prepared an implementation of this interface using Spring+Hibernate framework:

 
/*
* @(#) DaoAdapter.java
*
* Copyright 2004 Bartosz Jankiewicz. All rights reserved.
*
*/
package mayflower.dao.hibernate;

import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import torfox.dao.Dao;
import torfox.dao.DataAccessException;
import torfox.dao.helper.ConjunctionCriteria;
import torfox.dao.helper.DisjunctionCriteria;
import tofox.dao.helper.FilterCriteria;
import torfox.dao.helper.NotCriteria;
import torfox.dao.helper.Operator;
import torfox.dao.helper.PropertyCompareCriteria;
import torfox.dao.helper.SortCriteria;
import torfox.dao.helper.ValueCompareCriteria;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.hibernate.Criteria;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Junction;
import org.hibernate.criterion.MatchMode;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Property;
import org.hibernate.criterion.Restrictions;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

/**
* Business object access adapter
*
* @author bartosz.jankiewicz
*
* @param
*/
@SuppressWarnings("unchecked")
public abstract class DaoAdapter extends HibernateDaoSupport implements Dao
{
protected Logger logger = Logger.getLogger( getClass());

@Transactional( readOnly = false, propagation = Propagation.REQUIRES_NEW)
public E saveObject( E object)
{
getHibernateTemplate().saveOrUpdate( object);
return object;
}

public E retrieveById( Integer id) throws DataAccessException
{
FilterCriteria criteria = new ValueCompareCriteria( "id", id, Operator.EQ);
return getSingleEntity( criteria);
}

public E retrieveById( String id) throws DataAccessException
{
FilterCriteria criteria = new ValueCompareCriteria( "id", id, Operator.EQ);
return getSingleEntity( criteria);
}

public void removeObject( E object)
{
getHibernateTemplate().delete( object);
}

public void removeObject( Integer id)
{
Object object = getHibernateTemplate().get( getEntityClass(), id);
getHibernateTemplate().delete( object);
}

public List doSelectPaged( final int page, final int count, final FilterCriteria filter, final SortCriteria ... criteria)
{
List entityList = getHibernateTemplate().executeFind( new HibernateCallback() {
public Object doInHibernate( Session session) throws HibernateException, SQLException
{
Criteria query;
try
{
query = buildQuery( session, filter, criteria);
}
catch (DataAccessException e)
{
throw new HibernateException( e);
}
addListAssociations( query);
if( count > 0)
{
query.setFirstResult( count * (page - 1)).setMaxResults( count);
}
return query.list();
}
});
return entityList;
}

public List doSelect( final FilterCriteria filter, final SortCriteria ... criteria)
{
List entityList = getHibernateTemplate().executeFind( new HibernateCallback() {
public Object doInHibernate( Session session) throws HibernateException, SQLException
{
Criteria query;
try
{
query = buildQuery( session, filter, criteria);
}
catch (DataAccessException e)
{
throw new HibernateException( e);
}
addListAssociations( query);
return query.list();
}
});
return entityList;
}


public E getSingleEntity( final FilterCriteria filterCriteria)
{
E entity = (E) getHibernateTemplate().execute( new HibernateCallback() {
public Object doInHibernate( Session session) throws HibernateException, SQLException
{
Criteria query;
try
{
query = buildQuery( session, filterCriteria);
}
catch (DataAccessException e)
{
throw new HibernateException( e);
}
query = addEntityAssociations( query);
query.setResultTransformer( Criteria.ROOT_ENTITY);
return query.uniqueResult();
}
});
return entity;
}

public int getObjectCount( final FilterCriteria criteria)
{
return ((Number) getHibernateTemplate().execute( new HibernateCallback() {
public Object doInHibernate( Session session) throws HibernateException, SQLException
{
Criteria query;
try
{
query = buildQuery( session, criteria);
query.setProjection( Projections.rowCount());
}
catch (DataAccessException e)
{
throw new HibernateException( e);
}
addListAssociations( query);
return query.uniqueResult();
}
})).intValue();
}

protected Criteria buildQuery( Session session, FilterCriteria filterCriteria, SortCriteria ... criteria) throws DataAccessException
{
Map aliasMap = new HashMap(); //Mapa aliasów użytych przy tworzeniu filtrów i sortowaniu
Criteria query = session.createCriteria( getEntityClass());
Criterion criterion = decodeFiler( filterCriteria, query, aliasMap);
if( criterion != null)
{
query.add( criterion);
}
if( criteria != null && criteria.length > 0)
{
for( SortCriteria field : criteria)
{
addSort( query, field, aliasMap);
}
}
return query;
}

protected void addSort( Criteria criteria, SortCriteria sortCriteria, Map aliasMap)
{
if( sortCriteria == null) return;
Property property = getProperty( sortCriteria.getProperty(), criteria, aliasMap);
if( sortCriteria.isAsc())
{
criteria.addOrder( property.asc());
}
else
{
criteria.addOrder( property.desc());
}
}

protected Property getProperty( String path, Criteria criteria, Map aliasMap)
{
if( StringUtils.isEmpty( path)) return null;
String[] parts = path.split( "\\.");
if( parts.length > 1)
{
String newPath = parts[0];
String alias = null;
for( int part = 1; part < parts.length; part++)
{
if( !aliasMap.containsKey( newPath))
{
alias = "_alias" + aliasMap.size();
aliasMap.put( newPath, alias);
criteria.createAlias( newPath, alias);
newPath = alias;
}
else
{
alias = aliasMap.get( newPath);
}
newPath = alias + "." + parts[part];
}
return Property.forName( newPath);
}
return Property.forName( path);
}

private Criterion decodeFiler( FilterCriteria filterCriteria, Criteria query, Map aliasMap) throws DataAccessException
{
if( filterCriteria == null) return null;
if( filterCriteria instanceof ValueCompareCriteria)
{
ValueCompareCriteria criteria = (ValueCompareCriteria) filterCriteria;
Property property = getProperty( criteria.getProperty(), query, aliasMap);
switch( criteria.getOperator())
{
case EQ: return property.eq( criteria.getValue());
case GE: return property.ge( criteria.getValue());
case GT: return property.gt( criteria.getValue());
case LE: return property.le( criteria.getValue());
case LT: return property.lt( criteria.getValue());
case MA: return property.like( criteria.getValue().toString(), MatchMode.ANYWHERE);
case NE: return property.ne( criteria.getValue());
default: throw new DataAccessException( "Filter operator " + criteria.getOperator() + " not supported");
}
}
if( filterCriteria instanceof PropertyCompareCriteria)
{
PropertyCompareCriteria criteria = (PropertyCompareCriteria) filterCriteria;
Property property = getProperty( criteria.getProperty(), query, aliasMap);
Property otherProperty = getProperty( criteria.getOtherProperty(), query, aliasMap);
switch( criteria.getOperator())
{
case EQ: return property.eqProperty( otherProperty);
case GE: return property.geProperty( otherProperty);
case GT: return property.gtProperty( otherProperty);
case LE: return property.leProperty( otherProperty);
case LT: return property.ltProperty( otherProperty);
case NE: return property.neProperty( otherProperty);
default: throw new DataAccessException( "Filter operator " + criteria.getOperator() + " not supported");
}
}
if( filterCriteria instanceof ConjunctionCriteria)
{
Junction junction = Restrictions.conjunction();
for( FilterCriteria criteria : ((ConjunctionCriteria)filterCriteria).getCriterias())
{
junction.add( decodeFiler( criteria, query, aliasMap));
}
return junction;
}
if( filterCriteria instanceof DisjunctionCriteria)
{
Junction junction = Restrictions.disjunction();
for( FilterCriteria criteria : ((DisjunctionCriteria)filterCriteria).getCriterias())
{
junction.add( decodeFiler( criteria, query, aliasMap));
}
return junction;
}
if( filterCriteria instanceof NotCriteria)
{
return Restrictions.not( decodeFiler( ((NotCriteria)filterCriteria).getCriteria(), query, aliasMap));
}
throw new DataAccessException( "Filter criteria not supported: " + filterCriteria);
}

/**
* Returns managed entity class
*
* @return Managed entity class
*/
protected abstract Class getEntityClass();

/**
* TODO: adds additional associations to the criteria when calling select
* for list of objects.
* @param criteria Query object
* @return Modified Criteria object
*/
protected Criteria addListAssociations( Criteria criteria)
{
return criteria;
}

/**
* TODO: adds additional associations to the criteria when object relation
* requires that
* @param criteria Obiekt zapytania
* @return Mofied Criteria object
*/
protected Criteria addEntityAssociations( Criteria criteria)
{
return criteria;
}
}


What I have achieved is a very quick implementation of DAO objects providing the most commonly used methods. Using DaoAdapter all I have to do is to extend DaoAdapter and implement only 1 method providing managed object class. For those who think, that this class is already provided with the generics I would like to explain that this is impossible for some important architectural limitations of generics.

So, an example of implementation of DAO for Person entity whould require the following steps:
1) create Person entity and mappings
2) create an empty PersonDao interface extending Dao. This step is actually not required if you don't care about implementation independence.
3) create PersonDaoImpl class extending DaoAdapter and (only if you have followed the step 2nd) implementing PersonDao.
4) implement getEntityClass method putting the single line of code:
return Person.class


Using this mini-framework allowed me to implement a simple CRUD application with 7 DAO objects within 1 day and it's working pretty well so far :)

Full implementation of the code can be downloaded from: http://torfox.pl/examples/dao_facade.zip

Friday, 19 December 2008

Servlet and resources combat

Java is a great, everything is possible. After a few months of designing and developing PHP systems I was more than happy to move to Java. I'm developing now a project using old good Spring framework with MVC.
I was alwyas wondering what is the best way of referring to resources like javascript, css, images on the .jsp files. What should be natural is using absolute paths, but this was not an option in my situation since I didn't want my application to be application context dependent. Another option is to use relative paths everywhere and keep some standards of url structure to make these relative urls work. E.g. when opening a page using MVC we point the address using UrlResolvers, these URLs could be resembling this one: http://my.host.com/application/dispatcher/module/action
In my JSP pages I couls assume the URL structure and point the resource with relative path (let's assume there is an /images folder in web application root folder):
<img src="../../images/logo.gif" />

It's a outbraining solution in my opinion.

The solution that is mentioned the most often is to use ;<c:url value="/images/logo.gif"> tag.
I works, I can't say otherwise, but... I'm lazy and quickly I become tired of filling those damn <c:url> tags

So, the solution that finally brought smile on my face was... a filter. Actually, there is out of the box solution provided by Spring similar to mine- ResourceServlet... I had some problems deploying it to access image resources though.

My filter recognizes 3 types of resources: images, css, js. I have assumed that these resources are always available in the corresponding folders of web application root. Filter simply checks if requested url matches resource location. If it finds images/ css/ or js/ string in the url then it includes a corresponding resource in the response.
My filter implementation looks like this:


import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;

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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* This filter makes .jsp design related to image/css/js resource a lot easier. It translates all accesses to these resources no matter
* where are they called from. Basically it allows to use relative path everywhere on the sites e.g.:
* instead of calling:
* <code><img src="&#60;c:url value=" /&#62;"/></code>
* simple code can be user:
* <code><img src="images/logo.gif" /></code>
*
* Remember, that relative path must be user, since /images will not be passed to this filter at all.
*
* @author Bartosz Jankiewicz
*/
public class ResourcesFilter implements Filter
{
private FilterConfig config;

@Override
public void destroy()
{
}

@Override
public void doFilter( ServletRequest request, ServletResponse response, FilterChain filterChain)
throws IOException, ServletException
{
if ( request instanceof HttpServletRequest)
{
String url = ( (HttpServletRequest) request).getRequestURI();
String path = decodeResourcePath( url);
if( path != null)
{
File file = new File( path);
if( !file.exists())
{
((HttpServletResponse) response).sendError( HttpServletResponse.SC_NOT_FOUND);
return;
}
String contentType = this.config.getServletContext().getMimeType( file.getName());
doInclude( file, contentType, (HttpServletResponse) response);
return;
}
}
filterChain.doFilter( request, response);
}

private String decodeResourcePath( String url)
{
if ( url.contains( "images/"))
{
String imagePath = "/" + url.substring( url.indexOf( "images/"));
return this.config.getServletContext().getRealPath( imagePath);
}
if ( url.contains( "/css/") || url.startsWith( "css/"))
{
String resourcePath = "/" + url.substring( url.indexOf( "css/"));
return this.config.getServletContext().getRealPath( resourcePath);
}
if ( url.contains( "/js/") || url.startsWith( "js/"))
{
String resourcePath = "/" + url.substring( url.indexOf( "js/"));
return this.config.getServletContext().getRealPath( resourcePath);
}
return null;
}

private void doInclude( File file, String contentType, HttpServletResponse response) throws IOException
{
response.setContentType( contentType);
response.setContentLength((int)file.length());

// Open the file and output streams
FileInputStream in = new FileInputStream( file);
OutputStream out = response.getOutputStream();

// Copy the contents of the file to the output stream
byte[] buf = new byte[1024];
int count = 0;
while ( ( count = in.read( buf)) >= 0)
{
out.write( buf, 0, count);
}
in.close();
out.close();

}

@Override
public void init( FilterConfig config) throws ServletException
{
this.config = config;
}

}


Happy Java coding :)

Cheers,
Bartosz