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 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*; 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:

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*;
 2   BugReport bug = new BugReport(1.0, "Crashes when spell checker invoked", 2);
 3   FileOutputStream os = new FileOutputStream("MyBug.test");

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*;
 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*;
 2   BugReport bug = new BugReport(1.0, "Crashes when spell checker invoked", 2);
 3   ByteArrayOutputStream os = new ByteArrayOutputStream();

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 }

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.
1 2 Page 1
Page 1 of 2