001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017:
018: package org.apache.commons.configuration.plist;
019:
020: import java.io.File;
021: import java.io.PrintWriter;
022: import java.io.Reader;
023: import java.io.Writer;
024: import java.math.BigDecimal;
025: import java.net.URL;
026: import java.text.DateFormat;
027: import java.text.ParseException;
028: import java.text.SimpleDateFormat;
029: import java.util.ArrayList;
030: import java.util.Calendar;
031: import java.util.Date;
032: import java.util.Iterator;
033: import java.util.List;
034: import java.util.Map;
035:
036: import org.apache.commons.codec.binary.Base64;
037: import org.apache.commons.configuration.AbstractHierarchicalFileConfiguration;
038: import org.apache.commons.configuration.Configuration;
039: import org.apache.commons.configuration.ConfigurationException;
040: import org.apache.commons.configuration.HierarchicalConfiguration;
041: import org.apache.commons.configuration.MapConfiguration;
042: import org.apache.commons.digester.AbstractObjectCreationFactory;
043: import org.apache.commons.digester.Digester;
044: import org.apache.commons.digester.ObjectCreateRule;
045: import org.apache.commons.digester.SetNextRule;
046: import org.apache.commons.lang.StringEscapeUtils;
047: import org.apache.commons.lang.StringUtils;
048: import org.xml.sax.Attributes;
049: import org.xml.sax.EntityResolver;
050: import org.xml.sax.InputSource;
051:
052: /**
053: * Mac OS X configuration file (http://www.apple.com/DTDs/PropertyList-1.0.dtd).
054: *
055: * <p>Example:</p>
056: * <pre>
057: * <?xml version="1.0"?>
058: * <!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
059: * <plist version="1.0">
060: * <dict>
061: * <key>string</key>
062: * <string>value1</string>
063: *
064: * <key>integer</key>
065: * <integer>12345</integer>
066: *
067: * <key>real</key>
068: * <real>-123.45E-1</real>
069: *
070: * <key>boolean</key>
071: * <true/>
072: *
073: * <key>date</key>
074: * <date>2005-01-01T12:00:00-0700</date>
075: *
076: * <key>data</key>
077: * <data>RHJhY28gRG9ybWllbnMgTnVucXVhbSBUaXRpbGxhbmR1cw==</data>
078: *
079: * <key>array</key>
080: * <array>
081: * <string>value1</string>
082: * <string>value2</string>
083: * <string>value3</string>
084: * </array>
085: *
086: * <key>dictionnary</key>
087: * <dict>
088: * <key>key1</key>
089: * <string>value1</string>
090: * <key>key2</key>
091: * <string>value2</string>
092: * <key>key3</key>
093: * <string>value3</string>
094: * </dict>
095: *
096: * <key>nested</key>
097: * <dict>
098: * <key>node1</key>
099: * <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
118: AbstractHierarchicalFileConfiguration {
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: * Creates a new instance of <code>XMLPropertyListConfiguration</code> and
137: * copies the content of the specified configuration into this object.
138: *
139: * @param c the configuration to copy
140: * @since 1.4
141: */
142: public XMLPropertyListConfiguration(HierarchicalConfiguration c) {
143: super (c);
144: }
145:
146: /**
147: * Creates and loads the property list from the specified file.
148: *
149: * @param fileName The name of the plist file to load.
150: * @throws org.apache.commons.configuration.ConfigurationException Error
151: * while loading the plist file
152: */
153: public XMLPropertyListConfiguration(String fileName)
154: throws ConfigurationException {
155: super (fileName);
156: }
157:
158: /**
159: * Creates and loads the property list from the specified file.
160: *
161: * @param file The plist file to load.
162: * @throws ConfigurationException Error while loading the plist file
163: */
164: public XMLPropertyListConfiguration(File file)
165: throws ConfigurationException {
166: super (file);
167: }
168:
169: /**
170: * Creates and loads the property list from the specified URL.
171: *
172: * @param url The location of the plist file to load.
173: * @throws ConfigurationException Error while loading the plist file
174: */
175: public XMLPropertyListConfiguration(URL url)
176: throws ConfigurationException {
177: super (url);
178: }
179:
180: public void load(Reader in) throws ConfigurationException {
181: // set up the digester
182: Digester digester = new Digester();
183:
184: // set up the DTD validation
185: digester.setEntityResolver(new EntityResolver() {
186: public InputSource resolveEntity(String publicId,
187: String systemId) {
188: return new InputSource(getClass().getClassLoader()
189: .getResourceAsStream("PropertyList-1.0.dtd"));
190: }
191: });
192: digester.setValidating(true);
193:
194: // dictionary rules
195: digester.addRule("*/key",
196: new ObjectCreateRule(PListNode.class) {
197: public void end() throws Exception {
198: // leave the node on the stack to set the value
199: }
200: });
201:
202: digester.addCallMethod("*/key", "setName", 0);
203:
204: digester.addRule("*/dict/string", new SetNextAndPopRule(
205: "addChild"));
206: digester.addRule("*/dict/data", new SetNextAndPopRule(
207: "addChild"));
208: digester.addRule("*/dict/integer", new SetNextAndPopRule(
209: "addChild"));
210: digester.addRule("*/dict/real", new SetNextAndPopRule(
211: "addChild"));
212: digester.addRule("*/dict/true", new SetNextAndPopRule(
213: "addChild"));
214: digester.addRule("*/dict/false", new SetNextAndPopRule(
215: "addChild"));
216: digester.addRule("*/dict/date", new SetNextAndPopRule(
217: "addChild"));
218: digester.addRule("*/dict/dict", new SetNextAndPopRule(
219: "addChild"));
220:
221: digester.addCallMethod("*/dict/string", "addValue", 0);
222: digester.addCallMethod("*/dict/data", "addDataValue", 0);
223: digester.addCallMethod("*/dict/integer", "addIntegerValue", 0);
224: digester.addCallMethod("*/dict/real", "addRealValue", 0);
225: digester.addCallMethod("*/dict/true", "addTrueValue");
226: digester.addCallMethod("*/dict/false", "addFalseValue");
227: digester.addCallMethod("*/dict/date", "addDateValue", 0);
228:
229: // rules for arrays
230: digester.addRule("*/dict/array", new SetNextAndPopRule(
231: "addChild"));
232: digester.addRule("*/dict/array", new ObjectCreateRule(
233: ArrayNode.class));
234: digester.addSetNext("*/dict/array", "addList");
235:
236: digester.addRule("*/array/array", new ObjectCreateRule(
237: ArrayNode.class));
238: digester.addSetNext("*/array/array", "addList");
239:
240: digester.addCallMethod("*/array/string", "addValue", 0);
241: digester.addCallMethod("*/array/data", "addDataValue", 0);
242: digester.addCallMethod("*/array/integer", "addIntegerValue", 0);
243: digester.addCallMethod("*/array/real", "addRealValue", 0);
244: digester.addCallMethod("*/array/true", "addTrueValue");
245: digester.addCallMethod("*/array/false", "addFalseValue");
246: digester.addCallMethod("*/array/date", "addDateValue", 0);
247:
248: // rule for a dictionary in an array
249: digester.addFactoryCreate("*/array/dict",
250: new AbstractObjectCreationFactory() {
251: public Object createObject(Attributes attributes)
252: throws Exception {
253: // create the configuration
254: XMLPropertyListConfiguration config = new XMLPropertyListConfiguration();
255:
256: // add it to the ArrayNode
257: ArrayNode node = (ArrayNode) getDigester()
258: .peek();
259: node.addValue(config);
260:
261: // push the root on the stack
262: return config.getRoot();
263: }
264: });
265:
266: // parse the file
267: digester.push(getRoot());
268: try {
269: digester.parse(in);
270: } catch (Exception e) {
271: throw new ConfigurationException(
272: "Unable to parse the configuration file", e);
273: }
274: }
275:
276: /**
277: * Digester rule that sets the object on the stack to the n-1 object
278: * and remove both of them from the stack. This rule is used to remove
279: * the configuration node from the stack once its value has been parsed.
280: */
281: private class SetNextAndPopRule extends SetNextRule {
282: public SetNextAndPopRule(String methodName) {
283: super (methodName);
284: }
285:
286: public void end(String namespace, String name) throws Exception {
287: super .end(namespace, name);
288: digester.pop();
289: }
290: }
291:
292: public void save(Writer out) throws ConfigurationException {
293: PrintWriter writer = new PrintWriter(out);
294:
295: if (getEncoding() != null) {
296: writer.println("<?xml version=\"1.0\" encoding=\""
297: + getEncoding() + "\"?>");
298: } else {
299: writer.println("<?xml version=\"1.0\"?>");
300: }
301:
302: writer
303: .println("<!DOCTYPE plist SYSTEM \"file://localhost/System/Library/DTDs/PropertyList.dtd\">");
304: writer.println("<plist version=\"1.0\">");
305:
306: printNode(writer, 1, getRoot());
307:
308: writer.println("</plist>");
309: writer.flush();
310: }
311:
312: /**
313: * Append a node to the writer, indented according to a specific level.
314: */
315: private void printNode(PrintWriter out, int indentLevel, Node node) {
316: String padding = StringUtils.repeat(" ", indentLevel
317: * INDENT_SIZE);
318:
319: if (node.getName() != null) {
320: out.println(padding + "<key>"
321: + StringEscapeUtils.escapeXml(node.getName())
322: + "</key>");
323: }
324:
325: List children = node.getChildren();
326: if (!children.isEmpty()) {
327: out.println(padding + "<dict>");
328:
329: Iterator it = children.iterator();
330: while (it.hasNext()) {
331: Node child = (Node) it.next();
332: printNode(out, indentLevel + 1, child);
333:
334: if (it.hasNext()) {
335: out.println();
336: }
337: }
338:
339: out.println(padding + "</dict>");
340: } else {
341: Object value = node.getValue();
342: printValue(out, indentLevel, value);
343: }
344: }
345:
346: /**
347: * Append a value to the writer, indented according to a specific level.
348: */
349: private void printValue(PrintWriter out, int indentLevel,
350: Object value) {
351: String padding = StringUtils.repeat(" ", indentLevel
352: * INDENT_SIZE);
353:
354: if (value instanceof Date) {
355: out
356: .println(padding + "<date>"
357: + PListNode.format.format((Date) value)
358: + "</date>");
359: } else if (value instanceof Calendar) {
360: printValue(out, indentLevel, ((Calendar) value).getTime());
361: } else if (value instanceof Number) {
362: if (value instanceof Double || value instanceof Float
363: || value instanceof BigDecimal) {
364: out.println(padding + "<real>" + value.toString()
365: + "</real>");
366: } else {
367: out.println(padding + "<integer>" + value.toString()
368: + "</integer>");
369: }
370: } else if (value instanceof Boolean) {
371: if (((Boolean) value).booleanValue()) {
372: out.println(padding + "<true/>");
373: } else {
374: out.println(padding + "<false/>");
375: }
376: } else if (value instanceof List) {
377: out.println(padding + "<array>");
378: Iterator it = ((List) value).iterator();
379: while (it.hasNext()) {
380: printValue(out, indentLevel + 1, it.next());
381: }
382: out.println(padding + "</array>");
383: } else if (value instanceof HierarchicalConfiguration) {
384: printNode(out, indentLevel,
385: ((HierarchicalConfiguration) value).getRoot());
386: } else if (value instanceof Configuration) {
387: // display a flat Configuration as a dictionary
388: out.println(padding + "<dict>");
389:
390: Configuration config = (Configuration) value;
391: Iterator it = config.getKeys();
392: while (it.hasNext()) {
393: // create a node for each property
394: String key = (String) it.next();
395: Node node = new Node(key);
396: node.setValue(config.getProperty(key));
397:
398: // print the node
399: printNode(out, indentLevel + 1, node);
400:
401: if (it.hasNext()) {
402: out.println();
403: }
404: }
405: out.println(padding + "</dict>");
406: } else if (value instanceof Map) {
407: // display a Map as a dictionary
408: Map map = (Map) value;
409: printValue(out, indentLevel, new MapConfiguration(map));
410: } else if (value instanceof byte[]) {
411: String base64 = new String(Base64
412: .encodeBase64((byte[]) value));
413: out.println(padding + "<data>"
414: + StringEscapeUtils.escapeXml(base64) + "</data>");
415: } else {
416: out.println(padding
417: + "<string>"
418: + StringEscapeUtils
419: .escapeXml(String.valueOf(value))
420: + "</string>");
421: }
422: }
423:
424: /**
425: * Node extension with addXXX methods to parse the typed data passed by Digester.
426: * <b>Do not use this class !</b> It is used internally by XMLPropertyConfiguration
427: * to parse the configuration file, it may be removed at any moment in the future.
428: */
429: public static class PListNode extends Node {
430: /**
431: * The serial version UID.
432: */
433: private static final long serialVersionUID = -7614060264754798317L;
434:
435: /** The standard format of dates in plist files. */
436: private static DateFormat format = new SimpleDateFormat(
437: "yyyy-MM-dd'T'HH:mm:ssZ");
438:
439: /**
440: * Update the value of the node. If the existing value is null, it's
441: * replaced with the new value. If the existing value is a list, the
442: * specified value is appended to the list. If the existing value is
443: * not null, a list with the two values is built.
444: *
445: * @param value the value to be added
446: */
447: public void addValue(Object value) {
448: if (getValue() == null) {
449: setValue(value);
450: } else if (getValue() instanceof List) {
451: List list = (List) getValue();
452: list.add(value);
453: } else {
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: try {
468: addValue(format.parse(value));
469: } catch (ParseException e) {
470: // ignore
471: ;
472: }
473: }
474:
475: /**
476: * Parse the specified string as a byte array in base 64 format
477: * and add it to the values of the node.
478: *
479: * @param value the value to be added
480: */
481: public void addDataValue(String value) {
482: addValue(Base64.decodeBase64(value.getBytes()));
483: }
484:
485: /**
486: * Parse the specified string as an Interger and add it to the values of the node.
487: *
488: * @param value the value to be added
489: */
490: public void addIntegerValue(String value) {
491: addValue(new Integer(value));
492: }
493:
494: /**
495: * Parse the specified string as a Double and add it to the values of the node.
496: *
497: * @param value the value to be added
498: */
499: public void addRealValue(String value) {
500: addValue(new Double(value));
501: }
502:
503: /**
504: * Add a boolean value 'true' to the values of the node.
505: */
506: public void addTrueValue() {
507: addValue(Boolean.TRUE);
508: }
509:
510: /**
511: * Add a boolean value 'false' to the values of the node.
512: */
513: public void addFalseValue() {
514: addValue(Boolean.FALSE);
515: }
516:
517: /**
518: * Add a sublist to the values of the node.
519: *
520: * @param node the node whose value will be added to the current node value
521: */
522: public void addList(ArrayNode node) {
523: addValue(node.getValue());
524: }
525: }
526:
527: /**
528: * Container for array elements. <b>Do not use this class !</b>
529: * It is used internally by XMLPropertyConfiguration to parse the
530: * configuration file, it may be removed at any moment in the future.
531: */
532: public static class ArrayNode extends PListNode {
533: /**
534: * The serial version UID.
535: */
536: private static final long serialVersionUID = 5586544306664205835L;
537:
538: /** The list of values in the array. */
539: private List list = new ArrayList();
540:
541: /**
542: * Add an object to the array.
543: *
544: * @param value the value to be added
545: */
546: public void addValue(Object value) {
547: list.add(value);
548: }
549:
550: /**
551: * Return the list of values in the array.
552: *
553: * @return the {@link List} of values
554: */
555: public Object getValue() {
556: return list;
557: }
558: }
559: }
|