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.plist;
019
020 import java.io.File;
021 import java.io.PrintWriter;
022 import java.io.Reader;
023 import java.io.Writer;
024 import java.math.BigDecimal;
025 import java.math.BigInteger;
026 import java.net.URL;
027 import java.text.DateFormat;
028 import java.text.ParseException;
029 import java.text.SimpleDateFormat;
030 import java.util.ArrayList;
031 import java.util.Calendar;
032 import java.util.Collection;
033 import java.util.Date;
034 import java.util.Iterator;
035 import java.util.List;
036 import java.util.Map;
037 import java.util.TimeZone;
038
039 import javax.xml.parsers.SAXParser;
040 import javax.xml.parsers.SAXParserFactory;
041
042 import org.apache.commons.codec.binary.Base64;
043 import org.apache.commons.configuration.AbstractHierarchicalFileConfiguration;
044 import org.apache.commons.configuration.Configuration;
045 import org.apache.commons.configuration.ConfigurationException;
046 import org.apache.commons.configuration.HierarchicalConfiguration;
047 import org.apache.commons.configuration.MapConfiguration;
048 import org.apache.commons.lang.StringEscapeUtils;
049 import org.apache.commons.lang.StringUtils;
050 import org.xml.sax.Attributes;
051 import org.xml.sax.EntityResolver;
052 import org.xml.sax.InputSource;
053 import org.xml.sax.SAXException;
054 import org.xml.sax.helpers.DefaultHandler;
055
056 /**
057 * Property list file (plist) in XML format as used by Mac OS X (http://www.apple.com/DTDs/PropertyList-1.0.dtd).
058 * This configuration doesn't support the binary format used in OS X 10.4.
059 *
060 * <p>Example:</p>
061 * <pre>
062 * <?xml version="1.0"?>
063 * <!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
064 * <plist version="1.0">
065 * <dict>
066 * <key>string</key>
067 * <string>value1</string>
068 *
069 * <key>integer</key>
070 * <integer>12345</integer>
071 *
072 * <key>real</key>
073 * <real>-123.45E-1</real>
074 *
075 * <key>boolean</key>
076 * <true/>
077 *
078 * <key>date</key>
079 * <date>2005-01-01T12:00:00Z</date>
080 *
081 * <key>data</key>
082 * <data>RHJhY28gRG9ybWllbnMgTnVucXVhbSBUaXRpbGxhbmR1cw==</data>
083 *
084 * <key>array</key>
085 * <array>
086 * <string>value1</string>
087 * <string>value2</string>
088 * <string>value3</string>
089 * </array>
090 *
091 * <key>dictionnary</key>
092 * <dict>
093 * <key>key1</key>
094 * <string>value1</string>
095 * <key>key2</key>
096 * <string>value2</string>
097 * <key>key3</key>
098 * <string>value3</string>
099 * </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: 902596 $, $Date: 2010-01-24 17:28:55 +0100 (So, 24. Jan 2010) $
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 initRoot();
140 }
141
142 /**
143 * Creates a new instance of <code>XMLPropertyListConfiguration</code> and
144 * copies the content of the specified configuration into this object.
145 *
146 * @param configuration the configuration to copy
147 * @since 1.4
148 */
149 public XMLPropertyListConfiguration(HierarchicalConfiguration configuration)
150 {
151 super(configuration);
152 }
153
154 /**
155 * Creates and loads the property list from the specified file.
156 *
157 * @param fileName The name of the plist file to load.
158 * @throws org.apache.commons.configuration.ConfigurationException Error
159 * while loading the plist file
160 */
161 public XMLPropertyListConfiguration(String fileName) throws ConfigurationException
162 {
163 super(fileName);
164 }
165
166 /**
167 * Creates and loads the property list from the specified file.
168 *
169 * @param file The plist file to load.
170 * @throws ConfigurationException Error while loading the plist file
171 */
172 public XMLPropertyListConfiguration(File file) throws ConfigurationException
173 {
174 super(file);
175 }
176
177 /**
178 * Creates and loads the property list from the specified URL.
179 *
180 * @param url The location of the plist file to load.
181 * @throws ConfigurationException Error while loading the plist file
182 */
183 public XMLPropertyListConfiguration(URL url) throws ConfigurationException
184 {
185 super(url);
186 }
187
188 public void setProperty(String key, Object value)
189 {
190 // special case for byte arrays, they must be stored as is in the configuration
191 if (value instanceof byte[])
192 {
193 fireEvent(EVENT_SET_PROPERTY, key, value, true);
194 setDetailEvents(false);
195 try
196 {
197 clearProperty(key);
198 addPropertyDirect(key, value);
199 }
200 finally
201 {
202 setDetailEvents(true);
203 }
204 fireEvent(EVENT_SET_PROPERTY, key, value, false);
205 }
206 else
207 {
208 super.setProperty(key, value);
209 }
210 }
211
212 public void addProperty(String key, Object value)
213 {
214 if (value instanceof byte[])
215 {
216 fireEvent(EVENT_ADD_PROPERTY, key, value, true);
217 addPropertyDirect(key, value);
218 fireEvent(EVENT_ADD_PROPERTY, key, value, false);
219 }
220 else
221 {
222 super.addProperty(key, value);
223 }
224 }
225
226 public void load(Reader in) throws ConfigurationException
227 {
228 // We have to make sure that the root node is actually a PListNode.
229 // If this object was not created using the standard constructor, the
230 // root node is a plain Node.
231 if (!(getRootNode() instanceof PListNode))
232 {
233 initRoot();
234 }
235
236 // set up the DTD validation
237 EntityResolver resolver = new EntityResolver()
238 {
239 public InputSource resolveEntity(String publicId, String systemId)
240 {
241 return new InputSource(getClass().getClassLoader().getResourceAsStream("PropertyList-1.0.dtd"));
242 }
243 };
244
245 // parse the file
246 XMLPropertyListHandler handler = new XMLPropertyListHandler(getRoot());
247 try
248 {
249 SAXParserFactory factory = SAXParserFactory.newInstance();
250 factory.setValidating(true);
251
252 SAXParser parser = factory.newSAXParser();
253 parser.getXMLReader().setEntityResolver(resolver);
254 parser.getXMLReader().setContentHandler(handler);
255 parser.getXMLReader().parse(new InputSource(in));
256 }
257 catch (Exception e)
258 {
259 throw new ConfigurationException("Unable to parse the configuration file", e);
260 }
261 }
262
263 public void save(Writer out) throws ConfigurationException
264 {
265 PrintWriter writer = new PrintWriter(out);
266
267 if (getEncoding() != null)
268 {
269 writer.println("<?xml version=\"1.0\" encoding=\"" + getEncoding() + "\"?>");
270 }
271 else
272 {
273 writer.println("<?xml version=\"1.0\"?>");
274 }
275
276 writer.println("<!DOCTYPE plist SYSTEM \"file://localhost/System/Library/DTDs/PropertyList.dtd\">");
277 writer.println("<plist version=\"1.0\">");
278
279 printNode(writer, 1, getRoot());
280
281 writer.println("</plist>");
282 writer.flush();
283 }
284
285 /**
286 * Append a node to the writer, indented according to a specific level.
287 */
288 private void printNode(PrintWriter out, int indentLevel, Node node)
289 {
290 String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
291
292 if (node.getName() != null)
293 {
294 out.println(padding + "<key>" + StringEscapeUtils.escapeXml(node.getName()) + "</key>");
295 }
296
297 List children = node.getChildren();
298 if (!children.isEmpty())
299 {
300 out.println(padding + "<dict>");
301
302 Iterator it = children.iterator();
303 while (it.hasNext())
304 {
305 Node child = (Node) it.next();
306 printNode(out, indentLevel + 1, child);
307
308 if (it.hasNext())
309 {
310 out.println();
311 }
312 }
313
314 out.println(padding + "</dict>");
315 }
316 else if (node.getValue() == null)
317 {
318 out.println(padding + "<dict/>");
319 }
320 else
321 {
322 Object value = node.getValue();
323 printValue(out, indentLevel, value);
324 }
325 }
326
327 /**
328 * Append a value to the writer, indented according to a specific level.
329 */
330 private void printValue(PrintWriter out, int indentLevel, Object value)
331 {
332 String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
333
334 if (value instanceof Date)
335 {
336 synchronized (PListNode.format)
337 {
338 out.println(padding + "<date>" + PListNode.format.format((Date) value) + "</date>");
339 }
340 }
341 else if (value instanceof Calendar)
342 {
343 printValue(out, indentLevel, ((Calendar) value).getTime());
344 }
345 else if (value instanceof Number)
346 {
347 if (value instanceof Double || value instanceof Float || value instanceof BigDecimal)
348 {
349 out.println(padding + "<real>" + value.toString() + "</real>");
350 }
351 else
352 {
353 out.println(padding + "<integer>" + value.toString() + "</integer>");
354 }
355 }
356 else if (value instanceof Boolean)
357 {
358 if (((Boolean) value).booleanValue())
359 {
360 out.println(padding + "<true/>");
361 }
362 else
363 {
364 out.println(padding + "<false/>");
365 }
366 }
367 else if (value instanceof List)
368 {
369 out.println(padding + "<array>");
370 Iterator it = ((List) value).iterator();
371 while (it.hasNext())
372 {
373 printValue(out, indentLevel + 1, it.next());
374 }
375 out.println(padding + "</array>");
376 }
377 else if (value instanceof HierarchicalConfiguration)
378 {
379 printNode(out, indentLevel, ((HierarchicalConfiguration) value).getRoot());
380 }
381 else if (value instanceof Configuration)
382 {
383 // display a flat Configuration as a dictionary
384 out.println(padding + "<dict>");
385
386 Configuration config = (Configuration) value;
387 Iterator it = config.getKeys();
388 while (it.hasNext())
389 {
390 // create a node for each property
391 String key = (String) it.next();
392 Node node = new Node(key);
393 node.setValue(config.getProperty(key));
394
395 // print the node
396 printNode(out, indentLevel + 1, node);
397
398 if (it.hasNext())
399 {
400 out.println();
401 }
402 }
403 out.println(padding + "</dict>");
404 }
405 else if (value instanceof Map)
406 {
407 // display a Map as a dictionary
408 Map map = (Map) value;
409 printValue(out, indentLevel, new MapConfiguration(map));
410 }
411 else if (value instanceof byte[])
412 {
413 String base64 = new String(Base64.encodeBase64((byte[]) value));
414 out.println(padding + "<data>" + StringEscapeUtils.escapeXml(base64) + "</data>");
415 }
416 else if (value != null)
417 {
418 out.println(padding + "<string>" + StringEscapeUtils.escapeXml(String.valueOf(value)) + "</string>");
419 }
420 else
421 {
422 out.println(padding + "<string/>");
423 }
424 }
425
426 /**
427 * Helper method for initializing the configuration's root node.
428 */
429 private void initRoot()
430 {
431 setRootNode(new PListNode());
432 }
433
434 /**
435 * SAX Handler to build the configuration nodes while the document is being parsed.
436 */
437 private static class XMLPropertyListHandler extends DefaultHandler
438 {
439 /** The buffer containing the text node being read */
440 private StringBuffer buffer = new StringBuffer();
441
442 /** The stack of configuration nodes */
443 private List stack = new ArrayList();
444
445 public XMLPropertyListHandler(Node root)
446 {
447 push(root);
448 }
449
450 /**
451 * Return the node on the top of the stack.
452 */
453 private Node peek()
454 {
455 if (!stack.isEmpty())
456 {
457 return (Node) stack.get(stack.size() - 1);
458 }
459 else
460 {
461 return null;
462 }
463 }
464
465 /**
466 * Remove and return the node on the top of the stack.
467 */
468 private Node pop()
469 {
470 if (!stack.isEmpty())
471 {
472 return (Node) stack.remove(stack.size() - 1);
473 }
474 else
475 {
476 return null;
477 }
478 }
479
480 /**
481 * Put a node on the top of the stack.
482 */
483 private void push(Node node)
484 {
485 stack.add(node);
486 }
487
488 public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException
489 {
490 if ("array".equals(qName))
491 {
492 push(new ArrayNode());
493 }
494 else if ("dict".equals(qName))
495 {
496 if (peek() instanceof ArrayNode)
497 {
498 // create the configuration
499 XMLPropertyListConfiguration config = new XMLPropertyListConfiguration();
500
501 // add it to the ArrayNode
502 ArrayNode node = (ArrayNode) peek();
503 node.addValue(config);
504
505 // push the root on the stack
506 push(config.getRoot());
507 }
508 }
509 }
510
511 public void endElement(String uri, String localName, String qName) throws SAXException
512 {
513 if ("key".equals(qName))
514 {
515 // create a new node, link it to its parent and push it on the stack
516 PListNode node = new PListNode();
517 node.setName(buffer.toString());
518 peek().addChild(node);
519 push(node);
520 }
521 else if ("dict".equals(qName))
522 {
523 // remove the root of the XMLPropertyListConfiguration previously pushed on the stack
524 pop();
525 }
526 else
527 {
528 if ("string".equals(qName))
529 {
530 ((PListNode) peek()).addValue(buffer.toString());
531 }
532 else if ("integer".equals(qName))
533 {
534 ((PListNode) peek()).addIntegerValue(buffer.toString());
535 }
536 else if ("real".equals(qName))
537 {
538 ((PListNode) peek()).addRealValue(buffer.toString());
539 }
540 else if ("true".equals(qName))
541 {
542 ((PListNode) peek()).addTrueValue();
543 }
544 else if ("false".equals(qName))
545 {
546 ((PListNode) peek()).addFalseValue();
547 }
548 else if ("data".equals(qName))
549 {
550 ((PListNode) peek()).addDataValue(buffer.toString());
551 }
552 else if ("date".equals(qName))
553 {
554 ((PListNode) peek()).addDateValue(buffer.toString());
555 }
556 else if ("array".equals(qName))
557 {
558 ArrayNode array = (ArrayNode) pop();
559 ((PListNode) peek()).addList(array);
560 }
561
562 // remove the plist node on the stack once the value has been parsed,
563 // array nodes remains on the stack for the next values in the list
564 if (!(peek() instanceof ArrayNode))
565 {
566 pop();
567 }
568 }
569
570 buffer.setLength(0);
571 }
572
573 public void characters(char[] ch, int start, int length) throws SAXException
574 {
575 buffer.append(ch, start, length);
576 }
577 }
578
579 /**
580 * Node extension with addXXX methods to parse the typed data passed by the SAX handler.
581 * <b>Do not use this class !</b> It is used internally by XMLPropertyConfiguration
582 * to parse the configuration file, it may be removed at any moment in the future.
583 */
584 public static class PListNode extends Node
585 {
586 /**
587 * The serial version UID.
588 */
589 private static final long serialVersionUID = -7614060264754798317L;
590
591 /** The MacOS format of dates in plist files. */
592 private static DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
593 static
594 {
595 format.setTimeZone(TimeZone.getTimeZone("UTC"));
596 }
597
598 /** The GNUstep format of dates in plist files. */
599 private static DateFormat gnustepFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
600
601 /**
602 * Update the value of the node. If the existing value is null, it's
603 * replaced with the new value. If the existing value is a list, the
604 * specified value is appended to the list. If the existing value is
605 * not null, a list with the two values is built.
606 *
607 * @param value the value to be added
608 */
609 public void addValue(Object value)
610 {
611 if (getValue() == null)
612 {
613 setValue(value);
614 }
615 else if (getValue() instanceof Collection)
616 {
617 Collection collection = (Collection) getValue();
618 collection.add(value);
619 }
620 else
621 {
622 List list = new ArrayList();
623 list.add(getValue());
624 list.add(value);
625 setValue(list);
626 }
627 }
628
629 /**
630 * Parse the specified string as a date and add it to the values of the node.
631 *
632 * @param value the value to be added
633 */
634 public void addDateValue(String value)
635 {
636 try
637 {
638 if (value.indexOf(' ') != -1)
639 {
640 // parse the date using the GNUstep format
641 synchronized (gnustepFormat)
642 {
643 addValue(gnustepFormat.parse(value));
644 }
645 }
646 else
647 {
648 // parse the date using the MacOS X format
649 synchronized (format)
650 {
651 addValue(format.parse(value));
652 }
653 }
654 }
655 catch (ParseException e)
656 {
657 // ignore
658 ;
659 }
660 }
661
662 /**
663 * Parse the specified string as a byte array in base 64 format
664 * and add it to the values of the node.
665 *
666 * @param value the value to be added
667 */
668 public void addDataValue(String value)
669 {
670 addValue(Base64.decodeBase64(value.getBytes()));
671 }
672
673 /**
674 * Parse the specified string as an Interger and add it to the values of the node.
675 *
676 * @param value the value to be added
677 */
678 public void addIntegerValue(String value)
679 {
680 addValue(new BigInteger(value));
681 }
682
683 /**
684 * Parse the specified string as a Double and add it to the values of the node.
685 *
686 * @param value the value to be added
687 */
688 public void addRealValue(String value)
689 {
690 addValue(new BigDecimal(value));
691 }
692
693 /**
694 * Add a boolean value 'true' to the values of the node.
695 */
696 public void addTrueValue()
697 {
698 addValue(Boolean.TRUE);
699 }
700
701 /**
702 * Add a boolean value 'false' to the values of the node.
703 */
704 public void addFalseValue()
705 {
706 addValue(Boolean.FALSE);
707 }
708
709 /**
710 * Add a sublist to the values of the node.
711 *
712 * @param node the node whose value will be added to the current node value
713 */
714 public void addList(ArrayNode node)
715 {
716 addValue(node.getValue());
717 }
718 }
719
720 /**
721 * Container for array elements. <b>Do not use this class !</b>
722 * It is used internally by XMLPropertyConfiguration to parse the
723 * configuration file, it may be removed at any moment in the future.
724 */
725 public static class ArrayNode extends PListNode
726 {
727 /**
728 * The serial version UID.
729 */
730 private static final long serialVersionUID = 5586544306664205835L;
731
732 /** The list of values in the array. */
733 private List list = new ArrayList();
734
735 /**
736 * Add an object to the array.
737 *
738 * @param value the value to be added
739 */
740 public void addValue(Object value)
741 {
742 list.add(value);
743 }
744
745 /**
746 * Return the list of values in the array.
747 *
748 * @return the {@link List} of values
749 */
750 public Object getValue()
751 {
752 return list;
753 }
754 }
755 }