View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.configuration2.plist;
19  
20  import java.io.PrintWriter;
21  import java.io.Reader;
22  import java.io.Writer;
23  import java.math.BigDecimal;
24  import java.math.BigInteger;
25  import java.nio.charset.Charset;
26  import java.nio.charset.StandardCharsets;
27  import java.text.DateFormat;
28  import java.text.ParseException;
29  import java.text.SimpleDateFormat;
30  import java.util.ArrayList;
31  import java.util.Arrays;
32  import java.util.Calendar;
33  import java.util.Collection;
34  import java.util.Date;
35  import java.util.HashMap;
36  import java.util.Iterator;
37  import java.util.LinkedList;
38  import java.util.List;
39  import java.util.Map;
40  import java.util.TimeZone;
41  
42  import javax.xml.parsers.SAXParser;
43  import javax.xml.parsers.SAXParserFactory;
44  
45  import org.apache.commons.codec.binary.Base64;
46  import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
47  import org.apache.commons.configuration2.FileBasedConfiguration;
48  import org.apache.commons.configuration2.HierarchicalConfiguration;
49  import org.apache.commons.configuration2.ImmutableConfiguration;
50  import org.apache.commons.configuration2.MapConfiguration;
51  import org.apache.commons.configuration2.ex.ConfigurationException;
52  import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
53  import org.apache.commons.configuration2.io.FileLocator;
54  import org.apache.commons.configuration2.io.FileLocatorAware;
55  import org.apache.commons.configuration2.tree.ImmutableNode;
56  import org.apache.commons.configuration2.tree.InMemoryNodeModel;
57  import org.apache.commons.lang3.StringUtils;
58  import org.apache.commons.text.StringEscapeUtils;
59  import org.xml.sax.Attributes;
60  import org.xml.sax.EntityResolver;
61  import org.xml.sax.InputSource;
62  import org.xml.sax.SAXException;
63  import org.xml.sax.helpers.DefaultHandler;
64  
65  /**
66   * Property list file (plist) in XML FORMAT as used by macOS X (http://www.apple.com/DTDs/PropertyList-1.0.dtd). This
67   * configuration doesn't support the binary FORMAT used in OS X 10.4.
68   *
69   * <p>
70   * Example:
71   * </p>
72   *
73   * <pre>
74   * &lt;?xml version="1.0"?&gt;
75   * &lt;!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd"&gt;
76   * &lt;plist version="1.0"&gt;
77   *     &lt;dict&gt;
78   *         &lt;key&gt;string&lt;/key&gt;
79   *         &lt;string&gt;value1&lt;/string&gt;
80   *
81   *         &lt;key&gt;integer&lt;/key&gt;
82   *         &lt;integer&gt;12345&lt;/integer&gt;
83   *
84   *         &lt;key&gt;real&lt;/key&gt;
85   *         &lt;real&gt;-123.45E-1&lt;/real&gt;
86   *
87   *         &lt;key&gt;boolean&lt;/key&gt;
88   *         &lt;true/&gt;
89   *
90   *         &lt;key&gt;date&lt;/key&gt;
91   *         &lt;date&gt;2005-01-01T12:00:00Z&lt;/date&gt;
92   *
93   *         &lt;key&gt;data&lt;/key&gt;
94   *         &lt;data&gt;RHJhY28gRG9ybWllbnMgTnVucXVhbSBUaXRpbGxhbmR1cw==&lt;/data&gt;
95   *
96   *         &lt;key&gt;array&lt;/key&gt;
97   *         &lt;array&gt;
98   *             &lt;string&gt;value1&lt;/string&gt;
99   *             &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  */
131 public class XMLPropertyListConfiguration extends BaseHierarchicalConfiguration implements FileBasedConfiguration, FileLocatorAware {
132     /**
133      * Container for array elements. <b>Do not use this class !</b> 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.decodeBase64(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                 if ("string".equals(qName)) {
372                     peekNE().addValue(buffer.toString());
373                 } else if ("integer".equals(qName)) {
374                     peekNE().addIntegerValue(buffer.toString());
375                 } else if ("real".equals(qName)) {
376                     peekNE().addRealValue(buffer.toString());
377                 } else if ("true".equals(qName)) {
378                     peekNE().addTrueValue();
379                 } else if ("false".equals(qName)) {
380                     peekNE().addFalseValue();
381                 } else if ("data".equals(qName)) {
382                     peekNE().addDataValue(buffer.toString());
383                 } else if ("date".equals(qName)) {
384                     try {
385                         peekNE().addDateValue(buffer.toString());
386                     } catch (final IllegalArgumentException iex) {
387                         getLogger().warn("Ignoring invalid date property " + buffer);
388                     }
389                 } else if ("array".equals(qName)) {
390                     final ArrayNodeBuilder array = (ArrayNodeBuilder) pop();
391                     peekNE().addList(array);
392                 }
393 
394                 // remove the plist node on the stack once the value has been parsed,
395                 // array nodes remains on the stack for the next values in the list
396                 if (!(peek() instanceof ArrayNodeBuilder)) {
397                     pop();
398                 }
399             }
400 
401             buffer.setLength(0);
402         }
403 
404         /**
405          * Gets the builder for the result node.
406          *
407          * @return the result node builder
408          */
409         public PListNodeBuilder getResultBuilder() {
410             return resultBuilder;
411         }
412 
413         /**
414          * Return the node on the top of the stack.
415          */
416         private PListNodeBuilder peek() {
417             if (!stack.isEmpty()) {
418                 return stack.get(stack.size() - 1);
419             }
420             return null;
421         }
422 
423         /**
424          * Returns the node on top of the non-empty stack. Throws an exception if the stack is empty.
425          *
426          * @return the top node of the stack
427          * @throws ConfigurationRuntimeException if the stack is empty
428          */
429         private PListNodeBuilder peekNE() {
430             final PListNodeBuilder result = peek();
431             if (result == null) {
432                 throw new ConfigurationRuntimeException("Access to empty stack!");
433             }
434             return result;
435         }
436 
437         /**
438          * Remove and return the node on the top of the stack.
439          */
440         private PListNodeBuilder pop() {
441             if (!stack.isEmpty()) {
442                 return stack.remove(stack.size() - 1);
443             }
444             return null;
445         }
446 
447         /**
448          * Put a node on the top of the stack.
449          */
450         private void push(final PListNodeBuilder node) {
451             stack.add(node);
452         }
453 
454         @Override
455         public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) throws SAXException {
456             if ("array".equals(qName)) {
457                 push(new ArrayNodeBuilder());
458             } else if ("dict".equals(qName) && peek() instanceof ArrayNodeBuilder) {
459                 // push the new root builder on the stack
460                 push(new PListNodeBuilder());
461             }
462         }
463     }
464 
465     /** Size of the indentation for the generated file. */
466     private static final int INDENT_SIZE = 4;
467 
468     /** Constant for the encoding for binary data. */
469     private static final Charset DATA_ENCODING = StandardCharsets.UTF_8;
470 
471     /**
472      * Transform a map of arbitrary types into a map with string keys and object values. All keys of the source map which
473      * are not of type String are dropped.
474      *
475      * @param src the map to be converted
476      * @return the resulting map
477      */
478     private static Map<String, Object> transformMap(final Map<?, ?> src) {
479         final Map<String, Object> dest = new HashMap<>();
480         for (final Map.Entry<?, ?> e : src.entrySet()) {
481             if (e.getKey() instanceof String) {
482                 dest.put((String) e.getKey(), e.getValue());
483             }
484         }
485         return dest;
486     }
487 
488     /** Temporarily stores the current file location. */
489     private FileLocator locator;
490 
491     /**
492      * Creates an empty XMLPropertyListConfiguration object which can be used to synthesize a new plist file by adding
493      * values and then saving().
494      */
495     public XMLPropertyListConfiguration() {
496     }
497 
498     /**
499      * Creates a new instance of {@code XMLPropertyListConfiguration} and copies the content of the specified configuration
500      * into this object.
501      *
502      * @param configuration the configuration to copy
503      * @since 1.4
504      */
505     public XMLPropertyListConfiguration(final HierarchicalConfiguration<ImmutableNode> configuration) {
506         super(configuration);
507     }
508 
509     /**
510      * Creates a new instance of {@code XMLPropertyConfiguration} with the given root node.
511      *
512      * @param root the root node
513      */
514     XMLPropertyListConfiguration(final ImmutableNode root) {
515         super(new InMemoryNodeModel(root));
516     }
517 
518     @Override
519     protected void addPropertyInternal(final String key, final Object value) {
520         if (value instanceof byte[] || value instanceof List) {
521             addPropertyDirect(key, value);
522         } else if (value instanceof Object[]) {
523             addPropertyDirect(key, Arrays.asList((Object[]) value));
524         } else {
525             super.addPropertyInternal(key, value);
526         }
527     }
528 
529     /**
530      * Stores the current file locator. This method is called before I/O operations.
531      *
532      * @param locator the current {@code FileLocator}
533      */
534     @Override
535     public void initFileLocator(final FileLocator locator) {
536         this.locator = locator;
537     }
538 
539     /**
540      * Append a node to the writer, indented according to a specific level.
541      */
542     private void printNode(final PrintWriter out, final int indentLevel, final ImmutableNode node) {
543         final String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
544 
545         if (node.getNodeName() != null) {
546             out.println(padding + "<key>" + StringEscapeUtils.escapeXml10(node.getNodeName()) + "</key>");
547         }
548 
549         final List<ImmutableNode> children = node.getChildren();
550         if (!children.isEmpty()) {
551             out.println(padding + "<dict>");
552 
553             final Iterator<ImmutableNode> it = children.iterator();
554             while (it.hasNext()) {
555                 final ImmutableNode child = it.next();
556                 printNode(out, indentLevel + 1, child);
557 
558                 if (it.hasNext()) {
559                     out.println();
560                 }
561             }
562 
563             out.println(padding + "</dict>");
564         } else if (node.getValue() == null) {
565             out.println(padding + "<dict/>");
566         } else {
567             final Object value = node.getValue();
568             printValue(out, indentLevel, value);
569         }
570     }
571 
572     /**
573      * Append a value to the writer, indented according to a specific level.
574      */
575     private void printValue(final PrintWriter out, final int indentLevel, final Object value) {
576         final String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
577 
578         if (value instanceof Date) {
579             synchronized (PListNodeBuilder.FORMAT) {
580                 out.println(padding + "<date>" + PListNodeBuilder.FORMAT.format((Date) value) + "</date>");
581             }
582         } else if (value instanceof Calendar) {
583             printValue(out, indentLevel, ((Calendar) value).getTime());
584         } else if (value instanceof Number) {
585             if (value instanceof Double || value instanceof Float || value instanceof BigDecimal) {
586                 out.println(padding + "<real>" + value.toString() + "</real>");
587             } else {
588                 out.println(padding + "<integer>" + value.toString() + "</integer>");
589             }
590         } else if (value instanceof Boolean) {
591             if (((Boolean) value).booleanValue()) {
592                 out.println(padding + "<true/>");
593             } else {
594                 out.println(padding + "<false/>");
595             }
596         } else if (value instanceof List) {
597             out.println(padding + "<array>");
598             ((List<?>) value).forEach(o -> printValue(out, indentLevel + 1, o));
599             out.println(padding + "</array>");
600         } else if (value instanceof HierarchicalConfiguration) {
601             // This is safe because we have created this configuration
602             @SuppressWarnings("unchecked")
603             final HierarchicalConfiguration<ImmutableNode> config = (HierarchicalConfiguration<ImmutableNode>) value;
604             printNode(out, indentLevel, config.getNodeModel().getNodeHandler().getRootNode());
605         } else if (value instanceof ImmutableConfiguration) {
606             // display a flat Configuration as a dictionary
607             out.println(padding + "<dict>");
608 
609             final ImmutableConfiguration config = (ImmutableConfiguration) value;
610             final Iterator<String> it = config.getKeys();
611             while (it.hasNext()) {
612                 // create a node for each property
613                 final String key = it.next();
614                 final ImmutableNode node = new ImmutableNode.Builder().name(key).value(config.getProperty(key)).create();
615 
616                 // print the node
617                 printNode(out, indentLevel + 1, node);
618 
619                 if (it.hasNext()) {
620                     out.println();
621                 }
622             }
623             out.println(padding + "</dict>");
624         } else if (value instanceof Map) {
625             // display a Map as a dictionary
626             final Map<String, Object> map = transformMap((Map<?, ?>) value);
627             printValue(out, indentLevel, new MapConfiguration(map));
628         } else if (value instanceof byte[]) {
629             final String base64 = new String(Base64.encodeBase64((byte[]) value), DATA_ENCODING);
630             out.println(padding + "<data>" + StringEscapeUtils.escapeXml10(base64) + "</data>");
631         } else if (value != null) {
632             out.println(padding + "<string>" + StringEscapeUtils.escapeXml10(String.valueOf(value)) + "</string>");
633         } else {
634             out.println(padding + "<string/>");
635         }
636     }
637 
638     @Override
639     public void read(final Reader in) throws ConfigurationException {
640         // set up the DTD validation
641         final EntityResolver resolver = (publicId, systemId) -> new InputSource(getClass().getClassLoader().getResourceAsStream("PropertyList-1.0.dtd"));
642 
643         // parse the file
644         final XMLPropertyListHandler handler = new XMLPropertyListHandler();
645         try {
646             final SAXParserFactory factory = SAXParserFactory.newInstance();
647             factory.setValidating(true);
648 
649             final SAXParser parser = factory.newSAXParser();
650             parser.getXMLReader().setEntityResolver(resolver);
651             parser.getXMLReader().setContentHandler(handler);
652             parser.getXMLReader().parse(new InputSource(in));
653 
654             getNodeModel().mergeRoot(handler.getResultBuilder().createNode(), null, null, null, this);
655         } catch (final Exception e) {
656             throw new ConfigurationException("Unable to parse the configuration file", e);
657         }
658     }
659 
660     private void setPropertyDirect(final String key, final Object value) {
661         setDetailEvents(false);
662         try {
663             clearProperty(key);
664             addPropertyDirect(key, value);
665         } finally {
666             setDetailEvents(true);
667         }
668     }
669 
670     @Override
671     protected void setPropertyInternal(final String key, final Object value) {
672         // special case for byte arrays, they must be stored as is in the configuration
673         if (value instanceof byte[] || value instanceof List) {
674             setPropertyDirect(key, value);
675         } else if (value instanceof Object[]) {
676             setPropertyDirect(key, Arrays.asList((Object[]) value));
677         } else {
678             super.setPropertyInternal(key, value);
679         }
680     }
681 
682     @Override
683     public void write(final Writer out) throws ConfigurationException {
684         if (locator == null) {
685             throw new ConfigurationException(
686                 "Save operation not properly " + "initialized! Do not call write(Writer) directly," + " but use a FileHandler to save a configuration.");
687         }
688         final PrintWriter writer = new PrintWriter(out);
689 
690         if (locator.getEncoding() != null) {
691             writer.println("<?xml version=\"1.0\" encoding=\"" + locator.getEncoding() + "\"?>");
692         } else {
693             writer.println("<?xml version=\"1.0\"?>");
694         }
695 
696         writer.println("<!DOCTYPE plist SYSTEM \"file://localhost/System/Library/DTDs/PropertyList.dtd\">");
697         writer.println("<plist version=\"1.0\">");
698 
699         printNode(writer, 1, getNodeModel().getNodeHandler().getRootNode());
700 
701         writer.println("</plist>");
702         writer.flush();
703     }
704 }