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