001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * https://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.apache.commons.configuration2; 019 020import java.io.PrintWriter; 021import java.io.Reader; 022import java.io.Writer; 023import java.nio.charset.StandardCharsets; 024import java.util.List; 025import java.util.Objects; 026 027import javax.xml.parsers.SAXParser; 028import javax.xml.parsers.SAXParserFactory; 029 030import org.apache.commons.configuration2.convert.ListDelimiterHandler; 031import org.apache.commons.configuration2.ex.ConfigurationException; 032import org.apache.commons.configuration2.io.FileLocator; 033import org.apache.commons.configuration2.io.FileLocatorAware; 034import org.apache.commons.text.StringEscapeUtils; 035import org.w3c.dom.Document; 036import org.w3c.dom.Element; 037import org.w3c.dom.Node; 038import org.w3c.dom.NodeList; 039import org.xml.sax.Attributes; 040import org.xml.sax.InputSource; 041import org.xml.sax.XMLReader; 042import org.xml.sax.helpers.DefaultHandler; 043 044/** 045 * This configuration implements the XML properties format introduced in Java, see 046 * https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html. An XML properties file looks like this: 047 * 048 * <pre> 049 * <?xml version="1.0"?> 050 * <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> 051 * <properties> 052 * <comment>Description of the property list</comment> 053 * <entry key="key1">value1</entry> 054 * <entry key="key2">value2</entry> 055 * <entry key="key3">value3</entry> 056 * </properties> 057 * </pre> 058 * 059 * The Java runtime is not required to use this class. The default encoding for this configuration format is UTF-8. 060 * Note that unlike {@code PropertiesConfiguration}, {@code XMLPropertiesConfiguration} does not support includes. 061 * 062 * <em>Note:</em>Configuration objects of this type can be read concurrently by multiple threads. However if one of 063 * these threads modifies the object, synchronization has to be performed manually. 064 * 065 * @since 1.1 066 */ 067public class XMLPropertiesConfiguration extends BaseConfiguration implements FileBasedConfiguration, FileLocatorAware { 068 069 /** 070 * SAX Handler to parse a XML properties file. 071 * 072 * @since 1.2 073 */ 074 private final class XMLPropertiesHandler extends DefaultHandler { 075 /** The key of the current entry being parsed. */ 076 private String key; 077 078 /** The value of the current entry being parsed. */ 079 private StringBuilder value = new StringBuilder(); 080 081 /** Indicates that a comment is being parsed. */ 082 private boolean inCommentElement; 083 084 /** Indicates that an entry is being parsed. */ 085 private boolean inEntryElement; 086 087 @Override 088 public void characters(final char[] chars, final int start, final int length) { 089 /** 090 * We're currently processing an element. All character data from now until the next endElement() call will be the data 091 * for this element. 092 */ 093 value.append(chars, start, length); 094 } 095 096 @Override 097 public void endElement(final String uri, final String localName, final String qName) { 098 if (inCommentElement) { 099 // 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}