1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 * <?xml version="1.0"?>
57 * <!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
58 * <plist version="1.0">
59 * <dict>
60 * <key>string</key>
61 * <string>value1</string>
62 *
63 * <key>integer</key>
64 * <integer>12345</integer>
65 *
66 * <key>real</key>
67 * <real>-123.45E-1</real>
68 *
69 * <key>boolean</key>
70 * <true/>
71 *
72 * <key>date</key>
73 * <date>2005-01-01T12:00:00-0700</date>
74 *
75 * <key>data</key>
76 * <data>RHJhY28gRG9ybWllbnMgTnVucXVhbSBUaXRpbGxhbmR1cw==</data>
77 *
78 * <key>array</key>
79 * <array>
80 * <string>value1</string>
81 * <string>value2</string>
82 * <string>value3</string>
83 * </array>
84 *
85 * <key>dictionnary</key>
86 * <dict>
87 * <key>key1</key>
88 * <string>value1</string>
89 * <key>key2</key>
90 * <string>value2</string>
91 * <key>key3</key>
92 * <string>value3</string>
93 * </dict>
94 *
95 * <key>nested</key>
96 * <dict>
97 * <key>node1</key>
98 * <dict>
99 * <key>node2</key>
100 * <dict>
101 * <key>node3</key>
102 * <string>value</string>
103 * </dict>
104 * </dict>
105 * </dict>
106 *
107 * </dict>
108 * </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
166 Digester digester = new Digester();
167
168
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
179 digester.addRule("*/key", new ObjectCreateRule(PListNode.class)
180 {
181 public void end() throws Exception
182 {
183
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
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
223 digester.addFactoryCreate("*/array/dict", new AbstractObjectCreationFactory()
224 {
225 public Object createObject(Attributes attributes) throws Exception
226 {
227
228 XMLPropertyListConfiguration config = new XMLPropertyListConfiguration();
229
230
231 ArrayNode node = (ArrayNode) getDigester().peek();
232 node.addValue(config);
233
234
235 return config.getRoot();
236 }
237 });
238
239
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
384 out.println(padding + "<dict>");
385
386 Configuration config = (Configuration) value;
387 Iterator it = config.getKeys();
388 while (it.hasNext())
389 {
390
391 String key = (String) it.next();
392 Node node = new Node(key);
393 node.setValue(config.getProperty(key));
394
395
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
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 }