OSDN Git Service

* java/util/Properties.java (load): Correctly read \u sequences.
[pf3gnuchains/gcc-fork.git] / libjava / java / util / Properties.java
index 2f7a251..457048c 100644 (file)
-// Properties - Property list representation.
+/* java.util.Properties
+   Copyright (C) 1998, 1999, 2000, 2001 Free Software Foundation, Inc.
 
-/* Copyright (C) 1998, 1999, 2000  Free Software Foundation
+This file is part of GNU Classpath.
 
-   This file is part of libgcj.
+GNU Classpath is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2, or (at your option)
+any later version.
 
-This software is copyrighted work licensed under the terms of the
-Libgcj License.  Please consult the file "LIBGCJ_LICENSE" for
-details.  */
+GNU Classpath is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+General Public License for more details.
 
-package java.util;
+You should have received a copy of the GNU General Public License
+along with GNU Classpath; see the file COPYING.  If not, write to the
+Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+02111-1307 USA.
 
-import java.io.BufferedWriter;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.PrintStream;
-import java.io.PrintWriter;
-import java.io.PushbackReader;
+As a special exception, if you link this library with other files to
+produce an executable, this library does not by itself cause the
+resulting executable to be covered by the GNU General Public License.
+This exception does not however invalidate any other reasons why the
+executable file might be covered by the GNU General Public License. */
 
-/**
- * @author Tom Tromey <tromey@cygnus.com>
- * @date October 26, 1998.
- */
 
-/* Written using "Java Class Libraries", 2nd edition, ISBN 0-201-31002-3
- * Status: Complete to JDK 1.2.
- */
+package java.util;
+import java.io.*;
 
