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