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