XMLPropertyListConfiguration.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.math.BigDecimal;
  22. import java.math.BigInteger;
  23. import java.nio.charset.Charset;
  24. import java.nio.charset.StandardCharsets;
  25. import java.text.DateFormat;
  26. import java.text.ParseException;
  27. import java.text.SimpleDateFormat;
  28. import java.util.ArrayList;
  29. import java.util.Arrays;
  30. import java.util.Base64;
  31. import java.util.Calendar;
  32. import java.util.Collection;
  33. import java.util.Date;
  34. import java.util.HashMap;
  35. import java.util.Iterator;
  36. import java.util.LinkedList;
  37. import java.util.List;
  38. import java.util.Map;
  39. import java.util.TimeZone;

  40. import javax.xml.parsers.SAXParser;
  41. import javax.xml.parsers.SAXParserFactory;

  42. import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
  43. import org.apache.commons.configuration2.FileBasedConfiguration;
  44. import org.apache.commons.configuration2.HierarchicalConfiguration;
  45. import org.apache.commons.configuration2.ImmutableConfiguration;
  46. import org.apache.commons.configuration2.MapConfiguration;
  47. import org.apache.commons.configuration2.ex.ConfigurationException;
  48. import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
  49. import org.apache.commons.configuration2.io.FileLocator;
  50. import org.apache.commons.configuration2.io.FileLocatorAware;
  51. import org.apache.commons.configuration2.tree.ImmutableNode;
  52. import org.apache.commons.configuration2.tree.InMemoryNodeModel;
  53. import org.apache.commons.lang3.StringUtils;
  54. import org.apache.commons.text.StringEscapeUtils;
  55. import org.xml.sax.Attributes;
  56. import org.xml.sax.EntityResolver;
  57. import org.xml.sax.InputSource;
  58. import org.xml.sax.SAXException;
  59. import org.xml.sax.helpers.DefaultHandler;

  60. /**
  61.  * Property list file (plist) in XML FORMAT as used by macOS X (http://www.apple.com/DTDs/PropertyList-1.0.dtd). This
  62.  * configuration doesn't support the binary FORMAT used in OS X 10.4.
  63.  *
  64.  * <p>
  65.  * Example:
  66.  * </p>
  67.  *
  68.  * <pre>
  69.  * &lt;?xml version="1.0"?&gt;
  70.  * &lt;!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd"&gt;
  71.  * &lt;plist version="1.0"&gt;
  72.  *     &lt;dict&gt;
  73.  *         &lt;key&gt;string&lt;/key&gt;
  74.  *         &lt;string&gt;value1&lt;/string&gt;
  75.  *
  76.  *         &lt;key&gt;integer&lt;/key&gt;
  77.  *         &lt;integer&gt;12345&lt;/integer&gt;
  78.  *
  79.  *         &lt;key&gt;real&lt;/key&gt;
  80.  *         &lt;real&gt;-123.45E-1&lt;/real&gt;
  81.  *
  82.  *         &lt;key&gt;boolean&lt;/key&gt;
  83.  *         &lt;true/&gt;
  84.  *
  85.  *         &lt;key&gt;date&lt;/key&gt;
  86.  *         &lt;date&gt;2005-01-01T12:00:00Z&lt;/date&gt;
  87.  *
  88.  *         &lt;key&gt;data&lt;/key&gt;
  89.  *         &lt;data&gt;RHJhY28gRG9ybWllbnMgTnVucXVhbSBUaXRpbGxhbmR1cw==&lt;/data&gt;
  90.  *
  91.  *         &lt;key&gt;array&lt;/key&gt;
  92.  *         &lt;array&gt;
  93.  *             &lt;string&gt;value1&lt;/string&gt;
  94.  *             &lt;string&gt;value2&lt;/string&gt;
  95.  *             &lt;string&gt;value3&lt;/string&gt;
  96.  *         &lt;/array&gt;
  97.  *
  98.  *         &lt;key&gt;dictionnary&lt;/key&gt;
  99.  *         &lt;dict&gt;
  100.  *             &lt;key&gt;key1&lt;/key&gt;
  101.  *             &lt;string&gt;value1&lt;/string&gt;
  102.  *             &lt;key&gt;key2&lt;/key&gt;
  103.  *             &lt;string&gt;value2&lt;/string&gt;
  104.  *             &lt;key&gt;key3&lt;/key&gt;
  105.  *             &lt;string&gt;value3&lt;/string&gt;
  106.  *         &lt;/dict&gt;
  107.  *
  108.  *         &lt;key&gt;nested&lt;/key&gt;
  109.  *         &lt;dict&gt;
  110.  *             &lt;key&gt;node1&lt;/key&gt;
  111.  *             &lt;dict&gt;
  112.  *                 &lt;key&gt;node2&lt;/key&gt;
  113.  *                 &lt;dict&gt;
  114.  *                     &lt;key&gt;node3&lt;/key&gt;
  115.  *                     &lt;string&gt;value&lt;/string&gt;
  116.  *                 &lt;/dict&gt;
  117.  *             &lt;/dict&gt;
  118.  *         &lt;/dict&gt;
  119.  *
  120.  *     &lt;/dict&gt;
  121.  * &lt;/plist&gt;
  122.  * </pre>
  123.  *
  124.  * @since 1.2
  125.  */
  126. public class XMLPropertyListConfiguration extends BaseHierarchicalConfiguration implements FileBasedConfiguration, FileLocatorAware {
  127.     /**
  128.      * Container for array elements. <strong>Do not use this class !</strong> It is used internally by XMLPropertyConfiguration to
  129.      * parse the configuration file, it may be removed at any moment in the future.
  130.      */
  131.     private static final class ArrayNodeBuilder extends PListNodeBuilder {
  132.         /** The list of values in the array. */
  133.         private final List<Object> list = new ArrayList<>();

  134.         /**
  135.          * Add an object to the array.
  136.          *
  137.          * @param value the value to be added
  138.          */
  139.         @Override
  140.         public void addValue(final Object value) {
  141.             list.add(value);
  142.         }

  143.         /**
  144.          * Return the list of values in the array.
  145.          *
  146.          * @return the {@link List} of values
  147.          */
  148.         @Override
  149.         protected Object getNodeValue() {
  150.             return list;
  151.         }
  152.     }

  153.     /**
  154.      * A specialized builder class with addXXX methods to parse the typed data passed by the SAX handler. It is used for
  155.      * creating the nodes of the configuration.
  156.      */
  157.     private static class PListNodeBuilder {
  158.         /**
  159.          * The MacOS FORMAT of dates in plist files. Note: Because {@code SimpleDateFormat} is not thread-safe, each access has
  160.          * to be synchronized.
  161.          */
  162.         private static final DateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
  163.         static {
  164.             FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
  165.         }

  166.         /**
  167.          * The GNUstep FORMAT of dates in plist files. Note: Because {@code SimpleDateFormat} is not thread-safe, each access
  168.          * has to be synchronized.
  169.          */
  170.         private static final DateFormat GNUSTEP_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");

  171.         /** A collection with child builders of this builder. */
  172.         private final Collection<PListNodeBuilder> childBuilders = new LinkedList<>();

  173.         /** The name of the represented node. */
  174.         private String name;

  175.         /** The current value of the represented node. */
  176.         private Object value;

  177.         /**
  178.          * Adds the given child builder to this builder.
  179.          *
  180.          * @param child the child builder to be added
  181.          */
  182.         public void addChild(final PListNodeBuilder child) {
  183.             childBuilders.add(child);
  184.         }

  185.         /**
  186.          * Parse the specified string as a byte array in base 64 FORMAT and add it to the values of the node.
  187.          *
  188.          * @param value the value to be added
  189.          */
  190.         public void addDataValue(final String value) {
  191.             addValue(Base64.getMimeDecoder().decode(value.getBytes(DATA_ENCODING)));
  192.         }

  193.         /**
  194.          * Parse the specified string as a date and add it to the values of the node.
  195.          *
  196.          * @param value the value to be added
  197.          * @throws IllegalArgumentException if the date string cannot be parsed
  198.          */
  199.         public void addDateValue(final String value) {
  200.             try {
  201.                 if (value.indexOf(' ') != -1) {
  202.                     // parse the date using the GNUstep FORMAT
  203.                     synchronized (GNUSTEP_FORMAT) {
  204.                         addValue(GNUSTEP_FORMAT.parse(value));
  205.                     }
  206.                 } else {
  207.                     // parse the date using the MacOS X FORMAT
  208.                     synchronized (FORMAT) {
  209.                         addValue(FORMAT.parse(value));
  210.                     }
  211.                 }
  212.             } catch (final ParseException e) {
  213.                 throw new IllegalArgumentException(String.format("'%s' cannot be parsed to a date!", value), e);
  214.             }
  215.         }

  216.         /**
  217.          * Add a boolean value 'false' to the values of the node.
  218.          */
  219.         public void addFalseValue() {
  220.             addValue(Boolean.FALSE);
  221.         }

  222.         /**
  223.          * Parse the specified string as an Interger and add it to the values of the node.
  224.          *
  225.          * @param value the value to be added
  226.          */
  227.         public void addIntegerValue(final String value) {
  228.             addValue(new BigInteger(value));
  229.         }

  230.         /**
  231.          * Add a sublist to the values of the node.
  232.          *
  233.          * @param node the node whose value will be added to the current node value
  234.          */
  235.         public void addList(final ArrayNodeBuilder node) {
  236.             addValue(node.getNodeValue());
  237.         }

  238.         /**
  239.          * Parse the specified string as a Double and add it to the values of the node.
  240.          *
  241.          * @param value the value to be added
  242.          */
  243.         public void addRealValue(final String value) {
  244.             addValue(new BigDecimal(value));
  245.         }

  246.         /**
  247.          * Add a boolean value 'true' to the values of the node.
  248.          */
  249.         public void addTrueValue() {
  250.             addValue(Boolean.TRUE);
  251.         }

  252.         /**
  253.          * Update the value of the node. If the existing value is null, it's replaced with the new value. If the existing value
  254.          * is a list, the specified value is appended to the list. If the existing value is not null, a list with the two values
  255.          * is built.
  256.          *
  257.          * @param v the value to be added
  258.          */
  259.         public void addValue(final Object v) {
  260.             if (value == null) {
  261.                 value = v;
  262.             } else if (value instanceof Collection) {
  263.                 // This is safe because we create the collections ourselves
  264.                 @SuppressWarnings("unchecked")
  265.                 final Collection<Object> collection = (Collection<Object>) value;
  266.                 collection.add(v);
  267.             } else {
  268.                 final List<Object> list = new ArrayList<>();
  269.                 list.add(value);
  270.                 list.add(v);
  271.                 value = list;
  272.             }
  273.         }

  274.         /**
  275.          * Creates the configuration node defined by this builder.
  276.          *
  277.          * @return the newly created configuration node
  278.          */
  279.         public ImmutableNode createNode() {
  280.             final ImmutableNode.Builder nodeBuilder = new ImmutableNode.Builder(childBuilders.size());
  281.             childBuilders.forEach(child -> nodeBuilder.addChild(child.createNode()));
  282.             return nodeBuilder.name(name).value(getNodeValue()).create();
  283.         }

  284.         /**
  285.          * Gets the final value for the node to be created. This method is called when the represented configuration node is
  286.          * actually created.
  287.          *
  288.          * @return the value of the resulting configuration node
  289.          */
  290.         protected Object getNodeValue() {
  291.             return value;
  292.         }

  293.         /**
  294.          * Sets the name of the represented node.
  295.          *
  296.          * @param nodeName the node name
  297.          */
  298.         public void setName(final String nodeName) {
  299.             name = nodeName;
  300.         }
  301.     }

  302.     /**
  303.      * SAX Handler to build the configuration nodes while the document is being parsed.
  304.      */
  305.     private final class XMLPropertyListHandler extends DefaultHandler {
  306.         /** The buffer containing the text node being read */
  307.         private final StringBuilder buffer = new StringBuilder();

  308.         /** The stack of configuration nodes */
  309.         private final List<PListNodeBuilder> stack = new ArrayList<>();

  310.         /** The builder for the resulting node. */
  311.         private final PListNodeBuilder resultBuilder;

  312.         public XMLPropertyListHandler() {
  313.             resultBuilder = new PListNodeBuilder();
  314.             push(resultBuilder);
  315.         }

  316.         @Override
  317.         public void characters(final char[] ch, final int start, final int length) throws SAXException {
  318.             buffer.append(ch, start, length);
  319.         }

  320.         @Override
  321.         public void endElement(final String uri, final String localName, final String qName) throws SAXException {
  322.             if ("key".equals(qName)) {
  323.                 // create a new node, link it to its parent and push it on the stack
  324.                 final PListNodeBuilder node = new PListNodeBuilder();
  325.                 node.setName(buffer.toString());
  326.                 peekNE().addChild(node);
  327.                 push(node);
  328.             } else if ("dict".equals(qName)) {
  329.                 // remove the root of the XMLPropertyListConfiguration previously pushed on the stack
  330.                 final PListNodeBuilder builder = pop();
  331.                 assert builder != null : "Stack was empty!";
  332.                 if (peek() instanceof ArrayNodeBuilder) {
  333.                     // create the configuration
  334.                     final XMLPropertyListConfiguration config = new XMLPropertyListConfiguration(builder.createNode());

  335.                     // add it to the ArrayNodeBuilder
  336.                     final ArrayNodeBuilder node = (ArrayNodeBuilder) peekNE();
  337.                     node.addValue(config);
  338.                 }
  339.             } else {
  340.                 switch (qName) {
  341.                 case "string":
  342.                     peekNE().addValue(buffer.toString());
  343.                     break;
  344.                 case "integer":
  345.                     peekNE().addIntegerValue(buffer.toString());
  346.                     break;
  347.                 case "real":
  348.                     peekNE().addRealValue(buffer.toString());
  349.                     break;
  350.                 case "true":
  351.                     peekNE().addTrueValue();
  352.                     break;
  353.                 case "false":
  354.                     peekNE().addFalseValue();
  355.                     break;
  356.                 case "data":
  357.                     peekNE().addDataValue(buffer.toString());
  358.                     break;
  359.                 case "date":
  360.                     try {
  361.                         peekNE().addDateValue(buffer.toString());
  362.                     } catch (final IllegalArgumentException iex) {
  363.                         getLogger().warn("Ignoring invalid date property " + buffer);
  364.                     }
  365.                     break;
  366.                 case "array": {
  367.                     final ArrayNodeBuilder array = (ArrayNodeBuilder) pop();
  368.                     peekNE().addList(array);
  369.                     break;
  370.                 }
  371.                 default:
  372.                     break;
  373.                 }

  374.                 // remove the plist node on the stack once the value has been parsed,
  375.                 // array nodes remains on the stack for the next values in the list
  376.                 if (!(peek() instanceof ArrayNodeBuilder)) {
  377.                     pop();
  378.                 }
  379.             }

  380.             buffer.setLength(0);
  381.         }

  382.         /**
  383.          * Gets the builder for the result node.
  384.          *
  385.          * @return the result node builder
  386.          */
  387.         public PListNodeBuilder getResultBuilder() {
  388.             return resultBuilder;
  389.         }

  390.         /**
  391.          * Return the node on the top of the stack.
  392.          */
  393.         private PListNodeBuilder peek() {
  394.             if (!stack.isEmpty()) {
  395.                 return stack.get(stack.size() - 1);
  396.             }
  397.             return null;
  398.         }

  399.         /**
  400.          * Returns the node on top of the non-empty stack. Throws an exception if the stack is empty.
  401.          *
  402.          * @return the top node of the stack
  403.          * @throws ConfigurationRuntimeException if the stack is empty
  404.          */
  405.         private PListNodeBuilder peekNE() {
  406.             final PListNodeBuilder result = peek();
  407.             if (result == null) {
  408.                 throw new ConfigurationRuntimeException("Access to empty stack!");
  409.             }
  410.             return result;
  411.         }

  412.         /**
  413.          * Remove and return the node on the top of the stack.
  414.          */
  415.         private PListNodeBuilder pop() {
  416.             if (!stack.isEmpty()) {
  417.                 return stack.remove(stack.size() - 1);
  418.             }
  419.             return null;
  420.         }

  421.         /**
  422.          * Put a node on the top of the stack.
  423.          */
  424.         private void push(final PListNodeBuilder node) {
  425.             stack.add(node);
  426.         }

  427.         @Override
  428.         public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) throws SAXException {
  429.             if ("array".equals(qName)) {
  430.                 push(new ArrayNodeBuilder());
  431.             } else if ("dict".equals(qName) && peek() instanceof ArrayNodeBuilder) {
  432.                 // push the new root builder on the stack
  433.                 push(new PListNodeBuilder());
  434.             }
  435.         }
  436.     }

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

  439.     /** Constant for the encoding for binary data. */
  440.     private static final Charset DATA_ENCODING = StandardCharsets.UTF_8;

  441.     /**
  442.      * Transform a map of arbitrary types into a map with string keys and object values. All keys of the source map which
  443.      * are not of type String are dropped.
  444.      *
  445.      * @param src the map to be converted
  446.      * @return the resulting map
  447.      */
  448.     private static Map<String, Object> transformMap(final Map<?, ?> src) {
  449.         final Map<String, Object> dest = new HashMap<>();
  450.         for (final Map.Entry<?, ?> e : src.entrySet()) {
  451.             if (e.getKey() instanceof String) {
  452.                 dest.put((String) e.getKey(), e.getValue());
  453.             }
  454.         }
  455.         return dest;
  456.     }

  457.     /** Temporarily stores the current file location. */
  458.     private FileLocator locator;

  459.     /**
  460.      * Creates an empty XMLPropertyListConfiguration object which can be used to synthesize a new plist file by adding
  461.      * values and then saving().
  462.      */
  463.     public XMLPropertyListConfiguration() {
  464.     }

  465.     /**
  466.      * Creates a new instance of {@code XMLPropertyListConfiguration} and copies the content of the specified configuration
  467.      * into this object.
  468.      *
  469.      * @param configuration the configuration to copy
  470.      * @since 1.4
  471.      */
  472.     public XMLPropertyListConfiguration(final HierarchicalConfiguration<ImmutableNode> configuration) {
  473.         super(configuration);
  474.     }

  475.     /**
  476.      * Creates a new instance of {@code XMLPropertyConfiguration} with the given root node.
  477.      *
  478.      * @param root the root node
  479.      */
  480.     XMLPropertyListConfiguration(final ImmutableNode root) {
  481.         super(new InMemoryNodeModel(root));
  482.     }

  483.     @Override
  484.     protected void addPropertyInternal(final String key, final Object value) {
  485.         if (value instanceof byte[] || value instanceof List) {
  486.             addPropertyDirect(key, value);
  487.         } else if (value instanceof Object[]) {
  488.             addPropertyDirect(key, Arrays.asList((Object[]) value));
  489.         } else {
  490.             super.addPropertyInternal(key, value);
  491.         }
  492.     }

  493.     /**
  494.      * Stores the current file locator. This method is called before I/O operations.
  495.      *
  496.      * @param locator the current {@code FileLocator}
  497.      */
  498.     @Override
  499.     public void initFileLocator(final FileLocator locator) {
  500.         this.locator = locator;
  501.     }

  502.     /**
  503.      * Append a node to the writer, indented according to a specific level.
  504.      */
  505.     private void printNode(final PrintWriter out, final int indentLevel, final ImmutableNode node) {
  506.         final String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);

  507.         if (node.getNodeName() != null) {
  508.             out.println(padding + "<key>" + StringEscapeUtils.escapeXml10(node.getNodeName()) + "</key>");
  509.         }

  510.         final List<ImmutableNode> children = node.getChildren();
  511.         if (!children.isEmpty()) {
  512.             out.println(padding + "<dict>");

  513.             final Iterator<ImmutableNode> it = children.iterator();
  514.             while (it.hasNext()) {
  515.                 final ImmutableNode child = it.next();
  516.                 printNode(out, indentLevel + 1, child);

  517.                 if (it.hasNext()) {
  518.                     out.println();
  519.                 }
  520.             }

  521.             out.println(padding + "</dict>");
  522.         } else if (node.getValue() == null) {
  523.             out.println(padding + "<dict/>");
  524.         } else {
  525.             final Object value = node.getValue();
  526.             printValue(out, indentLevel, value);
  527.         }
  528.     }

  529.     /**
  530.      * Append a value to the writer, indented according to a specific level.
  531.      */
  532.     private void printValue(final PrintWriter out, final int indentLevel, final Object value) {
  533.         final String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);

  534.         if (value instanceof Date) {
  535.             synchronized (PListNodeBuilder.FORMAT) {
  536.                 out.println(padding + "<date>" + PListNodeBuilder.FORMAT.format((Date) value) + "</date>");
  537.             }
  538.         } else if (value instanceof Calendar) {
  539.             printValue(out, indentLevel, ((Calendar) value).getTime());
  540.         } else if (value instanceof Number) {
  541.             if (value instanceof Double || value instanceof Float || value instanceof BigDecimal) {
  542.                 out.println(padding + "<real>" + value.toString() + "</real>");
  543.             } else {
  544.                 out.println(padding + "<integer>" + value.toString() + "</integer>");
  545.             }
  546.         } else if (value instanceof Boolean) {
  547.             if (((Boolean) value).booleanValue()) {
  548.                 out.println(padding + "<true/>");
  549.             } else {
  550.                 out.println(padding + "<false/>");
  551.             }
  552.         } else if (value instanceof List) {
  553.             out.println(padding + "<array>");
  554.             ((List<?>) value).forEach(o -> printValue(out, indentLevel + 1, o));
  555.             out.println(padding + "</array>");
  556.         } else if (value instanceof HierarchicalConfiguration) {
  557.             // This is safe because we have created this configuration
  558.             @SuppressWarnings("unchecked")
  559.             final HierarchicalConfiguration<ImmutableNode> config = (HierarchicalConfiguration<ImmutableNode>) value;
  560.             printNode(out, indentLevel, config.getNodeModel().getNodeHandler().getRootNode());
  561.         } else if (value instanceof ImmutableConfiguration) {
  562.             // display a flat Configuration as a dictionary
  563.             out.println(padding + "<dict>");

  564.             final ImmutableConfiguration config = (ImmutableConfiguration) value;
  565.             final Iterator<String> it = config.getKeys();
  566.             while (it.hasNext()) {
  567.                 // create a node for each property
  568.                 final String key = it.next();
  569.                 final ImmutableNode node = new ImmutableNode.Builder().name(key).value(config.getProperty(key)).create();

  570.                 // print the node
  571.                 printNode(out, indentLevel + 1, node);

  572.                 if (it.hasNext()) {
  573.                     out.println();
  574.                 }
  575.             }
  576.             out.println(padding + "</dict>");
  577.         } else if (value instanceof Map) {
  578.             // display a Map as a dictionary
  579.             final Map<String, Object> map = transformMap((Map<?, ?>) value);
  580.             printValue(out, indentLevel, new MapConfiguration(map));
  581.         } else if (value instanceof byte[]) {
  582.             final String base64 = new String(Base64.getMimeEncoder().encode((byte[]) value), DATA_ENCODING);
  583.             out.println(padding + "<data>" + StringEscapeUtils.escapeXml10(base64) + "</data>");
  584.         } else if (value != null) {
  585.             out.println(padding + "<string>" + StringEscapeUtils.escapeXml10(String.valueOf(value)) + "</string>");
  586.         } else {
  587.             out.println(padding + "<string/>");
  588.         }
  589.     }

  590.     @Override
  591.     public void read(final Reader in) throws ConfigurationException {
  592.         // set up the DTD validation
  593.         final EntityResolver resolver = (publicId, systemId) -> new InputSource(getClass().getClassLoader().getResourceAsStream("PropertyList-1.0.dtd"));

  594.         // parse the file
  595.         final XMLPropertyListHandler handler = new XMLPropertyListHandler();
  596.         try {
  597.             final SAXParserFactory factory = SAXParserFactory.newInstance();
  598.             factory.setValidating(true);

  599.             final SAXParser parser = factory.newSAXParser();
  600.             parser.getXMLReader().setEntityResolver(resolver);
  601.             parser.getXMLReader().setContentHandler(handler);
  602.             parser.getXMLReader().parse(new InputSource(in));

  603.             getNodeModel().mergeRoot(handler.getResultBuilder().createNode(), null, null, null, this);
  604.         } catch (final Exception e) {
  605.             throw new ConfigurationException("Unable to parse the configuration file", e);
  606.         }
  607.     }

  608.     private void setPropertyDirect(final String key, final Object value) {
  609.         setDetailEvents(false);
  610.         try {
  611.             clearProperty(key);
  612.             addPropertyDirect(key, value);
  613.         } finally {
  614.             setDetailEvents(true);
  615.         }
  616.     }

  617.     @Override
  618.     protected void setPropertyInternal(final String key, final Object value) {
  619.         // special case for byte arrays, they must be stored as is in the configuration
  620.         if (value instanceof byte[] || value instanceof List) {
  621.             setPropertyDirect(key, value);
  622.         } else if (value instanceof Object[]) {
  623.             setPropertyDirect(key, Arrays.asList((Object[]) value));
  624.         } else {
  625.             super.setPropertyInternal(key, value);
  626.         }
  627.     }

  628.     @Override
  629.     public void write(final Writer out) throws ConfigurationException {
  630.         if (locator == null) {
  631.             throw new ConfigurationException(
  632.                 "Save operation not properly " + "initialized! Do not call write(Writer) directly," + " but use a FileHandler to save a configuration.");
  633.         }
  634.         final PrintWriter writer = new PrintWriter(out);

  635.         if (locator.getEncoding() != null) {
  636.             writer.println("<?xml version=\"1.0\" encoding=\"" + locator.getEncoding() + "\"?>");
  637.         } else {
  638.             writer.println("<?xml version=\"1.0\"?>");
  639.         }

  640.         writer.println("<!DOCTYPE plist SYSTEM \"file://localhost/System/Library/DTDs/PropertyList.dtd\">");
  641.         writer.println("<plist version=\"1.0\">");

  642.         printNode(writer, 1, getNodeModel().getNodeHandler().getRootNode());

  643.         writer.println("</plist>");
  644.         writer.flush();
  645.     }
  646. }