Aug 1, 1997 1:00 AM PT

Java Tip 36: Share Java objects using e-mail

The Serializable interface, new with JDK 1.1, simplifies object persistence. Here's how to transfer an object to another user via SMTP e-mail

Object persistence and sharing objects between users are fundamental to many business solutions. For example, a company might provide a mechanism for completing time sheets using a Java applet launched from its Web site. Similarly, the company may provide applets for expense reports, travel itineraries, and bug reports. In each of these scenarios, the data obtained from the user of the applet needs to be shared with the people responsible for payroll, accounts payable, travel reservations, and quality assurance. The people who perform these functions may do so in different cities or countries, are likely to work different hours than the night-owl users who tend to fill out such "forms," and should not be forced to retype the information. Enabling them to "save" and deliver pertinent objects to the business would give such applets a clear advantage over others.

There are several mechanisms for achieving object persistence such as object databases and disk files. Similarly, there are many ways to share objects such as writing data to a socket or implementing CORBA- or SOM-compliant models. Each of these alternatives has its merits and should be considered carefully when you are designing your business solution. But there is also an inexpensive-yet-reliable way to deliver copies of objects around the world using technologies and services accessible by all Internet and intranet users. This is Simple Mail Transport Protocol, or SMTP.

E-mail Java objects

A simple approach to storing and saving objects is to serialize the object and e-mail it to the intended recipient. This has the benefits of:

  • not requiring disk storage on the sending computer or NC

  • using existing systems to transmit, queue, and deliver the objects

  • allowing recipients to retrieve objects with their favorite mail client

  • providing a simple mechanism for distributing copies of the same object to many people

This approach also has its shortcomings. Here are just a few of them:

  • Delivery may be delayed considerably if an e-mail host is down. This is true of all systems, but e-mail servers often fall behind database servers in terms of priority when repairs need to be made.

  • Delivery is not actually guaranteed -- you may have to resend your message if your mail server notifies you that the message was not deliverable.

  • E-mail servers and POP clients may not be able to keep up with very large transaction volumes.

The relevance of these limitations is based on the nature of your application. For many business solutions, these shortcomings may not be important. Part of your job as architect and designer is to determine the best overall mechanisms by taking into consideration price, performance, and need.

Four steps to e-mailing Java objects

There are four steps that your applet must take in order to e-mail Java objects:

  1. Serialize the relevant objects.

  2. Base-64 encode the serialized data (per RFC 1521).

  3. Locate and connect to an SMTP server (RFC 891).

  4. Send the object to the SMTP server.

This article will show you how to mail a hypothetical bug report to the quality assurance (QA) department of your firm.

Serialize objects

Version 1.1 of the JDK provides a wonderful mechanism for serializing and reconstituting Java objects: the java.io.Serializable interface. This interface gives your applet access to methods for "saving" objects (writeObject()) and restoring them (readObject()). In many cases, using this interface is as easy as implementing it and calling these two methods.

The following code fragment defines a (very) simple BugReport object that implements the serializable interface at its easiest.

1 import java.io.*; 2 public class BugReport implements Serializable { 3 private Float m_SoftwareVersion; // version number from Help.About, e.g. "1.0" 4 private String m_ErrorDescription; // Description of error 5 private int m_Severity; // 1=System unusable - 5=Minor Aesthetic defect

6 public BugReport (Float SoftwareVersion, String ErrorDescription, int Severity) { 7 m_SoftwareVersion = SoftwareVersion; 8 m_ErrorDesctiption = ErrorDescription; 9 m_Severity = Severity; 10 }

11 public BugReport () {} // for reconstituting serialized objects

12 public void save (OutputStream os) 13 throws IOException { 14 try { 15 ObjectOutputStream o = new ObjectOutputStream(os); 16 o.writeObject(this); 17 o.flush(); 18 } 19 catch (IOException e) {throw e;} 20 }

21 public BugReport restore (InputStream is) 22 throws IOException, ClassNotFoundException { 23 BugReport RestoredBugReport = null; 24 try { 25 ObjectInputStream o = new ObjectInputStream(is); 26 RestoredBugReport = (BugReport)o.readObject(); 27 } 28 catch (IOException e) {throw e;} 29 catch (ClassNotFoundException e) {throw e;} 30 return RestoredBugReport; 31 } 32 }

