PropertyListConfiguration.java
- /*
- * 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.
- * <p>
- * References:
- * <ul>
- * <li><a href=
- * "https://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
- */
- public class PropertyListConfiguration extends BaseHierarchicalConfiguration implements FileBasedConfiguration {
- /**
- * 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 {
- /**
- * 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);
- }
- }
- /**
- * 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);
- /**
- * 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));
- }
- /**
- * 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;
- }
- /**
- * 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 final class DateFieldParser extends DateComponentParser {
- /** Stores the calendar field to be processed. */
- private final int calendarField;
- /** Stores the length of this field. */
- private final int length;
- /** An optional offset to add to the calendar field. */
- private final int offset;
- /**
- * 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 {@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.
- */
- private static final class DateSeparatorParser extends DateComponentParser {
- /** Stores the separator. */
- private final String separator;
- /**
- * 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.
- */
- private static final 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;
- }
- }
- /** 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. */
- private static final DateComponentParser TIME_SEPARATOR_PARSER = new DateSeparatorParser(":");
- /** 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. */
- 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. */
- private static final String TIME_ZONE_PREFIX = "GMT";
- /** Constant for the milliseconds of a minute. */
- private static final int MILLIS_PER_MINUTE = 1000 * 60;
- /** Constant for the minutes per hour. */
- private static final int MINUTES_PER_HOUR = 60;
- /** Size of the indentation for the generated file. */
- private static final int INDENT_SIZE = 4;
- /** 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. */
- private static final char PAD_CHAR = '0';
- /**
- * 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.
- *
- * @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);
- }
- /**
- * 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();
- }
- /**
- * 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<>();
- src.forEach((k, v) -> {
- if (k instanceof String) {
- dest.put((String) k, v);
- }
- });
- return dest;
- }
- /**
- * 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 {@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 {@code PropertyListConfiguration} with the given root node.
- *
- * @param root the root node
- */
- PropertyListConfiguration(final ImmutableNode root) {
- super(new InMemoryNodeModel(root));
- }
- @Override
- protected void addPropertyInternal(final String key, final Object value) {
- if (value instanceof byte[]) {
- addPropertyDirect(key, value);
- } else {
- super.addPropertyInternal(key, value);
- }
- }
- /**
- * 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.
- */
- 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:
- * <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.replace("\"", "\\\"");
- s = "\"" + s + "\"";
- }
- return s;
- }
- @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
- 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
- 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();
- }
- }