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