    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *     https://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017
    018package org.apache.commons.configuration2.plist;
    019
    020import java.io.PrintWriter;
    021import java.io.Reader;
    022import java.io.Writer;
    023import java.math.BigDecimal;
    024import java.math.BigInteger;
    025import java.nio.charset.Charset;
    026import java.nio.charset.StandardCharsets;
    027import java.text.DateFormat;
    028import java.text.ParseException;
    029import java.text.SimpleDateFormat;
    030import java.util.ArrayList;
    031import java.util.Arrays;
    032import java.util.Base64;
    033import java.util.Calendar;
    034import java.util.Collection;
    035import java.util.Date;
    036import java.util.HashMap;
    037import java.util.Iterator;
    038import java.util.LinkedList;
    039import java.util.List;
    040import java.util.Map;
    041import java.util.TimeZone;
    042
    043import javax.xml.parsers.SAXParserFactory;
    044
    045import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
    046import org.apache.commons.configuration2.FileBasedConfiguration;
    047import org.apache.commons.configuration2.HierarchicalConfiguration;
    048import org.apache.commons.configuration2.ImmutableConfiguration;
    049import org.apache.commons.configuration2.MapConfiguration;
    050import org.apache.commons.configuration2.ex.ConfigurationException;
    051import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
    052import org.apache.commons.configuration2.io.FileLocator;
    053import org.apache.commons.configuration2.io.FileLocatorAware;
    054import org.apache.commons.configuration2.tree.ImmutableNode;
    055import org.apache.commons.configuration2.tree.InMemoryNodeModel;
    056import org.apache.commons.lang3.StringUtils;
    057import org.apache.commons.text.StringEscapeUtils;
    058import org.xml.sax.Attributes;
    059import org.xml.sax.EntityResolver;
    060import org.xml.sax.InputSource;
    061import org.xml.sax.SAXException;
    062import org.xml.sax.XMLReader;
    063import org.xml.sax.helpers.DefaultHandler;
    064
    065/**
    066 * Property list file (plist) in XML FORMAT as used by macOS X (http://www.apple.com/DTDs/PropertyList-1.0.dtd). This
    067 * configuration doesn't support the binary FORMAT used in OS X 10.4.
    068 *
    069 * <p>
    070 * Example:
    071 * </p>
    072 *
    073 * <pre>
    074 * &lt;?xml version="1.0"?&gt;
    075 * &lt;!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd"&gt;
    076 * &lt;plist version="1.0"&gt;
    077 *     &lt;dict&gt;
    078 *         &lt;key&gt;string&lt;/key&gt;
    079 *         &lt;string&gt;value1&lt;/string&gt;
    080 *
    081 *         &lt;key&gt;integer&lt;/key&gt;
    082 *         &lt;integer&gt;12345&lt;/integer&gt;
    083 *
    084 *         &lt;key&gt;real&lt;/key&gt;
    085 *         &lt;real&gt;-123.45E-1&lt;/real&gt;
    086 *
    087 *         &lt;key&gt;boolean&lt;/key&gt;
    088 *         &lt;true/&gt;
    089 *
    090 *         &lt;key&gt;date&lt;/key&gt;
    091 *         &lt;date&gt;2005-01-01T12:00:00Z&lt;/date&gt;
    092 *
    093 *         &lt;key&gt;data&lt;/key&gt;
    094 *         &lt;data&gt;RHJhY28gRG9ybWllbnMgTnVucXVhbSBUaXRpbGxhbmR1cw==&lt;/data&gt;
    095 *
    096 *         &lt;key&gt;array&lt;/key&gt;
    097 *         &lt;array&gt;
    098 *             &lt;string&gt;value1&lt;/string&gt;
    099 *             &lt;string&gt;value2&lt;/string&gt;
    100 *             &lt;string&gt;value3&lt;/string&gt;
    101 *         &lt;/array&gt;
    102 *
    103 *         &lt;key&gt;dictionnary&lt;/key&gt;
    104 *         &lt;dict&gt;
    105 *             &lt;key&gt;key1&lt;/key&gt;
    106 *             &lt;string&gt;value1&lt;/string&gt;
    107 *             &lt;key&gt;key2&lt;/key&gt;
    108 *             &lt;string&gt;value2&lt;/string&gt;
    109 *             &lt;key&gt;key3&lt;/key&gt;
    110 *             &lt;string&gt;value3&lt;/string&gt;
    111 *         &lt;/dict&gt;
    112 *
    113 *         &lt;key&gt;nested&lt;/key&gt;
    114 *         &lt;dict&gt;
    115 *             &lt;key&gt;node1&lt;/key&gt;
    116 *             &lt;dict&gt;
    117 *                 &lt;key&gt;node2&lt;/key&gt;
    118 *                 &lt;dict&gt;
    119 *                     &lt;key&gt;node3&lt;/key&gt;
    120 *                     &lt;string&gt;value&lt;/string&gt;
    121 *                 &lt;/dict&gt;
    122 *             &lt;/dict&gt;
    123 *         &lt;/dict&gt;
    124 *
    125 *     &lt;/dict&gt;
    126 * &lt;/plist&gt;
    127 * </pre>
    128 *
    129 * @since 1.2
    130 */
    131public class XMLPropertyListConfiguration extends BaseHierarchicalConfiguration implements FileBasedConfiguration, FileLocatorAware {
    132
    133    /**
    134     * Container for array elements. <strong>Do not use this class !</strong> It is used internally by XMLPropertyConfiguration to
    135     * parse the configuration file, it may be removed at any moment in the future.
    136     */
    137    private static final class ArrayNodeBuilder extends PListNodeBuilder {
    138
    139        /** The list of values in the array. */
    140        private final List<Object> list = new ArrayList<>();
    141
    142        /**
    143         * Add an object to the array.
    144         *
    145         * @param value the value to be added
    146         */
    147        @Override
    148        public void addValue(final Object value) {
    149            list.add(value);
    150        }
    151
    152        /**
    153         * Return the list of values in the array.
    154         *
    155         * @return the {@link List} of values
    156         */
    157        @Override
    158        protected Object getNodeValue() {
    159            return list;
    160        }
    161    }
    162
    163    /**
    164     * A specialized builder class with addXXX methods to parse the typed data passed by the SAX handler. It is used for
    165     * creating the nodes of the configuration.
    166     */
    167    private static class PListNodeBuilder {
    168
    169        /**
    170         * The MacOS FORMAT of dates in plist files. Note: Because {@code SimpleDateFormat} is not thread-safe, each access has
    171         * to be synchronized.
    172         */
    173        private static final DateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
    174        static {
    175            FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
    176        }
    177
    178        /**
    179         * The GNUstep FORMAT of dates in plist files. Note: Because {@code SimpleDateFormat} is not thread-safe, each access
    180         * has to be synchronized.
    181         */
    182        private static final DateFormat GNUSTEP_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
    183
    184        /** A collection with child builders of this builder. */
    185        private final Collection<PListNodeBuilder> childBuilders = new LinkedList<>();
    186
    187        /** The name of the represented node. */
    188        private String name;
    189
    190        /** The current value of the represented node. */
    191        private Object value;
    192
    193        /**
    194         * Adds the given child builder to this builder.
    195         *
    196         * @param child the child builder to be added
    197         */
    198        public void addChild(final PListNodeBuilder child) {
    199            childBuilders.add(child);
    200        }
    201
    202        /**
    203         * Parse the specified string as a byte array in base 64 FORMAT and add it to the values of the node.
    204         *
    205         * @param value the value to be added
    206         */
    207        public void addDataValue(final String value) {
    208            addValue(Base64.getMimeDecoder().decode(value.getBytes(DATA_ENCODING)));
    209        }
    210
    211        /**
    212         * Parse the specified string as a date and add it to the values of the node.
    213         *
    214         * @param value the value to be added
    215         * @throws IllegalArgumentException if the date string cannot be parsed
    216         */
    217        public void addDateValue(final String value) {
    218            try {
    219                if (value.indexOf(' ') != -1) {
    220                    // parse the date using the GNUstep FORMAT
    221                    synchronized (GNUSTEP_FORMAT) {
    222                        addValue(GNUSTEP_FORMAT.parse(value));
    223                    }
    224                } else {
    225                    // parse the date using the MacOS X FORMAT
    226                    synchronized (FORMAT) {
    227                        addValue(FORMAT.parse(value));
    228                    }
    229                }
    230            } catch (final ParseException e) {
    231                throw new IllegalArgumentException(String.format("'%s' cannot be parsed to a date!", value), e);
    232            }
    233        }
    234
    235        /**
    236         * Add a boolean value 'false' to the values of the node.
    237         */
    238        public void addFalseValue() {
    239            addValue(Boolean.FALSE);
    240        }
    241
    242        /**
    243         * Parse the specified string as an Interger and add it to the values of the node.
    244         *
    245         * @param value the value to be added
    246         */
    247        public void addIntegerValue(final String value) {
    248            addValue(new BigInteger(value));
    249        }
    250
    251        /**
    252         * Add a sublist to the values of the node.
    253         *
    254         * @param node the node whose value will be added to the current node value
    255         */
    256        public void addList(final ArrayNodeBuilder node) {
    257            addValue(node.getNodeValue());
    258        }
    259
    260        /**
    261         * Parse the specified string as a Double and add it to the values of the node.
    262         *
    263         * @param value the value to be added
    264         */
    265        public void addRealValue(final String value) {
    266            addValue(new BigDecimal(value));
    267        }
    268
    269        /**
    270         * Add a boolean value 'true' to the values of the node.
    271         */
    272        public void addTrueValue() {
    273            addValue(Boolean.TRUE);
    274        }
    275
    276        /**
    277         * Update the value of the node. If the existing value is null, it's replaced with the new value. If the existing value
    278         * is a list, the specified value is appended to the list. If the existing value is not null, a list with the two values
    279         * is built.
    280         *
    281         * @param v the value to be added
    282         */
    283        public void addValue(final Object v) {
    284            if (value == null) {
    285                value = v;
    286            } else if (value instanceof Collection) {
    287                // This is safe because we create the collections ourselves
    288                @SuppressWarnings("unchecked")
    289                final Collection<Object> collection = (Collection<Object>) value;
    290                collection.add(v);
    291            } else {
    292                final List<Object> list = new ArrayList<>();
    293                list.add(value);
    294                list.add(v);
    295                value = list;
    296            }
    297        }
    298
    299        /**
    300         * Creates the configuration node defined by this builder.
    301         *
    302         * @return the newly created configuration node
    303         */
    304        public ImmutableNode createNode() {
    305            final ImmutableNode.Builder nodeBuilder = new ImmutableNode.Builder(childBuilders.size());
    306            childBuilders.forEach(child -> nodeBuilder.addChild(child.createNode()));
    307            return nodeBuilder.name(name).value(getNodeValue()).create();
    308        }
    309
    310        /**
    311         * Gets the final value for the node to be created. This method is called when the represented configuration node is
    312         * actually created.
    313         *
    314         * @return the value of the resulting configuration node
    315         */
    316        protected Object getNodeValue() {
    317            return value;
    318        }
    319
    320        /**
    321         * Sets the name of the represented node.
    322         *
    323         * @param nodeName the node name
    324         */
    325        public void setName(final String nodeName) {
    326            name = nodeName;
    327        }
    328    }
    329
    330    /**
    331     * SAX Handler to build the configuration nodes while the document is being parsed.
    332     */
    333    private final class XMLPropertyListHandler extends DefaultHandler {
    334
    335        /** The buffer containing the text node being read */
    336        private final StringBuilder buffer = new StringBuilder();
    337
    338        /** The stack of configuration nodes */
    339        private final List<PListNodeBuilder> stack = new ArrayList<>();
    340
    341        /** The builder for the resulting node. */
    342        private final PListNodeBuilder resultBuilder;
    343
    344        public XMLPropertyListHandler() {
    345            resultBuilder = new PListNodeBuilder();
    346            push(resultBuilder);
    347        }
    348
    349        @Override
    350        public void characters(final char[] ch, final int start, final int length) throws SAXException {
    351            buffer.append(ch, start, length);
    352        }
    353
    354        @Override
    355        public void endElement(final String uri, final String localName, final String qName) throws SAXException {
    356            if ("key".equals(qName)) {
    357                // create a new node, link it to its parent and push it on the stack
    358                final PListNodeBuilder node = new PListNodeBuilder();
    359                node.setName(buffer.toString());
    360                peekNE().addChild(node);
    361                push(node);
    362            } else if ("dict".equals(qName)) {
    363                // remove the root of the XMLPropertyListConfiguration previously pushed on the stack
    364                final PListNodeBuilder builder = pop();
    365                assert builder != null : "Stack was empty!";
    366                if (peek() instanceof ArrayNodeBuilder) {
    367                    // create the configuration
    368                    final XMLPropertyListConfiguration config = new XMLPropertyListConfiguration(builder.createNode());
    369
    370                    // add it to the ArrayNodeBuilder
    371                    final ArrayNodeBuilder node = (ArrayNodeBuilder) peekNE();
    372                    node.addValue(config);
    373                }
    374            } else {
    375                switch (qName) {
    376                case "string":
    377                    peekNE().addValue(buffer.toString());
    378                    break;
    379                case "integer":
    380                    peekNE().addIntegerValue(buffer.toString());
    381                    break;
    382                case "real":
    383                    peekNE().addRealValue(buffer.toString());
    384                    break;
    385                case "true":
    386                    peekNE().addTrueValue();
    387                    break;
    388                case "false":
    389                    peekNE().addFalseValue();
    390                    break;
    391                case "data":
    392                    peekNE().addDataValue(buffer.toString());
    393                    break;
    394                case "date":
    395                    try {
    396                        peekNE().addDateValue(buffer.toString());
    397                    } catch (final IllegalArgumentException iex) {
    398                        getLogger().warn("Ignoring invalid date property " + buffer);
    399                    }
    400                    break;
    401                case "array": {
    402                    final ArrayNodeBuilder array = (ArrayNodeBuilder) pop();
    403                    peekNE().addList(array);
    404                    break;
    405                }
    406                default:
    407                    break;
    408                }
    409
    410                // remove the plist node on the stack once the value has been parsed,
    411                // array nodes remains on the stack for the next values in the list
    412                if (!(peek() instanceof ArrayNodeBuilder)) {
    413                    pop();
    414                }
    415            }
    416
    417            buffer.setLength(0);
    418        }
    419
    420        /**
    421         * Gets the builder for the result node.
    422         *
    423         * @return the result node builder
    424         */
    425        public PListNodeBuilder getResultBuilder() {
    426            return resultBuilder;
    427        }
    428
    429        /**
    430         * Return the node on the top of the stack.
    431         */
    432        private PListNodeBuilder peek() {
    433            if (!stack.isEmpty()) {
    434                return stack.get(stack.size() - 1);
    435            }
    436            return null;
    437        }
    438
    439        /**
    440         * Returns the node on top of the non-empty stack. Throws an exception if the stack is empty.
    441         *
    442         * @return the top node of the stack
    443         * @throws ConfigurationRuntimeException if the stack is empty
    444         */
    445        private PListNodeBuilder peekNE() {
    446            final PListNodeBuilder result = peek();
    447            if (result == null) {
    448                throw new ConfigurationRuntimeException("Access to empty stack!");
    449            }
    450            return result;
    451        }
    452
    453        /**
    454         * Remove and return the node on the top of the stack.
    455         */
    456        private PListNodeBuilder pop() {
    457            if (!stack.isEmpty()) {
    458                return stack.remove(stack.size() - 1);
    459            }
    460            return null;
    461        }
    462
    463        /**
    464         * Put a node on the top of the stack.
    465         */
    466        private void push(final PListNodeBuilder node) {
    467            stack.add(node);
    468        }
    469
    470        @Override
    471        public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) throws SAXException {
    472            if ("array".equals(qName)) {
    473                push(new ArrayNodeBuilder());
    474            } else if ("dict".equals(qName) && peek() instanceof ArrayNodeBuilder) {
    475                // push the new root builder on the stack
    476                push(new PListNodeBuilder());
    477            }
    478        }
    479    }
    480
    481    /** Size of the indentation for the generated file. */
    482    private static final int INDENT_SIZE = 4;
    483
    484    /** Constant for the encoding for binary data. */
    485    private static final Charset DATA_ENCODING = StandardCharsets.UTF_8;
    486
    487    /**
    488     * Transform a map of arbitrary types into a map with string keys and object values. All keys of the source map which
    489     * are not of type String are dropped.
    490     *
    491     * @param src the map to be converted
    492     * @return the resulting map
    493     */
    494    private static Map<String, Object> transformMap(final Map<?, ?> src) {
    495        final Map<String, Object> dest = new HashMap<>();
    496        for (final Map.Entry<?, ?> e : src.entrySet()) {
    497            if (e.getKey() instanceof String) {
    498                dest.put((String) e.getKey(), e.getValue());
    499            }
    500        }
    501        return dest;
    502    }
    503
    504    /** Temporarily stores the current file location. */
    505    private FileLocator locator;
    506
    507    /**
    508     * Creates an empty XMLPropertyListConfiguration object which can be used to synthesize a new plist file by adding
    509     * values and then saving().
    510     */
    511    public XMLPropertyListConfiguration() {
    512    }
    513
    514    /**
    515     * Creates a new instance of {@code XMLPropertyListConfiguration} and copies the content of the specified configuration
    516     * into this object.
    517     *
    518     * @param configuration the configuration to copy
    519     * @since 1.4
    520     */
    521    public XMLPropertyListConfiguration(final HierarchicalConfiguration<ImmutableNode> configuration) {
    522        super(configuration);
    523    }
    524
    525    /**
    526     * Creates a new instance of {@code XMLPropertyConfiguration} with the given root node.
    527     *
    528     * @param root the root node
    529     */
    530    XMLPropertyListConfiguration(final ImmutableNode root) {
    531        super(new InMemoryNodeModel(root));
    532    }
    533
    534    @Override
    535    protected void addPropertyInternal(final String key, final Object value) {
    536        if (value instanceof byte[] || value instanceof List) {
    537            addPropertyDirect(key, value);
    538        } else if (value instanceof Object[]) {
    539            addPropertyDirect(key, Arrays.asList((Object[]) value));
    540        } else {
    541            super.addPropertyInternal(key, value);
    542        }
    543    }
    544
    545    /**
    546     * Stores the current file locator. This method is called before I/O operations.
    547     *
    548     * @param locator the current {@code FileLocator}
    549     */
    550    @Override
    551    public void initFileLocator(final FileLocator locator) {
    552        this.locator = locator;
    553    }
    554
    555    /**
    556     * Append a node to the writer, indented according to a specific level.
    557     */
    558    private void printNode(final PrintWriter out, final int indentLevel, final ImmutableNode node) {
    559        final String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
    560
    561        if (node.getNodeName() != null) {
    562            out.println(padding + "<key>" + StringEscapeUtils.escapeXml10(node.getNodeName()) + "</key>");
    563        }
    564
    565        final List<ImmutableNode> children = node.getChildren();
    566        if (!children.isEmpty()) {
    567            out.println(padding + "<dict>");
    568
    569            final Iterator<ImmutableNode> it = children.iterator();
    570            while (it.hasNext()) {
    571                final ImmutableNode child = it.next();
    572                printNode(out, indentLevel + 1, child);
    573
    574                if (it.hasNext()) {
    575                    out.println();
    576                }
    577            }
    578
    579            out.println(padding + "</dict>");
    580        } else if (node.getValue() == null) {
    581            out.println(padding + "<dict/>");
    582        } else {
    583            final Object value = node.getValue();
    584            printValue(out, indentLevel, value);
    585        }
    586    }
    587
    588    /**
    589     * Append a value to the writer, indented according to a specific level.
    590     */
    591    private void printValue(final PrintWriter out, final int indentLevel, final Object value) {
    592        final String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
    593
    594        if (value instanceof Date) {
    595            synchronized (PListNodeBuilder.FORMAT) {
    596                out.println(padding + "<date>" + PListNodeBuilder.FORMAT.format((Date) value) + "</date>");
    597            }
    598        } else if (value instanceof Calendar) {
    599            printValue(out, indentLevel, ((Calendar) value).getTime());
    600        } else if (value instanceof Number) {
    601            if (value instanceof Double || value instanceof Float || value instanceof BigDecimal) {
    602                out.println(padding + "<real>" + value.toString() + "</real>");
    603            } else {
    604                out.println(padding + "<integer>" + value.toString() + "</integer>");
    605            }
    606        } else if (value instanceof Boolean) {
    607            if (((Boolean) value).booleanValue()) {
    608                out.println(padding + "<true/>");
    609            } else {
    610                out.println(padding + "<false/>");
    611            }
    612        } else if (value instanceof List) {
    613            out.println(padding + "<array>");
    614            ((List<?>) value).forEach(o -> printValue(out, indentLevel + 1, o));
    615            out.println(padding + "</array>");
    616        } else if (value instanceof HierarchicalConfiguration) {
    617            // This is safe because we have created this configuration
    618            @SuppressWarnings("unchecked")
    619            final HierarchicalConfiguration<ImmutableNode> config = (HierarchicalConfiguration<ImmutableNode>) value;
    620            printNode(out, indentLevel, config.getNodeModel().getNodeHandler().getRootNode());
    621        } else if (value instanceof ImmutableConfiguration) {
    622            // display a flat Configuration as a dictionary
    623            out.println(padding + "<dict>");
    624            final ImmutableConfiguration config = (ImmutableConfiguration) value;
    625            config.forEach((k, v) -> {
    626                // create a node for each property
    627                final ImmutableNode node = new ImmutableNode.Builder().name(k).value(v).create();
    628                // print the node
    629                printNode(out, indentLevel + 1, node);
    630                out.println();
    631            });
    632            out.println(padding + "</dict>");
    633        } else if (value instanceof Map) {
    634            // display a Map as a dictionary
    635            final Map<String, Object> map = transformMap((Map<?, ?>) value);
    636            printValue(out, indentLevel, new MapConfiguration(map));
    637        } else if (value instanceof byte[]) {
    638            final String base64 = new String(Base64.getMimeEncoder().encode((byte[]) value), DATA_ENCODING);
    639            out.println(padding + "<data>" + StringEscapeUtils.escapeXml10(base64) + "</data>");
    640        } else if (value != null) {
    641            out.println(padding + "<string>" + StringEscapeUtils.escapeXml10(String.valueOf(value)) + "</string>");
    642        } else {
    643            out.println(padding + "<string/>");
    644        }
    645    }
    646
    647    @Override
    648    public void read(final Reader in) throws ConfigurationException {
    649        // set up the DTD validation
    650        final EntityResolver resolver = (publicId, systemId) -> new InputSource(getClass().getClassLoader().getResourceAsStream("PropertyList-1.0.dtd"));
    651        // parse the file
    652        final XMLPropertyListHandler handler = new XMLPropertyListHandler();
    653        try {
    654            final SAXParserFactory factory = SAXParserFactory.newInstance();
    655            factory.setValidating(true);
    656            final XMLReader xmlReader = factory.newSAXParser().getXMLReader();
    657            xmlReader.setEntityResolver(resolver);
    658            xmlReader.setContentHandler(handler);
    659            xmlReader.parse(new InputSource(in));
    660            getNodeModel().mergeRoot(handler.getResultBuilder().createNode(), null, null, null, this);
    661        } catch (final Exception e) {
    662            throw new ConfigurationException("Unable to parse the configuration file", e);
    663        }
    664    }
    665
    666    private void setPropertyDirect(final String key, final Object value) {
    667        setDetailEvents(false);
    668        try {
    669            clearProperty(key);
    670            addPropertyDirect(key, value);
    671        } finally {
    672            setDetailEvents(true);
    673        }
    674    }
    675
    676    @Override
    677    protected void setPropertyInternal(final String key, final Object value) {
    678        // special case for byte arrays, they must be stored as is in the configuration
    679        if (value instanceof byte[] || value instanceof List) {
    680            setPropertyDirect(key, value);
    681        } else if (value instanceof Object[]) {
    682            setPropertyDirect(key, Arrays.asList((Object[]) value));
    683        } else {
    684            super.setPropertyInternal(key, value);
    685        }
    686    }
    687
    688    @Override
    689    public void write(final Writer out) throws ConfigurationException {
    690        if (locator == null) {
    691            throw new ConfigurationException(
    692                    "Save operation not properly initialized! Do not call write(Writer) directly, but use a FileHandler to save a configuration.");
    693        }
    694        final PrintWriter writer = new PrintWriter(out);
    695        if (locator.getEncoding() != null) {
    696            writer.println("<?xml version=\"1.0\" encoding=\"" + locator.getEncoding() + "\"?>");
    697        } else {
    698            writer.println("<?xml version=\"1.0\"?>");
    699        }
    700        writer.println("<!DOCTYPE plist SYSTEM \"file://localhost/System/Library/DTDs/PropertyList.dtd\">");
    701        writer.println("<plist version=\"1.0\">");
    702        printNode(writer, 1, getNodeModel().getNodeHandler().getRootNode());
    703        writer.println("</plist>");
    704        writer.flush();
    705    }
    706}