|
Welcome to the Enterprise Java Technologies Tech Tips for June 25, 2004. 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:
Advanced HTML Email
Domain-neutral Messaging with JMS 1.1
These tips were developed using the Java 2, Enterprise Edition, v 1.4 SDK. You can download the SDK at http://java.sun.com/j2ee/1.4/download-dr.html.
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 J2EE Platform, 2nd Edition. Mark Johnson runs an open forum for discussion of the tips.
You can download the sample archive for these tips. Any use of this code and/or information below is subject to the license terms.
For more Java technology content, visit these sites:
java.sun.com - The latest Java platform releases, tutorials, and
newsletters.
java.net - A web forum for collaborating and building solutions
together.
java.com - The marketplace for Java technology, applications and
services.
ADVANCED HTML EMAIL
The April 26, 2004 Tech Tip titled Using the JavaMail API to Send HTML Email, showed how to use the JavaMail API to send HTML email that is also readable by text browsers. But the email from that tip was text only -- it didn't include images. In the following tip, you'll see how to use the JavaMail API to send HTML email that contains embedded images.
Email Image Links Considered Harmful
You might sometimes receive HTML email that includes images. Sometimes the message's HTML code links to the image on the sender's server using an <img> tag, and a src attribute that identifies the image's URL, like this:
<img src="http://originating_server/images/image.jpg" />
The browser accesses these images just as if it were displaying an image in a Web page. Unfortunately, unscrupulous spammers have used this mechanism as a sneaky way to record who visits their site. They do this by including a unique id in the image src URL, and then saving the ID along with your email address. Such an image URL might look like this:
<img src="http://evilspammer.com/images/x.jpg?userid=12" />
When you visit the site, the server looks up your email address by the ID, and singles you out for special treatment. Unknowingly, you've just told a spammer that you're considering their product, so they may choose to market to you more aggressively. Worse, you've told them that your email address is "live", so it can be sold at a higher price to yet other spammers.
To protect your privacy, many Web-based (and other) email clients don't display images in HTML emails. While blocking image URLs in emails protects your privacy as a user, it can complicate your life as an application developer. Fortunately, you have another option: embedded images.
Embedding Images in HTML Emails
An alternative to placing absolute URLs to images in your HTML is to include the images as attachments to the email. You then use relative URLs to those images in your HTML code. The email format for attachments is defined by MIME (Multipurpose Internet Email Extensions), the standard protocol specifications for Internet email.
MIME has a mechanism that can be used to send an email containing HTML, plus attachments that contain the images the HTML references. A specification called RFC 2387 defines the MIME content type "multipart/related". A message in multipart/related format contains a collection of objects that are meant to be used together. For the purposes of this tip, that means an HTML file and the images it uses.
Each attachment in a multipart/related message has a unique identifier called its content-id. The content-id is defined by the application that sends the email, and is assigned when the message is constructed. The HTML can reference the image in an attachment by using the protocol prefix cid: plus the content-id of the attachment. For example, if an attachment has content-id image.part.1@x.org, then an HTML file in the same message could use the image inline with the following tag:
<img src="cid:image.part.1@x.org" />
A mail reading program (either a standalone program, or a web-based email interface) would render the tag above using the image with the given content-id.
Providing a Text Alternative
As described in the April 2004 tip, you can use a multipart/alternative content type to include alternative versions of the same object. In doing this, you allow the receiving program (the mail reader) to choose the most appropriate content type. The "most appropriate" content type may be defined by the capabilities of the mail reader or by user preferences.
The sample code that accompanies this tip includes both a text version of the transmitted file and an HTML version with embedded images. The content-type of the message itself is multipart/alternative. The message's first body part is the text alternative. The second body part is the HTML alternative. Images are included in the second part (the HTML) by defining the second part's content-type as multipart/related. The first related body part is the HTML code (text/html), and subsequent body parts are attachments containing any images (image/jpg) used by the first (HTML) related body part.
Here is a diagram of the multipart message sent by the sample code.
JavaMail Sample Code
The class that sends the email, HtmlEmailer.java, uses the JavaMail API to send an email with the structure defined above. The program takes a single argument: the name of a properties file that configures the program's behavior. The first section of the code loads the properties file and uses its values to create and initialize a new MimeMessage.
Properties props = new Properties();
props.load(new FileInputStream(propfile));
Session session = Session.getInstance(props, null);
MimeMessage msg = new MimeMessage(session);
InternetAddress from =
new InternetAddress((String)props.get
("ttjun2004.from"));
InternetAddress to =
new InternetAddress((String)props.get
("ttjun2004.to"));
msg.setFrom(from);
msg.addRecipient(Message.RecipientType.TO, to);
msg.setSubject((String)props.get("ttjun2004.subject"));
The next section of the code creates the multipart/alternative section of the message by constructing a MimeMultipart object. It also creates the first alternative body part from the text file name supplied in the props file.
// Create a "related" Multipart message
Multipart mp = new MimeMultipart("alternative");
// Read text file, load it into a BodyPart,
// and add it to the message.
String textfile = (String)props.get("ttjun2004.txtfile");
BodyPart alt_bp1 = getFileBodyPart(textfile);
mp.addBodyPart(alt_bp1);
Method getFileBodyPart is simply a convenience method for creating body parts to use as file attachments:
public BodyPart getFileBodyPart(String filename)
throws javax.mail.MessagingException {
BodyPart bp = new MimeBodyPart();
bp.setDataHandler(new DataHandler
(new FileDataSource(filename)));
return bp;
}
The DataHandler loads the file. The JavaMail classes identify and correctly set the file's Content-type header.
Returning to the class's sendmail method, the next several lines of code create a multipart/related Multipart container, and load it with its first body part: the HTML file contents.
// Include an HTML version with images. The HTML file
// is a Multipart of type "related". Inside it are
// two BodyParts: the HTML file and an image
String htmlfile = (String)props.get("ttjun2004.htmlfile");
Multipart html_mp = new MimeMultipart("related");
// Get the HTML file
BodyPart rel_bph = getFileBodyPart(htmlfile);
html_mp.addBodyPart(rel_bph);
The next task is to get the image file. The simple example program allows only a single image file, but more files would be easy to add. The properties file contains the name of the image file to attach, and also defines the content-id. The content-id must match the content-id used by the HTML file. The HTML file can use the same content-id multiple times, but the content-id should be globally unique: no two content-ids should ever be the same.
The code below creates a new MimeBodyPart for the image, and loads it with a FileDataSource that acquires the image file contents. The code then initializes several of the image properties. It then adds the image as the second related part of the HTML alternative.
// Get the image file
String imagefile = (String)props.get("ttjun2004.imagefile");
String cid = (String)props.get("ttjun2004.cid");
MimeBodyPart rel_bpi = new MimeBodyPart();
FileDataSource ifds = new FileDataSource(imagefile);
// Initialize and add the image file to the html body part
rel_bpi.setFileName(ifds.getName());
rel_bpi.setText("Image 1");
rel_bpi.setDataHandler(new DataHandler(ifds));
rel_bpi.setHeader("Content-ID", "<" + cid + ">");
rel_bpi.setDisposition("inline");
html_mp.addBodyPart(rel_bpi);
Two of the initializations above merit special attention. First, if the Content-ID header is not set, the image will not be identifiable by the HTML file. In this case, the image will not appear. By convention, the Content-ID is enclosed in angle-brackets. Also, a MIME message's content-id must be URL-encoded (using the same encoding as an HTTP GET query string). Second, the call to setDisposition("inline") configures the image to appear inline in the HTML, rather than separately at the bottom of the message. This is necessary because some mail readers show images inline only when explicitly told to do so.
The final few lines of the sendmail method create a new MimeBodyPart to wrap the MimeMultipart that forms the second alternative in the message. This code adds the second alternative to the main message body, adds the main message body to the Message object, and then uses the JavaMail Transport class to send the message.
// Create the second BodyPart of the multipart/alternative,
// set its content to the html multipart, and add the
// second bodypart to the main multipart.
BodyPart alt_bp2 = new MimeBodyPart();
alt_bp2.setContent(html_mp);
mp.addBodyPart(alt_bp2);
// Set the content for the message and transmit
msg.setContent(mp);
Transport.send(msg);
The default content (text file, html file, and image) simulates a newsletter that provides a "Space Image of the Day". In this case, the "Space Image of the Day" is a picture of the planet Saturn, accompanied by some text describing the picture. Experiment with this class by defining your own HTML content, and linking and attaching images. Don't forget to change the parameters in the properties file to reflect your changes.
See the section Running the Sample Code for instructions on how to run the sample program.
DOMAIN-NEUTRAL MESSAGING WITH JMS 1.1
Two previous Enterprise Java Technologies Tech Tips discussed the two "domains" of JMS messaging: point-to-point messaging with Queues, and publish/subscribe messaging with Topics. (See Using JMS Queues and Publish/Subscribe Messaging With JMS Topics.) JMS 1.1 unifies these two domains, allowing you to write code that works for either or both messaging domains. You can still use the older 1.0 interfaces, but writing code to the newer JMS 1.1 API makes your messaging code more flexible. This tip shows how to write JMS messaging code that works without modification in either domain.
Introduction
JMS 1.1 still offers both messaging domains that were offered in JMS 1.0: point-to-point and publish/subscribe. In JMS 1.1, these domains still use Queues and Topics, respectively. The semantics are also no different than they were in JMS 1.0. As before, point-to-point messaging is still reliable, and is always a 1-to-1 communication. Publish/subscribe is still 1-to-many. The difference between JMS 1.1 and JMS 1.0 is that you can use the same code written with the JMS 1.1 APIs for point-to-point communication with Queues, or publish/subscribe communication with Topics. Before examining the new API, let's review JMS 1.0 messaging interfaces.
Point-to-Point Messaging with Queues
Point-to-point messaging always involves exactly one sender and one receiver for any delivered message. A sender can send messages to many receivers, and a receiver can receive messages from multiple senders. However, there is only one sender and one receiver associated with an individual message. The class that serves as the rendezvous point for the message delivery is called a Queue. A Queue ensures that each message is delivered exactly once (or zero times, if it expires) to only one receiver. Only one receiver can connect to a Queue at any one time. The JMS provider (the server that implements the JMS interfaces) blocks attempts by receivers to connect to a busy Queue.
Typically, here are the steps taken by a sender to send a message to a Queue:
- Look up a
QueueConnectionFactory by name using the JNDI API.
- Look up the
Queue by name using the JNDI API.
- Use the
QueueConnectionFactory to create a QueueConnection.
- Use the
Connection to create a QueueSession.
- Get a
QueueSender and a Message object from the QueueSession.
- Initialize
Message with data to send.
- Use the
QueueSender to send the Message object.
Typically, here are the steps that the JMS 1.0 queue receiver takes:
- Look up a
QueueConnectionFactory by name using the JNDI API.
- Look up the
Queue by name using the JNDI API.
- Use the
QueueConnectionFactory to create a QueueConnection.
- Use the
Connection to create a QueueSession.
- Get a
QueueReceiver from the session.
- Call
QueueReceiver.receive, which returns a Message.
- Use the
Message data.
Publish/Subscribe Messaging with Topics
Publish/subscribe messaging delivers each message from a single publisher to several subscribers. The rendezvous point for publishers and subscribers is called a Topic. A Topic ensures that each message is delivered exactly one (or zero times, if it expires) to every receiver that was listening when the Message was published to the Topic. A feature called "durable" subscriptions even allow publishers to send Messages to subscribers that are not running when the message is sent. The next time such a subscriber connects, the messages are delivered.
Here are the typical steps taken by a publisher for publish/subscribe message delivery:
- Look up a
TopicConnectionFactory by name using the JNDI API.
- Look up the
Topic by name using the JNDI API.
- Use the
TopicConnectionFactory to create a TopicConnection.
- Use the
Connection to create a TopicSession.
- Get a
TopicPublisher and a Message object from the TopicSession.
- Initialize the
Message object with data to send.
- Use the
TopicPublisher to publish the Message object.
Note how similar the steps are to the steps for sending a message to a Queue.
To receive a message from a Topic, a subscriber typically takes the following actions:
- Look up a
TopicConnectionFactory by name using the JNDI API.
- Look up the
Topic by name using the JNDI API.
- Use the
TopicConnectionFactory to create a TopicConnection.
- Use the
Connection to create a TopicSession.
- Get a
TopicSubscriber from the session.
- Call
TopicSubscriber.receive, which returns a Message.
- Use the
Message data.
If you look at the sequence of steps for the all of these tasks, you'll notice that the steps to send messages on Queues and Topics are almost identical. Similarly, the steps for receiving messages from Queues and Topics are also almost identical.
Sample Code
The sample code for this tip consists of message sending class, JMSSender, and a receiving class, JMSReceiver. These classes can be used to send message using either Queues (point-to-point) or Topics (pub/sub). The JMS 1.1 APIs make this unification possible by using the superclasses of each of the domain-specific classes. The superclasses do not refer to the domain. Notice in sample code excerpts below how the steps in the send or receive process match the steps listed in the previous discussion of "Point-to-Point Messaging with Queues" and "Publish/Subscribe Messaging with Topics."
The sequence of steps taken by the JMSSender class looks something like this:
-
Look up the
ConnectionFactory by name. The ConnectionFactory can be either a TopicConnectionFactory or a QueueConnectionFactory. A ConnectionFactory is a named object on the server that can create a connection to a JMS server. The application server administrator creates a ConnectionFactory using the server vendor's administration tools. A program that uses JMS can look up a ConnectionFactory by name, and use it to create a JMS server connection.
ConnectionFactory connectionFactory =
(ConnectionFactory)jndiContext.lookup
(connectionFactoryName);
-
Look up the
Destination by name. The Destination can be either a Queue or a Topic.
Destination dest =
(Destination)jndiContext.lookup(destName);
-
Use a
ConnectionFactory to create a Connection. The connection can be either a TopicConnection or a QueueConnection. A Destination is a named object on the server to which messages can be sent, using a connection to that server.
Connection connection =
connectionFactory.createConnection();
-
Use the
Connection to create a Session. The first argument indicates that the session is not transactional. The second argument makes message delivery acknowledgement automatic.
Session session = connection.createSession(false,
Session.AUTO_ACKNOWLEDGE);
-
Get a
Producer and a Message object from the Session.
TextMessage message = session.createTextMessage();
Producer producer = session.createProducer(dest);
-
Initialize Message with data to send
// within a loop...
message.setText(line);
-
Use the
Producer to publish the Message object
producer.send(message);
The message is sent out to the Queue or published on the Topic, assuming that the appropriate resource names are supplied on the program's command line. (See Running the Sample Code below for further details about these resource names.)
The receiver class, JMSReceiver, also works across domains, as shown in the following excerpt from the sample code class JMSReceiver.java. The sequence of steps taken by the JMSReceiver class looks like this:
Steps 1-4: Identical to those for the JMSSender class.
5. Get a MessageConsumer from the Session. Depending on the Session, the Session will return either a QueueReceiver or a TopicSubscriber,
consumer = session.createConsumer(dest);
6. Call MessageConsumer.receive, which returns a Message. The argument "1000" means to wait 1000 ms for a response. (This code is in a loop.)
Message m = consumer.receive(1000);
7. Use the Message data
String txt = message.getText();
Sample Program Experiments
To experiment with the sample programs, you need to first do some setup work. See the section Running the Sample Code before trying the experiments below.
The sample programs both take two arguments: the name of a ConnectionFactory, and the name of a Destination. You use vendor tools to create ConnectionFactories and Destinations manually. Configure them to work with either Queues or Topics, depending on your application--both will work.
When you run either the JMSSender or JMSReceiver, you need to
provide as command-line arguments the names of the ConnectionFactory and the Destination. For example, to send with topics, execute the following command:
java JMSSender jms/TopicConnectionFactory jms/Topic
This assumes that you have configured the factory and destination with those names.
To receive with topics, execute the following command:
java JMSReceive jms/TopicConnectionFactory jms/Topic
After you enter the appropriate command, type lines of text into the standard input of the JMSSender. It will send them through the Destination to the JMSReceiver. If you run the programs with the names of a TopicConnectionFactory and a Topic, you can run multiple receivers. Try creating a QueueConnectionFactory and a Queue and verify that messages are delivered. You'll find that you get an exception if you try to start a second JMSReceiver for that Queue,
For further information about JMS 1.1, see the JMS page.
If you're attending the 2004 JavaOne Conference here are two Birds-of-a-Feather (BOF) sessions that cover JMS-related topics:
| BOF-2130 | Effectively Using Java Message Service API for Asynchronous Business Processing
|
| BOF-2280 | Reliable Messaging Cookbook: How to Make Sure Your Java Message Service (JMS) Messages Absolutely, Positively, Get There No Matter What
|
RUNNING THE SAMPLE CODE
Download the sample archive for these tips. The downloaded JAR file also contains the complete source code for the sample.
- Extract all files from the sample archive:
jar xvf ttjun2004.jar
This will create a new directory, ttjun2004
- Change to the extracted directory:
cd ttjun2004
To run Tip 1:
From the command line:
- Edit the properties file
jun2004 to customize to your environment.
- Run the program, including ".",
mail.jar and activation.jar in the classpath (enter the following command on one line):
java -classpath ".":$J2EE_HOME/lib/activation.jar:
$J2EE_HOME/lib/mail.jar" HtmlEmailer jun2004.props
The target destination identified in the properties file should receive an email that displays a picture of the planet Saturn, accompanied by some text describing the picture.
To run tip 2:
- Use your JMS provider's tools to create a
QueueConnectionFactory and a Queue, naming them jms/QCF and jms/Queue, respectively.
If you are using the J2EE 1.4 reference implementation, you can create the server objects in steps 1 and 2 by using the Admin Console as described in the section Creating JMS Administered Objects in the J2EE 1.4 Tutorial.
- Create a
TopicConnectionFactory and a Topic, naming them jms/TCF and jms/Topic, respectively.
- Package the
JMSSender and JMSReceiver classes as application clients. If you're using the J2EE 1.4 reference implementation, you can use the deploytool to do the packaging, as described in the section Packaging the Clients in the J2EE 1.4 Tutorial. When you do the packaging, make sure that the file name you specify for JMSSender is JMSSender.jar and the AppClientName is JMSSender. For JMSReceiver, the file name is JMSReceiver.jar and the AppClientName is JMSReceiver.
- The
JMSSender and JMSReceiver clients take the name of a connection factory and the name of a Queue or Topic as their two arguments. For the sender:
appclient -client JMSSender.jar jms/TCF jms/Topic
Use the same arguments for the receiver:
appclient -client JMSReceiver.jar jms/TCF jms/Topic
The sender will prompt you to enter one or more messages. You'll see that each message you send will be received by the receiver.
|
|
 |
 |
