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 * Gets 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 values.forEach(value -> writeProperty(out, key, value)); 228 } 229 230 /** 231 * Writes the configuration as child to the given DOM node 232 * 233 * @param document The DOM document to add the configuration to 234 * @param parent The DOM parent node 235 * @since 2.0 236 */ 237 public void save(final Document document, final Node parent) { 238 final Element properties = document.createElement("properties"); 239 parent.appendChild(properties); 240 if (getHeader() != null) { 241 final Element comment = document.createElement("comment"); 242 properties.appendChild(comment); 243 comment.setTextContent(StringEscapeUtils.escapeXml10(getHeader())); 244 } 245 246 final Iterator<String> keys = getKeys(); 247 while (keys.hasNext()) { 248 final String key = keys.next(); 249 final Object value = getProperty(key); 250 251 if (value instanceof List) { 252 writeProperty(document, properties, key, (List<?>) value); 253 } else { 254 writeProperty(document, properties, key, value); 255 } 256 } 257 } 258 259 /** 260 * Initializes this object with a {@code FileLocator}. The locator is accessed during load and save operations. 261 * 262 * @param locator the associated {@code FileLocator} 263 */ 264 @Override 265 public void initFileLocator(final FileLocator locator) { 266 this.locator = locator; 267 } 268 269 private void writeProperty(final Document document, final Node properties, final String key, final Object value) { 270 final Element entry = document.createElement("entry"); 271 properties.appendChild(entry); 272 273 // escape the key 274 final String k = StringEscapeUtils.escapeXml10(key); 275 entry.setAttribute("key", k); 276 277 if (value != null) { 278 final String v = escapeValue(value); 279 entry.setTextContent(v); 280 } 281 } 282 283 private void writeProperty(final Document document, final Node properties, final String key, final List<?> values) { 284 values.forEach(value -> writeProperty(document, properties, key, value)); 285 } 286 287 /** 288 * Escapes a property value before it is written to disk. 289 * 290 * @param value the value to be escaped 291 * @return the escaped value 292 */ 293 private String escapeValue(final Object value) { 294 final String v = StringEscapeUtils.escapeXml10(String.valueOf(value)); 295 return String.valueOf(getListDelimiterHandler().escape(v, ListDelimiterHandler.NOOP_TRANSFORMER)); 296 } 297 298 /** 299 * SAX Handler to parse a XML properties file. 300 * 301 * @since 1.2 302 */ 303 private class XMLPropertiesHandler extends DefaultHandler { 304 /** The key of the current entry being parsed. */ 305 private String key; 306 307 /** The value of the current entry being parsed. */ 308 private StringBuilder value = new StringBuilder(); 309 310 /** Indicates that a comment is being parsed. */ 311 private boolean inCommentElement; 312 313 /** Indicates that an entry is being parsed. */ 314 private boolean inEntryElement; 315 316 @Override 317 public void startElement(final String uri, final String localName, final String qName, final Attributes attrs) { 318 if ("comment".equals(qName)) { 319 inCommentElement = true; 320 } 321 322 if ("entry".equals(qName)) { 323 key = attrs.getValue("key"); 324 inEntryElement = true; 325 } 326 } 327 328 @Override 329 public void endElement(final String uri, final String localName, final String qName) { 330 if (inCommentElement) { 331 // We've just finished a <comment> element so set the header 332 setHeader(value.toString()); 333 inCommentElement = false; 334 } 335 336 if (inEntryElement) { 337 // We've just finished an <entry> element, so add the key/value pair 338 addProperty(key, value.toString()); 339 inEntryElement = false; 340 } 341 342 // Clear the element value buffer 343 value = new StringBuilder(); 344 } 345 346 @Override 347 public void characters(final char[] chars, final int start, final int length) { 348 /** 349 * We're currently processing an element. All character data from now until the next endElement() call will be the data 350 * for this element. 351 */ 352 value.append(chars, start, length); 353 } 354 } 355}