Home > Articles > Programming > Java

Java Reference Guide

Hosted by

Toggle Open Guide Table of ContentsGuide Contents

Close Table of ContentsGuide Contents

Close Table of Contents

Project: Building a Web Photo Gallery

Last updated Mar 14, 2003.

A common feature on personal Web sites is a photo gallery, showing pictures and descriptions of vacations, special events, and (my favorite) pictures of your children. There are many ways to generate a photo gallery, such as instructing Adobe Photoshop to do it for me.

One modular way to do it is programmatically. In this article, we look at building a photo gallery Web application in Java, connecting to a backend database where we store the physical images.

Whether to store images inside a database or simply point to them on your Web server file system is your decision, but the processes of loading images into a database, retrieving them out of the database, and serving them as JPEG images is challenging and where we can have more fun!

Design

This example is meant to show you some cool tricks and bring together a group of disparate technologies to work meaningfully together. But in terms of design we will keep it very simple; feel free to expand upon it on your own.

Our Web Photo Gallery is built inside of two database tables:

  • Album: we allow the categorization of images into named albums

  • Image: this table holds the actual image, a thumbnail of the image, and a description of the image

Figure 47 shows the conceptual data model for the Web Photo Gallery.

Figure 47Figure 47. Web Photo Gallery Conceptual Data Model

The relationship between Album and Image, depicted in Figure 47, is a one-to-many relationship. An album can have zero or more images in it, but an image can only belong to a single album. This relationship yields the MySQL physical data model shown in Figure 48.

Figure 48Figure 48. Web Photo Gallery Physical Data Model

The physical data model places the album_id primary key into the Image table as a foreign key; this implements the one-to-many relationship.

Prerequisites

The code that we write will work generically across Servlet containers, but because we need a common platform for the purposes of this article, we use my two favorite open source projects:

We use JBoss version 3.2.3 and MySQL 4.0. We covered downloading, installing, and configuring both of them to work together in the J2EE Project; if you're unfamiliar with these tools, you may want to refer to that discussion before proceeding.

After you have both products installed, start MySQL and launch the MySQL Control Center. Right-click on the "Databases" folder and choose "New Database". Give your new database the name "webphotogallery". Click on the SQL icon (SQL Query; or Ctrl-Q or choose Query from the File menu) and then enter the SQL script shown in Listing 1 to generate the data model.

Listing 1. MySQL Script to generate the Web Photo Gallery database tables

DROP TABLE IF EXISTS Album;
DROP TABLE IF EXISTS Image;

CREATE TABLE Album
(
  album_id            INT       NOT NULL AUTO_INCREMENT,
  album_name           VARCHAR(255)  NOT NULL,
  PRIMARY KEY (album_id),
  INDEX album_ind (album_id)
);

CREATE TABLE Image
(
  image_id            INT       NOT NULL AUTO_INCREMENT,
  album_id            INT       NOT NULL,
  image_thumb          LONGBLOB,
  image_full           LONGBLOB    NOT NULL,
  image_desc			  VARCHAR(255)  NOT NULL,
  PRIMARY KEY (image_id),
  INDEX image_ind (image_id),
  FOREIGN KEY (album_id) REFERENCES Album(album_id)
);

The thing to notice from Listing 1 is that we are asking MySQL to generate our primary keys for us. This is accomplished by marking the primary keys with the AUTO INCREMENT modifier.

NOTE

If you use another database, you will have to find the equivalent or generate the primary keys yourself. SQL Server has an "identity" column that performs the same functionality. In Oracle the same functionality is usually implementing by defining a sequence. You will have to get the next item in the sequence yourself or write a stored procedure to do it for you.

Next, to allow JBoss to work with MySQL, you need to do two things:

  1. Download and install the MySQL JDBC driver (from above) and copy it (in my case the filename was mysql-connector-java-3.0.11-stable-bin.jar) to JBoss's "server/default/lib" folder

  2. Create a database configuration file for the "webphotopallery" database and copy it to JBoss's "server/default/deploy" folder (see listing 2)

Listing 2. webphotogallery-ds.xml

<?xml version="1.0" encoding="UTF-8"?>
<datasources>
 <local-tx-datasource>
 <jndi-name>WebPhotoGalleryDS</jndi-name>
 <connection-url>jdbc:mysql://localhost:3306/webphotogallery</connection-url>
 <driver-class>org.gjt.mm.mysql.Driver</driver-class>
 <user-name>root</user-name>
 <password></password>
 </local-tx-datasource>
</datasources>

Listing 2 creates a database connection to the localhost on port 3306 (MySQL default) accessing the "webphotogallery" database and makes it available with the JNDI name "WebPhotoGalleryDS".

Make sure that you can start MySQL and Jboss, and that JBoss shows no errors in creating its database connection on startup. In a successful configuration, should see something similar to the following in your startup window:

