Integrated Open Souce Technology Solutions

JSF - Rendering Images stored in Database

November 3rd, 2008 Posted in JAVA, JSF, Spring

by Michael Root

This article shows how to render images from data stored in the database to a JSF view.

This solution was created for the following environment, but will work in other configurations.

  • JavaServer Faces (JSF) 1.2
  • Richfaces 3.2.2.GA
  • SpringFramework 2.5.5
  • Spring Webflow 2.0.3
  • Hibernate 3.2.6.ga

Instead of storing the image data in the database one could store the images to an external directory. I have used external image directories in several projects but I find scalability and image management is more difficult. This post will describe how to perform image management using the database to store the images.

UserImage - BaseDAOHibernateJPA - ImageManager
First it is necessary implement the code to retrieve the data from the database. In our case we were using JPA with Hibernate so I will identify the UserImage entity class along with the DAO and ImageManager classes to achieve this functionality.

@Entity
  1. @Table(name = "user_image")
  2. public class UserImage extends BaseObject {
  3.     private static final long serialVersionUID = 1410602442670943241L;
  4.  
  5.     @Id
  6.     @GeneratedValue(strategy = GenerationType.AUTO)
  7.     @Column(name = "id")
  8.     private Long id;
  9.  
  10.     @Version
  11.     private int version;
  12.  
  13.     @Enumerated(EnumType.STRING)
  14.     @Column(name="image_type", nullable=false)
  15.     private ImageTypeEnum imageType = ImageTypeEnum.USER;
  16.  
  17.     @ManyToOne(fetch=FetchType.LAZY)
  18.     @JoinColumn(name="user", nullable=false)
  19.     private User user;
  20.  
  21.     @Column(name="filename")
  22.     private String filename;
  23.  
  24.     @Column(name="filesize")
  25.     private long filesize;
  26.  
  27.     @Column(name="data", columnDefinition="mediumBlob")
  28.     private byte[] data;
  29.  
  30.     @Column(name="content_type")
  31.     private String contentType;
  32.  
  33.     @Column(name="created_on", updatable=false, insertable=true)
  34.     private Date createdOn;
  35.  
  36.     public UserImage() {
  37.     }
  38.  
  39.     // Setters and Getters have been removed
  40. }

The UserImage.data field has been mapped to a mediumBlob which is fine for MySQL. For Oracle this would have to be changed to Blob. I have also added a ImageType so that this table can be used to store different types of image data.

The following code segment displays the DAO base class which for our purposes is used to retrieve the UserImage data from the database through its CRUD operation getObject.

/**
  1.  * This class serves as the Base class DAO JPA support.
  2.  * Can be used for standard CRUD operations.
  3.  
  4.  *
  5.  * @author <a href="mailto:mroot@serensystems.com">Michael Root</a>
  6.  * @since August 31, 2008
  7.  */
  8. @Repository("baseDAO")
  9. public class BaseDAOHibernateJPA implements DAO {
  10.     protected final transient Log log = LogFactory.getLog(getClass());
  11.  
  12.     @PersistenceContext
  13.     protected EntityManager entityManager;
  14.  
  15.     /**
  16.      * @see com.serensys.golf.dao.DAO#saveObject( entity)
  17.      */
  18.     public  T saveObject(T entity) throws DataAccessException {
  19.         return entityManager.merge(entity);
  20.     }
  21.  
  22.     /**
  23.      * @see com.serensys.golf.dao.DAO#getObject(Class, Object)
  24.      */
  25.     public  T getObject(Class entityClass, Object primaryKey) throws DataAccessException {
  26.         T entity = entityManager.find(entityClass, primaryKey);
  27.         if (entity == null) {
  28.             throw new DataRetrievalFailureException("Entity could not be found by key=" + primaryKey);
  29.         }
  30.         return entity;
  31.     }
  32.  
  33.     /**
  34.      * @see com.serensys.golf.dao.DAO#removeObject(Object)
  35.      */
  36.     public void removeObject(Object entity) throws DataAccessException {
  37.         entityManager.remove(entity);
  38.     }
  39.  
  40.     /**
  41.      * @return the entityManager
  42.      */
  43.     public EntityManager getEntityManager() {
  44.         return entityManager;
  45.     }
  46.  
  47.     /**
  48.      * @param entityManager the entityManager to set
  49.      */
  50.     public void setEntityManager(EntityManager entityManager) {
  51.         this.entityManager = entityManager;
  52.     }
  53. }

The following class is the ImageManager that is called from the ImageDBServlet to retrieve the actual UserImage entity.

