XMLPropertiesConfiguration.java

  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. package org.apache.commons.configuration2;

  18. import java.io.PrintWriter;
  19. import java.io.Reader;
  20. import java.io.Writer;
  21. import java.nio.charset.StandardCharsets;
  22. import java.util.Iterator;
  23. import java.util.List;
  24. import java.util.Objects;

  25. import javax.xml.parsers.SAXParser;
  26. import javax.xml.parsers.SAXParserFactory;

  27. import org.apache.commons.configuration2.convert.ListDelimiterHandler;
  28. import org.apache.commons.configuration2.ex.ConfigurationException;
  29. import org.apache.commons.configuration2.io.FileLocator;
  30. import org.apache.commons.configuration2.io.FileLocatorAware;
  31. import org.apache.commons.text.StringEscapeUtils;
  32. import org.w3c.dom.Document;
  33. import org.w3c.dom.Element;
  34. import org.w3c.dom.Node;
  35. import org.w3c.dom.NodeList;
  36. import org.xml.sax.Attributes;
  37. import org.xml.sax.InputSource;
  38. import org.xml.sax.XMLReader;
  39. import org.xml.sax.helpers.DefaultHandler;

  40. /**
  41.  * This configuration implements the XML properties format introduced in Java, see
  42.  * https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html. An XML properties file looks like this:
  43.  *
  44.  * <pre>
  45.  * &lt;?xml version="1.0"?&gt;
  46.  * &lt;!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"&gt;
  47.  * &lt;properties&gt;
  48.  *   &lt;comment&gt;Description of the property list&lt;/comment&gt;
  49.  *   &lt;entry key="key1"&gt;value1&lt;/entry&gt;
  50.  *   &lt;entry key="key2"&gt;value2&lt;/entry&gt;
  51.  *   &lt;entry key="key3"&gt;value3&lt;/entry&gt;
  52.  * &lt;/properties&gt;
  53.  * </pre>
  54.  *
  55.  * The Java runtime is not required to use this class. The default encoding for this configuration format is UTF-8.
  56.  * Note that unlike {@code PropertiesConfiguration}, {@code XMLPropertiesConfiguration} does not support includes.
  57.  *
  58.  * <em>Note:</em>Configuration objects of this type can be read concurrently by multiple threads. However if one of
  59.  * these threads modifies the object, synchronization has to be performed manually.
  60.  *
  61.  * @since 1.1
  62.  */
  63. public class XMLPropertiesConfiguration extends BaseConfiguration implements FileBasedConfiguration, FileLocatorAware {

  64.     /**
  65.      * SAX Handler to parse a XML properties file.
  66.      *
  67.      * @since 1.2
  68.      */
  69.     private final class XMLPropertiesHandler extends DefaultHandler {
  70.         /** The key of the current entry being parsed. */
  71.         private String key;

  72.         /** The value of the current entry being parsed. */
  73.         private StringBuilder value = new StringBuilder();

  74.         /** Indicates that a comment is being parsed. */
  75.         private boolean inCommentElement;

  76.         /** Indicates that an entry is being parsed. */
  77.         private boolean inEntryElement;

  78.         @Override
  79.         public void characters(final char[] chars, final int start, final int length) {
  80.             /**
  81.              * We're currently processing an element. All character data from now until the next endElement() call will be the data
  82.              * for this element.
  83.              */
  84.             value.append(chars, start, length);
  85.         }

  86.         @Override
  87.         public void endElement(final String uri, final String localName, final String qName) {
  88.             if (inCommentElement) {
  89.                 // We've just finished a <comment> element so set the header
  90.                 setHeader(value.toString());
  91.                 inCommentElement = false;
  92.             }

  93.             if (inEntryElement) {
  94.                 // We've just finished an <entry> element, so add the key/value pair
  95.                 addProperty(key, value.toString());
  96.                 inEntryElement = false;
  97.             }

  98.             // Clear the element value buffer
  99.             value = new StringBuilder();
  100.         }

  101.         @Override
  102.         public void startElement(final String uri, final String localName, final String qName, final Attributes attrs) {
  103.             if ("comment".equals(qName)) {
  104.                 inCommentElement = true;
  105.             }

  106.             if ("entry".equals(qName)) {
  107.                 key = attrs.getValue("key");
  108.                 inEntryElement = true;
  109.             }
  110.         }
  111.     }

  112.     /**
  113.      * The default encoding (UTF-8 as specified by https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html)
  114.      */
  115.     public static final String DEFAULT_ENCODING = StandardCharsets.UTF_8.name();

  116.     /**
  117.      * Default string used when the XML is malformed
  118.      */
  119.     private static final String MALFORMED_XML_EXCEPTION = "Malformed XML";

  120.     /** The temporary file locator. */
  121.     private FileLocator locator;

  122.     /** Stores a header comment. */
  123.     private String header;

  124.     /**
  125.      * Creates an empty XMLPropertyConfiguration object which can be used to synthesize a new Properties file by adding
  126.      * values and then saving(). An object constructed by this constructor cannot be tickled into loading included files because
  127.      * it cannot supply a base for relative includes.
  128.      */
  129.     public XMLPropertiesConfiguration() {
  130.     }

  131.     /**
  132.      * Creates and loads the XML properties from the specified DOM node.
  133.      *
  134.      * @param element The non-null DOM element.
  135.      * @throws ConfigurationException Error while loading the Element.
  136.      * @since 2.0
  137.      */
  138.     public XMLPropertiesConfiguration(final Element element) throws ConfigurationException {
  139.         load(Objects.requireNonNull(element, "element"));
  140.     }

  141.     /**
  142.      * Escapes a property value before it is written to disk.
  143.      *
  144.      * @param value the value to be escaped
  145.      * @return the escaped value
  146.      */
  147.     private String escapeValue(final Object value) {
  148.         final String v = StringEscapeUtils.escapeXml10(String.valueOf(value));
  149.         return String.valueOf(getListDelimiterHandler().escape(v, ListDelimiterHandler.NOOP_TRANSFORMER));
  150.     }

  151.     /**
  152.      * Gets the header comment of this configuration.
  153.      *
  154.      * @return the header comment
  155.      */
  156.     public String getHeader() {
  157.         return header;
  158.     }

  159.     /**
  160.      * Initializes this object with a {@code FileLocator}. The locator is accessed during load and save operations.
  161.      *
  162.      * @param locator the associated {@code FileLocator}
  163.      */
  164.     @Override
  165.     public void initFileLocator(final FileLocator locator) {
  166.         this.locator = locator;
  167.     }

  168.     /**
  169.      * Parses a DOM element containing the properties. The DOM element has to follow the XML properties format introduced in
  170.      * Java, see https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html
  171.      *
  172.      * @param element The DOM element
  173.      * @throws ConfigurationException Error while interpreting the DOM
  174.      * @since 2.0
  175.      */
  176.     public void load(final Element element) throws ConfigurationException {
  177.         if (!element.getNodeName().equals("properties")) {
  178.             throw new ConfigurationException(MALFORMED_XML_EXCEPTION);
  179.         }
  180.         final NodeList childNodes = element.getChildNodes();
  181.         for (int i = 0; i < childNodes.getLength(); i++) {
  182.             final Node item = childNodes.item(i);
  183.             if (item instanceof Element) {
  184.                 if (item.getNodeName().equals("comment")) {
  185.                     setHeader(item.getTextContent());
  186.                 } else if (item.getNodeName().equals("entry")) {
  187.                     final String key = ((Element) item).getAttribute("key");
  188.                     addProperty(key, item.getTextContent());
  189.                 } else {
  190.                     throw new ConfigurationException(MALFORMED_XML_EXCEPTION);
  191.                 }
  192.             }
  193.         }
  194.     }

  195.     @Override
  196.     public void read(final Reader in) throws ConfigurationException {
  197.         final SAXParserFactory factory = SAXParserFactory.newInstance();
  198.         factory.setNamespaceAware(false);
  199.         factory.setValidating(true);

  200.         try {
  201.             final SAXParser parser = factory.newSAXParser();

  202.             final XMLReader xmlReader = parser.getXMLReader();
  203.             xmlReader.setEntityResolver((publicId, systemId) -> new InputSource(getClass().getClassLoader().getResourceAsStream("properties.dtd")));
  204.             xmlReader.setContentHandler(new XMLPropertiesHandler());
  205.             xmlReader.parse(new InputSource(in));
  206.         } catch (final Exception e) {
  207.             throw new ConfigurationException("Unable to parse the configuration file", e);
  208.         }

  209.         // todo: support included properties ?
  210.     }

  211.     /**
  212.      * Writes the configuration as child to the given DOM node
  213.      *
  214.      * @param document The DOM document to add the configuration to
  215.      * @param parent The DOM parent node
  216.      * @since 2.0
  217.      */
  218.     public void save(final Document document, final Node parent) {
  219.         final Element properties = document.createElement("properties");
  220.         parent.appendChild(properties);
  221.         if (getHeader() != null) {
  222.             final Element comment = document.createElement("comment");
  223.             properties.appendChild(comment);
  224.             comment.setTextContent(StringEscapeUtils.escapeXml10(getHeader()));
  225.         }

  226.         final Iterator<String> keys = getKeys();
  227.         while (keys.hasNext()) {
  228.             final String key = keys.next();
  229.             final Object value = getProperty(key);

  230.             if (value instanceof List) {
  231.                 writeProperty(document, properties, key, (List<?>) value);
  232.             } else {
  233.                 writeProperty(document, properties, key, value);
  234.             }
  235.         }
  236.     }

  237.     /**
  238.      * Sets the header comment of this configuration.
  239.      *
  240.      * @param header the header comment
  241.      */
  242.     public void setHeader(final String header) {
  243.         this.header = header;
  244.     }

  245.     @Override
  246.     public void write(final Writer out) throws ConfigurationException {
  247.         final PrintWriter writer = new PrintWriter(out);

  248.         String encoding = locator != null ? locator.getEncoding() : null;
  249.         if (encoding == null) {
  250.             encoding = DEFAULT_ENCODING;
  251.         }
  252.         writer.println("<?xml version=\"1.0\" encoding=\"" + encoding + "\"?>");
  253.         writer.println("<!DOCTYPE properties SYSTEM \"http://java.sun.com/dtd/properties.dtd\">");
  254.         writer.println("<properties>");

  255.         if (getHeader() != null) {
  256.             writer.println("  <comment>" + StringEscapeUtils.escapeXml10(getHeader()) + "</comment>");
  257.         }

  258.         final Iterator<String> keys = getKeys();
  259.         while (keys.hasNext()) {
  260.             final String key = keys.next();
  261.             final Object value = getProperty(key);

  262.             if (value instanceof List) {
  263.                 writeProperty(writer, key, (List<?>) value);
  264.             } else {
  265.                 writeProperty(writer, key, value);
  266.             }
  267.         }

  268.         writer.println("</properties>");
  269.         writer.flush();
  270.     }

  271.     private void writeProperty(final Document document, final Node properties, final String key, final List<?> values) {
  272.         values.forEach(value -> writeProperty(document, properties, key, value));
  273.     }

  274.     private void writeProperty(final Document document, final Node properties, final String key, final Object value) {
  275.         final Element entry = document.createElement("entry");
  276.         properties.appendChild(entry);

  277.         // escape the key
  278.         final String k = StringEscapeUtils.escapeXml10(key);
  279.         entry.setAttribute("key", k);

  280.         if (value != null) {
  281.             final String v = escapeValue(value);
  282.             entry.setTextContent(v);
  283.         }
  284.     }

  285.     /**
  286.      * Writes a list property.
  287.      *
  288.      * @param out the output stream
  289.      * @param key the key of the property
  290.      * @param values a list with all property values
  291.      */
  292.     private void writeProperty(final PrintWriter out, final String key, final List<?> values) {
  293.         values.forEach(value -> writeProperty(out, key, value));
  294.     }

  295.     /**
  296.      * Writes a property.
  297.      *
  298.      * @param out the output stream
  299.      * @param key the key of the property
  300.      * @param value the value of the property
  301.      */
  302.     private void writeProperty(final PrintWriter out, final String key, final Object value) {
  303.         // escape the key
  304.         final String k = StringEscapeUtils.escapeXml10(key);

  305.         if (value != null) {
  306.             final String v = escapeValue(value);
  307.             out.println("  <entry key=\"" + k + "\">" + v + "</entry>");
  308.         } else {
  309.             out.println("  <entry key=\"" + k + "\"/>");
  310.         }
  311.     }
  312. }