20:56:08,156 INFO [WebPhotoGalleryDS] Bound connection factory for resource adapter for ConnectionManager 'jboss.jca:service=LocalTxCM,name=WebPhotoGalleryDS to JNDI name 'java:/WebPhotoGalleryDS'
20:56:08,156 INFO [TxConnectionManager] Started jboss.jca:service=LocalTxCM,name=WebPhotoGalleryDS

If you are having problems, troubleshoot them now. The code won't work right without this configuration running.

Inserting Images into a Database

I'm addressing three main areas of interest: reading and writing data from and to database BLOBs, serving dynamic content as JPEGs, and image manipulation.

The first thing is interacting with binary large objects, or BLOBs. Inserting and retrieving simple data, such as numbers and Strings, is simple to do by hand using any SQL editor. Consider adding a new album to our photo gallery from the MySQL Control Center Query screen:

INSERT INTO Album (album_name) VALUES ('Pictures of my son at Universal Studios');

And then verify that the data was successfully inserted, as follows:

SELECT * FROM Album

From this you should see something similar to:

album_id  album_name
--------  ----------
  1       Pictures of my son at Universal Studios

But inserting binary data is a little trickier. Databases are very capable of holding binary data in binary large object (BLOB) columns, but you do not have any way to enter binary data into one of these columns using a standard SQL editor. Rather, you have to programmatically gain access to the binary data and write it into the column. JDBC provides the Blob type to help us view BLOB data. The two keys to inserting binary data into a BLOB are:

  1. Use a PreparedStatement that parameterizes its columns

  2. Use the PreparedStatement's setBinaryStream() method in conjunction with the other Java I/O classes

Listing 3 shows code that opens an image file and inserts it into our database.

Listing 3. LoadImage.java

package com.javasrc.webphotogallery;

import java.io.*;
import java.sql.*;

public class LoadImage
{
  public static void main( String[] args ) 
  {
    if( args.length < 3 )
    {
      System.out.println( "Usage: LoadImage <image-filename> <album-id> <desc>" );
      System.exit( 0 );
    }

    String filename = args[ 0 ];
    int albumId = Integer.parseInt( args[ 1 ] );
    String desc = args[ 2 ];
    Connection conn = null;
    PreparedStatement ps = null;
    try
    {
      Class.forName( "org.gjt.mm.mysql.Driver" );
      String url = "jdbc:mysql://localhost:3306/webphotogallery";
      String username = "root";
      String password = "";
      conn = DriverManager.getConnection( url, username, password );
      ps = conn.prepareStatement( 
        "INSERT INTO Image (album_id, image_desc, image_full) VALUES( ?, ?, ? )" );

      ps.setInt( 1, albumId );
      ps.setString( 2, desc );

      // Insert the image into the second Blob
      File image = new File( filename );
      FileInputStream fis = new FileInputStream( image );
      ps.setBinaryStream( 3, fis, ( int )image.length() );

      // Execute the INSERT
      int count = ps.executeUpdate();
      System.out.println( "Rows inserted: " + count );
    }
    catch( Exception e )
    {
      e.printStackTrace();
    }
    finally
    {
      try 
      {
        if( ps != null ) ps.close();
        if( conn != null ) conn.close();
      }
      catch( Exception ee )
      {
        ee.printStackTrace();
      }
    }
  }
}

We use a PreparedStatement to insert the album_id, image_desc, and image_full (BLOB) values. A PreparedStatement allows you to embed question marks in your SQL and then use one of its many "set" methods to supply a value for that parameter.

The first two columns are satisfied with calls to setInt() and setString(), respectively. In order to insert data into a Blob, we have to obtain an InputStream that points to the binary data, and then pass that InputStream to the PreparedStatement's setBinaryStream() method. To further complicate matters, we have to tell the PreparedStatement how much data to read. We can create a FileInputStream that reads the contents of the specified file as follows:

File image = new File( filename );
FileInputStream fis = new FileInputStream( image );

Then we pass that FileInputStream to the PreparedStatement along with the length of the file:

ps.setBinaryStream( 3, fis, ( int )image.length() );

Finally, we call executeUpdate() to insert the data into the database. If it is successful, they we should see 1 record inserted.

To execute the LoadImage class, you need to include the MySQL JDBC JAR file in your CLASSPATH:

set CLASSPATH=.;<path-to-mysql-jdbc-drive>\mysql-connector-java-3.0.11-stable-bin.jar

And then execute it as follows:

java com.javasrc.webphotogallery.LoadImage myfile.jpg 1 "A picture of my son with Jimmy Neutron"

If you are successful, you'll see the following output:

Rows inserted: 1

Retrieving an Image from a Database

