Source for java.util.Properties

   1: /* Properties.java -- a set of persistent properties
   2:    Copyright (C) 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005  Free Software Foundation, Inc.
   3: 
   4: This file is part of GNU Classpath.
   5: 
   6: GNU Classpath is free software; you can redistribute it and/or modify
   7: it under the terms of the GNU General Public License as published by
   8: the Free Software Foundation; either version 2, or (at your option)
   9: any later version.
  10: 
  11: GNU Classpath is distributed in the hope that it will be useful, but
  12: WITHOUT ANY WARRANTY; without even the implied warranty of
  13: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  14: General Public License for more details.
  15: 
  16: You should have received a copy of the GNU General Public License
  17: along with GNU Classpath; see the file COPYING.  If not, write to the
  18: Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  19: 02110-1301 USA.
  20: 
  21: Linking this library statically or dynamically with other modules is
  22: making a combined work based on this library.  Thus, the terms and
  23: conditions of the GNU General Public License cover the whole
  24: combination.
  25: 
  26: As a special exception, the copyright holders of this library give you
  27: permission to link this library with independent modules to produce an
  28: executable, regardless of the license terms of these independent
  29: modules, and to copy and distribute the resulting executable under
  30: terms of your choice, provided that you also meet, for each linked
  31: independent module, the terms and conditions of the license of that
  32: module.  An independent module is a module which is not derived from
  33: or based on this library.  If you modify this library, you may extend
  34: this exception to your version of the library, but you are not
  35: obligated to do so.  If you do not wish to do so, delete this
  36: exception statement from your version. */
  37: 
  38: 
  39: package java.util;
  40: 
  41: import java.io.BufferedReader;
  42: import java.io.IOException;
  43: import java.io.InputStream;
  44: import java.io.InputStreamReader;
  45: import java.io.OutputStream;
  46: import java.io.OutputStreamWriter;
  47: import java.io.PrintStream;
  48: import java.io.PrintWriter;
  49: 
  50: import javax.xml.parsers.ParserConfigurationException;
  51: import javax.xml.parsers.SAXParser;
  52: import javax.xml.parsers.SAXParserFactory;
  53: 
  54: import org.xml.sax.Attributes;
  55: import org.xml.sax.InputSource;
  56: import org.xml.sax.SAXException;
  57: import org.xml.sax.XMLReader;
  58: import org.xml.sax.ext.DefaultHandler2;
  59: 
  60: import org.w3c.dom.Document;
  61: import org.w3c.dom.DocumentType;
  62: import org.w3c.dom.DOMImplementation;
  63: import org.w3c.dom.Element;
  64: import org.w3c.dom.bootstrap.DOMImplementationRegistry;
  65: import org.w3c.dom.ls.DOMImplementationLS;
  66: import org.w3c.dom.ls.LSOutput;
  67: import org.w3c.dom.ls.LSSerializer;
  68: 
  69: /**
  70:  * A set of persistent properties, which can be saved or loaded from a stream.
  71:  * A property list may also contain defaults, searched if the main list
  72:  * does not contain a property for a given key.
  73:  *
  74:  * An example of a properties file for the german language is given
  75:  * here.  This extends the example given in ListResourceBundle.
  76:  * Create a file MyResource_de.properties with the following contents
  77:  * and put it in the CLASSPATH.  (The character
  78:  * <code>\</code><code>u00e4</code> is the german umlaut)
  79:  *
  80:  * 
  81: <pre>s1=3
  82: s2=MeineDisk
  83: s3=3. M\<code></code>u00e4rz 96
  84: s4=Die Diskette ''{1}'' enth\<code></code>u00e4lt {0} in {2}.
  85: s5=0
  86: s6=keine Dateien
  87: s7=1
  88: s8=eine Datei
  89: s9=2
  90: s10={0,number} Dateien
  91: s11=Das Formatieren schlug fehl mit folgender Exception: {0}
  92: s12=FEHLER
  93: s13=Ergebnis
  94: s14=Dialog
  95: s15=Auswahlkriterium
  96: s16=1,3</pre>
  97:  *
  98:  * <p>Although this is a sub class of a hash table, you should never
  99:  * insert anything other than strings to this property, or several
 100:  * methods, that need string keys and values, will fail.  To ensure
 101:  * this, you should use the <code>get/setProperty</code> method instead
 102:  * of <code>get/put</code>.
 103:  *
 104:  * Properties are saved in ISO 8859-1 encoding, using Unicode escapes with
 105:  * a single <code>u</code> for any character which cannot be represented.
 106:  *
 107:  * @author Jochen Hoenicke
 108:  * @author Eric Blake (ebb9@email.byu.edu)
 109:  * @see PropertyResourceBundle
 110:  * @status updated to 1.4
 111:  */
 112: public class Properties extends Hashtable
 113: {
 114:   // WARNING: Properties is a CORE class in the bootstrap cycle. See the
 115:   // comments in vm/reference/java/lang/Runtime for implications of this fact.
 116: 
 117:   /**
 118:    * The property list that contains default values for any keys not
 119:    * in this property list.
 120:    *
 121:    * @serial the default properties
 122:    */
 123:   protected Properties defaults;
 124: 
 125:   /**
 126:    * Compatible with JDK 1.0+.
 127:    */
 128:   private static final long serialVersionUID = 4112578634029874840L;
 129: 
 130:   /**
 131:    * Creates a new empty property list with no default values.
 132:    */
 133:   public Properties()
 134:   {
 135:   }
 136: 
 137:   /**
 138:    * Create a new empty property list with the specified default values.
 139:    *
 140:    * @param defaults a Properties object containing the default values
 141:    */
 142:   public Properties(Properties defaults)
 143:   {
 144:     this.defaults = defaults;
 145:   }
 146: 
 147:   /**
 148:    * Adds the given key/value pair to this properties.  This calls
 149:    * the hashtable method put.
 150:    *
 151:    * @param key the key for this property
 152:    * @param value the value for this property
 153:    * @return The old value for the given key
 154:    * @see #getProperty(String)
 155:    * @since 1.2
 156:    */
 157:   public Object setProperty(String key, String value)
 158:   {
 159:     return put(key, value);
 160:   }
 161: 
 162:   /**
 163:    * Reads a property list from an input stream.  The stream should
 164:    * have the following format: <br>
 165:    *
 166:    * An empty line or a line starting with <code>#</code> or
 167:    * <code>!</code> is ignored.  An backslash (<code>\</code>) at the
 168:    * end of the line makes the line continueing on the next line
 169:    * (but make sure there is no whitespace after the backslash).
 170:    * Otherwise, each line describes a key/value pair. <br>
 171:    *
 172:    * The chars up to the first whitespace, = or : are the key.  You
 173:    * can include this caracters in the key, if you precede them with
 174:    * a backslash (<code>\</code>). The key is followed by optional
 175:    * whitespaces, optionally one <code>=</code> or <code>:</code>,
 176:    * and optionally some more whitespaces.  The rest of the line is
 177:    * the resource belonging to the key. <br>
 178:    *
 179:    * Escape sequences <code>\t, \n, \r, \\, \", \', \!, \#, \ </code>(a
 180:    * space), and unicode characters with the
 181:    * <code>\\u</code><em>xxxx</em> notation are detected, and
 182:    * converted to the corresponding single character. <br>
 183:    *
 184:    * 
 185: <pre># This is a comment
 186: key     = value
 187: k\:5      \ a string starting with space and ending with newline\n
 188: # This is a multiline specification; note that the value contains
 189: # no white space.
 190: weekdays: Sunday,Monday,Tuesday,Wednesday,\\
 191:           Thursday,Friday,Saturday
 192: # The safest way to include a space at the end of a value:
 193: label   = Name:\\u0020</pre>
 194:    *
 195:    * @param inStream the input stream
 196:    * @throws IOException if an error occurred when reading the input
 197:    * @throws NullPointerException if in is null
 198:    */
 199:   public void load(InputStream inStream) throws IOException
 200:   {
 201:     // The spec says that the file must be encoded using ISO-8859-1.
 202:     BufferedReader reader =
 203:       new BufferedReader(new InputStreamReader(inStream, "ISO-8859-1"));
 204:     String line;
 205: 
 206:     while ((line = reader.readLine()) != null)
 207:       {
 208:         char c = 0;
 209:         int pos = 0;
 210:     // Leading whitespaces must be deleted first.
 211:         while (pos < line.length()
 212:                && Character.isWhitespace(c = line.charAt(pos)))
 213:           pos++;
 214: 
 215:         // If empty line or begins with a comment character, skip this line.
 216:         if ((line.length() - pos) == 0
 217:         || line.charAt(pos) == '#' || line.charAt(pos) == '!')
 218:           continue;
 219: 
 220:         // The characters up to the next Whitespace, ':', or '='
 221:         // describe the key.  But look for escape sequences.
 222:     // Try to short-circuit when there is no escape char.
 223:     int start = pos;
 224:     boolean needsEscape = line.indexOf('\\', pos) != -1;
 225:         StringBuilder key = needsEscape ? new StringBuilder() : null;
 226:         while (pos < line.length()
 227:                && ! Character.isWhitespace(c = line.charAt(pos++))
 228:                && c != '=' && c != ':')
 229:           {
 230:             if (needsEscape && c == '\\')
 231:               {
 232:                 if (pos == line.length())
 233:                   {
 234:                     // The line continues on the next line.  If there
 235:                     // is no next line, just treat it as a key with an
 236:                     // empty value.
 237:                     line = reader.readLine();
 238:             if (line == null)
 239:               line = "";
 240:                     pos = 0;
 241:                     while (pos < line.length()
 242:                            && Character.isWhitespace(c = line.charAt(pos)))
 243:                       pos++;
 244:                   }
 245:                 else
 246:                   {
 247:                     c = line.charAt(pos++);
 248:                     switch (c)
 249:                       {
 250:                       case 'n':
 251:                         key.append('\n');
 252:                         break;
 253:                       case 't':
 254:                         key.append('\t');
 255:                         break;
 256:                       case 'r':
 257:                         key.append('\r');
 258:                         break;
 259:                       case 'u':
 260:                         if (pos + 4 <= line.length())
 261:                           {
 262:                             char uni = (char) Integer.parseInt
 263:                               (line.substring(pos, pos + 4), 16);
 264:                             key.append(uni);
 265:                             pos += 4;
 266:                           }        // else throw exception?
 267:                         break;
 268:                       default:
 269:                         key.append(c);
 270:                         break;
 271:                       }
 272:                   }
 273:               }
 274:             else if (needsEscape)
 275:               key.append(c);
 276:           }
 277: 
 278:         boolean isDelim = (c == ':' || c == '=');
 279: 
 280:     String keyString;
 281:     if (needsEscape)
 282:       keyString = key.toString();
 283:     else if (isDelim || Character.isWhitespace(c))
 284:       keyString = line.substring(start, pos - 1);
 285:     else
 286:       keyString = line.substring(start, pos);
 287: 
 288:         while (pos < line.length()
 289:                && Character.isWhitespace(c = line.charAt(pos)))
 290:           pos++;
 291: 
 292:         if (! isDelim && (c == ':' || c == '='))
 293:           {
 294:             pos++;
 295:             while (pos < line.length()
 296:                    && Character.isWhitespace(c = line.charAt(pos)))
 297:               pos++;
 298:           }
 299: 
 300:     // Short-circuit if no escape chars found.
 301:     if (!needsEscape)
 302:       {
 303:         put(keyString, line.substring(pos));
 304:         continue;
 305:       }
 306: 
 307:     // Escape char found so iterate through the rest of the line.
 308:         StringBuilder element = new StringBuilder(line.length() - pos);
 309:         while (pos < line.length())
 310:           {
 311:             c = line.charAt(pos++);
 312:             if (c == '\\')
 313:               {
 314:                 if (pos == line.length())
 315:                   {
 316:                     // The line continues on the next line.
 317:                     line = reader.readLine();
 318: 
 319:             // We might have seen a backslash at the end of
 320:             // the file.  The JDK ignores the backslash in
 321:             // this case, so we follow for compatibility.
 322:             if (line == null)
 323:               break;
 324: 
 325:                     pos = 0;
 326:                     while (pos < line.length()
 327:                            && Character.isWhitespace(c = line.charAt(pos)))
 328:                       pos++;
 329:                     element.ensureCapacity(line.length() - pos +
 330:                                            element.length());
 331:                   }
 332:                 else
 333:                   {
 334:                     c = line.charAt(pos++);
 335:                     switch (c)
 336:                       {
 337:                       case 'n':
 338:                         element.append('\n');
 339:                         break;
 340:                       case 't':
 341:                         element.append('\t');
 342:                         break;
 343:                       case 'r':
 344:                         element.append('\r');
 345:                         break;
 346:                       case 'u':
 347:                         if (pos + 4 <= line.length())
 348:                           {
 349:                             char uni = (char) Integer.parseInt
 350:                               (line.substring(pos, pos + 4), 16);
 351:                             element.append(uni);
 352:                             pos += 4;
 353:                           }        // else throw exception?
 354:                         break;
 355:                       default:
 356:                         element.append(c);
 357:                         break;
 358:                       }
 359:                   }
 360:               }
 361:             else
 362:               element.append(c);
 363:           }
 364:         put(keyString, element.toString());
 365:       }
 366:   }
 367: 
 368:   /**
 369:    * Calls <code>store(OutputStream out, String header)</code> and
 370:    * ignores the IOException that may be thrown.
 371:    *
 372:    * @param out the stream to write to
 373:    * @param header a description of the property list
 374:    * @throws ClassCastException if this property contains any key or
 375:    *         value that are not strings
 376:    * @deprecated use {@link #store(OutputStream, String)} instead
 377:    */
 378:   public void save(OutputStream out, String header)
 379:   {
 380:     try
 381:       {
 382:         store(out, header);
 383:       }
 384:     catch (IOException ex)
 385:       {
 386:       }
 387:   }
 388: 
 389:   /**
 390:    * Writes the key/value pairs to the given output stream, in a format
 391:    * suitable for <code>load</code>.<br>
 392:    *
 393:    * If header is not null, this method writes a comment containing
 394:    * the header as first line to the stream.  The next line (or first
 395:    * line if header is null) contains a comment with the current date.
 396:    * Afterwards the key/value pairs are written to the stream in the
 397:    * following format.<br>
 398:    *
 399:    * Each line has the form <code>key = value</code>.  Newlines,
 400:    * Returns and tabs are written as <code>\n,\t,\r</code> resp.
 401:    * The characters <code>\, !, #, =</code> and <code>:</code> are
 402:    * preceeded by a backslash.  Spaces are preceded with a backslash,
 403:    * if and only if they are at the beginning of the key.  Characters
 404:    * that are not in the ascii range 33 to 127 are written in the
 405:    * <code>\</code><code>u</code>xxxx Form.<br>
 406:    *
 407:    * Following the listing, the output stream is flushed but left open.
 408:    *
 409:    * @param out the output stream
 410:    * @param header the header written in the first line, may be null
 411:    * @throws ClassCastException if this property contains any key or
 412:    *         value that isn't a string
 413:    * @throws IOException if writing to the stream fails
 414:    * @throws NullPointerException if out is null
 415:    * @since 1.2
 416:    */
 417:   public void store(OutputStream out, String header) throws IOException
 418:   {
 419:     // The spec says that the file must be encoded using ISO-8859-1.
 420:     PrintWriter writer
 421:       = new PrintWriter(new OutputStreamWriter(out, "ISO-8859-1"));
 422:     if (header != null)
 423:       writer.println("#" + header);
 424:     writer.println ("#" + Calendar.getInstance ().getTime ());
 425:     
 426:     Iterator iter = entrySet ().iterator ();
 427:     int i = size ();
 428:     StringBuilder s = new StringBuilder (); // Reuse the same buffer.
 429:     while (--i >= 0)
 430:       {
 431:         Map.Entry entry = (Map.Entry) iter.next ();
 432:         formatForOutput ((String) entry.getKey (), s, true);
 433:         s.append ('=');
 434:         formatForOutput ((String) entry.getValue (), s, false);
 435:         writer.println (s);
 436:       }
 437: 
 438:     writer.flush ();
 439:   }
 440: 
 441:   /**
 442:    * Gets the property with the specified key in this property list.
 443:    * If the key is not found, the default property list is searched.
 444:    * If the property is not found in the default, null is returned.
 445:    *
 446:    * @param key The key for this property
 447:    * @return the value for the given key, or null if not found
 448:    * @throws ClassCastException if this property contains any key or
 449:    *         value that isn't a string
 450:    * @see #defaults
 451:    * @see #setProperty(String, String)
 452:    * @see #getProperty(String, String)
 453:    */
 454:   public String getProperty(String key)
 455:   {
 456:     Properties prop = this;
 457:     // Eliminate tail recursion.
 458:     do
 459:       {
 460:         String value = (String) prop.get(key);
 461:         if (value != null)
 462:           return value;
 463:         prop = prop.defaults;
 464:       }
 465:     while (prop != null);
 466:     return null;
 467:   }
 468: 
 469:   /**
 470:    * Gets the property with the specified key in this property list.  If
 471:    * the key is not found, the default property list is searched.  If the
 472:    * property is not found in the default, the specified defaultValue is
 473:    * returned.
 474:    *
 475:    * @param key The key for this property
 476:    * @param defaultValue A default value
 477:    * @return The value for the given key
 478:    * @throws ClassCastException if this property contains any key or
 479:    *         value that isn't a string
 480:    * @see #defaults
 481:    * @see #setProperty(String, String)
 482:    */
 483:   public String getProperty(String key, String defaultValue)
 484:   {
 485:     String prop = getProperty(key);
 486:     if (prop == null)
 487:       prop = defaultValue;
 488:     return prop;
 489:   }
 490: 
 491:   /**
 492:    * Returns an enumeration of all keys in this property list, including
 493:    * the keys in the default property list.
 494:    *
 495:    * @return an Enumeration of all defined keys
 496:    */
 497:   public Enumeration propertyNames()
 498:   {
 499:     // We make a new Set that holds all the keys, then return an enumeration
 500:     // for that. This prevents modifications from ruining the enumeration,
 501:     // as well as ignoring duplicates.
 502:     Properties prop = this;
 503:     Set s = new HashSet();
 504:     // Eliminate tail recursion.
 505:     do
 506:       {
 507:         s.addAll(prop.keySet());
 508:         prop = prop.defaults;
 509:       }
 510:     while (prop != null);
 511:     return Collections.enumeration(s);
 512:   }
 513: 
 514:   /**
 515:    * Prints the key/value pairs to the given print stream.  This is 
 516:    * mainly useful for debugging purposes.
 517:    *
 518:    * @param out the print stream, where the key/value pairs are written to
 519:    * @throws ClassCastException if this property contains a key or a
 520:    *         value that isn't a string
 521:    * @see #list(PrintWriter)
 522:    */
 523:   public void list(PrintStream out)
 524:   {
 525:     PrintWriter writer = new PrintWriter (out);
 526:     list (writer);
 527:   }
 528: 
 529:   /**
 530:    * Prints the key/value pairs to the given print writer.  This is
 531:    * mainly useful for debugging purposes.
 532:    *
 533:    * @param out the print writer where the key/value pairs are written to
 534:    * @throws ClassCastException if this property contains a key or a
 535:    *         value that isn't a string
 536:    * @see #list(PrintStream)
 537:    * @since 1.1
 538:    */
 539:   public void list(PrintWriter out)
 540:   {
 541:     out.println ("-- listing properties --");
 542: 
 543:     Iterator iter = entrySet ().iterator ();
 544:     int i = size ();
 545:     while (--i >= 0)
 546:       {
 547:         Map.Entry entry = (Map.Entry) iter.next ();
 548:         out.print ((String) entry.getKey () + "=");
 549: 
 550:         // JDK 1.3/1.4 restrict the printed value, but not the key,
 551:         // to 40 characters, including the truncating ellipsis.
 552:         String s = (String ) entry.getValue ();
 553:         if (s != null && s.length () > 40)
 554:           out.println (s.substring (0, 37) + "...");
 555:         else
 556:           out.println (s);
 557:       }
 558:     out.flush ();
 559:   }
 560: 
 561:   /**
 562:    * Formats a key or value for output in a properties file.
 563:    * See store for a description of the format.
 564:    *
 565:    * @param str the string to format
 566:    * @param buffer the buffer to add it to
 567:    * @param key true if all ' ' must be escaped for the key, false if only
 568:    *        leading spaces must be escaped for the value
 569:    * @see #store(OutputStream, String)
 570:    */
 571:   private void formatForOutput(String str, StringBuilder buffer, boolean key)
 572:   {
 573:     if (key)
 574:       {
 575:         buffer.setLength(0);
 576:         buffer.ensureCapacity(str.length());
 577:       }
 578:     else
 579:       buffer.ensureCapacity(buffer.length() + str.length());
 580:     boolean head = true;
 581:     int size = str.length();
 582:     for (int i = 0; i < size; i++)
 583:       {
 584:         char c = str.charAt(i);
 585:         switch (c)
 586:           {
 587:           case '\n':
 588:             buffer.append("\\n");
 589:             break;
 590:           case '\r':
 591:             buffer.append("\\r");
 592:             break;
 593:           case '\t':
 594:             buffer.append("\\t");
 595:             break;
 596:           case ' ':
 597:             buffer.append(head ? "\\ " : " ");
 598:             break;
 599:           case '\\':
 600:           case '!':
 601:           case '#':
 602:           case '=':
 603:           case ':':
 604:             buffer.append('\\').append(c);
 605:             break;
 606:           default:
 607:             if (c < ' ' || c > '~')
 608:               {
 609:                 String hex = Integer.toHexString(c);
 610:                 buffer.append("\\u0000".substring(0, 6 - hex.length()));
 611:                 buffer.append(hex);
 612:               }
 613:             else
 614:               buffer.append(c);
 615:           }
 616:         if (c != ' ')
 617:           head = key;
 618:       }
 619:   }
 620: 
 621:   /**
 622:    * <p>
 623:    * Encodes the properties as an XML file using the UTF-8 encoding.
 624:    * The format of the XML file matches the DTD
 625:    * <a href="http://java.sun.com/dtd/properties.dtd">
 626:    * http://java.sun.com/dtd/properties.dtd</a>.
 627:    * </p>
 628:    * <p>
 629:    * Invoking this method provides the same behaviour as invoking
 630:    * <code>storeToXML(os, comment, "UTF-8")</code>.
 631:    * </p>
 632:    * 
 633:    * @param os the stream to output to.
 634:    * @param comment a comment to include at the top of the XML file, or
 635:    *                <code>null</code> if one is not required.
 636:    * @throws IOException if the serialization fails.
 637:    * @throws NullPointerException if <code>os</code> is null.
 638:    * @since 1.5
 639:    */
 640:   public void storeToXML(OutputStream os, String comment)
 641:     throws IOException
 642:   {
 643:     storeToXML(os, comment, "UTF-8");
 644:   }
 645: 
 646:   /**
 647:    * <p>
 648:    * Encodes the properties as an XML file using the supplied encoding.
 649:    * The format of the XML file matches the DTD
 650:    * <a href="http://java.sun.com/dtd/properties.dtd">
 651:    * http://java.sun.com/dtd/properties.dtd</a>.
 652:    * </p>
 653:    * 
 654:    * @param os the stream to output to.
 655:    * @param comment a comment to include at the top of the XML file, or
 656:    *                <code>null</code> if one is not required.
 657:    * @param encoding the encoding to use for the XML output.
 658:    * @throws IOException if the serialization fails.
 659:    * @throws NullPointerException if <code>os</code> or <code>encoding</code>
 660:    *                              is null.
 661:    * @since 1.5
 662:    */
 663:   public void storeToXML(OutputStream os, String comment, String encoding)
 664:     throws IOException
 665:   {
 666:     if (os == null)
 667:       throw new NullPointerException("Null output stream supplied.");
 668:     if (encoding == null)
 669:       throw new NullPointerException("Null encoding supplied.");
 670:     try
 671:       {
 672:     DOMImplementationRegistry registry = 
 673:       DOMImplementationRegistry.newInstance();
 674:     DOMImplementation domImpl = registry.getDOMImplementation("LS 3.0");
 675:     DocumentType doctype =
 676:       domImpl.createDocumentType("properties", null,
 677:                      "http://java.sun.com/dtd/properties.dtd");
 678:     Document doc = domImpl.createDocument(null, "properties", doctype);
 679:     Element root = doc.getDocumentElement();
 680:     if (comment != null)
 681:       {
 682:         Element commentElement = doc.createElement("comment");
 683:         commentElement.appendChild(doc.createTextNode(comment));
 684:         root.appendChild(commentElement);
 685:       }
 686:     Iterator iterator = entrySet().iterator();
 687:     while (iterator.hasNext())
 688:       {
 689:         Map.Entry entry = (Map.Entry) iterator.next();
 690:         Element entryElement = doc.createElement("entry");
 691:         entryElement.setAttribute("key", (String) entry.getKey());
 692:         entryElement.appendChild(doc.createTextNode((String)
 693:                             entry.getValue()));
 694:         root.appendChild(entryElement);
 695:       }
 696:     DOMImplementationLS loadAndSave = (DOMImplementationLS) domImpl;
 697:     LSSerializer serializer = loadAndSave.createLSSerializer();
 698:     LSOutput output = loadAndSave.createLSOutput();
 699:     output.setByteStream(os);
 700:     output.setEncoding(encoding);
 701:     serializer.write(doc, output);
 702:       }
 703:     catch (ClassNotFoundException e)
 704:       {
 705:     throw (IOException) 
 706:       new IOException("The XML classes could not be found.").initCause(e);
 707:       }
 708:     catch (InstantiationException e)
 709:       {
 710:     throw (IOException)
 711:       new IOException("The XML classes could not be instantiated.")
 712:       .initCause(e);
 713:       }
 714:     catch (IllegalAccessException e)
 715:       {
 716:     throw (IOException)
 717:       new IOException("The XML classes could not be accessed.")
 718:       .initCause(e);
 719:       }
 720:   }
 721: 
 722:   /**
 723:    * <p>
 724:    * Decodes the contents of the supplied <code>InputStream</code> as
 725:    * an XML file, which represents a set of properties.  The format of
 726:    * the XML file must match the DTD
 727:    * <a href="http://java.sun.com/dtd/properties.dtd">
 728:    * http://java.sun.com/dtd/properties.dtd</a>.
 729:    * </p>
 730:    *
 731:    * @param in the input stream from which to receive the XML data.
 732:    * @throws IOException if an I/O error occurs in reading the input data.
 733:    * @throws InvalidPropertiesFormatException if the input data does not
 734:    *                                          constitute an XML properties
 735:    *                                          file.
 736:    * @throws NullPointerException if <code>in</code> is null.
 737:    * @since 1.5
 738:    */
 739:   public void loadFromXML(InputStream in)
 740:     throws IOException, InvalidPropertiesFormatException
 741:   {
 742:     if (in == null)
 743:       throw new NullPointerException("Null input stream supplied.");
 744:     try
 745:       {
 746:     SAXParserFactory factory = SAXParserFactory.newInstance();
 747:     factory.setValidating(false); /* Don't use the URI */
 748:     XMLReader parser = factory.newSAXParser().getXMLReader();
 749:     PropertiesHandler handler = new PropertiesHandler();
 750:     parser.setContentHandler(handler);
 751:     parser.setProperty("http://xml.org/sax/properties/lexical-handler",
 752:                handler);
 753:     parser.parse(new InputSource(in));
 754:       }
 755:     catch (SAXException e)
 756:       {
 757:     throw (InvalidPropertiesFormatException)
 758:       new InvalidPropertiesFormatException("Error in parsing XML.").
 759:       initCause(e);
 760:       }
 761:     catch (ParserConfigurationException e)
 762:       {
 763:     throw (IOException)
 764:       new IOException("An XML parser could not be found.").
 765:       initCause(e);
 766:       }
 767:   }
 768: 
 769:   /**
 770:    * This class deals with the parsing of XML using 
 771:    * <a href="http://java.sun.com/dtd/properties.dtd">
 772:    * http://java.sun.com/dtd/properties.dtd</a>.
 773:    *   
 774:    * @author Andrew John Hughes (gnu_andrew@member.fsf.org)
 775:    * @since 1.5
 776:    */
 777:   private class PropertiesHandler
 778:     extends DefaultHandler2
 779:   {
 780:     
 781:     /**
 782:      * The current key.
 783:      */
 784:     private String key;
 785:     
 786:     /**
 787:      * The current value.
 788:      */
 789:     private String value;
 790: 
 791:     /**
 792:      * A flag to check whether a valid DTD declaration has been seen.
 793:      */
 794:     private boolean dtdDeclSeen;
 795: 
 796:     /**
 797:      * Constructs a new Properties handler.
 798:      */
 799:     public PropertiesHandler()
 800:     {
 801:       key = null;
 802:       value = null;
 803:       dtdDeclSeen = false;
 804:     }
 805: 
 806:     /**
 807:      * <p>
 808:      * Captures the start of the DTD declarations, if they exist.
 809:      * A valid properties file must declare the following doctype:
 810:      * </p>
 811:      * <p>
 812:      * <code>!DOCTYPE properties SYSTEM
 813:      * "http://java.sun.com/dtd/properties.dtd"</code>
 814:      * </p>
 815:      * 
 816:      * @param name the name of the document type.
 817:      * @param publicId the public identifier that was declared, or
 818:      *                 null if there wasn't one.
 819:      * @param systemId the system identifier that was declared, or
 820:      *                 null if there wasn't one.
 821:      * @throws SAXException if some error occurs in parsing.
 822:      */
 823:     public void startDTD(String name, String publicId, String systemId)
 824:       throws SAXException
 825:     {
 826:       if (name.equals("properties") &&
 827:       publicId == null &&
 828:       systemId.equals("http://java.sun.com/dtd/properties.dtd"))
 829:     {
 830:       dtdDeclSeen = true;
 831:     }
 832:       else
 833:     throw new SAXException("Invalid DTD declaration: " + name);
 834:     }
 835: 
 836:     /**
 837:      * Captures the start of an XML element.
 838:      *
 839:      * @param uri the namespace URI.
 840:      * @param localName the local name of the element inside the namespace.
 841:      * @param qName the local name qualified with the namespace URI.
 842:      * @param attributes the attributes of this element.
 843:      * @throws SAXException if some error occurs in parsing.
 844:      */
 845:     public void startElement(String uri, String localName,
 846:                  String qName, Attributes attributes)
 847:       throws SAXException
 848:     {
 849:       if (qName.equals("entry"))
 850:     {
 851:       int index = attributes.getIndex("key");
 852:       if (index != -1)
 853:         key = attributes.getValue(index);
 854:     }
 855:       else if (qName.equals("comment") || qName.equals("properties"))
 856:     {
 857:       /* Ignore it */
 858:     }
 859:       else
 860:     throw new SAXException("Invalid tag: " + qName);
 861:     }
 862:     
 863:     /**
 864:      * Captures characters within an XML element.
 865:      *
 866:      * @param ch the array of characters.
 867:      * @param start the start index of the characters to use.
 868:      * @param length the number of characters to use from the start index on.
 869:      * @throws SAXException if some error occurs in parsing.
 870:      */
 871:     public void characters(char[] ch, int start, int length)
 872:       throws SAXException
 873:     {
 874:       if (key != null)
 875:     value = new String(ch,start,length);
 876:     }
 877:     
 878:     /**
 879:      * Captures the end of an XML element.
 880:      *
 881:      * @param uri the namespace URI.
 882:      * @param localName the local name of the element inside the namespace.
 883:      * @param qName the local name qualified with the namespace URI.
 884:      * @throws SAXException if some error occurs in parsing.
 885:      */
 886:     public void endElement(String uri, String localName,
 887:                String qName)
 888:       throws SAXException
 889:     {
 890:       if (qName.equals("entry"))
 891:     {
 892:       if (value == null)
 893:         value = "";
 894:       setProperty(key, value);
 895:       key = null;
 896:       value = null;
 897:     }
 898:     }
 899: 
 900:     /**
 901:      * Captures the end of the XML document.  If a DTD declaration has
 902:      * not been seen, the document is erroneous and an exception is thrown.
 903:      *
 904:      * @throws SAXException if the correct DTD declaration didn't appear.
 905:      */
 906:     public void endDocument()
 907:       throws SAXException
 908:     {
 909:       if (!dtdDeclSeen)
 910:     throw new SAXException("No appropriate DTD declaration was seen.");
 911:     }
 912: 
 913:   } // class PropertiesHandler
 914: 
 915: } // class Properties