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: 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
172 Digester digester = new Digester();
173
174
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
185 digester.addRule("*/key", new ObjectCreateRule(PListNode.class)
186 {
187 public void end() throws Exception
188 {
189
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
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
229 digester.addFactoryCreate("*/array/dict", new AbstractObjectCreationFactory()
230 {
231 public Object createObject(Attributes attributes) throws Exception
232 {
233
234 XMLPropertyListConfiguration config = new XMLPropertyListConfiguration();
235
236
237 ArrayNode node = (ArrayNode) getDigester().peek();
238 node.addValue(config);
239
240
241 return config.getRoot();
242 }
243 });
244
245
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
390 out.println(padding + "<dict>");
391
392 Configuration config = (Configuration) value;
393 Iterator it = config.getKeys();
394 while (it.hasNext())
395 {
396
397 String key = (String) it.next();
398 Node node = new Node(key);
399 node.setValue(config.getProperty(key));
400
401
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
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 }