View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.configuration2;
19  
20  import java.io.PrintWriter;
21  import java.io.Reader;
22  import java.io.Writer;
23  import java.nio.charset.StandardCharsets;
24  import java.util.Iterator;
25  import java.util.List;
26  import java.util.Objects;
27  
28  import javax.xml.parsers.SAXParser;
29  import javax.xml.parsers.SAXParserFactory;
30  
31  import org.apache.commons.configuration2.convert.ListDelimiterHandler;
32  import org.apache.commons.configuration2.ex.ConfigurationException;
33  import org.apache.commons.configuration2.io.FileLocator;
34  import org.apache.commons.configuration2.io.FileLocatorAware;
35  import org.apache.commons.text.StringEscapeUtils;
36  import org.w3c.dom.Document;
37  import org.w3c.dom.Element;
38  import org.w3c.dom.Node;
39  import org.w3c.dom.NodeList;
40  import org.xml.sax.Attributes;
41  import org.xml.sax.InputSource;
42  import org.xml.sax.XMLReader;
43  import org.xml.sax.helpers.DefaultHandler;
44  
45  /**
46   * This configuration implements the XML properties format introduced in Java, see
47   * https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html. An XML properties file looks like this:
48   *
49   * <pre>
50   * &lt;?xml version="1.0"?&gt;
51   * &lt;!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"&gt;
52   * &lt;properties&gt;
53   *   &lt;comment&gt;Description of the property list&lt;/comment&gt;
54   *   &lt;entry key="key1"&gt;value1&lt;/entry&gt;
55   *   &lt;entry key="key2"&gt;value2&lt;/entry&gt;
56   *   &lt;entry key="key3"&gt;value3&lt;/entry&gt;
57   * &lt;/properties&gt;
58   * </pre>
59   *
60   * The Java runtime is not required to use this class. The default encoding for this configuration format is UTF-8.
61   * Note that unlike {@code PropertiesConfiguration}, {@code XMLPropertiesConfiguration} does not support includes.
62   *
63   * <em>Note:</em>Configuration objects of this type can be read concurrently by multiple threads. However if one of
64   * these threads modifies the object, synchronization has to be performed manually.
65   *
66   * @since 1.1
67   */
68  public class XMLPropertiesConfiguration extends BaseConfiguration implements FileBasedConfiguration, FileLocatorAware {
69  
70      /**
71       * The default encoding (UTF-8 as specified by https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html)
72       */
73      public static final String DEFAULT_ENCODING = StandardCharsets.UTF_8.name();
74  
75      /**
76       * Default string used when the XML is malformed
77       */
78      private static final String MALFORMED_XML_EXCEPTION = "Malformed XML";
79  
80      /** The temporary file locator. */
81      private FileLocator locator;
82  
83      /** Stores a header comment. */
84      private String header;
85  
86      /**
87       * Creates an empty XMLPropertyConfiguration object which can be used to synthesize a new Properties file by adding
88       * values and then saving(). An object constructed by this C'tor can not be tickled into loading included files because
89       * it cannot supply a base for relative includes.
90       */
91      public XMLPropertiesConfiguration() {
92      }
93  
94      /**
95       * Creates and loads the XML properties from the specified DOM node.
96       *
97       * @param element The non-null DOM element.
98       * @throws ConfigurationException Error while loading the Element.
99       * @since 2.0
100      */
101     public XMLPropertiesConfiguration(final Element element) throws ConfigurationException {
102         load(Objects.requireNonNull(element, "element"));
103     }
104 
105     /**
106      * Gets the header comment of this configuration.
107      *
108      * @return the header comment
109      */
110     public String getHeader() {
111         return header;
112     }
113 
114     /**
115      * Sets the header comment of this configuration.
116      *
117      * @param header the header comment
118      */
119     public void setHeader(final String header) {
120         this.header = header;
121     }
122 
123     @Override
124     public void read(final Reader in) throws ConfigurationException {
125         final SAXParserFactory factory = SAXParserFactory.newInstance();
126         factory.setNamespaceAware(false);
127         factory.setValidating(true);
128 
129         try {
130             final SAXParser parser = factory.newSAXParser();
131 
132             final XMLReader xmlReader = parser.getXMLReader();
133             xmlReader.setEntityResolver((publicId, systemId) -> new InputSource(getClass().getClassLoader().getResourceAsStream("properties.dtd")));
134             xmlReader.setContentHandler(new XMLPropertiesHandler());
135             xmlReader.parse(new InputSource(in));
136         } catch (final Exception e) {
137             throw new ConfigurationException("Unable to parse the configuration file", e);
138         }
139 
140         // todo: support included properties ?
141     }
142 
143     /**
144      * Parses a DOM element containing the properties. The DOM element has to follow the XML properties format introduced in
145      * Java, see https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html
146      *
147      * @param element The DOM element
148      * @throws ConfigurationException Error while interpreting the DOM
149      * @since 2.0
150      */
151     public void load(final Element element) throws ConfigurationException {
152         if (!element.getNodeName().equals("properties")) {
153             throw new ConfigurationException(MALFORMED_XML_EXCEPTION);
154         }
155         final NodeList childNodes = element.getChildNodes();
156         for (int i = 0; i < childNodes.getLength(); i++) {
157             final Node item = childNodes.item(i);
158             if (item instanceof Element) {
159                 if (item.getNodeName().equals("comment")) {
160                     setHeader(item.getTextContent());
161                 } else if (item.getNodeName().equals("entry")) {
162                     final String key = ((Element) item).getAttribute("key");
163                     addProperty(key, item.getTextContent());
164                 } else {
165                     throw new ConfigurationException(MALFORMED_XML_EXCEPTION);
166                 }
167             }
168         }
169     }
170 
171     @Override
172     public void write(final Writer out) throws ConfigurationException {
173         final PrintWriter writer = new PrintWriter(out);
174 
175         String encoding = locator != null ? locator.getEncoding() : null;
176         if (encoding == null) {
177             encoding = DEFAULT_ENCODING;
178         }
179         writer.println("<?xml version=\"1.0\" encoding=\"" + encoding + "\"?>");
180         writer.println("<!DOCTYPE properties SYSTEM \"http://java.sun.com/dtd/properties.dtd\">");
181         writer.println("<properties>");
182 
183         if (getHeader() != null) {
184             writer.println("  <comment>" + StringEscapeUtils.escapeXml10(getHeader()) + "</comment>");
185         }
186 
187         final Iterator<String> keys = getKeys();
188         while (keys.hasNext()) {
189             final String key = keys.next();
190             final Object value = getProperty(key);
191 
192             if (value instanceof List) {
193                 writeProperty(writer, key, (List<?>) value);
194             } else {
195                 writeProperty(writer, key, value);
196             }
197         }
198 
199         writer.println("</properties>");
200         writer.flush();
201     }
202 
203     /**
204      * Write a property.
205      *
206      * @param out the output stream
207      * @param key the key of the property
208      * @param value the value of the property
209      */
210     private void writeProperty(final PrintWriter out, final String key, final Object value) {
211         // escape the key
212         final String k = StringEscapeUtils.escapeXml10(key);
213 
214         if (value != null) {
215             final String v = escapeValue(value);
216             out.println("  <entry key=\"" + k + "\">" + v + "</entry>");
217         } else {
218             out.println("  <entry key=\"" + k + "\"/>");
219         }
220     }
221 
222     /**
223      * Write a list property.
224      *
225      * @param out the output stream
226      * @param key the key of the property
227      * @param values a list with all property values
228      */
229     private void writeProperty(final PrintWriter out, final String key, final List<?> values) {
230         values.forEach(value -> writeProperty(out, key, value));
231     }
232 
233     /**
234      * Writes the configuration as child to the given DOM node
235      *
236      * @param document The DOM document to add the configuration to
237      * @param parent The DOM parent node
238      * @since 2.0
239      */
240     public void save(final Document document, final Node parent) {
241         final Element properties = document.createElement("properties");
242         parent.appendChild(properties);
243         if (getHeader() != null) {
244             final Element comment = document.createElement("comment");
245             properties.appendChild(comment);
246             comment.setTextContent(StringEscapeUtils.escapeXml10(getHeader()));
247         }
248 
249         final Iterator<String> keys = getKeys();
250         while (keys.hasNext()) {
251             final String key = keys.next();
252             final Object value = getProperty(key);
253 
254             if (value instanceof List) {
255                 writeProperty(document, properties, key, (List<?>) value);
256             } else {
257                 writeProperty(document, properties, key, value);
258             }
259         }
260     }
261 
262     /**
263      * Initializes this object with a {@code FileLocator}. The locator is accessed during load and save operations.
264      *
265      * @param locator the associated {@code FileLocator}
266      */
267     @Override
268     public void initFileLocator(final FileLocator locator) {
269         this.locator = locator;
270     }
271 
272     private void writeProperty(final Document document, final Node properties, final String key, final Object value) {
273         final Element entry = document.createElement("entry");
274         properties.appendChild(entry);
275 
276         // escape the key
277         final String k = StringEscapeUtils.escapeXml10(key);
278         entry.setAttribute("key", k);
279 
280         if (value != null) {
281             final String v = escapeValue(value);
282             entry.setTextContent(v);
283         }
284     }
285 
286     private void writeProperty(final Document document, final Node properties, final String key, final List<?> values) {
287         values.forEach(value -> writeProperty(document, properties, key, value));
288     }
289 
290     /**
291      * Escapes a property value before it is written to disk.
292      *
293      * @param value the value to be escaped
294      * @return the escaped value
295      */
296     private String escapeValue(final Object value) {
297         final String v = StringEscapeUtils.escapeXml10(String.valueOf(value));
298         return String.valueOf(getListDelimiterHandler().escape(v, ListDelimiterHandler.NOOP_TRANSFORMER));
299     }
300 
301     /**
302      * SAX Handler to parse a XML properties file.
303      *
304      * @since 1.2
305      */
306     private final class XMLPropertiesHandler extends DefaultHandler {
307         /** The key of the current entry being parsed. */
308         private String key;
309 
310         /** The value of the current entry being parsed. */
311         private StringBuilder value = new StringBuilder();
312 
313         /** Indicates that a comment is being parsed. */
314         private boolean inCommentElement;
315 
316         /** Indicates that an entry is being parsed. */
317         private boolean inEntryElement;
318 
319         @Override
320         public void startElement(final String uri, final String localName, final String qName, final Attributes attrs) {
321             if ("comment".equals(qName)) {
322                 inCommentElement = true;
323             }
324 
325             if ("entry".equals(qName)) {
326                 key = attrs.getValue("key");
327                 inEntryElement = true;
328             }
329         }
330 
331         @Override
332         public void endElement(final String uri, final String localName, final String qName) {
333             if (inCommentElement) {
334                 // We've just finished a <comment> element so set the header
335                 setHeader(value.toString());
336                 inCommentElement = false;
337             }
338 
339             if (inEntryElement) {
340                 // We've just finished an <entry> element, so add the key/value pair
341                 addProperty(key, value.toString());
342                 inEntryElement = false;
343             }
344 
345             // Clear the element value buffer
346             value = new StringBuilder();
347         }
348 
349         @Override
350         public void characters(final char[] chars, final int start, final int length) {
351             /**
352              * We're currently processing an element. All character data from now until the next endElement() call will be the data
353              * for this element.
354              */
355             value.append(chars, start, length);
356         }
357     }
358 }