Retrieving binary data from a database is actually a little bit easier than inserting it. You need to perform the following steps:

  • Execute a query that contains the BLOB, which returns a java.sql.ResultSet

  • Call the ResultSet's getBlob() method to obtain the java.sql.Blob instance for the appropriate column

  • Call the Blob's getBinaryStream() method to retrieve a java.io.InputStream that you can read as easily as if you opened a file

For example:

// Execute the query
PreparedStatement ps = conn.prepareStatement( "SELECT image_full FROM Image WHERE image_id = ?" );
ps.setInt( 1, imageId );
ResultSet rs = ps.executeQuery();

// Advance the ResultSet to the first record
rs.next();

// Get the Blob
Blob blob = rs.getBlob( 1 );

// Get an InputStream from the Blob
InputStream in = blob.getBinaryStream();

// Use the InputStream...

Serving an Image from a Servlet

Now that we can retrieve an image from a database column as an InputStream, we can easily read from that InputStream and write it out to a Servlet's HttpServletResponse's OutputStream.

Most of the time, when we build Servlets, we either (1) add objects to the HttpServletRequest and forward the request to a JavaServer Page (JSP) for presentation or (2) obtain a Writer to the HttpServletResponse and write HTML or XML back to the requester. Serving images is very similar to the latter use case, with the difference that instead of sending HTML content, we send JPEG content. Or, to be technical, we send "image/jpeg" or "image/gif" content instead of "text/hmtl" or "text/xml" content.

Perform the following steps to serve JPEGs from a Servlet (you can generalize these steps to serve any media type):

  1. Call the HttpServletResponse's setContentType() method, passing it the media type that you are serving. In this case, we pass the following String: "image/jpeg".

  2. Call the HttpServletResponse's getOutputStream() to get an OutputStream to which you can write the media.

  3. Obtain an InputStream from which to read your media.

  4. Read data from your InputStream and write it out to your OutputStream.

Listing 4 shows the code for our ImageServerServlet.

Listing 4. ImageServerServlet.java

package com.javasrc.webphotogallery.web;

// Import the JNDI classes
import java.io.*;
import javax.naming.*;

// Import the Servlet classes
import javax.servlet.*;
import javax.servlet.http.*;

// Import our database classes
import java.sql.*;
import javax.sql.*;

/**
 * Returns the requested JPG from the database; input parameters are:
 * 
 * @param imageid The id of the image to return
 * @param size   "full" or "thumbnail", default is "full"
 */
public class ImageServerServlet extends HttpServlet
{
  /**
   * Cache a reference to the datasource object that we will use later to obtain
   * a connection to the database
   */
  private transient DataSource datasource = null;

  public void init() throws ServletException
  {
    try
    {
      InitialContext ic = new InitialContext();
      this.datasource = ( DataSource )ic.lookup( "java:/WebPhotoGalleryDS" );
    }
    catch( Exception e )
    {
      throw new ServletException( e );
    }
  }

  public void service( HttpServletRequest req, HttpServletResponse res ) throws ServletException
  {
    String imageIdStr = req.getParameter( "imageid" );
    if( imageIdStr == null )
    {
      throw new ServletException( "Image Id Required" );
    }
 
    boolean fullImage = true;
    String size = req.getParameter( "size" );
    if( size != null && size.equalsIgnoreCase( "thumbnail" ) )
    {
      fullImage = false;
    }

    Connection conn = null;
    PreparedStatement ps = null;
    ResultSet rs = null;
    try
    {
      // Get a datbase connection
      conn = getDBConnection();

      // Build a Prepared Statement
      if( fullImage )
      {
        ps = conn.prepareStatement( "SELECT image_full FROM Image WHERE image_id = ?" );
      }
      else
      {
        ps = conn.prepareStatement( "SELECT image_thumb FROM Image WHERE image_id = ?" );
      }
      ps.setInt( 1, Integer.parseInt( imageIdStr ) );

      // Execute the query
      rs = ps.executeQuery();
      if( !rs.next() )
      {
        throw new ServletException( "There is no image defined with id: " + imageIdStr );
      }

      // Get the image Blob from the database
      Blob blob = rs.getBlob( 1 );
      InputStream in = blob.getBinaryStream();

      // Output the blob to the HttpServletResponse
      res.setContentType( "image/jpeg" );
      BufferedOutputStream out = new BufferedOutputStream( res.getOutputStream() );
      byte by[] = new byte[ 32768 ];
      int index = in.read( by, 0, 32768 );
      while ( index != -1 )
      {
        out.write( by, 0, index );
        index = in.read( by, 0, 32768 );
      }
      out.flush();
    }
    catch( Exception e )
    {
      e.printStackTrace();
      throw new ServletException( e );
    }
    finally
    {
      try
      {
        if( rs != null ) rs.close();
        if( ps != null ) ps.close();
        if( conn != null ) conn.close();
      }
      catch( SQLException sqlee ){}
    }
  }

