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}