- Overview
-
Table of Contents
- J2SE: Standard Java
- Java Windows NT Services
- Apache Velocity
- Advanced J2SE
- Bytecode Instrumentation
- Dynamic Languages and the JVM
- J2SE 1.5.0: "Tiger"
- Java SE 6
- Java 7
- Core Computer Science Principles in Java (Data Structures)
- Annotations
- Java Generics
- Java New I/O
- Java Sound
- Java Applets
- JavaFX
- Java SE Threading
- Resource Management Using Semaphores
- Java Atomic Operations
- JavaTemplate Pages
- Executing Templates with the JtpExecutor
- Java Cryptography Extensions (JCE)
- Java Database Connectivity (JDBC) API
- Jakarta Commons - Net Class Library
- Jakarta Commons HttpClient
- Apache POI
- Regular Expressions
- JavaMail
- Cool Tools
- Building an Really Simple Syndication (RSS) Java App
- Embedding JavaScript in Java with Rhino
- Logging with Log4J
- Inside Swing
- Swing Components
- SwingX
- Swing Styled Documents
- Web Rendering in Java Swing Applications
- Java Look-and-Feel Graphics Repository
- Java Media Framework
- Quicktime for Java
- Media in Java Review 2008
- External Multimedia in Java
- Graphs and Charts
- Holiday Special: Electronic Greeting Card
- Media Framework: Presenter Application
- Standard Widget Toolkit
- JFace
- Java Performance Tuning
- J2EE Performance Tuning
- Caches and Pools
- Java Caching System
- EHCache
- Java Compression and Decompression
- Obfuscating Java Applications
- Continuous Integration
- Load Testing
- Tomcat Clustering
- High Scalability with Terracotta
- Troubleshooting Production Performance Issues
- Enterprise Java Testing
- Automated Unit Testing with JUnit and Ant
- Unit Testing: Tips From The Trenches
- Custom Ant Tasks
- Extensible Markup Language (XML)
- Java Web Technologies
- Web Frameworks
- Struts 2
- Wicket
- JavaServer Faces
- Distributed Programming / RMI
- Behavior Tracking Servlet Filter
- Servlet Filters
- Building a Robust Java Server
- J2EE: Enterprise Java
- Spring
- Spring 3
- Java Design Patterns
- Model-Driven Architecture
- Enterprise Messaging with ActiveMQ
- Event-Driven Architecture
- XDoclet
- Hibernate
- Developing Standalone Database Applications with Hypersonic DB
- Project Backup
- J2EE Project: Hands-On
- Enterprise Java Beans (EJB) 3.0
- Disaster Recovery
- Java Management Extensions (JMX)
- Service-Oriented Architecture
- Web Services
- RESTful Web Services
- Web Services with Apache CXF
- Atom Syndication
- Project: Building a Web Photo Gallery
- J2ME: Micro Java
- Specialized J2ME
- Optional Packages
- Other Java Technologies
- Derivatives and Competitors
- Java, Engineered for Integration
-
Additional Resources
- The World of Java Tools
- Building Java Applications with Ant
- Managing Java Build Lifecycles with Maven
- Acceptance Testing with FitNesse
- Source Control with Subversion
- Inversion of Control and Dependency Injection
- Certification
- Roadmap: Becoming an Enterprise Java Developer
- Roadmap: Becoming an Enterprise Java Developer in 2007
- The Business of Enterprise Software
- JavaOne 2006
- JavaOne 2007
- JavaOne 2008 Wrap-Up
- JavaOne 2009 Wrap-Up
- JavaOne 2010
- JavaOne 2011
- How to Survive in a Turbulent Job Market
- How to Hire the Best Talent
- Unified Modeling Language (UML)
- Cloud Computing
- Amazon EC2 and Java
- MongoDB
- Enterprise Java in 2008 and Beyond
- Predictions for 2018
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
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
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:
-
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
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:
Use a PreparedStatement that parameterizes its columns
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):
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".
Call the HttpServletResponse's getOutputStream() to get an OutputStream to which you can write the media.
Obtain an InputStream from which to read your media.
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
