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