Here's what the above code does:

LineDescription
1Imports references to the I/O package, including the Serializable interface.
2-5Defines the member variables of the class and indicates that this class implements the Serializable interface.
6-10Provides a simple constructor.
11A constructor that creates a "blank" bug report. This constructor is used when we reconstitute a serialized object. See example below.
12-20Defines a method for writing the object to an already open OutputStream. The first thing this method does is create an ObjectOutputStream from the OutputStream object that was passed in by the calling program unit. It then invokes the writeObject() method and explicitly flushes the output stream prior to returning to the caller.
21-30Defines a method for reading a BugReport object from an already opened InputStream. Note that the readObject() method can throw an ObjectNotFoundException exception if the next object encountered on the input stream is not of the same class as the object into which it is being read.

Using the BugReport object is really simple. Suppose we wanted to create a new BugReport and save it to a file. Here is a code fragment that we could use:

 1   import java.io.*;
     :
 2   BugReport bug = new BugReport(1.0, "Crashes when spell checker invoked", 2);
 3   FileOutputStream os = new FileOutputStream("MyBug.test");
 4   bug.save(os);

Easy, right? Of course, once the object has been serialized, nothing stops you from further manipulating the state of the object. The file created by the example above will only contain a copy of the object as it existed when it was written to disk. Therefore you have to be sure you don't inadvertently lose changes to the state of the object by failing to serialize it after all changes have been made.

Now here's how we could reconstitute a copy of the object later:

 1   import java.io.*;
    :
 2   FileInputStream fis = new FileInputSteam("MyBug.test");
 3   BugReport bug = new BugReport().restore(fis);

Even easier! Isn't Java great...and getting better!

Now we'll modify line 3 in the second example a little so that it causes the object to be written to an array of bytes instead of a file:

 
 1   import java.io.*;
     :
 2   BugReport bug = new BugReport(1.0, "Crashes when spell checker invoked", 2);
 3   ByteArrayOutputStream os = new ByteArrayOutputStream();
 4   bug.save(os);

So that's it. We have created an object and learned how to serialize it into a ByteOutputStream. Next, we will take this ByteOutputStream and convert it to a String of Base64-encoded characters.

Base64 encoding

The current standard for Internet e-mail is set forth in RFC 821, Simple Mail Transport Protocol (SMTP). For our purposes, RFC 821 imposes two important but not too onerous restrictions on the content of mail messages:

  1. Mail messages must be composed of 7-bit US-ASCII characters.

  2. Lines in mail messages must not be longer than 1,000 characters.

Our memory-resident serialized object must, therefore, be converted to some other format in order to be e-mailed via SMTP.

RFC 1521 provides a possible solution. It defines mail message bodies such that they can contain multiple kinds of data. This standard is more commonly known as multipart MIME.

According to RFC 1521:

The encoding process represents 24-bit groups of input bits as output strings of 4 encoded characters. Proceeding from left to right, a 24-bit input group is formed by concatenating 3 8-bit input groups. These 24 bits are then treated as 4 concatenated 6-bit groups, each of which is translated into a single digit in the Base64 alphabet.

This means that if we had the following 3-byte bit pattern of arbitrary binary data as input -- xC, xF3, xFF -- it would be Base64-encoded as x3, xF, xF, x3F, as illustrated below:

Base64 encoding example

The description of Base64 encoding may sound a little arcane, but the code to implement it is quite simple, as illustrated in the next code example. In that example, I have created a new class, Codecs. For now, the Codecs class has two methods: one for encoding an array of bytes, and one for encoding a String. The String-encoder simply calls the getBytes() method of the String class and then encodes the resulting array of bytes. Later, we will add methods for decoding from Base64 to the original format.

