1/* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18package java.util; 19 20import java.io.BufferedReader; 21import java.io.IOException; 22import java.io.InputStream; 23import java.io.InputStreamReader; 24import java.io.OutputStream; 25import java.io.OutputStreamWriter; 26import java.io.PrintStream; 27import java.io.PrintWriter; 28import java.io.Reader; 29import java.io.StringReader; 30import java.io.Writer; 31import java.nio.charset.Charset; 32import java.nio.charset.IllegalCharsetNameException; 33import java.nio.charset.UnsupportedCharsetException; 34import javax.xml.parsers.DocumentBuilder; 35import javax.xml.parsers.DocumentBuilderFactory; 36import javax.xml.parsers.ParserConfigurationException; 37import org.w3c.dom.Document; 38import org.w3c.dom.Element; 39import org.w3c.dom.Node; 40import org.w3c.dom.NodeList; 41import org.w3c.dom.Text; 42import org.xml.sax.EntityResolver; 43import org.xml.sax.ErrorHandler; 44import org.xml.sax.InputSource; 45import org.xml.sax.SAXException; 46import org.xml.sax.SAXParseException; 47 48/** 49 * A {@code Properties} object is a {@code Hashtable} where the keys and values 50 * must be {@code String}s. Each property can have a default 51 * {@code Properties} list which specifies the default 52 * values to be used when a given key is not found in this {@code Properties} 53 * instance. 54 * 55 * <a name="character_encoding"><h3>Character Encoding</h3></a> 56 * <p>Note that in some cases {@code Properties} uses ISO-8859-1 instead of UTF-8. 57 * ISO-8859-1 is only capable of representing a tiny subset of Unicode. 58 * Use either the {@code loadFromXML}/{@code storeToXML} methods (which use UTF-8 by 59 * default) or the {@code load}/{@code store} overloads that take 60 * an {@code OutputStreamWriter} (so you can supply a UTF-8 instance) instead. 61 * 62 * @see Hashtable 63 * @see java.lang.System#getProperties 64 */ 65public class Properties extends Hashtable<Object, Object> { 66 67 private static final long serialVersionUID = 4112578634029874840L; 68 69 private transient DocumentBuilder builder = null; 70 71 private static final String PROP_DTD_NAME = "http://java.sun.com/dtd/properties.dtd"; 72 73 private static final String PROP_DTD = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" 74 + " <!ELEMENT properties (comment?, entry*) >" 75 + " <!ATTLIST properties version CDATA #FIXED \"1.0\" >" 76 + " <!ELEMENT comment (#PCDATA) >" 77 + " <!ELEMENT entry (#PCDATA) >" 78 + " <!ATTLIST entry key CDATA #REQUIRED >"; 79 80 /** 81 * The default values for keys not found in this {@code Properties} 82 * instance. 83 */ 84 protected Properties defaults; 85 86 private static final int NONE = 0, SLASH = 1, UNICODE = 2, CONTINUE = 3, 87 KEY_DONE = 4, IGNORE = 5; 88 89 /** 90 * Constructs a new {@code Properties} object. 91 */ 92 public Properties() { 93 } 94 95 /** 96 * Constructs a new {@code Properties} object using the specified default 97 * {@code Properties}. 98 * 99 * @param properties 100 * the default {@code Properties}. 101 */ 102 public Properties(Properties properties) { 103 defaults = properties; 104 } 105 106 private void dumpString(StringBuilder buffer, String string, boolean key) { 107 int i = 0; 108 if (!key && i < string.length() && string.charAt(i) == ' ') { 109 buffer.append("\\ "); 110 i++; 111 } 112 113 for (; i < string.length(); i++) { 114 char ch = string.charAt(i); 115 switch (ch) { 116 case '\t': 117 buffer.append("\\t"); 118 break; 119 case '\n': 120 buffer.append("\\n"); 121 break; 122 case '\f': 123 buffer.append("\\f"); 124 break; 125 case '\r': 126 buffer.append("\\r"); 127 break; 128 default: 129 if ("\\#!=:".indexOf(ch) >= 0 || (key && ch == ' ')) { 130 buffer.append('\\'); 131 } 132 if (ch >= ' ' && ch <= '~') { 133 buffer.append(ch); 134 } else { 135 String hex = Integer.toHexString(ch); 136 buffer.append("\\u"); 137 for (int j = 0; j < 4 - hex.length(); j++) { 138 buffer.append("0"); 139 } 140 buffer.append(hex); 141 } 142 } 143 } 144 } 145 146 /** 147 * Searches for the property with the specified name. If the property is not 148 * found, the default {@code Properties} are checked. If the property is not 149 * found in the default {@code Properties}, {@code null} is returned. 150 * 151 * @param name 152 * the name of the property to find. 153 * @return the named property value, or {@code null} if it can't be found. 154 */ 155 public String getProperty(String name) { 156 Object result = super.get(name); 157 String property = result instanceof String ? (String) result : null; 158 if (property == null && defaults != null) { 159 property = defaults.getProperty(name); 160 } 161 return property; 162 } 163 164 /** 165 * Searches for the property with the specified name. If the property is not 166 * found, it looks in the default {@code Properties}. If the property is not 167 * found in the default {@code Properties}, it returns the specified 168 * default. 169 * 170 * @param name 171 * the name of the property to find. 172 * @param defaultValue 173 * the default value. 174 * @return the named property value. 175 */ 176 public String getProperty(String name, String defaultValue) { 177 Object result = super.get(name); 178 String property = result instanceof String ? (String) result : null; 179 if (property == null && defaults != null) { 180 property = defaults.getProperty(name); 181 } 182 if (property == null) { 183 return defaultValue; 184 } 185 return property; 186 } 187 188 /** 189 * Lists the mappings in this {@code Properties} to {@code out} in a human-readable form. 190 * Note that values are truncated to 37 characters, so this method is rarely useful. 191 */ 192 public void list(PrintStream out) { 193 listToAppendable(out); 194 } 195 196 /** 197 * Lists the mappings in this {@code Properties} to {@code out} in a human-readable form. 198 * Note that values are truncated to 37 characters, so this method is rarely useful. 199 */ 200 public void list(PrintWriter out) { 201 listToAppendable(out); 202 } 203 204 private void listToAppendable(Appendable out) { 205 try { 206 if (out == null) { 207 throw new NullPointerException("out == null"); 208 } 209 StringBuilder sb = new StringBuilder(80); 210 Enumeration<?> keys = propertyNames(); 211 while (keys.hasMoreElements()) { 212 String key = (String) keys.nextElement(); 213 sb.append(key); 214 sb.append('='); 215 String property = (String) super.get(key); 216 Properties def = defaults; 217 while (property == null) { 218 property = (String) def.get(key); 219 def = def.defaults; 220 } 221 if (property.length() > 40) { 222 sb.append(property.substring(0, 37)); 223 sb.append("..."); 224 } else { 225 sb.append(property); 226 } 227 sb.append(System.lineSeparator()); 228 out.append(sb.toString()); 229 sb.setLength(0); 230 } 231 } catch (IOException ex) { 232 // Appendable.append throws IOException, but PrintStream and PrintWriter don't. 233 throw new AssertionError(ex); 234 } 235 } 236 237 /** 238 * Loads properties from the specified {@code InputStream}, assumed to be ISO-8859-1. 239 * See "<a href="#character_encoding">Character Encoding</a>". 240 * 241 * @param in the {@code InputStream} 242 * @throws IOException 243 */ 244 public synchronized void load(InputStream in) throws IOException { 245 if (in == null) { 246 throw new NullPointerException("in == null"); 247 } 248 load(new InputStreamReader(in, "ISO-8859-1")); 249 } 250 251 /** 252 * Loads properties from the specified {@code Reader}. 253 * The properties file is interpreted according to the following rules: 254 * <ul> 255 * <li>Empty lines are ignored.</li> 256 * <li>Lines starting with either a "#" or a "!" are comment lines and are 257 * ignored.</li> 258 * <li>A backslash at the end of the line escapes the following newline 259 * character ("\r", "\n", "\r\n"). If there's whitespace after the 260 * backslash it will just escape that whitespace instead of concatenating 261 * the lines. This does not apply to comment lines.</li> 262 * <li>A property line consists of the key, the space between the key and 263 * the value, and the value. The key goes up to the first whitespace, "=" or 264 * ":" that is not escaped. The space between the key and the value contains 265 * either one whitespace, one "=" or one ":" and any amount of additional 266 * whitespace before and after that character. The value starts with the 267 * first character after the space between the key and the value.</li> 268 * <li>Following escape sequences are recognized: "\ ", "\\", "\r", "\n", 269 * "\!", "\#", "\t", "\b", "\f", and "\uXXXX" (unicode character).</li> 270 * </ul> 271 * 272 * @param in the {@code Reader} 273 * @throws IOException 274 * @since 1.6 275 */ 276 @SuppressWarnings("fallthrough") 277 public synchronized void load(Reader in) throws IOException { 278 if (in == null) { 279 throw new NullPointerException("in == null"); 280 } 281 int mode = NONE, unicode = 0, count = 0; 282 char nextChar, buf[] = new char[40]; 283 int offset = 0, keyLength = -1, intVal; 284 boolean firstChar = true; 285 286 BufferedReader br = new BufferedReader(in); 287 288 while (true) { 289 intVal = br.read(); 290 if (intVal == -1) { 291 break; 292 } 293 nextChar = (char) intVal; 294 295 if (offset == buf.length) { 296 char[] newBuf = new char[buf.length * 2]; 297 System.arraycopy(buf, 0, newBuf, 0, offset); 298 buf = newBuf; 299 } 300 if (mode == UNICODE) { 301 int digit = Character.digit(nextChar, 16); 302 if (digit >= 0) { 303 unicode = (unicode << 4) + digit; 304 if (++count < 4) { 305 continue; 306 } 307 } else if (count <= 4) { 308 throw new IllegalArgumentException("Invalid Unicode sequence: illegal character"); 309 } 310 mode = NONE; 311 buf[offset++] = (char) unicode; 312 if (nextChar != '\n') { 313 continue; 314 } 315 } 316 if (mode == SLASH) { 317 mode = NONE; 318 switch (nextChar) { 319 case '\r': 320 mode = CONTINUE; // Look for a following \n 321 continue; 322 case '\n': 323 mode = IGNORE; // Ignore whitespace on the next line 324 continue; 325 case 'b': 326 nextChar = '\b'; 327 break; 328 case 'f': 329 nextChar = '\f'; 330 break; 331 case 'n': 332 nextChar = '\n'; 333 break; 334 case 'r': 335 nextChar = '\r'; 336 break; 337 case 't': 338 nextChar = '\t'; 339 break; 340 case 'u': 341 mode = UNICODE; 342 unicode = count = 0; 343 continue; 344 } 345 } else { 346 switch (nextChar) { 347 case '#': 348 case '!': 349 if (firstChar) { 350 while (true) { 351 intVal = br.read(); 352 if (intVal == -1) { 353 break; 354 } 355 nextChar = (char) intVal; 356 if (nextChar == '\r' || nextChar == '\n') { 357 break; 358 } 359 } 360 continue; 361 } 362 break; 363 case '\n': 364 if (mode == CONTINUE) { // Part of a \r\n sequence 365 mode = IGNORE; // Ignore whitespace on the next line 366 continue; 367 } 368 // fall into the next case 369 case '\r': 370 mode = NONE; 371 firstChar = true; 372 if (offset > 0 || (offset == 0 && keyLength == 0)) { 373 if (keyLength == -1) { 374 keyLength = offset; 375 } 376 String temp = new String(buf, 0, offset); 377 put(temp.substring(0, keyLength), temp 378 .substring(keyLength)); 379 } 380 keyLength = -1; 381 offset = 0; 382 continue; 383 case '\\': 384 if (mode == KEY_DONE) { 385 keyLength = offset; 386 } 387 mode = SLASH; 388 continue; 389 case ':': 390 case '=': 391 if (keyLength == -1) { // if parsing the key 392 mode = NONE; 393 keyLength = offset; 394 continue; 395 } 396 break; 397 } 398 if (Character.isWhitespace(nextChar)) { 399 if (mode == CONTINUE) { 400 mode = IGNORE; 401 } 402 // if key length == 0 or value length == 0 403 if (offset == 0 || offset == keyLength || mode == IGNORE) { 404 continue; 405 } 406 if (keyLength == -1) { // if parsing the key 407 mode = KEY_DONE; 408 continue; 409 } 410 } 411 if (mode == IGNORE || mode == CONTINUE) { 412 mode = NONE; 413 } 414 } 415 firstChar = false; 416 if (mode == KEY_DONE) { 417 keyLength = offset; 418 mode = NONE; 419 } 420 buf[offset++] = nextChar; 421 } 422 if (mode == UNICODE && count <= 4) { 423 throw new IllegalArgumentException("Invalid Unicode sequence: expected format \\uxxxx"); 424 } 425 if (keyLength == -1 && offset > 0) { 426 keyLength = offset; 427 } 428 if (keyLength >= 0) { 429 String temp = new String(buf, 0, offset); 430 String key = temp.substring(0, keyLength); 431 String value = temp.substring(keyLength); 432 if (mode == SLASH) { 433 value += "\u0000"; 434 } 435 put(key, value); 436 } 437 } 438 439 /** 440 * Returns all of the property names (keys) in this {@code Properties} object. 441 */ 442 public Enumeration<?> propertyNames() { 443 Hashtable<Object, Object> selected = new Hashtable<Object, Object>(); 444 selectProperties(selected, false); 445 return selected.keys(); 446 } 447 448 /** 449 * Returns those property names (keys) in this {@code Properties} object for which 450 * both key and value are strings. 451 * 452 * @return a set of keys in the property list 453 * @since 1.6 454 */ 455 public Set<String> stringPropertyNames() { 456 Hashtable<String, Object> stringProperties = new Hashtable<String, Object>(); 457 selectProperties(stringProperties, true); 458 return Collections.unmodifiableSet(stringProperties.keySet()); 459 } 460 461 private <K> void selectProperties(Hashtable<K, Object> selectProperties, final boolean isStringOnly) { 462 if (defaults != null) { 463 defaults.selectProperties(selectProperties, isStringOnly); 464 } 465 Enumeration<Object> keys = keys(); 466 while (keys.hasMoreElements()) { 467 @SuppressWarnings("unchecked") 468 K key = (K) keys.nextElement(); 469 if (isStringOnly && !(key instanceof String)) { 470 // Only select property with string key and value 471 continue; 472 } 473 Object value = get(key); 474 selectProperties.put(key, value); 475 } 476 } 477 478 /** 479 * Saves the mappings in this {@code Properties} to the specified {@code 480 * OutputStream}, putting the specified comment at the beginning. The output 481 * from this method is suitable for being read by the 482 * {@link #load(InputStream)} method. 483 * 484 * @param out the {@code OutputStream} to write to. 485 * @param comment the comment to add at the beginning. 486 * @throws ClassCastException if the key or value of a mapping is not a 487 * String. 488 * @deprecated This method ignores any {@code IOException} thrown while 489 * writing — use {@link #store} instead for better exception 490 * handling. 491 */ 492 @Deprecated 493 public void save(OutputStream out, String comment) { 494 try { 495 store(out, comment); 496 } catch (IOException e) { 497 } 498 } 499 500 /** 501 * Maps the specified key to the specified value. If the key already exists, 502 * the old value is replaced. The key and value cannot be {@code null}. 503 * 504 * @param name 505 * the key. 506 * @param value 507 * the value. 508 * @return the old value mapped to the key, or {@code null}. 509 */ 510 public Object setProperty(String name, String value) { 511 return put(name, value); 512 } 513 514 /** 515 * Stores properties to the specified {@code OutputStream}, using ISO-8859-1. 516 * See "<a href="#character_encoding">Character Encoding</a>". 517 * 518 * @param out the {@code OutputStream} 519 * @param comment an optional comment to be written, or null 520 * @throws IOException 521 * @throws ClassCastException if a key or value is not a string 522 */ 523 public synchronized void store(OutputStream out, String comment) throws IOException { 524 store(new OutputStreamWriter(out, "ISO-8859-1"), comment); 525 } 526 527 /** 528 * Stores the mappings in this {@code Properties} object to {@code out}, 529 * putting the specified comment at the beginning. 530 * 531 * @param writer the {@code Writer} 532 * @param comment an optional comment to be written, or null 533 * @throws IOException 534 * @throws ClassCastException if a key or value is not a string 535 * @since 1.6 536 */ 537 public synchronized void store(Writer writer, String comment) throws IOException { 538 if (comment != null) { 539 writer.write("#"); 540 writer.write(comment); 541 writer.write(System.lineSeparator()); 542 } 543 writer.write("#"); 544 writer.write(new Date().toString()); 545 writer.write(System.lineSeparator()); 546 547 StringBuilder sb = new StringBuilder(200); 548 for (Map.Entry<Object, Object> entry : entrySet()) { 549 String key = (String) entry.getKey(); 550 dumpString(sb, key, true); 551 sb.append('='); 552 dumpString(sb, (String) entry.getValue(), false); 553 sb.append(System.lineSeparator()); 554 writer.write(sb.toString()); 555 sb.setLength(0); 556 } 557 writer.flush(); 558 } 559 560 /** 561 * Loads the properties from an {@code InputStream} containing the 562 * properties in XML form. The XML document must begin with (and conform to) 563 * following DOCTYPE: 564 * 565 * <pre> 566 * <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> 567 * </pre> 568 * 569 * Also the content of the XML data must satisfy the DTD but the xml is not 570 * validated against it. The DTD is not loaded from the SYSTEM ID. After 571 * this method returns the InputStream is not closed. 572 * 573 * @param in the InputStream containing the XML document. 574 * @throws IOException in case an error occurs during a read operation. 575 * @throws InvalidPropertiesFormatException if the XML data is not a valid 576 * properties file. 577 */ 578 public synchronized void loadFromXML(InputStream in) throws IOException, 579 InvalidPropertiesFormatException { 580 if (in == null) { 581 throw new NullPointerException("in == null"); 582 } 583 584 if (builder == null) { 585 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 586 // BEGIN android-removed: we still don't support validation. 587 // factory.setValidating(true); 588 // END android-removed 589 590 try { 591 builder = factory.newDocumentBuilder(); 592 } catch (ParserConfigurationException e) { 593 throw new Error(e); 594 } 595 596 builder.setErrorHandler(new ErrorHandler() { 597 public void warning(SAXParseException e) throws SAXException { 598 throw e; 599 } 600 601 public void error(SAXParseException e) throws SAXException { 602 throw e; 603 } 604 605 public void fatalError(SAXParseException e) throws SAXException { 606 throw e; 607 } 608 }); 609 610 builder.setEntityResolver(new EntityResolver() { 611 public InputSource resolveEntity(String publicId, 612 String systemId) throws SAXException, IOException { 613 if (systemId.equals(PROP_DTD_NAME)) { 614 InputSource result = new InputSource(new StringReader( 615 PROP_DTD)); 616 result.setSystemId(PROP_DTD_NAME); 617 return result; 618 } 619 throw new SAXException("Invalid DOCTYPE declaration: " 620 + systemId); 621 } 622 }); 623 } 624 625 try { 626 Document doc = builder.parse(in); 627 NodeList entries = doc.getElementsByTagName("entry"); 628 if (entries == null) { 629 return; 630 } 631 int entriesListLength = entries.getLength(); 632 633 for (int i = 0; i < entriesListLength; i++) { 634 Element entry = (Element) entries.item(i); 635 String key = entry.getAttribute("key"); 636 String value = entry.getTextContent(); 637 638 /* 639 * key != null & value != null but key or(and) value can be 640 * empty String 641 */ 642 put(key, value); 643 } 644 } catch (IOException e) { 645 throw e; 646 } catch (SAXException e) { 647 throw new InvalidPropertiesFormatException(e); 648 } 649 } 650 651 /** 652 * Writes all properties stored in this instance into the {@code 653 * OutputStream} in XML representation. The DOCTYPE is 654 * 655 * <pre> 656 * <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> 657 * </pre> 658 * 659 * If the comment is null, no comment is added to the output. UTF-8 is used 660 * as the encoding. The {@code OutputStream} is not closed at the end. A 661 * call to this method is the same as a call to {@code storeToXML(os, 662 * comment, "UTF-8")}. 663 * 664 * @param os the {@code OutputStream} to write to. 665 * @param comment the comment to add. If null, no comment is added. 666 * @throws IOException if an error occurs during writing to the output. 667 */ 668 public void storeToXML(OutputStream os, String comment) throws IOException { 669 storeToXML(os, comment, "UTF-8"); 670 } 671 672 /** 673 * Writes all properties stored in this instance into the {@code 674 * OutputStream} in XML representation. The DOCTYPE is 675 * 676 * <pre> 677 * <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> 678 * </pre> 679 * 680 * If the comment is null, no comment is added to the output. The parameter 681 * {@code encoding} defines which encoding should be used. The {@code 682 * OutputStream} is not closed at the end. 683 * 684 * @param os the {@code OutputStream} to write to. 685 * @param comment the comment to add. If null, no comment is added. 686 * @param encoding the code identifying the encoding that should be used to 687 * write into the {@code OutputStream}. 688 * @throws IOException if an error occurs during writing to the output. 689 */ 690 public synchronized void storeToXML(OutputStream os, String comment, 691 String encoding) throws IOException { 692 693 if (os == null) { 694 throw new NullPointerException("os == null"); 695 } else if (encoding == null) { 696 throw new NullPointerException("encoding == null"); 697 } 698 699 /* 700 * We can write to XML file using encoding parameter but note that some 701 * aliases for encodings are not supported by the XML parser. Thus we 702 * have to know canonical name for encoding used to store data in XML 703 * since the XML parser must recognize encoding name used to store data. 704 */ 705 706 String encodingCanonicalName; 707 try { 708 encodingCanonicalName = Charset.forName(encoding).name(); 709 } catch (IllegalCharsetNameException e) { 710 System.out.println("Warning: encoding name " + encoding 711 + " is illegal, using UTF-8 as default encoding"); 712 encodingCanonicalName = "UTF-8"; 713 } catch (UnsupportedCharsetException e) { 714 System.out.println("Warning: encoding " + encoding 715 + " is not supported, using UTF-8 as default encoding"); 716 encodingCanonicalName = "UTF-8"; 717 } 718 719 PrintStream printStream = new PrintStream(os, false, 720 encodingCanonicalName); 721 722 printStream.print("<?xml version=\"1.0\" encoding=\""); 723 printStream.print(encodingCanonicalName); 724 printStream.println("\"?>"); 725 726 printStream.print("<!DOCTYPE properties SYSTEM \""); 727 printStream.print(PROP_DTD_NAME); 728 printStream.println("\">"); 729 730 printStream.println("<properties>"); 731 732 if (comment != null) { 733 printStream.print("<comment>"); 734 printStream.print(substitutePredefinedEntries(comment)); 735 printStream.println("</comment>"); 736 } 737 738 for (Map.Entry<Object, Object> entry : entrySet()) { 739 String keyValue = (String) entry.getKey(); 740 String entryValue = (String) entry.getValue(); 741 printStream.print("<entry key=\""); 742 printStream.print(substitutePredefinedEntries(keyValue)); 743 printStream.print("\">"); 744 printStream.print(substitutePredefinedEntries(entryValue)); 745 printStream.println("</entry>"); 746 } 747 printStream.println("</properties>"); 748 printStream.flush(); 749 } 750 751 private String substitutePredefinedEntries(String s) { 752 // substitution for predefined character entities to use them safely in XML. 753 s = s.replaceAll("&", "&"); 754 s = s.replaceAll("<", "<"); 755 s = s.replaceAll(">", ">"); 756 s = s.replaceAll("'", "'"); 757 s = s.replaceAll("\"", """); 758 return s; 759 } 760} 761