| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.apache.commons.configuration2; |
| |
| import java.io.PrintWriter; |
| import java.io.Reader; |
| import java.io.Writer; |
| import java.nio.charset.StandardCharsets; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Objects; |
| |
| import javax.xml.parsers.SAXParser; |
| import javax.xml.parsers.SAXParserFactory; |
| |
| import org.apache.commons.configuration2.convert.ListDelimiterHandler; |
| import org.apache.commons.configuration2.ex.ConfigurationException; |
| import org.apache.commons.configuration2.io.FileLocator; |
| import org.apache.commons.configuration2.io.FileLocatorAware; |
| import org.apache.commons.text.StringEscapeUtils; |
| import org.w3c.dom.Document; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.Node; |
| import org.w3c.dom.NodeList; |
| import org.xml.sax.Attributes; |
| import org.xml.sax.InputSource; |
| import org.xml.sax.XMLReader; |
| import org.xml.sax.helpers.DefaultHandler; |
| |
| /** |
| * This configuration implements the XML properties format introduced in Java, see |
| * https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html. An XML properties file looks like this: |
| * |
| * <pre> |
| * <?xml version="1.0"?> |
| * <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> |
| * <properties> |
| * <comment>Description of the property list</comment> |
| * <entry key="key1">value1</entry> |
| * <entry key="key2">value2</entry> |
| * <entry key="key3">value3</entry> |
| * </properties> |
| * </pre> |
| * |
| * The Java runtime is not required to use this class. The default encoding for this configuration format is UTF-8. |
| * Note that unlike {@code PropertiesConfiguration}, {@code XMLPropertiesConfiguration} does not support includes. |
| * |
| * <em>Note:</em>Configuration objects of this type can be read concurrently by multiple threads. However if one of |
| * these threads modifies the object, synchronization has to be performed manually. |
| * |
| * @since 1.1 |
| */ |
| public class XMLPropertiesConfiguration extends BaseConfiguration implements FileBasedConfiguration, FileLocatorAware { |
| |
| /** |
| * SAX Handler to parse a XML properties file. |
| * |
| * @since 1.2 |
| */ |
| private final class XMLPropertiesHandler extends DefaultHandler { |
| /** The key of the current entry being parsed. */ |
| private String key; |
| |
| /** The value of the current entry being parsed. */ |
| private StringBuilder value = new StringBuilder(); |
| |
| /** Indicates that a comment is being parsed. */ |
| private boolean inCommentElement; |
| |
| /** Indicates that an entry is being parsed. */ |
| private boolean inEntryElement; |
| |
| @Override |
| public void characters(final char[] chars, final int start, final int length) { |
| /** |
| * We're currently processing an element. All character data from now until the next endElement() call will be the data |
| * for this element. |
| */ |
| value.append(chars, start, length); |
| } |
| |
| @Override |
| public void endElement(final String uri, final String localName, final String qName) { |
| if (inCommentElement) { |
| // We've just finished a <comment> element so set the header |
| setHeader(value.toString()); |
| inCommentElement = false; |
| } |
| |
| if (inEntryElement) { |
| // We've just finished an <entry> element, so add the key/value pair |
| addProperty(key, value.toString()); |
| inEntryElement = false; |
| } |
| |
| // Clear the element value buffer |
| value = new StringBuilder(); |
| } |
| |
| @Override |
| public void startElement(final String uri, final String localName, final String qName, final Attributes attrs) { |
| if ("comment".equals(qName)) { |
| inCommentElement = true; |
| } |
| |
| if ("entry".equals(qName)) { |
| key = attrs.getValue("key"); |
| inEntryElement = true; |
| } |
| } |
| } |
| |
| /** |
| * The default encoding (UTF-8 as specified by https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html) |
| */ |
| public static final String DEFAULT_ENCODING = StandardCharsets.UTF_8.name(); |
| |
| /** |
| * Default string used when the XML is malformed |
| */ |
| private static final String MALFORMED_XML_EXCEPTION = "Malformed XML"; |
| |
| /** The temporary file locator. */ |
| private FileLocator locator; |
| |
| /** Stores a header comment. */ |
| private String header; |
| |
| /** |
| * Creates an empty XMLPropertyConfiguration object which can be used to synthesize a new Properties file by adding |
| * values and then saving(). An object constructed by this C'tor can not be tickled into loading included files because |
| * it cannot supply a base for relative includes. |
| */ |
| public XMLPropertiesConfiguration() { |
| } |
| |
| /** |
| * Creates and loads the XML properties from the specified DOM node. |
| * |
| * @param element The non-null DOM element. |
| * @throws ConfigurationException Error while loading the Element. |
| * @since 2.0 |
| */ |
| public XMLPropertiesConfiguration(final Element element) throws ConfigurationException { |
| load(Objects.requireNonNull(element, "element")); |
| } |
| |
| /** |
| * Escapes a property value before it is written to disk. |
| * |
| * @param value the value to be escaped |
| * @return the escaped value |
| */ |
| private String escapeValue(final Object value) { |
| final String v = StringEscapeUtils.escapeXml10(String.valueOf(value)); |
| return String.valueOf(getListDelimiterHandler().escape(v, ListDelimiterHandler.NOOP_TRANSFORMER)); |
| } |
| |
| /** |
| * Gets the header comment of this configuration. |
| * |
| * @return the header comment |
| */ |
| public String getHeader() { |
| return header; |
| } |
| |
| /** |
| * Initializes this object with a {@code FileLocator}. The locator is accessed during load and save operations. |
| * |
| * @param locator the associated {@code FileLocator} |
| */ |
| @Override |
| public void initFileLocator(final FileLocator locator) { |
| this.locator = locator; |
| } |
| |
| /** |
| * Parses a DOM element containing the properties. The DOM element has to follow the XML properties format introduced in |
| * Java, see https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html |
| * |
| * @param element The DOM element |
| * @throws ConfigurationException Error while interpreting the DOM |
| * @since 2.0 |
| */ |
| public void load(final Element element) throws ConfigurationException { |
| if (!element.getNodeName().equals("properties")) { |
| throw new ConfigurationException(MALFORMED_XML_EXCEPTION); |
| } |
| final NodeList childNodes = element.getChildNodes(); |
| for (int i = 0; i < childNodes.getLength(); i++) { |
| final Node item = childNodes.item(i); |
| if (item instanceof Element) { |
| if (item.getNodeName().equals("comment")) { |
| setHeader(item.getTextContent()); |
| } else if (item.getNodeName().equals("entry")) { |
| final String key = ((Element) item).getAttribute("key"); |
| addProperty(key, item.getTextContent()); |
| } else { |
| throw new ConfigurationException(MALFORMED_XML_EXCEPTION); |
| } |
| } |
| } |
| } |
| |
| @Override |
| public void read(final Reader in) throws ConfigurationException { |
| final SAXParserFactory factory = SAXParserFactory.newInstance(); |
| factory.setNamespaceAware(false); |
| factory.setValidating(true); |
| |
| try { |
| final SAXParser parser = factory.newSAXParser(); |
| |
| final XMLReader xmlReader = parser.getXMLReader(); |
| xmlReader.setEntityResolver((publicId, systemId) -> new InputSource(getClass().getClassLoader().getResourceAsStream("properties.dtd"))); |
| xmlReader.setContentHandler(new XMLPropertiesHandler()); |
| xmlReader.parse(new InputSource(in)); |
| } catch (final Exception e) { |
| throw new ConfigurationException("Unable to parse the configuration file", e); |
| } |
| |
| // todo: support included properties ? |
| } |
| |
| /** |
| * Writes the configuration as child to the given DOM node |
| * |
| * @param document The DOM document to add the configuration to |
| * @param parent The DOM parent node |
| * @since 2.0 |
| */ |
| public void save(final Document document, final Node parent) { |
| final Element properties = document.createElement("properties"); |
| parent.appendChild(properties); |
| if (getHeader() != null) { |
| final Element comment = document.createElement("comment"); |
| properties.appendChild(comment); |
| comment.setTextContent(StringEscapeUtils.escapeXml10(getHeader())); |
| } |
| |
| final Iterator<String> keys = getKeys(); |
| while (keys.hasNext()) { |
| final String key = keys.next(); |
| final Object value = getProperty(key); |
| |
| if (value instanceof List) { |
| writeProperty(document, properties, key, (List<?>) value); |
| } else { |
| writeProperty(document, properties, key, value); |
| } |
| } |
| } |
| |
| /** |
| * Sets the header comment of this configuration. |
| * |
| * @param header the header comment |
| */ |
| public void setHeader(final String header) { |
| this.header = header; |
| } |
| |
| @Override |
| public void write(final Writer out) throws ConfigurationException { |
| final PrintWriter writer = new PrintWriter(out); |
| |
| String encoding = locator != null ? locator.getEncoding() : null; |
| if (encoding == null) { |
| encoding = DEFAULT_ENCODING; |
| } |
| writer.println("<?xml version=\"1.0\" encoding=\"" + encoding + "\"?>"); |
| writer.println("<!DOCTYPE properties SYSTEM \"http://java.sun.com/dtd/properties.dtd\">"); |
| writer.println("<properties>"); |
| |
| if (getHeader() != null) { |
| writer.println(" <comment>" + StringEscapeUtils.escapeXml10(getHeader()) + "</comment>"); |
| } |
| |
| final Iterator<String> keys = getKeys(); |
| while (keys.hasNext()) { |
| final String key = keys.next(); |
| final Object value = getProperty(key); |
| |
| if (value instanceof List) { |
| writeProperty(writer, key, (List<?>) value); |
| } else { |
| writeProperty(writer, key, value); |
| } |
| } |
| |
| writer.println("</properties>"); |
| writer.flush(); |
| } |
| |
| private void writeProperty(final Document document, final Node properties, final String key, final List<?> values) { |
| values.forEach(value -> writeProperty(document, properties, key, value)); |
| } |
| |
| private void writeProperty(final Document document, final Node properties, final String key, final Object value) { |
| final Element entry = document.createElement("entry"); |
| properties.appendChild(entry); |
| |
| // escape the key |
| final String k = StringEscapeUtils.escapeXml10(key); |
| entry.setAttribute("key", k); |
| |
| if (value != null) { |
| final String v = escapeValue(value); |
| entry.setTextContent(v); |
| } |
| } |
| |
| /** |
| * Write a list property. |
| * |
| * @param out the output stream |
| * @param key the key of the property |
| * @param values a list with all property values |
| */ |
| private void writeProperty(final PrintWriter out, final String key, final List<?> values) { |
| values.forEach(value -> writeProperty(out, key, value)); |
| } |
| |
| /** |
| * Write a property. |
| * |
| * @param out the output stream |
| * @param key the key of the property |
| * @param value the value of the property |
| */ |
| private void writeProperty(final PrintWriter out, final String key, final Object value) { |
| // escape the key |
| final String k = StringEscapeUtils.escapeXml10(key); |
| |
| if (value != null) { |
| final String v = escapeValue(value); |
| out.println(" <entry key=\"" + k + "\">" + v + "</entry>"); |
| } else { |
| out.println(" <entry key=\"" + k + "\"/>"); |
| } |
| } |
| } |