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 "&#92;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     * &lt;!DOCTYPE properties SYSTEM &quot;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     * &lt;!DOCTYPE properties SYSTEM &quot;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     * &lt;!DOCTYPE properties SYSTEM &quot;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("&", "&amp;");
754        s = s.replaceAll("<", "&lt;");
755        s = s.replaceAll(">", "&gt;");
756        s = s.replaceAll("'", "&apos;");
757        s = s.replaceAll("\"", "&quot;");
758        return s;
759    }
760}
761