    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *     http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017
    018package org.apache.commons.configuration2;
    019
    020import java.io.PrintWriter;
    021import java.io.Reader;
    022import java.io.Writer;
    023import java.util.Iterator;
    024import java.util.List;
    025
    026import javax.xml.parsers.SAXParser;
    027import javax.xml.parsers.SAXParserFactory;
    028
    029import org.apache.commons.configuration2.convert.ListDelimiterHandler;
    030import org.apache.commons.configuration2.ex.ConfigurationException;
    031import org.apache.commons.configuration2.io.FileLocator;
    032import org.apache.commons.configuration2.io.FileLocatorAware;
    033import org.apache.commons.text.StringEscapeUtils;
    034import org.w3c.dom.Document;
    035import org.w3c.dom.Element;
    036import org.w3c.dom.Node;
    037import org.w3c.dom.NodeList;
    038import org.xml.sax.Attributes;
    039import org.xml.sax.InputSource;
    040import org.xml.sax.XMLReader;
    041import org.xml.sax.helpers.DefaultHandler;
    042
    043/**
    044 * This configuration implements the XML properties format introduced in Java 5.0, see
    045 * http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html. An XML properties file looks like this:
    046 *
    047 * <pre>
    048 * &lt;?xml version="1.0"?&gt;
    049 * &lt;!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"&gt;
    050 * &lt;properties&gt;
    051 *   &lt;comment&gt;Description of the property list&lt;/comment&gt;
    052 *   &lt;entry key="key1"&gt;value1&lt;/entry&gt;
    053 *   &lt;entry key="key2"&gt;value2&lt;/entry&gt;
    054 *   &lt;entry key="key3"&gt;value3&lt;/entry&gt;
    055 * &lt;/properties&gt;
    056 * </pre>
    057 *
    058 * The Java 5.0 runtime is not required to use this class. The default encoding for this configuration format is UTF-8.
    059 * Note that unlike {@code PropertiesConfiguration}, {@code XMLPropertiesConfiguration} does not support includes.
    060 *
    061 * <em>Note:</em>Configuration objects of this type can be read concurrently by multiple threads. However if one of
    062 * these threads modifies the object, synchronization has to be performed manually.
    063 *
    064 * @since 1.1
    065 */
    066public class XMLPropertiesConfiguration extends BaseConfiguration implements FileBasedConfiguration, FileLocatorAware {
    067    /**
    068     * The default encoding (UTF-8 as specified by http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html)
    069     */
    070    public static final String DEFAULT_ENCODING = "UTF-8";
    071
    072    /**
    073     * Default string used when the XML is malformed
    074     */
    075    private static final String MALFORMED_XML_EXCEPTION = "Malformed XML";
    076
    077    /** The temporary file locator. */
    078    private FileLocator locator;
    079
    080    /** Stores a header comment. */
    081    private String header;
    082
    083    /**
    084     * Creates an empty XMLPropertyConfiguration object which can be used to synthesize a new Properties file by adding
    085     * values and then saving(). An object constructed by this C'tor can not be tickled into loading included files because
    086     * it cannot supply a base for relative includes.
    087     */
    088    public XMLPropertiesConfiguration() {
    089    }
    090
    091    /**
    092     * Creates and loads the xml properties from the specified DOM node.
    093     *
    094     * @param element The DOM element
    095     * @throws ConfigurationException Error while loading the properties file
    096     * @since 2.0
    097     */
    098    public XMLPropertiesConfiguration(final Element element) throws ConfigurationException {
    099        this.load(element);
    100    }
    101
    102    /**
    103     * Gets the header comment of this configuration.
    104     *
    105     * @return the header comment
    106     */
    107    public String getHeader() {
    108        return header;
    109    }
    110
    111    /**
    112     * Sets the header comment of this configuration.
    113     *
    114     * @param header the header comment
    115     */
    116    public void setHeader(final String header) {
    117        this.header = header;
    118    }
    119
    120    @Override
    121    public void read(final Reader in) throws ConfigurationException {
    122        final SAXParserFactory factory = SAXParserFactory.newInstance();
    123        factory.setNamespaceAware(false);
    124        factory.setValidating(true);
    125
    126        try {
    127            final SAXParser parser = factory.newSAXParser();
    128
    129            final XMLReader xmlReader = parser.getXMLReader();
    130            xmlReader.setEntityResolver((publicId, systemId) -> new InputSource(getClass().getClassLoader().getResourceAsStream("properties.dtd")));
    131            xmlReader.setContentHandler(new XMLPropertiesHandler());
    132            xmlReader.parse(new InputSource(in));
    133        } catch (final Exception e) {
    134            throw new ConfigurationException("Unable to parse the configuration file", e);
    135        }
    136
    137        // todo: support included properties ?
    138    }
    139
    140    /**
    141     * Parses a DOM element containing the properties. The DOM element has to follow the XML properties format introduced in
    142     * Java 5.0, see http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html
    143     *
    144     * @param element The DOM element
    145     * @throws ConfigurationException Error while interpreting the DOM
    146     * @since 2.0
    147     */
    148    public void load(final Element element) throws ConfigurationException {
    149        if (!element.getNodeName().equals("properties")) {
    150            throw new ConfigurationException(MALFORMED_XML_EXCEPTION);
    151        }
    152        final NodeList childNodes = element.getChildNodes();
    153        for (int i = 0; i < childNodes.getLength(); i++) {
    154            final Node item = childNodes.item(i);
    155            if (item instanceof Element) {
    156                if (item.getNodeName().equals("comment")) {
    157                    setHeader(item.getTextContent());
    158                } else if (item.getNodeName().equals("entry")) {
    159                    final String key = ((Element) item).getAttribute("key");
    160                    addProperty(key, item.getTextContent());
    161                } else {
    162                    throw new ConfigurationException(MALFORMED_XML_EXCEPTION);
    163                }
    164            }
    165        }
    166    }
    167
    168    @Override
    169    public void write(final Writer out) throws ConfigurationException {
    170        final PrintWriter writer = new PrintWriter(out);
    171
    172        String encoding = locator != null ? locator.getEncoding() : null;
    173        if (encoding == null) {
    174            encoding = DEFAULT_ENCODING;
    175        }
    176        writer.println("<?xml version=\"1.0\" encoding=\"" + encoding + "\"?>");
    177        writer.println("<!DOCTYPE properties SYSTEM \"http://java.sun.com/dtd/properties.dtd\">");
    178        writer.println("<properties>");
    179
    180        if (getHeader() != null) {
    181            writer.println("  <comment>" + StringEscapeUtils.escapeXml10(getHeader()) + "</comment>");
    182        }
    183
    184        final Iterator<String> keys = getKeys();
    185        while (keys.hasNext()) {
    186            final String key = keys.next();
    187            final Object value = getProperty(key);
    188
    189            if (value instanceof List) {
    190                writeProperty(writer, key, (List<?>) value);
    191            } else {
    192                writeProperty(writer, key, value);
    193            }
    194        }
    195
    196        writer.println("</properties>");
    197        writer.flush();
    198    }
    199
    200    /**
    201     * Write a property.
    202     *
    203     * @param out the output stream
    204     * @param key the key of the property
    205     * @param value the value of the property
    206     */
    207    private void writeProperty(final PrintWriter out, final String key, final Object value) {
    208        // escape the key
    209        final String k = StringEscapeUtils.escapeXml10(key);
    210
    211        if (value != null) {
    212            final String v = escapeValue(value);
    213            out.println("  <entry key=\"" + k + "\">" + v + "</entry>");
    214        } else {
    215            out.println("  <entry key=\"" + k + "\"/>");
    216        }
    217    }
    218
    219    /**
    220     * Write a list property.
    221     *
    222     * @param out the output stream
    223     * @param key the key of the property
    224     * @param values a list with all property values
    225     */
    226    private void writeProperty(final PrintWriter out, final String key, final List<?> values) {
    227        values.forEach(value -> writeProperty(out, key, value));
    228    }
    229
    230    /**
    231     * Writes the configuration as child to the given DOM node
    232     *
    233     * @param document The DOM document to add the configuration to
    234     * @param parent The DOM parent node
    235     * @since 2.0
    236     */
    237    public void save(final Document document, final Node parent) {
    238        final Element properties = document.createElement("properties");
    239        parent.appendChild(properties);
    240        if (getHeader() != null) {
    241            final Element comment = document.createElement("comment");
    242            properties.appendChild(comment);
    243            comment.setTextContent(StringEscapeUtils.escapeXml10(getHeader()));
    244        }
    245
    246        final Iterator<String> keys = getKeys();
    247        while (keys.hasNext()) {
    248            final String key = keys.next();
    249            final Object value = getProperty(key);
    250
    251            if (value instanceof List) {
    252                writeProperty(document, properties, key, (List<?>) value);
    253            } else {
    254                writeProperty(document, properties, key, value);
    255            }
    256        }
    257    }
    258
    259    /**
    260     * Initializes this object with a {@code FileLocator}. The locator is accessed during load and save operations.
    261     *
    262     * @param locator the associated {@code FileLocator}
    263     */
    264    @Override
    265    public void initFileLocator(final FileLocator locator) {
    266        this.locator = locator;
    267    }
    268
    269    private void writeProperty(final Document document, final Node properties, final String key, final Object value) {
    270        final Element entry = document.createElement("entry");
    271        properties.appendChild(entry);
    272
    273        // escape the key
    274        final String k = StringEscapeUtils.escapeXml10(key);
    275        entry.setAttribute("key", k);
    276
    277        if (value != null) {
    278            final String v = escapeValue(value);
    279            entry.setTextContent(v);
    280        }
    281    }
    282
    283    private void writeProperty(final Document document, final Node properties, final String key, final List<?> values) {
    284        values.forEach(value -> writeProperty(document, properties, key, value));
    285    }
    286
    287    /**
    288     * Escapes a property value before it is written to disk.
    289     *
    290     * @param value the value to be escaped
    291     * @return the escaped value
    292     */
    293    private String escapeValue(final Object value) {
    294        final String v = StringEscapeUtils.escapeXml10(String.valueOf(value));
    295        return String.valueOf(getListDelimiterHandler().escape(v, ListDelimiterHandler.NOOP_TRANSFORMER));
    296    }
    297
    298    /**
    299     * SAX Handler to parse a XML properties file.
    300     *
    301     * @since 1.2
    302     */
    303    private class XMLPropertiesHandler extends DefaultHandler {
    304        /** The key of the current entry being parsed. */
    305        private String key;
    306
    307        /** The value of the current entry being parsed. */
    308        private StringBuilder value = new StringBuilder();
    309
    310        /** Indicates that a comment is being parsed. */
    311        private boolean inCommentElement;
    312
    313        /** Indicates that an entry is being parsed. */
    314        private boolean inEntryElement;
    315
    316        @Override
    317        public void startElement(final String uri, final String localName, final String qName, final Attributes attrs) {
    318            if ("comment".equals(qName)) {
    319                inCommentElement = true;
    320            }
    321
    322            if ("entry".equals(qName)) {
    323                key = attrs.getValue("key");
    324                inEntryElement = true;
    325            }
    326        }
    327
    328        @Override
    329        public void endElement(final String uri, final String localName, final String qName) {
    330            if (inCommentElement) {
    331                // We've just finished a <comment> element so set the header
    332                setHeader(value.toString());
    333                inCommentElement = false;
    334            }
    335
    336            if (inEntryElement) {
    337                // We've just finished an <entry> element, so add the key/value pair
    338                addProperty(key, value.toString());
    339                inEntryElement = false;
    340            }
    341
    342            // Clear the element value buffer
    343            value = new StringBuilder();
    344        }
    345
    346        @Override
    347        public void characters(final char[] chars, final int start, final int length) {
    348            /**
    349             * We're currently processing an element. All character data from now until the next endElement() call will be the data
    350             * for this element.
    351             */
    352            value.append(chars, start, length);
    353        }
    354    }
    355}