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 * http://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.Iterator; 025import java.util.List; 026import java.util.Objects; 027 028import javax.xml.parsers.SAXParser; 029import javax.xml.parsers.SAXParserFactory; 030 031import org.apache.commons.configuration2.convert.ListDelimiterHandler; 032import org.apache.commons.configuration2.ex.ConfigurationException; 033import org.apache.commons.configuration2.io.FileLocator; 034import org.apache.commons.configuration2.io.FileLocatorAware; 035import org.apache.commons.text.StringEscapeUtils; 036import org.w3c.dom.Document; 037import org.w3c.dom.Element; 038import org.w3c.dom.Node; 039import org.w3c.dom.NodeList; 040import org.xml.sax.Attributes; 041import org.xml.sax.InputSource; 042import org.xml.sax.XMLReader; 043import org.xml.sax.helpers.DefaultHandler; 044 045/** 046 * This configuration implements the XML properties format introduced in Java, see 047 * https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html. An XML properties file looks like this: 048 * 049 * <pre> 050 * <?xml version="1.0"?> 051 * <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> 052 * <properties> 053 * <comment>Description of the property list</comment> 054 * <entry key="key1">value1</entry> 055 * <entry key="key2">value2</entry> 056 * <entry key="key3">value3</entry> 057 * </properties> 058 * </pre> 059 * 060 * The Java runtime is not required to use this class. The default encoding for this configuration format is UTF-8. 061 * Note that unlike {@code PropertiesConfiguration}, {@code XMLPropertiesConfiguration} does not support includes. 062 * 063 * <em>Note:</em>Configuration objects of this type can be read concurrently by multiple threads. However if one of 064 * these threads modifies the object, synchronization has to be performed manually. 065 * 066 * @since 1.1 067 */ 068public class XMLPropertiesConfiguration extends BaseConfiguration implements FileBasedConfiguration, FileLocatorAware { 069 070 /** 071 * The default encoding (UTF-8 as specified by https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html) 072 */ 073 public static final String DEFAULT_ENCODING = StandardCharsets.UTF_8.name(); 074 075 /** 076 * Default string used when the XML is malformed 077 */ 078 private static final String MALFORMED_XML_EXCEPTION = "Malformed XML"; 079 080 /** The temporary file locator. */ 081 private FileLocator locator; 082 083 /** Stores a header comment. */ 084 private String header; 085 086 /** 087 * Creates an empty XMLPropertyConfiguration object which can be used to synthesize a new Properties file by adding 088 * values and then saving(). An object constructed by this C'tor can not be tickled into loading included files because 089 * it cannot supply a base for relative includes. 090 */ 091 public XMLPropertiesConfiguration() { 092 } 093 094 /** 095 * Creates and loads the XML properties from the specified DOM node. 096 * 097 * @param element The non-null DOM element. 098 * @throws ConfigurationException Error while loading the Element. 099 * @since 2.0 100 */ 101 public XMLPropertiesConfiguration(final Element element) throws ConfigurationException { 102 load(Objects.requireNonNull(element, "element")); 103 } 104 105 /** 106 * Gets the header comment of this configuration. 107 * 108 * @return the header comment 109 */ 110 public String getHeader() { 111 return header; 112 } 113 114 /** 115 * Sets the header comment of this configuration. 116 * 117 * @param header the header comment 118 */ 119 public void setHeader(final String header) { 120 this.header = header; 121 } 122 123 @Override 124 public void read(final Reader in) throws ConfigurationException { 125 final SAXParserFactory factory = SAXParserFactory.newInstance(); 126 factory.setNamespaceAware(false); 127 factory.setValidating(true); 128 129 try { 130 final SAXParser parser = factory.newSAXParser(); 131 132 final XMLReader xmlReader = parser.getXMLReader(); 133 xmlReader.setEntityResolver((publicId, systemId) -> new InputSource(getClass().getClassLoader().getResourceAsStream("properties.dtd"))); 134 xmlReader.setContentHandler(new XMLPropertiesHandler()); 135 xmlReader.parse(new InputSource(in)); 136 } catch (final Exception e) { 137 throw new ConfigurationException("Unable to parse the configuration file", e); 138 } 139 140 // todo: support included properties ? 141 } 142 143 /** 144 * Parses a DOM element containing the properties. The DOM element has to follow the XML properties format introduced in 145 * Java, see https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html 146 * 147 * @param element The DOM element 148 * @throws ConfigurationException Error while interpreting the DOM 149 * @since 2.0 150 */ 151 public void load(final Element element) throws ConfigurationException { 152 if (!element.getNodeName().equals("properties")) { 153 throw new ConfigurationException(MALFORMED_XML_EXCEPTION); 154 } 155 final NodeList childNodes = element.getChildNodes(); 156 for (int i = 0; i < childNodes.getLength(); i++) { 157 final Node item = childNodes.item(i); 158 if (item instanceof Element) { 159 if (item.getNodeName().equals("comment")) { 160 setHeader(item.getTextContent()); 161 } else if (item.getNodeName().equals("entry")) { 162 final String key = ((Element) item).getAttribute("key"); 163 addProperty(key, item.getTextContent()); 164 } else { 165 throw new ConfigurationException(MALFORMED_XML_EXCEPTION); 166 } 167 } 168 } 169 } 170 171 @Override 172 public void write(final Writer out) throws ConfigurationException { 173 final PrintWriter writer = new PrintWriter(out); 174 175 String encoding = locator != null ? locator.getEncoding() : null; 176 if (encoding == null) { 177 encoding = DEFAULT_ENCODING; 178 } 179 writer.println("<?xml version=\"1.0\" encoding=\"" + encoding + "\"?>"); 180 writer.println("<!DOCTYPE properties SYSTEM \"http://java.sun.com/dtd/properties.dtd\">"); 181 writer.println("<properties>"); 182 183 if (getHeader() != null) { 184 writer.println(" <comment>" + StringEscapeUtils.escapeXml10(getHeader()) + "</comment>"); 185 } 186 187 final Iterator<String> keys = getKeys(); 188 while (keys.hasNext()) { 189 final String key = keys.next(); 190 final Object value = getProperty(key); 191 192 if (value instanceof List) { 193 writeProperty(writer, key, (List<?>) value); 194 } else { 195 writeProperty(writer, key, value); 196 } 197 } 198 199 writer.println("</properties>"); 200 writer.flush(); 201 } 202 203 /** 204 * Write a property. 205 * 206 * @param out the output stream 207 * @param key the key of the property 208 * @param value the value of the property 209 */ 210 private void writeProperty(final PrintWriter out, final String key, final Object value) { 211 // escape the key 212 final String k = StringEscapeUtils.escapeXml10(key); 213 214 if (value != null) { 215 final String v = escapeValue(value); 216 out.println(" <entry key=\"" + k + "\">" + v + "</entry>"); 217 } else { 218 out.println(" <entry key=\"" + k + "\"/>"); 219 } 220 } 221 222 /** 223 * Write a list property. 224 * 225 * @param out the output stream 226 * @param key the key of the property 227 * @param values a list with all property values 228 */ 229 private void writeProperty(final PrintWriter out, final String key, final List<?> values) { 230 values.forEach(value -> writeProperty(out, key, value)); 231 } 232 233 /** 234 * Writes the configuration as child to the given DOM node 235 * 236 * @param document The DOM document to add the configuration to 237 * @param parent The DOM parent node 238 * @since 2.0 239 */ 240 public void save(final Document document, final Node parent) { 241 final Element properties = document.createElement("properties"); 242 parent.appendChild(properties); 243 if (getHeader() != null) { 244 final Element comment = document.createElement("comment"); 245 properties.appendChild(comment); 246 comment.setTextContent(StringEscapeUtils.escapeXml10(getHeader())); 247 } 248 249 final Iterator<String> keys = getKeys(); 250 while (keys.hasNext()) { 251 final String key = keys.next(); 252 final Object value = getProperty(key); 253 254 if (value instanceof List) { 255 writeProperty(document, properties, key, (List<?>) value); 256 } else { 257 writeProperty(document, properties, key, value); 258 } 259 } 260 } 261 262 /** 263 * Initializes this object with a {@code FileLocator}. The locator is accessed during load and save operations. 264 * 265 * @param locator the associated {@code FileLocator} 266 */ 267 @Override 268 public void initFileLocator(final FileLocator locator) { 269 this.locator = locator; 270 } 271 272 private void writeProperty(final Document document, final Node properties, final String key, final Object value) { 273 final Element entry = document.createElement("entry"); 274 properties.appendChild(entry); 275 276 // escape the key 277 final String k = StringEscapeUtils.escapeXml10(key); 278 entry.setAttribute("key", k); 279 280 if (value != null) { 281 final String v = escapeValue(value); 282 entry.setTextContent(v); 283 } 284 } 285 286 private void writeProperty(final Document document, final Node properties, final String key, final List<?> values) { 287 values.forEach(value -> writeProperty(document, properties, key, value)); 288 } 289 290 /** 291 * Escapes a property value before it is written to disk. 292 * 293 * @param value the value to be escaped 294 * @return the escaped value 295 */ 296 private String escapeValue(final Object value) { 297 final String v = StringEscapeUtils.escapeXml10(String.valueOf(value)); 298 return String.valueOf(getListDelimiterHandler().escape(v, ListDelimiterHandler.NOOP_TRANSFORMER)); 299 } 300 301 /** 302 * SAX Handler to parse a XML properties file. 303 * 304 * @since 1.2 305 */ 306 private final class XMLPropertiesHandler extends DefaultHandler { 307 /** The key of the current entry being parsed. */ 308 private String key; 309 310 /** The value of the current entry being parsed. */ 311 private StringBuilder value = new StringBuilder(); 312 313 /** Indicates that a comment is being parsed. */ 314 private boolean inCommentElement; 315 316 /** Indicates that an entry is being parsed. */ 317 private boolean inEntryElement; 318 319 @Override 320 public void startElement(final String uri, final String localName, final String qName, final Attributes attrs) { 321 if ("comment".equals(qName)) { 322 inCommentElement = true; 323 } 324 325 if ("entry".equals(qName)) { 326 key = attrs.getValue("key"); 327 inEntryElement = true; 328 } 329 } 330 331 @Override 332 public void endElement(final String uri, final String localName, final String qName) { 333 if (inCommentElement) { 334 // We've just finished a <comment> element so set the header 335 setHeader(value.toString()); 336 inCommentElement = false; 337 } 338 339 if (inEntryElement) { 340 // We've just finished an <entry> element, so add the key/value pair 341 addProperty(key, value.toString()); 342 inEntryElement = false; 343 } 344 345 // Clear the element value buffer 346 value = new StringBuilder(); 347 } 348 349 @Override 350 public void characters(final char[] chars, final int start, final int length) { 351 /** 352 * We're currently processing an element. All character data from now until the next endElement() call will be the data 353 * for this element. 354 */ 355 value.append(chars, start, length); 356 } 357 } 358}