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 package org.apache.commons.configuration;
018
019 import java.io.BufferedReader;
020 import java.io.File;
021 import java.io.IOException;
022 import java.io.PrintWriter;
023 import java.io.Reader;
024 import java.io.Writer;
025 import java.net.URL;
026 import java.util.Collection;
027 import java.util.Collections;
028 import java.util.Iterator;
029 import java.util.LinkedHashSet;
030 import java.util.List;
031 import java.util.Set;
032
033 import org.apache.commons.configuration.tree.ConfigurationNode;
034 import org.apache.commons.configuration.tree.ViewNode;
035
036 /**
037 * <p>
038 * A specialized hierarchical configuration implementation for parsing ini
039 * files.
040 * </p>
041 * <p>
042 * An initialization or ini file is a configuration file typically found on
043 * Microsoft's Windows operating system and contains data for Windows based
044 * applications.
045 * </p>
046 * <p>
047 * Although popularized by Windows, ini files can be used on any system or
048 * platform due to the fact that they are merely text files that can easily be
049 * parsed and modified by both humans and computers.
050 * </p>
051 * <p>
052 * A typical ini file could look something like:
053 * </p>
054 * <pre>
055 * [section1]
056 * ; this is a comment!
057 * var1 = foo
058 * var2 = bar
059 *
060 * [section2]
061 * var1 = doo
062 * </pre>
063 * <p>
064 * The format of ini files is fairly straight forward and is composed of three
065 * components:<br>
066 * <ul>
067 * <li><b>Sections:</b> Ini files are split into sections, each section starting
068 * with a section declaration. A section declaration starts with a '[' and ends
069 * with a ']'. Sections occur on one line only.</li>
070 * <li><b>Parameters:</b> Items in a section are known as parameters. Parameters
071 * have a typical {@code key = value} format.</li>
072 * <li><b>Comments:</b> Lines starting with a ';' are assumed to be comments.</li>
073 * </ul>
074 * </p>
075 * <p>
076 * There are various implementations of the ini file format by various vendors
077 * which has caused a number of differences to appear. As far as possible this
078 * configuration tries to be lenient and support most of the differences.
079 * </p>
080 * <p>
081 * Some of the differences supported are as follows:
082 * <ul>
083 * <li><b>Comments:</b> The '#' character is also accepted as a comment
084 * signifier.</li>
085 * <li><b>Key value separator:</b> The ':' character is also accepted in place of
086 * '=' to separate keys and values in parameters, for example
087 * {@code var1 : foo}.</li>
088 * <li><b>Duplicate sections:</b> Typically duplicate sections are not allowed,
089 * this configuration does however support this feature. In the event of a duplicate
090 * section, the two section's values are merged so that there is only a single
091 * section. <strong>Note</strong>: This also affects the internal data of the
092 * configuration. If it is saved, only a single section is written!</li>
093 * <li><b>Duplicate parameters:</b> Typically duplicate parameters are only
094 * allowed if they are in two different sections, thus they are local to
095 * sections; this configuration simply merges duplicates; if a section has a
096 * duplicate parameter the values are then added to the key as a list.</li>
097 * </ul>
098 * </p>
099 * <p>
100 * Global parameters are also allowed; any parameters declared before a section
101 * is declared are added to a global section. It is important to note that this
102 * global section does not have a name.
103 * </p>
104 * <p>
105 * In all instances, a parameter's key is prepended with its section name and a
106 * '.' (period). Thus a parameter named "var1" in "section1" will have the key
107 * {@code section1.var1} in this configuration. (This is the default
108 * behavior. Because this is a hierarchical configuration you can change this by
109 * setting a different {@link org.apache.commons.configuration.tree.ExpressionEngine}.)
110 * </p>
111 * <p>
112 * <h3>Implementation Details:</h3> Consider the following ini file:<br>
113 * <pre>
114 * default = ok
115 *
116 * [section1]
117 * var1 = foo
118 * var2 = doodle
119 *
120 * [section2]
121 * ; a comment
122 * var1 = baz
123 * var2 = shoodle
124 * bad =
125 * = worse
126 *
127 * [section3]
128 * # another comment
129 * var1 : foo
130 * var2 : bar
131 * var5 : test1
132 *
133 * [section3]
134 * var3 = foo
135 * var4 = bar
136 * var5 = test2
137 *
138 * [sectionSeparators]
139 * passwd : abc=def
140 * a:b = "value"
141 * </pre>
142 * </p>
143 * <p>
144 * This ini file will be parsed without error. Note:
145 * <ul>
146 * <li>The parameter named "default" is added to the global section, it's value
147 * is accessed simply using {@code getProperty("default")}.</li>
148 * <li>Section 1's parameters can be accessed using
149 * {@code getProperty("section1.var1")}.</li>
150 * <li>The parameter named "bad" simply adds the parameter with an empty value.</li>
151 * <li>The empty key with value "= worse" is added using a key consisting of a
152 * single space character. This key is still added to section 2 and the value
153 * can be accessed using {@code getProperty("section2. ")}, notice the
154 * period '.' and the space following the section name.</li>
155 * <li>Section three uses both '=' and ':' to separate keys and values.</li>
156 * <li>Section 3 has a duplicate key named "var5". The value for this key is
157 * [test1, test2], and is represented as a List.</li>
158 * <li>The section called <em>sectionSeparators</em> demonstrates how the
159 * configuration deals with multiple occurrences of separator characters. Per
160 * default the first separator character in a line is detected and used to
161 * split the key from the value. Therefore the first property definition in this
162 * section has the key {@code passwd} and the value {@code abc=def}.
163 * This default behavior can be changed by using quotes. If there is a separator
164 * character before the first quote character (ignoring whitespace), this
165 * character is used as separator. Thus the second property definition in the
166 * section has the key {@code a:b} and the value {@code value}.</li>
167 * </ul>
168 * </p>
169 * <p>
170 * Internally, this configuration maps the content of the represented ini file
171 * to its node structure in the following way:
172 * <ul>
173 * <li>Sections are represented by direct child nodes of the root node.</li>
174 * <li>For the content of a section, corresponding nodes are created as children
175 * of the section node.</li>
176 * </ul>
177 * This explains how the keys for the properties can be constructed. You can
178 * also use other methods of {@link HierarchicalConfiguration} for querying or
179 * manipulating the hierarchy of configuration nodes, for instance the
180 * {@code configurationAt()} method for obtaining the data of a specific
181 * section. However, be careful that the storage scheme described above is not
182 * violated (e.g. by adding multiple levels of nodes or inserting duplicate
183 * section nodes). Otherwise, the special methods for ini configurations may not
184 * work correctly!
185 * </p>
186 * <p>
187 * The set of sections in this configuration can be retrieved using the
188 * {@code getSections()} method. For obtaining a
189 * {@code SubnodeConfiguration} with the content of a specific section the
190 * {@code getSection()} method can be used.
191 * </p>
192 * <p>
193 * <em>Note:</em> Configuration objects of this type can be read concurrently by
194 * multiple threads. However if one of these threads modifies the object,
195 * synchronization has to be performed manually.
196 * </p>
197 *
198 * @author <a
199 * href="http://commons.apache.org/configuration/team-list.html">Commons
200 * Configuration team</a>
201 * @version $Id: HierarchicalINIConfiguration.java 1234362 2012-01-21 16:59:48Z oheger $
202 * @since 1.6
203 */
204 public class HierarchicalINIConfiguration extends
205 AbstractHierarchicalFileConfiguration
206 {
207 /**
208 * The characters that signal the start of a comment line.
209 */
210 protected static final String COMMENT_CHARS = "#;";
211
212 /**
213 * The characters used to separate keys from values.
214 */
215 protected static final String SEPARATOR_CHARS = "=:";
216
217 /**
218 * The serial version UID.
219 */
220 private static final long serialVersionUID = 2548006161386850670L;
221
222 /**
223 * Constant for the line separator.
224 */
225 private static final String LINE_SEPARATOR = System.getProperty("line.separator");
226
227 /**
228 * The characters used for quoting values.
229 */
230 private static final String QUOTE_CHARACTERS = "\"'";
231
232 /**
233 * The line continuation character.
234 */
235 private static final String LINE_CONT = "\\";
236
237 /**
238 * Create a new empty INI Configuration.
239 */
240 public HierarchicalINIConfiguration()
241 {
242 super();
243 }
244
245 /**
246 * Create and load the ini configuration from the given file.
247 *
248 * @param filename The name pr path of the ini file to load.
249 * @throws ConfigurationException If an error occurs while loading the file
250 */
251 public HierarchicalINIConfiguration(String filename)
252 throws ConfigurationException
253 {
254 super(filename);
255 }
256
257 /**
258 * Create and load the ini configuration from the given file.
259 *
260 * @param file The ini file to load.
261 * @throws ConfigurationException If an error occurs while loading the file
262 */
263 public HierarchicalINIConfiguration(File file)
264 throws ConfigurationException
265 {
266 super(file);
267 }
268
269 /**
270 * Create and load the ini configuration from the given url.
271 *
272 * @param url The url of the ini file to load.
273 * @throws ConfigurationException If an error occurs while loading the file
274 */
275 public HierarchicalINIConfiguration(URL url) throws ConfigurationException
276 {
277 super(url);
278 }
279
280 /**
281 * Save the configuration to the specified writer.
282 *
283 * @param writer - The writer to save the configuration to.
284 * @throws ConfigurationException If an error occurs while writing the
285 * configuration
286 */
287 public void save(Writer writer) throws ConfigurationException
288 {
289 PrintWriter out = new PrintWriter(writer);
290 Iterator<String> it = getSections().iterator();
291 while (it.hasNext())
292 {
293 String section = it.next();
294 Configuration subset;
295 if (section != null)
296 {
297 out.print("[");
298 out.print(section);
299 out.print("]");
300 out.println();
301 subset = createSubnodeConfiguration(getSectionNode(section));
302 }
303 else
304 {
305 subset = getSection(null);
306 }
307
308 Iterator<String> keys = subset.getKeys();
309 while (keys.hasNext())
310 {
311 String key = keys.next();
312 Object value = subset.getProperty(key);
313 if (value instanceof Collection)
314 {
315 Iterator<?> values = ((Collection<?>) value).iterator();
316 while (values.hasNext())
317 {
318 value = values.next();
319 out.print(key);
320 out.print(" = ");
321 out.print(formatValue(value.toString()));
322 out.println();
323 }
324 }
325 else
326 {
327 out.print(key);
328 out.print(" = ");
329 out.print(formatValue(value.toString()));
330 out.println();
331 }
332 }
333
334 out.println();
335 }
336
337 out.flush();
338 }
339
340 /**
341 * Load the configuration from the given reader. Note that the
342 * {@code clear()} method is not called so the configuration read in will
343 * be merged with the current configuration.
344 *
345 * @param reader The reader to read the configuration from.
346 * @throws ConfigurationException If an error occurs while reading the
347 * configuration
348 */
349 public void load(Reader reader) throws ConfigurationException
350 {
351 try
352 {
353 BufferedReader bufferedReader = new BufferedReader(reader);
354 ConfigurationNode sectionNode = getRootNode();
355
356 String line = bufferedReader.readLine();
357 while (line != null)
358 {
359 line = line.trim();
360 if (!isCommentLine(line))
361 {
362 if (isSectionLine(line))
363 {
364 String section = line.substring(1, line.length() - 1);
365 sectionNode = getSectionNode(section);
366 }
367
368 else
369 {
370 String key = "";
371 String value = "";
372 int index = findSeparator(line);
373 if (index >= 0)
374 {
375 key = line.substring(0, index);
376 value = parseValue(line.substring(index + 1), bufferedReader);
377 }
378 else
379 {
380 key = line;
381 }
382 key = key.trim();
383 if (key.length() < 1)
384 {
385 // use space for properties with no key
386 key = " ";
387 }
388 createValueNodes(sectionNode, key, value);
389 }
390 }
391
392 line = bufferedReader.readLine();
393 }
394 }
395 catch (IOException e)
396 {
397 throw new ConfigurationException(
398 "Unable to load the configuration", e);
399 }
400 }
401
402 /**
403 * Creates the node(s) for the given key value-pair. If delimiter parsing is
404 * enabled, the value string is split if possible, and for each single value
405 * a node is created. Otherwise only a single node is added to the section.
406 *
407 * @param sectionNode the section node new nodes have to be added
408 * @param key the key
409 * @param value the value string
410 */
411 private void createValueNodes(ConfigurationNode sectionNode, String key,
412 String value)
413 {
414 Collection<String> values;
415 if (isDelimiterParsingDisabled())
416 {
417 values = Collections.singleton(value);
418 }
419 else
420 {
421 values = PropertyConverter.split(value, getListDelimiter(), false);
422 }
423
424 for (String v : values)
425 {
426 ConfigurationNode node = createNode(key);
427 node.setValue(v);
428 sectionNode.addChild(node);
429 }
430 }
431
432 /**
433 * Parse the value to remove the quotes and ignoring the comment. Example:
434 *
435 * <pre>
436 * "value" ; comment -> value
437 * </pre>
438 *
439 * <pre>
440 * 'value' ; comment -> value
441 * </pre>
442 * Note that a comment character is only recognized if there is at least one
443 * whitespace character before it. So it can appear in the property value,
444 * e.g.:
445 * <pre>
446 * C:\\Windows;C:\\Windows\\system32
447 * </pre>
448 *
449 * @param val the value to be parsed
450 * @param reader the reader (needed if multiple lines have to be read)
451 * @throws IOException if an IO error occurs
452 */
453 private static String parseValue(String val, BufferedReader reader) throws IOException
454 {
455 StringBuilder propertyValue = new StringBuilder();
456 boolean lineContinues;
457 String value = val.trim();
458
459 do
460 {
461 boolean quoted = value.startsWith("\"") || value.startsWith("'");
462 boolean stop = false;
463 boolean escape = false;
464
465 char quote = quoted ? value.charAt(0) : 0;
466
467 int i = quoted ? 1 : 0;
468
469 StringBuilder result = new StringBuilder();
470 char lastChar = 0;
471 while (i < value.length() && !stop)
472 {
473 char c = value.charAt(i);
474
475 if (quoted)
476 {
477 if ('\\' == c && !escape)
478 {
479 escape = true;
480 }
481 else if (!escape && quote == c)
482 {
483 stop = true;
484 }
485 else if (escape && quote == c)
486 {
487 escape = false;
488 result.append(c);
489 }
490 else
491 {
492 if (escape)
493 {
494 escape = false;
495 result.append('\\');
496 }
497
498 result.append(c);
499 }
500 }
501 else
502 {
503 if (isCommentChar(c) && Character.isWhitespace(lastChar))
504 {
505 stop = true;
506 }
507 else
508 {
509 result.append(c);
510 }
511 }
512
513 i++;
514 lastChar = c;
515 }
516
517 String v = result.toString();
518 if (!quoted)
519 {
520 v = v.trim();
521 lineContinues = lineContinues(v);
522 if (lineContinues)
523 {
524 // remove trailing "\"
525 v = v.substring(0, v.length() - 1).trim();
526 }
527 }
528 else
529 {
530 lineContinues = lineContinues(value, i);
531 }
532 propertyValue.append(v);
533
534 if (lineContinues)
535 {
536 propertyValue.append(LINE_SEPARATOR);
537 value = reader.readLine();
538 }
539 } while (lineContinues && value != null);
540
541 return propertyValue.toString();
542 }
543
544 /**
545 * Tests whether the specified string contains a line continuation marker.
546 *
547 * @param line the string to check
548 * @return a flag whether this line continues
549 */
550 private static boolean lineContinues(String line)
551 {
552 String s = line.trim();
553 return s.equals(LINE_CONT)
554 || (s.length() > 2 && s.endsWith(LINE_CONT) && Character
555 .isWhitespace(s.charAt(s.length() - 2)));
556 }
557
558 /**
559 * Tests whether the specified string contains a line continuation marker
560 * after the specified position. This method parses the string to remove a
561 * comment that might be present. Then it checks whether a line continuation
562 * marker can be found at the end.
563 *
564 * @param line the line to check
565 * @param pos the start position
566 * @return a flag whether this line continues
567 */
568 private static boolean lineContinues(String line, int pos)
569 {
570 String s;
571
572 if (pos >= line.length())
573 {
574 s = line;
575 }
576 else
577 {
578 int end = pos;
579 while (end < line.length() && !isCommentChar(line.charAt(end)))
580 {
581 end++;
582 }
583 s = line.substring(pos, end);
584 }
585
586 return lineContinues(s);
587 }
588
589 /**
590 * Tests whether the specified character is a comment character.
591 *
592 * @param c the character
593 * @return a flag whether this character starts a comment
594 */
595 private static boolean isCommentChar(char c)
596 {
597 return COMMENT_CHARS.indexOf(c) >= 0;
598 }
599
600 /**
601 * Tries to find the index of the separator character in the given string.
602 * This method checks for the presence of separator characters in the given
603 * string. If multiple characters are found, the first one is assumed to be
604 * the correct separator. If there are quoting characters, they are taken
605 * into account, too.
606 *
607 * @param line the line to be checked
608 * @return the index of the separator character or -1 if none is found
609 */
610 private static int findSeparator(String line)
611 {
612 int index =
613 findSeparatorBeforeQuote(line,
614 findFirstOccurrence(line, QUOTE_CHARACTERS));
615 if (index < 0)
616 {
617 index = findFirstOccurrence(line, SEPARATOR_CHARS);
618 }
619 return index;
620 }
621
622 /**
623 * Checks for the occurrence of the specified separators in the given line.
624 * The index of the first separator is returned.
625 *
626 * @param line the line to be investigated
627 * @param separators a string with the separator characters to look for
628 * @return the lowest index of a separator character or -1 if no separator
629 * is found
630 */
631 private static int findFirstOccurrence(String line, String separators)
632 {
633 int index = -1;
634
635 for (int i = 0; i < separators.length(); i++)
636 {
637 char sep = separators.charAt(i);
638 int pos = line.indexOf(sep);
639 if (pos >= 0)
640 {
641 if (index < 0 || pos < index)
642 {
643 index = pos;
644 }
645 }
646 }
647
648 return index;
649 }
650
651 /**
652 * Searches for a separator character directly before a quoting character.
653 * If the first non-whitespace character before a quote character is a
654 * separator, it is considered the "real" separator in this line - even if
655 * there are other separators before.
656 *
657 * @param line the line to be investigated
658 * @param quoteIndex the index of the quote character
659 * @return the index of the separator before the quote or < 0 if there is
660 * none
661 */
662 private static int findSeparatorBeforeQuote(String line, int quoteIndex)
663 {
664 int index = quoteIndex - 1;
665 while (index >= 0 && Character.isWhitespace(line.charAt(index)))
666 {
667 index--;
668 }
669
670 if (index >= 0 && SEPARATOR_CHARS.indexOf(line.charAt(index)) < 0)
671 {
672 index = -1;
673 }
674
675 return index;
676 }
677
678 /**
679 * Add quotes around the specified value if it contains a comment character.
680 */
681 private String formatValue(String value)
682 {
683 boolean quoted = false;
684
685 for (int i = 0; i < COMMENT_CHARS.length() && !quoted; i++)
686 {
687 char c = COMMENT_CHARS.charAt(i);
688 if (value.indexOf(c) != -1)
689 {
690 quoted = true;
691 }
692 }
693
694 if (quoted)
695 {
696 return '"' + value.replaceAll("\"", "\\\\\\\"") + '"';
697 }
698 else
699 {
700 return value;
701 }
702 }
703
704 /**
705 * Determine if the given line is a comment line.
706 *
707 * @param line The line to check.
708 * @return true if the line is empty or starts with one of the comment
709 * characters
710 */
711 protected boolean isCommentLine(String line)
712 {
713 if (line == null)
714 {
715 return false;
716 }
717 // blank lines are also treated as comment lines
718 return line.length() < 1 || COMMENT_CHARS.indexOf(line.charAt(0)) >= 0;
719 }
720
721 /**
722 * Determine if the given line is a section.
723 *
724 * @param line The line to check.
725 * @return true if the line contains a section
726 */
727 protected boolean isSectionLine(String line)
728 {
729 if (line == null)
730 {
731 return false;
732 }
733 return line.startsWith("[") && line.endsWith("]");
734 }
735
736 /**
737 * Return a set containing the sections in this ini configuration. Note that
738 * changes to this set do not affect the configuration.
739 *
740 * @return a set containing the sections.
741 */
742 public Set<String> getSections()
743 {
744 Set<String> sections = new LinkedHashSet<String>();
745 boolean globalSection = false;
746 boolean inSection = false;
747
748 for (ConfigurationNode node : getRootNode().getChildren())
749 {
750 if (isSectionNode(node))
751 {
752 inSection = true;
753 sections.add(node.getName());
754 }
755 else
756 {
757 if (!inSection && !globalSection)
758 {
759 globalSection = true;
760 sections.add(null);
761 }
762 }
763 }
764
765 return sections;
766 }
767
768 /**
769 * Returns a configuration with the content of the specified section. This
770 * provides an easy way of working with a single section only. The way this
771 * configuration is structured internally, this method is very similar to
772 * calling {@link HierarchicalConfiguration#configurationAt(String)} with
773 * the name of the section in question. There are the following differences
774 * however:
775 * <ul>
776 * <li>This method never throws an exception. If the section does not exist,
777 * it is created now. The configuration returned in this case is empty.</li>
778 * <li>If section is contained multiple times in the configuration, the
779 * configuration returned by this method is initialized with the first
780 * occurrence of the section. (This can only happen if
781 * {@code addProperty()} has been used in a way that does not conform
782 * to the storage scheme used by {@code HierarchicalINIConfiguration}.
783 * If used correctly, there will not be duplicate sections.)</li>
784 * <li>There is special support for the global section: Passing in
785 * <b>null</b> as section name returns a configuration with the content of
786 * the global section (which may also be empty).</li>
787 * </ul>
788 *
789 * @param name the name of the section in question; <b>null</b> represents
790 * the global section
791 * @return a configuration containing only the properties of the specified
792 * section
793 */
794 public SubnodeConfiguration getSection(String name)
795 {
796 if (name == null)
797 {
798 return getGlobalSection();
799 }
800
801 else
802 {
803 try
804 {
805 return configurationAt(name);
806 }
807 catch (IllegalArgumentException iex)
808 {
809 // the passed in key does not map to exactly one node
810 // obtain the node for the section, create it on demand
811 return new SubnodeConfiguration(this, getSectionNode(name));
812 }
813 }
814 }
815
816 /**
817 * Obtains the node representing the specified section. This method is
818 * called while the configuration is loaded. If a node for this section
819 * already exists, it is returned. Otherwise a new node is created.
820 *
821 * @param sectionName the name of the section
822 * @return the node for this section
823 */
824 private ConfigurationNode getSectionNode(String sectionName)
825 {
826 List<ConfigurationNode> nodes = getRootNode().getChildren(sectionName);
827 if (!nodes.isEmpty())
828 {
829 return nodes.get(0);
830 }
831
832 ConfigurationNode node = createNode(sectionName);
833 markSectionNode(node);
834 getRootNode().addChild(node);
835 return node;
836 }
837
838 /**
839 * Creates a sub configuration for the global section of the represented INI
840 * configuration.
841 *
842 * @return the sub configuration for the global section
843 */
844 private SubnodeConfiguration getGlobalSection()
845 {
846 ViewNode parent = new ViewNode();
847
848 for (ConfigurationNode node : getRootNode().getChildren())
849 {
850 if (!isSectionNode(node))
851 {
852 synchronized (node)
853 {
854 parent.addChild(node);
855 }
856 }
857 }
858
859 return createSubnodeConfiguration(parent);
860 }
861
862 /**
863 * Marks a configuration node as a section node. This means that this node
864 * represents a section header. This implementation uses the node's
865 * reference property to store a flag.
866 *
867 * @param node the node to be marked
868 */
869 private static void markSectionNode(ConfigurationNode node)
870 {
871 node.setReference(Boolean.TRUE);
872 }
873
874 /**
875 * Checks whether the specified configuration node represents a section.
876 *
877 * @param node the node in question
878 * @return a flag whether this node represents a section
879 */
880 private static boolean isSectionNode(ConfigurationNode node)
881 {
882 return node.getReference() != null || node.getChildrenCount() > 0;
883 }
884 }