View Javadoc

1   /*
2    * Copyright 2004-2005 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.InputStream;
21  import java.io.OutputStream;
22  import java.io.Reader;
23  import java.io.Writer;
24  import java.net.URL;
25  import java.util.ArrayList;
26  import java.util.Collection;
27  import java.util.Iterator;
28  import java.util.List;
29  
30  import javax.xml.parsers.DocumentBuilder;
31  import javax.xml.parsers.DocumentBuilderFactory;
32  import javax.xml.parsers.ParserConfigurationException;
33  import javax.xml.transform.OutputKeys;
34  import javax.xml.transform.Result;
35  import javax.xml.transform.Source;
36  import javax.xml.transform.Transformer;
37  import javax.xml.transform.TransformerException;
38  import javax.xml.transform.TransformerFactory;
39  import javax.xml.transform.dom.DOMSource;
40  import javax.xml.transform.stream.StreamResult;
41  
42  import org.w3c.dom.Attr;
43  import org.w3c.dom.CDATASection;
44  import org.w3c.dom.DOMException;
45  import org.w3c.dom.Document;
46  import org.w3c.dom.Element;
47  import org.w3c.dom.NamedNodeMap;
48  import org.w3c.dom.NodeList;
49  import org.w3c.dom.Text;
50  import org.xml.sax.InputSource;
51  import org.apache.commons.configuration.reloading.ReloadingStrategy;
52  
53  /***
54   * A specialized hierarchical configuration class that is able to parse XML
55   * documents.
56   * 
57   * <p>The parsed document will be stored keeping its structure. The class also
58   * tries to preserve as much information from the loaded XML document as
59   * possible, including comments and processing instructions. These will be
60   * contained in documents created by the <code>save()</code> methods, too.
61   * 
62   * @since commons-configuration 1.0
63   * 
64   * @author J&ouml;rg Schaible
65   * @author <a href="mailto:oliver.heger@t-online.de">Oliver Heger </a>
66   * @version $Revision: 156237 $, $Date: 2005-03-05 11:26:22 +0100 (Sa, 05 Mrz 2005) $
67   */
68  public class XMLConfiguration extends HierarchicalConfiguration implements FileConfiguration
69  {
70      /*** Constant for the default root element name. */
71      private static final String DEFAULT_ROOT_NAME = "configuration";
72  
73      /*** Delimiter character for attributes. */
74      private static char ATTR_DELIMITER = ',';
75  
76      private FileConfigurationDelegate delegate = new FileConfigurationDelegate();
77  
78      /*** The document from this configuration's data source. */
79      private Document document;
80  
81      /*** Stores the name of the root element. */
82      private String rootElementName;
83  
84      /***
85       * Creates a new instance of <code>XMLConfiguration</code>.
86       */
87      public XMLConfiguration()
88      {
89          super();
90      }
91  
92      /***
93       * Creates a new instance of <code>XMLConfiguration</code>.
94       * The configuration is loaded from the specified file
95       * 
96       * @param fileName the name of the file to load
97       * @throws ConfigurationException if the file cannot be loaded
98       */
99      public XMLConfiguration(String fileName) throws ConfigurationException
100     {
101         this();
102         setFileName(fileName);
103         load();
104     }
105 
106     /***
107      * Creates a new instance of <code>XMLConfiguration</code>.
108      * The configuration is loaded from the specified file.
109      * 
110      * @param file the file
111      * @throws ConfigurationException if an error occurs while loading the file
112      */
113     public XMLConfiguration(File file) throws ConfigurationException
114     {
115         this();
116         setFile(file);
117         if (file.exists())
118         {
119             load();
120         }
121     }
122 
123     /***
124      * Creates a new instance of <code>XMLConfiguration</code>.
125      * The configuration is loaded from the specified URL.
126      * 
127      * @param url the URL
128      * @throws ConfigurationException if loading causes an error
129      */
130     public XMLConfiguration(URL url) throws ConfigurationException
131     {
132         this();
133         setURL(url);
134         load();
135     }
136 
137     /***
138      * Returns the name of the root element. If this configuration was loaded
139      * from a XML document, the name of this document's root element is
140      * returned. Otherwise it is possible to set a name for the root element
141      * that will be used when this configuration is stored.
142      * 
143      * @return the name of the root element
144      */
145     public String getRootElementName()
146     {
147         if (getDocument() == null)
148         {
149             return (rootElementName == null) ? DEFAULT_ROOT_NAME : rootElementName;
150         }
151         else
152         {
153             return getDocument().getDocumentElement().getNodeName();
154         }
155     }
156 
157     /***
158      * Sets the name of the root element. This name is used when this
159      * configuration object is stored in an XML file. Note that setting the name
160      * of the root element works only if this configuration has been newly
161      * created. If the configuration was loaded from an XML file, the name
162      * cannot be changed and an <code>UnsupportedOperationException</code>
163      * exception is thrown. Whether this configuration has been loaded from an
164      * XML document or not can be found out using the <code>getDocument()</code>
165      * method.
166      * 
167      * @param name the name of the root element
168      */
169     public void setRootElementName(String name)
170     {
171         if (getDocument() != null)
172         {
173             throw new UnsupportedOperationException("The name of the root element "
174                     + "cannot be changed when loaded from an XML document!");
175         }
176         rootElementName = name;
177     }
178     
179     /***
180      * Returns the XML document this configuration was loaded from. The return
181      * value is <b>null</b> if this configuration was not loaded from a XML
182      * document.
183      * 
184      * @return the XML document this configuration was loaded from
185      */
186     public Document getDocument()
187     {
188         return document;
189     }
190 
191     /***
192      * @inheritDoc
193      */
194     protected void addPropertyDirect(String key, Object obj)
195     {
196         super.addPropertyDirect(key, obj);
197         delegate.possiblySave();
198     }
199 
200     /***
201      * @inheritDoc
202      */
203     public void clearProperty(String key)
204     {
205         super.clearProperty(key);
206         delegate.possiblySave();
207     }
208 
209     /***
210      * @inheritDoc
211      */
212     public void clearTree(String key)
213     {
214         super.clearTree(key);
215         delegate.possiblySave();
216     }
217     
218     /***
219      * @inheritDoc
220      */
221     public void setProperty(String key, Object value)
222     {
223         super.setProperty(key, value);
224         delegate.possiblySave();
225     }
226 
227     /***
228      * Initializes this configuration from an XML document.
229      * 
230      * @param document the document to be parsed
231      * @param elemRefs a flag whether references to the XML elements should be set
232      */
233     public void initProperties(Document document, boolean elemRefs)
234     {
235         constructHierarchy(getRoot(), document.getDocumentElement(), elemRefs);
236     }
237 
238     /***
239      * Helper method for building the internal storage hierarchy. The XML
240      * elements are transformed into node objects.
241      * 
242      * @param node the actual node
243      * @param element the actual XML element
244      * @param elemRefs a flag whether references to the XML elements should be set
245      */
246     private void constructHierarchy(Node node, Element element, boolean elemRefs)
247     {
248         processAttributes(node, element);
249         StringBuffer buffer = new StringBuffer();
250         NodeList list = element.getChildNodes();
251         for (int i = 0; i < list.getLength(); i++)
252         {
253             org.w3c.dom.Node w3cNode = list.item(i);
254             if (w3cNode instanceof Element)
255             {
256                 Element child = (Element) w3cNode;
257                 Node childNode = new XMLNode(child.getTagName(), 
258                         (elemRefs) ? child : null);
259                 constructHierarchy(childNode, child, elemRefs);
260                 node.addChild(childNode);
261             }
262             else if (w3cNode instanceof Text)
263             {
264                 Text data = (Text) w3cNode;
265                 buffer.append(data.getData());
266             }
267         }
268         String text = buffer.toString().trim();
269         if (text.length() > 0)
270         {
271             node.setValue(text);
272         }
273     }
274 
275     /***
276      * Helper method for constructing node objects for the attributes of the
277      * given XML element.
278      * 
279      * @param node the actual node
280      * @param element the actual XML element
281      */
282     private void processAttributes(Node node, Element element)
283     {
284         NamedNodeMap attributes = element.getAttributes();
285         for (int i = 0; i < attributes.getLength(); ++i)
286         {
287             org.w3c.dom.Node w3cNode = attributes.item(i);
288             if (w3cNode instanceof Attr)
289             {
290                 Attr attr = (Attr) w3cNode;
291                 for (Iterator it = PropertyConverter.split(attr.getValue(), ATTR_DELIMITER).iterator(); it.hasNext();)
292                 {
293                     Node child = new XMLNode(ConfigurationKey.constructAttributeKey(attr.getName()), element);
294                     child.setValue(it.next());
295                     node.addChild(child);
296                 }
297             }
298         }
299     }
300 
301     /***
302      * Creates a DOM document from the internal tree of configuration nodes.
303      * 
304      * @return the new document
305      * @throws ConfigurationException if an error occurs
306      */
307     protected Document createDocument() throws ConfigurationException
308     {
309         try
310         {
311             if (document == null)
312             {
313                 DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
314                 Document newDocument = builder.newDocument();
315                 Element rootElem = newDocument.createElement(getRootElementName());
316                 newDocument.appendChild(rootElem);
317                 document = newDocument;
318             }
319 
320             XMLBuilderVisitor builder = new XMLBuilderVisitor(document);
321             builder.processDocument(getRoot());
322             return document;
323         } /* try */
324         catch (DOMException domEx)
325         {
326             throw new ConfigurationException(domEx);
327         }
328         catch (ParserConfigurationException pex)
329         {
330             throw new ConfigurationException(pex);
331         }
332     }
333 
334     /***
335      * Creates a new node object. This implementation returns an instance of the
336      * <code>XMLNode</code> class.
337      * 
338      * @param name the node's name
339      * @return the new node
340      */
341     protected Node createNode(String name)
342     {
343         return new XMLNode(name, null);
344     }
345 
346     public void load() throws ConfigurationException
347     {
348         delegate.load();
349     }
350 
351     public void load(String fileName) throws ConfigurationException
352     {
353         delegate.load(fileName);
354     }
355 
356     public void load(File file) throws ConfigurationException
357     {
358         delegate.load(file);
359     }
360 
361     public void load(URL url) throws ConfigurationException
362     {
363         delegate.load(url);
364     }
365 
366     public void load(InputStream in) throws ConfigurationException
367     {
368         delegate.load(in);
369     }
370 
371     public void load(InputStream in, String encoding) throws ConfigurationException
372     {
373         delegate.load(in, encoding);
374     }
375 
376     /***
377      * Load the properties from the given reader.
378      * Note that the <code>clear()</code> method is not called, so
379      * the properties contained in the loaded file will be added to the
380      * actual set of properties.
381      *
382      * @param in An InputStream.
383      *
384      * @throws ConfigurationException
385      */
386     public void load(Reader in) throws ConfigurationException
387     {
388         try
389         {
390             DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
391             Document newDocument = builder.parse(new InputSource(in));
392             Document oldDocument = document;
393             document = null;
394             initProperties(newDocument, oldDocument == null);
395             document = (oldDocument == null) ? newDocument : oldDocument;
396         }
397         catch (Exception e)
398         {
399             throw new ConfigurationException(e.getMessage(), e);
400         }
401     }
402 
403     public void save() throws ConfigurationException
404     {
405         delegate.save();
406     }
407 
408     public void save(String fileName) throws ConfigurationException
409     {
410         delegate.save(fileName);
411     }
412 
413     public void save(File file) throws ConfigurationException
414     {
415         delegate.save(file);
416     }
417 
418     public void save(URL url) throws ConfigurationException
419     {
420         delegate.save(url);
421     }
422 
423     public void save(OutputStream out) throws ConfigurationException
424     {
425         delegate.save(out);
426     }
427 
428     public void save(OutputStream out, String encoding) throws ConfigurationException
429     {
430         delegate.save(out, encoding);
431     }
432 
433     /***
434      * Saves the configuration to the specified writer.
435      * 
436      * @param writer the writer used to save the configuration
437      * @throws ConfigurationException if an error occurs
438      */
439     public void save(Writer writer) throws ConfigurationException
440     {
441         try
442         {
443             Transformer transformer = TransformerFactory.newInstance().newTransformer();
444             Source source = new DOMSource(createDocument());
445             Result result = new StreamResult(writer);
446 
447             transformer.setOutputProperty(OutputKeys.INDENT, "yes");
448             transformer.transform(source, result);
449         }
450         catch (TransformerException e)
451         {
452             throw new ConfigurationException(e.getMessage(), e);
453         }
454     }
455 
456     public String getFileName()
457     {
458         return delegate.getFileName();
459     }
460 
461     public void setFileName(String fileName)
462     {
463         delegate.setFileName(fileName);
464     }
465 
466     public String getBasePath()
467     {
468         return delegate.getBasePath();
469     }
470 
471     public void setBasePath(String basePath)
472     {
473         delegate.setBasePath(basePath);
474     }
475 
476     public File getFile()
477     {
478         return delegate.getFile();
479     }
480 
481     public void setFile(File file)
482     {
483         delegate.setFile(file);
484     }
485 
486     public URL getURL()
487     {
488         return delegate.getURL();
489     }
490 
491     public void setURL(URL url)
492     {
493         delegate.setURL(url);
494     }
495 
496     public void setAutoSave(boolean autoSave)
497     {
498         delegate.setAutoSave(autoSave);
499     }
500 
501     public boolean isAutoSave()
502     {
503         return delegate.isAutoSave();
504     }
505 
506     public ReloadingStrategy getReloadingStrategy()
507     {
508         return delegate.getReloadingStrategy();
509     }
510 
511     public void setReloadingStrategy(ReloadingStrategy strategy)
512     {
513         delegate.setReloadingStrategy(strategy);
514     }
515 
516     public void reload()
517     {
518         delegate.reload();
519     }
520 
521     public String getEncoding()
522     {
523         return delegate.getEncoding();
524     }
525 
526     public void setEncoding(String encoding)
527     {
528         delegate.setEncoding(encoding);
529     }
530 
531     /***
532      * A specialized <code>Node</code> class that is connected with an XML
533      * element. Changes on a node are also performed on the associated element.
534      */
535     class XMLNode extends Node
536     {
537         /***
538          * Creates a new instance of <code>XMLNode</code> and initializes it
539          * with the corresponding XML element.
540          * 
541          * @param elem the XML element
542          */
543         public XMLNode(Element elem)
544         {
545             super();
546             setReference(elem);
547         }
548 
549         /***
550          * Creates a new instance of <code>XMLNode</code> and initializes it
551          * with a name and the corresponding XML element.
552          * 
553          * @param name the node's name
554          * @param elem the XML element
555          */
556         public XMLNode(String name, Element elem)
557         {
558             super(name);
559             setReference(elem);
560         }
561 
562         /***
563          * Sets the value of this node. If this node is associated with an XML
564          * element, this element will be updated, too.
565          * 
566          * @param value the node's new value
567          */
568         public void setValue(Object value)
569         {
570             super.setValue(value);
571 
572             if (getReference() != null && document != null)
573             {
574                 if (ConfigurationKey.isAttributeKey(getName()))
575                 {
576                     updateAttribute();
577                 }
578                 else
579                 {
580                     updateElement(value);
581                 }
582             }
583         }
584 
585         /***
586          * Updates the associated XML elements when a node is removed.
587          */
588         protected void removeReference()
589         {
590             if (getReference() != null)
591             {
592                 Element element = (Element) getReference();
593                 if (ConfigurationKey.isAttributeKey(getName()))
594                 {
595                     updateAttribute();
596                 }
597                 else
598                 {
599                     org.w3c.dom.Node parentElem = element.getParentNode();
600                     if (parentElem != null)
601                     {
602                         parentElem.removeChild(element);
603                     }
604                 }
605             }
606         }
607 
608         /***
609          * Updates the node's value if it represents an element node.
610          * 
611          * @param value the new value
612          */
613         private void updateElement(Object value)
614         {
615             Text txtNode = findTextNodeForUpdate();
616             if (value == null)
617             {
618                 // remove text
619                 if (txtNode != null)
620                 {
621                     ((Element) getReference()).removeChild(txtNode);
622                 }
623             }
624             else
625             {
626                 if (txtNode == null)
627                 {
628                     txtNode = document.createTextNode(value.toString());
629                     if (((Element) getReference()).getFirstChild() != null)
630                     {
631                         ((Element) getReference()).insertBefore(txtNode, ((Element) getReference()).getFirstChild());
632                     }
633                     else
634                     {
635                         ((Element) getReference()).appendChild(txtNode);
636                     }
637                 }
638                 else
639                 {
640                     txtNode.setNodeValue(value.toString());
641                 }
642             }
643         }
644 
645         /***
646          * Updates the node's value if it represents an attribute.
647          *  
648          */
649         private void updateAttribute()
650         {
651             XMLBuilderVisitor.updateAttribute(getParent(), getName());
652         }
653 
654         /***
655          * Returns the only text node of this element for update. This method is
656          * called when the element's text changes. Then all text nodes except
657          * for the first are removed. A reference to the first is returned or
658          * <b>null </b> if there is no text node at all.
659          * 
660          * @return the first and only text node
661          */
662         private Text findTextNodeForUpdate()
663         {
664             Text result = null;
665             Element elem = (Element) getReference();
666             // Find all Text nodes
667             NodeList children = elem.getChildNodes();
668             Collection textNodes = new ArrayList();
669             for (int i = 0; i < children.getLength(); i++)
670             {
671                 org.w3c.dom.Node nd = children.item(i);
672                 if (nd instanceof Text)
673                 {
674                     if (result == null)
675                     {
676                         result = (Text) nd;
677                     }
678                     else
679                     {
680                         textNodes.add(nd);
681                     }
682                 }
683             }
684 
685             // We don't want CDATAs
686             if (result instanceof CDATASection)
687             {
688                 textNodes.add(result);
689                 result = null;
690             }
691 
692             // Remove all but the first Text node
693             for (Iterator it = textNodes.iterator(); it.hasNext();)
694             {
695                 elem.removeChild((org.w3c.dom.Node) it.next());
696             }
697             return result;
698         }
699     }
700 
701     /***
702      * A concrete <code>BuilderVisitor</code> that can construct XML
703      * documents.
704      */
705     static class XMLBuilderVisitor extends BuilderVisitor
706     {
707         /*** Stores the document to be constructed. */
708         private Document document;
709 
710         /***
711          * Creates a new instance of <code>XMLBuilderVisitor</code>
712          * 
713          * @param doc the document to be created
714          */
715         public XMLBuilderVisitor(Document doc)
716         {
717             document = doc;
718         }
719 
720         /***
721          * Processes the node hierarchy and adds new nodes to the document.
722          * 
723          * @param rootNode the root node
724          */
725         public void processDocument(Node rootNode)
726         {
727             rootNode.visit(this, null);
728         }
729 
730         /***
731          * @inheritDoc
732          */
733         protected Object insert(Node newNode, Node parent, Node sibling1, Node sibling2)
734         {
735             if (ConfigurationKey.isAttributeKey(newNode.getName()))
736             {
737                 updateAttribute(parent, getElement(parent), newNode.getName());
738                 return null;
739             }
740 
741             else
742             {
743                 Element elem = document.createElement(newNode.getName());
744                 if (newNode.getValue() != null)
745                 {
746                     elem.appendChild(document.createTextNode(newNode.getValue().toString()));
747                 }
748                 if (sibling2 == null)
749                 {
750                     getElement(parent).appendChild(elem);
751                 }
752                 else if (sibling1 != null)
753                 {
754                     getElement(parent).insertBefore(elem, getElement(sibling1).getNextSibling());
755                 }
756                 else
757                 {
758                     getElement(parent).insertBefore(elem, getElement(parent).getFirstChild());
759                 }
760                 return elem;
761             }
762         }
763 
764         /***
765          * Helper method for updating the value of the specified node's
766          * attribute with the given name.
767          * 
768          * @param node the affected node
769          * @param elem the element that is associated with this node
770          * @param name the name of the affected attribute
771          */
772         private static void updateAttribute(Node node, Element elem, String name)
773         {
774             if (node != null && elem != null)
775             {
776                 List attrs = node.getChildren(name);
777                 StringBuffer buf = new StringBuffer();
778                 for (Iterator it = attrs.iterator(); it.hasNext();)
779                 {
780                     Node attr = (Node) it.next();
781                     if (attr.getValue() != null)
782                     {
783                         if (buf.length() > 0)
784                         {
785                             buf.append(ATTR_DELIMITER);
786                         }
787                         buf.append(attr.getValue());
788                     }
789                     attr.setReference(elem);
790                 }
791 
792                 if (buf.length() < 1)
793                 {
794                     elem.removeAttribute(ConfigurationKey.removeAttributeMarkers(name));
795                 }
796                 else
797                 {
798                     elem.setAttribute(ConfigurationKey.removeAttributeMarkers(name), buf.toString());
799                 }
800             }
801         }
802 
803         /***
804          * Updates the value of the specified attribute of the given node.
805          * Because there can be multiple child nodes representing this attribute
806          * the new value is determined by iterating over all those child nodes.
807          * 
808          * @param node the affected node
809          * @param name the name of the attribute
810          */
811         static void updateAttribute(Node node, String name)
812         {
813             if (node != null)
814             {
815                 updateAttribute(node, (Element) node.getReference(), name);
816             }
817         }
818 
819         /***
820          * Helper method for accessing the element of the specified node.
821          * 
822          * @param node the node
823          * @return the element of this node
824          */
825         private Element getElement(Node node)
826         {
827             // special treatement for root node of the hierarchy
828             return (node.getName() != null) ? (Element) node.getReference() : document.getDocumentElement();
829         }
830     }
831 
832     private class FileConfigurationDelegate extends AbstractFileConfiguration
833     {
834         public void load(Reader in) throws ConfigurationException
835         {
836             XMLConfiguration.this.load(in);
837         }
838 
839         public void save(Writer out) throws ConfigurationException
840         {
841             XMLConfiguration.this.save(out);
842         }
843     }
844 }