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     * &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 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    }