/*
 * 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.configuration2.plist;

import java.io.PrintWriter;
import java.io.Reader;
import java.io.Writer;
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.configuration2.BaseHierarchicalConfiguration;
import org.apache.commons.configuration2.Configuration;
import org.apache.commons.configuration2.FileBasedConfiguration;
import org.apache.commons.configuration2.HierarchicalConfiguration;
import org.apache.commons.configuration2.ImmutableConfiguration;
import org.apache.commons.configuration2.MapConfiguration;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.configuration2.tree.ImmutableNode;
import org.apache.commons.configuration2.tree.InMemoryNodeModel;
import org.apache.commons.configuration2.tree.NodeHandler;
import org.apache.commons.lang3.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
        }
    }
}
Since:1.2
/** * 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 = &lt;4f3e0145ab&gt;; * * date = &lt;*D2007-05-05 20:05:00 +0100&gt;; * * nested = * { * key1 = value1; * key2 = value; * nested = * { * foo = bar * } * } * } * </pre> * * @since 1.2 * */
public class PropertyListConfiguration extends BaseHierarchicalConfiguration implements FileBasedConfiguration {
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";
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(final HierarchicalConfiguration<ImmutableNode> c) { super(c); }
Creates a new instance of PropertyListConfiguration with the given root node.
Params:
  • root – the root node
/** * Creates a new instance of {@code PropertyListConfiguration} with the * given root node. * * @param root the root node */
PropertyListConfiguration(final ImmutableNode root) { super(new InMemoryNodeModel(root)); } @Override protected void setPropertyInternal(final String key, final Object value) { // special case for byte arrays, they must be stored as is in the configuration if (value instanceof byte[]) { setDetailEvents(false); try { clearProperty(key); addPropertyDirect(key, value); } finally { setDetailEvents(true); } } else { super.setPropertyInternal(key, value); } } @Override protected void addPropertyInternal(final String key, final Object value) { if (value instanceof byte[]) { addPropertyDirect(key, value); } else { super.addPropertyInternal(key, value); } } @Override public void read(final Reader in) throws ConfigurationException { final PropertyListParser parser = new PropertyListParser(in); try { final PropertyListConfiguration config = parser.parse(); getModel().setRootNode( config.getNodeModel().getNodeHandler().getRootNode()); } catch (final ParseException e) { throw new ConfigurationException(e); } } @Override public void write(final Writer out) throws ConfigurationException { final PrintWriter writer = new PrintWriter(out); final NodeHandler<ImmutableNode> handler = getModel().getNodeHandler(); printNode(writer, 0, handler.getRootNode(), handler); 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(final PrintWriter out, final int indentLevel, final ImmutableNode node, final NodeHandler<ImmutableNode> handler) { final String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE); if (node.getNodeName() != null) { out.print(padding + quoteString(node.getNodeName()) + " = "); } final List<ImmutableNode> children = new ArrayList<>(node.getChildren()); if (!children.isEmpty()) { // skip a line, except for the root dictionary if (indentLevel > 0) { out.println(); } out.println(padding + "{"); // display the children final Iterator<ImmutableNode> it = children.iterator(); while (it.hasNext()) { final ImmutableNode child = it.next(); printNode(out, indentLevel + 1, child, handler); // add a semi colon for elements that are not dictionaries final 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 (handler.getParent(node) != null) { out.println(); } } else if (node.getValue() == null) { out.println(); out.print(padding + "{ };"); // line feed if the dictionary is not in an array if (handler.getParent(node) != null) { out.println(); } } else { // display the leaf value final 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(final PrintWriter out, final int indentLevel, final Object value) { final String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE); if (value instanceof List) { out.print("( "); final 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 PropertyListConfiguration) { final NodeHandler<ImmutableNode> handler = ((PropertyListConfiguration) value).getModel() .getNodeHandler(); printNode(out, indentLevel, handler.getRootNode(), handler); } else if (value instanceof ImmutableConfiguration) { // display a flat Configuration as a dictionary out.println(); out.println(padding + "{"); final ImmutableConfiguration config = (ImmutableConfiguration) value; final Iterator<String> it = config.getKeys(); while (it.hasNext()) { final String key = it.next(); final ImmutableNode node = new ImmutableNode.Builder().name(key) .value(config.getProperty(key)).create(); final InMemoryNodeModel tempModel = new InMemoryNodeModel(node); printNode(out, indentLevel + 1, node, tempModel.getNodeHandler()); out.println(";"); } out.println(padding + "}"); } else if (value instanceof Map) { // display a Map as a dictionary final 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:
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(final String s) throws ParseException { final Calendar cal = Calendar.getInstance(); cal.clear(); int index = 0; for (final 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(final Calendar cal) { final StringBuilder buf = new StringBuilder(); for (final DateComponentParser element : DATE_PARSERS) { element.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(final Date date) { final 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(final Map<?, ?> src) { final Map<String, Object> dest = new HashMap<>(); for (final 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:
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:
/** * 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(final String s, final int index, final int length) throws ParseException { final 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(final StringBuilder buf, final int num, final 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 final int calendarField;
Stores the length of this field.
/** Stores the length of this field. */
private final int length;
An optional offset to add to the calendar field.
/** An optional offset to add to the calendar field. */
private final 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(final int calFld, final 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(final int calFld, final int len, final int ofs) { calendarField = calFld; length = len; offset = ofs; } @Override public void formatComponent(final StringBuilder buf, final Calendar cal) { padNum(buf, cal.get(calendarField) + offset, length); } @Override public int parseComponent(final String s, final int index, final Calendar cal) throws ParseException { checkLength(s, index, length); try { cal.set(calendarField, Integer.parseInt(s.substring(index, index + length)) - offset); return length; } catch (final 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 final 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(final String sep) { separator = sep; } @Override public void formatComponent(final StringBuilder buf, final Calendar cal) { buf.append(separator); } @Override public int parseComponent(final String s, final int index, final 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(final StringBuilder buf, final Calendar cal) { final TimeZone tz = cal.getTimeZone(); int ofs = tz.getRawOffset() / MILLIS_PER_MINUTE; if (ofs < 0) { buf.append('-'); ofs = -ofs; } else { buf.append('+'); } final int hour = ofs / MINUTES_PER_HOUR; final int min = ofs % MINUTES_PER_HOUR; padNum(buf, hour, 2); padNum(buf, min, 2); } @Override public int parseComponent(final String s, final int index, final Calendar cal) throws ParseException { checkLength(s, index, TIME_ZONE_LENGTH); final TimeZone tz = TimeZone.getTimeZone(TIME_ZONE_PREFIX + s.substring(index, index + TIME_ZONE_LENGTH)); cal.setTimeZone(tz); return TIME_ZONE_LENGTH; } } }