View Javadoc

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.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.HashMap;
35  import java.util.Iterator;
36  import java.util.List;
37  import java.util.Map;
38  import java.util.TimeZone;
39  
40  import javax.xml.parsers.SAXParser;
41  import javax.xml.parsers.SAXParserFactory;
42  
43  import org.apache.commons.codec.binary.Base64;
44  import org.apache.commons.configuration.AbstractHierarchicalFileConfiguration;
45  import org.apache.commons.configuration.Configuration;
46  import org.apache.commons.configuration.ConfigurationException;
47  import org.apache.commons.configuration.HierarchicalConfiguration;
48  import org.apache.commons.configuration.MapConfiguration;
49  import org.apache.commons.configuration.tree.ConfigurationNode;
50  import org.apache.commons.lang.StringEscapeUtils;
51  import org.apache.commons.lang.StringUtils;
52  import org.xml.sax.Attributes;
53  import org.xml.sax.EntityResolver;
54  import org.xml.sax.InputSource;
55  import org.xml.sax.SAXException;
56  import org.xml.sax.helpers.DefaultHandler;
57  
58  /**
59   * Property list file (plist) in XML FORMAT as used by Mac OS X (http://www.apple.com/DTDs/PropertyList-1.0.dtd).
60   * This configuration doesn't support the binary FORMAT used in OS X 10.4.
61   *
62   * <p>Example:</p>
63   * <pre>
64   * &lt;?xml version="1.0"?>
65   * &lt;!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
66   * &lt;plist version="1.0">
67   *     &lt;dict>
68   *         &lt;key>string&lt;/key>
69   *         &lt;string>value1&lt;/string>
70   *
71   *         &lt;key>integer&lt;/key>
72   *         &lt;integer>12345&lt;/integer>
73   *
74   *         &lt;key>real&lt;/key>
75   *         &lt;real>-123.45E-1&lt;/real>
76   *
77   *         &lt;key>boolean&lt;/key>
78   *         &lt;true/>
79   *
80   *         &lt;key>date&lt;/key>
81   *         &lt;date>2005-01-01T12:00:00Z&lt;/date>
82   *
83   *         &lt;key>data&lt;/key>
84   *         &lt;data>RHJhY28gRG9ybWllbnMgTnVucXVhbSBUaXRpbGxhbmR1cw==&lt;/data>
85   *
86   *         &lt;key>array&lt;/key>
87   *         &lt;array>
88   *             &lt;string>value1&lt;/string>
89   *             &lt;string>value2&lt;/string>
90   *             &lt;string>value3&lt;/string>
91   *         &lt;/array>
92   *
93   *         &lt;key>dictionnary&lt;/key>
94   *         &lt;dict>
95   *             &lt;key>key1&lt;/key>
96   *             &lt;string>value1&lt;/string>
97   *             &lt;key>key2&lt;/key>
98   *             &lt;string>value2&lt;/string>
99   *             &lt;key>key3&lt;/key>
100  *             &lt;string>value3&lt;/string>
101  *         &lt;/dict>
102  *
103  *         &lt;key>nested&lt;/key>
104  *         &lt;dict>
105  *             &lt;key>node1&lt;/key>
106  *             &lt;dict>
107  *                 &lt;key>node2&lt;/key>
108  *                 &lt;dict>
109  *                     &lt;key>node3&lt;/key>
110  *                     &lt;string>value&lt;/string>
111  *                 &lt;/dict>
112  *             &lt;/dict>
113  *         &lt;/dict>
114  *
115  *     &lt;/dict>
116  * &lt;/plist>
117  * </pre>
118  *
119  * @since 1.2
120  *
121  * @author Emmanuel Bourg
122  * @version $Id: XMLPropertyListConfiguration.java 1368665 2012-08-02 19:48:26Z 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 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                     try
582                     {
583                         ((PListNode) peek()).addDateValue(buffer.toString());
584                     }
585                     catch (IllegalArgumentException iex)
586                     {
587                         getLogger().warn(
588                                 "Ignoring invalid date property " + buffer);
589                     }
590                 }
591                 else if ("array".equals(qName))
592                 {
593                     ArrayNode array = (ArrayNode) pop();
594                     ((PListNode) peek()).addList(array);
595                 }
596 
597                 // remove the plist node on the stack once the value has been parsed,
598                 // array nodes remains on the stack for the next values in the list
599                 if (!(peek() instanceof ArrayNode))
600                 {
601                     pop();
602                 }
603             }
604 
605             buffer.setLength(0);
606         }
607 
608         @Override
609         public void characters(char[] ch, int start, int length) throws SAXException
610         {
611             buffer.append(ch, start, length);
612         }
613     }
614 
615     /**
616      * Node extension with addXXX methods to parse the typed data passed by the SAX handler.
617      * <b>Do not use this class !</b> It is used internally by XMLPropertyConfiguration
618      * to parse the configuration file, it may be removed at any moment in the future.
619      */
620     public static class PListNode extends Node
621     {
622         /**
623          * The serial version UID.
624          */
625         private static final long serialVersionUID = -7614060264754798317L;
626 
627         /**
628          * The MacOS FORMAT of dates in plist files. Note: Because
629          * {@code SimpleDateFormat} is not thread-safe, each access has to be
630          * synchronized.
631          */
632         private static final DateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
633         static
634         {
635             FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
636         }
637 
638         /**
639          * The GNUstep FORMAT of dates in plist files. Note: Because
640          * {@code SimpleDateFormat} is not thread-safe, each access has to be
641          * synchronized.
642          */
643         private static final DateFormat GNUSTEP_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
644 
645         /**
646          * Update the value of the node. If the existing value is null, it's
647          * replaced with the new value. If the existing value is a list, the
648          * specified value is appended to the list. If the existing value is
649          * not null, a list with the two values is built.
650          *
651          * @param value the value to be added
652          */
653         public void addValue(Object value)
654         {
655             if (getValue() == null)
656             {
657                 setValue(value);
658             }
659             else if (getValue() instanceof Collection)
660             {
661                 // This is safe because we create the collections ourselves
662                 @SuppressWarnings("unchecked")
663                 Collection<Object> collection = (Collection<Object>) getValue();
664                 collection.add(value);
665             }
666             else
667             {
668                 List<Object> list = new ArrayList<Object>();
669                 list.add(getValue());
670                 list.add(value);
671                 setValue(list);
672             }
673         }
674 
675         /**
676          * Parse the specified string as a date and add it to the values of the node.
677          *
678          * @param value the value to be added
679          * @throws IllegalArgumentException if the date string cannot be parsed
680          */
681         public void addDateValue(String value)
682         {
683             try
684             {
685                 if (value.indexOf(' ') != -1)
686                 {
687                     // parse the date using the GNUstep FORMAT
688                     synchronized (GNUSTEP_FORMAT)
689                     {
690                         addValue(GNUSTEP_FORMAT.parse(value));
691                     }
692                 }
693                 else
694                 {
695                     // parse the date using the MacOS X FORMAT
696                     synchronized (FORMAT)
697                     {
698                         addValue(FORMAT.parse(value));
699                     }
700                 }
701             }
702             catch (ParseException e)
703             {
704                 throw new IllegalArgumentException(String.format(
705                         "'%s' cannot be parsed to a date!", value), e);
706             }
707         }
708 
709         /**
710          * Parse the specified string as a byte array in base 64 FORMAT
711          * and add it to the values of the node.
712          *
713          * @param value the value to be added
714          */
715         public void addDataValue(String value)
716         {
717             addValue(Base64.decodeBase64(value.getBytes()));
718         }
719 
720         /**
721          * Parse the specified string as an Interger and add it to the values of the node.
722          *
723          * @param value the value to be added
724          */
725         public void addIntegerValue(String value)
726         {
727             addValue(new BigInteger(value));
728         }
729 
730         /**
731          * Parse the specified string as a Double and add it to the values of the node.
732          *
733          * @param value the value to be added
734          */
735         public void addRealValue(String value)
736         {
737             addValue(new BigDecimal(value));
738         }
739 
740         /**
741          * Add a boolean value 'true' to the values of the node.
742          */
743         public void addTrueValue()
744         {
745             addValue(Boolean.TRUE);
746         }
747 
748         /**
749          * Add a boolean value 'false' to the values of the node.
750          */
751         public void addFalseValue()
752         {
753             addValue(Boolean.FALSE);
754         }
755 
756         /**
757          * Add a sublist to the values of the node.
758          *
759          * @param node the node whose value will be added to the current node value
760          */
761         public void addList(ArrayNode node)
762         {
763             addValue(node.getValue());
764         }
765     }
766 
767     /**
768      * Container for array elements. <b>Do not use this class !</b>
769      * It is used internally by XMLPropertyConfiguration to parse the
770      * configuration file, it may be removed at any moment in the future.
771      */
772     public static class ArrayNode extends PListNode
773     {
774         /**
775          * The serial version UID.
776          */
777         private static final long serialVersionUID = 5586544306664205835L;
778 
779         /** The list of values in the array. */
780         private List<Object> list = new ArrayList<Object>();
781 
782         /**
783          * Add an object to the array.
784          *
785          * @param value the value to be added
786          */
787         @Override
788         public void addValue(Object value)
789         {
790             list.add(value);
791         }
792 
793         /**
794          * Return the list of values in the array.
795          *
796          * @return the {@link List} of values
797          */
798         @Override
799         public Object getValue()
800         {
801             return list;
802         }
803     }
804 }