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