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