Welcome to the Enterprise Java Technologies Tech Tips for April 15, 2003. Here you'll get tips on using enterprise Java technologies and APIs, such as those in Java 2 Platform, Enterprise Edition (J2EE). This issue covers:
These tips were developed using Java 2 SDK, Standard Edition, v 1.4 and Java 2 SDK, Enterprise Edition, v 1.3.1 (Reference Implementation). This issue of the Tech Tips is written by Mark Johnson, president of elucify technical communications, and co-author of Designing Enterprise Applications with the Java 2, Enterprise Edition, 2nd Edition. Mark Johnson runs an open forum for discussion of the tips. You can download the sample code. The context root for the application is ttapr2003. The index.html welcome file indicates how to use the sample code. Any use of this code and/or information below is subject to the license terms. PUBLISH/SUBSCRIBE MESSAGING WITH JMS TOPICSThe tip Using JMS Queues in the March 11, 2003 issue explained how to use Java Messaging Service (JMS) Queues for point-to-point messaging. The tip that follows explains how to implement publish/subscribe messaging using JMS Topics. Publish/Subscribe MessagingIn publish/subscribe messaging, a single publisher sends each message to multiple subscribers with a single method call. Between the publisher and subscriber is a messaging server. In JMS, the messaging server is called a "JMS provider". The publisher sends messages to the JMS provider. Subscribers receive messages from the JMS provider. This arrangement is illustrated in the following figure.
In JMS, publish/subscribe messaging uses a JMS-managed object called a This is illustrated below.
Publish/subscribe messaging using JMS
There are also several differences between point-to-point and publish/subscribe messaging:
Neither publish/subscribe messaging nor point-to-point messaging is superior to the other. Rather, they are complementary tools, used for different purposes. Point-to-point messaging is often used where the message receiver has a unique identity within a system. Publish/subscribe messaging is more often used when several agents in a system need to know when an event or condition occurs. The JMS messaging model is very similar to event listeners in conventional Java 2 programming. Point-to-point messaging is like a unicast event listener model. Publish-subscribe messaging is like a multicast listener model. The difference between traditional Java event listeners and JMS (other than programming syntax) is that the event sources and listeners are called message producers and consumers, respectively. JMS message producers and consumers can run in different address spaces, or even on different machines. JMS messaging also provides a much higher level of service than does the traditional event listener model. Nevertheless, the basic messaging model is the same. The sample code for this tip, comprises three programs:
Here's a screen shot of an HTML page and Web form for publishing the weather report, a terminal session running the command-line subscriber, and the GUI application.
Publishing a Message To A Topic
The
That's all there is to it. The JMS provider is responsible for delivering the message to all subscribers. Subscribing to a
|
protected TopicConnection _tc;
...
public SubscriptionHelper(String tcfName,
String topicName,
MessageListener listener) {
// Get references to topic connection factory
// and topic.
_tc = null;
TopicConnectionFactory tcf = null;
Topic topic = null;
try {
InitialContext ic = new InitialContext();
tcf = (TopicConnectionFactory)
ic.lookup(tcfName);
topic = (Topic) ic.lookup(topicName);
} catch (NamingException e) {
System.err.println(e.toString());
e.printStackTrace(System.err);
}
try {
// Create a connection and so on
// Subscribe self to topic--messages will be
// delivered to this.onMessage()
_tc = tcf.createTopicConnection();
TopicSession ts =
_tc.createTopicSession(
false, Session.AUTO_ACKNOWLEDGE);
TopicSubscriber tsub =
ts.createSubscriber(topic);
tsub.setMessageListener(listener);
} catch (JMSException e) {
System.err.println(e.toString());
e.printStackTrace(System.err);
close();
}
}
|
SubscriptionHelper class is identical to the publisher code. It uses the JNDI API to acquire references to a Topic and TopicConnectionFactory, and creates TopicConnection and TopicSession objects. But instead of creating a TopicPublisher, this class creates a TopicSubscriber, and sets the TopicSubscriber's message listener to the MessageListener that was passed in. After this point, whenever that Topic receives a message, the message is delivered to the onMessage method of the MessageListener. Because a callback in used in this way, this example demonstrates asynchronous message reception.
javax.jms.MessageListener. The WeatherReceiver class is itself a MessageListener. The MessageListener interface has only method: onMessage. The WeatherReceiver's onMessage method appears below:
public class WeatherReceiver implements
MessageListener {
// Print a weather message when it is received
public void onMessage(Message message) {
try {
if (message instanceof TextMessage) {
TextMessage m = (TextMessage) message;
System.out.println(
"--- Received weather report");
System.out.println(m.getText());
System.out.println("----------");
} else {
System.out.println(
"Received message of type " +
message.getClass().getName());
}
} catch (JMSException e) {
System.err.println(e.toString());
e.printStackTrace(System.err);
}
}
...
public static void main(String[] args) {
if (args.length != 2) {
System.out.println(
"Usage: WeatherReceiver " +
"topicConnectionFactorName topicName");
System.exit(1);
}
// Create a receiver, then set it up to listen
// for messages on the topic. Then wait for
// messages and print them as they come in.
WeatherReceiver wr = new WeatherReceiver();
SubscriptionHelper sh =
new SubscriptionHelper(args[0], args[1], wr);
// Wait for publications...
System.out.println(
"Waiting for publications to topic " +
args[1]);
sh.waitForMessages();
}
|
WeatherReceiver's main method creates a WeatherReceiver instance and a SubscriptionHelper instance. It passes the SubscriptionHelper the WeatherReceiver and the names of the Topic and TopicConnectionFactory that the application should use (these are specified on the command line). The SubscriptionHelper instance creates the subscription. It then registers the WeatherReceiver as the message consumer for messages from the Topic.
onMessage method simply casts received Messages to class TextMessage, where appropriate, and prints the received XML document.
Publish/subscribe messaging code is easy with JMS. However, the deployment descriptor presents an issue that requires resolution. PublishWeatherServlet is a Web component that looks up external components using the JNDI API. Web components look up external resources (such as Topics and TopicConnectionFactories) using a coded name. The deployment descriptor must define these coded names as resource references or resource environment references. The following excerpt from the Web application's deployment descriptor web.xml defines the coded names used by the servlet. (This code appears after <welcome-file-list> in web.xml.)
<!-- JMS topics and connection factories used -->
<resource-env-ref>
<resource-env-ref-name>
jms/Topic
</resource-env-ref-name>
<resource-env-ref-type>
javax.jms.Topic
</resource-env-ref-type>
</resource-env-ref>
<resource-ref>
<res-ref-name>
jms/TopicConnectionFactory
</res-ref-name>
<res-type>
javax.jms.TopicConnectionFactory
</res-type>
<res-auth>
Container
</res-auth>
</resource-ref>
|
The resource-env-ref block defines the name "jms/Topic" as being of type javax.jms.Topic. The string "jms/Topic" is the string used to look up the Topic ("java:comp/env/jms/Topic"), with the "java:comp/env/" part deleted. The deployment tools for a product allow the application deployer to map this name to a Topic in the environment.
In the case of the J2EE Reference Implementation, this mapping is already configured in the Web archive in the file META-INF/sun-j2ee-ri.xml. This file is the Web application's runtime deployment descriptor. The runtime deployment descriptor is vendor-specific both in name and in content.
The resource-ref block defines the name, type, and authorization mode for the TopicConnectionFactory. Usually, a deployer would use deployment tools to associate the coded name jms/TopicConnectionFactory with a TopicConnectionFactory in the platform. The J2EE Reference Implementation comes preconfigured with a TopicConnectionFactory called jms/TopicConnectionFactory in the JNDI namespace.
The WeatherReceiver described in the tip "Publish/Subscribe Messaging With JMS Topics" above receives XML messages from a publisher, and prints those messages to the screen. But one of the major reasons to format data as XML is to be able to separate the pieces of data in the file and use them in various places.
The tip Creating Parsers with JAXP in the February 11, 2003 issue explained how to use JAXP to create a DocumentBuilder class that can parse an XML document, returning an object of type org.w3c.dom.Document. The tip that follows explains how to use the Document Object Model (DOM) APIs to get information from a parsed XML Document.
XML markup can be represented as a tree of nodes. Each node can represent a feature in the XML such as an element, an attribute, a comment, or a piece of text. A tree of these objects forms a model of the XML document. The Document Object Model is a set of APIs that provide programmatic access to a virtual tree of nodes that represent XML document features.
A JAXP DocumentBuilder parses a character stream formatted in XML notation, and returns an object that implements interface org.w3c.dom.Document. The Document object represents the entire XML document as one object, but the objects within the XML document are accessible through the Document methods. The virtual tree represented by the Document object is often called a DOM tree.
The DOM has quite a number of interfaces, all of which reside in package org.w3c.dom (as defined by the World Wide Web Consortium, W3C). A few of the interfaces are of special interest:
Node interface is the highest-level interface in the DOM. Almost every interface in the DOM extends Node. A Node simply represents a distinct feature in the input XML. A Node has exactly one parent node, zero or more sibling nodes, and zero or more child nodes. Methods of interface Node, which its subinterfaces inherit, allow a programmer to access the parent, child, and siblings of any Node in a DOM tree.Document interface represents an entire XML document. It is usually created by an XML parser. It contains the top-level element, and can also contain nodes for the XML declaration, processing instructions, and DTD elements.Element interface represents a tag in an XML document. Its parent Node is the Node that contains it (the containing Node is the Document, in the case of the top-level tag). An Element's sibling nodes are all nodes that share that parent, and its child nodes are all tags that it contains. Elements are frequently accessed by their tag name. Attributes are modeled separately.CharacterData interface represents character data that is not in a tag. CharacterData has three subinterfaces: Text, which represents Text nodes; CDATASection, which represents CDATA; and Comment, which (optionally) represents comments. Parser settings determine whether or not comments appear in the parser output.
Many DOM methods have return type NodeList. A NodeList does not represent a feature in an XML document, but rather represents a list of Nodes. Interface NodeList has two methods: getLength, which returns the number of items in the list; and item, which returns the nth item in the list (numbered from 0 to getLength-1). Methods that access attributes return a NamedNodeMap instead.
You can explore the DOM interfaces for yourself in the documentation for package org.w3c.dom, which is a part of the standard J2EE distribution.
The WeatherClient sample program that was introduced in the tip "Publish/Subscribe Messaging With JMS Topics" receives an XML document from a Topic in exactly the same way as the WeatherReceiver described in that tip. But instead of printing the XML, as WeatherReceiver does, WeatherClient parses the XML as a DOM tree. WeatherReceiver then uses pieces of data from the tree to create HTML, which it then displays in a Swing GUI. Receiving the XML and parsing it have already been covered in
other tips, such as Creating Parsers with JAXP, so this section focuses on how the WeatherReceive program accesses the data in the DOM tree.
The WeatherClient's onMessage method receives the XML and parses it with a DocumentBuilder, resulting in a DOM tree rooted in the Document variable doc. The following code from class WeatherClient gets the top-level element from the document. Then three methods pull different subsets of data out of that element, and return the data subsets formatted as HTML. Each of the three HTML display panels in the application gets different HTML.
if (doc != null) {
Element e = doc.getDocumentElement();
// Set the HTML for the first panel
_panels[0].setText(getCurrentConditionsHtml(e));
_panels[1].setText(getForecastHtml(e));
_panels[2].setText(getDetailedForecastHtml(e));
pack();
}
|
A sample of the beginning of the XML message received by the program appears below.
<weather-report>
<current>
<conditions>
<sky>C</sky>
<temp>15</temp>
<humidity>23</humidity>
<wind>
<speed>14</speed>
<dir>SW</dir>
</wind>
</conditions>
</current>
... <!-- Followed by "forecast" and "detailed
... forecast elements" -->
</weather-report>
|
The top-level Element returned by getDocumentElement() is the top-level tag, <weather-report>. Method getCurrentConditionsHtml pulls pieces of text out of the XML and formats them as HTML for display in the GUI. The method first creates a PrintWriter that writes to a String, so the resulting XML can be returned as a String.
// Get "current conditions" from document
protected String getCurrentConditionsHtml(
Element top) {
StringWriter sw = new StringWriter(4096);
PrintWriter pw = new PrintWriter(sw);
Element current, conditions, el;
pw.println("<HTML><BODY><CENTER>");
pw.println("<TABLE BORDER=0>");
current = getFirstMatchingElement(
top, "current");
conditions = getFirstMatchingElement(
current, "conditions");
|
The code then gets the first element matching the name "current". Then it gets the first element matching the name "conditions" under "current". At this point, the method has a handle to the object representing the tag <conditions> in the XML excerpt above. The next section of code formats the display of the condition of the sky:
el = getFirstMatchingElement(conditions, "sky");
String sky = getMergedTextChildren(el);
sky = getSkyDescription(sky);
pw.println(
"<TR><TD BGCOLOR=BLUE COLSPAN=2 ALIGN=CENTER>");
pw.println("<H1><FONT COLOR=\"WHITE\">");
pw.println(sky);
pw.println("</FONT></H1></TD></TR>");
|
Because the Element "conditions" contains only one Element called "sky", the <sky> Element can be retrieved by name, using method getFirstMatchingElement. Method getMergedTextChildren gets the contents of the element as a string. That string is then formatted as HTML.
Method getFirstMatchingElement is defined in WeatherClient as follows:
static protected Element getFirstMatchingElement(
Node n, String nodeName) {
NodeList nl;
Element el = null;
if (n instanceof Element) {
el = (Element)n;
}
if ((el != null) &&
(nl = el.getElementsByTagName(nodeName))
!= null &&
nl.getLength() != 0) {
el = (Element)(nl.item(0));
return el;
}
return null;
}
|
The first line ensures that what was passed in was an element, and typecasts the Node to an Element. If the Element is non-null and contains (however far down the tree) nodes with the requested name, the first of these nodes is returned. Otherwise, the method returns null.
Method getMergedTextChildren combines all non-comment CharacterData nodes (Text nodes and CDATASection nodes) into a single string. The text contained inside the sky element is composed of CharacterData nodes, not Elements. Method getMergedTextChildren combines the zero or more Text and CDATASection nodes under the node passed in to a single string.
protected static String getMergedTextChildren(
Element e) {
NodeList nl = e.getChildNodes();
String result = "";
for (int i = 0; i < nl.getLength(); i++) {
Node n = nl.item(i);
if (n instanceof Text) {
result = result + ((Text)n).getData();
} else if (n instanceof CDATASection) {
result = result +
((CDATASection)n).getData();
}
}
return result;
}
|
These methods are used multiple times in the code to simplify working with the DOM.
You might have noticed that the DOM interfaces are somewhat clumsy to use. The reason DOM seems to require a great deal of coding to do something simple is because it was designed to interoperate with many different source languages. Not all of these languages have a syntax that is as succinct as Java. Fortunately, there are other solutions:
The sample code for the tips consists of two archives: an EAR file containing a Web application, and a JAR file containing two application clients. For convenience, the JAR file is packaged inside the EAR file. Visit the application context root (see below) and follow the instructions to download the application client JAR file from the Web application.
The Web application publishes from the Web tier to both application clients in the Client Tier. Running both clients, or even multiple copies of both clients simultaneously, helps to demonstrate the one-to-many nature of publish/subscribe messaging.
Publishing: Download the sample archive for these tips. The application's context root is ttapr2003. The downloaded EAR file also contains the complete source code for the sample.
You can deploy the application archive (ttapr2003.ear) in the J2EE Reference Implementation using the deploytool program:
$J2EE_HOME/deploytool -deploy ttapr2003.ear localhost
Replace localhost with the name of the host on which the server is installed. For a standard installation on a single machine, the hostname typically (and literally) is localhost. You can access the application at http://localhost:8000/ttapr2003. Visiting this URL in a Web browser will present you with an HTML form that you can use to publish weather reports. The servlet publishes to the JMS Topic called "jms/Topic", and uses the Topic Connection Factory called "jms/TopicConnectionFactory" (these are the names of the default topic and connection factory that come pre-configured with the Reference Implementation).
Also, use deploytool to undeploy the application when you're finished with it. The -uninstall argument of deploytool takes the application name, not the EAR file name, as an argument:
deploytool -uninstall Apr2003 localhost
For a J2EE-compliant implementation other than the Reference Implementation, use your J2EE product's tools to deploy the application on your platform, and then undeploy it when you're finished with it.
Receiving XML Text: The sample code also contains a JAR file, ttapr2003.jar, that receives messages from the topics listed above. To run the WeatherReceiver on the command line, be sure that ttapr2003.jar is in your classpath, and execute the following command:
java com.elucify.tips.apr2003.WeatherReceiver
Using the GUI: The sample JAR ttapr2003.jar also contains a Swing GUI. To use it, be sure that the jar file is in your path, and execute:
java com.elucify.tips.apr2003.WeatherClient
Either of the two receivers can be configured to use a different TopicConnectionFactory and Topic by providing their JNDI names on the command line.
|
| ||||||||||||