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.nio.charset.StandardCharsets;
024import java.util.Iterator;
025import java.util.List;
026
027import javax.xml.parsers.SAXParser;
028import javax.xml.parsers.SAXParserFactory;
029
030import org.apache.commons.configuration2.convert.ListDelimiterHandler;
031import org.apache.commons.configuration2.ex.ConfigurationException;
032import org.apache.commons.configuration2.io.FileLocator;
033import org.apache.commons.configuration2.io.FileLocatorAware;
034import org.apache.commons.text.StringEscapeUtils;
035import org.w3c.dom.Document;
036import org.w3c.dom.Element;
037import org.w3c.dom.Node;
038import org.w3c.dom.NodeList;
039import org.xml.sax.Attributes;
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, see
046 * https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html. 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 runtime is not required to use this class. The default encoding for this configuration format is UTF-8.
060 * Note that unlike {@code PropertiesConfiguration}, {@code XMLPropertiesConfiguration} does not support includes.
061 *
062 * <em>Note:</em>Configuration objects of this type can be read concurrently by multiple threads. However if one of
063 * these threads modifies the object, synchronization has to be performed manually.
064 *
065 * @since 1.1
066 */
067public class XMLPropertiesConfiguration extends BaseConfiguration implements FileBasedConfiguration, FileLocatorAware {
068
069    /**
070     * The default encoding (UTF-8 as specified by https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html)
071     */
072    public static final String DEFAULT_ENCODING = StandardCharsets.UTF_8.name();
073
074    /**
075     * Default string used when the XML is malformed
076     */
077    private static final String MALFORMED_XML_EXCEPTION = "Malformed XML";
078
079    /** The temporary file locator. */
080    private FileLocator locator;
081
082    /** Stores a header comment. */
083    private String header;
084
085    /**
086     * Creates an empty XMLPropertyConfiguration object which can be used to synthesize a new Properties file by adding
087     * values and then saving(). An object constructed by this C'tor can not be tickled into loading included files because
088     * it cannot supply a base for relative includes.
089     */
090    public XMLPropertiesConfiguration() {
091    }
092
093    /**
094     * Creates and loads the xml properties from the specified DOM node.
095     *
096     * @param element The DOM element
097     * @throws ConfigurationException Error while loading the properties file
098     * @since 2.0
099     */
100    public XMLPropertiesConfiguration(final Element element) throws ConfigurationException {
101        this.load(element);
102    }
103
104    /**
105     * Gets the header comment of this configuration.
106     *
107     * @return the header comment
108     */
109    public String getHeader() {
110        return header;
111    }
112
113    /**
114     * Sets the header comment of this configuration.
115     *
116     * @param header the header comment
117     */
118    public void setHeader(final String header) {
119        this.header = header;
120    }
121
122    @Override
123    public void read(final Reader in) throws ConfigurationException {
124        final SAXParserFactory factory = SAXParserFactory.newInstance();
125        factory.setNamespaceAware(false);
126        factory.setValidating(true);
127
128        try {
129            final SAXParser parser = factory.newSAXParser();
130
131            final XMLReader xmlReader = parser.getXMLReader();
132            xmlReader.setEntityResolver((publicId, systemId) -> new InputSource(getClass().getClassLoader().getResourceAsStream("properties.dtd")));
133            xmlReader.setContentHandler(new XMLPropertiesHandler());
134            xmlReader.parse(new InputSource(in));
135        } catch (final Exception e) {
136            throw new ConfigurationException("Unable to parse the configuration file", e);
137        }
138
139        // todo: support included properties ?
140    }
141
142    /**
143     * Parses a DOM element containing the properties. The DOM element has to follow the XML properties format introduced in
144     * Java, see https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html
145     *
146     * @param element The DOM element
147     * @throws ConfigurationException Error while interpreting the DOM
148     * @since 2.0
149     */
150    public void load(final Element element) throws ConfigurationException {
151        if (!element.getNodeName().equals("properties")) {
152            throw new ConfigurationException(MALFORMED_XML_EXCEPTION);
153        }
154        final NodeList childNodes = element.getChildNodes();
155        for (int i = 0; i < childNodes.getLength(); i++) {
156            final Node item = childNodes.item(i);
157            if (item instanceof Element) {
158                if (item.getNodeName().equals("comment")) {
159                    setHeader(item.getTextContent());
160                } else if (item.getNodeName().equals("entry")) {
161                    final String key = ((Element) item).getAttribute("key");
162                    addProperty(key, item.getTextContent());
163                } else {
164                    throw new ConfigurationException(MALFORMED_XML_EXCEPTION);
165                }
166            }
167        }
168    }
169
170    @Override
171    public void write(final Writer out) throws ConfigurationException {
172        final PrintWriter writer = new PrintWriter(out);
173
174        String encoding = locator != null ? locator.getEncoding() : null;
175        if (encoding == null) {
176            encoding = DEFAULT_ENCODING;
177        }
178        writer.println("<?xml version=\"1.0\" encoding=\"" + encoding + "\"?>");
179        writer.println("<!DOCTYPE properties SYSTEM \"http://java.sun.com/dtd/properties.dtd\">");
180        writer.println("<properties>");
181
182        if (getHeader() != null) {
183            writer.println("  <comment>" + StringEscapeUtils.escapeXml10(getHeader()) + "</comment>");
184        }
185
186        final Iterator<String> keys = getKeys();
187        while (keys.hasNext()) {
188            final String key = keys.next();
189            final Object value = getProperty(key);
190
191            if (value instanceof List) {
192                writeProperty(writer, key, (List<?>) value);
193            } else {
194                writeProperty(writer, key, value);
195            }
196        }
197
198        writer.println("</properties>");
199        writer.flush();
200    }
201
202    /**
203     * Write a property.
204     *
205     * @param out the output stream
206     * @param key the key of the property
207     * @param value the value of the property
208     */
209    private void writeProperty(final PrintWriter out, final String key, final Object value) {
210        // escape the key
211        final String k = StringEscapeUtils.escapeXml10(key);
212
213        if (value != null) {
214            final String v = escapeValue(value);
215            out.println("  <entry key=\"" + k + "\">" + v + "</entry>");
216        } else {
217            out.println("  <entry key=\"" + k + "\"/>");
218        }
219    }
220
221    /**
222     * Write a list property.
223     *
224     * @param out the output stream
225     * @param key the key of the property
226     * @param values a list with all property values
227     */
228    private void writeProperty(final PrintWriter out, final String key, final List<?> values) {
229        values.forEach(value -> writeProperty(out, key, value));
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        values.forEach(value -> writeProperty(document, properties, key, value));
287    }
288
289    /**
290     * Escapes a property value before it is written to disk.
291     *
292     * @param value the value to be escaped
293     * @return the escaped value
294     */
295    private String escapeValue(final Object value) {
296        final String v = StringEscapeUtils.escapeXml10(String.valueOf(value));
297        return String.valueOf(getListDelimiterHandler().escape(v, ListDelimiterHandler.NOOP_TRANSFORMER));
298    }
299
300    /**
301     * SAX Handler to parse a XML properties file.
302     *
303     * @since 1.2
304     */
305    private final class XMLPropertiesHandler extends DefaultHandler {
306        /** The key of the current entry being parsed. */
307        private String key;
308
309        /** The value of the current entry being parsed. */
310        private StringBuilder value = new StringBuilder();
311
312        /** Indicates that a comment is being parsed. */
313        private boolean inCommentElement;
314
315        /** Indicates that an entry is being parsed. */
316        private boolean inEntryElement;
317
318        @Override
319        public void startElement(final String uri, final String localName, final String qName, final Attributes attrs) {
320            if ("comment".equals(qName)) {
321                inCommentElement = true;
322            }
323
324            if ("entry".equals(qName)) {
325                key = attrs.getValue("key");
326                inEntryElement = true;
327            }
328        }
329
330        @Override
331        public void endElement(final String uri, final String localName, final String qName) {
332            if (inCommentElement) {
333                // We've just finished a <comment> element so set the header
334                setHeader(value.toString());
335                inCommentElement = false;
336            }
337
338            if (inEntryElement) {
339                // We've just finished an <entry> element, so add the key/value pair
340                addProperty(key, value.toString());
341                inEntryElement = false;
342            }
343
344            // Clear the element value buffer
345            value = new StringBuilder();
346        }
347
348        @Override
349        public void characters(final char[] chars, final int start, final int length) {
350            /**
351             * We're currently processing an element. All character data from now until the next endElement() call will be the data
352             * for this element.
353             */
354            value.append(chars, start, length);
355        }
356    }
357}