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: 439648 $, $Date: 2006-09-02 22:42:10 +0200 (Sa, 02 Sep 2006) $
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 and loads the property list from the specified file.
138      *
139      * @param fileName The name of the plist file to load.
140      * @throws org.apache.commons.configuration.ConfigurationException Error while loading the plist file
141      */
142     public XMLPropertyListConfiguration(String fileName) throws ConfigurationException
143     {
144         super(fileName);
145     }
146 
147     /***
148      * Creates and loads the property list from the specified file.
149      *
150      * @param file The plist file to load.
151      * @throws ConfigurationException Error while loading the plist file
152      */
153     public XMLPropertyListConfiguration(File file) throws ConfigurationException
154     {
155         super(file);
156     }
157 
158     /***
159      * Creates and loads the property list from the specified URL.
160      *
161      * @param url The location of the plist file to load.
162      * @throws ConfigurationException Error while loading the plist file
163      */
164     public XMLPropertyListConfiguration(URL url) throws ConfigurationException
165     {
166         super(url);
167     }
168 
169     public void load(Reader in) throws ConfigurationException
170     {
171         // set up the digester
172         Digester digester = new Digester();
173 
174         // set up the DTD validation
175         digester.setEntityResolver(new EntityResolver()
176         {
177             public InputSource resolveEntity(String publicId, String systemId)
178             {
179                 return new InputSource(getClass().getClassLoader().getResourceAsStream("PropertyList-1.0.dtd"));
180             }
181         });
182         digester.setValidating(true);
183 
184         // dictionary rules
185         digester.addRule("*/key", new ObjectCreateRule(PListNode.class)
186         {
187             public void end() throws Exception
188             {
189                 // leave the node on the stack to set the value
190             }
191         });
192 
193         digester.addCallMethod("*/key", "setName", 0);
194 
195         digester.addRule("*/dict/string", new SetNextAndPopRule("addChild"));
196         digester.addRule("*/dict/data", new SetNextAndPopRule("addChild"));
197         digester.addRule("*/dict/integer", new SetNextAndPopRule("addChild"));
198         digester.addRule("*/dict/real", new SetNextAndPopRule("addChild"));
199         digester.addRule("*/dict/true", new SetNextAndPopRule("addChild"));
200         digester.addRule("*/dict/false", new SetNextAndPopRule("addChild"));
201         digester.addRule("*/dict/date", new SetNextAndPopRule("addChild"));
202         digester.addRule("*/dict/dict", new SetNextAndPopRule("addChild"));
203 
204         digester.addCallMethod("*/dict/string", "addValue", 0);
205         digester.addCallMethod("*/dict/data", "addDataValue", 0);
206         digester.addCallMethod("*/dict/integer", "addIntegerValue", 0);
207         digester.addCallMethod("*/dict/real", "addRealValue", 0);
208         digester.addCallMethod("*/dict/true", "addTrueValue");
209         digester.addCallMethod("*/dict/false", "addFalseValue");
210         digester.addCallMethod("*/dict/date", "addDateValue", 0);
211 
212         // rules for arrays
213         digester.addRule("*/dict/array", new SetNextAndPopRule("addChild"));
214         digester.addRule("*/dict/array", new ObjectCreateRule(ArrayNode.class));
215         digester.addSetNext("*/dict/array", "addList");
216 
217         digester.addRule("*/array/array", new ObjectCreateRule(ArrayNode.class));
218         digester.addSetNext("*/array/array", "addList");
219 
220         digester.addCallMethod("*/array/string", "addValue", 0);
221         digester.addCallMethod("*/array/data", "addDataValue", 0);
222         digester.addCallMethod("*/array/integer", "addIntegerValue", 0);
223         digester.addCallMethod("*/array/real", "addRealValue", 0);
224         digester.addCallMethod("*/array/true", "addTrueValue");
225         digester.addCallMethod("*/array/false", "addFalseValue");
226         digester.addCallMethod("*/array/date", "addDateValue", 0);
227 
228         // rule for a dictionary in an array
229         digester.addFactoryCreate("*/array/dict", new AbstractObjectCreationFactory()
230         {
231             public Object createObject(Attributes attributes) throws Exception
232             {
233                 // create the configuration
234                 XMLPropertyListConfiguration config = new XMLPropertyListConfiguration();
235 
236                 // add it to the ArrayNode
237                 ArrayNode node = (ArrayNode) getDigester().peek();
238                 node.addValue(config);
239 
240                 // push the root on the stack
241                 return config.getRoot();
242             }
243         });
244 
245         // parse the file
246         digester.push(getRoot());
247         try
248         {
249             digester.parse(in);
250         }
251         catch (Exception e)
252         {
253             throw new ConfigurationException("Unable to parse the configuration file", e);
254         }
255     }
256 
257     /***
258      * Digester rule that sets the object on the stack to the n-1 object
259      * and remove both of them from the stack. This rule is used to remove
260      * the configuration node from the stack once its value has been parsed.
261      */
262     private class SetNextAndPopRule extends SetNextRule
263     {
264         public SetNextAndPopRule(String methodName)
265         {
266             super(methodName);
267         }
268 
269         public void end(String namespace, String name) throws Exception
270         {
271             super.end(namespace, name);
272             digester.pop();
273         }
274     }
275 
276     public void save(Writer out) throws ConfigurationException
277     {
278         PrintWriter writer = new PrintWriter(out);
279 
280         if (getEncoding() != null)
281         {
282             writer.println("<?xml version=\"1.0\" encoding=\"" + getEncoding() + "\"?>");
283         }
284         else
285         {
286             writer.println("<?xml version=\"1.0\"?>");
287         }
288 
289         writer.println("<!DOCTYPE plist SYSTEM \"file://localhost/System/Library/DTDs/PropertyList.dtd\">");
290         writer.println("<plist version=\"1.0\">");
291 
292         printNode(writer, 1, getRoot());
293 
294         writer.println("</plist>");
295         writer.flush();
296     }
297 
298     /***
299      * Append a node to the writer, indented according to a specific level.
300      */
301     private void printNode(PrintWriter out, int indentLevel, Node node)
302     {
303         String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
304 
305         if (node.getName() != null)
306         {
307             out.println(padding + "<key>" + StringEscapeUtils.escapeXml(node.getName()) + "</key>");
308         }
309 
310         List children = node.getChildren();
311         if (!children.isEmpty())
312         {
313             out.println(padding + "<dict>");
314 
315             Iterator it = children.iterator();
316             while (it.hasNext())
317             {
318                 Node child = (Node) it.next();
319                 printNode(out, indentLevel + 1, child);
320 
321                 if (it.hasNext())
322                 {
323                     out.println();
324                 }
325             }
326 
327             out.println(padding + "</dict>");
328         }
329         else
330         {
331             Object value = node.getValue();
332             printValue(out, indentLevel, value);
333         }
334     }
335 
336     /***
337      * Append a value to the writer, indented according to a specific level.
338      */
339     private void printValue(PrintWriter out, int indentLevel, Object value)
340     {
341         String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
342 
343         if (value instanceof Date)
344         {
345             out.println(padding + "<date>" + PListNode.format.format((Date) value) + "</date>");
346         }
347         else if (value instanceof Calendar)
348         {
349             printValue(out, indentLevel, ((Calendar) value).getTime());
350         }
351         else if (value instanceof Number)
352         {
353             if (value instanceof Double || value instanceof Float || value instanceof BigDecimal)
354             {
355                 out.println(padding + "<real>" + value.toString() + "</real>");
356             }
357             else
358             {
359                 out.println(padding + "<integer>" + value.toString() + "</integer>");
360             }
361         }
362         else if (value instanceof Boolean)
363         {
364             if (((Boolean) value).booleanValue())
365             {
366                 out.println(padding + "<true/>");
367             }
368             else
369             {
370                 out.println(padding + "<false/>");
371             }
372         }
373         else if (value instanceof List)
374         {
375             out.println(padding + "<array>");
376             Iterator it = ((List) value).iterator();
377             while (it.hasNext())
378             {
379                 printValue(out, indentLevel + 1, it.next());
380             }
381             out.println(padding + "</array>");
382         }
383         else if (value instanceof HierarchicalConfiguration)
384         {
385             printNode(out, indentLevel, ((HierarchicalConfiguration) value).getRoot());
386         }
387         else if (value instanceof Configuration)
388         {
389             // display a flat Configuration as a dictionary
390             out.println(padding + "<dict>");
391 
392             Configuration config = (Configuration) value;
393             Iterator it = config.getKeys();
394             while (it.hasNext())
395             {
396                 // create a node for each property
397                 String key = (String) it.next();
398                 Node node = new Node(key);
399                 node.setValue(config.getProperty(key));
400 
401                 // print the node
402                 printNode(out, indentLevel + 1, node);
403 
404                 if (it.hasNext())
405                 {
406                     out.println();
407                 }
408             }
409             out.println(padding + "</dict>");
410         }
411         else if (value instanceof Map)
412         {
413             // display a Map as a dictionary
414             Map map = (Map) value;
415             printValue(out, indentLevel, new MapConfiguration(map));
416         }
417         else if (value instanceof byte[])
418         {
419             String base64 = new String(Base64.encodeBase64((byte[]) value));
420             out.println(padding + "<data>" + StringEscapeUtils.escapeXml(base64) + "</data>");
421         }
422         else
423         {
424             out.println(padding + "<string>" + StringEscapeUtils.escapeXml(String.valueOf(value)) + "</string>");
425         }
426     }
427 
428 
429     /***
430      * Node extension with addXXX methods to parse the typed data passed by Digester.
431      * <b>Do not use this class !</b> It is used internally by XMLPropertyConfiguration
432      * to parse the configuration file, it may be removed at any moment in the future.
433      */
434     public static class PListNode extends Node
435     {
436         /***
437          * The serial version UID.
438          */
439         private static final long serialVersionUID = -7614060264754798317L;
440 
441         /*** The standard format of dates in plist files. */
442         private static DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
443 
444         /***
445          * Update the value of the node. If the existing value is null, it's
446          * replaced with the new value. If the existing value is a list, the
447          * specified value is appended to the list. If the existing value is
448          * not null, a list with the two values is built.
449          *
450          * @param value the value to be added
451          */
452         public void addValue(Object value)
453         {
454             if (getValue() == null)
455             {
456                 setValue(value);
457             }
458             else if (getValue() instanceof List)
459             {
460                 List list = (List) getValue();
461                 list.add(value);
462             }
463             else
464             {
465                 List list = new ArrayList();
466                 list.add(getValue());
467                 list.add(value);
468                 setValue(list);
469             }
470         }
471 
472         /***
473          * Parse the specified string as a date and add it to the values of the node.
474          *
475          * @param value the value to be added
476          */
477         public void addDateValue(String value)
478         {
479             try
480             {
481                 addValue(format.parse(value));
482             }
483             catch (ParseException e)
484             {
485                 e.printStackTrace();
486             }
487         }
488 
489         /***
490          * Parse the specified string as a byte array in base 64 format
491          * and add it to the values of the node.
492          *
493          * @param value the value to be added
494          */
495         public void addDataValue(String value)
496         {
497             addValue(Base64.decodeBase64(value.getBytes()));
498         }
499 
500         /***
501          * Parse the specified string as an Interger and add it to the values of the node.
502          *
503          * @param value the value to be added
504          */
505         public void addIntegerValue(String value)
506         {
507             addValue(new Integer(value));
508         }
509 
510         /***
511          * Parse the specified string as a Double and add it to the values of the node.
512          *
513          * @param value the value to be added
514          */
515         public void addRealValue(String value)
516         {
517             addValue(new Double(value));
518         }
519 
520         /***
521          * Add a boolean value 'true' to the values of the node.
522          */
523         public void addTrueValue()
524         {
525             addValue(Boolean.TRUE);
526         }
527 
528         /***
529          * Add a boolean value 'false' to the values of the node.
530          */
531         public void addFalseValue()
532         {
533             addValue(Boolean.FALSE);
534         }
535 
536         /***
537          * Add a sublist to the values of the node.
538          *
539          * @param node the node whose value will be added to the current node value
540          */
541         public void addList(ArrayNode node)
542         {
543             addValue(node.getValue());
544         }
545     }
546 
547     /***
548      * Container for array elements. <b>Do not use this class !</b>
549      * It is used internally by XMLPropertyConfiguration to parse the
550      * configuration file, it may be removed at any moment in the future.
551      */
552     public static class ArrayNode extends PListNode
553     {
554         /***
555          * The serial version UID.
556          */
557         private static final long serialVersionUID = 5586544306664205835L;
558 
559         /*** The list of values in the array. */
560         private List list = new ArrayList();
561 
562         /***
563          * Add an object to the array.
564          *
565          * @param value the value to be added
566          */
567         public void addValue(Object value)
568         {
569             list.add(value);
570         }
571 
572         /***
573          * Return the list of values in the array.
574          *
575          * @return the {@link List} of values
576          */
577         public Object getValue()
578         {
579             return list;
580         }
581     }
582 }