+/**
+ * An example of a properties file for the german language is given
+ * here.  This extends the example given in ListResourceBundle.
+ * Create a file MyResource_de.properties with the following contents
+ * and put it in the CLASSPATH.  (The character
+ * <code>\</code><code>u00e4</code> is the german &auml;)
+ * 
+ * <pre>
+ * s1=3
+ * s2=MeineDisk
+ * s3=3. M\<code></code>u00e4rz 96
+ * s4=Die Diskette ''{1}'' enth\<code></code>u00e4lt {0} in {2}.
+ * s5=0
+ * s6=keine Dateien
+ * s7=1
+ * s8=eine Datei
+ * s9=2
+ * s10={0,number} Dateien
+ * s11=Das Formatieren schlug fehl mit folgender Exception: {0}
+ * s12=FEHLER
+ * s13=Ergebnis
+ * s14=Dialog
+ * s15=Auswahlkriterium
+ * s16=1,3
+ * </pre>
+ *
+ * Although this is a sub class of a hash table, you should never
+ * insert anything other than strings to this property, or several
+ * methods, that need string keys and values, will fail.  To ensure
+ * this, you should use the <code>get/setProperty</code> method instead
+ * of <code>get/put</code>.
+ *
+ * @see PropertyResourceBundle
+ * @author Jochen Hoenicke
+ */
 public class Properties extends Hashtable
 {
+  /**
+   * The property list that contains default values for any keys not
+   * in this property list.  
+   */
   protected Properties defaults;
 
-  public String getProperty (String propName)
-    {
-      return getProperty (propName, null);
-    }
+  private static final long serialVersionUID = 4112578634029874840L;
 
-  public String getProperty (String propName, String defVal)
-    {
-      String r = (String) get (propName);
-      if (r == null)
-       {
-         if (defaults != null)
-           r = defaults.getProperty(propName, defVal);
-         else
-           r = defVal;
-       }
-      return r;
-    }
-
-  public Object setProperty (String key, String value)
+  /**
+   * Creates a new empty property list.
+   */
+  public Properties()
   {
-    return put (key, value);
+    this.defaults = null;
   }
 
-  public void list (PrintStream out)
-    {
-      Enumeration e = propertyNames ();
-      while (e.hasMoreElements())
-       {
-         String key = (String) e.nextElement();
-         String value = getProperty(key);
-         if (value != null)
-           {
-             if (value.length() > 40)
-               {
-                 // JDK compatibility.
-                 value = value.substring(0, 37) + "...";
-               }
-             out.print(key);
-             out.print("=");
-             out.println(value);
-           }
-       }
-    }
-
-  public void list (PrintWriter writer)
-    {
-      Enumeration e = propertyNames ();
-      while (e.hasMoreElements())
-       {
-         String key = (String) e.nextElement();
-         String value = getProperty(key);
-         if (value != null)
-           {
-             if (value.length() > 40)
-               {
-                 // JDK compatibility.
-                 value = value.substring(0, 37) + "...";
-               }
-             writer.print(key);
-             writer.print("=");
-             writer.println(value);
-           }
-       }
-    }
-
-  private final boolean skip_ws (PushbackReader reader) throws IOException
-    {
-      while (true)
-       {
-         int c = reader.read();
-         if (c == -1)
-           return false;
-         // FIXME: we use our own definition of whitespace.
-         // Character.isWhitespace includes newlines, which we don't
-         // want.  Character.isSpaceChar doesn't include \t.
-         if (c != ' ' && c != '\t')
-           {
-             reader.unread(c);
-             return true;
-           }
-       }
-    }
-
-  // Note: this method needs to be rewritten for JDK 1.2.
-  // We rather arbitrarily decide that an EOF in the middle of a line
-  // means that the whole line should be ignored.  The spec doesn't
-  // specifically address this, but this interpretation seems valid.
-  public synchronized void load (InputStream in) throws IOException
-    {
-      PushbackReader reader = new PushbackReader (new InputStreamReader (in));
-
-      StringBuffer key = new StringBuffer ();
-      StringBuffer value = new StringBuffer ();
-
-    nextLine:
-      while (true)
-       {
-         key.setLength(0);
-         value.setLength(0);
-
-         // Skip leading whitespace.
-         if (! skip_ws (reader))
-           return;
-
-         // Read key until key terminator.
-         boolean first_char = true;
-         int c;
-         while (true)
-           {
-             c = reader.read();
-             if (c == -1)
-               return;
-             if (c == '\\')
-               {
-                 first_char = false;
-                 c = reader.read();
-                 if (c == -1)
-                   return;
-               }
-
-             // If we found a comment, just read to end of line and
-             // then keep going.
-             if (first_char == true && (c == '#' || c == '!'))
-               {
-                 while (c != -1 && c != '\r' && c != '\n')
-                   c = reader.read();
-                 if (c == -1)
-                   return;
-                 continue nextLine;
-               }
-
-             if (c == '\r' || c == '\n')
-               {
-                 if (first_char)
-                   continue nextLine;
-                 reader.unread(c);
-                 break;
-               }
-             // FIXME: again, our own definition of whitespace.
-             if (c == ' ' || c == '\t' || c == ':' || c == '=')
-               break;
-
-             first_char = false;
-             key.append((char) c);
-           }
-
-         // Found end of key.  Skip whitespace.  If the terminator
-         // was whitespace, also skip a single instance of a "real"
-         // terminator, and then more whitespace.
-         if (! skip_ws (reader))
-           return;
-         if (c != ':' && c != '=')
-           {
-             c = reader.read();
-             if (c == -1)
-               return;
-             if (c == ':' || c == '=')
-               {
-                 // Skip more whitespace.
-                 if (! skip_ws (reader))
-                   return;
-               }
-             else
-               reader.unread(c);
-           }
-
-         // Now read the value.
-         while (true)
-           {
-             c = reader.read();
-             if (c == -1)
-               return;
-             if (c == '\r' || c == '\n')
-               break;
-             if (c == '\\')
-               {
-                 c = reader.read();
-                 switch (c)
-                   {
-                   case -1:
-                     return;
-                   case 't':
-                     c = '\t';
-                     break;
-                   case 'r':
-                     c = '\r';
-                     break;
-                   case 'n':
-                     c = '\n';
-                     break;
-                   case 'u':
-                     c = 0;
-                     for (int i = 0; i < 4; ++i)
-                       {
-                         int x = reader.read();
-                         if (x == -1)
-                           return;
-                         int d = Character.digit((char) x, 16);
-                         // We follow JDK here: invalid characters
-                         // are treated as terminators.
-                         if (d == -1)
-                           {
-                             value.append((char) c);
-                             c = x;
-                             break;
-                           }
-                         c <<= 4;
-                         c |= d;
-                       }
-                     break;
-                   default:
-                     // Nothing.
-                   }
-               }
-             value.append((char) c);
-           }
-
-         put (key.toString(), value.toString());
-       }
-    }
-
-  public Properties ()
-    {
-      defaults = null;
-    }
-
-  public Properties (Properties defs)
-    {
-      defaults = defs;
-    }
-
-  private final void addHashEntries (Hashtable base)
-    {
-      if (defaults != null)
-       defaults.addHashEntries(base);
-      Enumeration keys = keys ();
-      while (keys.hasMoreElements())
-       base.put(keys.nextElement(), base);
-    }
+  /**
+   * Create a new empty property list with the specified default values.
+   * @param defaults a Properties object containing the default values.
+   */
+  public Properties(Properties defaults)
+  {
+    this.defaults = defaults;
+  }
 
-  public Enumeration propertyNames ()
-    {
-      // We make a new Hashtable that holds all the keys.  Then we
-      // return an enumeration for this hash.  We do this because we
-      // don't want modifications to be reflected in the enumeration
-      // (per JCL), and because there doesn't seem to be a
-      // particularly better way to ensure that duplicates are
-      // ignored.
-      Hashtable t = new Hashtable ();
-      addHashEntries (t);
-      return t.keys();
-    }
+  /**
+   * Reads a property list from an input stream.  The stream should
+   * have the following format: <br>
+   *
+   * An empty line or a line starting with <code>#</code> or
+   * <code>!</code> is ignored.  An backslash (<code>\</code>) at the
+   * end of the line makes the line continueing on the next line
+   * (but make sure there is no whitespace after the backslash).
+   * Otherwise, each line describes a key/value pair. <br>
+   *
+   * The chars up to the first whitespace, = or : are the key.  You
+   * can include this caracters in the key, if you precede them with
+   * a backslash (<code>\</code>). The key is followed by optional
+   * whitespaces, optionally one <code>=</code> or <code>:</code>,
+   * and optionally some more whitespaces.  The rest of the line is
+   * the resource belonging to the key. <br>
+   *
+   * Escape sequences <code>\t, \n, \r, \\, \", \', \!, \#, \ </code>(a
+   * space), and unicode characters with the
+   * <code>\</code><code>u</code>xxxx notation are detected, and 
+   * converted to the corresponding single character. <br>
+   *
+   * <pre>
+   * # This is a comment
+   * key     = value
+   * k\:5      \ a string starting with space and ending with newline\n
+   * # This is a multiline specification; note that the value contains
+   * # no white space.
+   * weekdays: Sunday,Monday,Tuesday,Wednesday,\
+   *           Thursday,Friday,Saturday
+   * # The safest way to include a space at the end of a value:
+   * label   = Name:\<code></code>u0020
+   * </pre>
+   *
+   * @param in the input stream
+   * @exception IOException if an error occured when reading
+   * from the input.  */
+  public void load(InputStream inStream) throws IOException
+  {
+    // The spec says that the file must be encoded using ISO-8859-1.
+    BufferedReader reader =
+      new BufferedReader(new InputStreamReader(inStream, "ISO-8859-1"));
+    String line;
+    
+    while ((line = reader.readLine()) != null)
+      {
+       char c = 0;
+       int pos = 0;
+       while (pos < line.length()
+              && Character.isWhitespace(c = line.charAt(pos)))
+         pos++;
+
+       // If line is empty or begins with a comment character,
+       // skip this line.
+       if (pos == line.length() || c == '#' || c == '!')
+         continue;
+
+       // The characters up to the next Whitespace, ':', or '='
+       // describe the key.  But look for escape sequences.
+       StringBuffer key = new StringBuffer();
+       while (pos < line.length()
+              && !Character.isWhitespace(c = line.charAt(pos++))
+              && c != '=' && c != ':')
+         {
+           if (c == '\\')
+             {
+               if (pos == line.length())
+                 {
+                   // The line continues on the next line.
+                   line = reader.readLine();
+                   pos = 0;
+                   while (pos < line.length()
+                          && Character.isWhitespace(c = line.charAt(pos)))
+                     pos++;
+                 }
+               else
+                 {
+                   c = line.charAt(pos++);
+                   switch (c)
+                     {
+                     case 'n':
+                       key.append('\n');
+                       break;
+                     case 't':
+                       key.append('\t');
+                       break;
+                     case 'r':
+                       key.append('\r');
+                       break;
+                     case 'u':
+                       if (pos + 4 <= line.length())
+                         {
+                           char uni = (char) Integer.parseInt
+                             (line.substring(pos, pos + 4), 16);
+                           key.append(uni);
+                           pos += 4;
+                         }     // else throw exception?
+                       break;
+                     default:
+                       key.append(c);
+                       break;
+                     }
+                 }
+             }
+           else
+             key.append(c);
+         }
+
+       boolean isDelim = (c == ':' || c == '=');
+       while (pos < line.length()
+              && Character.isWhitespace(c = line.charAt(pos)))
+         pos++;
+
+       if (!isDelim && (c == ':' || c == '='))
+         {
+           pos++;
+           while (pos < line.length()
+                  && Character.isWhitespace(c = line.charAt(pos)))
+             pos++;
+         }
+
+       StringBuffer element = new StringBuffer(line.length() - pos);
+       while (pos < line.length())
+         {
+           c = line.charAt(pos++);
+           if (c == '\\')
+             {
+               if (pos == line.length())
+                 {
+                   // The line continues on the next line.
+                   line = reader.readLine();
+                   pos = 0;
+                   while (pos < line.length()
+                          && Character.isWhitespace(c = line.charAt(pos)))
+                     pos++;
+                   element.ensureCapacity(line.length() - pos +
+                                          element.length());
+                 }
+               else
+                 {
+                   c = line.charAt(pos++);
+                   switch (c)
+                     {
+                     case 'n':
+                       element.append('\n');
+                       break;
+                     case 't':
+                       element.append('\t');
+                       break;
+                     case 'r':
+                       element.append('\r');
+                       break;
+                     case 'u':
+                       if (pos + 4 <= line.length())
+                         {
+                           char uni = (char) Integer.parseInt
+                             (line.substring(pos, pos + 4), 16);
+                           element.append(uni);
+                           pos += 4;
+                         }     // else throw exception?
+                       break;
+                     default:
+                       element.append(c);
+                       break;
+                     }
+                 }
+             }
+           else
+             element.append(c);
+         }
+       put(key.toString(), element.toString());
+      }
+  }
 
-  public synchronized void save (OutputStream out, String comment)
+  /**
+   * Calls <code>store(OutputStream out, String header)</code> and
+   * ignores the IOException that may be thrown.
+   * @deprecated use store instead.
+   * @exception ClassCastException if this property contains any key or
+   * value that isn't a string.
+   */
+  public void save(OutputStream out, String header)
   {
     try
       {
-       store (out, comment);
+       store(out, header);
       }
-    catch (IOException _)
+    catch (IOException ex)
       {
       }
   }
 
-  public synchronized void store (OutputStream out, String comment)
-    throws IOException
+  /**
+   * Writes the key/value pairs to the given output stream. <br>
+   *
+   * If header is not null, this method writes a comment containing
+   * the header as first line to the stream.  The next line (or first
+   * line if header is null) contains a comment with the current date.
+   * Afterwards the key/value pairs are written to the stream in the
+   * following format. <br>
+   *
+   * Each line has the form <code>key = value</code>.  Newlines,
+   * Returns and tabs are written as <code>\n,\t,\r</code> resp.
+   * The characters <code>\, !, #, =</code> and <code>:</code> are
+   * preceeded by a backslash.  Spaces are preceded with a backslash,
+   * if and only if they are at the beginning of the key.  Characters
+   * that are not in the ascii range 33 to 127 are written in the
+   * <code>\</code><code>u</code>xxxx Form.
+   *
+   * @param out the output stream
+   * @param header the header written in the first line, may be null.
+   * @exception ClassCastException if this property contains any key or
+   * value that isn't a string.
+   */
+  public void store(OutputStream out, String header) throws IOException
   {
-      // Use a buffer because writing a single string through
-      // OutputStreamWriter is fairly expensive.
-      BufferedWriter output
-       = new BufferedWriter (new OutputStreamWriter (out));
-      String newline = System.getProperty("line.separator");
+    // The spec says that the file must be encoded using ISO-8859-1.
+    PrintWriter writer
+      = new PrintWriter(new OutputStreamWriter (out, "ISO-8859-1"));
+    if (header != null)
+      writer.println("#" + header);
+    writer.println("#" + new Date().toString());
+    list(writer);
+    writer.flush();
+  }
 
-      if (comment != null)
-       {
-         // We just lose if COMMENT contains a newline.  This is
-         // what JDK 1.1 does.
-         output.write("#");
-         output.write(comment);
-         output.write(newline);
-       }
-      output.write("# ");
-      output.write(new Date().toString());
-      output.write(newline);
+  /**
+   * Adds the given key/value pair to this properties.  This calls
+   * the hashtable method put.
+   * @param key the key for this property
+   * @param value the value for this property
+   * @return The old value for the given key.
+   * @since JDK1.2 */
+  public Object setProperty(String key, String value)
+  {
+    return put(key, value);
+  }
 
-      Enumeration keys = keys ();
-      while (keys.hasMoreElements())
-       {
-         String key = (String) keys.nextElement();
-         String value = (String) get (key);
+  /**
+   * Gets the property with the specified key in this property list.
+   * If the key is not found, the default property list is searched.
+   * If the property is not found in default or the default of
+   * default, null is returned.
+   * @param key The key for this property.
+   * @param defaulValue A default value
+   * @return The value for the given key, or null if not found. 
+   * @exception ClassCastException if this property contains any key or
+   * value that isn't a string.
+   */
+  public String getProperty(String key)
+  {
+    return getProperty(key, null);
+  }
 
-         // FIXME: JCL says that the key can contain many Unicode
-         // characters.  But it also doesn't say we should encode
-         // it in any way.
-         // FIXME: if key contains ':', '=', or whitespace, must
-         // quote it here.  Note that JDK 1.1 does not do this.
-         output.write(key);
-         output.write("=");
+  /**
+   * Gets the property with the specified key in this property list.  If
+   * the key is not found, the default property list is searched.  If the
+   * property is not found in default or the default of default, the 
+   * specified defaultValue is returned.
+   * @param key The key for this property.
+   * @param defaulValue A default value
+   * @return The value for the given key.
+   * @exception ClassCastException if this property contains any key or
+   * value that isn't a string.
+   */
+  public String getProperty(String key, String defaultValue)
+  {
+    Properties prop = this;
+    // Eliminate tail recursion.
+    do
+      {
+       String value = (String) prop.get(key);
+       if (value != null)
+         return value;
+       prop = prop.defaults;
+      }
+    while (prop != null);
+    return defaultValue;
+  }
 
-         boolean leading = true;
-         for (int i = 0; i < value.length(); ++i)
-           {
-             boolean new_lead = false;
-             char c = value.charAt(i);
-             switch (c)
-               {
-               case '\n':
-                 output.write("\\n");
-                 break;
-               case '\r':
-                 output.write("\\r");
-                 break;
-               case '\t':
-                 output.write("\\t");
-                 break;
-               case '\\':
-                 output.write("\\\\");
-                 break;
+  private final void addHashEntries(Hashtable base)
+  {
+    if (defaults != null)
+      defaults.addHashEntries(base);
+    Enumeration keys = keys();
+    while (keys.hasMoreElements())
+      base.put(keys.nextElement(), base);
+  }
 
-               case '#':
-               case '!':
-               case '=':
-               case ':':
-                 output.write("\\");
-                 output.write(c);
-                 break;
+  /**
+   * Returns an enumeration of all keys in this property list, including
+   * the keys in the default property list.
+   */
+  public Enumeration propertyNames()
+  {
+    // We make a new Hashtable that holds all the keys.  Then we
+    // return an enumeration for this hash.  We do this because we
+    // don't want modifications to be reflected in the enumeration
+    // (per JCL), and because there doesn't seem to be a
+    // particularly better way to ensure that duplicates are
+    // ignored.
+    Hashtable t = new Hashtable();
+    addHashEntries(t);
+    return t.keys();
+  }
 
-               case ' ':
-                 new_lead = leading;
-                 if (leading)
-                   output.write("\\");
-                 output.write(c);
-                 break;
+  /**
+   * Formats a key/value pair for output in a properties file.
+   * See store for a description of the format.
+   * @param key the key.
+   * @param value the value.
+   * @see #store
+   */
+  private String formatForOutput(String key, String value)
+  {
+    // This is a simple approximation of the expected line size.
+    StringBuffer result =
+      new StringBuffer(key.length() + value.length() + 16);
+    boolean head = true;
+    for (int i = 0; i < key.length(); i++)
+      {
+       char c = key.charAt(i);
+       switch (c)
+         {
+         case '\n':
+           result.append("\\n");
+           break;
+         case '\r':
+           result.append("\\r");
+           break;
+         case '\t':
+           result.append("\\t");
+           break;
+         case '\\':
+           result.append("\\\\");
+           break;
+         case '!':
+           result.append("\\!");
+           break;
+         case '#':
+           result.append("\\#");
+           break;
+         case '=':
+           result.append("\\=");
+           break;
+         case ':':
+           result.append("\\:");
+           break;
+         case ' ':
+           result.append("\\ ");
+           break;
+         default:
+           if (c < 32 || c > '~')
+             {
+               String hex = Integer.toHexString(c);
+               result.append("\\u0000".substring(0, 6 - hex.length()));
+               result.append(hex);
+             }
+           else
+               result.append(c);
+         }
+       if (c != 32)
+         head = false;
+      }
+    result.append('=');
+    head = true;
+    for (int i = 0; i < value.length(); i++)
+      {
+       char c = value.charAt(i);
+       switch (c)
+         {
+         case '\n':
+           result.append("\\n");
+           break;
+         case '\r':
+           result.append("\\r");
+           break;
+         case '\t':
+           result.append("\\t");
+           break;
+         case '\\':
+           result.append("\\\\");
+           break;
+         case '!':
+           result.append("\\!");
+           break;
+         case '#':
+           result.append("\\#");
+           break;
+         case ' ':
+           result.append(head ? "\\ " : " ");
+           break;
+         default:
+           if (c < 32 || c > '~')
+             {
+               String hex = Integer.toHexString(c);
+               result.append("\\u0000".substring(0, 6 - hex.length()));
+               result.append(hex);
+             }
+           else
+             result.append(c);
+         }
+       if (c != 32)
+         head = false;
+      }
+    return result.toString();
+  }
 
-               default:
-                 if (c < '\u0020' || c > '\u007e')
-                   {
-                     output.write("\\u");
-                     output.write(Character.forDigit(c >>> 12, 16));
-                     output.write(Character.forDigit((c >>> 8) & 0xff,
-                                                     16));
-                     output.write(Character.forDigit((c >>> 4) & 0xff,
-                                                     16));
-                     output.write(Character.forDigit(c & 0xff, 16));
-                   }
-                 else
-                   output.write(c);
-               }
-             leading = new_lead;
-           }
-         output.write(newline);
-       }
+  /**
+   * Writes the key/value pairs to the given print stream.  They are
+   * written in the way, described in the method store.
+   * @param out the stream, where the key/value pairs are written to.
+   * @exception ClassCastException if this property contains any key or
+   * value that isn't a string.
+   * @see #store
+   */
+  public void list(PrintStream out)
+  {
+    Enumeration keys = keys();
+    Enumeration elts = elements();
+    while (keys.hasMoreElements())
+      {
+       String key = (String) keys.nextElement();
+       String elt = (String) elts.nextElement();
+       String output = formatForOutput(key, elt);
+       out.println(output);
+      }
+  }
 
-      output.flush();
+  /**
+   * Writes the key/value pairs to the given print writer.  They are
+   * written in the way, described in the method store.
+   * @param out the writer, where the key/value pairs are written to.
+   * @exception ClassCastException if this property contains any key or
+   * value that isn't a string.
+   * @see #store
+   * @see #list(java.io.PrintStream)
+   * @since JDK1.1
+   */
+  public void list(PrintWriter out)
+  {
+    Enumeration keys = keys();
+    Enumeration elts = elements();
+    while (keys.hasMoreElements())
+      {
+       String key = (String) keys.nextElement();
+       String elt = (String) elts.nextElement();
+       String output = formatForOutput(key, elt);
+       out.println(output);
+      }
   }
 }