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