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