October 16, 2007

Image Handling in Seam Apps part IV: Displaying and Caching

This is the fourth part of a tutorial about how to manage images in your Seam based web application. This tutorial consists of several parts:

Part One: The Database Model
Part Two: File Upload
Part Three: Scaling
Part Four: Displaying and Caching
Part Five: Restricting Access

This fourth installment shows you how to use a Servlet to access seam components, load data from the database and view artworks in a web browser.

Seam Context Filter

Seam 2 provides a context filter that allows servlets to access component managed by Seam. This includes built in components, such as Identity, and user defined ones. This context filter needs to be configured in components.xml with a url pattern general enough to address all servlets not processed through the JSF lifecycle.


<web:context-filter url-pattern="/gallery/*" />

You define your servlets in web.xml using the usual tags servlet and servlet-mapping:

<servlet>
<servlet-name>Artwork Servlet</servlet-name>
<servlet-class>com.truerunes.web.ArtworkServlet</servlet-class>
<load-on-startup>2</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Artwork Servlet</servlet-name>
<url-pattern>/gallery/artworks/*</url-pattern>
</servlet-mapping>

The Artwork Servlet

This servlet services all requests to view artworks whether being initiated using an anchor tag, a h:graphicImage tag or accessed directly from the web browser. The example used in this tutorial has a request uri similar to this one: /truerunes/gallery/artworks/01.jpg. The first thing you need to do is to get the file name using the getPathInfo method of the request object:

public class ArtworkServlet extends javax.servlet.http.HttpServlet {

public static final long serialVersionUID = 596009789004L;

protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
String pathInfo = request.getPathInfo();
String fileName = WebUtil.getFileName(pathInfo);
if (fileName == null) {
String contextPath = request.getContextPath();
response.sendRedirect(contextPath + "/404.jsf");
return;
}
}
}

public class WebUtil {

public static String getFileName(String path) {
// Path format is "/file_name.jpg"
if (path != null && path.length() > 1) {
String fileName = path.substring(1);
int dotIndex = fileName.indexOf('.');
String extention = fileName.substring(dotIndex + 1);
if (extention.equals(Extention.jpg.getExtention())
|| extention.equals(Extention.png.getExtention())
|| extention.equals(Extention.gif.getExtention())) {
return fileName;
}
}
return null;
}

enum Extention {
jpg("jpg"), png("png"), gif("gif");

Extention(String extention) {
this.extention = extention;
}

private String extention;

String getExtention() {
return this.extention;
}
}
}

Now comes the beauty of Seam Context Filter, getting a component is as simple as calling Component.getInstance() with the name of the component you need. Here an entity manager and a user transaction are obtained using the getInstance method. Since an user managed transaction is obtaine you need to demarcate the boundaries of the transaction and account for any exceptions. The status of the transaction is verified agains Status.STATUS_ACTIVE to make sure that the transaction has not been started and the transaction manager did not begin the two-phase commit.

public class ArtworkServlet extends javax.servlet.http.HttpServlet {

protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {

// Thanks to Seam Context Filter, getting an entity manager is easy.
EntityManager em = (EntityManager) Component
.getInstance("entityManager");
UserTransaction utx = null;
boolean txStarted = false;
ArtworkInfo artworkInfo = null;
try {
utx = (UserTransaction) Component
.getInstance("org.jboss.seam.transaction.transaction");
if (utx.getStatus() != Status.STATUS_ACTIVE) {
utx.begin();
txStarted = true;
}
em.joinTransaction();
String query = "from gs_artwork_info a where a.fileName = :filename";
artworkInfo = (ArtworkInfo) em.createQuery(query).setParameter("filename",
fileName).getSingleResult();
if (txStarted)
utx.commit();
} catch (NoResultException nre) {
err.println("No artwork was found with name " + fileName);
} catch (Exception e) {
err.println("Exception when trying to read artwork " + fileName);
err.println("Exception message: " + e.getMessage());
e.printStackTrace();
try {
if (txStarted)
utx.rollback();
} catch (Exception ex) {
err.println("Rollback attempt failed: "+ex.getMessage());
ex.printStackTrace();
}
}
// calling em.close() causes "Exception calling @Destroy method."
}
}

Before streaming the artwork to the web browser, two important HTTP headers need to be set; Expires and Last-Modified. With these two header specified artworks will be cached locally in the web browser cache. This is crucial to avoid having to serve the same artwork each time a user decides to revisit a page that has an artwork. The Expires header is set to be a year from now and the Last-Modified header is set to the date the artwork got uploaded to the server.

public class ArtworkServlet extends javax.servlet.http.HttpServlet {

protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
if(artworkInfo == null){
err.println("Could not find an artwork with file name: "+fileName);
String contextPath = request.getContextPath();
response.sendRedirect(contextPath+"/404.jsf");
return;
}

Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
calendar.add(Calendar.YEAR, 1);
response.setDateHeader("Expires", calendar.getTimeInMillis());

calendar.setTime(artworkInfo.getUploadDate());
response.setDateHeader("Last-Modified", calendar.getTimeInMillis());

response.setContentType(artworkInfo.getContentType());
Artwork artwork = artworkInfo.getArtwork();
response.setContentLength((int)artwork.getSize());
response.getOutputStream().write(artwork.getData());
response.getOutputStream().flush();
response.getOutputStream().close();
}
}

6 comments:

tischer said...

I receive a error:
No application context active
Error Exception Filter

Any solution

Nils said...

Nice demo of Seam's servlet integration. However, it's easier to just return the image data by using the HTTP response object's output stream. See Sending Files"

Nils Kassube

Anonymous said...

I can't deploy when this line:

web:context-filter url-pattern="/gallery/*"/

is in component.xml.

I'm using seam 2, any solutions?

/Jacob

firstthumb said...

Add the lines below in your component.xml to use web:context-filter

xmlns:web="http://jboss.com/products/seam/web"

http://jboss.com/products/seam/web http://jboss.com/products/seam/web-2.0.xsd

Guilherme said...

you can also display the image with

"s:graphicImage value="#{yourEntityHome.instance.imageBytes}" /"

without the need of a servlet.

raj said...

Thanks for the useful post.

On the caching front, you need to complete the loop and correctly handle cache revalidation requests too. So, in your doGet() method, you need to look for a request header called "if-modified-since" and match it with the modification date of the artwork. If the artwork hasn't changed, you need to do

response.setStatus( HttpServletResponse.SC_NOT_MODIFIED );