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     * &lt;?xml version="1.0"?>
046     * &lt;!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
047     * &lt;properties>
048     *   &lt;comment>Description of the property list&lt;/comment>
049     *   &lt;entry key="key1">value1&lt;/entry>
050     *   &lt;entry key="key2">value2&lt;/entry>
051     *   &lt;entry key="key3">value3&lt;/entry>
052     * &lt;/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    }