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 018 package org.apache.commons.configuration; 019 020 import java.io.File; 021 import java.io.PrintWriter; 022 import java.io.Reader; 023 import java.io.Writer; 024 import java.net.URL; 025 import java.util.Iterator; 026 import java.util.List; 027 import javax.xml.parsers.SAXParser; 028 import javax.xml.parsers.SAXParserFactory; 029 030 import org.apache.commons.lang.StringEscapeUtils; 031 import org.apache.commons.lang.StringUtils; 032 033 import org.xml.sax.Attributes; 034 import org.xml.sax.EntityResolver; 035 import org.xml.sax.InputSource; 036 import org.xml.sax.XMLReader; 037 import org.xml.sax.helpers.DefaultHandler; 038 039 /** 040 * This configuration implements the XML properties format introduced in Java 041 * 5.0, see http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html. 042 * An XML properties file looks like this: 043 * 044 * <pre> 045 * <?xml version="1.0"?> 046 * <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> 047 * <properties> 048 * <comment>Description of the property list</comment> 049 * <entry key="key1">value1</entry> 050 * <entry key="key2">value2</entry> 051 * <entry key="key3">value3</entry> 052 * </properties> 053 * </pre> 054 * 055 * The Java 5.0 runtime is not required to use this class. The default encoding 056 * for this configuration format is UTF-8. Note that unlike 057 * <code>PropertiesConfiguration</code>, <code>XMLPropertiesConfiguration</code> 058 * does not support includes. 059 * 060 * <em>Note:</em>Configuration objects of this type can be read concurrently 061 * by multiple threads. However if one of these threads modifies the object, 062 * synchronization has to be performed manually. 063 * 064 * @author Emmanuel Bourg 065 * @author Alistair Young 066 * @version $Revision: 548098 $, $Date: 2007-06-17 21:34:03 +0200 (So, 17 Jun 2007) $ 067 * @since 1.1 068 */ 069 public class XMLPropertiesConfiguration extends PropertiesConfiguration 070 { 071 /** 072 * The default encoding (UTF-8 as specified by http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html) 073 */ 074 private static final String DEFAULT_ENCODING = "UTF-8"; 075 076 // initialization block to set the encoding before loading the file in the constructors 077 { 078 setEncoding(DEFAULT_ENCODING); 079 } 080 081 /** 082 * Creates an empty XMLPropertyConfiguration object which can be 083 * used to synthesize a new Properties file by adding values and 084 * then saving(). An object constructed by this C'tor can not be 085 * tickled into loading included files because it cannot supply a 086 * base for relative includes. 087 */ 088 public XMLPropertiesConfiguration() 089 { 090 super(); 091 } 092 093 /** 094 * Creates and loads the xml properties from the specified file. 095 * The specified file can contain "include" properties which then 096 * are loaded and merged into the properties. 097 * 098 * @param fileName The name of the properties file to load. 099 * @throws ConfigurationException Error while loading the properties file 100 */ 101 public XMLPropertiesConfiguration(String fileName) throws ConfigurationException 102 { 103 super(fileName); 104 } 105 106 /** 107 * Creates and loads the xml properties from the specified file. 108 * The specified file can contain "include" properties which then 109 * are loaded and merged into the properties. 110 * 111 * @param file The properties file to load. 112 * @throws ConfigurationException Error while loading the properties file 113 */ 114 public XMLPropertiesConfiguration(File file) throws ConfigurationException 115 { 116 super(file); 117 } 118 119 /** 120 * Creates and loads the xml properties from the specified URL. 121 * The specified file can contain "include" properties which then 122 * are loaded and merged into the properties. 123 * 124 * @param url The location of the properties file to load. 125 * @throws ConfigurationException Error while loading the properties file 126 */ 127 public XMLPropertiesConfiguration(URL url) throws ConfigurationException 128 { 129 super(url); 130 } 131 132 public void load(Reader in) throws ConfigurationException 133 { 134 SAXParserFactory factory = SAXParserFactory.newInstance(); 135 factory.setNamespaceAware(false); 136 factory.setValidating(true); 137 138 try 139 { 140 SAXParser parser = factory.newSAXParser(); 141 142 XMLReader xmlReader = parser.getXMLReader(); 143 xmlReader.setEntityResolver(new EntityResolver() 144 { 145 public InputSource resolveEntity(String publicId, String systemId) 146 { 147 return new InputSource(getClass().getClassLoader().getResourceAsStream("properties.dtd")); 148 } 149 }); 150 xmlReader.setContentHandler(new XMLPropertiesHandler()); 151 xmlReader.parse(new InputSource(in)); 152 } 153 catch (Exception e) 154 { 155 throw new ConfigurationException("Unable to parse the configuration file", e); 156 } 157 158 // todo: support included properties ? 159 } 160 161 public void save(Writer out) throws ConfigurationException 162 { 163 PrintWriter writer = new PrintWriter(out); 164 165 String encoding = getEncoding() != null ? getEncoding() : DEFAULT_ENCODING; 166 writer.println("<?xml version=\"1.0\" encoding=\"" + encoding + "\"?>"); 167 writer.println("<!DOCTYPE properties SYSTEM \"http://java.sun.com/dtd/properties.dtd\">"); 168 writer.println("<properties>"); 169 170 if (getHeader() != null) 171 { 172 writer.println(" <comment>" + StringEscapeUtils.escapeXml(getHeader()) + "</comment>"); 173 } 174 175 Iterator keys = getKeys(); 176 while (keys.hasNext()) 177 { 178 String key = (String) keys.next(); 179 Object value = getProperty(key); 180 181 if (value instanceof List) 182 { 183 writeProperty(writer, key, (List) value); 184 } 185 else 186 { 187 writeProperty(writer, key, value); 188 } 189 } 190 191 writer.println("</properties>"); 192 writer.flush(); 193 } 194 195 /** 196 * Write a property. 197 * 198 * @param out the output stream 199 * @param key the key of the property 200 * @param value the value of the property 201 */ 202 private void writeProperty(PrintWriter out, String key, Object value) 203 { 204 // escape the key 205 String k = StringEscapeUtils.escapeXml(key); 206 207 if (value != null) 208 { 209 // escape the value 210 String v = StringEscapeUtils.escapeXml(String.valueOf(value)); 211 v = StringUtils.replace(v, String.valueOf(getListDelimiter()), "\\" + getListDelimiter()); 212 213 out.println(" <entry key=\"" + k + "\">" + v + "</entry>"); 214 } 215 else 216 { 217 out.println(" <entry key=\"" + k + "\"/>"); 218 } 219 } 220 221 /** 222 * Write a list property. 223 * 224 * @param out the output stream 225 * @param key the key of the property 226 * @param values a list with all property values 227 */ 228 private void writeProperty(PrintWriter out, String key, List values) 229 { 230 for (int i = 0; i < values.size(); i++) 231 { 232 writeProperty(out, key, values.get(i)); 233 } 234 } 235 236 /** 237 * SAX Handler to parse a XML properties file. 238 * 239 * @author Alistair Young 240 * @since 1.2 241 */ 242 private class XMLPropertiesHandler extends DefaultHandler 243 { 244 /** The key of the current entry being parsed. */ 245 private String key; 246 247 /** The value of the current entry being parsed. */ 248 private StringBuffer value = new StringBuffer(); 249 250 /** Indicates that a comment is being parsed. */ 251 private boolean inCommentElement; 252 253 /** Indicates that an entry is being parsed. */ 254 private boolean inEntryElement; 255 256 public void startElement(String uri, String localName, String qName, Attributes attrs) 257 { 258 if ("comment".equals(qName)) 259 { 260 inCommentElement = true; 261 } 262 263 if ("entry".equals(qName)) 264 { 265 key = attrs.getValue("key"); 266 inEntryElement = true; 267 } 268 } 269 270 public void endElement(String uri, String localName, String qName) 271 { 272 if (inCommentElement) 273 { 274 // We've just finished a <comment> element so set the header 275 setHeader(value.toString()); 276 inCommentElement = false; 277 } 278 279 if (inEntryElement) 280 { 281 // We've just finished an <entry> element, so add the key/value pair 282 addProperty(key, value.toString()); 283 inEntryElement = false; 284 } 285 286 // Clear the element value buffer 287 value = new StringBuffer(); 288 } 289 290 public void characters(char[] chars, int start, int length) 291 { 292 /** 293 * We're currently processing an element. All character data from now until 294 * the next endElement() call will be the data for this element. 295 */ 296 value.append(chars, start, length); 297 } 298 } 299 }