1 public class Codecs { 2 private Codecs() {} // do not instantiate this class

3 public final static String base64Encode(String strInput) { 4 if (strInput == null) return null;

5 byte byteData[] = new byte[strInput.length()]; 6 strInput.getBytes(0, strInput.length(), byteData, 0); 7 return new String(base64Encode(byteData), 0); 8 }

9 public final static byte[] base64Encode(byte[] byteData) { 10 if (byteData == null) return null; 11 int iSrcIdx; // index into source (byteData) 12 int iDestIdx; // index into destination (byteDest) 13 byte byteDest[] = new byte[((byteData.length+2)/3)*4];

14 for (iSrcIdx=0, iDestIdx=0; iSrcIdx < byteData.length-2; iSrcIdx += 3) { 15 byteDest[iDestIdx++] = (byte) ((byteData[iSrcIdx] >>> 2) & 077); 16 byteDest[iDestIdx++] = (byte) ((byteData[iSrcIdx+1] >>> 4) & 017 | (byteData[iSrcIdx] << 4) & 077); 17 byteDest[iDestIdx++] = (byte) ((byteData[iSrcIdx+2] >>> 6) & 003 | (byteData[iSrcIdx+1] << 2) & 077); 18 byteDest[iDestIdx++] = (byte) (byteData[iSrcIdx+2] & 077); 19 }

20 if (iSrcIdx < byteData.length) { 21 byteDest[iDestIdx++] = (byte) ((byteData[iSrcIdx] >>> 2) & 077); 22 if (iSrcIdx < byteData.length-1) { 23 byteDest[iDestIdx++] = (byte) ((byteData[iSrcIdx+1] >>> 4) & 017 | (byteData[iSrcIdx] << 4) & 077); 24 byteDest[iDestIdx++] = (byte) ((byteData[iSrcIdx+1] << 2) & 077); 25 } 26 else 27 byteDest[iDestIdx++] = (byte) ((byteData[iSrcIdx] << 4) & 077); 28 }

29 for (iSrcIdx = 0; iSrcIdx < iDestIdx; iSrcIdx++) { 30 if (byteDest[iSrcIdx] < 26) byteDest[iSrcIdx] = (byte)(byteDest[iSrcIdx] + 'A'); 31 else if (byteDest[iSrcIdx] < 52) byteDest[iSrcIdx] = (byte)(byteDest[iSrcIdx] + 'a'-26); 32 else if (byteDest[iSrcIdx] < 62) byteDest[iSrcIdx] = (byte)(byteDest[iSrcIdx] + '0'-52); 33 else if (byteDest[iSrcIdx] < 63) byteDest[iSrcIdx] = '+'; 34 else byteDest[iSrcIdx] = '/'; 35 }

36 for ( ; iSrcIdx < byteDest.length; iSrcIdx++) 37 byteDest[iSrcIdx] = '=';

38 return byteDest; 39 } 40 }

LineDescription
1-2Declares the class public and defines a constructor that is not meant to be called by user-written code. Generally, this class should not be instantiated.
3-8Defines an encodeBase64() method that returns a Base64-encoded version of the String passed in as an argument. It accomplishes this by calling the String.getBytes() method and passing the resulting array of bytes to encodeBase64(byte[]).
9-39Defines an encodeBase64() method that returns a Base64-encoded array of bytes based on the array of bytes passed in as an argument.
10If we received a null argument, exit this method.
11-13Declare working variables including an array of bytes that will contain the encoded data to be returned to the caller. Note that the encoded array is about 1/3 larger than the input. This is because every group of 3 bytes is being encoded into 4 bytes.
14-19Walk through the input array, 24 bits at a time, converting them from 3 groups of 8 to 4 groups of 6 with two unset bits between. This code is simpler than it first appears. Study it carefully, comparing it to the illustration above, until you understand what is going on.
20-28If the number of bytes we received in the input array was not an even multiple of 3, convert the remaining 1 or 2 bytes.
29-35Use the encoded data as indexes into the Base64 alphabet. (The Base64 alphabet is completely documented in RFC 1521.)
36-37Pad any unused bytes in the destination string with '=' characters.
38Return Base64-encoded bytes to the caller.