/**
  1.  * Implementation of UserImage interface.
  2.  
  3.  *
  4.  * @author <a href="mailto:mroot@serensystems.com">Michael Root</a>
  5.  * @since October 31, 2008
  6.  */
  7. @Service("imageManager")
  8. public class ImageManagerImpl extends BaseManager implements ImageManager {
  9.  
  10.     /**
  11.      * @see com.serensys.golf.service.ImageManager#getUserImage(Long)
  12.      */
  13.     @Transactional(readOnly = true)
  14.     public UserImage getUserImage(Long id) {
  15.   return this.baseDAO.getObject(UserImage.class, id);
  16.     }
  17. }

ImageDBServlet
The following servlet renders the image data to the output stream as shown in the class below.

/**
  1.  * Special image servlet for efficiently resolving and rendering image resources from a database.
  2.  *
  3.  * @author Michael Root
  4.  */
  5. public class ImageDBServlet extends HttpServletBean {
  6.     private static final long serialVersionUID = -8604559719271081403L;
  7.  
  8.     private static final String HTTP_CONTENT_LENGTH_HEADER = "Content-Length";
  9.  
  10.     private static final String HTTP_LAST_MODIFIED_HEADER = "Last-Modified";
  11.  
  12.     private static final String HTTP_EXPIRES_HEADER = "Expires";
  13.  
  14.     private static final String HTTP_CACHE_CONTROL_HEADER = "Cache-Control";
  15.  
  16.     private boolean gzipEnabled = true;
  17.  
  18.     private static ApplicationContext appContext = null;
  19.     private ImageManager imageManager;
  20.  
  21.     private Set compressedMimeTypes = new HashSet();
  22.     {
  23.         compressedMimeTypes.add("text/css");
  24.         compressedMimeTypes.add("text/javascript");
  25.     }
  26.  
  27.     /**
  28.      * Method called by base class to allow subclass to initialize.  The applicationContext
  29.      * imageManager will be retrieved from the applicationContext.
  30.      */
  31.     protected void initServletBean() throws ServletException {
  32.         appContext = WebApplicationContextUtils.getWebApplicationContext(super.getServletContext());
  33.         if (appContext != null) {
  34.             imageManager = (ImageManager)appContext.getBean("imageManager");
  35.         }
  36.     }
  37.  
  38.     protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  39.  
  40.         String rawResourcePath = request.getPathInfo();
  41.         Long imageId = null;
  42.  
  43.         if (logger.isDebugEnabled()) {
  44.             logger.debug("Attempting to GET resource: " + rawResourcePath);
  45.         }
  46.  
  47.         // Get the index from the rawResourcePath
  48.         int idx = rawResourcePath.lastIndexOf("_");
  49.  
  50.         // If index &gt; 0 then parse imageId
  51.         if (idx &gt; -1) {
  52.             String imageIdStr = rawResourcePath.substring(idx+1);
  53.             if (logger.isDebugEnabled()) {
  54.                 logger.debug("doGet: imageIdStr: " + imageIdStr);
  55.             }
  56.             imageId = Long.parseLong(imageIdStr);
  57.             if (logger.isDebugEnabled()) {
  58.                 logger.debug("doGet: imageId: " + imageId);
  59.             }
  60.         }      
  61.  
  62.         if (imageId == null || imageId.longValue() == 0) {
  63.             if (logger.isDebugEnabled()) {
  64.                 logger.debug("Resource not found: " + rawResourcePath);
  65.             }
  66.             response.sendError(HttpServletResponse.SC_NOT_FOUND);
  67.             return;
  68.         }
  69.  
  70.         try {
  71.             UserImage userImage = imageManager.getUserImage(imageId);
  72.             prepareResponse(response, userImage);
  73.  
  74.             OutputStream out = selectOutputStream(request, response);
  75.  
  76.             try {
  77.                 InputStream in = new ByteArrayInputStream(userImage.getData());
  78.                 try {
  79.                     byte[] buffer = new byte[1024];
  80.                     while (in.available() &gt; 0) {
  81.                         int len = in.read(buffer);
  82.                         out.write(buffer, 0, len);
  83.                     }
  84.                 } finally {
  85.                     in.close();
  86.                 }
  87.             } finally {
  88.                 out.close();
  89.             }
  90.         } finally {
  91.  
  92.         }
  93.     }
  94.  
  95.     private OutputStream selectOutputStream(HttpServletRequest request, HttpServletResponse response)
  96.             throws IOException {
  97.  
  98.         String acceptEncoding = request.getHeader("Accept-Encoding");
  99.         String mimeType = response.getContentType();
  100.  
  101.         if (gzipEnabled &amp;&amp; StringUtils.hasText(acceptEncoding) &amp;&amp; acceptEncoding.indexOf("gzip") &gt; -1
  102.                 &amp;&amp; compressedMimeTypes.contains(mimeType)) {
  103.             logger.debug("Enabling GZIP compression for the current response.");
  104.             return new GZIPResponseStream(response);
  105.         } else {
  106.             return response.getOutputStream();
  107.         }
  108.     }
  109.  
  110.     private void prepareResponse(HttpServletResponse response, UserImage userImage)
  111.             throws IOException {
  112.         long lastModified = -1;    
  113.  
  114.         response.setContentType(userImage.getContentType());
  115.         response.setHeader(HTTP_CONTENT_LENGTH_HEADER, Long.toString(userImage.getFilesize()));
  116.         response.setDateHeader(HTTP_LAST_MODIFIED_HEADER, lastModified);
  117.         configureCaching(response, 31556926);
  118.     }
  119.  
  120.     /**
  121.      * Set HTTP headers to allow caching for the given number of seconds.
  122.      * @param seconds number of seconds into the future that the response should be cacheable for
  123.      */
  124.     private void configureCaching(HttpServletResponse response, int seconds) {
  125.         // HTTP 1.0 header
  126.         response.setDateHeader(HTTP_EXPIRES_HEADER, System.currentTimeMillis() + seconds * 1000L);
  127.         // HTTP 1.1 header
  128.         response.setHeader(HTTP_CACHE_CONTROL_HEADER, "max-age=" + seconds);
  129.     }
  130.  
  131.     private class GZIPResponseStream extends ServletOutputStream {
  132.  
  133.         private ByteArrayOutputStream byteStream = null;
  134.  
  135.         private GZIPOutputStream gzipStream = null;
  136.  
  137.         private boolean closed = false;
  138.  
  139.         private HttpServletResponse response = null;
  140.  
  141.         private ServletOutputStream servletStream = null;
  142.  
  143.         public GZIPResponseStream(HttpServletResponse response) throws IOException {
  144.             super();
  145.             closed = false;
  146.             this.response = response;
  147.             this.servletStream = response.getOutputStream();
  148.             byteStream = new ByteArrayOutputStream();
  149.             gzipStream = new GZIPOutputStream(byteStream);
  150.         }
  151.  
  152.         public void close() throws IOException {
  153.             if (closed) {
  154.                 throw new IOException("This output stream has already been closed");
  155.             }
  156.             gzipStream.finish();
  157.  
  158.             byte[] bytes = byteStream.toByteArray();
  159.  
  160.             response.setContentLength(bytes.length);
  161.             response.addHeader("Content-Encoding", "gzip");
  162.             servletStream.write(bytes);
  163.             servletStream.flush();
  164.             servletStream.close();
  165.             closed = true;
  166.         }
  167.  
  168.         public void flush() throws IOException {
  169.             if (closed) {
  170.                 throw new IOException("Cannot flush a closed output stream");
  171.             }
  172.             gzipStream.flush();
  173.         }
  174.  
  175.         public void write(int b) throws IOException {
  176.             if (closed) {
  177.                 throw new IOException("Cannot write to a closed output stream");
  178.             }
  179.             gzipStream.write((byte) b);
  180.         }
  181.  
  182.         public void write(byte b[]) throws IOException {
  183.             write(b, 0, b.length);
  184.         }
  185.  
  186.         public void write(byte b[], int off, int len) throws IOException {
  187.             if (closed) {
  188.                 throw new IOException("Cannot write to a closed output stream");
  189.             }
  190.             gzipStream.write(b, off, len);
  191.         }
  192.  
  193.         public boolean closed() {
  194.             return (this.closed);
  195.         }
  196.  
  197.         public void reset() {
  198.             // noop
  199.         }
  200.     }
  201.  
  202.     public void setGzipEnabled(boolean gzipEnabled) {
  203.         this.gzipEnabled = gzipEnabled;
  204.     }
  205. }

This code has implemented compression on the rendered image. More importantly I have implemented caching which will be discussed when we talk about the usage.

Configuration
In order to get the ImageServlet to work, add the following entries to the Web Deployment Descriptor web.xml:

ImageDB Servlet
  1.   com.serensys.golf.webapp.servlets.ImageDBServlet
  2.   0
  3.  
  4.   ImageDB Servlet
  5.   /imagedb/*

Usage
The following code fragment shows the usage to display an image from the database

I am using the graphicImage from the JSF html library. The user.version and user.userImageId are added to the url of the userImage. The image is cached based on this name (e.g. userImage_1_23). If the userImage data does not change the image will be pulled from the cache. If the userImage data is changed the user.version property will be incremented and the userImage will be re-rendered with a new user.version. I hope this article helps in creating a solution to render images from the database.

  1. 2 Trackback(s)

  2. Apr 20, 2010: Kylie Batt
  3. May 19, 2010: elvihostnet-453

You must be logged in to post a comment.