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}