We're making good progress. So far we have serialized the object into a memory-resident object and converted the serialization data to Base64-encoding so that it is ready to be e-mailed to its destination user(s). To summarize what we have accomplished so far, here is a code fragment that creates an instance of the BugReport object, serializes it into memory, and Base64-encodes it:

 
 1   import java.io.*;
 2   import Codecs.base64Encode;
     :
 3   BugReport bug = new BugReport(1.0, "Crashes when spell checker invoked", 2);
 4   ByteArrayOutputStream os = new ByteArrayOutputStream();
 5   bug.save(os);
 6   String strSerializedBug = os.toString();
 7   strSerializedBug = Codecs.base64Encode(strSerializedBug);

Connecting to an SMTP Server

To send the message to our recipient(s), we have to walk through five easy steps:

  1. Obtain the DNS name for an SMTP server.

  2. Establish a TCP/IP session with the server.

  3. "Log in" to the server.

  4. Address the envelope.

  5. Stuff the envelope.

Each of these steps is discussed below.

Obtain the DNS name for an SMTP server

Just as when you send snail mail, the first thing you have to do is locate a post office that will handle the delivery process for you. For most residences and businesses, this is a simple task because they have a mailbox right outside their front door. It's that easy for Internet mail too.

You probably already have an Internet e-mail account. If so, your mail reader, either the one bundled with your Web browser or a standalone package such as Eudora, required you to enter some information regarding an SMTP server. That information is the DNS name of the SMTP server you can use. Here are some SMTP server names for various ISPs:

ISPSMTP Server
AT&Tmailhost.worldnet.att.com
Netcomsmtp.ix.netcom.com
Interaccesssmtp.interaccess.com

Your ISP will probably use a DNS name for its SMTP server that is similar to these examples.

Establish a TCP/IP session with the server

Generally, SMTP servers listen for connections on port 25. Therefore, establishing a connection to an SMTP host is simply a matter of creating a TCP/IP socket connected to port 25 of the SMTP host. Here is some Java code to connect to a hypothetical server with a DNS name of "smtp.tjd.com":

1 import java.net.*; 2 import java.io.*; 3 : 4 Socket socketSmtpServer = null; 5 DataOutputStream dos = null; 6 DataInputStream dis = null;

7 try { 8 socketSmtpServer = new Socket("smtp.tjd.com", 25); 9 dos = new DataOutputStream(socketSmtpServer.getOutputStream()); 10 dis = new DataInputStream (socketSmtpServer.getInputStream()); 11 } 12 catch (UnknownHostException e) {throw (e);} 13 catch (IOException e) {throw (e);}

Notice that this code creates a DataOutputStream object and a DataInputStream object as soon as the TCP/IP connection is made. We will use these later for sending and receiving data from the SMTP server.

"Log in" to the server

You don't really log in to an SMTP server the way you log in to a Unix system or a database. Since there is no authentication/authorization process, it's not really a log in at all. You simply identify yourself so the server can try to verify the sender. This step is not really needed, but it is rude to omit it.

When you first connect to the server, it will send you two lines of data that identify it and the version of SMTP that it speaks. For our example, we are not concerned with these data so we will just read them in and discard them. After we have read them in, the server will put us in the driver's seat and wait for and respond to commands. Here is one way to perform this login process from within a Java program:

1 String strBuf;

2 String strMyName = "tomdaley"; 3 strBuf = dis.readLine(); 4 strBuf = dis.readLine(); 5 dos.writeBytes("HELO " + strMyName + "\n"); 6 strBuf = dis.readLine(); 7 dos.writeBytes("RSET\n"); 8 strBuf = dis.readLine();

The HELO command identifies you to the SMTP server. The RSET command resets the state of the SMTP server. If everything always works well, the RSET command is not necessary. But since things don't always work perfectly and RSET is a "cheap" command to send and execute, it's a good idea to go ahead and send it in.

Note the readLine() after each writeBytes(). SMTP servers send a status message back for each command that you try to send. The status message begins with a 3-byte number that can be used to determine the success or failure of a command. RFC 821 explains this thoroughly.

Address the envelope

To continue with a snail-mail analogy, we now are ready to address the envelope. Like all polite correspondence, we will provide a return address as well as the recipient(s)' address(es) to the mail delivery agent. The next code fragment shows how to perform this in Java.

 1   dos.writeBytes("MAIL FROM:<tomdaley@ix.netcom.com>\n");
 2   dis.readLine();
 3   dos.writeBytes("RCPT TO:<tdaley@sei-it.com>\n");
 4   dos.readLine();

