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.IOException;
22 import java.io.InputStream;
23 import java.io.Reader;
24 import java.io.Writer;
25 import java.net.URL;
26 import java.net.URLConnection;
27 import java.util.ArrayList;
28 import java.util.Collection;
29 import java.util.HashMap;
30 import java.util.Iterator;
31 import java.util.List;
32 import java.util.Map;
33
34 import javax.xml.parsers.DocumentBuilder;
35 import javax.xml.parsers.DocumentBuilderFactory;
36 import javax.xml.parsers.ParserConfigurationException;
37 import javax.xml.transform.OutputKeys;
38 import javax.xml.transform.Result;
39 import javax.xml.transform.Source;
40 import javax.xml.transform.Transformer;
41 import javax.xml.transform.TransformerException;
42 import javax.xml.transform.TransformerFactory;
43 import javax.xml.transform.TransformerFactoryConfigurationError;
44 import javax.xml.transform.dom.DOMSource;
45 import javax.xml.transform.stream.StreamResult;
46
47 import org.apache.commons.configuration.tree.ConfigurationNode;
48 import org.w3c.dom.Attr;
49 import org.w3c.dom.CDATASection;
50 import org.w3c.dom.DOMException;
51 import org.w3c.dom.Document;
52 import org.w3c.dom.Element;
53 import org.w3c.dom.NamedNodeMap;
54 import org.w3c.dom.NodeList;
55 import org.w3c.dom.Text;
56 import org.xml.sax.EntityResolver;
57 import org.xml.sax.InputSource;
58 import org.xml.sax.SAXException;
59 import org.xml.sax.SAXParseException;
60 import org.xml.sax.helpers.DefaultHandler;
61
62 /***
63 * <p>A specialized hierarchical configuration class that is able to parse XML
64 * documents.</p>
65 *
66 * <p>The parsed document will be stored keeping its structure. The class also
67 * tries to preserve as much information from the loaded XML document as
68 * possible, including comments and processing instructions. These will be
69 * contained in documents created by the <code>save()</code> methods, too.</p>
70 *
71 * <p>Like other file based configuration classes this class maintains the name
72 * and path to the loaded configuration file. These properties can be altered
73 * using several setter methods, but they are not modified by <code>save()</code>
74 * and <code>load()</code> methods. If XML documents contain relative paths to
75 * other documents (e.g. to a DTD), these references are resolved based on the
76 * path set for this configuration.</p>
77 *
78 * <p>By inheriting from <code>{@link AbstractConfiguration}</code> this class
79 * provides some extended functionality, e.g. interpolation of property values.
80 * Like in <code>{@link PropertiesConfiguration}</code> property values can
81 * contain delimiter characters (the comma ',' per default) and are then split
82 * into multiple values. This works for XML attributes and text content of
83 * elements as well. The delimiter can be escaped by a backslash. As an example
84 * consider the following XML fragment:</p>
85 *
86 * <p>
87 * <pre>
88 * <config>
89 * <array>10,20,30,40</array>
90 * <scalar>3\,1415</scalar>
91 * <cite text="To be or not to be\, this is the question!"/>
92 * </config>
93 * </pre>
94 * </p>
95 * <p>Here the content of the <code>array</code> element will be split at
96 * the commas, so the <code>array</code> key will be assigned 4 values. In the
97 * <code>scalar</code> property and the <code>text</code> attribute of the
98 * <code>cite</code> element the comma is escaped, so that no splitting is
99 * performed.</p>
100 *
101 * <p>The configuration API allows setting multiple values for a single attribute,
102 * e.g. something like the following is legal (assuming that the default
103 * expression engine is used):
104 * <pre>
105 * XMLConfiguration config = new XMLConfiguration();
106 * config.addProperty("test.dir[@name]", "C://Temp//");
107 * config.addProperty("test.dir[@name]", "D://Data//");
108 * </pre></p>
109 *
110 * <p>Because in XML such a constellation is not directly supported (an attribute
111 * can appear only once for a single element), the values are concatenated to a
112 * single value. If delimiter parsing is enabled (refer to the
113 * <code>{@link #setDelimiterParsingDisabled(boolean)}</code> method), the
114 * current list delimiter character will be used as separator. Otherwise the
115 * pipe symbol ("|") will be used for this purpose. No matter which character is
116 * used as delimiter, it can always be escaped with a backslash. A backslash
117 * itself can also be escaped with another backslash. Consider the following
118 * example fragment from a configuration file:
119 * <pre>
120 * <directories names="C:\Temp//|D:\Data\"/>
121 * </pre>
122 * Here the backslash after Temp is escaped. This is necessary because it
123 * would escape the list delimiter (the pipe symbol assuming that list delimiter
124 * parsing is disabled) otherwise. So this attribute would have two values.</p>
125 *
126 * <p>Note: You should ensure that the <em>delimiter parsing disabled</em>
127 * property is always consistent when you load and save a configuration file.
128 * Otherwise the values of properties can become corrupted.</p>
129 *
130 * <p><code>XMLConfiguration</code> implements the <code>{@link FileConfiguration}</code>
131 * interface and thus provides full support for loading XML documents from
132 * different sources like files, URLs, or streams. A full description of these
133 * features can be found in the documentation of
134 * <code>{@link AbstractFileConfiguration}</code>.</p>
135 *
136 * <p><em>Note:</em>Configuration objects of this type can be read concurrently
137 * by multiple threads. However if one of these threads modifies the object,
138 * synchronization has to be performed manually.</p>
139 *
140 * @since commons-configuration 1.0
141 *
142 * @author Jörg Schaible
143 * @author Oliver Heger
144 * @version $Revision: 589380 $, $Date: 2007-10-28 17:37:35 +0100 (So, 28 Okt 2007) $
145 */
146 public class XMLConfiguration extends AbstractHierarchicalFileConfiguration
147 implements EntityResolver
148 {
149 /***
150 * The serial version UID.
151 */
152 private static final long serialVersionUID = 2453781111653383552L;
153
154 /*** Constant for the default root element name. */
155 private static final String DEFAULT_ROOT_NAME = "configuration";
156
157 /*** Constant for the delimiter for multiple attribute values.*/
158 private static final char ATTR_VALUE_DELIMITER = '|';
159
160 /*** The document from this configuration's data source. */
161 private Document document;
162
163 /*** Stores a map with the registered public IDs.*/
164 private Map registeredEntities = new HashMap();
165
166 /*** Stores the name of the root element. */
167 private String rootElementName;
168
169 /*** Stores the public ID from the DOCTYPE.*/
170 private String publicID;
171
172 /*** Stores the system ID from the DOCTYPE.*/
173 private String systemID;
174
175 /*** Stores the document builder that should be used for loading.*/
176 private DocumentBuilder documentBuilder;
177
178 /*** Stores a flag whether DTD validation should be performed.*/
179 private boolean validating;
180
181 /***
182 * Creates a new instance of <code>XMLConfiguration</code>.
183 */
184 public XMLConfiguration()
185 {
186 super();
187 }
188
189 /***
190 * Creates a new instance of <code>XMLConfiguration</code> and copies the
191 * content of the passed in configuration into this object. Note that only
192 * the data of the passed in configuration will be copied. If, for instance,
193 * the other configuration is a <code>XMLConfiguration</code>, too,
194 * things like comments or processing instructions will be lost.
195 *
196 * @param c the configuration to copy
197 * @since 1.4
198 */
199 public XMLConfiguration(HierarchicalConfiguration c)
200 {
201 super(c);
202 clearReferences(getRootNode());
203 }
204
205 /***
206 * Creates a new instance of <code>XMLConfiguration</code>. The
207 * configuration is loaded from the specified file
208 *
209 * @param fileName the name of the file to load
210 * @throws ConfigurationException if the file cannot be loaded
211 */
212 public XMLConfiguration(String fileName) throws ConfigurationException
213 {
214 super(fileName);
215 }
216
217 /***
218 * Creates a new instance of <code>XMLConfiguration</code>.
219 * The configuration is loaded from the specified file.
220 *
221 * @param file the file
222 * @throws ConfigurationException if an error occurs while loading the file
223 */
224 public XMLConfiguration(File file) throws ConfigurationException
225 {
226 super(file);
227 }
228
229 /***
230 * Creates a new instance of <code>XMLConfiguration</code>.
231 * The configuration is loaded from the specified URL.
232 *
233 * @param url the URL
234 * @throws ConfigurationException if loading causes an error
235 */
236 public XMLConfiguration(URL url) throws ConfigurationException
237 {
238 super(url);
239 }
240
241 /***
242 * Returns the name of the root element. If this configuration was loaded
243 * from a XML document, the name of this document's root element is
244 * returned. Otherwise it is possible to set a name for the root element
245 * that will be used when this configuration is stored.
246 *
247 * @return the name of the root element
248 */
249 public String getRootElementName()
250 {
251 if (getDocument() == null)
252 {
253 return (rootElementName == null) ? DEFAULT_ROOT_NAME : rootElementName;
254 }
255 else
256 {
257 return getDocument().getDocumentElement().getNodeName();
258 }
259 }
260
261 /***
262 * Sets the name of the root element. This name is used when this
263 * configuration object is stored in an XML file. Note that setting the name
264 * of the root element works only if this configuration has been newly
265 * created. If the configuration was loaded from an XML file, the name
266 * cannot be changed and an <code>UnsupportedOperationException</code>
267 * exception is thrown. Whether this configuration has been loaded from an
268 * XML document or not can be found out using the <code>getDocument()</code>
269 * method.
270 *
271 * @param name the name of the root element
272 */
273 public void setRootElementName(String name)
274 {
275 if (getDocument() != null)
276 {
277 throw new UnsupportedOperationException("The name of the root element "
278 + "cannot be changed when loaded from an XML document!");
279 }
280 rootElementName = name;
281 }
282
283 /***
284 * Returns the <code>DocumentBuilder</code> object that is used for
285 * loading documents. If no specific builder has been set, this method
286 * returns <b>null</b>.
287 *
288 * @return the <code>DocumentBuilder</code> for loading new documents
289 * @since 1.2
290 */
291 public DocumentBuilder getDocumentBuilder()
292 {
293 return documentBuilder;
294 }
295
296 /***
297 * Sets the <code>DocumentBuilder</code> object to be used for loading
298 * documents. This method makes it possible to specify the exact document
299 * builder. So an application can create a builder, configure it for its
300 * special needs, and then pass it to this method.
301 *
302 * @param documentBuilder the document builder to be used; if undefined, a
303 * default builder will be used
304 * @since 1.2
305 */
306 public void setDocumentBuilder(DocumentBuilder documentBuilder)
307 {
308 this.documentBuilder = documentBuilder;
309 }
310
311 /***
312 * Returns the public ID of the DOCTYPE declaration from the loaded XML
313 * document. This is <b>null</b> if no document has been loaded yet or if
314 * the document does not contain a DOCTYPE declaration with a public ID.
315 *
316 * @return the public ID
317 * @since 1.3
318 */
319 public String getPublicID()
320 {
321 return publicID;
322 }
323
324 /***
325 * Sets the public ID of the DOCTYPE declaration. When this configuration is
326 * saved, a DOCTYPE declaration will be constructed that contains this
327 * public ID.
328 *
329 * @param publicID the public ID
330 * @since 1.3
331 */
332 public void setPublicID(String publicID)
333 {
334 this.publicID = publicID;
335 }
336
337 /***
338 * Returns the system ID of the DOCTYPE declaration from the loaded XML
339 * document. This is <b>null</b> if no document has been loaded yet or if
340 * the document does not contain a DOCTYPE declaration with a system ID.
341 *
342 * @return the system ID
343 * @since 1.3
344 */
345 public String getSystemID()
346 {
347 return systemID;
348 }
349
350 /***
351 * Sets the system ID of the DOCTYPE declaration. When this configuration is
352 * saved, a DOCTYPE declaration will be constructed that contains this
353 * system ID.
354 *
355 * @param systemID the system ID
356 * @since 1.3
357 */
358 public void setSystemID(String systemID)
359 {
360 this.systemID = systemID;
361 }
362
363 /***
364 * Returns the value of the validating flag.
365 *
366 * @return the validating flag
367 * @since 1.2
368 */
369 public boolean isValidating()
370 {
371 return validating;
372 }
373
374 /***
375 * Sets the value of the validating flag. This flag determines whether
376 * DTD validation should be performed when loading XML documents. This
377 * flag is evaluated only if no custom <code>DocumentBuilder</code> was set.
378 *
379 * @param validating the validating flag
380 * @since 1.2
381 */
382 public void setValidating(boolean validating)
383 {
384 this.validating = validating;
385 }
386
387 /***
388 * Returns the XML document this configuration was loaded from. The return
389 * value is <b>null</b> if this configuration was not loaded from a XML
390 * document.
391 *
392 * @return the XML document this configuration was loaded from
393 */
394 public Document getDocument()
395 {
396 return document;
397 }
398
399 /***
400 * Removes all properties from this configuration. If this configuration
401 * was loaded from a file, the associated DOM document is also cleared.
402 */
403 public void clear()
404 {
405 super.clear();
406 document = null;
407 }
408
409 /***
410 * Initializes this configuration from an XML document.
411 *
412 * @param document the document to be parsed
413 * @param elemRefs a flag whether references to the XML elements should be set
414 */
415 public void initProperties(Document document, boolean elemRefs)
416 {
417 if (document.getDoctype() != null)
418 {
419 setPublicID(document.getDoctype().getPublicId());
420 setSystemID(document.getDoctype().getSystemId());
421 }
422
423 constructHierarchy(getRoot(), document.getDocumentElement(), elemRefs);
424 if (elemRefs)
425 {
426 getRoot().setReference(document.getDocumentElement());
427 }
428 }
429
430 /***
431 * Helper method for building the internal storage hierarchy. The XML
432 * elements are transformed into node objects.
433 *
434 * @param node the actual node
435 * @param element the actual XML element
436 * @param elemRefs a flag whether references to the XML elements should be set
437 */
438 private void constructHierarchy(Node node, Element element, boolean elemRefs)
439 {
440 processAttributes(node, element, elemRefs);
441 StringBuffer buffer = new StringBuffer();
442 NodeList list = element.getChildNodes();
443 for (int i = 0; i < list.getLength(); i++)
444 {
445 org.w3c.dom.Node w3cNode = list.item(i);
446 if (w3cNode instanceof Element)
447 {
448 Element child = (Element) w3cNode;
449 Node childNode = new XMLNode(child.getTagName(),
450 elemRefs ? child : null);
451 constructHierarchy(childNode, child, elemRefs);
452 node.addChild(childNode);
453 handleDelimiters(node, childNode);
454 }
455 else if (w3cNode instanceof Text)
456 {
457 Text data = (Text) w3cNode;
458 buffer.append(data.getData());
459 }
460 }
461 String text = buffer.toString().trim();
462 if (text.length() > 0 || !node.hasChildren())
463 {
464 node.setValue(text);
465 }
466 }
467
468 /***
469 * Helper method for constructing node objects for the attributes of the
470 * given XML element.
471 *
472 * @param node the actual node
473 * @param element the actual XML element
474 * @param elemRefs a flag whether references to the XML elements should be set
475 */
476 private void processAttributes(Node node, Element element, boolean elemRefs)
477 {
478 NamedNodeMap attributes = element.getAttributes();
479 for (int i = 0; i < attributes.getLength(); ++i)
480 {
481 org.w3c.dom.Node w3cNode = attributes.item(i);
482 if (w3cNode instanceof Attr)
483 {
484 Attr attr = (Attr) w3cNode;
485 Iterator it = PropertyConverter.split(
486 attr.getValue(),
487 isDelimiterParsingDisabled() ? ATTR_VALUE_DELIMITER
488 : getListDelimiter()).iterator();
489 while (it.hasNext())
490 {
491 Node child = new XMLNode(attr.getName(),
492 elemRefs ? element : null);
493 child.setValue(it.next());
494 node.addAttribute(child);
495 }
496 }
497 }
498 }
499
500 /***
501 * Deals with elements whose value is a list. In this case multiple child
502 * elements must be added.
503 *
504 * @param parent the parent element
505 * @param child the child element
506 */
507 private void handleDelimiters(Node parent, Node child)
508 {
509 if (child.getValue() != null)
510 {
511 List values;
512 if (isDelimiterParsingDisabled())
513 {
514 values = new ArrayList();
515 values.add(child.getValue().toString());
516 }
517 else
518 {
519 values = PropertyConverter.split(child.getValue().toString(),
520 getListDelimiter());
521 }
522
523 if (values.size() > 1)
524 {
525 Iterator it = values.iterator();
526
527 Node c = createNode(child.getName());
528 c.setValue(it.next());
529
530 for (Iterator itAttrs = child.getAttributes().iterator(); itAttrs
531 .hasNext();)
532 {
533 Node ndAttr = (Node) itAttrs.next();
534 ndAttr.setReference(null);
535 c.addAttribute(ndAttr);
536 }
537 parent.remove(child);
538 parent.addChild(c);
539
540
541 while (it.hasNext())
542 {
543 c = new XMLNode(child.getName(), null);
544 c.setValue(it.next());
545 parent.addChild(c);
546 }
547 }
548 else if (values.size() == 1)
549 {
550
551
552 child.setValue(values.get(0));
553 }
554 }
555 }
556
557 /***
558 * Creates the <code>DocumentBuilder</code> to be used for loading files.
559 * This implementation checks whether a specific
560 * <code>DocumentBuilder</code> has been set. If this is the case, this
561 * one is used. Otherwise a default builder is created. Depending on the
562 * value of the validating flag this builder will be a validating or a non
563 * validating <code>DocumentBuilder</code>.
564 *
565 * @return the <code>DocumentBuilder</code> for loading configuration
566 * files
567 * @throws ParserConfigurationException if an error occurs
568 * @since 1.2
569 */
570 protected DocumentBuilder createDocumentBuilder()
571 throws ParserConfigurationException
572 {
573 if (getDocumentBuilder() != null)
574 {
575 return getDocumentBuilder();
576 }
577 else
578 {
579 DocumentBuilderFactory factory = DocumentBuilderFactory
580 .newInstance();
581 factory.setValidating(isValidating());
582 DocumentBuilder result = factory.newDocumentBuilder();
583 result.setEntityResolver(this);
584
585 if (isValidating())
586 {
587
588 result.setErrorHandler(new DefaultHandler()
589 {
590 public void error(SAXParseException ex) throws SAXException
591 {
592 throw ex;
593 }
594 });
595 }
596 return result;
597 }
598 }
599
600 /***
601 * Creates a DOM document from the internal tree of configuration nodes.
602 *
603 * @return the new document
604 * @throws ConfigurationException if an error occurs
605 */
606 protected Document createDocument() throws ConfigurationException
607 {
608 try
609 {
610 if (document == null)
611 {
612 DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
613 Document newDocument = builder.newDocument();
614 Element rootElem = newDocument.createElement(getRootElementName());
615 newDocument.appendChild(rootElem);
616 document = newDocument;
617 }
618
619 XMLBuilderVisitor builder = new XMLBuilderVisitor(document,
620 isDelimiterParsingDisabled() ? (char) 0 : getListDelimiter());
621 builder.processDocument(getRoot());
622 return document;
623 }
624 catch (DOMException domEx)
625 {
626 throw new ConfigurationException(domEx);
627 }
628 catch (ParserConfigurationException pex)
629 {
630 throw new ConfigurationException(pex);
631 }
632 }
633
634 /***
635 * Creates a new node object. This implementation returns an instance of the
636 * <code>XMLNode</code> class.
637 *
638 * @param name the node's name
639 * @return the new node
640 */
641 protected Node createNode(String name)
642 {
643 return new XMLNode(name, null);
644 }
645
646 /***
647 * Loads the configuration from the given input stream.
648 *
649 * @param in the input stream
650 * @throws ConfigurationException if an error occurs
651 */
652 public void load(InputStream in) throws ConfigurationException
653 {
654 load(new InputSource(in));
655 }
656
657 /***
658 * Load the configuration from the given reader.
659 * Note that the <code>clear()</code> method is not called, so
660 * the properties contained in the loaded file will be added to the
661 * actual set of properties.
662 *
663 * @param in An InputStream.
664 *
665 * @throws ConfigurationException if an error occurs
666 */
667 public void load(Reader in) throws ConfigurationException
668 {
669 load(new InputSource(in));
670 }
671
672 /***
673 * Loads a configuration file from the specified input source.
674 * @param source the input source
675 * @throws ConfigurationException if an error occurs
676 */
677 private void load(InputSource source) throws ConfigurationException
678 {
679 try
680 {
681 URL sourceURL = getDelegate().getURL();
682 if (sourceURL != null)
683 {
684 source.setSystemId(sourceURL.toString());
685 }
686
687 DocumentBuilder builder = createDocumentBuilder();
688 Document newDocument = builder.parse(source);
689 Document oldDocument = document;
690 document = null;
691 initProperties(newDocument, oldDocument == null);
692 document = (oldDocument == null) ? newDocument : oldDocument;
693 }
694 catch (Exception e)
695 {
696 throw new ConfigurationException(e.getMessage(), e);
697 }
698 }
699
700 /***
701 * Saves the configuration to the specified writer.
702 *
703 * @param writer the writer used to save the configuration
704 * @throws ConfigurationException if an error occurs
705 */
706 public void save(Writer writer) throws ConfigurationException
707 {
708 try
709 {
710 Transformer transformer = createTransformer();
711 Source source = new DOMSource(createDocument());
712 Result result = new StreamResult(writer);
713 transformer.transform(source, result);
714 }
715 catch (TransformerException e)
716 {
717 throw new ConfigurationException(e.getMessage(), e);
718 }
719 catch (TransformerFactoryConfigurationError err)
720 {
721 throw new ConfigurationException(err.getMessage(), err);
722 }
723 }
724
725 /***
726 * Creates and initializes the transformer used for save operations. This
727 * base implementation initializes all of the default settings like
728 * indention mode and the DOCTYPE. Derived classes may overload this method
729 * if they have specific needs.
730 *
731 * @return the transformer to use for a save operation
732 * @throws TransformerException if an error occurs
733 * @since 1.3
734 */
735 protected Transformer createTransformer() throws TransformerException
736 {
737 Transformer transformer = TransformerFactory.newInstance()
738 .newTransformer();
739
740 transformer.setOutputProperty(OutputKeys.INDENT, "yes");
741 if (getEncoding() != null)
742 {
743 transformer.setOutputProperty(OutputKeys.ENCODING, getEncoding());
744 }
745 if (getPublicID() != null)
746 {
747 transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC,
748 getPublicID());
749 }
750 if (getSystemID() != null)
751 {
752 transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM,
753 getSystemID());
754 }
755
756 return transformer;
757 }
758
759 /***
760 * Creates a copy of this object. The new configuration object will contain
761 * the same properties as the original, but it will lose any connection to a
762 * source document (if one exists). This is to avoid race conditions if both
763 * the original and the copy are modified and then saved.
764 *
765 * @return the copy
766 */
767 public Object clone()
768 {
769 XMLConfiguration copy = (XMLConfiguration) super.clone();
770
771
772 copy.document = null;
773 copy.setDelegate(copy.createDelegate());
774
775 clearReferences(copy.getRootNode());
776
777 return copy;
778 }
779
780 /***
781 * Creates the file configuration delegate for this object. This implementation
782 * will return an instance of a class derived from <code>FileConfigurationDelegate</code>
783 * that deals with some specialities of <code>XMLConfiguration</code>.
784 * @return the delegate for this object
785 */
786 protected FileConfigurationDelegate createDelegate()
787 {
788 return new XMLFileConfigurationDelegate();
789 }
790
791 /***
792 * Adds a collection of nodes directly to this configuration. This
793 * implementation ensures that the nodes to be added are of the correct node
794 * type (they have to be converted to <code>XMLNode</code> if necessary).
795 *
796 * @param key the key where the nodes are to be added
797 * @param nodes the collection with the new nodes
798 * @since 1.5
799 */
800 public void addNodes(String key, Collection nodes)
801 {
802 Collection xmlNodes;
803
804 if (nodes != null && !nodes.isEmpty())
805 {
806 xmlNodes = new ArrayList(nodes.size());
807 for (Iterator it = nodes.iterator(); it.hasNext();)
808 {
809 xmlNodes.add(convertToXMLNode((ConfigurationNode) it.next()));
810 }
811 }
812 else
813 {
814 xmlNodes = nodes;
815 }
816
817 super.addNodes(key, xmlNodes);
818 }
819
820 /***
821 * Converts the specified node into a <code>XMLNode</code> if necessary.
822 * This is required for nodes that are directly added, e.g. by
823 * <code>addNodes()</code>. If the passed in node is already an instance
824 * of <code>XMLNode</code>, it is directly returned, and conversion
825 * stops. Otherwise a new <code>XMLNode</code> is created, and the
826 * children are also converted.
827 *
828 * @param node the node to be converted
829 * @return the converted node
830 */
831 private XMLNode convertToXMLNode(ConfigurationNode node)
832 {
833 if (node instanceof XMLNode)
834 {
835 return (XMLNode) node;
836 }
837
838 XMLNode nd = (XMLNode) createNode(node.getName());
839 nd.setValue(node.getValue());
840 for (Iterator it = node.getChildren().iterator(); it.hasNext();)
841 {
842 nd.addChild(convertToXMLNode((ConfigurationNode) it.next()));
843 }
844 for (Iterator it = node.getAttributes().iterator(); it.hasNext();)
845 {
846 nd.addAttribute(convertToXMLNode((ConfigurationNode) it.next()));
847 }
848 return nd;
849 }
850
851 /***
852 * <p>
853 * Registers the specified DTD URL for the specified public identifier.
854 * </p>
855 * <p>
856 * <code>XMLConfiguration</code> contains an internal
857 * <code>EntityResolver</code> implementation. This maps
858 * <code>PUBLICID</code>'s to URLs (from which the resource will be
859 * loaded). A common use case for this method is to register local URLs
860 * (possibly computed at runtime by a class loader) for DTDs. This allows
861 * the performance advantage of using a local version without having to
862 * ensure every <code>SYSTEM</code> URI on every processed XML document is
863 * local. This implementation provides only basic functionality. If more
864 * sophisticated features are required, using
865 * {@link #setDocumentBuilder(DocumentBuilder)} to set a custom
866 * <code>DocumentBuilder</code> (which also can be initialized with a
867 * custom <code>EntityResolver</code>) is recommended.
868 * </p>
869 * <p>
870 * <strong>Note:</strong> This method will have no effect when a custom
871 * <code>DocumentBuilder</code> has been set. (Setting a custom
872 * <code>DocumentBuilder</code> overrides the internal implementation.)
873 * </p>
874 * <p>
875 * <strong>Note:</strong> This method must be called before the
876 * configuration is loaded. So the default constructor of
877 * <code>XMLConfiguration</code> should be used, the location of the
878 * configuration file set, <code>registerEntityId()</code> called, and
879 * finally the <code>load()</code> method can be invoked.
880 * </p>
881 *
882 * @param publicId Public identifier of the DTD to be resolved
883 * @param entityURL The URL to use for reading this DTD
884 * @throws IllegalArgumentException if the public ID is undefined
885 * @since 1.5
886 */
887 public void registerEntityId(String publicId, URL entityURL)
888 {
889 if (publicId == null)
890 {
891 throw new IllegalArgumentException("Public ID must not be null!");
892 }
893 getRegisteredEntities().put(publicId, entityURL);
894 }
895
896 /***
897 * Resolves the requested external entity. This is the default
898 * implementation of the <code>EntityResolver</code> interface. It checks
899 * the passed in public ID against the registered entity IDs and uses a
900 * local URL if possible.
901 *
902 * @param publicId the public identifier of the entity being referenced
903 * @param systemId the system identifier of the entity being referenced
904 * @return an input source for the specified entity
905 * @throws SAXException if a parsing exception occurs
906 * @since 1.5
907 */
908 public InputSource resolveEntity(String publicId, String systemId)
909 throws SAXException
910 {
911
912 URL entityURL = null;
913 if (publicId != null)
914 {
915 entityURL = (URL) getRegisteredEntities().get(publicId);
916 }
917
918 if (entityURL != null)
919 {
920
921
922 try
923 {
924 URLConnection connection = entityURL.openConnection();
925 connection.setUseCaches(false);
926 InputStream stream = connection.getInputStream();
927 InputSource source = new InputSource(stream);
928 source.setSystemId(entityURL.toExternalForm());
929 return source;
930 }
931 catch (IOException e)
932 {
933 throw new SAXException(e);
934 }
935 }
936 else
937 {
938
939 return null;
940 }
941 }
942
943 /***
944 * Returns a map with the entity IDs that have been registered using the
945 * <code>registerEntityId()</code> method.
946 *
947 * @return a map with the registered entity IDs
948 */
949 Map getRegisteredEntities()
950 {
951 return registeredEntities;
952 }
953
954 /***
955 * A specialized <code>Node</code> class that is connected with an XML
956 * element. Changes on a node are also performed on the associated element.
957 */
958 class XMLNode extends Node
959 {
960 /***
961 * The serial version UID.
962 */
963 private static final long serialVersionUID = -4133988932174596562L;
964
965 /***
966 * Creates a new instance of <code>XMLNode</code> and initializes it
967 * with a name and the corresponding XML element.
968 *
969 * @param name the node's name
970 * @param elem the XML element
971 */
972 public XMLNode(String name, Element elem)
973 {
974 super(name);
975 setReference(elem);
976 }
977
978 /***
979 * Sets the value of this node. If this node is associated with an XML
980 * element, this element will be updated, too.
981 *
982 * @param value the node's new value
983 */
984 public void setValue(Object value)
985 {
986 super.setValue(value);
987
988 if (getReference() != null && document != null)
989 {
990 if (isAttribute())
991 {
992 updateAttribute();
993 }
994 else
995 {
996 updateElement(value);
997 }
998 }
999 }
1000
1001 /***
1002 * Updates the associated XML elements when a node is removed.
1003 */
1004 protected void removeReference()
1005 {
1006 if (getReference() != null)
1007 {
1008 Element element = (Element) getReference();
1009 if (isAttribute())
1010 {
1011 updateAttribute();
1012 }
1013 else
1014 {
1015 org.w3c.dom.Node parentElem = element.getParentNode();
1016 if (parentElem != null)
1017 {
1018 parentElem.removeChild(element);
1019 }
1020 }
1021 }
1022 }
1023
1024 /***
1025 * Updates the node's value if it represents an element node.
1026 *
1027 * @param value the new value
1028 */
1029 private void updateElement(Object value)
1030 {
1031 Text txtNode = findTextNodeForUpdate();
1032 if (value == null)
1033 {
1034
1035 if (txtNode != null)
1036 {
1037 ((Element) getReference()).removeChild(txtNode);
1038 }
1039 }
1040 else
1041 {
1042 if (txtNode == null)
1043 {
1044 txtNode = document
1045 .createTextNode(PropertyConverter.escapeDelimiters(
1046 value.toString(), getListDelimiter()));
1047 if (((Element) getReference()).getFirstChild() != null)
1048 {
1049 ((Element) getReference()).insertBefore(txtNode,
1050 ((Element) getReference()).getFirstChild());
1051 }
1052 else
1053 {
1054 ((Element) getReference()).appendChild(txtNode);
1055 }
1056 }
1057 else
1058 {
1059 txtNode.setNodeValue(PropertyConverter.escapeDelimiters(
1060 value.toString(), getListDelimiter()));
1061 }
1062 }
1063 }
1064
1065 /***
1066 * Updates the node's value if it represents an attribute.
1067 *
1068 */
1069 private void updateAttribute()
1070 {
1071 XMLBuilderVisitor.updateAttribute(getParent(), getName(), getListDelimiter());
1072 }
1073
1074 /***
1075 * Returns the only text node of this element for update. This method is
1076 * called when the element's text changes. Then all text nodes except
1077 * for the first are removed. A reference to the first is returned or
1078 * <b>null </b> if there is no text node at all.
1079 *
1080 * @return the first and only text node
1081 */
1082 private Text findTextNodeForUpdate()
1083 {
1084 Text result = null;
1085 Element elem = (Element) getReference();
1086
1087 NodeList children = elem.getChildNodes();
1088 Collection textNodes = new ArrayList();
1089 for (int i = 0; i < children.getLength(); i++)
1090 {
1091 org.w3c.dom.Node nd = children.item(i);
1092 if (nd instanceof Text)
1093 {
1094 if (result == null)
1095 {
1096 result = (Text) nd;
1097 }
1098 else
1099 {
1100 textNodes.add(nd);
1101 }
1102 }
1103 }
1104
1105
1106 if (result instanceof CDATASection)
1107 {
1108 textNodes.add(result);
1109 result = null;
1110 }
1111
1112
1113 for (Iterator it = textNodes.iterator(); it.hasNext();)
1114 {
1115 elem.removeChild((org.w3c.dom.Node) it.next());
1116 }
1117 return result;
1118 }
1119 }
1120
1121 /***
1122 * A concrete <code>BuilderVisitor</code> that can construct XML
1123 * documents.
1124 */
1125 static class XMLBuilderVisitor extends BuilderVisitor
1126 {
1127 /*** Stores the document to be constructed. */
1128 private Document document;
1129
1130 /*** Stores the list delimiter.*/
1131 private char listDelimiter = AbstractConfiguration.
1132 getDefaultListDelimiter();
1133
1134 /***
1135 * Creates a new instance of <code>XMLBuilderVisitor</code>
1136 *
1137 * @param doc the document to be created
1138 * @param listDelimiter the delimiter for attribute properties with multiple values
1139 */
1140 public XMLBuilderVisitor(Document doc, char listDelimiter)
1141 {
1142 document = doc;
1143 this.listDelimiter = listDelimiter;
1144 }
1145
1146 /***
1147 * Processes the node hierarchy and adds new nodes to the document.
1148 *
1149 * @param rootNode the root node
1150 */
1151 public void processDocument(Node rootNode)
1152 {
1153 rootNode.visit(this, null);
1154 }
1155
1156 /***
1157 * Inserts a new node. This implementation ensures that the correct
1158 * XML element is created and inserted between the given siblings.
1159 *
1160 * @param newNode the node to insert
1161 * @param parent the parent node
1162 * @param sibling1 the first sibling
1163 * @param sibling2 the second sibling
1164 * @return the new node
1165 */
1166 protected Object insert(Node newNode, Node parent, Node sibling1, Node sibling2)
1167 {
1168 if (newNode.isAttribute())
1169 {
1170 updateAttribute(parent, getElement(parent), newNode.getName(), listDelimiter);
1171 return null;
1172 }
1173
1174 else
1175 {
1176 Element elem = document.createElement(newNode.getName());
1177 if (newNode.getValue() != null)
1178 {
1179 String txt = newNode.getValue().toString();
1180 if (listDelimiter != 0)
1181 {
1182 txt = PropertyConverter.escapeDelimiters(txt, listDelimiter);
1183 }
1184 elem.appendChild(document.createTextNode(txt));
1185 }
1186 if (sibling2 == null)
1187 {
1188 getElement(parent).appendChild(elem);
1189 }
1190 else if (sibling1 != null)
1191 {
1192 getElement(parent).insertBefore(elem, getElement(sibling1).getNextSibling());
1193 }
1194 else
1195 {
1196 getElement(parent).insertBefore(elem, getElement(parent).getFirstChild());
1197 }
1198 return elem;
1199 }
1200 }
1201
1202 /***
1203 * Helper method for updating the value of the specified node's
1204 * attribute with the given name.
1205 *
1206 * @param node the affected node
1207 * @param elem the element that is associated with this node
1208 * @param name the name of the affected attribute
1209 * @param listDelimiter the delimiter for attributes with multiple values
1210 */
1211 private static void updateAttribute(Node node, Element elem, String name, char listDelimiter)
1212 {
1213 if (node != null && elem != null)
1214 {
1215 List attrs = node.getAttributes(name);
1216 StringBuffer buf = new StringBuffer();
1217 char delimiter = (listDelimiter != 0) ? listDelimiter : ATTR_VALUE_DELIMITER;
1218 for (Iterator it = attrs.iterator(); it.hasNext();)
1219 {
1220 Node attr = (Node) it.next();
1221 if (attr.getValue() != null)
1222 {
1223 if (buf.length() > 0)
1224 {
1225 buf.append(delimiter);
1226 }
1227 buf.append(PropertyConverter.escapeDelimiters(attr
1228 .getValue().toString(), delimiter));
1229 }
1230 attr.setReference(elem);
1231 }
1232
1233 if (buf.length() < 1)
1234 {
1235 elem.removeAttribute(name);
1236 }
1237 else
1238 {
1239 elem.setAttribute(name, buf.toString());
1240 }
1241 }
1242 }
1243
1244 /***
1245 * Updates the value of the specified attribute of the given node.
1246 * Because there can be multiple child nodes representing this attribute
1247 * the new value is determined by iterating over all those child nodes.
1248 *
1249 * @param node the affected node
1250 * @param name the name of the attribute
1251 * @param listDelimiter the delimiter for attributes with multiple values
1252 */
1253 static void updateAttribute(Node node, String name, char listDelimiter)
1254 {
1255 if (node != null)
1256 {
1257 updateAttribute(node, (Element) node.getReference(), name, listDelimiter);
1258 }
1259 }
1260
1261 /***
1262 * Helper method for accessing the element of the specified node.
1263 *
1264 * @param node the node
1265 * @return the element of this node
1266 */
1267 private Element getElement(Node node)
1268 {
1269
1270 return (node.getName() != null) ? (Element) node.getReference() : document.getDocumentElement();
1271 }
1272 }
1273
1274 /***
1275 * A special implementation of the <code>FileConfiguration</code> interface that is
1276 * used internally to implement the <code>FileConfiguration</code> methods
1277 * for <code>XMLConfiguration</code>, too.
1278 */
1279 private class XMLFileConfigurationDelegate extends FileConfigurationDelegate
1280 {
1281 public void load(InputStream in) throws ConfigurationException
1282 {
1283 XMLConfiguration.this.load(in);
1284 }
1285 }
1286 }