1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 * <?xml version="1.0"?>
58 * <!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
59 * <plist version="1.0">
60 * <dict>
61 * <key>string</key>
62 * <string>value1</string>
63 *
64 * <key>integer</key>
65 * <integer>12345</integer>
66 *
67 * <key>real</key>
68 * <real>-123.45E-1</real>
69 *
70 * <key>boolean</key>
71 * <true/>
72 *
73 * <key>date</key>
74 * <date>2005-01-01T12:00:00-0700</date>
75 *
76 * <key>data</key>
77 * <data>RHJhY28gRG9ybWllbnMgTnVucXVhbSBUaXRpbGxhbmR1cw==</data>
78 *
79 * <key>array</key>
80 * <array>
81 * <string>value1</string>
82 * <string>value2</string>
83 * <string>value3</string>
84 * </array>
85 *
86 * <key>dictionnary</key>
87 * <dict>
88 * <key>key1</key>
89 * <string>value1</string>
90 * <key>key2</key>
91 * <string>value2</string>
92 * <key>key3</key>
93 * <string>value3</string>
94 * </dict>
95 *
96 * <key>nested</key>
97 * <dict>
98 * <key>node1</key>
99 * <dict>
100 * <key>node2</key>
101 * <dict>
102 * <key>node3</key>
103 * <string>value</string>
104 * </dict>
105 * </dict>
106 * </dict>
107 *
108 * </dict>
109 * </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
185 Digester digester = new Digester();
186
187
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
198 digester.addRule("*/key", new ObjectCreateRule(PListNode.class)
199 {
200 public void end() throws Exception
201 {
202
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
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
242 digester.addFactoryCreate("*/array/dict", new AbstractObjectCreationFactory()
243 {
244 public Object createObject(Attributes attributes) throws Exception
245 {
246
247 XMLPropertyListConfiguration config = new XMLPropertyListConfiguration();
248
249
250 ArrayNode node = (ArrayNode) getDigester().peek();
251 node.addValue(config);
252
253
254 return config.getRoot();
255 }
256 });
257
258
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
403 out.println(padding + "<dict>");
404
405 Configuration config = (Configuration) value;
406 Iterator it = config.getKeys();
407 while (it.hasNext())
408 {
409
410 String key = (String) it.next();
411 Node node = new Node(key);
412 node.setValue(config.getProperty(key));
413
414
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
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
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 }