Stuff the envelope

Now we're ready to construct the interesting part of the message, the DATA section. The DATA section will be composed of two sections:

  1. Headers for mail clients to read

  2. MIME-encoded text and data

The headers part of the DATA section is not required but it makes your message look nicer when viewed through the recipient's mail client. The headers give the message a more professional polish and, depending on the capabilities of the recipient's mail client, can make the message easier to manage.

The headers are analogous to the top of any interoffice memorandum and can be sent thus:

 
�?1   dos.writeBytes ("DATA\N");
 2   strBuf = dis.readLine();
 3   dos.writeBytes ("To: Tom Daley <tdaley@sei-it.com>\n");
 4   dos.writeBytes ("From: Tom Daley <tomdaley@ix.netcom.com>\n");
 5   dos.writeBytes ("Subject: Bug Report\n");

Notice that we read one line after we sent the DATA command and then did not read any more. When you send the DATA command, the server responds with a message like this: 354 Enter mail, end with "." on a line by itself". That means that the SMTP server will not send any more data across the socket until it sees the end of the message.

In order to mail our serialized, encoded object, we will encapsulate it within a multipart MIME message. The first part of the message will be plain text, which, in MIME parlance, is "Content-Type: text/plain." In the text portion we will send some instructions to accompany the object and let the recipient know a little about it. Here's how to set up the MIME document and send the recipient some instructions:

 1   String strBoundary = "SimpleBoundary";
 2   String strInstructions = "Save the attached file and read it with BugNews.class.";
 2   dos.writeBytes("Mime-Version 1.0\n");
 3   dos.writeBytes("Content-Type: multipart/mixed; boundary=\"" + strBoundary + "\"\n");
 4   dos.writeBytes("--" + strBoundary + "\n");
 5   dos.writeBytes("Content-Type: text/plain; charset=\"us-ascii\"\n\n");
 6   dos.writeBytes(strInstructions + "\n");

Now we are ready to do what we've been working toward for lo these many pages: Attach the serialized Java object to our SMTP mail message. Remember that the message lines cannot exceed 1,000 bytes. For multipart MIME, there is the additional constraint that there can be no more than 74 bytes of encoded binary data per line. This means that we have to take the String object that contains our serialized, Base64-encoded object and write it out to the SMTP socket 74 bytes at a time. The following code fragment will send the MIME boundary marker, indicate that the data that follow are application data, write out the object, and wrap up this SMTP session with the server.

1 dos.writeBytes("--" + strBoundary + "\n"); 2 dos.writeBytes("Content-Type: application/octet-stream; name=\"BugReport.bug\"\n"); 3 dos.writeBytes("Content-Transfer-Encoding: base64\n"); 4 dos.writeBytes("Content-Disposition: attachment; filename=\"BugReport.bug\"\n"); 5 dos.writeBytes("Content-Description: Bug Report from a customer\n\n");

6 int iLines = strObject.length() / 74; 7 for (i = 0; i < iLines; i++) 8 dos.writeBytes(strSerializedBug.substring(i*74, i*74+74)+"\n");

9 if (iLines*74 < strSerializedBug.length()) 10 dos.writeBytes(strSerializedBug.substring(i*74, strSerializedBug.length()) + "\n");

11 dos.writeBytes("--" + strBoundary + "--\n\n");

12 dos.writeBytes("\n.\n"); 13 strBuf = dis.readLine(); 14 dos.writeBytes("QUIT\n"); 15 strBuf = dis.readLine(); 16 dos.close(); 17 dis.close(); 18 socketSmtpServer.close();

We did it!

By now our bug report is winding its way through the global SMTP mail distribution system and will soon be on someone's desk. I bet they won't be as happy to receive the bug report as you were when you figured out how to send it!

Tom Daley is director of SEI Information Technology's Financial Services Practice (http://www.sei-it.com), where he provides technical guidance and management advice to companies in the financial services industry. He has worked extensively with Java and supporting technologies in a critical line of business systems ever since he figured out that Java was not just another Starbucks wannabe. Other areas of interest include CORBA, asynchronous middleware, and real-time, high volume data distribution over the Internet.