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