1
2
3
4
5
6
7
8
9
10
11
12
13
14
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ö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 }
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
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
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
686 if (result instanceof CDATASection)
687 {
688 textNodes.add(result);
689 result = null;
690 }
691
692
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
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 }