001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017:
018: package org.apache.commons.configuration;
019:
020: import java.io.File;
021: import java.io.InputStream;
022: import java.io.Reader;
023: import java.io.Writer;
024: import java.net.URL;
025: import java.util.ArrayList;
026: import java.util.Collection;
027: import java.util.Iterator;
028: import java.util.List;
029:
030: import javax.xml.parsers.DocumentBuilder;
031: import javax.xml.parsers.DocumentBuilderFactory;
032: import javax.xml.parsers.ParserConfigurationException;
033: import javax.xml.transform.OutputKeys;
034: import javax.xml.transform.Result;
035: import javax.xml.transform.Source;
036: import javax.xml.transform.Transformer;
037: import javax.xml.transform.TransformerException;
038: import javax.xml.transform.TransformerFactory;
039: import javax.xml.transform.TransformerFactoryConfigurationError;
040: import javax.xml.transform.dom.DOMSource;
041: import javax.xml.transform.stream.StreamResult;
042:
043: import org.apache.commons.collections.iterators.SingletonIterator;
044: import org.w3c.dom.Attr;
045: import org.w3c.dom.CDATASection;
046: import org.w3c.dom.DOMException;
047: import org.w3c.dom.Document;
048: import org.w3c.dom.Element;
049: import org.w3c.dom.NamedNodeMap;
050: import org.w3c.dom.NodeList;
051: import org.w3c.dom.Text;
052: import org.xml.sax.InputSource;
053: import org.xml.sax.SAXException;
054: import org.xml.sax.SAXParseException;
055: import org.xml.sax.helpers.DefaultHandler;
056:
057: /**
058: * <p>A specialized hierarchical configuration class that is able to parse XML
059: * documents.</p>
060: *
061: * <p>The parsed document will be stored keeping its structure. The class also
062: * tries to preserve as much information from the loaded XML document as
063: * possible, including comments and processing instructions. These will be
064: * contained in documents created by the <code>save()</code> methods, too.</p>
065: *
066: * <p>Like other file based configuration classes this class maintains the name
067: * and path to the loaded configuration file. These properties can be altered
068: * using several setter methods, but they are not modified by <code>save()</code>
069: * and <code>load()</code> methods. If XML documents contain relative paths to
070: * other documents (e.g. to a DTD), these references are resolved based on the
071: * path set for this configuration.</p>
072: *
073: * <p>By inheriting from <code>{@link AbstractConfiguration}</code> this class
074: * provides some extended functionaly, e.g. interpolation of property values.
075: * Like in <code>{@link PropertiesConfiguration}</code> property values can
076: * contain delimiter characters (the comma ',' per default) and are then splitted
077: * into multiple values. This works for XML attributes and text content of
078: * elements as well. The delimiter can be escaped by a backslash. As an example
079: * consider the following XML fragment:</p>
080: *
081: * <p>
082: * <pre>
083: * <config>
084: * <array>10,20,30,40</array>
085: * <scalar>3\,1415</scalar>
086: * <cite text="To be or not to be\, this is the question!"/>
087: * </config>
088: * </pre>
089: * </p>
090: * <p>Here the content of the <code>array</code> element will be splitted at
091: * the commas, so the <code>array</code> key will be assigned 4 values. In the
092: * <code>scalar</code> property and the <code>text</code> attribute of the
093: * <code>cite</code> element the comma is escaped, so that no splitting is
094: * performed.</p>
095: *
096: * <p><code>XMLConfiguration</code> implements the <code>{@link FileConfiguration}</code>
097: * interface and thus provides full support for loading XML documents from
098: * different sources like files, URLs, or streams. A full description of these
099: * 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: 513498 $, $Date: 2007-03-01 22:15:07 +0100 (Do, 01 Mrz 2007) $
107: */
108: public class XMLConfiguration extends
109: AbstractHierarchicalFileConfiguration {
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: super ();
141: }
142:
143: /**
144: * Creates a new instance of <code>XMLConfiguration</code> and copies the
145: * content of the passed in configuration into this object. Note that only
146: * the data of the passed in configuration will be copied. If, for instance,
147: * the other configuration is a <code>XMLConfiguration</code>, too,
148: * things like comments or processing instructions will be lost.
149: *
150: * @param c the configuration to copy
151: * @since 1.4
152: */
153: public XMLConfiguration(HierarchicalConfiguration c) {
154: super (c);
155: clearReferences(getRootNode());
156: }
157:
158: /**
159: * Creates a new instance of <code>XMLConfiguration</code>. The
160: * configuration is loaded from the specified file
161: *
162: * @param fileName the name of the file to load
163: * @throws ConfigurationException if the file cannot be loaded
164: */
165: public XMLConfiguration(String fileName)
166: throws ConfigurationException {
167: super (fileName);
168: }
169:
170: /**
171: * Creates a new instance of <code>XMLConfiguration</code>.
172: * The configuration is loaded from the specified file.
173: *
174: * @param file the file
175: * @throws ConfigurationException if an error occurs while loading the file
176: */
177: public XMLConfiguration(File file) throws ConfigurationException {
178: super (file);
179: }
180:
181: /**
182: * Creates a new instance of <code>XMLConfiguration</code>.
183: * The configuration is loaded from the specified URL.
184: *
185: * @param url the URL
186: * @throws ConfigurationException if loading causes an error
187: */
188: public XMLConfiguration(URL url) throws ConfigurationException {
189: super (url);
190: }
191:
192: /**
193: * Returns the name of the root element. If this configuration was loaded
194: * from a XML document, the name of this document's root element is
195: * returned. Otherwise it is possible to set a name for the root element
196: * that will be used when this configuration is stored.
197: *
198: * @return the name of the root element
199: */
200: public String getRootElementName() {
201: if (getDocument() == null) {
202: return (rootElementName == null) ? DEFAULT_ROOT_NAME
203: : rootElementName;
204: } else {
205: return getDocument().getDocumentElement().getNodeName();
206: }
207: }
208:
209: /**
210: * Sets the name of the root element. This name is used when this
211: * configuration object is stored in an XML file. Note that setting the name
212: * of the root element works only if this configuration has been newly
213: * created. If the configuration was loaded from an XML file, the name
214: * cannot be changed and an <code>UnsupportedOperationException</code>
215: * exception is thrown. Whether this configuration has been loaded from an
216: * XML document or not can be found out using the <code>getDocument()</code>
217: * method.
218: *
219: * @param name the name of the root element
220: */
221: public void setRootElementName(String name) {
222: if (getDocument() != null) {
223: throw new UnsupportedOperationException(
224: "The name of the root element "
225: + "cannot be changed when loaded from an XML document!");
226: }
227: rootElementName = name;
228: }
229:
230: /**
231: * Returns the <code>DocumentBuilder</code> object that is used for
232: * loading documents. If no specific builder has been set, this method
233: * returns <b>null</b>.
234: *
235: * @return the <code>DocumentBuilder</code> for loading new documents
236: * @since 1.2
237: */
238: public DocumentBuilder getDocumentBuilder() {
239: return documentBuilder;
240: }
241:
242: /**
243: * Sets the <code>DocumentBuilder</code> object to be used for loading
244: * documents. This method makes it possible to specify the exact document
245: * builder. So an application can create a builder, configure it for its
246: * special needs, and then pass it to this method.
247: *
248: * @param documentBuilder the document builder to be used; if undefined, a
249: * default builder will be used
250: * @since 1.2
251: */
252: public void setDocumentBuilder(DocumentBuilder documentBuilder) {
253: this .documentBuilder = documentBuilder;
254: }
255:
256: /**
257: * Returns the public ID of the DOCTYPE declaration from the loaded XML
258: * document. This is <b>null</b> if no document has been loaded yet or if
259: * the document does not contain a DOCTYPE declaration with a public ID.
260: *
261: * @return the public ID
262: * @since 1.3
263: */
264: public String getPublicID() {
265: return publicID;
266: }
267:
268: /**
269: * Sets the public ID of the DOCTYPE declaration. When this configuration is
270: * saved, a DOCTYPE declaration will be constructed that contains this
271: * public ID.
272: *
273: * @param publicID the public ID
274: * @since 1.3
275: */
276: public void setPublicID(String publicID) {
277: this .publicID = publicID;
278: }
279:
280: /**
281: * Returns the system ID of the DOCTYPE declaration from the loaded XML
282: * document. This is <b>null</b> if no document has been loaded yet or if
283: * the document does not contain a DOCTYPE declaration with a system ID.
284: *
285: * @return the system ID
286: * @since 1.3
287: */
288: public String getSystemID() {
289: return systemID;
290: }
291:
292: /**
293: * Sets the system ID of the DOCTYPE declaration. When this configuration is
294: * saved, a DOCTYPE declaration will be constructed that contains this
295: * system ID.
296: *
297: * @param systemID the system ID
298: * @since 1.3
299: */
300: public void setSystemID(String systemID) {
301: this .systemID = systemID;
302: }
303:
304: /**
305: * Returns the value of the validating flag.
306: *
307: * @return the validating flag
308: * @since 1.2
309: */
310: public boolean isValidating() {
311: return validating;
312: }
313:
314: /**
315: * Sets the value of the validating flag. This flag determines whether
316: * DTD validation should be performed when loading XML documents. This
317: * flag is evaluated only if no custom <code>DocumentBuilder</code> was set.
318: *
319: * @param validating the validating flag
320: * @since 1.2
321: */
322: public void setValidating(boolean validating) {
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: return document;
335: }
336:
337: /**
338: * Removes all properties from this configuration. If this configuration
339: * was loaded from a file, the associated DOM document is also cleared.
340: */
341: public void clear() {
342: super .clear();
343: document = null;
344: }
345:
346: /**
347: * Initializes this configuration from an XML document.
348: *
349: * @param document the document to be parsed
350: * @param elemRefs a flag whether references to the XML elements should be set
351: */
352: public void initProperties(Document document, boolean elemRefs) {
353: if (document.getDoctype() != null) {
354: setPublicID(document.getDoctype().getPublicId());
355: setSystemID(document.getDoctype().getSystemId());
356: }
357: constructHierarchy(getRoot(), document.getDocumentElement(),
358: elemRefs);
359: }
360:
361: /**
362: * Helper method for building the internal storage hierarchy. The XML
363: * elements are transformed into node objects.
364: *
365: * @param node the actual node
366: * @param element the actual XML element
367: * @param elemRefs a flag whether references to the XML elements should be set
368: */
369: private void constructHierarchy(Node node, Element element,
370: boolean elemRefs) {
371: processAttributes(node, element, elemRefs);
372: StringBuffer buffer = new StringBuffer();
373: NodeList list = element.getChildNodes();
374: for (int i = 0; i < list.getLength(); i++) {
375: org.w3c.dom.Node w3cNode = list.item(i);
376: if (w3cNode instanceof Element) {
377: Element child = (Element) w3cNode;
378: Node childNode = new XMLNode(child.getTagName(),
379: elemRefs ? child : null);
380: constructHierarchy(childNode, child, elemRefs);
381: node.addChild(childNode);
382: handleDelimiters(node, childNode);
383: } else if (w3cNode instanceof Text) {
384: Text data = (Text) w3cNode;
385: buffer.append(data.getData());
386: }
387: }
388: String text = buffer.toString().trim();
389: if (text.length() > 0 || !node.hasChildren()) {
390: node.setValue(text);
391: }
392: }
393:
394: /**
395: * Helper method for constructing node objects for the attributes of the
396: * given XML element.
397: *
398: * @param node the actual node
399: * @param element the actual XML element
400: * @param elemRefs a flag whether references to the XML elements should be set
401: */
402: private void processAttributes(Node node, Element element,
403: boolean elemRefs) {
404: NamedNodeMap attributes = element.getAttributes();
405: for (int i = 0; i < attributes.getLength(); ++i) {
406: org.w3c.dom.Node w3cNode = attributes.item(i);
407: if (w3cNode instanceof Attr) {
408: Attr attr = (Attr) w3cNode;
409: Iterator it;
410: if (isDelimiterParsingDisabled()) {
411: it = new SingletonIterator(attr.getValue());
412: } else {
413: it = PropertyConverter.split(attr.getValue(),
414: getListDelimiter()).iterator();
415: }
416: while (it.hasNext()) {
417: Node child = new XMLNode(attr.getName(),
418: elemRefs ? element : null);
419: child.setValue(it.next());
420: node.addAttribute(child);
421: }
422: }
423: }
424: }
425:
426: /**
427: * Deals with elements whose value is a list. In this case multiple child
428: * elements must be added.
429: *
430: * @param parent the parent element
431: * @param child the child element
432: */
433: private void handleDelimiters(Node parent, Node child) {
434: if (child.getValue() != null) {
435: List values;
436: if (isDelimiterParsingDisabled()) {
437: values = new ArrayList();
438: values.add(child.getValue().toString());
439: } else {
440: values = PropertyConverter.split(child.getValue()
441: .toString(), getListDelimiter());
442: }
443:
444: if (values.size() > 1) {
445: // remove the original child
446: parent.remove(child);
447: // add multiple new children
448: for (Iterator it = values.iterator(); it.hasNext();) {
449: Node c = new XMLNode(child.getName(), null);
450: c.setValue(it.next());
451: parent.addChild(c);
452: }
453: } else if (values.size() == 1) {
454: // we will have to replace the value because it might
455: // contain escaped delimiters
456: child.setValue(values.get(0));
457: }
458: }
459: }
460:
461: /**
462: * Creates the <code>DocumentBuilder</code> to be used for loading files.
463: * This implementation checks whether a specific
464: * <code>DocumentBuilder</code> has been set. If this is the case, this
465: * one is used. Otherwise a default builder is created. Depending on the
466: * value of the validating flag this builder will be a validating or a non
467: * validating <code>DocumentBuilder</code>.
468: *
469: * @return the <code>DocumentBuilder</code> for loading configuration
470: * files
471: * @throws ParserConfigurationException if an error occurs
472: * @since 1.2
473: */
474: protected DocumentBuilder createDocumentBuilder()
475: throws ParserConfigurationException {
476: if (getDocumentBuilder() != null) {
477: return getDocumentBuilder();
478: } else {
479: DocumentBuilderFactory factory = DocumentBuilderFactory
480: .newInstance();
481: factory.setValidating(isValidating());
482: DocumentBuilder result = factory.newDocumentBuilder();
483:
484: if (isValidating()) {
485: // register an error handler which detects validation errors
486: result.setErrorHandler(new DefaultHandler() {
487: public void error(SAXParseException ex)
488: throws SAXException {
489: throw ex;
490: }
491: });
492: }
493: return result;
494: }
495: }
496:
497: /**
498: * Creates a DOM document from the internal tree of configuration nodes.
499: *
500: * @return the new document
501: * @throws ConfigurationException if an error occurs
502: */
503: protected Document createDocument() throws ConfigurationException {
504: try {
505: if (document == null) {
506: DocumentBuilder builder = DocumentBuilderFactory
507: .newInstance().newDocumentBuilder();
508: Document newDocument = builder.newDocument();
509: Element rootElem = newDocument
510: .createElement(getRootElementName());
511: newDocument.appendChild(rootElem);
512: document = newDocument;
513: }
514:
515: XMLBuilderVisitor builder = new XMLBuilderVisitor(document,
516: getListDelimiter());
517: builder.processDocument(getRoot());
518: return document;
519: } /* try */
520: catch (DOMException domEx) {
521: throw new ConfigurationException(domEx);
522: } catch (ParserConfigurationException pex) {
523: throw new ConfigurationException(pex);
524: }
525: }
526:
527: /**
528: * Creates a new node object. This implementation returns an instance of the
529: * <code>XMLNode</code> class.
530: *
531: * @param name the node's name
532: * @return the new node
533: */
534: protected Node createNode(String name) {
535: return new XMLNode(name, null);
536: }
537:
538: /**
539: * Loads the configuration from the given input stream.
540: *
541: * @param in the input stream
542: * @throws ConfigurationException if an error occurs
543: */
544: public void load(InputStream in) throws ConfigurationException {
545: load(new InputSource(in));
546: }
547:
548: /**
549: * Load the configuration from the given reader.
550: * Note that the <code>clear()</code> method is not called, so
551: * the properties contained in the loaded file will be added to the
552: * actual set of properties.
553: *
554: * @param in An InputStream.
555: *
556: * @throws ConfigurationException if an error occurs
557: */
558: public void load(Reader in) throws ConfigurationException {
559: load(new InputSource(in));
560: }
561:
562: /**
563: * Loads a configuration file from the specified input source.
564: * @param source the input source
565: * @throws ConfigurationException if an error occurs
566: */
567: private void load(InputSource source) throws ConfigurationException {
568: try {
569: URL sourceURL = getDelegate().getURL();
570: if (sourceURL != null) {
571: source.setSystemId(sourceURL.toString());
572: }
573:
574: DocumentBuilder builder = createDocumentBuilder();
575: Document newDocument = builder.parse(source);
576: Document oldDocument = document;
577: document = null;
578: initProperties(newDocument, oldDocument == null);
579: document = (oldDocument == null) ? newDocument
580: : oldDocument;
581: } catch (Exception e) {
582: throw new ConfigurationException(e.getMessage(), e);
583: }
584: }
585:
586: /**
587: * Saves the configuration to the specified writer.
588: *
589: * @param writer the writer used to save the configuration
590: * @throws ConfigurationException if an error occurs
591: */
592: public void save(Writer writer) throws ConfigurationException {
593: try {
594: Transformer transformer = createTransformer();
595: Source source = new DOMSource(createDocument());
596: Result result = new StreamResult(writer);
597: transformer.transform(source, result);
598: } catch (TransformerException e) {
599: throw new ConfigurationException(e.getMessage(), e);
600: } catch (TransformerFactoryConfigurationError err) {
601: throw new ConfigurationException(err.getMessage(), err);
602: }
603: }
604:
605: /**
606: * Creates and initializes the transformer used for save operations. This
607: * base implementation initializes all of the default settings like
608: * indention mode and the DOCTYPE. Derived classes may overload this method
609: * if they have specific needs.
610: *
611: * @return the transformer to use for a save operation
612: * @throws TransformerException if an error occurs
613: * @since 1.3
614: */
615: protected Transformer createTransformer()
616: throws TransformerException {
617: Transformer transformer = TransformerFactory.newInstance()
618: .newTransformer();
619:
620: transformer.setOutputProperty(OutputKeys.INDENT, "yes");
621: if (getEncoding() != null) {
622: transformer.setOutputProperty(OutputKeys.ENCODING,
623: getEncoding());
624: }
625: if (getPublicID() != null) {
626: transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC,
627: getPublicID());
628: }
629: if (getSystemID() != null) {
630: transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM,
631: getSystemID());
632: }
633:
634: return transformer;
635: }
636:
637: /**
638: * Creates a copy of this object. The new configuration object will contain
639: * the same properties as the original, but it will lose any connection to a
640: * source document (if one exists). This is to avoid race conditions if both
641: * the original and the copy are modified and then saved.
642: *
643: * @return the copy
644: */
645: public Object clone() {
646: XMLConfiguration copy = (XMLConfiguration) super .clone();
647:
648: // clear document related properties
649: copy.document = null;
650: copy.setDelegate(copy.createDelegate());
651: // clear all references in the nodes, too
652: clearReferences(copy.getRootNode());
653:
654: return copy;
655: }
656:
657: /**
658: * Creates the file configuration delegate for this object. This implementation
659: * will return an instance of a class derived from <code>FileConfigurationDelegate</code>
660: * that deals with some specialities of <code>XMLConfiguration</code>.
661: * @return the delegate for this object
662: */
663: protected FileConfigurationDelegate createDelegate() {
664: return new XMLFileConfigurationDelegate();
665: }
666:
667: /**
668: * A specialized <code>Node</code> class that is connected with an XML
669: * element. Changes on a node are also performed on the associated element.
670: */
671: class XMLNode extends Node {
672: /**
673: * The serial version UID.
674: */
675: private static final long serialVersionUID = -4133988932174596562L;
676:
677: /**
678: * Creates a new instance of <code>XMLNode</code> and initializes it
679: * with a name and the corresponding XML element.
680: *
681: * @param name the node's name
682: * @param elem the XML element
683: */
684: public XMLNode(String name, Element elem) {
685: super (name);
686: setReference(elem);
687: }
688:
689: /**
690: * Sets the value of this node. If this node is associated with an XML
691: * element, this element will be updated, too.
692: *
693: * @param value the node's new value
694: */
695: public void setValue(Object value) {
696: super .setValue(value);
697:
698: if (getReference() != null && document != null) {
699: if (isAttribute()) {
700: updateAttribute();
701: } else {
702: updateElement(value);
703: }
704: }
705: }
706:
707: /**
708: * Updates the associated XML elements when a node is removed.
709: */
710: protected void removeReference() {
711: if (getReference() != null) {
712: Element element = (Element) getReference();
713: if (isAttribute()) {
714: updateAttribute();
715: } else {
716: org.w3c.dom.Node parentElem = element
717: .getParentNode();
718: if (parentElem != null) {
719: parentElem.removeChild(element);
720: }
721: }
722: }
723: }
724:
725: /**
726: * Updates the node's value if it represents an element node.
727: *
728: * @param value the new value
729: */
730: private void updateElement(Object value) {
731: Text txtNode = findTextNodeForUpdate();
732: if (value == null) {
733: // remove text
734: if (txtNode != null) {
735: ((Element) getReference()).removeChild(txtNode);
736: }
737: } else {
738: if (txtNode == null) {
739: txtNode = document.createTextNode(PropertyConverter
740: .escapeDelimiters(value.toString(),
741: getListDelimiter()));
742: if (((Element) getReference()).getFirstChild() != null) {
743: ((Element) getReference()).insertBefore(
744: txtNode, ((Element) getReference())
745: .getFirstChild());
746: } else {
747: ((Element) getReference()).appendChild(txtNode);
748: }
749: } else {
750: txtNode.setNodeValue(PropertyConverter
751: .escapeDelimiters(value.toString(),
752: getListDelimiter()));
753: }
754: }
755: }
756:
757: /**
758: * Updates the node's value if it represents an attribute.
759: *
760: */
761: private void updateAttribute() {
762: XMLBuilderVisitor.updateAttribute(getParent(), getName(),
763: getListDelimiter());
764: }
765:
766: /**
767: * Returns the only text node of this element for update. This method is
768: * called when the element's text changes. Then all text nodes except
769: * for the first are removed. A reference to the first is returned or
770: * <b>null </b> if there is no text node at all.
771: *
772: * @return the first and only text node
773: */
774: private Text findTextNodeForUpdate() {
775: Text result = null;
776: Element elem = (Element) getReference();
777: // Find all Text nodes
778: NodeList children = elem.getChildNodes();
779: Collection textNodes = new ArrayList();
780: for (int i = 0; i < children.getLength(); i++) {
781: org.w3c.dom.Node nd = children.item(i);
782: if (nd instanceof Text) {
783: if (result == null) {
784: result = (Text) nd;
785: } else {
786: textNodes.add(nd);
787: }
788: }
789: }
790:
791: // We don't want CDATAs
792: if (result instanceof CDATASection) {
793: textNodes.add(result);
794: result = null;
795: }
796:
797: // Remove all but the first Text node
798: for (Iterator it = textNodes.iterator(); it.hasNext();) {
799: elem.removeChild((org.w3c.dom.Node) it.next());
800: }
801: return result;
802: }
803: }
804:
805: /**
806: * A concrete <code>BuilderVisitor</code> that can construct XML
807: * documents.
808: */
809: static class XMLBuilderVisitor extends BuilderVisitor {
810: /** Stores the document to be constructed. */
811: private Document document;
812:
813: /** Stores the list delimiter.*/
814: private char listDelimiter = AbstractConfiguration
815: .getDefaultListDelimiter();
816:
817: /**
818: * Creates a new instance of <code>XMLBuilderVisitor</code>
819: *
820: * @param doc the document to be created
821: * @param listDelimiter the delimiter for attribute properties with multiple values
822: */
823: public XMLBuilderVisitor(Document doc, char listDelimiter) {
824: document = doc;
825: this .listDelimiter = listDelimiter;
826: }
827:
828: /**
829: * Processes the node hierarchy and adds new nodes to the document.
830: *
831: * @param rootNode the root node
832: */
833: public void processDocument(Node rootNode) {
834: rootNode.visit(this , null);
835: }
836:
837: /**
838: * Inserts a new node. This implementation ensures that the correct
839: * XML element is created and inserted between the given siblings.
840: *
841: * @param newNode the node to insert
842: * @param parent the parent node
843: * @param sibling1 the first sibling
844: * @param sibling2 the second sibling
845: * @return the new node
846: */
847: protected Object insert(Node newNode, Node parent,
848: Node sibling1, Node sibling2) {
849: if (newNode.isAttribute()) {
850: updateAttribute(parent, getElement(parent), newNode
851: .getName(), listDelimiter);
852: return null;
853: }
854:
855: else {
856: Element elem = document
857: .createElement(newNode.getName());
858: if (newNode.getValue() != null) {
859: elem.appendChild(document
860: .createTextNode(PropertyConverter
861: .escapeDelimiters(newNode
862: .getValue().toString(),
863: listDelimiter)));
864: }
865: if (sibling2 == null) {
866: getElement(parent).appendChild(elem);
867: } else if (sibling1 != null) {
868: getElement(parent).insertBefore(elem,
869: getElement(sibling1).getNextSibling());
870: } else {
871: getElement(parent).insertBefore(elem,
872: getElement(parent).getFirstChild());
873: }
874: return elem;
875: }
876: }
877:
878: /**
879: * Helper method for updating the value of the specified node's
880: * attribute with the given name.
881: *
882: * @param node the affected node
883: * @param elem the element that is associated with this node
884: * @param name the name of the affected attribute
885: * @param listDelimiter the delimiter vor attributes with multiple values
886: */
887: private static void updateAttribute(Node node, Element elem,
888: String name, char listDelimiter) {
889: if (node != null && elem != null) {
890: List attrs = node.getAttributes(name);
891: StringBuffer buf = new StringBuffer();
892: for (Iterator it = attrs.iterator(); it.hasNext();) {
893: Node attr = (Node) it.next();
894: if (attr.getValue() != null) {
895: if (buf.length() > 0) {
896: buf.append(listDelimiter);
897: }
898: buf.append(PropertyConverter.escapeDelimiters(
899: attr.getValue().toString(),
900: getDefaultListDelimiter()));
901: }
902: attr.setReference(elem);
903: }
904:
905: if (buf.length() < 1) {
906: elem.removeAttribute(name);
907: } else {
908: elem.setAttribute(name, buf.toString());
909: }
910: }
911: }
912:
913: /**
914: * Updates the value of the specified attribute of the given node.
915: * Because there can be multiple child nodes representing this attribute
916: * the new value is determined by iterating over all those child nodes.
917: *
918: * @param node the affected node
919: * @param name the name of the attribute
920: * @param listDelimiter the delimiter vor attributes with multiple values
921: */
922: static void updateAttribute(Node node, String name,
923: char listDelimiter) {
924: if (node != null) {
925: updateAttribute(node, (Element) node.getReference(),
926: name, listDelimiter);
927: }
928: }
929:
930: /**
931: * Helper method for accessing the element of the specified node.
932: *
933: * @param node the node
934: * @return the element of this node
935: */
936: private Element getElement(Node node) {
937: // special treatement for root node of the hierarchy
938: return (node.getName() != null) ? (Element) node
939: .getReference() : document.getDocumentElement();
940: }
941: }
942:
943: /**
944: * A special implementation of the <code>FileConfiguration</code> interface that is
945: * used internally to implement the <code>FileConfiguration</code> methods
946: * for <code>XMLConfiguration</code>, too.
947: */
948: private class XMLFileConfigurationDelegate extends
949: FileConfigurationDelegate {
950: public void load(InputStream in) throws ConfigurationException {
951: XMLConfiguration.this.load(in);
952: }
953: }
954: }
|