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