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 5.0, see 045 * http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html. An XML properties file looks like this: 046 * 047 * <pre> 048 * <?xml version="1.0"?> 049 * <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> 050 * <properties> 051 * <comment>Description of the property list</comment> 052 * <entry key="key1">value1</entry> 053 * <entry key="key2">value2</entry> 054 * <entry key="key3">value3</entry> 055 * </properties> 056 * </pre> 057 * 058 * The Java 5.0 runtime is not required to use this class. The default encoding for this configuration format is UTF-8. 059 * Note that unlike {@code PropertiesConfiguration}, {@code XMLPropertiesConfiguration} does not support includes. 060 * 061 * <em>Note:</em>Configuration objects of this type can be read concurrently by multiple threads. However if one of 062 * these threads modifies the object, synchronization has to be performed manually. 063 * 064 * @since 1.1 065 */ 066public class XMLPropertiesConfiguration extends BaseConfiguration implements FileBasedConfiguration, FileLocatorAware { 067 /** 068 * The default encoding (UTF-8 as specified by http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html) 069 */ 070 public static final String DEFAULT_ENCODING = "UTF-8"; 071 072 /** 073 * Default string used when the XML is malformed 074 */ 075 private static final String MALFORMED_XML_EXCEPTION = "Malformed XML"; 076 077 /** The temporary file locator. */ 078 private FileLocator locator; 079 080 /** Stores a header comment. */ 081 private String header; 082 083 /** 084 * Creates an empty XMLPropertyConfiguration object which can be used to synthesize a new Properties file by adding 085 * values and then saving(). An object constructed by this C'tor can not be tickled into loading included files because 086 * it cannot supply a base for relative includes. 087 */ 088 public XMLPropertiesConfiguration() { 089 } 090 091 /** 092 * Creates and loads the xml properties from the specified DOM node. 093 * 094 * @param element The DOM element 095 * @throws ConfigurationException Error while loading the properties file 096 * @since 2.0 097 */ 098 public XMLPropertiesConfiguration(final Element element) throws ConfigurationException { 099 this.load(element); 100 } 101 102 /** 103 * Returns the header comment of this configuration. 104 * 105 * @return the header comment 106 */ 107 public String getHeader() { 108 return header; 109 } 110 111 /** 112 * Sets the header comment of this configuration. 113 * 114 * @param header the header comment 115 */ 116 public void setHeader(final String header) { 117 this.header = header; 118 } 119 120 @Override 121 public void read(final Reader in) throws ConfigurationException { 122 final SAXParserFactory factory = SAXParserFactory.newInstance(); 123 factory.setNamespaceAware(false); 124 factory.setValidating(true); 125 126 try { 127 final SAXParser parser = factory.newSAXParser(); 128 129 final XMLReader xmlReader = parser.getXMLReader(); 130 xmlReader.setEntityResolver((publicId, systemId) -> new InputSource(getClass().getClassLoader().getResourceAsStream("properties.dtd"))); 131 xmlReader.setContentHandler(new XMLPropertiesHandler()); 132 xmlReader.parse(new InputSource(in)); 133 } catch (final Exception e) { 134 throw new ConfigurationException("Unable to parse the configuration file", e); 135 } 136 137 // todo: support included properties ? 138 } 139 140 /** 141 * Parses a DOM element containing the properties. The DOM element has to follow the XML properties format introduced in 142 * Java 5.0, see http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html 143 * 144 * @param element The DOM element 145 * @throws ConfigurationException Error while interpreting the DOM 146 * @since 2.0 147 */ 148 public void load(final Element element) throws ConfigurationException { 149 if (!element.getNodeName().equals("properties")) { 150 throw new ConfigurationException(MALFORMED_XML_EXCEPTION); 151 } 152 final NodeList childNodes = element.getChildNodes(); 153 for (int i = 0; i < childNodes.getLength(); i++) { 154 final Node item = childNodes.item(i); 155 if (item instanceof Element) { 156 if (item.getNodeName().equals("comment")) { 157 setHeader(item.getTextContent()); 158 } else if (item.getNodeName().equals("entry")) { 159 final String key = ((Element) item).getAttribute("key"); 160 addProperty(key, item.getTextContent()); 161 } else { 162 throw new ConfigurationException(MALFORMED_XML_EXCEPTION); 163 } 164 } 165 } 166 } 167 168 @Override 169 public void write(final Writer out) throws ConfigurationException { 170 final PrintWriter writer = new PrintWriter(out); 171 172 String encoding = locator != null ? locator.getEncoding() : null; 173 if (encoding == null) { 174 encoding = DEFAULT_ENCODING; 175 } 176 writer.println("<?xml version=\"1.0\" encoding=\"" + encoding + "\"?>"); 177 writer.println("<!DOCTYPE properties SYSTEM \"http://java.sun.com/dtd/properties.dtd\">"); 178 writer.println("<properties>"); 179 180 if (getHeader() != null) { 181 writer.println(" <comment>" + StringEscapeUtils.escapeXml10(getHeader()) + "</comment>"); 182 } 183 184 final Iterator<String> keys = getKeys(); 185 while (keys.hasNext()) { 186 final String key = keys.next(); 187 final Object value = getProperty(key); 188 189 if (value instanceof List) { 190 writeProperty(writer, key, (List<?>) value); 191 } else { 192 writeProperty(writer, key, value); 193 } 194 } 195 196 writer.println("</properties>"); 197 writer.flush(); 198 } 199 200 /** 201 * Write a property. 202 * 203 * @param out the output stream 204 * @param key the key of the property 205 * @param value the value of the property 206 */ 207 private void writeProperty(final PrintWriter out, final String key, final Object value) { 208 // escape the key 209 final String k = StringEscapeUtils.escapeXml10(key); 210 211 if (value != null) { 212 final String v = escapeValue(value); 213 out.println(" <entry key=\"" + k + "\">" + v + "</entry>"); 214 } else { 215 out.println(" <entry key=\"" + k + "\"/>"); 216 } 217 } 218 219 /** 220 * Write a list property. 221 * 222 * @param out the output stream 223 * @param key the key of the property 224 * @param values a list with all property values 225 */ 226 private void writeProperty(final PrintWriter out, final String key, final List<?> values) { 227 for (final Object value : values) { 228 writeProperty(out, key, value); 229 } 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 for (final Object value : values) { 287 writeProperty(document, properties, key, value); 288 } 289 } 290 291 /** 292 * Escapes a property value before it is written to disk. 293 * 294 * @param value the value to be escaped 295 * @return the escaped value 296 */ 297 private String escapeValue(final Object value) { 298 final String v = StringEscapeUtils.escapeXml10(String.valueOf(value)); 299 return String.valueOf(getListDelimiterHandler().escape(v, ListDelimiterHandler.NOOP_TRANSFORMER)); 300 } 301 302 /** 303 * SAX Handler to parse a XML properties file. 304 * 305 * @since 1.2 306 */ 307 private class XMLPropertiesHandler extends DefaultHandler { 308 /** The key of the current entry being parsed. */ 309 private String key; 310 311 /** The value of the current entry being parsed. */ 312 private StringBuilder value = new StringBuilder(); 313 314 /** Indicates that a comment is being parsed. */ 315 private boolean inCommentElement; 316 317 /** Indicates that an entry is being parsed. */ 318 private boolean inEntryElement; 319 320 @Override 321 public void startElement(final String uri, final String localName, final String qName, final Attributes attrs) { 322 if ("comment".equals(qName)) { 323 inCommentElement = true; 324 } 325 326 if ("entry".equals(qName)) { 327 key = attrs.getValue("key"); 328 inEntryElement = true; 329 } 330 } 331 332 @Override 333 public void endElement(final String uri, final String localName, final String qName) { 334 if (inCommentElement) { 335 // We've just finished a <comment> element so set the header 336 setHeader(value.toString()); 337 inCommentElement = false; 338 } 339 340 if (inEntryElement) { 341 // We've just finished an <entry> element, so add the key/value pair 342 addProperty(key, value.toString()); 343 inEntryElement = false; 344 } 345 346 // Clear the element value buffer 347 value = new StringBuilder(); 348 } 349 350 @Override 351 public void characters(final char[] chars, final int start, final int length) { 352 /** 353 * We're currently processing an element. All character data from now until the next endElement() call will be the data 354 * for this element. 355 */ 356 value.append(chars, start, length); 357 } 358 } 359}