Core Java Technologies Tech Tips Tips, Techniques, and Sample Code Welcome to the Core Java Technologies Tech Tips for August 10, 2004. Here you'll get tips on using core Java technologies and APIs, such as those in Java 2 Platform, Standard Edition (J2SE). This issue covers: * Sending and Receiving Multicast Messages * Extending a DefaultHandler to Parse XML files These tips were developed using Java 2 SDK, Standard Edition, v 1.4.2. This issue of the Core Java Technologies Tech Tips is written by Daniel H. Steinberg, Director of Java Offerings for Dim Sum Thinking, Inc, and editor-in-chief for java.net (http://java.net). You can view this issue of the Tech Tips on the Web at http://java.sun.com/developer/JDCTechTips/2004/tt0810.html. See the Subscribe/Unsubscribe note at the end of this newsletter to subscribe to Tech Tips that focus on technologies and products in other Java platforms. For more Java technology content, visit these sites: java.sun.com - The Java technology source for developers. Get the latest Java platform releases, tutorials, newsletters and more. java.net - A web forum where enthusiasts of Java technology can collaborate and build solutions together. java.com - The ultimate marketplace promoting Java technology, applications and services. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - SENDING AND RECEIVING MULTICAST MESSAGES The April 20, 2004 Tech Tip "User Datagram Protocol Programming, (http://java.sun.com/developer/JDCTechTips/2004/tt0420.html#1) presented an example of a time server that used User Datagram Protocol (UDP) for sending messages over the Internet instead of using Transmission Control Protocol (TCP). In that example, guaranteed delivery was not required or even desirable. By choosing UDP, you relax the requirement of guaranteed delivery. Also, there was a restriction in that the client needed to know the location of the server before connecting to it. That example demonstrated a unicast communication, that is, a communication between a single sender and a single receiver in a network. In the following tip, you'll learn how to set up your server to send and receive multicast messages. By implementing a multicast solution you decouple the client from the server. In doing this, you can have more than one machine serving packets to a given multicast address. You can use a single machine as both a client and a server so that more than one machine is leaving and retrieving messages from a given IP address port number pair. In fact, as with unicast, you are sending and receiving, but leaving and retrieving is more in line with the metaphor for multicasting. With multicast you agree on a multicast address to be used for communication in a reserved range. There is no machine at that address. However, the server will send out packets with the destination address set to the agreed-on host number, and the clients will listen to the network traffic for packets being sent to that address. It is as if you have a virtual location for messages that is agreed to by the server and all clients. You can send messages to a multicast address without joining a group, but you can't receive messages from the group until you join it. To understand the changes necessary to move from a client-server direct conversation to a multicast transaction, it helps to take a look at the code from the previous tip. In that tip, the TimeServer class created the response address and port for the client from the DatagramPacket. To create a new packet (which the socket sends), the class used the buffer containing the current time, the length of the buffer, and the IP address and port number. Here is the code for the TimeServer class: import java.io.IOException; import java.net.DatagramSocket; import java.net.DatagramPacket; import java.net.InetAddress; import java.util.Date; public class TimeServer { final private static int DAYTIME_PORT = 1113; public static void main(String args[]) throws IOException { DatagramSocket socket = new DatagramSocket(DAYTIME_PORT); while (true) { byte buffer[] = new byte[256]; DatagramPacket packet = new DatagramPacket(buffer, buffer.length); socket.receive(packet); String date = new Date().toString(); buffer = date.getBytes(); // Get response address/port // for client from packet InetAddress address = packet.getAddress(); int port = packet.getPort(); packet = new DatagramPacket(buffer, buffer.length, address, port); socket.send(packet); } } } For the multicast version, the IP address is no longer that of the server. Rather, you need to choose the address and port in a designated range that has been reserved for multicasting. It is as if the server and all potential clients are agreeing that messages will be left in a particular location. The available addresses for multicasting are in the range 224.0.0.0 through 239.255.255.255. You can check for the assigned addresses in this range at http://www.iana.org/assignments/multicast-addresses. In this tip, use the IP address 230.0.0.1 and the port 9013. InetAddress group = InetAddress.getByName("230.0.0.1"); DatagramPacket packet = new DatagramPacket(buffer, buffer.length, group, 9013); You also need a socket on the server machine that is used to broadcast the packet. Use any open port above 1024. Here is the multicast version of the time server. import java.io.IOException; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.DatagramPacket; import java.util.Date; public class MulticastTimeServer { public static void main(String args[]) throws IOException { DatagramSocket socket = new DatagramSocket(1213); while (true) { byte buffer[] = new byte[256]; String date = new Date().toString(); buffer = date.getBytes(); InetAddress address = InetAddress.getByName("230.0.0.1"); DatagramPacket packet = new DatagramPacket(buffer, buffer.length, address, 9013); socket.send(packet); // sleep so as not to flood the network try{ Thread.sleep(1000); } catch (InterruptedException e){ e.printStackTrace(); } } } } With the earlier unicast version of the client, you had to pass the IP address of the server in at run time. The first five lines of the client's main() method verified that a host was passed as a command line parameter, and stored the string. The client then created an InetAddress object from this host String, sent and received a DatagramPacket to that address, and printed the time returned from the host. Here is the client class from the previous tip; import java.io.IOException; import java.net.InetAddress; import java.net.DatagramPacket; import java.net.DatagramSocket; public class GetTime { final private static int DAYTIME_PORT = 1113; public static void main(String args[]) throws IOException { if (args.length == 0) { System.err.println ("Please specify daytime host"); System.exit(-1); } String host = args[0]; byte message[] = new byte[256]; InetAddress address = InetAddress.getByName(host); System.out.println("Checking at: " + address); DatagramPacket packet = new DatagramPacket(message, message.length, address, DAYTIME_PORT); DatagramSocket socket = new DatagramSocket(); socket.send(packet); packet = new DatagramPacket(message, message.length); socket.receive(packet); String time = new String(packet.getData()); System.out.println("The time at " + host + " is: " + time); socket.close(); } } In the multicast version of the client, you use a MulticastSocket in place of a DatagramSocket. You need to create the socket with the same port number as you set on the server, and then create an InetAddress object with the same IP address that you set on the MulticastTimeServer: MulticastSocket socket = new MulticastSocket(9013); InetAddress address = InetAddress.getByName("230.0.0.1"); In MulticastSocket, you use the methods joinGroup() and leaveGroup() to receive packets from the designated multicast address: socket.joinGroup(address); // ... communication with 230.0.0.1 port 9013 socket.leaveGroup(address); Most of the changes needed in moving to the multicast version are to omit unnecessary method calls. Here is the multicast client code: import java.io.IOException; import java.net.InetAddress; import java.net.DatagramPacket; import java.net.MulticastSocket; public class MulticastGetTime { public static void main(String args[]) throws IOException { MulticastSocket socket = new MulticastSocket(9013); InetAddress address = InetAddress.getByName("230.0.0.1"); socket.joinGroup(address); DatagramPacket packet; byte message[] = new byte[256]; packet = new DatagramPacket(message, message.length); socket.receive(packet); String time = new String(packet.getData()); System.out.println("The time is: " + time); socket.leaveGroup(address); socket.close(); } } Compile and run MulticastTimeServer. Now compile and run MulticastGetTime on one or more machines. java MulticastGetTime The client machines will all receive the multicast packets created by the server. You should see something like the following: The time is: Mon Jul 26 12:14:08 PDT 2004 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - EXTENDING A DEFAULT HANDLER TO PARSE XML FILES You might not realize it, but a lot of the information you see each day is stored in XML documents. You might think that XML is primarily used in enterprise applications and configuration files, but some dialect of XML is also used in many other documents. For example, you can find an XML version of the periodic table of the elements at http://www.ibiblio.org/xml/examples/periodic_table/allelements.xml. In this tip you will learn how to use a Simple API for XML (SAX) parser to extract information from specified places in an XML document. The tip is designed for developers new to working with XML from a Java application. It introduces one of the models for processing XML, and highlights some of the places where those new to SAX often make mistakes. Download the file allelements.xml from http://www.ibiblio.org/xml/examples/periodic_table/allelements.xml. You should see a file that begins like this: Actinium 227 89 3 3470 Ac 10.07 [Rn] 6d1 7s2 1.1 1.88 22.5 0.12 5.17 12 Imagine traversing this file in such a way that you have no memory of what you have already seen. You can perform actions at the beginning or end of the document, at the beginning or end of an element of a given type, and at other easily identified parts of the file. However you can't remember what you've found along the way. For example, when you get to the SYMBOL element for Actinium, you can't remember that the contents of the ATOMIC_NUMBER element is the text 89. What you've just imagined is actually the streaming processing model in SAX parsers. A SAX parser reads through an XML document in a linear way (as opposed to a Document Object Model, DOM, parser which traverses trees in a hierarchical way). As each markup event occurs, a method in the active content is called to handle it. You can customize the handler by overriding appropriate methods. In this tip, you start with the DefaultHandler and override the methods startElement(), endElement(), startDocument(), endDocument(), and characters(). Consider how you might process this file to extract and display the name of the atomic element, its symbol, and its atomic weight. The objective is to produce a result that looks something like the following: Parsing allelements.xml ======================= Actinium (Ac) 89 Aluminum (Al) 13 Americium (Am) 95 Antimony (Sb) 51 Argon (Ar) 18 Arsenic (As) 33 ... Zinc (Zn) 30 Zirconium (Zr) 40 ============================ End Parse of allelements.xml The first task is to output the header when you begin parsing the document. This can be done in the following method: public void startDocument() throws SAXException { System.out.println("Parsing allelements.xml"); System.out.println("======================="); } This is a callback method. This method is called when the parser reaches the beginning of the document. The exception is part of the signature of the method -- in each of the callbacks you need to specify that the method throws SAXException. Notice that in the desired report there is an additional space inserted every three atomic elements. You can do this by listening for the start of an element and then incrementing a counter. Begin the increment at the start of an ATOM element. This way every three times the counter is incremented you can output an extra blank line. Here is what the code for that looks like: public void startElement(String namespace, String localName, String qName, Attributes atts) throws SAXException { if (localName.equals("ATOM")) { if (count++ % 3 == 0) System.out.println(""); } } You might be tempted to try to output the symbol by checking if(localName.equals("SYMBOL"). At the start of the SYMBOL element you know the namespace, localName, qualifiedName, and attributes. However you do not know the contents of that element. So you need to record these values at the end of an element, like this: public void endElement(String uri, String localName, String qName) throws SAXException { if (localName.equals("NAME")) name = temp; else if (localName.equals("ATOMIC_NUMBER")) number = temp; else if (localName.equals("SYMBOL")) symbol = temp; else if (localName.equals("ATOM")) { System.out.println("" + name + " (" + symbol + ") " + number); } } In the endElement() method you save the value of the contents of the NAME, ATOMIC_NUMBER, and SYMBOL elements. You can then output this information to the screen in the correct order when you encounter the end of an ATOM element. Next you need to examine how the value of the temp variable is set. You do this in the characters() method: public void characters(char buf[],int offset,int len) throws SAXException { String temp2; temp2 = new String(buf, offset, len).trim(); if (!temp2.equals("")) { temp = temp2; } } You create a new String named temp2. This is formed by reading the contents of an element and then trimming the leading and trailing white space. If after trimming, you are left with something that is not an empty String, store this as temp. At the end of each element, temp will contain the String of characters that comprise the contents of that element. What remains is to create a SAXParserFactory that is used to obtain a SAXParser. The SAXParser is passed into the ParserAdapter constructor to return a ParserAdapter. You associate a ContentHandler with the ParserAdapter and then parse a specified file. Here's what that code looks like: SAXParserFactory spf = SAXParserFactory.newInstance(); SAXParser sp = spf.newSAXParser(); ParserAdapter pa = new ParserAdapter(sp.getParser()); pa.setContentHandler(this); pa.parse("file:allelements.xml"); Here then is the entire program, AlphaList, that extracts information from an XML document and produces a report in the desired format: import org.xml.sax.helpers.DefaultHandler; import org.xml.sax.helpers.ParserAdapter; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import javax.xml.parsers.SAXParserFactory; import javax.xml.parsers.SAXParser; import javax.xml.parsers.ParserConfigurationException; import java.io.IOException; public class AlphaList extends DefaultHandler { private String name; private String number; private String symbol; private String temp; private int count; public void startDocument() throws SAXException { System.out.println("Parsing allelements.xml"); System.out.println("======================="); } public void startElement(String namespace, String localName, String qName, Attributes atts) throws SAXException { if (localName.equals("ATOM")) { if (count++ % 3 == 0) System.out.println(""); } } public void endElement(String uri, String localName, String qName) throws SAXException { if (localName.equals("NAME")) name = temp; else if (localName.equals("ATOMIC_NUMBER")) number = temp; else if (localName.equals("SYMBOL")) symbol = temp; else if (localName.equals("ATOM")) { System.out.println("" + name + " (" + symbol + ") " + number); } } public void endDocument() throws SAXException { System.out.println("============================"); System.out.println("End Parse of allelements.xml"); } public void characters(char buf[],int offset,int len) throws SAXException { String temp2; temp2 = new String(buf, offset, len).trim(); if (!temp2.equals("")) { temp = temp2; } } public void processWithSAX() { try { SAXParserFactory spf = SAXParserFactory.newInstance(); SAXParser sp = spf.newSAXParser(); ParserAdapter pa = new ParserAdapter(sp.getParser()); pa.setContentHandler(this); pa.parse("file:allelements.xml"); } catch (ParserConfigurationException e) { System.err.println("Problem configuring parser. " + e.getMessage()); } catch (SAXException e) { System.err.println("Problem parsing file. " + e.getMessage()); } catch (IOException e) { System.err.println("Problem reading file. " + e.getMessage()); } } public static void main(String[] args) { new AlphaList().processWithSAX(); } } Compile and run AlphaList against allelements.xml: java AlphaList allelements.xml You should get a result that begins like this: Parsing allelements.xml ======================= Antimony (Sb) 51 Argon (Ar) 18 Arsenic (As) 33 Astatine (At) 85 Gold (Au) 79 Boron (B) 5 Barium (Ba) 56 Beryllium (Be) 4 Bohrium (Bh) 107 Bismuth (Bi) 83 Berkelium (Bk) 97 Bromine (Br) 35 As more documents are stored in XML, it is useful to have the tools to parse them on your desktop. For more information about SAX and SAX parsers, see Chapter 5: "Simple API for XML" in the J2EE 1.4 Tutorial (http://java.sun.com/j2ee/1.4/docs/tutorial/doc/JAXPSAX.html). - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - OTHER RESOURCES Got a question about Java technologies or tools? Then ask the experts in these upcoming chats: August 17. 11:00 A.M. PDT/6:00 P.M. GMT. What's Happening in the JCP? August 24. 11:00 A.M. PDT/6:00 P.M. GMT. J2ME Wireless Toolkit 2.2 For further information about these chats, go to the SDN Chat Sessions page (http://java.sun.com/developer/community/chat/JavaLive/index.html) . . . . . . . . . . . . . . . . . . . . . . . PRIVACY STATEMENT: Sun respects your online time and privacy (http://sun.com/privacy). You have received this based on your e-mail preferences. If you would prefer not to receive this information, please follow the steps at the bottom of this message to unsubscribe. Please read our Terms of Use and Licensing policies: http://www.sun.com/share/text/termsofuse.html http://developers.sun.com/dispatcher.jsp?uid=6910008 * FEEDBACK Comments? Please enter your feedback on the Tech Tips at: http://developers.sun.com/contact/feedback.jsp?category=sdn * SUBSCRIBE/UNSUBSCRIBE Subscribe to other Java developer Tech Tips: - Enterprise Java Technologies Tech Tips. Get tips on using enterprise Java technologies and APIs, such as those in the Java 2 Platform, Enterprise Edition (J2EE). - Wireless Developer Tech Tips. Get tips on using wireless Java technologies and APIs, such as those in the Java 2 Platform, Micro Edition (J2ME). To subscribe to these and other JDC publications: - Go to the JDC Newsletters and Publications page, (https://softwarereg.sun.com/registration/developer/en_US/subscriptions), choose the newsletters you want to subscribe to and click "Update". - To unsubscribe, go to the subscriptions page, (https://softwarereg.sun.com/registration/developer/en_US/subscriptions), uncheck the appropriate checkbox, and click "Update". - To use our one-click unsubscribe facility, see the link at the end of this email: - ARCHIVES You'll find the Core Java Technologies Tech Tips archives at: http://java.sun.com/developer/TechTips/index.html - COPYRIGHT Copyright 2004 Sun Microsystems, Inc. All rights reserved. 4150 Network Circle, Santa Clara, California 95054 USA. This document is protected by copyright. For more information, see: http://java.sun.com/jdc/copyright.html Core Java Technologies Tech Tips August 10, 2004 Trademark Information: http://www.sun.com/suntrademarks/ Java, J2SE, J2EE, J2ME, and all Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries.