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