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