1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.configuration;
18
19 import java.io.File;
20 import java.io.Reader;
21 import java.io.StringWriter;
22 import java.io.Writer;
23 import java.net.URL;
24 import java.util.ArrayList;
25 import java.util.Iterator;
26 import java.util.List;
27 import javax.xml.parsers.DocumentBuilder;
28 import javax.xml.parsers.DocumentBuilderFactory;
29 import javax.xml.parsers.ParserConfigurationException;
30 import javax.xml.transform.Result;
31 import javax.xml.transform.Source;
32 import javax.xml.transform.Transformer;
33 import javax.xml.transform.TransformerException;
34 import javax.xml.transform.TransformerFactory;
35 import javax.xml.transform.dom.DOMSource;
36 import javax.xml.transform.stream.StreamResult;
37
38 import org.apache.commons.lang.StringUtils;
39 import org.w3c.dom.Attr;
40 import org.w3c.dom.CDATASection;
41 import org.w3c.dom.CharacterData;
42 import org.w3c.dom.Document;
43 import org.w3c.dom.Element;
44 import org.w3c.dom.NamedNodeMap;
45 import org.w3c.dom.Node;
46 import org.w3c.dom.NodeList;
47 import org.w3c.dom.Text;
48 import org.xml.sax.InputSource;
49
50 /***
51 * Reads a XML configuration file.
52 *
53 * To retrieve the value of an attribute of an element, use
54 * <code>X.Y.Z[@attribute]</code>. The '@' symbol was chosen for consistency
55 * with XPath.
56 *
57 * Setting property values will <b>NOT </b> automatically persist changes to
58 * disk, unless <code>autoSave=true</code>.
59 *
60 * @since commons-configuration 1.0
61 *
62 * @author J�rg Schaible
63 * @author <a href="mailto:kelvint@apache.org">Kelvin Tan </a>
64 * @author <a href="mailto:dlr@apache.org">Daniel Rall </a>
65 * @author Emmanuel Bourg
66 * @version $Revision: 1.17 $, $Date: 2004/10/04 19:35:45 $
67 */
68 public class XMLConfiguration extends AbstractFileConfiguration
69 {
70
71 private static final String ATTRIBUTE_START = "[@";
72
73 private static final String ATTRIBUTE_END = "]";
74
75 /***
76 * For consistency with properties files. Access nodes via an "A.B.C"
77 * notation.
78 */
79 private static final String NODE_DELIMITER = ".";
80
81 /***
82 * The XML document from our data source.
83 */
84 private Document document;
85
86 /***
87 * If true, modifications are immediately persisted.
88 */
89 private boolean autoSave = false;
90
91 /***
92 * Creates an empty XML configuration.
93 */
94 public XMLConfiguration()
95 {
96
97 DocumentBuilder builder = null;
98 try
99 {
100 builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
101 }
102 catch (ParserConfigurationException e)
103 {
104 throw new ConfigurationRuntimeException(e.getMessage(), e);
105 }
106
107 document = builder.newDocument();
108 document.appendChild(document.createElement("configuration"));
109 }
110
111 /***
112 * Creates and loads the XML configuration from the specified resource.
113 *
114 * @param resource The name of the resource to load.
115 *
116 * @throws ConfigurationException Error while loading the XML file
117 */
118 public XMLConfiguration(String resource) throws ConfigurationException
119 {
120 this.fileName = resource;
121 url = ConfigurationUtils.locate(resource);
122 load();
123 }
124
125 /***
126 * Creates and loads the XML configuration from the specified file.
127 *
128 * @param file The XML file to load.
129 * @throws ConfigurationException Error while loading the XML file
130 */
131 public XMLConfiguration(File file) throws ConfigurationException
132 {
133 setFile(file);
134 load();
135 }
136
137 /***
138 * Creates and loads the XML configuration from the specified URL.
139 *
140 * @param url The location of the XML file to load.
141 * @throws ConfigurationException Error while loading the XML file
142 */
143 public XMLConfiguration(URL url) throws ConfigurationException
144 {
145 setURL(url);
146 load();
147 }
148
149 /***
150 * {@inheritDoc}
151 */
152 public void load(Reader in) throws ConfigurationException
153 {
154 try
155 {
156 DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
157 document = builder.parse(new InputSource(in));
158 }
159 catch (Exception e)
160 {
161 throw new ConfigurationException(e.getMessage(), e);
162 }
163
164 initProperties(document.getDocumentElement(), new StringBuffer());
165 }
166
167 /***
168 * Loads and initializes from the XML file.
169 *
170 * @param element The element to start processing from. Callers should supply the root element of the document.
171 * @param hierarchy
172 */
173 private void initProperties(Element element, StringBuffer hierarchy)
174 {
175 StringBuffer buffer = new StringBuffer();
176 NodeList list = element.getChildNodes();
177 for (int i = 0; i < list.getLength(); i++)
178 {
179 Node node = list.item(i);
180 if (node instanceof Element)
181 {
182 Element child = (Element) node;
183
184 StringBuffer subhierarchy = new StringBuffer(hierarchy.toString());
185 subhierarchy.append(child.getTagName());
186 processAttributes(subhierarchy.toString(), child);
187 initProperties(child, subhierarchy.append(NODE_DELIMITER));
188 }
189 else if (node instanceof CDATASection || node instanceof Text)
190 {
191 CharacterData data = (CharacterData) node;
192 buffer.append(data.getData());
193 }
194 }
195
196 String text = buffer.toString().trim();
197 if (text.length() > 0 && hierarchy.length() > 0)
198 {
199 super.addProperty(hierarchy.substring(0, hierarchy.length() - 1), text);
200 }
201 }
202
203 /***
204 * Helper method for constructing properties for the attributes of the given
205 * XML element.
206 *
207 * @param hierarchy the actual hierarchy
208 * @param element the actual XML element
209 */
210 private void processAttributes(String hierarchy, Element element)
211 {
212
213 NamedNodeMap attributes = element.getAttributes();
214 for (int i = 0; i < attributes.getLength(); ++i)
215 {
216 Attr attr = (Attr) attributes.item(i);
217 String attrName = hierarchy + ATTRIBUTE_START + attr.getName() + ATTRIBUTE_END;
218 super.addProperty(attrName, attr.getValue());
219 }
220 }
221
222 /***
223 * Calls super method, and also ensures the underlying {@linkDocument} is
224 * modified so changes are persisted when saved.
225 *
226 * @param name
227 * @param value
228 */
229 public void addProperty(String name, Object value)
230 {
231 super.addProperty(name, value);
232 addXmlProperty(name, value);
233 possiblySave();
234 }
235
236 Object getXmlProperty(String name)
237 {
238
239 String[] nodes = parseElementNames(name);
240 String attName = parseAttributeName(name);
241
242
243 List children = findElementsForPropertyNodes(nodes);
244
245 List properties = new ArrayList();
246 if (attName == null)
247 {
248
249 Iterator cIter = children.iterator();
250 while (cIter.hasNext())
251 {
252 Element child = (Element) cIter.next();
253
254 String text = getChildText(child);
255 if (StringUtils.isNotEmpty(text))
256 {
257 properties.add(text);
258 }
259 }
260 }
261 else
262 {
263
264 Iterator cIter = children.iterator();
265 while (cIter.hasNext())
266 {
267 Element child = (Element) cIter.next();
268 if (child.hasAttribute(attName))
269 {
270 properties.add(child.getAttribute(attName));
271 }
272 }
273 }
274
275 switch (properties.size())
276 {
277 case 0:
278 return null;
279 case 1:
280 return properties.get(0);
281 default:
282 return properties;
283 }
284 }
285
286 /***
287 * TODO Add comment.
288 *
289 * @param nodes
290 * @return
291 */
292 private List findElementsForPropertyNodes(String[] nodes)
293 {
294 List children = new ArrayList();
295 List elements = new ArrayList();
296
297 children.add(document.getDocumentElement());
298 for (int i = 0; i < nodes.length; i++)
299 {
300 elements.clear();
301 elements.addAll(children);
302 children.clear();
303
304 String eName = nodes[i];
305 Iterator eIter = elements.iterator();
306 while (eIter.hasNext())
307 {
308 Element element = (Element) eIter.next();
309 NodeList list = element.getChildNodes();
310 for (int j = 0; j < list.getLength(); j++)
311 {
312 Node node = list.item(j);
313 if (node instanceof Element)
314 {
315 Element child = (Element) node;
316 if (eName.equals(child.getTagName()))
317 {
318 children.add(child);
319 }
320 }
321 }
322 }
323 }
324
325 return children;
326 }
327
328 private static String getChildText(Node node)
329 {
330
331 if (node == null)
332 {
333 return null;
334 }
335
336
337 StringBuffer str = new StringBuffer();
338 Node child = node.getFirstChild();
339 while (child != null)
340 {
341 short type = child.getNodeType();
342 if (type == Node.TEXT_NODE)
343 {
344 str.append(child.getNodeValue());
345 }
346 else if (type == Node.CDATA_SECTION_NODE)
347 {
348 str.append(child.getNodeValue());
349 }
350 child = child.getNextSibling();
351 }
352
353
354 return StringUtils.trimToNull(str.toString());
355
356 }
357
358 private Element getChildElementWithName(String eName, Element element)
359 {
360 Element child = null;
361
362 NodeList list = element.getChildNodes();
363 for (int j = 0; j < list.getLength(); j++)
364 {
365 Node node = list.item(j);
366 if (node instanceof Element)
367 {
368 child = (Element) node;
369 if (eName.equals(child.getTagName()))
370 {
371 break;
372 }
373 child = null;
374 }
375 }
376 return child;
377 }
378
379 /***
380 * Adds the property value in our document tree.
381 *
382 * @param name The name of the element to set a value for.
383 * @param value The value to set.
384 */
385 private void addXmlProperty(String name, Object value)
386 {
387
388 String[] nodes = parseElementNames(name);
389 String attName = parseAttributeName(name);
390
391 Element element = document.getDocumentElement();
392 Element parent = element;
393
394 for (int i = 0; i < nodes.length; i++)
395 {
396 if (element == null)
397 {
398 break;
399 }
400 parent = element;
401 String eName = nodes[i];
402 Element child = getChildElementWithName(eName, element);
403
404 element = child;
405 }
406
407 Element child = document.createElement(nodes[nodes.length - 1]);
408 parent.appendChild(child);
409 if (attName == null)
410 {
411 CharacterData data = document.createTextNode(String.valueOf(value));
412 child.appendChild(data);
413 }
414 else
415 {
416 child.setAttribute(attName, String.valueOf(value));
417 }
418 }
419
420 /***
421 * Calls super method, and also ensures the underlying {@link Document}is
422 * modified so changes are persisted when saved.
423 *
424 * @param name The name of the property to clear.
425 */
426 public void clearProperty(String name)
427 {
428 super.clearProperty(name);
429 clearXmlProperty(name);
430 possiblySave();
431 }
432
433 private void clearXmlProperty(String name)
434 {
435
436 String[] nodes = parseElementNames(name);
437 String attName = parseAttributeName(name);
438
439
440 List children = findElementsForPropertyNodes(nodes);
441
442 if (attName == null)
443 {
444
445 Iterator cIter = children.iterator();
446 while (cIter.hasNext())
447 {
448 Element child = (Element) cIter.next();
449
450
451 boolean hasSubelements = false;
452 Node subchild = child.getFirstChild();
453 while (subchild != null)
454 {
455 if (subchild.getNodeType() == Node.ELEMENT_NODE)
456 {
457 hasSubelements = true;
458 break;
459 }
460 subchild = subchild.getNextSibling();
461 }
462
463 if (!hasSubelements)
464 {
465
466 if (!child.hasAttributes())
467 {
468
469 Node parent = child.getParentNode();
470 parent.removeChild(child);
471 }
472 else
473 {
474
475 subchild = child.getLastChild();
476 while (subchild != null)
477 {
478 child.removeChild(subchild);
479 subchild = child.getLastChild();
480 }
481 }
482 }
483 }
484 }
485 else
486 {
487
488 Iterator cIter = children.iterator();
489 while (cIter.hasNext())
490 {
491 Element child = (Element) cIter.next();
492 child.removeAttribute(attName);
493 }
494 }
495 }
496
497 /***
498 * Save the configuration if the automatic persistence is enabled and a file
499 * is specified.
500 */
501 private void possiblySave()
502 {
503 if (autoSave && fileName != null)
504 {
505 try
506 {
507 save();
508 }
509 catch (ConfigurationException ce)
510 {
511 throw new ConfigurationRuntimeException("Failed to auto-save", ce);
512 }
513 }
514 }
515
516 /***
517 * If true, changes are automatically persisted.
518 *
519 * @param autoSave
520 */
521 public void setAutoSave(boolean autoSave)
522 {
523 this.autoSave = autoSave;
524 }
525
526 /***
527 * {@inheritDoc}
528 */
529 public void save(Writer writer) throws ConfigurationException
530 {
531 try
532 {
533 Transformer transformer = TransformerFactory.newInstance().newTransformer();
534 Source source = new DOMSource(document);
535 Result result = new StreamResult(writer);
536
537 transformer.setOutputProperty("indent", "yes");
538 transformer.transform(source, result);
539 }
540 catch (TransformerException e)
541 {
542 throw new ConfigurationException(e.getMessage(), e);
543 }
544 }
545
546 public String toString()
547 {
548 StringWriter writer = new StringWriter();
549 try
550 {
551 save(writer);
552 }
553 catch (ConfigurationException e)
554 {
555 e.printStackTrace();
556 }
557 return writer.toString();
558 }
559
560 /***
561 * Parse a property key and return an array of the element hierarchy it
562 * specifies. For example the key "x.y.z[@abc]" will result in [x, y, z].
563 *
564 * @param key the key to parse
565 *
566 * @return the elements in the key
567 */
568 protected static String[] parseElementNames(String key)
569 {
570 if (key == null)
571 {
572 return new String[]{};
573 }
574 else
575 {
576
577 int attStart = key.indexOf(ATTRIBUTE_START);
578
579 if (attStart > -1)
580 {
581
582 key = key.substring(0, attStart);
583 }
584
585 return StringUtils.split(key, NODE_DELIMITER);
586 }
587 }
588
589 /***
590 * Parse a property key and return the attribute name if it existst.
591 *
592 * @param key the key to parse
593 *
594 * @return the attribute name, or null if the key doesn't contain one
595 */
596 protected static String parseAttributeName(String key)
597 {
598 String name = null;
599
600 if (key != null)
601 {
602
603 int attStart = key.indexOf(ATTRIBUTE_START);
604
605 if (attStart > -1)
606 {
607
608 int attEnd = key.indexOf(ATTRIBUTE_END);
609 attEnd = attEnd > -1 ? attEnd : key.length();
610
611 name = key.substring(attStart + ATTRIBUTE_START.length(), attEnd);
612 }
613 }
614
615 return name;
616 }
617 }