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