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}