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