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  
76          /** The key of the current entry being parsed. */
77          private String key;
78  
79          /** The value of the current entry being parsed. */
80          private StringBuilder value = new StringBuilder();
81  
82          /** Indicates that a comment is being parsed. */
83          private boolean inCommentElement;
84  
85          /** Indicates that an entry is being parsed. */
86          private boolean inEntryElement;
87  
88          @Override
89          public void characters(final char[] chars, final int start, final int length) {
90  
91              /**
92               * We're currently processing an element. All character data from now until the next endElement() call will be the data
93               * for this element.
94               */
95              value.append(chars, start, length);
96          }
97  
98          @Override
99          public void endElement(final String uri, final String localName, final String qName) {
100             if (inCommentElement) {
101                 // We've just finished a <comment> element so set the header
102                 setHeader(value.toString());
103                 inCommentElement = false;
104             }
105 
106             if (inEntryElement) {
107                 // We've just finished an <entry> element, so add the key/value pair
108                 addProperty(key, value.toString());
109                 inEntryElement = false;
110             }
111 
112             // Clear the element value buffer
113             value = new StringBuilder();
114         }
115 
116         @Override
117         public void startElement(final String uri, final String localName, final String qName, final Attributes attrs) {
118             if ("comment".equals(qName)) {
119                 inCommentElement = true;
120             }
121 
122             if ("entry".equals(qName)) {
123                 key = attrs.getValue("key");
124                 inEntryElement = true;
125             }
126         }
127     }
128 
129     /**
130      * The default encoding (UTF-8 as specified by https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html)
131      */
132     public static final String DEFAULT_ENCODING = StandardCharsets.UTF_8.name();
133 
134     /**
135      * Default string used when the XML is malformed
136      */
137     private static final String MALFORMED_XML_EXCEPTION = "Malformed XML";
138 
139     /** The temporary file locator. */
140     private FileLocator locator;
141 
142     /** Stores a header comment. */
143     private String header;
144 
145     /**
146      * Creates an empty XMLPropertyConfiguration object which can be used to synthesize a new Properties file by adding
147      * values and then saving(). An object constructed by this constructor cannot be tickled into loading included files because
148      * it cannot supply a base for relative includes.
149      */
150     public XMLPropertiesConfiguration() {
151     }
152 
153     /**
154      * Creates and loads the XML properties from the specified DOM node.
155      *
156      * @param element The non-null DOM element.
157      * @throws ConfigurationException Error while loading the Element.
158      * @since 2.0
159      */
160     public XMLPropertiesConfiguration(final Element element) throws ConfigurationException {
161         load(Objects.requireNonNull(element, "element"));
162     }
163 
164     /**
165      * Escapes a property value before it is written to disk.
166      *
167      * @param value the value to be escaped
168      * @return the escaped value
169      */
170     private String escapeValue(final Object value) {
171         final String v = StringEscapeUtils.escapeXml10(String.valueOf(value));
172         return String.valueOf(getListDelimiterHandler().escape(v, ListDelimiterHandler.NOOP_TRANSFORMER));
173     }
174 
175     /**
176      * Gets the header comment of this configuration.
177      *
178      * @return the header comment
179      */
180     public String getHeader() {
181         return header;
182     }
183 
184     /**
185      * Initializes this object with a {@code FileLocator}. The locator is accessed during load and save operations.
186      *
187      * @param locator the associated {@code FileLocator}
188      */
189     @Override
190     public void initFileLocator(final FileLocator locator) {
191         this.locator = locator;
192     }
193 
194     /**
195      * Parses a DOM element containing the properties. The DOM element has to follow the XML properties format introduced in
196      * Java, see https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html
197      *
198      * @param element The DOM element
199      * @throws ConfigurationException Error while interpreting the DOM
200      * @since 2.0
201      */
202     public void load(final Element element) throws ConfigurationException {
203         if (!element.getNodeName().equals("properties")) {
204             throw new ConfigurationException(MALFORMED_XML_EXCEPTION);
205         }
206         final NodeList childNodes = element.getChildNodes();
207         for (int i = 0; i < childNodes.getLength(); i++) {
208             final Node item = childNodes.item(i);
209             if (item instanceof Element) {
210                 if (item.getNodeName().equals("comment")) {
211                     setHeader(item.getTextContent());
212                 } else if (item.getNodeName().equals("entry")) {
213                     final String key = ((Element) item).getAttribute("key");
214                     addProperty(key, item.getTextContent());
215                 } else {
216                     throw new ConfigurationException(MALFORMED_XML_EXCEPTION);
217                 }
218             }
219         }
220     }
221 
222     @Override
223     public void read(final Reader in) throws ConfigurationException {
224         final SAXParserFactory factory = SAXParserFactory.newInstance();
225         factory.setNamespaceAware(false);
226         factory.setValidating(true);
227         try {
228             final SAXParser parser = factory.newSAXParser();
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         // todo: support included properties ?
237     }
238 
239     /**
240      * Writes the configuration as child to the given DOM node
241      *
242      * @param document The DOM document to add the configuration to.
243      * @param parent The DOM parent node.
244      * @since 2.0
245      */
246     public void save(final Document document, final Node parent) {
247         final Element properties = document.createElement("properties");
248         parent.appendChild(properties);
249         if (getHeader() != null) {
250             final Element comment = document.createElement("comment");
251             properties.appendChild(comment);
252             comment.setTextContent(StringEscapeUtils.escapeXml10(getHeader()));
253         }
254         forEach((k, v) -> {
255             if (v instanceof List) {
256                 writeProperty(document, properties, k, (List<?>) v);
257             } else {
258                 writeProperty(document, properties, k, v);
259             }
260         });
261     }
262 
263     /**
264      * Sets the header comment of this configuration.
265      *
266      * @param header the header comment
267      */
268     public void setHeader(final String header) {
269         this.header = header;
270     }
271 
272     @Override
273     public void write(final Writer out) throws ConfigurationException {
274         final PrintWriter writer = new PrintWriter(out);
275         String encoding = locator != null ? locator.getEncoding() : null;
276         if (encoding == null) {
277             encoding = DEFAULT_ENCODING;
278         }
279         writer.println("<?xml version=\"1.0\" encoding=\"" + encoding + "\"?>");
280         writer.println("<!DOCTYPE properties SYSTEM \"http://java.sun.com/dtd/properties.dtd\">");
281         writer.println("<properties>");
282         if (getHeader() != null) {
283             writer.println("  <comment>" + StringEscapeUtils.escapeXml10(getHeader()) + "</comment>");
284         }
285         forEach((k, v) -> {
286             if (v instanceof List) {
287                 writeProperty(writer, k, (List<?>) v);
288             } else {
289                 writeProperty(writer, k, v);
290             }
291         });
292         writer.println("</properties>");
293         writer.flush();
294     }
295 
296     private void writeProperty(final Document document, final Node properties, final String key, final List<?> values) {
297         values.forEach(value -> writeProperty(document, properties, key, value));
298     }
299 
300     private void writeProperty(final Document document, final Node properties, final String key, final Object value) {
301         final Element entry = document.createElement("entry");
302         properties.appendChild(entry);
303 
304         // escape the key
305         final String k = StringEscapeUtils.escapeXml10(key);
306         entry.setAttribute("key", k);
307 
308         if (value != null) {
309             final String v = escapeValue(value);
310             entry.setTextContent(v);
311         }
312     }
313 
314     /**
315      * Writes a list property.
316      *
317      * @param out the output stream
318      * @param key the key of the property
319      * @param values a list with all property values
320      */
321     private void writeProperty(final PrintWriter out, final String key, final List<?> values) {
322         values.forEach(value -> writeProperty(out, key, value));
323     }
324 
325     /**
326      * Writes a property.
327      *
328      * @param out the output stream
329      * @param key the key of the property
330      * @param value the value of the property
331      */
332     private void writeProperty(final PrintWriter out, final String key, final Object value) {
333         // escape the key
334         final String k = StringEscapeUtils.escapeXml10(key);
335 
336         if (value != null) {
337             final String v = escapeValue(value);
338             out.println("  <entry key=\"" + k + "\">" + v + "</entry>");
339         } else {
340             out.println("  <entry key=\"" + k + "\"/>");
341         }
342     }
343 }