lundi 11 février 2008

Seam : a way to handle server-side exceptions

In this article, we'll see how one can handle server-side exceptions using seam remoting.

Because Seam Remoting doesn't handle gracefully those exceptions (see http://jira.jboss.org/jira/browse/JBSEAM-633), we must manage those exceptions and be able to show the user a comprehensive error message.

For my current ExtJS + Seam project, I'm using the following pattern :

- a business SessionBean which do the actual business job (open a transaction, do the persistence work, etc)



package xxx.yyy.zzz;

import java.util.List;
import javax.ejb.Local;
import xxx.yyy.zzz.Chauffeur;

/**
* @author cvigouroux
*
*/

@Local
public interface HelloAction
{
/**
* @param nouveauPays
* @return Le pays mis à jour
*/

public Chauffeur modifierChauffeur(Chauffeur nouveauChauffeur);
}



package xxx.yyy.zzz;

import java.util.List;

import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.Query;

import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Logger;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.TransactionPropagationType;
import org.jboss.seam.annotations.Transactional;
import org.jboss.seam.log.Log;

import xxx.yyy.zzz.Chauffeur;

/**
* @author cvigouroux
*
*/

@Stateless
@Name("faucon80_cartographie_helloAction")
@SuppressWarnings("unchecked")
public class HelloActionBean implements HelloAction
{
@Logger
private Log log;

@In
private EntityManager entityManager;

/*
* (non-Javadoc)
*
* @see com.elomobile.faucon80.cartographie.session.metier.HelloAction#modifierChauffeur(com.elomobile.faucon80.cartographie.entity.Chauffeur)
*/

@Transactional(TransactionPropagationType.REQUIRED)
public Chauffeur modifierChauffeur(Chauffeur nouveauChauffeur)
{
// mise à jour de la date
// nouveauChauffeur.setDtheumod(new Date());

// mise à jour de la base de données
entityManager.merge(nouveauChauffeur);
entityManager.flush();

return nouveauChauffeur;
}
}



- an adapter for that SessionBean (using adapter design pattern) in the form of another SessionBean whose job is to call the business and to handle the exceptions. The exception is then redirected to the client in the form of a message with a criticity (success, information, error). In order to ligthen the exception handling code within each service method, I use an interceptor for doing the job.


package xxx.yyy.zzz;

import java.io.InputStream;
import java.util.List;

import javax.ejb.Local;

import org.jboss.seam.annotations.remoting.WebRemote;

import xxx.yyy.zzz.Chauffeur;
import xxx.yyy.zzz.web.messages.BasicPopupMessage;
import xxx.yyy.zzz.web.paging.PagingQueryParam;
import xxx.yyy.zzz.web.paging.PagingResultList;
import xxx.yyy.zzz.web.paging.QueryParam;
import xxx.yyy.zzz.web.paging.ResultList;

/**
* @author cvigouroux
*
*/

@Local
public interface HelloWebAction
{
/**
* @param nouveauPays
* @return Le pays mis à jour
* @throws Exception
*/

@WebRemote
public BasicPopupMessage modifierChauffeur(Chauffeur nouveauChauffeur) throws Exception;

/**
* Méthode destroy...
*/

public void destroy();

}



package xxx.yyy.zzz.session.web;

import java.io.InputStream;
import java.rmi.RemoteException;
import java.util.List;
import java.util.Map;

import javax.ejb.Remove;
import javax.ejb.Stateful;
import javax.interceptor.Interceptors;
import javax.xml.rpc.ServiceException;

import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.*;
import org.jboss.seam.log.Log;

import xxx.yyy.zzz.Identity;
import xxx.yyy.zzz.Chauffeur;
import xxx.yyy.zzz.session.metier.HelloAction;
import xxx.yyy.zzz.web.messages.BasicPopupMessage;
import xxx.yyy.zzz.web.paging.PagingQueryParam;
import xxx.yyy.zzz.web.paging.PagingResultList;
import xxx.yyy.zzz.web.paging.QueryParam;
import xxx.yyy.zzz.web.paging.ResultList;

/**
* @author cvigouroux
*
*/

@Stateful
@Scope(ScopeType.SESSION)
@Name("faucon80_cartographie_helloWebAction")
public class HelloWebActionBean implements HelloWebAction
{
@Logger
private Log log;

@In(create = true, value = "helloAction")
private HelloAction helloAction;

@In
private Map<String, String> messages;

/*
* (non-Javadoc)
*
* @see com.alteca.seam.prototype.session.web.HelloWebAction#updatePays(com.alteca.seam.prototype.entity.Tpays)
*/

@Interceptors(InterceptorBasicPopupMessageMethod.class)
public BasicPopupMessage modifierChauffeur(Chauffeur nouveauChauffeur) throws Exception
{
Identity.instance().checkRestriction("#{org.jboss.seam.security.identity.hasPermission('helloAction')}");

Chauffeur chauffeurMaj = helloAction.modifierChauffeur(nouveauChauffeur);

return new BasicPopupMessage(BasicPopupMessage.Criticite.INFO, messages
.get("helloAction.modifierChauffeur.succes"), messages.get("bouton.ok"), chauffeurMaj);
}
}




package xxx.yyy.zzz.session.web;

import java.util.Map;

import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;

import org.jboss.seam.annotations.Logger;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.log.Log;
import org.jboss.seam.security.AuthorizationException;

