View Javadoc

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