  protected Connection getDBConnection()
  {
    try
    {
      return datasource.getConnection();
    } 
    catch( SQLException se )
    {
      se.printStackTrace();
    }
    return null;
  }
}

The core image serving functionality in listing 4 is:

      // Get the image Blob from the database
      Blob blob = rs.getBlob( 1 );
      InputStream in = blob.getBinaryStream();

      // Output the blob to the HttpServletResponse
      res.setContentType( "image/jpeg" );
      BufferedOutputStream out = new BufferedOutputStream( res.getOutputStream() );
      byte by[] = new byte[ 32768 ];
      int index = in.read( by, 0, 32768 );
      while ( index != -1 )
      {
        out.write( by, 0, index );
        index = in.read( by, 0, 32768 );
      }
      out.flush();

Just as in the previous section, we obtained an InputStream from which to read data from the BLOB. Then we set the content type of our response to "image/jpeg" and obtained an OutputStream to write back to the response. We wrapped the OutputStream with a BufferedOutputStream to improve performance. Finally, we created a 32 kilobyte byte array that we use to read data from our BLOB InputStream and write it out to our BufferedOutputStream. When we're done (the read() method returns -1), we flush the buffer back to the requester.

Another point to note in this code is how it obtains its database connection. Because we configured the connection to the database back in listing 2, JBoss created a connection pool for us to MySQL and bound that connection pool to the JNDI name: "java:/WebPhotoGalleryDS". When the Servlet is initialized, we create an InitialContext (our main interface to querying the Java Naming and Directory Interface (JNDI) server) and ask it to find the resource bound to "java:/WebPhotoGalleryDS". When database connection pools are used in J2EE environments, they are returned as javax.sql.DataSource instances, so we cast the Object returned by lookup() to a DataSource and then cache it in the Servlet for later use.

When the user makes a request from our Servlet, its service() method is invoked. We make a call to the DataSource's getConnection() method to obtain a java.sql.Connection from the connection pool. Once we have a Connection instance, the rest of the JDBC interactions are the same as if we used it outside the context of a J2EE connection pool.

The final thing you need to use this Servlet is a Web Deployment Descriptor (web.xml file). Listing 5 shows the contents of our web.xml file.

Listing 5. web.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app PUBLIC '-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN' 'http://java.sun.com/dtd/web-app_2_3.dtd'>

<web-app>
  <servlet>
    <servlet-name>ImageServerServlet</servlet-name>
    <servlet-class>com.javasrc.webphotogallery.web.ImageServerServlet</servlet-class>
  </servlet>
  
  <servlet-mapping>
    <servlet-name>ImageServerServlet</servlet-name>
    <url-pattern>/image</url-pattern>
  </servlet-mapping>
</web-app>

In this case, we mapped the URL pattern "/image" in our web application context to be directed to our Servlet. To build our Web Archive (WAR file), build a directory structure like the following:

/myproject/
/myproject/WEB-INF/
/myproject/WEB-INF/web.xml
/myproject/WEB-INF/classes/
/myproject/WEB-INF/classes/com/
/myproject/WEB-INF/classes/com/javasrc/
/myproject/WEB-INF/classes/com/javasrc/webphotogallery/
/myproject/WEB-INF/classes/com/javasrc/webphotogallery/web/
/myproject/WEB-INF/classes/com/javasrc/webphotogallery/web/ImageServerServlet.class

And from the "myproject" folder execute the following command:

jar cf ../webphotogallery.war *

This uses the jar tool provided with your JDK to create a Java archive named webphotogallery.war in the parent directory, containing all folders and subfolders from the myproject folder. You can verify the contents of the WAR file by executing the following command:

jar tf webphotogallery.war

My WAR file reads as follows:

META-INF/
META-INF/MANIFEST.MF
WEB-INF/
WEB-INF/classes/
WEB-INF/classes/com/
WEB-INF/classes/com/javasrc/
WEB-INF/classes/com/javasrc/webphotogallery/
WEB-INF/classes/com/javasrc/webphotogallery/web/
WEB-INF/classes/com/javasrc/webphotogallery/web/ImageServerServlet.class
WEB-INF/web.xml

Copy this file to JBoss's "server/default/deploy" folder. Then you can access it through the following URL:

http://localhost:8080/webphotogallery/image?imageid=1

Make sure that you have manually created an album (INSERT statement) and have inserted an image using the LoadImage class.

Summary

Thus far, we have seen how to insert an image into a Binary Large OBject (BLOB) column of a database, retrieve the image out of the BLOB field, and serve that image as a JPEG from inside a Servlet.

In the next article, I'll discuss a better way to load images into the database and how to programmatically generate the image's thumbnail version. I'll conclude the series by setting up a Web user interface around these capabilities, so that we can browse albums, thumbnails, and images.

Online Resources

Sample jGuru article that demonstrates how to work with BLOBs