7.3. Making the XML DynamicAt this point in the process, we have specified what each web page looks like, the XML data for each page, and the XSLT stylesheets to perform the necessary transformations. The next step is to figure out where the XML actually comes from. During the design and prototyping process, all XML data is created as a collection of static text files. This makes development of the XSLT stylesheets much easier, because the stylesheet authors can see results immediately without waiting for the back-end business logic and database access code to be created. In the real system, static XML will not meet our requirements. We need the ability to extract data from a relational database and convert it into XML on the fly, as each page is requested. This makes the application "live," making updates to the database immediately visible to users. To the XSLT stylesheet developer, this is a moot point. The XSLT transformations work the same, regardless of whether the XML data came from a flat file, a relational database, or any other source. 7.3.1. Domain ClassesA domain class is a Java class that represents something in the problem domain. That's a fancy way to describe a class that represents the underlying problem you are trying to solve. In this example, we need to model the discussion forum as a series of Java classes to provide a buffer between the XML and the underlying relational database. In addition to representing data about the discussion forum, these Java classes can contain business logic. Figure 7-6 contains a UML diagram of the classes found in the com.oreilly.forum.domain package. These classes do not contain any database access code, nor do they have any XML capability. Instead, they are simply data structures with a few key pieces of functionality. This makes it possible, for example, to rip out the relational database and replace it with some other back-end data source without changing to the XML generation logic. Figure 7-6. Key domain classesBoardSummary, MessageSummary, and Message are the key interfaces that describe the basic discussion forum capabilities. For each interface, an associated Impl class provides a basic implementation that contains get and set methods, which are not shown here. The MonthYear, DayMonthYear, and DateUtil classes are designed to represent and manipulate dates in an easy way and are listed in Appendix B, "JAXP API Reference". Finally, the MessageTree class encapsulates some business logic to sort a collection of messages into a hierarchical tree based on message replies and creation dates. The BoardSummary interface, shown in Example 7-11, contains data that will eventually be used to build the discussion forum home page. Example 7-11. BoardSummary.javapackage com.oreilly.forum.domain; import java.util.Iterator; /** * Information about a message board. */ public interface BoardSummary { /** * @return a unique ID for this board. */ long getID( ); /** * @return a name for this board. */ String getName( ); /** * @return a description for this board. */ String getDescription( ); /** * @return an iterator of <code>MonthYear</code> objects. */ Iterator getMonthsWithMessages( ); } By design, the BoardSummary interface is read-only. This is an important feature because it means that once an instance of this class is extracted from the back-end data source, a programmer cannot accidentally call a set method only to discover later that the updates were not saved in the database. Technically, the client of this class could retrieve an Iterator of months with messages and then call the remove( ) method on the Iterator instance. Although we could take steps to make instances of this interface truly immutable, such efforts are probably overkill. An early decision made in the design of the discussion forum was to assign a unique long identifier to each domain object. These identifiers have absolutely no meaning other than to identify objects uniquely, which will make the SQL queries much simpler later on.[29] This technique also makes it easy to reference objects from hyperlinks in the XHTML, because a simple identifier can be easily converted to and from a string representation.
The next interface, shown in Example 7-12, provides a summary for an individual message. Example 7-12. MessageSummary.javapackage com.oreilly.forum.domain; import java.util.*; /** * Basic information about a message, not including the message text. */ public interface MessageSummary extends Comparable { /** * @return the ID of the message that this one is a reply to, or * -1 if none. */ long getInReplyTo( ); /** * @return the unique ID of this message. */ long getID( ); /** * @return when this message was created. */ DayMonthYear getCreateDate( ); /** * @return the board that this message belongs to. */ BoardSummary getBoard( ); /** * @return the subject of this message. */ String getSubject( ); /** * The author Email can be 80 characters. */ String getAuthorEmail( ); } The only thing missing from the MessageSummary interface is the actual message text. The Message interface, which extends from MessageSummary, adds the getText( ) method. This interface is shown in Example 7-13. Example 7-13. Message.javapackage com.oreilly.forum.domain; /** * Represent a message, including the text. */ public interface Message extends MessageSummary { /** * @return the text of this message. */ String getText( ); } The decision to keep the message text in a separate interface was driven by a prediction that performance could be dramatically improved. Consider a web page that shows a hierarchical view of all messages for a given month. This page may contain hundreds of messages, displaying key information found in the MessageSummary interface. But the text of each message could contain thousands of words, so it was decided that the text should be retrieved later when a message is displayed in its entirety. For this page, an instance of a class that implements Message can be created. These are the sorts of design decisions that cannot be made in complete isolation. Regardless of how cleanly XSLT and XML separate the presentation from the underlying data model, heavily used web pages should have some influence on design decisions made on the back end. The trick is to avoid falling into the trap of focusing too hard on early optimization at the expense of a clean design. In this case, the potential for large numbers of very long messages was significant enough to warrant a separate interface for Message. The three reference implementation classes are MessageImpl, Message-SummaryImpl, and BoardSummaryImpl. These are basic Java classes that hold data and are listed in Appendix B, "JAXP API Reference". The JDBC data adapter layer (see Section 7.3.2, "Data Adapter Layer") will create and return new instances of these classes, which implement the interfaces in this package. If creating a new back-end data source in the future, it is possible to reuse these classes or write brand new classes that implement the appropriate interfaces. The final class in this package, MessageTree , is listed in Example 7-14. Example 7-14. MessageTree.javapackage com.oreilly.forum.domain; import java.util.*; /** * Arranges a collection of MessageSummary objects into a tree. */ public class MessageTree { private List topLevelMsgs = new ArrayList( ); // map ids to MessageSummary objects private Map idToMsgMap = new HashMap( ); // map reply-to ids to lists of MessageSummary objects private Map replyIDToMsgListMap = new HashMap( ); /** * Construct a new message tree from an iterator of MessageSummary * objects. */ public MessageTree(Iterator messages) { while (messages.hasNext( )) { // store each message in a map for fast retrieval by ID MessageSummary curMsg = (MessageSummary) messages.next( ); this.idToMsgMap.put(new Long(curMsg.getID( )), curMsg); // build the inverted map that maps reply-to IDs to // lists of messages Long curReplyID = new Long(curMsg.getInReplyTo( )); List replyToList = (List) this.replyIDToMsgListMap.get(curReplyID); if (replyToList == null) { replyToList = new ArrayList( ); this.replyIDToMsgListMap.put(curReplyID, replyToList); } replyToList.add(curMsg); } // build the list of top-level messages. A top-level message // fits one of the following two criteria: // - its reply-to ID is -1 // - its reply-to ID was not found in the list of messages. This // occurs when a message is a reply to a previous month's message Iterator iter = this.replyIDToMsgListMap.keySet().iterator( ); while (iter.hasNext( )) { Long curReplyToID = (Long) iter.next( ); if (curReplyToID.longValue( ) == -1 || !this.idToMsgMap.containsKey(curReplyToID)) { List msgsToAdd = (List) this.replyIDToMsgListMap.get(curReplyToID); this.topLevelMsgs.addAll(msgsToAdd); } } Collections.sort(this.topLevelMsgs); } public Iterator getTopLevelMessages( ) { return this.topLevelMsgs.iterator( ); } /** * @return an iterator of MessageSummary objects that are replies * to the specified message. */ public Iterator getReplies(MessageSummary msg) { List replies = (List) this.replyIDToMsgListMap.get( new Long(msg.getID( ))); if (replies != null) { Collections.sort(replies); return replies.iterator( ); } else { return Collections.EMPTY_LIST.iterator( ); } } } The MessageTree class helps organize a list of messages according to threads of discussion. If you look back at the code for MessageSummary, you will see that each message keeps track of the message that it is in reply to: public interface MessageSummary extends Comparable { ... long getInReplyTo( ); ... } If the message is a top-level message, then the reply-to id is -1. Otherwise, it always refers to some other message. Since a message does not have a corresponding method to retrieve a list of replies, the MessageTree class must build this list for each message. This leads to the three data structures found in the MessageTree class: private List topLevelMsgs = new ArrayList( ); private Map idToMsgMap = new HashMap( ); private Map replyIDToMsgListMap = new HashMap( ); When the MessageTree is constructed, it is given an Iterator of all messages in a month. From this Iterator, the idToMsgMap data structure is built. All messages are stored in idToMsgMap, which is used for rapid retrieval based on message ids. While building the idToMsgMap, the constructor also builds the replyIDToMsgListMap. The keys in this map are reply-to ids, and the values are lists of message ids. In other words, each key maps to a list of replies. After the first two data structures are built, the list of top-level messages is built. This is accomplished by looping over all keys in the idToMsgMap and then looking for messages that have a reply-to id of -1. In addition, messages whose reply-to id could not be located are also considered to be top-level messages. This occurs when a message is in reply to a previous month's message. All of this code can be seen in the MessageTree constructor. 7.3.2. Data Adapter LayerBridging the gap between an object-oriented class library and a physical database is often quite difficult. Enterprise JavaBeans (EJB) can be used for this purpose. However, this makes it extremely hard to deploy the discussion forum at a typical web hosting service. By limiting the application to servlets and a relational database, it is possible to choose from several ISPs that support both servlets and JDBC access to databases such as MySQL. In addition to the software constraints found at most web hosting providers, design flexibility is another consideration. Today, direct access to a MySQL database may be the preferred approach. In the future, a full EJB solution with some other database may be desired. Or, we may choose to store messages in flat files instead of any database at all. All of these capabilities are achieved by using an abstract class called DataAdapter. This class is shown in Figure 7-7 along with several related classes. Figure 7-7. Data adapter designThe DataAdapter class defines an interface to some back-end data source. As shown in the class diagram, FakeDataAdapter and JdbcDataAdapter are subclasses. These implement the data tier using flat files and relational databases, respectively. It is easy to imagine someone creating an EJBDataAdapter at some point in the future. ForumConfig is used to determine which subclass of DataAdapter to instantiate, and the DBUtil class encapsulates a few commonly used JDBC functions. The source code for ForumConfig is shown in Example 7-15. This is a simple class that places configurable application settings in a single place. As shown later in this chapter, all configurable settings are stored in the servlet's deployment descriptor, so they do not have to be hardcoded. The first thing the servlet does is read the values and store them in ForumConfig.[30]
Example 7-15. ForumConfig.javapackage com.oreilly.forum; /** * Define application-wide configuration information. The Servlet * must call the setValues( ) method before any of the get * methods in this class can be used. */ public class ForumConfig { // maximum sizes of various fields in the database public static final int MAX_BOARD_NAME_LEN = 80; public static final int MAX_BOARD_DESC_LEN = 255; public static final int MAX_MSG_SUBJECT_LEN = 80; public static final int MAX_EMAIL_LEN = 80; private static String jdbcDriverClassName; private static String databaseURL; private static String adapterClassName; public static void setValues( String jdbcDriverClassName, String databaseURL, String adapterClassName) { ForumConfig.jdbcDriverClassName = jdbcDriverClassName; ForumConfig.databaseURL = databaseURL; ForumConfig.adapterClassName = adapterClassName; } /** * @return the JDBC driver class name. */ public static String getJDBCDriverClassName( ) { return ForumConfig.jdbcDriverClassName; } /** * @return the JDBC database URL. */ public static String getDatabaseURL( ) { return ForumConfig.databaseURL; } /** * @return the data adapter implementation class name. */ public static String getAdapterClassName( ) { return ForumConfig.adapterClassName; } private ForumConfig( ) { } } The DataException class is a very basic exception that indicates a problem with the back-end data source. It hides database-specific exceptions from the client, leaving the door open for nondatabase implementations in the future. For example, an EJB tier could be added, but the EJBs would throw RemoteException and EJBException instead of SQLException. Therefore, whenever a specific exception is thrown, it is wrapped in an instance of DataException before being propogated to the caller. The source code for DataException is found in Appendix B, "JAXP API Reference". The code for DataAdapter, shown in Example 7-16, demonstrates how each method throws DataException. This class is the centerpiece of the "data abstraction" layer, insulating the domain classes from the underlying database implementation. Example 7-16. DataAdapter.javapackage com.oreilly.forum.adapter; import com.oreilly.forum.*; import com.oreilly.forum.domain.*; import java.util.*; /** * Defines an interface to a data source. */ public abstract class DataAdapter { private static DataAdapter instance; /** * @return the singleton instance of this class. */ public static synchronized DataAdapter getInstance( ) throws DataException { if (instance == null) { String adapterClassName = ForumConfig.getAdapterClassName( ); try { Class adapterClass = Class.forName(adapterClassName); instance = (DataAdapter) adapterClass.newInstance( ); } catch (Exception ex) { throw new DataException("Unable to instantiate " + adapterClassName); } } return instance; } /** * @param msgID must be a valid message identifier. * @return the message with the specified id. * @throws DataException if msgID does not exist or a database * error occurs. */ public abstract Message getMessage(long msgID) throws DataException; /** * Add a reply to an existing message. * * @throws DataException if a database error occurs, or if any * parameter is illegal. */ public abstract Message replyToMessage(long origMsgID, String msgSubject, String authorEmail, String msgText) throws DataException; /** * Post a new message. * * @return the newly created message. * @throws DataException if a database error occurs, or if any * parameter is illegal. */ public abstract Message postNewMessage(long boardID, String msgSubject, String authorEmail, String msgText) throws DataException; /** * If no messages exist for the specified board and month, return * an empty iterator. * @return an iterator of <code>MessageSummary</code> objects. * @throws DataException if the boardID is illegal or a database * error occurs. */ public abstract Iterator getAllMessages(long boardID, MonthYear month) throws DataException; /** * @return an iterator of all <code>BoardSummary</code> objects. */ public abstract Iterator getAllBoards( ) throws DataException; /** * @return a board summary for the given id. * @throws DataException if boardID is illegal or a database * error occurs. */ public abstract BoardSummary getBoardSummary(long boardID) throws DataException; } DataAdapter consists of abstract methods and one static method called getInstance( ). This implements a singleton design pattern, returning an instance of a DataAdapter subclass.[31] The actual return type is specified in the ForumConfig class, and Java reflection APIs are used to instantiate the object:
String adapterClassName = ForumConfig.getAdapterClassName( ); try { Class adapterClass = Class.forName(adapterClassName); instance = (DataAdapter) adapterClass.newInstance( ); } catch (Exception ex) { throw new DataException("Unable to instantiate " + adapterClassName); } All remaining methods are abstract and are written in terms of interfaces defined in the com.oreilly.forum.domain package. For example, a message can be retrieved by its ID: public abstract Message getMessage(long msgID) throws DataException; By writing this code in terms of the Message interface, a future programmer could easily write a new class that implements Message in a different way. Throughout the DataAdapter class, a DataException occurs when an id is invalid, or when the underlying database fails. The downloadable discussion forum implementation comes with a "fake" implementation of DataAdapter as well as a JDBC-based implementation. The fake implementation is listed in Appendix B, "JAXP API Reference". The database implementation has been tested on Microsoft Access as well as MySQL and should work on just about any relational database that includes a JDBC driver. Figure 7-8 shows the physical database design that the JdbcDataAdapter class uses. Figure 7-8. Database designThe database is quite simple. Each table has an id column that defines a unique identifier and primary key for each row of data. Message.inReplyToID contains a reference to another message that this one is in reply to, or -1 if this is a top-level message. The create date for each message is broken down into month, day, and year. Although the application could store the date and time in some other format, this approach makes it really easy to issue queries such as: SELECT subject FROM Message WHERE createMonth=3 AND createYear=2001 The Message.boardID column is a foreign key that identifies which board a message belongs to. The Message.msgText column can contain an unlimited amount of text, while the remaining fields all contain fixed-length text. If you are using MySQL, Example 7-17 shows a "dump" file that can be used to easily recreate the database using the import utility that comes with the database. Example 7-17. MySQL dump# MySQL dump 8.8 # # Host: localhost Database: forum #-------------------------------------------------------- # Server version 3.23.23-beta # # Table structure for table 'board' # CREATE TABLE board ( id bigint(20) DEFAULT '0' NOT NULL, name char(80) DEFAULT '' NOT NULL, description char(255) DEFAULT '' NOT NULL, PRIMARY KEY (id) ); # # Dumping data for table 'board' # INSERT INTO board VALUES (0,'XSLT Basics', 'How to create and use XSLT stylesheets and processors'); INSERT INTO board VALUES (1,'JAXP Programming Techniques','How to use JAXP 1.1'); # # Table structure for table 'message' # CREATE TABLE message ( id bigint(20) DEFAULT '0' NOT NULL, inReplyToID bigint(20) DEFAULT '0' NOT NULL, createMonth int(11) DEFAULT '0' NOT NULL, createDay int(11) DEFAULT '0' NOT NULL, createYear int(11) DEFAULT '0' NOT NULL, boardID bigint(20) DEFAULT '0' NOT NULL, subject varchar(80) DEFAULT '' NOT NULL, authorEmail varchar(80) DEFAULT '' NOT NULL, msgText text DEFAULT '' NOT NULL, PRIMARY KEY (id), KEY inReplyToID (inReplyToID), KEY createMonth (createMonth), KEY createDay (createDay), KEY boardID (boardID) ); The DBUtil class, shown in Example 7-18, consists of utility functions that make it a little easier to work with relational databases from Java code. Example 7-18. DBUtil.javapackage com.oreilly.forum.jdbcimpl; import java.io.*; import java.sql.*; import java.util.*; /** * Helper methods for relational database access using JDBC. */ public class DBUtil { // a map of table names to maximum ID numbers private static Map tableToMaxIDMap = new HashMap( ); /** * Close a statement and connection. */ public static void close(Statement stmt, Connection con) { if (stmt != null) { try { stmt.close( ); } catch (Exception ignored1) { } } if (con != null) { try { con.close( ); } catch (Exception ignored2) { } } } /** * @return a new Connection to the database. */ public static Connection getConnection(String dbURL) throws SQLException { // NOTE: implementing a connection pool would be a worthy // enhancement return DriverManager.getConnection(dbURL); } /** * Close any connections that are still open. The Servlet will * call this method from its destroy( ) method. */ public static void closeAllConnections( ) { // NOTE: if connection pooling is ever implemented, close // the connections here. } /** * Store a long text field in the database. For example, a message's * text will be quite long and cannot be stored using JDBC's * setString( ) method. */ public static void setLongString(PreparedStatement stmt, int columnIndex, String data) throws SQLException { if (data.length( ) > 0) { stmt.setAsciiStream(columnIndex, new ByteArrayInputStream(data.getBytes( )), data.length( )); } else { // this 'else' condition was introduced as a bug fix. It was // discovered that the 'setAsciiStream' code shown above // caused MS Access throws a "function sequence error" // when the string was zero length. This code now works. stmt.setString(columnIndex, ""); } } /** * @return a long text field from the database. */ public static String getLongString(ResultSet rs, int columnIndex) throws SQLException { try { InputStream in = rs.getAsciiStream(columnIndex); if (in == null) { return ""; } byte[] arr = new byte[250]; StringBuffer buf = new StringBuffer( ); int numRead = in.read(arr); while (numRead != -1) { buf.append(new String(arr, 0, numRead)); numRead = in.read(arr); } return buf.toString( ); } catch (IOException ioe) { ioe.printStackTrace( ); throw new SQLException(ioe.getMessage( )); } } /** * Compute a new unique ID. It is assumed that the specified table * has a column named 'id' of type 'long'. It is assumed that * that all parts of the program will use this method to compute * new IDs. * @return the next available unique ID for a table. */ public static synchronized long getNextID(String tableName, Connection con) throws SQLException { Statement stmt = null; try { // if a max has already been retrieved from this table, // compute the next id without hitting the database if (tableToMaxIDMap.containsKey(tableName)) { Long curMax = (Long) tableToMaxIDMap.get(tableName); Long newMax = new Long(curMax.longValue( ) + 1L); tableToMaxIDMap.put(tableName, newMax); return newMax.longValue( ); } stmt = con.createStatement( ); ResultSet rs = stmt.executeQuery( "SELECT MAX(id) FROM " + tableName); long max = 0; if (rs.next( )) { max = rs.getLong(1); } max++; tableToMaxIDMap.put(tableName, new Long(max)); return max; } finally { // just close the statement close(stmt, null); } } } DBUtil has a private class field called tableToMaxIDMap that keeps track of the largest unique id found in each table. This works in conjunction with the getNextID( ) method, which returns the next available unique id for a given table name. By keeping the unique ids cached in the Map, the application reduces the required database hits. It should be noted that this approach is likely to fail if anyone manually adds a new id to the database without consulting this method. The close( ) method is useful because nearly everything done with JDBC requires the programmer to close a Statement and Connection. This method should always be called from a finally block, which is guaranteed to be called regardless of whether or not an exception was thrown. For example: Connection con = null; Statement stmt = null; try { // code to create the Connection and Statement ... // code to access the database ... } finally { DBUtil.close(stmt, con); } If JDBC resources are not released inside of a finally block, it is possible to accidentally leave Connections open for long periods of time. This is problematic because database performance can suffer, and some databases limit the number of concurrent connections. Although connection pooling is not supported in this version of the application, DBUtil does include the following method: public static Connection getConnection(String dbURL) In a future version of the class, it will be very easy to have this method return a Connection instance from pool, rather than creating a new instance with each call. Additionally, the DBUtil.close( ) method could return the Connection back to the pool instead of actually closing it. These are left as future considerations to keep things reasonably sized for the book. The setLongString( ) and getLongString( ) methods are used for setting and retrieving text for messages. Since this text may be extremely long, it cannot be stored in the same way that shorter strings are stored. In some databases, these are referred to as CLOB columns. MS Access uses the MEMO type, while MySQL uses the TEXT data type. Since this is an area where databases can be implemented differently, the code is placed into the DBUtil class for consistency. If a special concession has to be made for a particular database, it can be made in one place rather than in every SQL statement throughout the application. Finally, the JdbcDataAdapter class is presented in Example 7-19. This is the relational database implementation of the DataAdapter class and should work with just about any relational database. Example 7-19. JdbcDataAdapter.javapackage com.oreilly.forum.jdbcimpl; import com.oreilly.forum.*; import com.oreilly.forum.adapter.*; import com.oreilly.forum.domain.*; import java.sql.*; import java.util.*; /** * An implementation of the DataAdapter that uses JDBC. */ public class JdbcDataAdapter extends DataAdapter { private static String dbURL = ForumConfig.getDatabaseURL( ); /** * Construct the data adapter and load the JDBC driver. */ public JdbcDataAdapter( ) throws DataException { try { Class.forName(ForumConfig.getJDBCDriverClassName( )); } catch (Exception ex) { ex.printStackTrace( ); throw new DataException("Unable to load JDBC driver: " + ForumConfig.getJDBCDriverClassName( )); } } /** * @param msgID must be a valid message identifier. * @return the message with the specified id. * @throws DataException if msgID does not exist or a database * error occurs. */ public Message getMessage(long msgID) throws DataException { Connection con = null; Statement stmt = null; try { con = DBUtil.getConnection(dbURL); stmt = con.createStatement( ); ResultSet rs = stmt.executeQuery( "SELECT inReplyToID, createDay, createMonth, createYear, " + "boardID, subject, authorEmail, msgText " + "FROM Message WHERE id=" + msgID); if (rs.next( )) { long inReplyToID = rs.getLong(1); int createDay = rs.getInt(2); int createMonth = rs.getInt(3); int createYear = rs.getInt(4); long boardID = rs.getLong(5); String subject = rs.getString(6); String authorEmail = rs.getString(7); String msgText = DBUtil.getLongString(rs, 8); BoardSummary boardSummary = this.getBoardSummary(boardID, stmt); return new MessageImpl(msgID, new DayMonthYear(createDay, createMonth, createYear), boardSummary, subject, authorEmail, msgText, inReplyToID); } else { throw new DataException("Illegal msgID"); } } catch (SQLException sqe) { sqe.printStackTrace( ); throw new DataException(sqe.getMessage( )); } finally { DBUtil.close(stmt, con); } } /** * Add a reply to an existing message. * * @throws DataException if a database error occurs, or if any * parameter is illegal. */ public Message replyToMessage(long origMsgID, String msgSubject, String authorEmail, String msgText) throws DataException { Message inReplyToMsg = this.getMessage(origMsgID); return insertMessage(inReplyToMsg.getBoard( ), origMsgID, msgSubject, authorEmail, msgText); } /** * Post a new message. * * @return the newly created message. * @throws DataException if a database error occurs, or if any * parameter is illegal. */ public Message postNewMessage(long boardID, String msgSubject, String authorEmail, String msgText) throws DataException { BoardSummary board = this.getBoardSummary(boardID); return insertMessage(board, -1, msgSubject, authorEmail, msgText); } /** * If no messages exist for the specified board and month, return * an empty iterator. * @return an iterator of <code>MessageSummary</code> objects. * @throws DataException if the boardID is illegal or a database * error occurs. */ public Iterator getAllMessages(long boardID, MonthYear month) throws DataException { List allMsgs = new ArrayList( ); Connection con = null; Statement stmt = null; try { con = DBUtil.getConnection(dbURL); stmt = con.createStatement( ); BoardSummary boardSum = this.getBoardSummary(boardID, stmt); ResultSet rs = stmt.executeQuery( "SELECT id, inReplyToID, createDay, " + "subject, authorEmail " + "FROM Message WHERE createMonth=" + month.getMonth( ) + " AND createYear=" + month.getYear( ) + " AND boardID=" + boardID); while (rs.next( )) { long msgID = rs.getLong(1); long inReplyTo = rs.getLong(2); int createDay = rs.getInt(3); String subject = rs.getString(4); String authorEmail = rs.getString(5); DayMonthYear createDMY = new DayMonthYear( createDay, month.getMonth(), month.getYear( )); allMsgs.add(new MessageSummaryImpl(msgID, createDMY, boardSum, subject, authorEmail, inReplyTo)); } return allMsgs.iterator( ); } catch (SQLException sqe) { sqe.printStackTrace( ); throw new DataException(sqe); } finally { DBUtil.close(stmt, con); } } /** * @return an iterator of all <code>BoardSummary</code> objects. */ public Iterator getAllBoards( ) throws DataException { List allBoards = new ArrayList( ); Connection con = null; Statement stmt = null; Statement stmt2 = null; try { con = DBUtil.getConnection(dbURL); stmt = con.createStatement( ); stmt2 = con.createStatement( ); ResultSet rs = stmt.executeQuery( "SELECT id, name, description FROM Board " + "ORDER BY name"); while (rs.next( )) { long id = rs.getLong(1); String name = rs.getString(2); String description = rs.getString(3); // get the months with messages. Use a different // Statement object because we are in the middle of // traversing a ResultSet that was created with the // first Statement. List monthsWithMessages = this.getMonthsWithMessages(id, stmt2); allBoards.add(new BoardSummaryImpl(id, name, description, monthsWithMessages)); } return allBoards.iterator( ); } catch (SQLException sqe) { sqe.printStackTrace( ); throw new DataException(sqe); } finally { if (stmt2 != null) { try { stmt2.close( ); } catch (SQLException ignored) { } } DBUtil.close(stmt, con); } } /** * @return a board summary for the given id. * @throws DataException if boardID is illegal or a database * error occurs. */ public BoardSummary getBoardSummary(long boardID) throws DataException { Connection con = null; Statement stmt = null; try { con = DBUtil.getConnection(dbURL); stmt = con.createStatement( ); return getBoardSummary(boardID, stmt); } catch (SQLException sqe) { sqe.printStackTrace( ); throw new DataException(sqe); } finally { DBUtil.close(stmt, con); } } private BoardSummary getBoardSummary(long boardID, Statement stmt) throws DataException, SQLException { ResultSet rs = stmt.executeQuery( "SELECT name, description FROM Board WHERE id=" + boardID); if (rs.next( )) { String name = rs.getString(1); String description = rs.getString(2); List monthsWithMessages = getMonthsWithMessages(boardID, stmt); return new BoardSummaryImpl(boardID, name, description, monthsWithMessages); } else { throw new DataException("Unknown boardID"); } } /** * @return a list of MonthYear objects */ private List getMonthsWithMessages(long boardID, Statement stmt) throws SQLException { List monthsWithMessages = new ArrayList( ); ResultSet rs = stmt.executeQuery( "SELECT DISTINCT createMonth, createYear " + "FROM Message " + "WHERE boardID=" + boardID); while (rs.next( )) { monthsWithMessages.add(new MonthYear( rs.getInt(1), rs.getInt(2))); } return monthsWithMessages; } private Message insertMessage(BoardSummary board, long inReplyToID, String msgSubject, String authorEmail, String msgText) throws DataException { // avoid overflowing the max database column lengths if (msgSubject.length( ) > ForumConfig.MAX_MSG_SUBJECT_LEN) { msgSubject = msgSubject.substring(0, ForumConfig.MAX_MSG_SUBJECT_LEN); } if (authorEmail.length( ) > ForumConfig.MAX_EMAIL_LEN) { authorEmail = authorEmail.substring(0, ForumConfig.MAX_EMAIL_LEN); } DayMonthYear createDate = new DayMonthYear( ); Connection con = null; PreparedStatement stmt = null; try { con = DBUtil.getConnection(dbURL); long newMsgID = DBUtil.getNextID("Message", con); stmt = con.prepareStatement("INSERT INTO Message " + "(id, inReplyToID, createMonth, createDay, createYear, " + "boardID, subject, authorEmail, msgText) " + "VALUES (?,?,?,?,?,?,?,?,?)"); stmt.setString(1, Long.toString(newMsgID)); stmt.setString(2, Long.toString(inReplyToID)); stmt.setInt(3, createDate.getMonth( )); stmt.setInt(4, createDate.getDay( )); stmt.setInt(5, createDate.getYear( )); stmt.setString(6, Long.toString(board.getID( ))); stmt.setString(7, msgSubject); stmt.setString(8, authorEmail); DBUtil.setLongString(stmt, 9, msgText); stmt.executeUpdate( ); return new MessageImpl(newMsgID, createDate, board, msgSubject, authorEmail, msgText, inReplyToID); } catch (SQLException sqe) { sqe.printStackTrace( ); throw new DataException(sqe); } finally { DBUtil.close(stmt, con); } } } Since this is not a book about relational database access using Java, we will not focus on the low-level JDBC details found in this class. The SQL code is intentionally simple to make this class portable to several different relational databases. The database URL and JDBC driver class name are retrieved from the ForumConfig class instead of hardcoded into the class: private static String dbURL = ForumConfig.getDatabaseURL( ); /** * Construct the data adapter and load the JDBC driver. */ public JdbcDataAdapter( ) throws DataException { try { Class.forName(ForumConfig.getJDBCDriverClassName( )); } catch (Exception ex) { ex.printStackTrace( ); throw new DataException("Unable to load JDBC driver: " + ForumConfig.getJDBCDriverClassName( )); } } Creating connections with the DBUtil class is another common pattern: Connection con = null; try { con = DBUtil.getConnection(dbURL); As mentioned earlier, this approach leaves the door open for connection pooling in a future implementation. When the pool is written, it only needs to be added to the DBUtil class in a single place. When connections and statements are no longer needed, they should always be closed in a finally block: } finally { DBUtil.close(stmt, con); } As mentioned earlier, this ensures that they will be closed because finally blocks are executed regardless of whether an exception occurs. 7.3.3. JDOM XML ProductionThe discussion forum code presented up to this point can extract data from a relational database and create instances of Java domain classes. The next step is to convert the domain objects into XML that can be transformed using XSLT. For this task, we use the JDOM class library. As mentioned in earlier chapters, JDOM is available at http://www.jdom.org and is open source software. Although the DOM API can also be used, JDOM is somewhat easier to work with, which results in cleaner code.[32]
The basic pattern relies on various JDOM "producer" classes, each of which knows how to convert one or more domain objects into XML. This approach capitalizes on the recursive nature of XML by having each class produce a JDOM Element instance. Some of these Element instances represent entire documents, while others represent a small fragment of XML. These fragments can be recursively embedded into other Element instances to build up more complex structures. Keeping XML production outside of domain objects is useful for several reasons:
The HomeJDOM class, shown in Example 7-20, is quite simple. It merely produces a <home> element containing a list of <board> elements. Since a separate JDOM producer class creates the <board> elements, the HomeJDOM class merely assembles those XML fragments into a larger structure. Example 7-20. HomeJDOM.javapackage com.oreilly.forum.xml; import com.oreilly.forum.domain.*; import java.util.*; import org.jdom.*; /** * Produce JDOM data for the home page. */ public class HomeJDOM { /** * @param boards an iterator of <code>BoardSummary</code> objects. */ public static Element produceElement(Iterator boards) { Element homeElem = new Element("home"); while (boards.hasNext( )) { BoardSummary curBoard = (BoardSummary) boards.next( ); homeElem.addContent(BoardSummaryJDOM.produceElement(curBoard)); } return homeElem; } private HomeJDOM( ) { } } As shown in the HomeJDOM class, the constructor is private. This prevents instantiation of the class, another decision made in the name of efficiency. Since each of the JDOM producer classes for the discussion forum are stateless and thread-safe, the produceElement( ) method can be static. This means that there is no reason to create instances of the JDOM producers, because the same method is shared by many concurrent threads. Additionally, there is no common base class because each of the produceElement( ) methods accept different types of objects as parameters.
The code for ViewMonthJDOM is shown in Example 7-21. This class creates XML data for an entire month's worth of messages. Example 7-21. ViewMonthJDOM.javapackage com.oreilly.forum.xml; import java.util.*; import com.oreilly.forum.*; import com.oreilly.forum.adapter.*; import com.oreilly.forum.domain.*; import org.jdom.*; /** * Creates the JDOM for the month view of a board. */ public class ViewMonthJDOM { /** * @param board the message board to generate JDOM for. * @param month the month and year to view. */ public static Element produceElement(BoardSummary board, MonthYear month) throws DataException { Element viewMonthElem = new Element("viewMonth"); viewMonthElem.addAttribute("month", Integer.toString(month.getMonth( ))); viewMonthElem.addAttribute("year", Integer.toString(month.getYear( ))); // create the <board> element... Element boardElem = BoardSummaryJDOM.produceNameIDElement(board); viewMonthElem.addContent(boardElem); DataAdapter adapter = DataAdapter.getInstance( ); MessageTree msgTree = new MessageTree(adapter.getAllMessages( board.getID( ), month)); // get an iterator of MessageSummary objects Iterator msgs = msgTree.getTopLevelMessages( ); while (msgs.hasNext( )) { MessageSummary curMsg = (MessageSummary) msgs.next( ); Element elem = produceMessageElement(curMsg, msgTree); viewMonthElem.addContent(elem); } return viewMonthElem; } /** * Produce a fragment of XML for an individual message. This * is a recursive function. */ private static Element produceMessageElement(MessageSummary msg, MessageTree msgTree) { Element msgElem = new Element("message"); msgElem.addAttribute("id", Long.toString(msg.getID( ))); msgElem.addAttribute("day", Integer.toString(msg.getCreateDate().getDay( ))); msgElem.addContent(new Element("subject") .setText(msg.getSubject( ))); msgElem.addContent(new Element("authorEmail") .setText(msg.getAuthorEmail( ))); Iterator iter = msgTree.getReplies(msg); while (iter.hasNext( )) { MessageSummary curReply = (MessageSummary) iter.next( ); // recursively build the XML for all replies msgElem.addContent(produceMessageElement(curReply, msgTree)); } return msgElem; } private ViewMonthJDOM( ) { } } The recursive method that produces <message> elements is the only difficult code in ViewMonthJDOM. Since <message> elements are nested according to replies, the XML forms a recursive tree structure that could be arbitrarily deep. JDOM supports this nicely, because a JDOM Element can contain other nested Element s. The produceMessageElement( ) method is designed to create the required XML data. The next JDOM producer class, shown in Example 7-22, is quite simple. It merely creates an XML view of an individual message. Example 7-22. ViewMessageJDOM.javapackage com.oreilly.forum.xml; import com.oreilly.forum.domain.*; import java.util.Date; import org.jdom.*; import org.jdom.output.*; /** * Generate JDOM for the View Message page. */ public class ViewMessageJDOM { /** * @param message the message to view. * @param inResponseTo the message this one is in response to, or * perhaps null. */ public static Element produceElement(Message message, MessageSummary inResponseTo) { Element messageElem = new Element("message"); messageElem.addAttribute("id", Long.toString(message.getID( ))); DayMonthYear d = message.getCreateDate( ); messageElem.addAttribute("month", Integer.toString(d.getMonth( ))); messageElem.addAttribute("day", Integer.toString(d.getDay( ))); messageElem.addAttribute("year", Integer.toString(d.getYear( ))); Element boardElem = BoardSummaryJDOM.produceNameIDElement( message.getBoard( )); messageElem.addContent(boardElem); if (inResponseTo != null) { Element inRespToElem = new Element("inResponseTo") .addAttribute("id", Long.toString(inResponseTo.getID( ))); inRespToElem.addContent(new Element("subject") .setText(inResponseTo.getSubject( ))); messageElem.addContent(inRespToElem); } messageElem.addContent(new Element("subject") .setText(message.getSubject( ))); messageElem.addContent(new Element("authorEmail") .setText(message.getAuthorEmail( ))); messageElem.addContent(new Element("text") .setText(message.getText( ))); return messageElem; } private ViewMessageJDOM( ) { } } The JDOM producer shown in Example 7-23 is also quite simple. Its job is to create XML for a BoardSummary object. This class is unique because it is not designed to create an entire XML document. Instead, the elements produced by BoardSummaryJDOM are embedded into other XML pages in the application. For example, the home page shows a list of all <board> elements found in the system, each of which is generated by BoardSummaryJDOM. As you design your own systems, you will certainly find common fragments of XML that are reused by several pages. When this occurs, write a common helper class rather than duplicate code. Example 7-23. BoardSummaryJDOM.javapackage com.oreilly.forum.xml; import com.oreilly.forum.domain.*; import java.util.*; import org.jdom.*; /** * Produces JDOM for a BoardSummary object. */ public class BoardSummaryJDOM { public static Element produceNameIDElement(BoardSummary board) { // produce the following: // <board id="123"> // <name>the board name</name> // <description>board description</description> // </board> Element boardElem = new Element("board"); boardElem.addAttribute("id", Long.toString(board.getID( ))); boardElem.addContent(new Element("name") .setText(board.getName( ))); boardElem.addContent(new Element("description") .setText(board.getDescription( ))); return boardElem; } public static Element produceElement(BoardSummary board) { Element boardElem = produceNameIDElement(board); // add the list of messages Iterator iter = board.getMonthsWithMessages( ); while (iter.hasNext( )) { MonthYear curMonth = (MonthYear) iter.next( ); Element elem = new Element("messages"); elem.addAttribute("month", Integer.toString(curMonth.getMonth( ))); elem.addAttribute("year", Integer.toString(curMonth.getYear( ))); boardElem.addContent(elem); } return boardElem; } private BoardSummaryJDOM( ) { } } The final JDOM producer, PostMessageJDOM, is shown in Example 7-24. The produceElement( ) method takes numerous arguments that allow the method to produce XML for posting a new message or replying to an existing message. Also, values for the message subject, author email, and message text may be pre-filled in the XML. The application takes advantage of this capability whenever it must redisplay an HTML form to a user with its values filled in. Example 7-24. PostMessageJDOM.javapackage com.oreilly.forum.xml; import com.oreilly.forum.domain.*; import org.jdom.*; /** * Produce JDOM for the "Post Message" page. */ public class PostMessageJDOM { public static Element produceElement( BoardSummary board, MessageSummary inResponseToMsg, boolean showError, String subject, String authorEmail, String msgText) { Element messageElem = new Element("postMsg"); // reuse the BoardSummaryJDOM class to produce a // fragment of the XML messageElem.addContent(BoardSummaryJDOM.produceNameIDElement(board)); if (inResponseToMsg != null) { Element inRespTo = new Element("inResponseTo") .addAttribute("id", Long.toString(inResponseToMsg.getID( ))); inRespTo.addContent(new Element("subject") .setText(inResponseToMsg.getSubject( ))); messageElem.addContent(inRespTo); } if (showError) { messageElem.addContent(new Element("error") .addAttribute("code", "ALL_FIELDS_REQUIRED")); } Element prefill = new Element("prefill"); prefill.addContent(new Element("subject") .setText(subject)); prefill.addContent(new Element("authorEmail") .setText(authorEmail)); prefill.addContent(new Element("message") .setText(msgText)); messageElem.addContent(prefill); return messageElem; } private PostMessageJDOM( ) { } } Copyright © 2002 O'Reilly & Associates. All rights reserved. |
|
|