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.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   * &lt;?xml version="1.0"?>
63   * &lt;!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
64   * &lt;plist version="1.0">
65   *     &lt;dict>
66   *         &lt;key>string&lt;/key>
67   *         &lt;string>value1&lt;/string>
68   *
69   *         &lt;key>integer&lt;/key>
70   *         &lt;integer>12345&lt;/integer>
71   *
72   *         &lt;key>real&lt;/key>
73   *         &lt;real>-123.45E-1&lt;/real>
74   *
75   *         &lt;key>boolean&lt;/key>
76   *         &lt;true/>
77   *
78   *         &lt;key>date&lt;/key>
79   *         &lt;date>2005-01-01T12:00:00Z&lt;/date>
80   *
81   *         &lt;key>data&lt;/key>
82   *         &lt;data>RHJhY28gRG9ybWllbnMgTnVucXVhbSBUaXRpbGxhbmR1cw==&lt;/data>
83   *
84   *         &lt;key>array&lt;/key>
85   *         &lt;array>
86   *             &lt;string>value1&lt;/string>
87   *             &lt;string>value2&lt;/string>
88   *             &lt;string>value3&lt;/string>
89   *         &lt;/array>
90   *
91   *         &lt;key>dictionnary&lt;/key>
92   *         &lt;dict>
93   *             &lt;key>key1&lt;/key>
94   *             &lt;string>value1&lt;/string>
95   *             &lt;key>key2&lt;/key>
96   *             &lt;string>value2&lt;/string>
97   *             &lt;key>key3&lt;/key>
98   *             &lt;string>value3&lt;/string>
99   *         &lt;/dict>
100  *
101  *         &lt;key>nested&lt;/key>
102  *         &lt;dict>
103  *             &lt;key>node1&lt;/key>
104  *             &lt;dict>
105  *                 &lt;key>node2&lt;/key>
106  *                 &lt;dict>
107  *                     &lt;key>node3&lt;/key>
108  *                     &lt;string>value&lt;/string>
109  *                 &lt;/dict>
110  *             &lt;/dict>
111  *         &lt;/dict>
112  *
113  *     &lt;/dict>
114  * &lt;/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         // special case for byte arrays, they must be stored as is in the configuration
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         // set up the DTD validation
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         // parse the file
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             // display a flat Configuration as a dictionary
371             out.println(padding + "<dict>");
372 
373             Configuration config = (Configuration) value;
374             Iterator it = config.getKeys();
375             while (it.hasNext())
376             {
377                 // create a node for each property
378                 String key = (String) it.next();
379                 Node node = new Node(key);
380                 node.setValue(config.getProperty(key));
381 
382                 // print the node
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             // display a Map as a dictionary
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                     // create the configuration
474                     XMLPropertyListConfiguration config = new XMLPropertyListConfiguration();
475 
476                     // add it to the ArrayNode
477                     ArrayNode node = (ArrayNode) peek();
478                     node.addValue(config);
479 
480                     // push the root on the stack
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                 // create a new node, link it to its parent and push it on the stack
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                 // remove the root of the XMLPropertyListConfiguration previously pushed on the stack
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                 // remove the plist node on the stack once the value has been parsed,
538                 // array nodes remains on the stack for the next values in the list
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                     // parse the date using the GNUstep format
616                     synchronized (gnustepFormat)
617                     {
618                         addValue(gnustepFormat.parse(value));
619                     }
620                 }
621                 else
622                 {
623                     // parse the date using the MacOS X format
624                     synchronized (format)
625                     {
626                         addValue(format.parse(value));
627                     }
628                 }
629             }
630             catch (ParseException e)
631             {
632                 // ignore
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 }