1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.apache.commons.configuration.plist;
19
20 import java.io.File;
21 import java.io.PrintWriter;
22 import java.io.Reader;
23 import java.io.Writer;
24 import java.math.BigDecimal;
25 import java.net.URL;
26 import java.text.DateFormat;
27 import java.text.ParseException;
28 import java.text.SimpleDateFormat;
29 import java.util.ArrayList;
30 import java.util.Calendar;
31 import java.util.Date;
32 import java.util.Iterator;
33 import java.util.List;
34 import java.util.Map;
35 import javax.xml.parsers.SAXParser;
36 import javax.xml.parsers.SAXParserFactory;
37
38 import org.apache.commons.codec.binary.Base64;
39 import org.apache.commons.configuration.AbstractHierarchicalFileConfiguration;
40 import org.apache.commons.configuration.Configuration;
41 import org.apache.commons.configuration.ConfigurationException;
42 import org.apache.commons.configuration.HierarchicalConfiguration;
43 import org.apache.commons.configuration.MapConfiguration;
44 import org.apache.commons.lang.StringEscapeUtils;
45 import org.apache.commons.lang.StringUtils;
46
47 import org.xml.sax.Attributes;
48 import org.xml.sax.EntityResolver;
49 import org.xml.sax.InputSource;
50 import org.xml.sax.SAXException;
51 import org.xml.sax.helpers.DefaultHandler;
52
53 /***
54 * Property list file (plist) in XML format as used by Mac OS X (http://www.apple.com/DTDs/PropertyList-1.0.dtd).
55 * This configuration doesn't support the binary format used in OS X 10.4.
56 *
57 * <p>Example:</p>
58 * <pre>
59 * <?xml version="1.0"?>
60 * <!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
61 * <plist version="1.0">
62 * <dict>
63 * <key>string</key>
64 * <string>value1</string>
65 *
66 * <key>integer</key>
67 * <integer>12345</integer>
68 *
69 * <key>real</key>
70 * <real>-123.45E-1</real>
71 *
72 * <key>boolean</key>
73 * <true/>
74 *
75 * <key>date</key>
76 * <date>2005-01-01T12:00:00-0700</date>
77 *
78 * <key>data</key>
79 * <data>RHJhY28gRG9ybWllbnMgTnVucXVhbSBUaXRpbGxhbmR1cw==</data>
80 *
81 * <key>array</key>
82 * <array>
83 * <string>value1</string>
84 * <string>value2</string>
85 * <string>value3</string>
86 * </array>
87 *
88 * <key>dictionnary</key>
89 * <dict>
90 * <key>key1</key>
91 * <string>value1</string>
92 * <key>key2</key>
93 * <string>value2</string>
94 * <key>key3</key>
95 * <string>value3</string>
96 * </dict>
97 *
98 * <key>nested</key>
99 * <dict>
100 * <key>node1</key>
101 * <dict>
102 * <key>node2</key>
103 * <dict>
104 * <key>node3</key>
105 * <string>value</string>
106 * </dict>
107 * </dict>
108 * </dict>
109 *
110 * </dict>
111 * </plist>
112 * </pre>
113 *
114 * @since 1.2
115 *
116 * @author Emmanuel Bourg
117 * @version $Revision: 532336 $, $Date: 2007-04-25 14:23:01 +0200 (Mi, 25 Apr 2007) $
118 */
119 public class XMLPropertyListConfiguration extends AbstractHierarchicalFileConfiguration
120 {
121 /***
122 * The serial version UID.
123 */
124 private static final long serialVersionUID = -3162063751042475985L;
125
126 /*** Size of the indentation for the generated file. */
127 private static final int INDENT_SIZE = 4;
128
129 /***
130 * Creates an empty XMLPropertyListConfiguration object which can be
131 * used to synthesize a new plist file by adding values and
132 * then saving().
133 */
134 public XMLPropertyListConfiguration()
135 {
136 }
137
138 /***
139 * Creates a new instance of <code>XMLPropertyListConfiguration</code> and
140 * copies the content of the specified configuration into this object.
141 *
142 * @param configuration the configuration to copy
143 * @since 1.4
144 */
145 public XMLPropertyListConfiguration(HierarchicalConfiguration configuration)
146 {
147 super(configuration);
148 }
149
150 /***
151 * Creates and loads the property list from the specified file.
152 *
153 * @param fileName The name of the plist file to load.
154 * @throws org.apache.commons.configuration.ConfigurationException Error
155 * while loading the plist file
156 */
157 public XMLPropertyListConfiguration(String fileName) throws ConfigurationException
158 {
159 super(fileName);
160 }
161
162 /***
163 * Creates and loads the property list from the specified file.
164 *
165 * @param file The plist file to load.
166 * @throws ConfigurationException Error while loading the plist file
167 */
168 public XMLPropertyListConfiguration(File file) throws ConfigurationException
169 {
170 super(file);
171 }
172
173 /***
174 * Creates and loads the property list from the specified URL.
175 *
176 * @param url The location of the plist file to load.
177 * @throws ConfigurationException Error while loading the plist file
178 */
179 public XMLPropertyListConfiguration(URL url) throws ConfigurationException
180 {
181 super(url);
182 }
183
184 public void setProperty(String key, Object value)
185 {
186
187 if (value instanceof byte[])
188 {
189 fireEvent(EVENT_SET_PROPERTY, key, value, true);
190 setDetailEvents(false);
191 try
192 {
193 clearProperty(key);
194 addPropertyDirect(key, value);
195 }
196 finally
197 {
198 setDetailEvents(true);
199 }
200 fireEvent(EVENT_SET_PROPERTY, key, value, false);
201 }
202 else
203 {
204 super.setProperty(key, value);
205 }
206 }
207
208 public void addProperty(String key, Object value)
209 {
210 if (value instanceof byte[])
211 {
212 fireEvent(EVENT_ADD_PROPERTY, key, value, true);
213 addPropertyDirect(key, value);
214 fireEvent(EVENT_ADD_PROPERTY, key, value, false);
215 }
216 else
217 {
218 super.addProperty(key, value);
219 }
220 }
221
222 public void load(Reader in) throws ConfigurationException
223 {
224
225 EntityResolver resolver = new EntityResolver()
226 {
227 public InputSource resolveEntity(String publicId, String systemId)
228 {
229 return new InputSource(getClass().getClassLoader().getResourceAsStream("PropertyList-1.0.dtd"));
230 }
231 };
232
233
234 XMLPropertyListHandler handler = new XMLPropertyListHandler(getRoot());
235 try
236 {
237 SAXParserFactory factory = SAXParserFactory.newInstance();
238 factory.setValidating(true);
239
240 SAXParser parser = factory.newSAXParser();
241 parser.getXMLReader().setEntityResolver(resolver);
242 parser.getXMLReader().setContentHandler(handler);
243 parser.getXMLReader().parse(new InputSource(in));
244 }
245 catch (Exception e)
246 {
247 throw new ConfigurationException("Unable to parse the configuration file", e);
248 }
249 }
250
251 public void save(Writer out) throws ConfigurationException
252 {
253 PrintWriter writer = new PrintWriter(out);
254
255 if (getEncoding() != null)
256 {
257 writer.println("<?xml version=\"1.0\" encoding=\"" + getEncoding() + "\"?>");
258 }
259 else
260 {
261 writer.println("<?xml version=\"1.0\"?>");
262 }
263
264 writer.println("<!DOCTYPE plist SYSTEM \"file://localhost/System/Library/DTDs/PropertyList.dtd\">");
265 writer.println("<plist version=\"1.0\">");
266
267 printNode(writer, 1, getRoot());
268
269 writer.println("</plist>");
270 writer.flush();
271 }
272
273 /***
274 * Append a node to the writer, indented according to a specific level.
275 */
276 private void printNode(PrintWriter out, int indentLevel, Node node)
277 {
278 String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
279
280 if (node.getName() != null)
281 {
282 out.println(padding + "<key>" + StringEscapeUtils.escapeXml(node.getName()) + "</key>");
283 }
284
285 List children = node.getChildren();
286 if (!children.isEmpty())
287 {
288 out.println(padding + "<dict>");
289
290 Iterator it = children.iterator();
291 while (it.hasNext())
292 {
293 Node child = (Node) it.next();
294 printNode(out, indentLevel + 1, child);
295
296 if (it.hasNext())
297 {
298 out.println();
299 }
300 }
301
302 out.println(padding + "</dict>");
303 }
304 else
305 {
306 Object value = node.getValue();
307 printValue(out, indentLevel, value);
308 }
309 }
310
311 /***
312 * Append a value to the writer, indented according to a specific level.
313 */
314 private void printValue(PrintWriter out, int indentLevel, Object value)
315 {
316 String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
317
318 if (value instanceof Date)
319 {
320 out.println(padding + "<date>" + PListNode.format.format((Date) value) + "</date>");
321 }
322 else if (value instanceof Calendar)
323 {
324 printValue(out, indentLevel, ((Calendar) value).getTime());
325 }
326 else if (value instanceof Number)
327 {
328 if (value instanceof Double || value instanceof Float || value instanceof BigDecimal)
329 {
330 out.println(padding + "<real>" + value.toString() + "</real>");
331 }
332 else
333 {
334 out.println(padding + "<integer>" + value.toString() + "</integer>");
335 }
336 }
337 else if (value instanceof Boolean)
338 {
339 if (((Boolean) value).booleanValue())
340 {
341 out.println(padding + "<true/>");
342 }
343 else
344 {
345 out.println(padding + "<false/>");
346 }
347 }
348 else if (value instanceof List)
349 {
350 out.println(padding + "<array>");
351 Iterator it = ((List) value).iterator();
352 while (it.hasNext())
353 {
354 printValue(out, indentLevel + 1, it.next());
355 }
356 out.println(padding + "</array>");
357 }
358 else if (value instanceof HierarchicalConfiguration)
359 {
360 printNode(out, indentLevel, ((HierarchicalConfiguration) value).getRoot());
361 }
362 else if (value instanceof Configuration)
363 {
364
365 out.println(padding + "<dict>");
366
367 Configuration config = (Configuration) value;
368 Iterator it = config.getKeys();
369 while (it.hasNext())
370 {
371
372 String key = (String) it.next();
373 Node node = new Node(key);
374 node.setValue(config.getProperty(key));
375
376
377 printNode(out, indentLevel + 1, node);
378
379 if (it.hasNext())
380 {
381 out.println();
382 }
383 }
384 out.println(padding + "</dict>");
385 }
386 else if (value instanceof Map)
387 {
388
389 Map map = (Map) value;
390 printValue(out, indentLevel, new MapConfiguration(map));
391 }
392 else if (value instanceof byte[])
393 {
394 String base64 = new String(Base64.encodeBase64((byte[]) value));
395 out.println(padding + "<data>" + StringEscapeUtils.escapeXml(base64) + "</data>");
396 }
397 else
398 {
399 out.println(padding + "<string>" + StringEscapeUtils.escapeXml(String.valueOf(value)) + "</string>");
400 }
401 }
402
403 /***
404 * SAX Handler to build the configuration nodes while the document is being parsed.
405 */
406 private class XMLPropertyListHandler extends DefaultHandler
407 {
408 /*** The buffer containing the text node being read */
409 private StringBuffer buffer = new StringBuffer();
410
411 /*** The stack of configuration nodes */
412 private List stack = new ArrayList();
413
414 public XMLPropertyListHandler(Node root)
415 {
416 push(root);
417 }
418
419 /***
420 * Return the node on the top of the stack.
421 */
422 private Node peek()
423 {
424 if (!stack.isEmpty())
425 {
426 return (Node) stack.get(stack.size() - 1);
427 }
428 else
429 {
430 return null;
431 }
432 }
433
434 /***
435 * Remove and return the node on the top of the stack.
436 */
437 private Node pop()
438 {
439 if (!stack.isEmpty())
440 {
441 return (Node) stack.remove(stack.size() - 1);
442 }
443 else
444 {
445 return null;
446 }
447 }
448
449 /***
450 * Put a node on the top of the stack.
451 */
452 private void push(Node node)
453 {
454 stack.add(node);
455 }
456
457 public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException
458 {
459 if ("array".equals(qName))
460 {
461 push(new ArrayNode());
462 }
463 else if ("dict".equals(qName))
464 {
465 if (peek() instanceof ArrayNode)
466 {
467
468 XMLPropertyListConfiguration config = new XMLPropertyListConfiguration();
469
470
471 ArrayNode node = (ArrayNode) peek();
472 node.addValue(config);
473
474
475 push(config.getRoot());
476 }
477 }
478 }
479
480 public void endElement(String uri, String localName, String qName) throws SAXException
481 {
482 if ("key".equals(qName))
483 {
484
485 PListNode node = new PListNode();
486 node.setName(buffer.toString());
487 peek().addChild(node);
488 push(node);
489 }
490 else if ("dict".equals(qName))
491 {
492
493 pop();
494 }
495 else
496 {
497 if ("string".equals(qName))
498 {
499 ((PListNode) peek()).addValue(buffer.toString());
500 }
501 else if ("integer".equals(qName))
502 {
503 ((PListNode) peek()).addIntegerValue(buffer.toString());
504 }
505 else if ("real".equals(qName))
506 {
507 ((PListNode) peek()).addRealValue(buffer.toString());
508 }
509 else if ("true".equals(qName))
510 {
511 ((PListNode) peek()).addTrueValue();
512 }
513 else if ("false".equals(qName))
514 {
515 ((PListNode) peek()).addFalseValue();
516 }
517 else if ("data".equals(qName))
518 {
519 ((PListNode) peek()).addDataValue(buffer.toString());
520 }
521 else if ("date".equals(qName))
522 {
523 ((PListNode) peek()).addDateValue(buffer.toString());
524 }
525 else if ("array".equals(qName))
526 {
527 ArrayNode array = (ArrayNode) pop();
528 ((PListNode) peek()).addList(array);
529 }
530
531
532
533 if (!(peek() instanceof ArrayNode))
534 {
535 pop();
536 }
537 }
538
539 buffer.setLength(0);
540 }
541
542 public void characters(char[] ch, int start, int length) throws SAXException
543 {
544 buffer.append(ch, start, length);
545 }
546 }
547
548 /***
549 * Node extension with addXXX methods to parse the typed data passed by the SAX handler.
550 * <b>Do not use this class !</b> It is used internally by XMLPropertyConfiguration
551 * to parse the configuration file, it may be removed at any moment in the future.
552 */
553 public static class PListNode extends Node
554 {
555 /***
556 * The serial version UID.
557 */
558 private static final long serialVersionUID = -7614060264754798317L;
559
560 /*** The standard format of dates in plist files. */
561 private static DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
562
563 /***
564 * Update the value of the node. If the existing value is null, it's
565 * replaced with the new value. If the existing value is a list, the
566 * specified value is appended to the list. If the existing value is
567 * not null, a list with the two values is built.
568 *
569 * @param value the value to be added
570 */
571 public void addValue(Object value)
572 {
573 if (getValue() == null)
574 {
575 setValue(value);
576 }
577 else if (getValue() instanceof List)
578 {
579 List list = (List) getValue();
580 list.add(value);
581 }
582 else
583 {
584 List list = new ArrayList();
585 list.add(getValue());
586 list.add(value);
587 setValue(list);
588 }
589 }
590
591 /***
592 * Parse the specified string as a date and add it to the values of the node.
593 *
594 * @param value the value to be added
595 */
596 public void addDateValue(String value)
597 {
598 try
599 {
600 addValue(format.parse(value));
601 }
602 catch (ParseException e)
603 {
604
605 ;
606 }
607 }
608
609 /***
610 * Parse the specified string as a byte array in base 64 format
611 * and add it to the values of the node.
612 *
613 * @param value the value to be added
614 */
615 public void addDataValue(String value)
616 {
617 addValue(Base64.decodeBase64(value.getBytes()));
618 }
619
620 /***
621 * Parse the specified string as an Interger and add it to the values of the node.
622 *
623 * @param value the value to be added
624 */
625 public void addIntegerValue(String value)
626 {
627 addValue(new Integer(value));
628 }
629
630 /***
631 * Parse the specified string as a Double and add it to the values of the node.
632 *
633 * @param value the value to be added
634 */
635 public void addRealValue(String value)
636 {
637 addValue(new Double(value));
638 }
639
640 /***
641 * Add a boolean value 'true' to the values of the node.
642 */
643 public void addTrueValue()
644 {
645 addValue(Boolean.TRUE);
646 }
647
648 /***
649 * Add a boolean value 'false' to the values of the node.
650 */
651 public void addFalseValue()
652 {
653 addValue(Boolean.FALSE);
654 }
655
656 /***
657 * Add a sublist to the values of the node.
658 *
659 * @param node the node whose value will be added to the current node value
660 */
661 public void addList(ArrayNode node)
662 {
663 addValue(node.getValue());
664 }
665 }
666
667 /***
668 * Container for array elements. <b>Do not use this class !</b>
669 * It is used internally by XMLPropertyConfiguration to parse the
670 * configuration file, it may be removed at any moment in the future.
671 */
672 public static class ArrayNode extends PListNode
673 {
674 /***
675 * The serial version UID.
676 */
677 private static final long serialVersionUID = 5586544306664205835L;
678
679 /*** The list of values in the array. */
680 private List list = new ArrayList();
681
682 /***
683 * Add an object to the array.
684 *
685 * @param value the value to be added
686 */
687 public void addValue(Object value)
688 {
689 list.add(value);
690 }
691
692 /***
693 * Return the list of values in the array.
694 *
695 * @return the {@link List} of values
696 */
697 public Object getValue()
698 {
699 return list;
700 }
701 }
702 }