import xxx.yyy.zzz.web.messages.BasicPopupMessage;

@Name("interceptorBasicPopupMessageMethod")
public class InterceptorBasicPopupMessageMethod
{
@Logger
private static Log log;

@AroundInvoke
public Object interceptGenericExceptions(InvocationContext invocation) throws Exception
{
Object o = invocation.getParameters()[0];

Map<String, String> messages = org.jboss.seam.international.Messages.instance();

try
{
return invocation.proceed();
}
catch (AuthorizationException ae)
{
return new BasicPopupMessage(BasicPopupMessage.Criticite.ERROR, messages.get("erreur.nonAuthorise"), messages.get("bouton.ok"), o);
}
catch (Exception e)
{
log.error("", e);
return new BasicPopupMessage(BasicPopupMessage.Criticite.ERROR, messages.get("erreur.generique") + e.getMessage(), messages.get("bouton.ok"), o);
}
}
}



On the javascript side, I use a standard callback function which intercepts the returned message and shows a message box with the transmitted message string.


/**
*
* @param {Ext.data.Store} store The store to which belong the record.
* @param {Ext.data.Record} record The record to be updated server-side.
* @param {String} formName The name of the form to update with the server-side updated bean.
* @param {function} callbackFunction The function to call after handling.
*/

Ext.data.SeamRemotingRecordHandler = function(store, record, formName, callbackFunction)
{
this.callback = function(reponse)
{
if (reponse.criticite != 'ERROR')
{
Ext.getCmp(formName).getForm().updateRecord(record);

// get new data from server
record.data = reponse.data;
record.endEdit();

// tell the store to refresh
store.commitChanges();

// loads the new record in the form
Ext.getCmp(formName).getForm().loadRecord(record);
}

if (reponse.criticite != 'SUCCES')
{
// show an information or an error message
Ext.Msg.show(
{
minWidth: 200,
closable: false,
msg: reponse.message,
buttons:
{
ok: reponse.messageBouton
},
icon: Ext.MessageBox[reponse.criticite]
});
}

// call client's callback function
callbackFunction();
}
}


Example of use :


var handler = new Ext.data.SeamRemotingRecordHandler(store, recordAModifier, "tpays-form", function()
{
});

helloWebAction.modifierChauffeur(copieRecord.data, handler.callback);

Seam ExtJS Extension, updated code

Being used in developpement stage of our current projet, my extension keeps to be updated.

Changes since first version :
- optimized code : the field definition of the records are only parsed once. It was parsed for every returned row in the last version
- added static Ext.data.BeanRecord.createFromBean(bean) function that creates a record directly from a bean (usefull for example in order to populate a form via form.loadRecord(beanRecord)).


Future version will include a proxy Store using a modified "PagingMemoryProxy" implementation in order to be able to define a store able to do local paging from a defined SeamRemotingStore. The proxy is already prototyped but needs to be package in some smart form of a store encapsulation at once the paging store and the proxied SeamRemotingStore. This kind of store can be VERY usefull in case of a SeamRemotingStore having 1000+ rows. The grid cannot handle so much data (moving, sorting, rendering in general veryyyy slow), so we needed to add a pager to it (without compromising the needed functionnality to have all rows loaded on client-side !).

Download complete file here.

Seam + ExtJS, Java glue code

In order to use my ExtJS extensions, we need some Java glue code like the generic method parameters and return values.

So, let's begin with the generic method parameters :

- for a simple query method :

package xxx.yyy.zzz;
import org.jboss.seam.annotations.Name;

@Name("queryParam")
public class QueryParam
{
private String queryString;

public QueryParam()
{
super();
}

public String getQueryString()
{
return queryString;
}

public void setQueryString(String queryString)
{
this.queryString = queryString;
}

}

- for a simple paging query method :


package xxx.yyy.zzz;

import org.jboss.seam.annotations.Name;

@Name("pagingQueryParam")
public class PagingQueryParam extends QueryParam
{
private int start;
private int limit;


public int getStart()
{
return start;
}

public void setStart(int start)
{
this.start = start;
}

public int getLimit()
{
return limit;
}

public void setLimit(int limit)
{
this.limit = limit;
}
}




Now for the return values :
- for a simple query method :
package xxx.yyy.zzz;

import java.util.List;

import org.jboss.seam.annotations.Name;

/**
*
* @author cvigouroux
*
* @param <T>
*/

@Name("resultList")
public class ResultList<T>
{
private List<T> list;
private int totalRecords;

public ResultList(List<T> list)
{
this.list = list;
this.totalRecords = list.size();
}

public int getTotalRecords()
{
return totalRecords;
}

public void setTotalRecords(int totalRecords)
{
this.totalRecords = totalRecords;
}

public List<T> getList()
{
return list;
}

public void setList(List<T> list)
{
this.list = list;
}
}

- for a simple paging query method :

package xxx.yyy.zzz;

import java.util.List;

import org.jboss.seam.annotations.Name;

/**
* @author cvigouroux
*
* @param <T>
*/

@Name("pagingResultList")
public class PagingResultList<T> extends ResultList<T>
{
// private int totalRecords;
// private List<T> list;

public PagingResultList(int totalRecords, List<T> list)
{
super(list);
this.setTotalRecords(totalRecords);
}
}