/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.configuration.plist;
import java.io.File;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.URL;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.configuration.AbstractHierarchicalFileConfiguration;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.HierarchicalConfiguration;
import org.apache.commons.configuration.MapConfiguration;
import org.apache.commons.configuration.tree.ConfigurationNode;
import org.apache.commons.lang.StringUtils;
NeXT / OpenStep style configuration. This configuration can read and write
ASCII plist files. It supports the GNUStep extension to specify date objects.
References:
Example:
{
foo = "bar";
array = ( value1, value2, value3 );
data = <4f3e0145ab>;
date = <*D2007-05-05 20:05:00 +0100>;
nested =
{
key1 = value1;
key2 = value;
nested =
{
foo = bar
}
}
}
Author: Emmanuel Bourg Since: 1.2 Version: $Id: PropertyListConfiguration.java 1210637 2011-12-05 21:12:12Z oheger $
/**
* NeXT / OpenStep style configuration. This configuration can read and write
* ASCII plist files. It supports the GNUStep extension to specify date objects.
* <p>
* References:
* <ul>
* <li><a
* href="http://developer.apple.com/documentation/Cocoa/Conceptual/PropertyLists/OldStylePlists/OldStylePLists.html">
* Apple Documentation - Old-Style ASCII Property Lists</a></li>
* <li><a
* href="http://www.gnustep.org/resources/documentation/Developer/Base/Reference/NSPropertyList.html">
* GNUStep Documentation</a></li>
* </ul>
*
* <p>Example:</p>
* <pre>
* {
* foo = "bar";
*
* array = ( value1, value2, value3 );
*
* data = <4f3e0145ab>;
*
* date = <*D2007-05-05 20:05:00 +0100>;
*
* nested =
* {
* key1 = value1;
* key2 = value;
* nested =
* {
* foo = bar
* }
* }
* }
* </pre>
*
* @since 1.2
*
* @author Emmanuel Bourg
* @version $Id: PropertyListConfiguration.java 1210637 2011-12-05 21:12:12Z oheger $
*/
public class PropertyListConfiguration extends AbstractHierarchicalFileConfiguration
{
Constant for the separator parser for the date part. /** Constant for the separator parser for the date part. */
private static final DateComponentParser DATE_SEPARATOR_PARSER = new DateSeparatorParser(
"-");
Constant for the separator parser for the time part. /** Constant for the separator parser for the time part. */
private static final DateComponentParser TIME_SEPARATOR_PARSER = new DateSeparatorParser(
":");
Constant for the separator parser for blanks between the parts. /** Constant for the separator parser for blanks between the parts. */
private static final DateComponentParser BLANK_SEPARATOR_PARSER = new DateSeparatorParser(
" ");
An array with the component parsers for dealing with dates. /** An array with the component parsers for dealing with dates. */
private static final DateComponentParser[] DATE_PARSERS =
{new DateSeparatorParser("<*D"), new DateFieldParser(Calendar.YEAR, 4),
DATE_SEPARATOR_PARSER, new DateFieldParser(Calendar.MONTH, 2, 1),
DATE_SEPARATOR_PARSER, new DateFieldParser(Calendar.DATE, 2),
BLANK_SEPARATOR_PARSER,
new DateFieldParser(Calendar.HOUR_OF_DAY, 2),
TIME_SEPARATOR_PARSER, new DateFieldParser(Calendar.MINUTE, 2),
TIME_SEPARATOR_PARSER, new DateFieldParser(Calendar.SECOND, 2),
BLANK_SEPARATOR_PARSER, new DateTimeZoneParser(),
new DateSeparatorParser(">")};
Constant for the ID prefix for GMT time zones. /** Constant for the ID prefix for GMT time zones. */
private static final String TIME_ZONE_PREFIX = "GMT";
The serial version UID. /** The serial version UID. */
private static final long serialVersionUID = 3227248503779092127L;
Constant for the milliseconds of a minute./** Constant for the milliseconds of a minute.*/
private static final int MILLIS_PER_MINUTE = 1000 * 60;
Constant for the minutes per hour./** Constant for the minutes per hour.*/
private static final int MINUTES_PER_HOUR = 60;
Size of the indentation for the generated file. /** Size of the indentation for the generated file. */
private static final int INDENT_SIZE = 4;
Constant for the length of a time zone./** Constant for the length of a time zone.*/
private static final int TIME_ZONE_LENGTH = 5;
Constant for the padding character in the date format./** Constant for the padding character in the date format.*/
private static final char PAD_CHAR = '0';
Creates an empty PropertyListConfiguration object which can be
used to synthesize a new plist file by adding values and
then saving().
/**
* Creates an empty PropertyListConfiguration object which can be
* used to synthesize a new plist file by adding values and
* then saving().
*/
public PropertyListConfiguration()
{
}
Creates a new instance of PropertyListConfiguration
and copies the content of the specified configuration into this object. Params: - c – the configuration to copy
Since: 1.4
/**
* Creates a new instance of {@code PropertyListConfiguration} and
* copies the content of the specified configuration into this object.
*
* @param c the configuration to copy
* @since 1.4
*/
public PropertyListConfiguration(HierarchicalConfiguration c)
{
super(c);
}
Creates and loads the property list from the specified file.
Params: - fileName – The name of the plist file to load.
Throws: - ConfigurationException – Error while loading the plist file
/**
* Creates and loads the property list from the specified file.
*
* @param fileName The name of the plist file to load.
* @throws ConfigurationException Error while loading the plist file
*/
public PropertyListConfiguration(String fileName) throws ConfigurationException
{
super(fileName);
}
Creates and loads the property list from the specified file.
Params: - file – The plist file to load.
Throws: - ConfigurationException – Error while loading the plist file
/**
* Creates and loads the property list from the specified file.
*
* @param file The plist file to load.
* @throws ConfigurationException Error while loading the plist file
*/
public PropertyListConfiguration(File file) throws ConfigurationException
{
super(file);
}
Creates and loads the property list from the specified URL.
Params: - url – The location of the plist file to load.
Throws: - ConfigurationException – Error while loading the plist file
/**
* Creates and loads the property list from the specified URL.
*
* @param url The location of the plist file to load.
* @throws ConfigurationException Error while loading the plist file
*/
public PropertyListConfiguration(URL url) throws ConfigurationException
{
super(url);
}
@Override
public void setProperty(String key, Object value)
{
// special case for byte arrays, they must be stored as is in the configuration
if (value instanceof byte[])
{
fireEvent(EVENT_SET_PROPERTY, key, value, true);
setDetailEvents(false);
try
{
clearProperty(key);
addPropertyDirect(key, value);
}
finally
{
setDetailEvents(true);
}
fireEvent(EVENT_SET_PROPERTY, key, value, false);
}
else
{
super.setProperty(key, value);
}
}
@Override
public void addProperty(String key, Object value)
{
if (value instanceof byte[])
{
fireEvent(EVENT_ADD_PROPERTY, key, value, true);
addPropertyDirect(key, value);
fireEvent(EVENT_ADD_PROPERTY, key, value, false);
}
else
{
super.addProperty(key, value);
}
}
public void load(Reader in) throws ConfigurationException
{
PropertyListParser parser = new PropertyListParser(in);
try
{
HierarchicalConfiguration config = parser.parse();
setRoot(config.getRoot());
}
catch (ParseException e)
{
throw new ConfigurationException(e);
}
}
public void save(Writer out) throws ConfigurationException
{
PrintWriter writer = new PrintWriter(out);
printNode(writer, 0, getRoot());
writer.flush();
}
Append a node to the writer, indented according to a specific level.
/**
* Append a node to the writer, indented according to a specific level.
*/
private void printNode(PrintWriter out, int indentLevel, ConfigurationNode node)
{
String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
if (node.getName() != null)
{
out.print(padding + quoteString(node.getName()) + " = ");
}
List<ConfigurationNode> children = new ArrayList<ConfigurationNode>(node.getChildren());
if (!children.isEmpty())
{
// skip a line, except for the root dictionary
if (indentLevel > 0)
{
out.println();
}
out.println(padding + "{");
// display the children
Iterator<ConfigurationNode> it = children.iterator();
while (it.hasNext())
{
ConfigurationNode child = it.next();
printNode(out, indentLevel + 1, child);
// add a semi colon for elements that are not dictionaries
Object value = child.getValue();
if (value != null && !(value instanceof Map) && !(value instanceof Configuration))
{
out.println(";");
}
// skip a line after arrays and dictionaries
if (it.hasNext() && (value == null || value instanceof List))
{
out.println();
}
}
out.print(padding + "}");
// line feed if the dictionary is not in an array
if (node.getParentNode() != null)
{
out.println();
}
}
else if (node.getValue() == null)
{
out.println();
out.print(padding + "{ };");
// line feed if the dictionary is not in an array
if (node.getParentNode() != null)
{
out.println();
}
}
else
{
// display the leaf value
Object value = node.getValue();
printValue(out, indentLevel, value);
}
}
Append a value to the writer, indented according to a specific level.
/**
* Append a value to the writer, indented according to a specific level.
*/
private void printValue(PrintWriter out, int indentLevel, Object value)
{
String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
if (value instanceof List)
{
out.print("( ");
Iterator<?> it = ((List<?>) value).iterator();
while (it.hasNext())
{
printValue(out, indentLevel + 1, it.next());
if (it.hasNext())
{
out.print(", ");
}
}
out.print(" )");
}
else if (value instanceof HierarchicalConfiguration)
{
printNode(out, indentLevel, ((HierarchicalConfiguration) value).getRoot());
}
else if (value instanceof Configuration)
{
// display a flat Configuration as a dictionary
out.println();
out.println(padding + "{");
Configuration config = (Configuration) value;
Iterator<String> it = config.getKeys();
while (it.hasNext())
{
String key = it.next();
Node node = new Node(key);
node.setValue(config.getProperty(key));
printNode(out, indentLevel + 1, node);
out.println(";");
}
out.println(padding + "}");
}
else if (value instanceof Map)
{
// display a Map as a dictionary
Map<String, Object> map = transformMap((Map<?, ?>) value);
printValue(out, indentLevel, new MapConfiguration(map));
}
else if (value instanceof byte[])
{
out.print("<" + new String(Hex.encodeHex((byte[]) value)) + ">");
}
else if (value instanceof Date)
{
out.print(formatDate((Date) value));
}
else if (value != null)
{
out.print(quoteString(String.valueOf(value)));
}
}
Quote the specified string if necessary, that's if the string contains:
- a space character (' ', '\t', '\r', '\n')
- a quote '"'
- special characters in plist files ('(', ')', '{', '}', '=', ';', ',')
Quotes within the string are escaped.
Examples:
- abcd -> abcd
- ab cd -> "ab cd"
- foo"bar -> "foo\"bar"
- foo;bar -> "foo;bar"
/**
* Quote the specified string if necessary, that's if the string contains:
* <ul>
* <li>a space character (' ', '\t', '\r', '\n')</li>
* <li>a quote '"'</li>
* <li>special characters in plist files ('(', ')', '{', '}', '=', ';', ',')</li>
* </ul>
* Quotes within the string are escaped.
*
* <p>Examples:</p>
* <ul>
* <li>abcd -> abcd</li>
* <li>ab cd -> "ab cd"</li>
* <li>foo"bar -> "foo\"bar"</li>
* <li>foo;bar -> "foo;bar"</li>
* </ul>
*/
String quoteString(String s)
{
if (s == null)
{
return null;
}
if (s.indexOf(' ') != -1
|| s.indexOf('\t') != -1
|| s.indexOf('\r') != -1
|| s.indexOf('\n') != -1
|| s.indexOf('"') != -1
|| s.indexOf('(') != -1
|| s.indexOf(')') != -1
|| s.indexOf('{') != -1
|| s.indexOf('}') != -1
|| s.indexOf('=') != -1
|| s.indexOf(',') != -1
|| s.indexOf(';') != -1)
{
s = s.replaceAll("\"", "\\\\\\\"");
s = "\"" + s + "\"";
}
return s;
}
Parses a date in a format like <*D2002-03-22 11:30:00 +0100>
. Params: - s – the string with the date to be parsed
Throws: - ParseException – if an error occurred while parsing the string
Returns: the parsed date
/**
* Parses a date in a format like
* {@code <*D2002-03-22 11:30:00 +0100>}.
*
* @param s the string with the date to be parsed
* @return the parsed date
* @throws ParseException if an error occurred while parsing the string
*/
static Date parseDate(String s) throws ParseException
{
Calendar cal = Calendar.getInstance();
cal.clear();
int index = 0;
for (DateComponentParser parser : DATE_PARSERS)
{
index += parser.parseComponent(s, index, cal);
}
return cal.getTime();
}
Returns a string representation for the date specified by the given
calendar.
Params: - cal – the calendar with the initialized date
Returns: a string for this date
/**
* Returns a string representation for the date specified by the given
* calendar.
*
* @param cal the calendar with the initialized date
* @return a string for this date
*/
static String formatDate(Calendar cal)
{
StringBuilder buf = new StringBuilder();
for (int i = 0; i < DATE_PARSERS.length; i++)
{
DATE_PARSERS[i].formatComponent(buf, cal);
}
return buf.toString();
}
Returns a string representation for the specified date.
Params: - date – the date
Returns: a string for this date
/**
* Returns a string representation for the specified date.
*
* @param date the date
* @return a string for this date
*/
static String formatDate(Date date)
{
Calendar cal = Calendar.getInstance();
cal.setTime(date);
return formatDate(cal);
}
Transform a map of arbitrary types into a map with string keys and object
values. All keys of the source map which are not of type String are
dropped.
Params: - src – the map to be converted
Returns: the resulting map
/**
* Transform a map of arbitrary types into a map with string keys and object
* values. All keys of the source map which are not of type String are
* dropped.
*
* @param src the map to be converted
* @return the resulting map
*/
private static Map<String, Object> transformMap(Map<?, ?> src)
{
Map<String, Object> dest = new HashMap<String, Object>();
for (Map.Entry<?, ?> e : src.entrySet())
{
if (e.getKey() instanceof String)
{
dest.put((String) e.getKey(), e.getValue());
}
}
return dest;
}
A helper class for parsing and formatting date literals. Usually we would use SimpleDateFormat
for this purpose, but in Java 1.3 the functionality of this class is limited. So we have a hierarchy of parser classes instead that deal with the different components of a date literal. /**
* A helper class for parsing and formatting date literals. Usually we would
* use {@code SimpleDateFormat} for this purpose, but in Java 1.3 the
* functionality of this class is limited. So we have a hierarchy of parser
* classes instead that deal with the different components of a date
* literal.
*/
private abstract static class DateComponentParser
{
Parses a component from the given input string.
Params: - s – the string to be parsed
- index – the current parsing position
- cal – the calendar where to store the result
Throws: - ParseException – if the component cannot be extracted
Returns: the length of the processed component
/**
* Parses a component from the given input string.
*
* @param s the string to be parsed
* @param index the current parsing position
* @param cal the calendar where to store the result
* @return the length of the processed component
* @throws ParseException if the component cannot be extracted
*/
public abstract int parseComponent(String s, int index, Calendar cal)
throws ParseException;
Formats a date component. This method is used for converting a date
in its internal representation into a string literal.
Params: - buf – the target buffer
- cal – the calendar with the current date
/**
* Formats a date component. This method is used for converting a date
* in its internal representation into a string literal.
*
* @param buf the target buffer
* @param cal the calendar with the current date
*/
public abstract void formatComponent(StringBuilder buf, Calendar cal);
Checks whether the given string has at least length
characters starting from the given parsing position. If this is not the case, an exception will be thrown. Params: - s – the string to be tested
- index – the current index
- length – the minimum length after the index
Throws: - ParseException – if the string is too short
/**
* Checks whether the given string has at least {@code length}
* characters starting from the given parsing position. If this is not
* the case, an exception will be thrown.
*
* @param s the string to be tested
* @param index the current index
* @param length the minimum length after the index
* @throws ParseException if the string is too short
*/
protected void checkLength(String s, int index, int length)
throws ParseException
{
int len = (s == null) ? 0 : s.length();
if (index + length > len)
{
throw new ParseException("Input string too short: " + s
+ ", index: " + index);
}
}
Adds a number to the given string buffer and adds leading '0'
characters until the given length is reached.
Params: - buf – the target buffer
- num – the number to add
- length – the required length
/**
* Adds a number to the given string buffer and adds leading '0'
* characters until the given length is reached.
*
* @param buf the target buffer
* @param num the number to add
* @param length the required length
*/
protected void padNum(StringBuilder buf, int num, int length)
{
buf.append(StringUtils.leftPad(String.valueOf(num), length,
PAD_CHAR));
}
}
A specialized date component parser implementation that deals with
numeric calendar fields. The class is able to extract fields from a
string literal and to format a literal from a calendar.
/**
* A specialized date component parser implementation that deals with
* numeric calendar fields. The class is able to extract fields from a
* string literal and to format a literal from a calendar.
*/
private static class DateFieldParser extends DateComponentParser
{
Stores the calendar field to be processed. /** Stores the calendar field to be processed. */
private int calendarField;
Stores the length of this field. /** Stores the length of this field. */
private int length;
An optional offset to add to the calendar field. /** An optional offset to add to the calendar field. */
private int offset;
Creates a new instance of DateFieldParser
. Params: - calFld – the calendar field code
- len – the length of this field
/**
* Creates a new instance of {@code DateFieldParser}.
*
* @param calFld the calendar field code
* @param len the length of this field
*/
public DateFieldParser(int calFld, int len)
{
this(calFld, len, 0);
}
Creates a new instance of DateFieldParser
and fully initializes it. Params: - calFld – the calendar field code
- len – the length of this field
- ofs – an offset to add to the calendar field
/**
* Creates a new instance of {@code DateFieldParser} and fully
* initializes it.
*
* @param calFld the calendar field code
* @param len the length of this field
* @param ofs an offset to add to the calendar field
*/
public DateFieldParser(int calFld, int len, int ofs)
{
calendarField = calFld;
length = len;
offset = ofs;
}
@Override
public void formatComponent(StringBuilder buf, Calendar cal)
{
padNum(buf, cal.get(calendarField) + offset, length);
}
@Override
public int parseComponent(String s, int index, Calendar cal)
throws ParseException
{
checkLength(s, index, length);
try
{
cal.set(calendarField, Integer.parseInt(s.substring(index,
index + length))
- offset);
return length;
}
catch (NumberFormatException nfex)
{
throw new ParseException("Invalid number: " + s + ", index "
+ index);
}
}
}
A specialized date component parser implementation that deals with
separator characters.
/**
* A specialized date component parser implementation that deals with
* separator characters.
*/
private static class DateSeparatorParser extends DateComponentParser
{
Stores the separator. /** Stores the separator. */
private String separator;
Creates a new instance of DateSeparatorParser
and sets the separator string. Params: - sep – the separator string
/**
* Creates a new instance of {@code DateSeparatorParser} and sets
* the separator string.
*
* @param sep the separator string
*/
public DateSeparatorParser(String sep)
{
separator = sep;
}
@Override
public void formatComponent(StringBuilder buf, Calendar cal)
{
buf.append(separator);
}
@Override
public int parseComponent(String s, int index, Calendar cal)
throws ParseException
{
checkLength(s, index, separator.length());
if (!s.startsWith(separator, index))
{
throw new ParseException("Invalid input: " + s + ", index "
+ index + ", expected " + separator);
}
return separator.length();
}
}
A specialized date component parser implementation that deals with the
time zone part of a date component.
/**
* A specialized date component parser implementation that deals with the
* time zone part of a date component.
*/
private static class DateTimeZoneParser extends DateComponentParser
{
@Override
public void formatComponent(StringBuilder buf, Calendar cal)
{
TimeZone tz = cal.getTimeZone();
int ofs = tz.getRawOffset() / MILLIS_PER_MINUTE;
if (ofs < 0)
{
buf.append('-');
ofs = -ofs;
}
else
{
buf.append('+');
}
int hour = ofs / MINUTES_PER_HOUR;
int min = ofs % MINUTES_PER_HOUR;
padNum(buf, hour, 2);
padNum(buf, min, 2);
}
@Override
public int parseComponent(String s, int index, Calendar cal)
throws ParseException
{
checkLength(s, index, TIME_ZONE_LENGTH);
TimeZone tz = TimeZone.getTimeZone(TIME_ZONE_PREFIX
+ s.substring(index, index + TIME_ZONE_LENGTH));
cal.setTimeZone(tz);
return TIME_ZONE_LENGTH;
}
}
}