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