Previous | Next
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. 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 Figure 7-6. Key domain classes
The Example 7-11. BoardSummary.javapackage com.anonymous.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( ); }
An early decision made in the design of the discussion forum was to assign a unique
The next interface, shown in Example 7-12, provides a summary for an individual message. Example 7-12. MessageSummary.javapackage com.anonymous.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 Example 7-13. Message.javapackage com.anonymous.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 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 The three reference implementation classes are The final class in this package, Example 7-14. MessageTree.javapackage com.anonymous.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 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 private List topLevelMsgs = new ArrayList( ); private Map idToMsgMap = new HashMap( ); private Map replyIDToMsgListMap = new HashMap( ); When the 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 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 Figure 7-7. Data adapter designThe The source code for
Example 7-15. ForumConfig.javapackage com.anonymous.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 The code for Example 7-16. DataAdapter.javapackage com.anonymous.forum.adapter; import com.anonymous.forum.*; import com.anonymous.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; }
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 public abstract Message getMessage(long msgID) throws DataException;
The downloadable discussion forum implementation comes with a "fake" implementation of Figure 7-8. Database designThe database is quite simple. Each table has an SELECT subject FROM Message WHERE createMonth=3 AND createYear=2001 The 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 Developing 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 Example 7-18. DBUtil.javapackage com.anonymous.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); } } }
The Connection con = null; Statement stmt = null; try { // code to create the Connection and Statement ... // code to access the database ... 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 The Finally, the Example 7-19. JdbcDataAdapter.javapackage com.anonymous.forum.jdbcimpl; import com.anonymous.forum.*; import com.anonymous.forum.adapter.*; import com.anonymous.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 tutorial 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 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 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 } finally { DBUtil.close(stmt, con); } As mentioned earlier, this ensures that they will be closed because 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 Keeping XML production outside of domain objects is useful for several reasons:
The Example 7-20. HomeJDOM.javapackage com.anonymous.forum.xml; import com.anonymous.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
The code for Example 7-21. ViewMonthJDOM.javapackage com.anonymous.forum.xml; import java.util.*; import com.anonymous.forum.*; import com.anonymous.forum.adapter.*; import com.anonymous.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( ))); The recursive method that produces 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.anonymous.forum.xml; import com.anonymous.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 Example 7-23. BoardSummaryJDOM.javapackage com.anonymous.forum.xml; import com.anonymous.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, Example 7-24. PostMessageJDOM.javapackage com.anonymous.forum.xml; import com.anonymous.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( ) { } } |