View Javadoc

1   /*
2    * Copyright 2004 The Apache Software Foundation.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License")
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.apache.commons.configuration;
18  
19  import java.io.File;
20  import java.io.Reader;
21  import java.io.StringWriter;
22  import java.io.Writer;
23  import java.net.URL;
24  import java.util.ArrayList;
25  import java.util.Iterator;
26  import java.util.List;
27  import javax.xml.parsers.DocumentBuilder;
28  import javax.xml.parsers.DocumentBuilderFactory;
29  import javax.xml.parsers.ParserConfigurationException;
30  import javax.xml.transform.Result;
31  import javax.xml.transform.Source;
32  import javax.xml.transform.Transformer;
33  import javax.xml.transform.TransformerException;
34  import javax.xml.transform.TransformerFactory;
35  import javax.xml.transform.dom.DOMSource;
36  import javax.xml.transform.stream.StreamResult;
37  
38  import org.apache.commons.lang.StringUtils;
39  import org.w3c.dom.Attr;
40  import org.w3c.dom.CDATASection;
41  import org.w3c.dom.CharacterData;
42  import org.w3c.dom.Document;
43  import org.w3c.dom.Element;
44  import org.w3c.dom.NamedNodeMap;
45  import org.w3c.dom.Node;
46  import org.w3c.dom.NodeList;
47  import org.w3c.dom.Text;
48  import org.xml.sax.InputSource;
49  
50  /***
51   * Reads a XML configuration file.
52   *
53   * To retrieve the value of an attribute of an element, use
54   * <code>X.Y.Z[@attribute]</code>. The '@' symbol was chosen for consistency
55   * with XPath.
56   *
57   * Setting property values will <b>NOT </b> automatically persist changes to
58   * disk, unless <code>autoSave=true</code>.
59   *
60   * @since commons-configuration 1.0
61   *
62   * @author J�rg Schaible
63   * @author <a href="mailto:kelvint@apache.org">Kelvin Tan </a>
64   * @author <a href="mailto:dlr@apache.org">Daniel Rall </a>
65   * @author Emmanuel Bourg
66   * @version $Revision: 1.17 $, $Date: 2004/10/04 19:35:45 $
67   */
68  public class XMLConfiguration extends AbstractFileConfiguration
69  {
70      // For conformance with xpath
71      private static final String ATTRIBUTE_START = "[@";
72  
73      private static final String ATTRIBUTE_END = "]";
74  
75      /***
76       * For consistency with properties files. Access nodes via an "A.B.C"
77       * notation.
78       */
79      private static final String NODE_DELIMITER = ".";
80  
81      /***
82       * The XML document from our data source.
83       */
84      private Document document;
85  
86      /***
87       * If true, modifications are immediately persisted.
88       */
89      private boolean autoSave = false;
90  
91      /***
92       * Creates an empty XML configuration.
93       */
94      public XMLConfiguration()
95      {
96          // build an empty document.
97          DocumentBuilder builder = null;
98          try
99          {
100             builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
101         }
102         catch (ParserConfigurationException e)
103         {
104             throw new ConfigurationRuntimeException(e.getMessage(), e);
105         }
106 
107         document = builder.newDocument();
108         document.appendChild(document.createElement("configuration"));
109     }
110 
111     /***
112      * Creates and loads the XML configuration from the specified resource.
113      *
114      * @param resource The name of the resource to load.
115      *
116      * @throws ConfigurationException Error while loading the XML file
117      */
118     public XMLConfiguration(String resource) throws ConfigurationException
119     {
120         this.fileName = resource;
121         url = ConfigurationUtils.locate(resource);
122         load();
123     }
124 
125     /***
126      * Creates and loads the XML configuration from the specified file.
127      *
128      * @param file The XML file to load.
129      * @throws ConfigurationException Error while loading the XML file
130      */
131     public XMLConfiguration(File file) throws ConfigurationException
132     {
133         setFile(file);
134         load();
135     }
136 
137     /***
138      * Creates and loads the XML configuration from the specified URL.
139      *
140      * @param url The location of the XML file to load.
141      * @throws ConfigurationException Error while loading the XML file
142      */
143     public XMLConfiguration(URL url) throws ConfigurationException
144     {
145         setURL(url);
146         load();
147     }
148 
149     /***
150      * {@inheritDoc}
151      */
152     public void load(Reader in) throws ConfigurationException
153     {
154         try
155         {
156             DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
157             document = builder.parse(new InputSource(in));
158         }
159         catch (Exception e)
160         {
161             throw new ConfigurationException(e.getMessage(), e);
162         }
163 
164         initProperties(document.getDocumentElement(), new StringBuffer());
165     }
166 
167     /***
168      * Loads and initializes from the XML file.
169      *
170      * @param element The element to start processing from. Callers should supply the root element of the document.
171      * @param hierarchy
172      */
173     private void initProperties(Element element, StringBuffer hierarchy)
174     {
175         StringBuffer buffer = new StringBuffer();
176         NodeList list = element.getChildNodes();
177         for (int i = 0; i < list.getLength(); i++)
178         {
179             Node node = list.item(i);
180             if (node instanceof Element)
181             {
182                 Element child = (Element) node;
183 
184                 StringBuffer subhierarchy = new StringBuffer(hierarchy.toString());
185                 subhierarchy.append(child.getTagName());
186                 processAttributes(subhierarchy.toString(), child);
187                 initProperties(child, subhierarchy.append(NODE_DELIMITER));
188             }
189             else if (node instanceof CDATASection || node instanceof Text)
190             {
191                 CharacterData data = (CharacterData) node;
192                 buffer.append(data.getData());
193             }
194         }
195 
196         String text = buffer.toString().trim();
197         if (text.length() > 0 && hierarchy.length() > 0)
198         {
199             super.addProperty(hierarchy.substring(0, hierarchy.length() - 1), text);
200         }
201     }
202 
203     /***
204      * Helper method for constructing properties for the attributes of the given
205      * XML element.
206      *
207      * @param hierarchy the actual hierarchy
208      * @param element   the actual XML element
209      */
210     private void processAttributes(String hierarchy, Element element)
211     {
212         // Add attributes as x.y{ATTRIBUTE_START}att{ATTRIBUTE_END}
213         NamedNodeMap attributes = element.getAttributes();
214         for (int i = 0; i < attributes.getLength(); ++i)
215         {
216             Attr attr = (Attr) attributes.item(i);
217             String attrName = hierarchy + ATTRIBUTE_START + attr.getName() + ATTRIBUTE_END;
218             super.addProperty(attrName, attr.getValue());
219         }
220     }
221 
222     /***
223      * Calls super method, and also ensures the underlying {@linkDocument} is
224      * modified so changes are persisted when saved.
225      *
226      * @param name
227      * @param value
228      */
229     public void addProperty(String name, Object value)
230     {
231         super.addProperty(name, value);
232         addXmlProperty(name, value);
233         possiblySave();
234     }
235 
236     Object getXmlProperty(String name)
237     {
238         // parse the key
239         String[] nodes = parseElementNames(name);
240         String attName = parseAttributeName(name);
241 
242         // get all the matching elements
243         List children = findElementsForPropertyNodes(nodes);
244 
245         List properties = new ArrayList();
246         if (attName == null)
247         {
248             // return text contents of elements
249             Iterator cIter = children.iterator();
250             while (cIter.hasNext())
251             {
252                 Element child = (Element) cIter.next();
253                 // add non-empty strings
254                 String text = getChildText(child);
255                 if (StringUtils.isNotEmpty(text))
256                 {
257                     properties.add(text);
258                 }
259             }
260         }
261         else
262         {
263             // return text contents of attributes
264             Iterator cIter = children.iterator();
265             while (cIter.hasNext())
266             {
267                 Element child = (Element) cIter.next();
268                 if (child.hasAttribute(attName))
269                 {
270                     properties.add(child.getAttribute(attName));
271                 }
272             }
273         }
274 
275         switch (properties.size())
276         {
277             case 0:
278                 return null;
279             case 1:
280                 return properties.get(0);
281             default:
282                 return properties;
283         }
284     }
285 
286     /***
287      * TODO Add comment.
288      *
289      * @param nodes
290      * @return
291      */
292     private List findElementsForPropertyNodes(String[] nodes)
293     {
294         List children = new ArrayList();
295         List elements = new ArrayList();
296 
297         children.add(document.getDocumentElement());
298         for (int i = 0; i < nodes.length; i++)
299         {
300             elements.clear();
301             elements.addAll(children);
302             children.clear();
303 
304             String eName = nodes[i];
305             Iterator eIter = elements.iterator();
306             while (eIter.hasNext())
307             {
308                 Element element = (Element) eIter.next();
309                 NodeList list = element.getChildNodes();
310                 for (int j = 0; j < list.getLength(); j++)
311                 {
312                     Node node = list.item(j);
313                     if (node instanceof Element)
314                     {
315                         Element child = (Element) node;
316                         if (eName.equals(child.getTagName()))
317                         {
318                             children.add(child);
319                         }
320                     }
321                 }
322             }
323         }
324 
325         return children;
326     }
327 
328     private static String getChildText(Node node)
329     {
330         // is there anything to do?
331         if (node == null)
332         {
333             return null;
334         }
335 
336         // concatenate children text
337         StringBuffer str = new StringBuffer();
338         Node child = node.getFirstChild();
339         while (child != null)
340         {
341             short type = child.getNodeType();
342             if (type == Node.TEXT_NODE)
343             {
344                 str.append(child.getNodeValue());
345             }
346             else if (type == Node.CDATA_SECTION_NODE)
347             {
348                 str.append(child.getNodeValue());
349             }
350             child = child.getNextSibling();
351         }
352 
353         // return text value
354         return StringUtils.trimToNull(str.toString());
355 
356     }
357 
358     private Element getChildElementWithName(String eName, Element element)
359     {
360         Element child = null;
361 
362         NodeList list = element.getChildNodes();
363         for (int j = 0; j < list.getLength(); j++)
364         {
365             Node node = list.item(j);
366             if (node instanceof Element)
367             {
368                 child = (Element) node;
369                 if (eName.equals(child.getTagName()))
370                 {
371                     break;
372                 }
373                 child = null;
374             }
375         }
376         return child;
377     }
378 
379     /***
380      * Adds the property value in our document tree.
381      *
382      * @param name  The name of the element to set a value for.
383      * @param value The value to set.
384      */
385     private void addXmlProperty(String name, Object value)
386     {
387         // parse the key
388         String[] nodes = parseElementNames(name);
389         String attName = parseAttributeName(name);
390 
391         Element element = document.getDocumentElement();
392         Element parent = element;
393 
394         for (int i = 0; i < nodes.length; i++)
395         {
396             if (element == null)
397             {
398                 break;
399             }
400             parent = element;
401             String eName = nodes[i];
402             Element child = getChildElementWithName(eName, element);
403 
404             element = child;
405         }
406 
407         Element child = document.createElement(nodes[nodes.length - 1]);
408         parent.appendChild(child);
409         if (attName == null)
410         {
411             CharacterData data = document.createTextNode(String.valueOf(value));
412             child.appendChild(data);
413         }
414         else
415         {
416             child.setAttribute(attName, String.valueOf(value));
417         }
418     }
419 
420     /***
421      * Calls super method, and also ensures the underlying {@link Document}is
422      * modified so changes are persisted when saved.
423      *
424      * @param name The name of the property to clear.
425      */
426     public void clearProperty(String name)
427     {
428         super.clearProperty(name);
429         clearXmlProperty(name);
430         possiblySave();
431     }
432 
433     private void clearXmlProperty(String name)
434     {
435         // parse the key
436         String[] nodes = parseElementNames(name);
437         String attName = parseAttributeName(name);
438 
439         // get all the matching elements
440         List children = findElementsForPropertyNodes(nodes);
441 
442         if (attName == null)
443         {
444             // remove children with no subelements
445             Iterator cIter = children.iterator();
446             while (cIter.hasNext())
447             {
448                 Element child = (Element) cIter.next();
449 
450                 // determine if child has subelments
451                 boolean hasSubelements = false;
452                 Node subchild = child.getFirstChild();
453                 while (subchild != null)
454                 {
455                     if (subchild.getNodeType() == Node.ELEMENT_NODE)
456                     {
457                         hasSubelements = true;
458                         break;
459                     }
460                     subchild = subchild.getNextSibling();
461                 }
462 
463                 if (!hasSubelements)
464                 {
465                     // safe to remove
466                     if (!child.hasAttributes())
467                     {
468                         // remove entire node
469                         Node parent = child.getParentNode();
470                         parent.removeChild(child);
471                     }
472                     else
473                     {
474                         // only remove node contents
475                         subchild = child.getLastChild();
476                         while (subchild != null)
477                         {
478                             child.removeChild(subchild);
479                             subchild = child.getLastChild();
480                         }
481                     }
482                 }
483             }
484         }
485         else
486         {
487             // remove attributes from children
488             Iterator cIter = children.iterator();
489             while (cIter.hasNext())
490             {
491                 Element child = (Element) cIter.next();
492                 child.removeAttribute(attName);
493             }
494         }
495     }
496 
497     /***
498      * Save the configuration if the automatic persistence is enabled and a file
499      * is specified.
500      */
501     private void possiblySave()
502     {
503         if (autoSave && fileName != null)
504         {
505             try
506             {
507                 save();
508             }
509             catch (ConfigurationException ce)
510             {
511                 throw new ConfigurationRuntimeException("Failed to auto-save", ce);
512             }
513         }
514     }
515 
516     /***
517      * If true, changes are automatically persisted.
518      *
519      * @param autoSave
520      */
521     public void setAutoSave(boolean autoSave)
522     {
523         this.autoSave = autoSave;
524     }
525 
526     /***
527      * {@inheritDoc}
528      */
529     public void save(Writer writer) throws ConfigurationException
530     {
531         try
532         {
533             Transformer transformer = TransformerFactory.newInstance().newTransformer();
534             Source source = new DOMSource(document);
535             Result result = new StreamResult(writer);
536 
537             transformer.setOutputProperty("indent", "yes");
538             transformer.transform(source, result);
539         }
540         catch (TransformerException e)
541         {
542             throw new ConfigurationException(e.getMessage(), e);
543         }
544     }
545 
546     public String toString()
547     {
548         StringWriter writer = new StringWriter();
549         try
550         {
551             save(writer);
552         }
553         catch (ConfigurationException e)
554         {
555             e.printStackTrace();
556         }
557         return writer.toString();
558     }
559 
560     /***
561      * Parse a property key and return an array of the element hierarchy it
562      * specifies. For example the key "x.y.z[@abc]" will result in [x, y, z].
563      *
564      * @param key the key to parse
565      *
566      * @return the elements in the key
567      */
568     protected static String[] parseElementNames(String key)
569     {
570         if (key == null)
571         {
572             return new String[]{};
573         }
574         else
575         {
576             // find the beginning of the attribute name
577             int attStart = key.indexOf(ATTRIBUTE_START);
578 
579             if (attStart > -1)
580             {
581                 // remove the attribute part of the key
582                 key = key.substring(0, attStart);
583             }
584 
585             return StringUtils.split(key, NODE_DELIMITER);
586         }
587     }
588 
589     /***
590      * Parse a property key and return the attribute name if it existst.
591      *
592      * @param key the key to parse
593      *
594      * @return the attribute name, or null if the key doesn't contain one
595      */
596     protected static String parseAttributeName(String key)
597     {
598         String name = null;
599 
600         if (key != null)
601         {
602             // find the beginning of the attribute name
603             int attStart = key.indexOf(ATTRIBUTE_START);
604 
605             if (attStart > -1)
606             {
607                 // find the end of the attribute name
608                 int attEnd = key.indexOf(ATTRIBUTE_END);
609                 attEnd = attEnd > -1 ? attEnd : key.length();
610 
611                 name = key.substring(attStart + ATTRIBUTE_START.length(), attEnd);
612             }
613         }
614 
615         return name;
616     }
617 }