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
028 import javax.xml.parsers.SAXParser;
029 import javax.xml.parsers.SAXParserFactory;
030
031 import org.apache.commons.lang.StringEscapeUtils;
032 import org.apache.commons.lang.StringUtils;
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 XMLPropertiesConfiguration}
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 $Id: XMLPropertiesConfiguration.java 1210207 2011-12-04 20:43:50Z oheger $
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 @Override
133 public void load(Reader in) throws ConfigurationException
134 {
135 SAXParserFactory factory = SAXParserFactory.newInstance();
136 factory.setNamespaceAware(false);
137 factory.setValidating(true);
138
139 try
140 {
141 SAXParser parser = factory.newSAXParser();
142
143 XMLReader xmlReader = parser.getXMLReader();
144 xmlReader.setEntityResolver(new EntityResolver()
145 {
146 public InputSource resolveEntity(String publicId, String systemId)
147 {
148 return new InputSource(getClass().getClassLoader().getResourceAsStream("properties.dtd"));
149 }
150 });
151 xmlReader.setContentHandler(new XMLPropertiesHandler());
152 xmlReader.parse(new InputSource(in));
153 }
154 catch (Exception e)
155 {
156 throw new ConfigurationException("Unable to parse the configuration file", e);
157 }
158
159 // todo: support included properties ?
160 }
161
162 @Override
163 public void save(Writer out) throws ConfigurationException
164 {
165 PrintWriter writer = new PrintWriter(out);
166
167 String encoding = getEncoding() != null ? getEncoding() : DEFAULT_ENCODING;
168 writer.println("<?xml version=\"1.0\" encoding=\"" + encoding + "\"?>");
169 writer.println("<!DOCTYPE properties SYSTEM \"http://java.sun.com/dtd/properties.dtd\">");
170 writer.println("<properties>");
171
172 if (getHeader() != null)
173 {
174 writer.println(" <comment>" + StringEscapeUtils.escapeXml(getHeader()) + "</comment>");
175 }
176
177 Iterator<String> keys = getKeys();
178 while (keys.hasNext())
179 {
180 String key = keys.next();
181 Object value = getProperty(key);
182
183 if (value instanceof List)
184 {
185 writeProperty(writer, key, (List<?>) value);
186 }
187 else
188 {
189 writeProperty(writer, key, value);
190 }
191 }
192
193 writer.println("</properties>");
194 writer.flush();
195 }
196
197 /**
198 * Write a property.
199 *
200 * @param out the output stream
201 * @param key the key of the property
202 * @param value the value of the property
203 */
204 private void writeProperty(PrintWriter out, String key, Object value)
205 {
206 // escape the key
207 String k = StringEscapeUtils.escapeXml(key);
208
209 if (value != null)
210 {
211 // escape the value
212 String v = StringEscapeUtils.escapeXml(String.valueOf(value));
213 v = StringUtils.replace(v, String.valueOf(getListDelimiter()), "\\" + getListDelimiter());
214
215 out.println(" <entry key=\"" + k + "\">" + v + "</entry>");
216 }
217 else
218 {
219 out.println(" <entry key=\"" + k + "\"/>");
220 }
221 }
222
223 /**
224 * Write a list property.
225 *
226 * @param out the output stream
227 * @param key the key of the property
228 * @param values a list with all property values
229 */
230 private void writeProperty(PrintWriter out, String key, List<?> values)
231 {
232 for (Object value : values)
233 {
234 writeProperty(out, key, value);
235 }
236 }
237
238 /**
239 * SAX Handler to parse a XML properties file.
240 *
241 * @author Alistair Young
242 * @since 1.2
243 */
244 private class XMLPropertiesHandler extends DefaultHandler
245 {
246 /** The key of the current entry being parsed. */
247 private String key;
248
249 /** The value of the current entry being parsed. */
250 private StringBuilder value = new StringBuilder();
251
252 /** Indicates that a comment is being parsed. */
253 private boolean inCommentElement;
254
255 /** Indicates that an entry is being parsed. */
256 private boolean inEntryElement;
257
258 @Override
259 public void startElement(String uri, String localName, String qName, Attributes attrs)
260 {
261 if ("comment".equals(qName))
262 {
263 inCommentElement = true;
264 }
265
266 if ("entry".equals(qName))
267 {
268 key = attrs.getValue("key");
269 inEntryElement = true;
270 }
271 }
272
273 @Override
274 public void endElement(String uri, String localName, String qName)
275 {
276 if (inCommentElement)
277 {
278 // We've just finished a <comment> element so set the header
279 setHeader(value.toString());
280 inCommentElement = false;
281 }
282
283 if (inEntryElement)
284 {
285 // We've just finished an <entry> element, so add the key/value pair
286 addProperty(key, value.toString());
287 inEntryElement = false;
288 }
289
290 // Clear the element value buffer
291 value = new StringBuilder();
292 }
293
294 @Override
295 public void characters(char[] chars, int start, int length)
296 {
297 /**
298 * We're currently processing an element. All character data from now until
299 * the next endElement() call will be the data for this element.
300 */
301 value.append(chars, start, length);
302 }
303 }
304 }