PropertyListConfiguration.java

  1. /*
  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
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  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.
  16.  */

  17. package org.apache.commons.configuration2.plist;

  18. import java.io.PrintWriter;
  19. import java.io.Reader;
  20. import java.io.Writer;
  21. import java.util.ArrayList;
  22. import java.util.Calendar;
  23. import java.util.Date;
  24. import java.util.HashMap;
  25. import java.util.Iterator;
  26. import java.util.List;
  27. import java.util.Map;
  28. import java.util.TimeZone;

  29. import org.apache.commons.codec.binary.Hex;
  30. import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
  31. import org.apache.commons.configuration2.Configuration;
  32. import org.apache.commons.configuration2.FileBasedConfiguration;
  33. import org.apache.commons.configuration2.HierarchicalConfiguration;
  34. import org.apache.commons.configuration2.ImmutableConfiguration;
  35. import org.apache.commons.configuration2.MapConfiguration;
  36. import org.apache.commons.configuration2.ex.ConfigurationException;
  37. import org.apache.commons.configuration2.tree.ImmutableNode;
  38. import org.apache.commons.configuration2.tree.InMemoryNodeModel;
  39. import org.apache.commons.configuration2.tree.NodeHandler;
  40. import org.apache.commons.lang3.StringUtils;

  41. /**
  42.  * NeXT / OpenStep style configuration. This configuration can read and write ASCII plist files. It supports the GNUStep
  43.  * extension to specify date objects.
  44.  * <p>
  45.  * References:
  46.  * <ul>
  47.  * <li><a href=
  48.  * "https://developer.apple.com/documentation/Cocoa/Conceptual/PropertyLists/OldStylePlists/OldStylePLists.html"> Apple
  49.  * Documentation - Old-Style ASCII Property Lists</a></li>
  50.  * <li><a href="http://www.gnustep.org/resources/documentation/Developer/Base/Reference/NSPropertyList.html"> GNUStep
  51.  * Documentation</a></li>
  52.  * </ul>
  53.  *
  54.  * <p>
  55.  * Example:
  56.  * </p>
  57.  *
  58.  * <pre>
  59.  * {
  60.  *     foo = "bar";
  61.  *
  62.  *     array = ( value1, value2, value3 );
  63.  *
  64.  *     data = &lt;4f3e0145ab&gt;;
  65.  *
  66.  *     date = &lt;*D2007-05-05 20:05:00 +0100&gt;;
  67.  *
  68.  *     nested =
  69.  *     {
  70.  *         key1 = value1;
  71.  *         key2 = value;
  72.  *         nested =
  73.  *         {
  74.  *             foo = bar
  75.  *         }
  76.  *     }
  77.  * }
  78.  * </pre>
  79.  *
  80.  * @since 1.2
  81.  */
  82. public class PropertyListConfiguration extends BaseHierarchicalConfiguration implements FileBasedConfiguration {
  83.     /**
  84.      * A helper class for parsing and formatting date literals. Usually we would use {@code SimpleDateFormat} for this
  85.      * purpose, but in Java 1.3 the functionality of this class is limited. So we have a hierarchy of parser classes instead
  86.      * that deal with the different components of a date literal.
  87.      */
  88.     private abstract static class DateComponentParser {
  89.         /**
  90.          * Checks whether the given string has at least {@code length} characters starting from the given parsing position. If
  91.          * this is not the case, an exception will be thrown.
  92.          *
  93.          * @param s the string to be tested
  94.          * @param index the current index
  95.          * @param length the minimum length after the index
  96.          * @throws ParseException if the string is too short
  97.          */
  98.         protected void checkLength(final String s, final int index, final int length) throws ParseException {
  99.             final int len = s == null ? 0 : s.length();
  100.             if (index + length > len) {
  101.                 throw new ParseException("Input string too short: " + s + ", index: " + index);
  102.             }
  103.         }

  104.         /**
  105.          * Formats a date component. This method is used for converting a date in its internal representation into a string
  106.          * literal.
  107.          *
  108.          * @param buf the target buffer
  109.          * @param cal the calendar with the current date
  110.          */
  111.         public abstract void formatComponent(StringBuilder buf, Calendar cal);

  112.         /**
  113.          * Adds a number to the given string buffer and adds leading '0' characters until the given length is reached.
  114.          *
  115.          * @param buf the target buffer
  116.          * @param num the number to add
  117.          * @param length the required length
  118.          */
  119.         protected void padNum(final StringBuilder buf, final int num, final int length) {
  120.             buf.append(StringUtils.leftPad(String.valueOf(num), length, PAD_CHAR));
  121.         }

  122.         /**
  123.          * Parses a component from the given input string.
  124.          *
  125.          * @param s the string to be parsed
  126.          * @param index the current parsing position
  127.          * @param cal the calendar where to store the result
  128.          * @return the length of the processed component
  129.          * @throws ParseException if the component cannot be extracted
  130.          */
  131.         public abstract int parseComponent(String s, int index, Calendar cal) throws ParseException;
  132.     }

  133.     /**
  134.      * A specialized date component parser implementation that deals with numeric calendar fields. The class is able to
  135.      * extract fields from a string literal and to format a literal from a calendar.
  136.      */
  137.     private static final class DateFieldParser extends DateComponentParser {
  138.         /** Stores the calendar field to be processed. */
  139.         private final int calendarField;

  140.         /** Stores the length of this field. */
  141.         private final int length;

  142.         /** An optional offset to add to the calendar field. */
  143.         private final int offset;

  144.         /**
  145.          * Creates a new instance of {@code DateFieldParser}.
  146.          *
  147.          * @param calFld the calendar field code
  148.          * @param len the length of this field
  149.          */
  150.         public DateFieldParser(final int calFld, final int len) {
  151.             this(calFld, len, 0);
  152.         }

  153.         /**
  154.          * Creates a new instance of {@code DateFieldParser} and fully initializes it.
  155.          *
  156.          * @param calFld the calendar field code
  157.          * @param len the length of this field
  158.          * @param ofs an offset to add to the calendar field
  159.          */
  160.         public DateFieldParser(final int calFld, final int len, final int ofs) {
  161.             calendarField = calFld;
  162.             length = len;
  163.             offset = ofs;
  164.         }

  165.         @Override
  166.         public void formatComponent(final StringBuilder buf, final Calendar cal) {
  167.             padNum(buf, cal.get(calendarField) + offset, length);
  168.         }

  169.         @Override
  170.         public int parseComponent(final String s, final int index, final Calendar cal) throws ParseException {
  171.             checkLength(s, index, length);
  172.             try {
  173.                 cal.set(calendarField, Integer.parseInt(s.substring(index, index + length)) - offset);
  174.                 return length;
  175.             } catch (final NumberFormatException nfex) {
  176.                 throw new ParseException("Invalid number: " + s + ", index " + index);
  177.             }
  178.         }
  179.     }

  180.     /**
  181.      * A specialized date component parser implementation that deals with separator characters.
  182.      */
  183.     private static final class DateSeparatorParser extends DateComponentParser {
  184.         /** Stores the separator. */
  185.         private final String separator;

  186.         /**
  187.          * Creates a new instance of {@code DateSeparatorParser} and sets the separator string.
  188.          *
  189.          * @param sep the separator string
  190.          */
  191.         public DateSeparatorParser(final String sep) {
  192.             separator = sep;
  193.         }

  194.         @Override
  195.         public void formatComponent(final StringBuilder buf, final Calendar cal) {
  196.             buf.append(separator);
  197.         }

  198.         @Override
  199.         public int parseComponent(final String s, final int index, final Calendar cal) throws ParseException {
  200.             checkLength(s, index, separator.length());
  201.             if (!s.startsWith(separator, index)) {
  202.                 throw new ParseException("Invalid input: " + s + ", index " + index + ", expected " + separator);
  203.             }
  204.             return separator.length();
  205.         }
  206.     }

  207.     /**
  208.      * A specialized date component parser implementation that deals with the time zone part of a date component.
  209.      */
  210.     private static final class DateTimeZoneParser extends DateComponentParser {
  211.         @Override
  212.         public void formatComponent(final StringBuilder buf, final Calendar cal) {
  213.             final TimeZone tz = cal.getTimeZone();
  214.             int ofs = tz.getRawOffset() / MILLIS_PER_MINUTE;
  215.             if (ofs < 0) {
  216.                 buf.append('-');
  217.                 ofs = -ofs;
  218.             } else {
  219.                 buf.append('+');
  220.             }
  221.             final int hour = ofs / MINUTES_PER_HOUR;
  222.             final int min = ofs % MINUTES_PER_HOUR;
  223.             padNum(buf, hour, 2);
  224.             padNum(buf, min, 2);
  225.         }

  226.         @Override
  227.         public int parseComponent(final String s, final int index, final Calendar cal) throws ParseException {
  228.             checkLength(s, index, TIME_ZONE_LENGTH);
  229.             final TimeZone tz = TimeZone.getTimeZone(TIME_ZONE_PREFIX + s.substring(index, index + TIME_ZONE_LENGTH));
  230.             cal.setTimeZone(tz);
  231.             return TIME_ZONE_LENGTH;
  232.         }
  233.     }

  234.     /** Constant for the separator parser for the date part. */
  235.     private static final DateComponentParser DATE_SEPARATOR_PARSER = new DateSeparatorParser("-");

  236.     /** Constant for the separator parser for the time part. */
  237.     private static final DateComponentParser TIME_SEPARATOR_PARSER = new DateSeparatorParser(":");

  238.     /** Constant for the separator parser for blanks between the parts. */
  239.     private static final DateComponentParser BLANK_SEPARATOR_PARSER = new DateSeparatorParser(" ");

  240.     /** An array with the component parsers for dealing with dates. */
  241.     private static final DateComponentParser[] DATE_PARSERS = {new DateSeparatorParser("<*D"), new DateFieldParser(Calendar.YEAR, 4), DATE_SEPARATOR_PARSER,
  242.         new DateFieldParser(Calendar.MONTH, 2, 1), DATE_SEPARATOR_PARSER, new DateFieldParser(Calendar.DATE, 2), BLANK_SEPARATOR_PARSER,
  243.         new DateFieldParser(Calendar.HOUR_OF_DAY, 2), TIME_SEPARATOR_PARSER, new DateFieldParser(Calendar.MINUTE, 2), TIME_SEPARATOR_PARSER,
  244.         new DateFieldParser(Calendar.SECOND, 2), BLANK_SEPARATOR_PARSER, new DateTimeZoneParser(), new DateSeparatorParser(">")};

  245.     /** Constant for the ID prefix for GMT time zones. */
  246.     private static final String TIME_ZONE_PREFIX = "GMT";

  247.     /** Constant for the milliseconds of a minute. */
  248.     private static final int MILLIS_PER_MINUTE = 1000 * 60;

  249.     /** Constant for the minutes per hour. */
  250.     private static final int MINUTES_PER_HOUR = 60;

  251.     /** Size of the indentation for the generated file. */
  252.     private static final int INDENT_SIZE = 4;

  253.     /** Constant for the length of a time zone. */
  254.     private static final int TIME_ZONE_LENGTH = 5;

  255.     /** Constant for the padding character in the date format. */
  256.     private static final char PAD_CHAR = '0';

  257.     /**
  258.      * Returns a string representation for the date specified by the given calendar.
  259.      *
  260.      * @param cal the calendar with the initialized date
  261.      * @return a string for this date
  262.      */
  263.     static String formatDate(final Calendar cal) {
  264.         final StringBuilder buf = new StringBuilder();

  265.         for (final DateComponentParser element : DATE_PARSERS) {
  266.             element.formatComponent(buf, cal);
  267.         }

  268.         return buf.toString();
  269.     }

  270.     /**
  271.      * Returns a string representation for the specified date.
  272.      *
  273.      * @param date the date
  274.      * @return a string for this date
  275.      */
  276.     static String formatDate(final Date date) {
  277.         final Calendar cal = Calendar.getInstance();
  278.         cal.setTime(date);
  279.         return formatDate(cal);
  280.     }

  281.     /**
  282.      * Parses a date in a format like {@code <*D2002-03-22 11:30:00 +0100>}.
  283.      *
  284.      * @param s the string with the date to be parsed
  285.      * @return the parsed date
  286.      * @throws ParseException if an error occurred while parsing the string
  287.      */
  288.     static Date parseDate(final String s) throws ParseException {
  289.         final Calendar cal = Calendar.getInstance();
  290.         cal.clear();
  291.         int index = 0;

  292.         for (final DateComponentParser parser : DATE_PARSERS) {
  293.             index += parser.parseComponent(s, index, cal);
  294.         }

  295.         return cal.getTime();
  296.     }

  297.     /**
  298.      * Transform a map of arbitrary types into a map with string keys and object values. All keys of the source map which
  299.      * are not of type String are dropped.
  300.      *
  301.      * @param src the map to be converted
  302.      * @return the resulting map
  303.      */
  304.     private static Map<String, Object> transformMap(final Map<?, ?> src) {
  305.         final Map<String, Object> dest = new HashMap<>();
  306.         src.forEach((k, v) -> {
  307.             if (k instanceof String) {
  308.                 dest.put((String) k, v);
  309.             }
  310.         });
  311.         return dest;
  312.     }

  313.     /**
  314.      * Creates an empty PropertyListConfiguration object which can be used to synthesize a new plist file by adding values
  315.      * and then saving().
  316.      */
  317.     public PropertyListConfiguration() {
  318.     }

  319.     /**
  320.      * Creates a new instance of {@code PropertyListConfiguration} and copies the content of the specified configuration
  321.      * into this object.
  322.      *
  323.      * @param c the configuration to copy
  324.      * @since 1.4
  325.      */
  326.     public PropertyListConfiguration(final HierarchicalConfiguration<ImmutableNode> c) {
  327.         super(c);
  328.     }

  329.     /**
  330.      * Creates a new instance of {@code PropertyListConfiguration} with the given root node.
  331.      *
  332.      * @param root the root node
  333.      */
  334.     PropertyListConfiguration(final ImmutableNode root) {
  335.         super(new InMemoryNodeModel(root));
  336.     }

  337.     @Override
  338.     protected void addPropertyInternal(final String key, final Object value) {
  339.         if (value instanceof byte[]) {
  340.             addPropertyDirect(key, value);
  341.         } else {
  342.             super.addPropertyInternal(key, value);
  343.         }
  344.     }

  345.     /**
  346.      * Append a node to the writer, indented according to a specific level.
  347.      */
  348.     private void printNode(final PrintWriter out, final int indentLevel, final ImmutableNode node, final NodeHandler<ImmutableNode> handler) {
  349.         final String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);

  350.         if (node.getNodeName() != null) {
  351.             out.print(padding + quoteString(node.getNodeName()) + " = ");
  352.         }

  353.         final List<ImmutableNode> children = new ArrayList<>(node.getChildren());
  354.         if (!children.isEmpty()) {
  355.             // skip a line, except for the root dictionary
  356.             if (indentLevel > 0) {
  357.                 out.println();
  358.             }

  359.             out.println(padding + "{");

  360.             // display the children
  361.             final Iterator<ImmutableNode> it = children.iterator();
  362.             while (it.hasNext()) {
  363.                 final ImmutableNode child = it.next();

  364.                 printNode(out, indentLevel + 1, child, handler);

  365.                 // add a semi colon for elements that are not dictionaries
  366.                 final Object value = child.getValue();
  367.                 if (value != null && !(value instanceof Map) && !(value instanceof Configuration)) {
  368.                     out.println(";");
  369.                 }

  370.                 // skip a line after arrays and dictionaries
  371.                 if (it.hasNext() && (value == null || value instanceof List)) {
  372.                     out.println();
  373.                 }
  374.             }

  375.             out.print(padding + "}");

  376.             // line feed if the dictionary is not in an array
  377.             if (handler.getParent(node) != null) {
  378.                 out.println();
  379.             }
  380.         } else if (node.getValue() == null) {
  381.             out.println();
  382.             out.print(padding + "{ };");

  383.             // line feed if the dictionary is not in an array
  384.             if (handler.getParent(node) != null) {
  385.                 out.println();
  386.             }
  387.         } else {
  388.             // display the leaf value
  389.             final Object value = node.getValue();
  390.             printValue(out, indentLevel, value);
  391.         }
  392.     }

  393.     /**
  394.      * Append a value to the writer, indented according to a specific level.
  395.      */
  396.     private void printValue(final PrintWriter out, final int indentLevel, final Object value) {
  397.         final String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);

  398.         if (value instanceof List) {
  399.             out.print("( ");
  400.             final Iterator<?> it = ((List<?>) value).iterator();
  401.             while (it.hasNext()) {
  402.                 printValue(out, indentLevel + 1, it.next());
  403.                 if (it.hasNext()) {
  404.                     out.print(", ");
  405.                 }
  406.             }
  407.             out.print(" )");
  408.         } else if (value instanceof PropertyListConfiguration) {
  409.             final NodeHandler<ImmutableNode> handler = ((PropertyListConfiguration) value).getModel().getNodeHandler();
  410.             printNode(out, indentLevel, handler.getRootNode(), handler);
  411.         } else if (value instanceof ImmutableConfiguration) {
  412.             // display a flat Configuration as a dictionary
  413.             out.println();
  414.             out.println(padding + "{");

  415.             final ImmutableConfiguration config = (ImmutableConfiguration) value;
  416.             final Iterator<String> it = config.getKeys();
  417.             while (it.hasNext()) {
  418.                 final String key = it.next();
  419.                 final ImmutableNode node = new ImmutableNode.Builder().name(key).value(config.getProperty(key)).create();
  420.                 final InMemoryNodeModel tempModel = new InMemoryNodeModel(node);
  421.                 printNode(out, indentLevel + 1, node, tempModel.getNodeHandler());
  422.                 out.println(";");
  423.             }
  424.             out.println(padding + "}");
  425.         } else if (value instanceof Map) {
  426.             // display a Map as a dictionary
  427.             final Map<String, Object> map = transformMap((Map<?, ?>) value);
  428.             printValue(out, indentLevel, new MapConfiguration(map));
  429.         } else if (value instanceof byte[]) {
  430.             out.print("<" + new String(Hex.encodeHex((byte[]) value)) + ">");
  431.         } else if (value instanceof Date) {
  432.             out.print(formatDate((Date) value));
  433.         } else if (value != null) {
  434.             out.print(quoteString(String.valueOf(value)));
  435.         }
  436.     }

  437.     /**
  438.      * Quote the specified string if necessary, that's if the string contains:
  439.      * <ul>
  440.      * <li>a space character (' ', '\t', '\r', '\n')</li>
  441.      * <li>a quote '"'</li>
  442.      * <li>special characters in plist files ('(', ')', '{', '}', '=', ';', ',')</li>
  443.      * </ul>
  444.      * Quotes within the string are escaped.
  445.      *
  446.      * <p>
  447.      * Examples:
  448.      * </p>
  449.      * <ul>
  450.      * <li>abcd -> abcd</li>
  451.      * <li>ab cd -> "ab cd"</li>
  452.      * <li>foo"bar -> "foo\"bar"</li>
  453.      * <li>foo;bar -> "foo;bar"</li>
  454.      * </ul>
  455.      */
  456.     String quoteString(String s) {
  457.         if (s == null) {
  458.             return null;
  459.         }

  460.         if (s.indexOf(' ') != -1 || s.indexOf('\t') != -1 || s.indexOf('\r') != -1 || s.indexOf('\n') != -1 || s.indexOf('"') != -1 || s.indexOf('(') != -1
  461.             || s.indexOf(')') != -1 || s.indexOf('{') != -1 || s.indexOf('}') != -1 || s.indexOf('=') != -1 || s.indexOf(',') != -1 || s.indexOf(';') != -1) {
  462.             s = s.replace("\"", "\\\"");
  463.             s = "\"" + s + "\"";
  464.         }

  465.         return s;
  466.     }

  467.     @Override
  468.     public void read(final Reader in) throws ConfigurationException {
  469.         final PropertyListParser parser = new PropertyListParser(in);
  470.         try {
  471.             final PropertyListConfiguration config = parser.parse();
  472.             getModel().setRootNode(config.getNodeModel().getNodeHandler().getRootNode());
  473.         } catch (final ParseException e) {
  474.             throw new ConfigurationException(e);
  475.         }
  476.     }

  477.     @Override
  478.     protected void setPropertyInternal(final String key, final Object value) {
  479.         // special case for byte arrays, they must be stored as is in the configuration
  480.         if (value instanceof byte[]) {
  481.             setDetailEvents(false);
  482.             try {
  483.                 clearProperty(key);
  484.                 addPropertyDirect(key, value);
  485.             } finally {
  486.                 setDetailEvents(true);
  487.             }
  488.         } else {
  489.             super.setPropertyInternal(key, value);
  490.         }
  491.     }

  492.     @Override
  493.     public void write(final Writer out) throws ConfigurationException {
  494.         final PrintWriter writer = new PrintWriter(out);
  495.         final NodeHandler<ImmutableNode> handler = getModel().getNodeHandler();
  496.         printNode(writer, 0, handler.getRootNode(), handler);
  497.         writer.flush();
  498.     }
  499. }