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