|
|
 |
 |
IMPORTANT: Please read our Licensing, Terms of Use, and Privacy policies:
http://developer.java.sun.com/berkeley_license.html
http://www.sun.com/share/text/termsofuse.html
Privacy Statement: Sun respects your online time and privacy (http://sun.com/privacy). You have received this based on your email preferences. If you would prefer not to receive this information, please follow the steps at the bottom of this message to unsubscribe.
Comments? Send your feedback on the Enterprise Java Technologies Tech Tips to: http://developers.sun.com/contact/feedback.jsp?category=newslet
Subscribe to other Java developer Tech Tips:
- Core Java Technologies Tech Tips. Get tips on using core Java technologies and APIs, such as those in the Java 2 Platform, Standard Edition (J2SE).
- 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, choose the newsletters you want to subscribe to and click
"Update".
- To unsubscribe, go to the subscriptions page, uncheck the appropriate checkbox, and click "Update".
ARCHIVES: You'll find the Enterprise Java Technologies Tech Tips archives at:
http://java.sun.com/developer/EJTechTips/index.html
Copyright 1994-2004 Sun Microsystems, Inc. All
rights reserved.
4150 Network Circle, Santa Clara, CA 95054 USA.
This document is protected by Copyright 1994-2004 Sun Microsystems, Inc. in the United States and other countries.
|