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