View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.configuration;
19  
20  import java.io.File;
21  import java.io.PrintWriter;
22  import java.io.Reader;
23  import java.io.Writer;
24  import java.net.URL;
25  import java.util.Iterator;
26  import java.util.List;
27  
28  import javax.xml.parsers.SAXParser;
29  import javax.xml.parsers.SAXParserFactory;
30  
31  import org.apache.commons.lang.StringEscapeUtils;
32  import org.apache.commons.lang.StringUtils;
33  import org.xml.sax.Attributes;
34  import org.xml.sax.EntityResolver;
35  import org.xml.sax.InputSource;
36  import org.xml.sax.XMLReader;
37  import org.xml.sax.helpers.DefaultHandler;
38  
39  /**
40   * This configuration implements the XML properties format introduced in Java
41   * 5.0, see http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html.
42   * An XML properties file looks like this:
43   *
44   * <pre>
45   * &lt;?xml version="1.0"?>
46   * &lt;!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
47   * &lt;properties>
48   *   &lt;comment>Description of the property list&lt;/comment>
49   *   &lt;entry key="key1">value1&lt;/entry>
50   *   &lt;entry key="key2">value2&lt;/entry>
51   *   &lt;entry key="key3">value3&lt;/entry>
52   * &lt;/properties>
53   * </pre>
54   *
55   * The Java 5.0 runtime is not required to use this class. The default encoding
56   * for this configuration format is UTF-8. Note that unlike
57   * {@code PropertiesConfiguration}, {@code XMLPropertiesConfiguration}
58   * does not support includes.
59   *
60   * <em>Note:</em>Configuration objects of this type can be read concurrently
61   * by multiple threads. However if one of these threads modifies the object,
62   * synchronization has to be performed manually.
63   *
64   * @author Emmanuel Bourg
65   * @author Alistair Young
66   * @version $Id: XMLPropertiesConfiguration.java 1210207 2011-12-04 20:43:50Z oheger $
67   * @since 1.1
68   */
69  public class XMLPropertiesConfiguration extends PropertiesConfiguration
70  {
71      /**
72       * The default encoding (UTF-8 as specified by http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html)
73       */
74      private static final String DEFAULT_ENCODING = "UTF-8";
75  
76      // initialization block to set the encoding before loading the file in the constructors
77      {
78          setEncoding(DEFAULT_ENCODING);
79      }
80  
81      /**
82       * Creates an empty XMLPropertyConfiguration object which can be
83       * used to synthesize a new Properties file by adding values and
84       * then saving(). An object constructed by this C'tor can not be
85       * tickled into loading included files because it cannot supply a
86       * base for relative includes.
87       */
88      public XMLPropertiesConfiguration()
89      {
90          super();
91      }
92  
93      /**
94       * Creates and loads the xml properties from the specified file.
95       * The specified file can contain "include" properties which then
96       * are loaded and merged into the properties.
97       *
98       * @param fileName The name of the properties file to load.
99       * @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 }