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}