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