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