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
9
* http://www.apache.org/licenses/LICENSE-2.0
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.
18
package org.apache.commons.configuration.plist;
21
import java.io.PrintWriter;
22
import java.io.Reader;
23
import java.io.Writer;
25
import java.util.ArrayList;
26
import java.util.Calendar;
27
import java.util.Date;
28
import java.util.Iterator;
29
import java.util.List;
31
import java.util.TimeZone;
33
import org.apache.commons.codec.binary.Hex;
34
import org.apache.commons.configuration.AbstractHierarchicalFileConfiguration;
35
import org.apache.commons.configuration.Configuration;
36
import org.apache.commons.configuration.ConfigurationException;
37
import org.apache.commons.configuration.HierarchicalConfiguration;
38
import org.apache.commons.configuration.MapConfiguration;
39
import org.apache.commons.lang.StringUtils;
42
* NeXT / OpenStep style configuration. This configuration can read and write
43
* ASCII plist files. It supports the GNUStep extension to specify date objects.
48
* href="http://developer.apple.com/documentation/Cocoa/Conceptual/PropertyLists/OldStylePlists/OldStylePLists.html">
49
* Apple Documentation - Old-Style ASCII Property Lists</a></li>
51
* href="http://www.gnustep.org/resources/documentation/Developer/Base/Reference/NSPropertyList.html">
52
* GNUStep Documentation</a></li>
60
* array = ( value1, value2, value3 );
62
* data = <4f3e0145ab>;
64
* date = <*D2007-05-05 20:05:00 +0100>;
80
* @author Emmanuel Bourg
81
* @version $Revision: 797282 $, $Date: 2009-07-24 02:39:29 +0200 (Fr, 24. Jul 2009) $
83
public class PropertyListConfiguration extends AbstractHierarchicalFileConfiguration
85
/** Constant for the separator parser for the date part. */
86
private static final DateComponentParser DATE_SEPARATOR_PARSER = new DateSeparatorParser(
89
/** Constant for the separator parser for the time part. */
90
private static final DateComponentParser TIME_SEPARATOR_PARSER = new DateSeparatorParser(
93
/** Constant for the separator parser for blanks between the parts. */
94
private static final DateComponentParser BLANK_SEPARATOR_PARSER = new DateSeparatorParser(
97
/** An array with the component parsers for dealing with dates. */
98
private static final DateComponentParser[] DATE_PARSERS =
99
{new DateSeparatorParser("<*D"), new DateFieldParser(Calendar.YEAR, 4),
100
DATE_SEPARATOR_PARSER, new DateFieldParser(Calendar.MONTH, 2, 1),
101
DATE_SEPARATOR_PARSER, new DateFieldParser(Calendar.DATE, 2),
102
BLANK_SEPARATOR_PARSER,
103
new DateFieldParser(Calendar.HOUR_OF_DAY, 2),
104
TIME_SEPARATOR_PARSER, new DateFieldParser(Calendar.MINUTE, 2),
105
TIME_SEPARATOR_PARSER, new DateFieldParser(Calendar.SECOND, 2),
106
BLANK_SEPARATOR_PARSER, new DateTimeZoneParser(),
107
new DateSeparatorParser(">")};
109
/** Constant for the ID prefix for GMT time zones. */
110
private static final String TIME_ZONE_PREFIX = "GMT";
112
/** The serial version UID. */
113
private static final long serialVersionUID = 3227248503779092127L;
115
/** Constant for the milliseconds of a minute.*/
116
private static final int MILLIS_PER_MINUTE = 1000 * 60;
118
/** Constant for the minutes per hour.*/
119
private static final int MINUTES_PER_HOUR = 60;
121
/** Size of the indentation for the generated file. */
122
private static final int INDENT_SIZE = 4;
124
/** Constant for the length of a time zone.*/
125
private static final int TIME_ZONE_LENGTH = 5;
127
/** Constant for the padding character in the date format.*/
128
private static final char PAD_CHAR = '0';
131
* Creates an empty PropertyListConfiguration object which can be
132
* used to synthesize a new plist file by adding values and
135
public PropertyListConfiguration()
140
* Creates a new instance of <code>PropertyListConfiguration</code> and
141
* copies the content of the specified configuration into this object.
143
* @param c the configuration to copy
146
public PropertyListConfiguration(HierarchicalConfiguration c)
152
* Creates and loads the property list from the specified file.
154
* @param fileName The name of the plist file to load.
155
* @throws ConfigurationException Error while loading the plist file
157
public PropertyListConfiguration(String fileName) throws ConfigurationException
163
* Creates and loads the property list from the specified file.
165
* @param file The plist file to load.
166
* @throws ConfigurationException Error while loading the plist file
168
public PropertyListConfiguration(File file) throws ConfigurationException
174
* Creates and loads the property list from the specified URL.
176
* @param url The location of the plist file to load.
177
* @throws ConfigurationException Error while loading the plist file
179
public PropertyListConfiguration(URL url) throws ConfigurationException
184
public void setProperty(String key, Object value)
186
// special case for byte arrays, they must be stored as is in the configuration
187
if (value instanceof byte[])
189
fireEvent(EVENT_SET_PROPERTY, key, value, true);
190
setDetailEvents(false);
194
addPropertyDirect(key, value);
198
setDetailEvents(true);
200
fireEvent(EVENT_SET_PROPERTY, key, value, false);
204
super.setProperty(key, value);
208
public void addProperty(String key, Object value)
210
if (value instanceof byte[])
212
fireEvent(EVENT_ADD_PROPERTY, key, value, true);
213
addPropertyDirect(key, value);
214
fireEvent(EVENT_ADD_PROPERTY, key, value, false);
218
super.addProperty(key, value);
222
public void load(Reader in) throws ConfigurationException
224
PropertyListParser parser = new PropertyListParser(in);
227
HierarchicalConfiguration config = parser.parse();
228
setRoot(config.getRoot());
230
catch (ParseException e)
232
throw new ConfigurationException(e);
236
public void save(Writer out) throws ConfigurationException
238
PrintWriter writer = new PrintWriter(out);
239
printNode(writer, 0, getRoot());
244
* Append a node to the writer, indented according to a specific level.
246
private void printNode(PrintWriter out, int indentLevel, Node node)
248
String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
250
if (node.getName() != null)
252
out.print(padding + quoteString(node.getName()) + " = ");
255
List children = new ArrayList(node.getChildren());
256
if (!children.isEmpty())
258
// skip a line, except for the root dictionary
264
out.println(padding + "{");
266
// display the children
267
Iterator it = children.iterator();
270
Node child = (Node) it.next();
272
printNode(out, indentLevel + 1, child);
274
// add a semi colon for elements that are not dictionaries
275
Object value = child.getValue();
276
if (value != null && !(value instanceof Map) && !(value instanceof Configuration))
281
// skip a line after arrays and dictionaries
282
if (it.hasNext() && (value == null || value instanceof List))
288
out.print(padding + "}");
290
// line feed if the dictionary is not in an array
291
if (node.getParent() != null)
296
else if (node.getValue() == null)
299
out.print(padding + "{ };");
301
// line feed if the dictionary is not in an array
302
if (node.getParentNode() != null)
309
// display the leaf value
310
Object value = node.getValue();
311
printValue(out, indentLevel, value);
316
* Append a value to the writer, indented according to a specific level.
318
private void printValue(PrintWriter out, int indentLevel, Object value)
320
String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
322
if (value instanceof List)
325
Iterator it = ((List) value).iterator();
328
printValue(out, indentLevel + 1, it.next());
336
else if (value instanceof HierarchicalConfiguration)
338
printNode(out, indentLevel, ((HierarchicalConfiguration) value).getRoot());
340
else if (value instanceof Configuration)
342
// display a flat Configuration as a dictionary
344
out.println(padding + "{");
346
Configuration config = (Configuration) value;
347
Iterator it = config.getKeys();
350
String key = (String) it.next();
351
Node node = new Node(key);
352
node.setValue(config.getProperty(key));
354
printNode(out, indentLevel + 1, node);
357
out.println(padding + "}");
359
else if (value instanceof Map)
361
// display a Map as a dictionary
362
Map map = (Map) value;
363
printValue(out, indentLevel, new MapConfiguration(map));
365
else if (value instanceof byte[])
367
out.print("<" + new String(Hex.encodeHex((byte[]) value)) + ">");
369
else if (value instanceof Date)
371
out.print(formatDate((Date) value));
373
else if (value != null)
375
out.print(quoteString(String.valueOf(value)));
380
* Quote the specified string if necessary, that's if the string contains:
382
* <li>a space character (' ', '\t', '\r', '\n')</li>
383
* <li>a quote '"'</li>
384
* <li>special characters in plist files ('(', ')', '{', '}', '=', ';', ',')</li>
386
* Quotes within the string are escaped.
390
* <li>abcd -> abcd</li>
391
* <li>ab cd -> "ab cd"</li>
392
* <li>foo"bar -> "foo\"bar"</li>
393
* <li>foo;bar -> "foo;bar"</li>
396
String quoteString(String s)
403
if (s.indexOf(' ') != -1
404
|| s.indexOf('\t') != -1
405
|| s.indexOf('\r') != -1
406
|| s.indexOf('\n') != -1
407
|| s.indexOf('"') != -1
408
|| s.indexOf('(') != -1
409
|| s.indexOf(')') != -1
410
|| s.indexOf('{') != -1
411
|| s.indexOf('}') != -1
412
|| s.indexOf('=') != -1
413
|| s.indexOf(',') != -1
414
|| s.indexOf(';') != -1)
416
s = StringUtils.replace(s, "\"", "\\\"");
424
* Parses a date in a format like
425
* <code><*D2002-03-22 11:30:00 +0100></code>.
427
* @param s the string with the date to be parsed
428
* @return the parsed date
429
* @throws ParseException if an error occurred while parsing the string
431
static Date parseDate(String s) throws ParseException
433
Calendar cal = Calendar.getInstance();
437
for (int i = 0; i < DATE_PARSERS.length; i++)
439
index += DATE_PARSERS[i].parseComponent(s, index, cal);
442
return cal.getTime();
446
* Returns a string representation for the date specified by the given
449
* @param cal the calendar with the initialized date
450
* @return a string for this date
452
static String formatDate(Calendar cal)
454
StringBuffer buf = new StringBuffer();
456
for (int i = 0; i < DATE_PARSERS.length; i++)
458
DATE_PARSERS[i].formatComponent(buf, cal);
461
return buf.toString();
465
* Returns a string representation for the specified date.
467
* @param date the date
468
* @return a string for this date
470
static String formatDate(Date date)
472
Calendar cal = Calendar.getInstance();
474
return formatDate(cal);
478
* A helper class for parsing and formatting date literals. Usually we would
479
* use <code>SimpleDateFormat</code> for this purpose, but in Java 1.3 the
480
* functionality of this class is limited. So we have a hierarchy of parser
481
* classes instead that deal with the different components of a date
484
private abstract static class DateComponentParser
487
* Parses a component from the given input string.
489
* @param s the string to be parsed
490
* @param index the current parsing position
491
* @param cal the calendar where to store the result
492
* @return the length of the processed component
493
* @throws ParseException if the component cannot be extracted
495
public abstract int parseComponent(String s, int index, Calendar cal)
496
throws ParseException;
499
* Formats a date component. This method is used for converting a date
500
* in its internal representation into a string literal.
502
* @param buf the target buffer
503
* @param cal the calendar with the current date
505
public abstract void formatComponent(StringBuffer buf, Calendar cal);
508
* Checks whether the given string has at least <code>length</code>
509
* characters starting from the given parsing position. If this is not
510
* the case, an exception will be thrown.
512
* @param s the string to be tested
513
* @param index the current index
514
* @param length the minimum length after the index
515
* @throws ParseException if the string is too short
517
protected void checkLength(String s, int index, int length)
518
throws ParseException
520
int len = (s == null) ? 0 : s.length();
521
if (index + length > len)
523
throw new ParseException("Input string too short: " + s
524
+ ", index: " + index);
529
* Adds a number to the given string buffer and adds leading '0'
530
* characters until the given length is reached.
532
* @param buf the target buffer
533
* @param num the number to add
534
* @param length the required length
536
protected void padNum(StringBuffer buf, int num, int length)
538
buf.append(StringUtils.leftPad(String.valueOf(num), length,
544
* A specialized date component parser implementation that deals with
545
* numeric calendar fields. The class is able to extract fields from a
546
* string literal and to format a literal from a calendar.
548
private static class DateFieldParser extends DateComponentParser
550
/** Stores the calendar field to be processed. */
551
private int calendarField;
553
/** Stores the length of this field. */
556
/** An optional offset to add to the calendar field. */
560
* Creates a new instance of <code>DateFieldParser</code>.
562
* @param calFld the calendar field code
563
* @param len the length of this field
565
public DateFieldParser(int calFld, int len)
567
this(calFld, len, 0);
571
* Creates a new instance of <code>DateFieldParser</code> and fully
574
* @param calFld the calendar field code
575
* @param len the length of this field
576
* @param ofs an offset to add to the calendar field
578
public DateFieldParser(int calFld, int len, int ofs)
580
calendarField = calFld;
585
public void formatComponent(StringBuffer buf, Calendar cal)
587
padNum(buf, cal.get(calendarField) + offset, length);
590
public int parseComponent(String s, int index, Calendar cal)
591
throws ParseException
593
checkLength(s, index, length);
596
cal.set(calendarField, Integer.parseInt(s.substring(index,
601
catch (NumberFormatException nfex)
603
throw new ParseException("Invalid number: " + s + ", index "
610
* A specialized date component parser implementation that deals with
611
* separator characters.
613
private static class DateSeparatorParser extends DateComponentParser
615
/** Stores the separator. */
616
private String separator;
619
* Creates a new instance of <code>DateSeparatorParser</code> and sets
620
* the separator string.
622
* @param sep the separator string
624
public DateSeparatorParser(String sep)
629
public void formatComponent(StringBuffer buf, Calendar cal)
631
buf.append(separator);
634
public int parseComponent(String s, int index, Calendar cal)
635
throws ParseException
637
checkLength(s, index, separator.length());
638
if (!s.startsWith(separator, index))
640
throw new ParseException("Invalid input: " + s + ", index "
641
+ index + ", expected " + separator);
643
return separator.length();
648
* A specialized date component parser implementation that deals with the
649
* time zone part of a date component.
651
private static class DateTimeZoneParser extends DateComponentParser
653
public void formatComponent(StringBuffer buf, Calendar cal)
655
TimeZone tz = cal.getTimeZone();
656
int ofs = tz.getRawOffset() / MILLIS_PER_MINUTE;
666
int hour = ofs / MINUTES_PER_HOUR;
667
int min = ofs % MINUTES_PER_HOUR;
668
padNum(buf, hour, 2);
672
public int parseComponent(String s, int index, Calendar cal)
673
throws ParseException
675
checkLength(s, index, TIME_ZONE_LENGTH);
676
TimeZone tz = TimeZone.getTimeZone(TIME_ZONE_PREFIX
677
+ s.substring(index, index + TIME_ZONE_LENGTH));
679
return TIME_ZONE_LENGTH;