Tuesday, February 14, 2006

Fresh, hot JNLP muffins

An OutputStream wrapper class for JNLP muffins (which are to JNLP as cookies are to HTTP):

  1 public class MuffinOutputStream
  2         extends DataOutputStream {
  3     final String name;
  4 
  5     /**
  6      * Constructs a new {@code MuffinOutputStream} for
  7      * the given <var>name</var>.  Nothing
  8      * happens<em>even on writes</em>  9      * until {@link #close()} when everything is written
 10      * at once after all the writes are collected
 11      * together first before committing.
 12      *
 13      * @param name the muffin name relative to the JNLP
 14      * code base
 15      *
 16      * @see PersistenceService#get(URL)
 17      * @see PersistenceService#create(URL, long)
 18      */
 19     public MuffinOutputStream(final String name) {
 20         super(new ByteArrayOutputStream());
 21 
 22         this.name = name;
 23     }
 24 
 25     /**
 26      * Really writes the queued data to the muffin.
 27      * <p/>
 28      * {@inheritDoc}
 29      *
 30      * @throws IOException if the muffin cannot be created,
 31      * opened or written
 32      */
 33     @Override
 34     public void close()
 35             throws IOException {
 36         super.close();
 37 
 38         final ByteArrayOutputStream baos
 39                 = (ByteArrayOutputStream) out;
 40         final OutputStream muffinStream = open(name,
 41                 baos.size());
 42 
 43         copyStreams(new ByteArrayInputStream(
 44                 baos.toByteArray()), muffinStream);
 45 
 46         muffinStream.flush();
 47         muffinStream.close();
 48     }
 49 
 50     private static OutputStream open(final String name,
 51             final int maxsize)
 52             throws IOException {
 53         try {
 54             final PersistenceService ps = lookup(
 55                     PersistenceService.class);
 56             final URL codeBase = lookup(
 57                     BasicService.class).getCodeBase();
 58             // Escape bad URL characters
 59             final URL muffin = createMuffin(codeBase,
 60                     encode(name, "UTF-8"));
 61 
 62             // Parent must exist, but do not clobber if so
 63             boolean parentAlreadyExists = false;
 64 
 65             try {
 66                 try {
 67                     ps.create(codeBase, 0);
 68 
 69                 } catch (final IOException e) {
 70                     parentAlreadyExists = true;
 71                 }
 72 
 73                 try {
 74                     // Remove exising in case size is wrong
 75                     ps.delete(muffin);
 76                 } catch (final IOException e) {
 77                     // Truly ignore: ok to fail on deletion
 78                 }
 79 
 80                 ps.create(muffin, maxsize);
 81                 // Saved on client, not on server
 82                 ps.setTag(muffin, DIRTY);
 83 
 84                 return ps.get(muffin).getOutputStream(true);
 85 
 86             } finally {
 87                 if (!parentAlreadyExists)
 88                     ps.delete(codeBase);
 89             }
 90 
 91         } catch (final IOException wrapped) {
 92             final IOException e = new IOException(
 93                     "Cannot save data to " + name);
 94 
 95             e.initCause(wrapped);
 96 
 97             throw e;
 98         }
 99     }
100 }

(Note several helper methods used in the code, but not presented here: lookup, createMuffin and copyStreams.)

Writing MuffinInputStream is much simpler since one need not worry about parent muffins, nor bunch up the output to work out the size of the muffin before persisting. And neither does the "delay until close" pattern come into play. This is one of those times I wish I had C++ destructors for my Java.

No comments: