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