1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.apache.commons.configuration;
19
20 import java.io.File;
21 import java.io.InputStream;
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.TransformerFactoryConfigurationError;
40 import javax.xml.transform.dom.DOMSource;
41 import javax.xml.transform.stream.StreamResult;
42
43 import org.apache.commons.collections.iterators.SingletonIterator;
44 import org.w3c.dom.Attr;
45 import org.w3c.dom.CDATASection;
46 import org.w3c.dom.DOMException;
47 import org.w3c.dom.Document;
48 import org.w3c.dom.Element;
49 import org.w3c.dom.NamedNodeMap;
50 import org.w3c.dom.NodeList;
51 import org.w3c.dom.Text;
52 import org.xml.sax.InputSource;
53 import org.xml.sax.SAXException;
54 import org.xml.sax.SAXParseException;
55 import org.xml.sax.helpers.DefaultHandler;
56
57 /***
58 * <p>A specialized hierarchical configuration class that is able to parse XML
59 * documents.</p>
60 *
61 * <p>The parsed document will be stored keeping its structure. The class also
62 * tries to preserve as much information from the loaded XML document as
63 * possible, including comments and processing instructions. These will be
64 * contained in documents created by the <code>save()</code> methods, too.</p>
65 *
66 * <p>Like other file based configuration classes this class maintains the name
67 * and path to the loaded configuration file. These properties can be altered
68 * using several setter methods, but they are not modified by <code>save()</code>
69 * and <code>load()</code> methods. If XML documents contain relative paths to
70 * other documents (e.g. to a DTD), these references are resolved based on the
71 * path set for this configuration.</p>
72 *
73 * <p>By inheriting from <code>{@link AbstractConfiguration}</code> this class
74 * provides some extended functionaly, e.g. interpolation of property values.
75 * Like in <code>{@link PropertiesConfiguration}</code> property values can
76 * contain delimiter characters (the comma ',' per default) and are then splitted
77 * into multiple values. This works for XML attributes and text content of
78 * elements as well. The delimiter can be escaped by a backslash. As an example
79 * consider the following XML fragment:</p>
80 *
81 * <p>
82 * <pre>
83 * <config>
84 * <array>10,20,30,40</array>
85 * <scalar>3\,1415</scalar>
86 * <cite text="To be or not to be\, this is the question!"/>
87 * </config>
88 * </pre>
89 * </p>
90 * <p>Here the content of the <code>array</code> element will be splitted at
91 * the commas, so the <code>array</code> key will be assigned 4 values. In the
92 * <code>scalar</code> property and the <code>text</code> attribute of the
93 * <code>cite</code> element the comma is escaped, so that no splitting is
94 * performed.</p>
95 *
96 * <p><code>XMLConfiguration</code> implements the <code>{@link FileConfiguration}</code>
97 * interface and thus provides full support for loading XML documents from
98 * different sources like files, URLs, or streams. A full description of these
99 * features can be found in the documentation of
100 * <code>{@link AbstractFileConfiguration}</code>.</p>
101 *
102 * @since commons-configuration 1.0
103 *
104 * @author Jörg Schaible
105 * @author <a href="mailto:oliver.heger@t-online.de">Oliver Heger </a>
106 * @version $Revision: 439648 $, $Date: 2006-09-02 22:42:10 +0200 (Sa, 02 Sep 2006) $
107 */
108 public class XMLConfiguration extends AbstractHierarchicalFileConfiguration
109 {
110 /***
111 * The serial version UID.
112 */
113 private static final long serialVersionUID = 2453781111653383552L;
114
115 /*** Constant for the default root element name. */
116 private static final String DEFAULT_ROOT_NAME = "configuration";
117
118 /*** The document from this configuration's data source. */
119 private Document document;
120
121 /*** Stores the name of the root element. */
122 private String rootElementName;
123
124 /*** Stores the public ID from the DOCTYPE.*/
125 private String publicID;
126
127 /*** Stores the system ID from the DOCTYPE.*/
128 private String systemID;
129
130 /*** Stores the document builder that should be used for loading.*/
131 private DocumentBuilder documentBuilder;
132
133 /*** Stores a flag whether DTD validation should be performed.*/
134 private boolean validating;
135
136 /***
137 * Creates a new instance of <code>XMLConfiguration</code>.
138 */
139 public XMLConfiguration()
140 {
141 super();
142 }
143
144 /***
145 * Creates a new instance of <code>XMLConfiguration</code>.
146 * The configuration is loaded from the specified file
147 *
148 * @param fileName the name of the file to load
149 * @throws ConfigurationException if the file cannot be loaded
150 */
151 public XMLConfiguration(String fileName) throws ConfigurationException
152 {
153 super(fileName);
154 }
155
156 /***
157 * Creates a new instance of <code>XMLConfiguration</code>.
158 * The configuration is loaded from the specified file.
159 *
160 * @param file the file
161 * @throws ConfigurationException if an error occurs while loading the file
162 */
163 public XMLConfiguration(File file) throws ConfigurationException
164 {
165 super(file);
166 }
167
168 /***
169 * Creates a new instance of <code>XMLConfiguration</code>.
170 * The configuration is loaded from the specified URL.
171 *
172 * @param url the URL
173 * @throws ConfigurationException if loading causes an error
174 */
175 public XMLConfiguration(URL url) throws ConfigurationException
176 {
177 super(url);
178 }
179
180 /***
181 * Returns the name of the root element. If this configuration was loaded
182 * from a XML document, the name of this document's root element is
183 * returned. Otherwise it is possible to set a name for the root element
184 * that will be used when this configuration is stored.
185 *
186 * @return the name of the root element
187 */
188 public String getRootElementName()
189 {
190 if (getDocument() == null)
191 {
192 return (rootElementName == null) ? DEFAULT_ROOT_NAME : rootElementName;
193 }
194 else
195 {
196 return getDocument().getDocumentElement().getNodeName();
197 }
198 }
199
200 /***
201 * Sets the name of the root element. This name is used when this
202 * configuration object is stored in an XML file. Note that setting the name
203 * of the root element works only if this configuration has been newly
204 * created. If the configuration was loaded from an XML file, the name
205 * cannot be changed and an <code>UnsupportedOperationException</code>
206 * exception is thrown. Whether this configuration has been loaded from an
207 * XML document or not can be found out using the <code>getDocument()</code>
208 * method.
209 *
210 * @param name the name of the root element
211 */
212 public void setRootElementName(String name)
213 {
214 if (getDocument() != null)
215 {
216 throw new UnsupportedOperationException("The name of the root element "
217 + "cannot be changed when loaded from an XML document!");
218 }
219 rootElementName = name;
220 }
221
222 /***
223 * Returns the <code>DocumentBuilder</code> object that is used for
224 * loading documents. If no specific builder has been set, this method
225 * returns <b>null</b>.
226 *
227 * @return the <code>DocumentBuilder</code> for loading new documents
228 * @since 1.2
229 */
230 public DocumentBuilder getDocumentBuilder()
231 {
232 return documentBuilder;
233 }
234
235 /***
236 * Sets the <code>DocumentBuilder</code> object to be used for loading
237 * documents. This method makes it possible to specify the exact document
238 * builder. So an application can create a builder, configure it for its
239 * special needs, and then pass it to this method.
240 *
241 * @param documentBuilder the document builder to be used; if undefined, a
242 * default builder will be used
243 * @since 1.2
244 */
245 public void setDocumentBuilder(DocumentBuilder documentBuilder)
246 {
247 this.documentBuilder = documentBuilder;
248 }
249
250 /***
251 * Returns the public ID of the DOCTYPE declaration from the loaded XML
252 * document. This is <b>null</b> if no document has been loaded yet or if
253 * the document does not contain a DOCTYPE declaration with a public ID.
254 *
255 * @return the public ID
256 * @since 1.3
257 */
258 public String getPublicID()
259 {
260 return publicID;
261 }
262
263 /***
264 * Sets the public ID of the DOCTYPE declaration. When this configuration is
265 * saved, a DOCTYPE declaration will be constructed that contains this
266 * public ID.
267 *
268 * @param publicID the public ID
269 * @since 1.3
270 */
271 public void setPublicID(String publicID)
272 {
273 this.publicID = publicID;
274 }
275
276 /***
277 * Returns the system ID of the DOCTYPE declaration from the loaded XML
278 * document. This is <b>null</b> if no document has been loaded yet or if
279 * the document does not contain a DOCTYPE declaration with a system ID.
280 *
281 * @return the system ID
282 * @since 1.3
283 */
284 public String getSystemID()
285 {
286 return systemID;
287 }
288
289 /***
290 * Sets the system ID of the DOCTYPE declaration. When this configuration is
291 * saved, a DOCTYPE declaration will be constructed that contains this
292 * system ID.
293 *
294 * @param systemID the system ID
295 * @since 1.3
296 */
297 public void setSystemID(String systemID)
298 {
299 this.systemID = systemID;
300 }
301
302 /***
303 * Returns the value of the validating flag.
304 *
305 * @return the validating flag
306 * @since 1.2
307 */
308 public boolean isValidating()
309 {
310 return validating;
311 }
312
313 /***
314 * Sets the value of the validating flag. This flag determines whether
315 * DTD validation should be performed when loading XML documents. This
316 * flag is evaluated only if no custom <code>DocumentBuilder</code> was set.
317 *
318 * @param validating the validating flag
319 * @since 1.2
320 */
321 public void setValidating(boolean validating)
322 {
323 this.validating = validating;
324 }
325
326 /***
327 * Returns the XML document this configuration was loaded from. The return
328 * value is <b>null</b> if this configuration was not loaded from a XML
329 * document.
330 *
331 * @return the XML document this configuration was loaded from
332 */
333 public Document getDocument()
334 {
335 return document;
336 }
337
338 /***
339 * Removes all properties from this configuration. If this configuration
340 * was loaded from a file, the associated DOM document is also cleared.
341 */
342 public void clear()
343 {
344 super.clear();
345 document = null;
346 }
347
348 /***
349 * Initializes this configuration from an XML document.
350 *
351 * @param document the document to be parsed
352 * @param elemRefs a flag whether references to the XML elements should be set
353 */
354 public void initProperties(Document document, boolean elemRefs)
355 {
356 if (document.getDoctype() != null)
357 {
358 setPublicID(document.getDoctype().getPublicId());
359 setSystemID(document.getDoctype().getSystemId());
360 }
361 constructHierarchy(getRoot(), document.getDocumentElement(), elemRefs);
362 }
363
364 /***
365 * Helper method for building the internal storage hierarchy. The XML
366 * elements are transformed into node objects.
367 *
368 * @param node the actual node
369 * @param element the actual XML element
370 * @param elemRefs a flag whether references to the XML elements should be set
371 */
372 private void constructHierarchy(Node node, Element element, boolean elemRefs)
373 {
374 processAttributes(node, element, elemRefs);
375 StringBuffer buffer = new StringBuffer();
376 NodeList list = element.getChildNodes();
377 for (int i = 0; i < list.getLength(); i++)
378 {
379 org.w3c.dom.Node w3cNode = list.item(i);
380 if (w3cNode instanceof Element)
381 {
382 Element child = (Element) w3cNode;
383 Node childNode = new XMLNode(child.getTagName(),
384 elemRefs ? child : null);
385 constructHierarchy(childNode, child, elemRefs);
386 node.addChild(childNode);
387 handleDelimiters(node, childNode);
388 }
389 else if (w3cNode instanceof Text)
390 {
391 Text data = (Text) w3cNode;
392 buffer.append(data.getData());
393 }
394 }
395 String text = buffer.toString().trim();
396 if (text.length() > 0 || !node.hasChildren())
397 {
398 node.setValue(text);
399 }
400 }
401
402 /***
403 * Helper method for constructing node objects for the attributes of the
404 * given XML element.
405 *
406 * @param node the actual node
407 * @param element the actual XML element
408 * @param elemRefs a flag whether references to the XML elements should be set
409 */
410 private void processAttributes(Node node, Element element, boolean elemRefs)
411 {
412 NamedNodeMap attributes = element.getAttributes();
413 for (int i = 0; i < attributes.getLength(); ++i)
414 {
415 org.w3c.dom.Node w3cNode = attributes.item(i);
416 if (w3cNode instanceof Attr)
417 {
418 Attr attr = (Attr) w3cNode;
419 Iterator it;
420 if (isDelimiterParsingDisabled())
421 {
422 it = new SingletonIterator(attr.getValue());
423 }
424 else
425 {
426 it = PropertyConverter.split(attr.getValue(), getListDelimiter()).iterator();
427 }
428 while (it.hasNext())
429 {
430 Node child = new XMLNode(attr.getName(),
431 elemRefs ? element : null);
432 child.setValue(it.next());
433 node.addAttribute(child);
434 }
435 }
436 }
437 }
438
439 /***
440 * Deals with elements whose value is a list. In this case multiple child
441 * elements must be added.
442 *
443 * @param parent the parent element
444 * @param child the child element
445 */
446 private void handleDelimiters(Node parent, Node child)
447 {
448 if (child.getValue() != null)
449 {
450 List values;
451 if (isDelimiterParsingDisabled())
452 {
453 values = new ArrayList();
454 values.add(child.getValue().toString());
455 }
456 else
457 {
458 values = PropertyConverter.split(child.getValue().toString(),
459 getListDelimiter());
460 }
461
462 if (values.size() > 1)
463 {
464
465 parent.remove(child);
466
467 for (Iterator it = values.iterator(); it.hasNext();)
468 {
469 Node c = new XMLNode(child.getName(), null);
470 c.setValue(it.next());
471 parent.addChild(c);
472 }
473 }
474 else if (values.size() == 1)
475 {
476
477
478 child.setValue(values.get(0));
479 }
480 }
481 }
482
483 /***
484 * Creates the <code>DocumentBuilder</code> to be used for loading files.
485 * This implementation checks whether a specific
486 * <code>DocumentBuilder</code> has been set. If this is the case, this
487 * one is used. Otherwise a default builder is created. Depending on the
488 * value of the validating flag this builder will be a validating or a non
489 * validating <code>DocumentBuilder</code>.
490 *
491 * @return the <code>DocumentBuilder</code> for loading configuration
492 * files
493 * @throws ParserConfigurationException if an error occurs
494 * @since 1.2
495 */
496 protected DocumentBuilder createDocumentBuilder()
497 throws ParserConfigurationException
498 {
499 if (getDocumentBuilder() != null)
500 {
501 return getDocumentBuilder();
502 }
503 else
504 {
505 DocumentBuilderFactory factory = DocumentBuilderFactory
506 .newInstance();
507 factory.setValidating(isValidating());
508 DocumentBuilder result = factory.newDocumentBuilder();
509
510 if (isValidating())
511 {
512
513 result.setErrorHandler(new DefaultHandler()
514 {
515 public void error(SAXParseException ex) throws SAXException
516 {
517 throw ex;
518 }
519 });
520 }
521 return result;
522 }
523 }
524
525 /***
526 * Creates a DOM document from the internal tree of configuration nodes.
527 *
528 * @return the new document
529 * @throws ConfigurationException if an error occurs
530 */
531 protected Document createDocument() throws ConfigurationException
532 {
533 try
534 {
535 if (document == null)
536 {
537 DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
538 Document newDocument = builder.newDocument();
539 Element rootElem = newDocument.createElement(getRootElementName());
540 newDocument.appendChild(rootElem);
541 document = newDocument;
542 }
543
544 XMLBuilderVisitor builder = new XMLBuilderVisitor(document, getListDelimiter());
545 builder.processDocument(getRoot());
546 return document;
547 }
548 catch (DOMException domEx)
549 {
550 throw new ConfigurationException(domEx);
551 }
552 catch (ParserConfigurationException pex)
553 {
554 throw new ConfigurationException(pex);
555 }
556 }
557
558 /***
559 * Creates a new node object. This implementation returns an instance of the
560 * <code>XMLNode</code> class.
561 *
562 * @param name the node's name
563 * @return the new node
564 */
565 protected Node createNode(String name)
566 {
567 return new XMLNode(name, null);
568 }
569
570 /***
571 * Loads the configuration from the given input stream.
572 *
573 * @param in the input stream
574 * @throws ConfigurationException if an error occurs
575 */
576 public void load(InputStream in) throws ConfigurationException
577 {
578 load(new InputSource(in));
579 }
580
581 /***
582 * Load the configuration from the given reader.
583 * Note that the <code>clear()</code> method is not called, so
584 * the properties contained in the loaded file will be added to the
585 * actual set of properties.
586 *
587 * @param in An InputStream.
588 *
589 * @throws ConfigurationException if an error occurs
590 */
591 public void load(Reader in) throws ConfigurationException
592 {
593 load(new InputSource(in));
594 }
595
596 /***
597 * Loads a configuration file from the specified input source.
598 * @param source the input source
599 * @throws ConfigurationException if an error occurs
600 */
601 private void load(InputSource source) throws ConfigurationException
602 {
603 try
604 {
605 URL sourceURL = getDelegate().getURL();
606 if (sourceURL != null)
607 {
608 source.setSystemId(sourceURL.toString());
609 }
610
611 DocumentBuilder builder = createDocumentBuilder();
612 Document newDocument = builder.parse(source);
613 Document oldDocument = document;
614 document = null;
615 initProperties(newDocument, oldDocument == null);
616 document = (oldDocument == null) ? newDocument : oldDocument;
617 }
618 catch (Exception e)
619 {
620 throw new ConfigurationException(e.getMessage(), e);
621 }
622 }
623
624 /***
625 * Saves the configuration to the specified writer.
626 *
627 * @param writer the writer used to save the configuration
628 * @throws ConfigurationException if an error occurs
629 */
630 public void save(Writer writer) throws ConfigurationException
631 {
632 try
633 {
634 Transformer transformer = createTransformer();
635 Source source = new DOMSource(createDocument());
636 Result result = new StreamResult(writer);
637 transformer.transform(source, result);
638 }
639 catch (TransformerException e)
640 {
641 throw new ConfigurationException(e.getMessage(), e);
642 }
643 catch (TransformerFactoryConfigurationError err)
644 {
645 throw new ConfigurationException(err.getMessage(), err);
646 }
647 }
648
649 /***
650 * Creates and initializes the transformer used for save operations. This
651 * base implementation initializes all of the default settings like
652 * indention mode and the DOCTYPE. Derived classes may overload this method
653 * if they have specific needs.
654 *
655 * @return the transformer to use for a save operation
656 * @throws TransformerException if an error occurs
657 * @since 1.3
658 */
659 protected Transformer createTransformer() throws TransformerException
660 {
661 Transformer transformer = TransformerFactory.newInstance()
662 .newTransformer();
663
664 transformer.setOutputProperty(OutputKeys.INDENT, "yes");
665 if (getEncoding() != null)
666 {
667 transformer.setOutputProperty(OutputKeys.ENCODING, getEncoding());
668 }
669 if (getPublicID() != null)
670 {
671 transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC,
672 getPublicID());
673 }
674 if (getSystemID() != null)
675 {
676 transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM,
677 getSystemID());
678 }
679
680 return transformer;
681 }
682
683 /***
684 * Creates a copy of this object. The new configuration object will contain
685 * the same properties as the original, but it will lose any connection to a
686 * source document (if one exists). This is to avoid race conditions if both
687 * the original and the copy are modified and then saved.
688 *
689 * @return the copy
690 */
691 public Object clone()
692 {
693 XMLConfiguration copy = (XMLConfiguration) super.clone();
694
695
696 copy.document = null;
697 copy.setDelegate(createDelegate());
698
699 copy.getRoot().visit(new NodeVisitor()
700 {
701 public void visitBeforeChildren(Node node, ConfigurationKey key)
702 {
703 node.setReference(null);
704 }
705 }, null);
706
707 return copy;
708 }
709
710 /***
711 * Creates the file configuration delegate for this object. This implementation
712 * will return an instance of a class derived from <code>FileConfigurationDelegate</code>
713 * that deals with some specialities of <code>XMLConfiguration</code>.
714 * @return the delegate for this object
715 */
716 protected FileConfigurationDelegate createDelegate()
717 {
718 return new XMLFileConfigurationDelegate();
719 }
720
721 /***
722 * A specialized <code>Node</code> class that is connected with an XML
723 * element. Changes on a node are also performed on the associated element.
724 */
725 class XMLNode extends Node
726 {
727 /***
728 * The serial version UID.
729 */
730 private static final long serialVersionUID = -4133988932174596562L;
731
732 /***
733 * Creates a new instance of <code>XMLNode</code> and initializes it
734 * with a name and the corresponding XML element.
735 *
736 * @param name the node's name
737 * @param elem the XML element
738 */
739 public XMLNode(String name, Element elem)
740 {
741 super(name);
742 setReference(elem);
743 }
744
745 /***
746 * Sets the value of this node. If this node is associated with an XML
747 * element, this element will be updated, too.
748 *
749 * @param value the node's new value
750 */
751 public void setValue(Object value)
752 {
753 super.setValue(value);
754
755 if (getReference() != null && document != null)
756 {
757 if (isAttribute())
758 {
759 updateAttribute();
760 }
761 else
762 {
763 updateElement(value);
764 }
765 }
766 }
767
768 /***
769 * Updates the associated XML elements when a node is removed.
770 */
771 protected void removeReference()
772 {
773 if (getReference() != null)
774 {
775 Element element = (Element) getReference();
776 if (isAttribute())
777 {
778 updateAttribute();
779 }
780 else
781 {
782 org.w3c.dom.Node parentElem = element.getParentNode();
783 if (parentElem != null)
784 {
785 parentElem.removeChild(element);
786 }
787 }
788 }
789 }
790
791 /***
792 * Updates the node's value if it represents an element node.
793 *
794 * @param value the new value
795 */
796 private void updateElement(Object value)
797 {
798 Text txtNode = findTextNodeForUpdate();
799 if (value == null)
800 {
801
802 if (txtNode != null)
803 {
804 ((Element) getReference()).removeChild(txtNode);
805 }
806 }
807 else
808 {
809 if (txtNode == null)
810 {
811 txtNode = document
812 .createTextNode(PropertyConverter.escapeDelimiters(
813 value.toString(), getListDelimiter()));
814 if (((Element) getReference()).getFirstChild() != null)
815 {
816 ((Element) getReference()).insertBefore(txtNode,
817 ((Element) getReference()).getFirstChild());
818 }
819 else
820 {
821 ((Element) getReference()).appendChild(txtNode);
822 }
823 }
824 else
825 {
826 txtNode.setNodeValue(PropertyConverter.escapeDelimiters(
827 value.toString(), getListDelimiter()));
828 }
829 }
830 }
831
832 /***
833 * Updates the node's value if it represents an attribute.
834 *
835 */
836 private void updateAttribute()
837 {
838 XMLBuilderVisitor.updateAttribute(getParent(), getName(), getListDelimiter());
839 }
840
841 /***
842 * Returns the only text node of this element for update. This method is
843 * called when the element's text changes. Then all text nodes except
844 * for the first are removed. A reference to the first is returned or
845 * <b>null </b> if there is no text node at all.
846 *
847 * @return the first and only text node
848 */
849 private Text findTextNodeForUpdate()
850 {
851 Text result = null;
852 Element elem = (Element) getReference();
853
854 NodeList children = elem.getChildNodes();
855 Collection textNodes = new ArrayList();
856 for (int i = 0; i < children.getLength(); i++)
857 {
858 org.w3c.dom.Node nd = children.item(i);
859 if (nd instanceof Text)
860 {
861 if (result == null)
862 {
863 result = (Text) nd;
864 }
865 else
866 {
867 textNodes.add(nd);
868 }
869 }
870 }
871
872
873 if (result instanceof CDATASection)
874 {
875 textNodes.add(result);
876 result = null;
877 }
878
879
880 for (Iterator it = textNodes.iterator(); it.hasNext();)
881 {
882 elem.removeChild((org.w3c.dom.Node) it.next());
883 }
884 return result;
885 }
886 }
887
888 /***
889 * A concrete <code>BuilderVisitor</code> that can construct XML
890 * documents.
891 */
892 static class XMLBuilderVisitor extends BuilderVisitor
893 {
894 /*** Stores the document to be constructed. */
895 private Document document;
896
897 /*** Stores the list delimiter.*/
898 private char listDelimiter = AbstractConfiguration.
899 getDefaultListDelimiter();
900
901 /***
902 * Creates a new instance of <code>XMLBuilderVisitor</code>
903 *
904 * @param doc the document to be created
905 * @param listDelimiter the delimiter for attribute properties with multiple values
906 */
907 public XMLBuilderVisitor(Document doc, char listDelimiter)
908 {
909 document = doc;
910 this.listDelimiter = listDelimiter;
911 }
912
913 /***
914 * Processes the node hierarchy and adds new nodes to the document.
915 *
916 * @param rootNode the root node
917 */
918 public void processDocument(Node rootNode)
919 {
920 rootNode.visit(this, null);
921 }
922
923 /***
924 * Inserts a new node. This implementation ensures that the correct
925 * XML element is created and inserted between the given siblings.
926 *
927 * @param newNode the node to insert
928 * @param parent the parent node
929 * @param sibling1 the first sibling
930 * @param sibling2 the second sibling
931 * @return the new node
932 */
933 protected Object insert(Node newNode, Node parent, Node sibling1, Node sibling2)
934 {
935 if (newNode.isAttribute())
936 {
937 updateAttribute(parent, getElement(parent), newNode.getName(), listDelimiter);
938 return null;
939 }
940
941 else
942 {
943 Element elem = document.createElement(newNode.getName());
944 if (newNode.getValue() != null)
945 {
946 elem.appendChild(document.createTextNode(
947 PropertyConverter.escapeDelimiters(newNode.getValue().toString(), listDelimiter)));
948 }
949 if (sibling2 == null)
950 {
951 getElement(parent).appendChild(elem);
952 }
953 else if (sibling1 != null)
954 {
955 getElement(parent).insertBefore(elem, getElement(sibling1).getNextSibling());
956 }
957 else
958 {
959 getElement(parent).insertBefore(elem, getElement(parent).getFirstChild());
960 }
961 return elem;
962 }
963 }
964
965 /***
966 * Helper method for updating the value of the specified node's
967 * attribute with the given name.
968 *
969 * @param node the affected node
970 * @param elem the element that is associated with this node
971 * @param name the name of the affected attribute
972 * @param listDelimiter the delimiter vor attributes with multiple values
973 */
974 private static void updateAttribute(Node node, Element elem, String name, char listDelimiter)
975 {
976 if (node != null && elem != null)
977 {
978 List attrs = node.getAttributes(name);
979 StringBuffer buf = new StringBuffer();
980 for (Iterator it = attrs.iterator(); it.hasNext();)
981 {
982 Node attr = (Node) it.next();
983 if (attr.getValue() != null)
984 {
985 if (buf.length() > 0)
986 {
987 buf.append(listDelimiter);
988 }
989 buf.append(PropertyConverter.escapeDelimiters(attr
990 .getValue().toString(), getDefaultListDelimiter()));
991 }
992 attr.setReference(elem);
993 }
994
995 if (buf.length() < 1)
996 {
997 elem.removeAttribute(name);
998 }
999 else
1000 {
1001 elem.setAttribute(name, buf.toString());
1002 }
1003 }
1004 }
1005
1006 /***
1007 * Updates the value of the specified attribute of the given node.
1008 * Because there can be multiple child nodes representing this attribute
1009 * the new value is determined by iterating over all those child nodes.
1010 *
1011 * @param node the affected node
1012 * @param name the name of the attribute
1013 * @param listDelimiter the delimiter vor attributes with multiple values
1014 */
1015 static void updateAttribute(Node node, String name, char listDelimiter)
1016 {
1017 if (node != null)
1018 {
1019 updateAttribute(node, (Element) node.getReference(), name, listDelimiter);
1020 }
1021 }
1022
1023 /***
1024 * Helper method for accessing the element of the specified node.
1025 *
1026 * @param node the node
1027 * @return the element of this node
1028 */
1029 private Element getElement(Node node)
1030 {
1031
1032 return (node.getName() != null) ? (Element) node.getReference() : document.getDocumentElement();
1033 }
1034 }
1035
1036 /***
1037 * A special implementation of the <code>FileConfiguration</code> interface that is
1038 * used internally to implement the <code>FileConfiguration</code> methods
1039 * for <code>XMLConfiguration</code>, too.
1040 */
1041 private class XMLFileConfigurationDelegate extends FileConfigurationDelegate
1042 {
1043 public void load(InputStream in) throws ConfigurationException
1044 {
1045 XMLConfiguration.this.load(in);
1046 }
1047 }
1048 }