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