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  package org.apache.commons.configuration2.tree;
18  
19  import java.util.Iterator;
20  import java.util.NoSuchElementException;
21  
22  import org.apache.commons.lang3.StringUtils;
23  
24  /**
25   * <p>
26   * A simple class that supports creation of and iteration on configuration keys supported by a
27   * {@link DefaultExpressionEngine} object.
28   * </p>
29   * <p>
30   * For key creation the class works similar to a StringBuffer: There are several {@code appendXXXX()} methods with which
31   * single parts of a key can be constructed. All these methods return a reference to the actual object so they can be
32   * written in a chain. When using this methods the exact syntax for keys need not be known.
33   * </p>
34   * <p>
35   * This class also defines a specialized iterator for configuration keys. With such an iterator a key can be tokenized
36   * into its single parts. For each part it can be checked whether it has an associated index.
37   * </p>
38   * <p>
39   * Instances of this class are always associated with an instance of {@link DefaultExpressionEngine}, from which the
40   * current delimiters are obtained. So key creation and parsing is specific to this associated expression engine.
41   * </p>
42   *
43   * @since 1.3
44   */
45  public class DefaultConfigurationKey {
46      /**
47       * A specialized iterator class for tokenizing a configuration key. This class implements the normal iterator interface.
48       * In addition it provides some specific methods for configuration keys.
49       */
50      public class KeyIterator implements Iterator<Object>, Cloneable {
51          /** Stores the current key name. */
52          private String current;
53  
54          /** Stores the start index of the actual token. */
55          private int startIndex;
56  
57          /** Stores the end index of the actual token. */
58          private int endIndex;
59  
60          /** Stores the index of the actual property if there is one. */
61          private int indexValue;
62  
63          /** Stores a flag if the actual property has an index. */
64          private boolean hasIndex;
65  
66          /** Stores a flag if the actual property is an attribute. */
67          private boolean attribute;
68  
69          /**
70           * Helper method for checking if the passed key is an attribute. If this is the case, the internal fields will be set.
71           *
72           * @param key the key to be checked
73           * @return a flag if the key is an attribute
74           */
75          private boolean checkAttribute(final String key) {
76              if (isAttributeKey(key)) {
77                  current = removeAttributeMarkers(key);
78                  return true;
79              }
80              return false;
81          }
82  
83          /**
84           * Helper method for checking if the passed key contains an index. If this is the case, internal fields will be set.
85           *
86           * @param key the key to be checked
87           * @return a flag if an index is defined
88           */
89          private boolean checkIndex(final String key) {
90              boolean result = false;
91  
92              try {
93                  final int idx = key.lastIndexOf(getSymbols().getIndexStart());
94                  if (idx > 0) {
95                      final int endidx = key.indexOf(getSymbols().getIndexEnd(), idx);
96  
97                      if (endidx > idx + 1) {
98                          indexValue = Integer.parseInt(key.substring(idx + 1, endidx));
99                          current = key.substring(0, idx);
100                         result = true;
101                     }
102                 }
103             } catch (final NumberFormatException nfe) {
104                 result = false;
105             }
106 
107             return result;
108         }
109 
110         /**
111          * Creates a clone of this object.
112          *
113          * @return a clone of this object
114          */
115         @Override
116         public Object clone() {
117             try {
118                 return super.clone();
119             } catch (final CloneNotSupportedException cex) {
120                 // should not happen
121                 return null;
122             }
123         }
124 
125         /**
126          * Returns the current key of the iteration (without skipping to the next element). This is the same key the previous
127          * {@code next()} call had returned. (Short form of {@code currentKey(false)}.
128          *
129          * @return the current key
130          */
131         public String currentKey() {
132             return currentKey(false);
133         }
134 
135         /**
136          * Returns the current key of the iteration (without skipping to the next element). The boolean parameter indicates
137          * wheter a decorated key should be returned. This affects only attribute keys: if the parameter is <strong>false</strong>, the
138          * attribute markers are stripped from the key; if it is <strong>true</strong>, they remain.
139          *
140          * @param decorated a flag if the decorated key is to be returned
141          * @return the current key
142          */
143         public String currentKey(final boolean decorated) {
144             return decorated && !isPropertyKey() ? constructAttributeKey(current) : current;
145         }
146 
147         /**
148          * Checks if a delimiter at the specified position is escaped. If this is the case, the next valid search position will
149          * be returned. Otherwise the return value is -1.
150          *
151          * @param key the key to check
152          * @param pos the position where a delimiter was found
153          * @return information about escaped delimiters
154          */
155         private int escapedPosition(final String key, final int pos) {
156             if (getSymbols().getEscapedDelimiter() == null) {
157                 // nothing to escape
158                 return -1;
159             }
160             final int escapeOffset = escapeOffset();
161             if (escapeOffset < 0 || escapeOffset > pos) {
162                 // No escaping possible at this position
163                 return -1;
164             }
165 
166             final int escapePos = key.indexOf(getSymbols().getEscapedDelimiter(), pos - escapeOffset);
167             if (escapePos <= pos && escapePos >= 0) {
168                 // The found delimiter is escaped. Next valid search position
169                 // is behind the escaped delimiter.
170                 return escapePos + getSymbols().getEscapedDelimiter().length();
171             }
172             return -1;
173         }
174 
175         /**
176          * Determines the relative offset of an escaped delimiter in relation to a delimiter. Depending on the used delimiter
177          * and escaped delimiter tokens the position where to search for an escaped delimiter is different. If, for instance,
178          * the dot character (&quot;.&quot;) is used as delimiter, and a doubled dot (&quot;..&quot;) as escaped delimiter, the
179          * escaped delimiter starts at the same position as the delimiter. If the token &quot;\.&quot; was used, it would start
180          * one character before the delimiter because the delimiter character &quot;.&quot; is the second character in the
181          * escaped delimiter string. This relation will be determined by this method. For this to work the delimiter string must
182          * be contained in the escaped delimiter string.
183          *
184          * @return the relative offset of the escaped delimiter in relation to a delimiter
185          */
186         private int escapeOffset() {
187             return getSymbols().getEscapedDelimiter().indexOf(getSymbols().getPropertyDelimiter());
188         }
189 
190         /**
191          * Helper method for determining the next indices.
192          *
193          * @return the next key part
194          */
195         private String findNextIndices() {
196             startIndex = endIndex;
197             // skip empty names
198             while (startIndex < length() && hasLeadingDelimiter(keyBuffer.substring(startIndex))) {
199                 startIndex += getSymbols().getPropertyDelimiter().length();
200             }
201 
202             // Key ends with a delimiter?
203             if (startIndex >= length()) {
204                 endIndex = length();
205                 startIndex = endIndex - 1;
206                 return keyBuffer.substring(startIndex, endIndex);
207             }
208             return nextKeyPart();
209         }
210 
211         /**
212          * Gets the index value of the current key. If the current key does not have an index, return value is -1. This
213          * method can be called after {@code next()}.
214          *
215          * @return the index value of the current key
216          */
217         public int getIndex() {
218             return indexValue;
219         }
220 
221         /**
222          * Returns a flag if the current key has an associated index. This method can be called after {@code next()}.
223          *
224          * @return a flag if the current key has an index
225          */
226         public boolean hasIndex() {
227             return hasIndex;
228         }
229 
230         /**
231          * Checks if there is a next element.
232          *
233          * @return a flag if there is a next element
234          */
235         @Override
236         public boolean hasNext() {
237             return endIndex < keyBuffer.length();
238         }
239 
240         /**
241          * Returns a flag if the current key is an attribute. This method can be called after {@code next()}.
242          *
243          * @return a flag if the current key is an attribute
244          */
245         public boolean isAttribute() {
246             // if attribute emulation mode is active, the last part of a key is
247             // always an attribute key, too
248             return attribute || isAttributeEmulatingMode() && !hasNext();
249         }
250 
251         /**
252          * Returns a flag whether attributes are marked the same way as normal property keys. We call this the &quot;attribute
253          * emulating mode&quot;. When navigating through node hierarchies it might be convenient to treat attributes the same
254          * way than other child nodes, so an expression engine supports to set the attribute markers to the same value than the
255          * property delimiter. If this is the case, some special checks have to be performed.
256          *
257          * @return a flag if attributes and normal property keys are treated the same way
258          */
259         private boolean isAttributeEmulatingMode() {
260             return getSymbols().getAttributeEnd() == null && StringUtils.equals(getSymbols().getPropertyDelimiter(), getSymbols().getAttributeStart());
261         }
262 
263         /**
264          * Returns a flag whether the current key refers to a property (i.e. is no special attribute key). Usually this method
265          * will return the opposite of {@code isAttribute()}, but if the delimiters for normal properties and attributes are set
266          * to the same string, it is possible that both methods return <strong>true</strong>.
267          *
268          * @return a flag if the current key is a property key
269          * @see #isAttribute()
270          */
271         public boolean isPropertyKey() {
272             return !attribute;
273         }
274 
275         /**
276          * Returns the next object in the iteration.
277          *
278          * @return the next object
279          */
280         @Override
281         public Object next() {
282             return nextKey();
283         }
284 
285         /**
286          * Searches the next unescaped delimiter from the given position.
287          *
288          * @param key the key
289          * @param pos the start position
290          * @param endPos the end position
291          * @return the position of the next delimiter or -1 if there is none
292          */
293         private int nextDelimiterPos(final String key, final int pos, final int endPos) {
294             int delimiterPos = pos;
295             boolean found = false;
296 
297             do {
298                 delimiterPos = key.indexOf(getSymbols().getPropertyDelimiter(), delimiterPos);
299                 if (delimiterPos < 0 || delimiterPos >= endPos) {
300                     return -1;
301                 }
302                 final int escapePos = escapedPosition(key, delimiterPos);
303                 if (escapePos < 0) {
304                     found = true;
305                 } else {
306                     delimiterPos = escapePos;
307                 }
308             } while (!found);
309 
310             return delimiterPos;
311         }
312 
313         /**
314          * Returns the next key part of this configuration key. This is a short form of {@code nextKey(false)}.
315          *
316          * @return the next key part
317          */
318         public String nextKey() {
319             return nextKey(false);
320         }
321 
322         /**
323          * Returns the next key part of this configuration key. The boolean parameter indicates wheter a decorated key should be
324          * returned. This affects only attribute keys: if the parameter is <strong>false</strong>, the attribute markers are stripped from
325          * the key; if it is <strong>true</strong>, they remain.
326          *
327          * @param decorated a flag if the decorated key is to be returned
328          * @return the next key part
329          */
330         public String nextKey(final boolean decorated) {
331             if (!hasNext()) {
332                 throw new NoSuchElementException("No more key parts!");
333             }
334 
335             hasIndex = false;
336             indexValue = -1;
337             final String key = findNextIndices();
338 
339             current = key;
340             hasIndex = checkIndex(key);
341             attribute = checkAttribute(current);
342 
343             return currentKey(decorated);
344         }
345 
346         /**
347          * Helper method for extracting the next key part. Takes escaping of delimiter characters into account.
348          *
349          * @return the next key part
350          */
351         private String nextKeyPart() {
352             int attrIdx = keyBuffer.toString().indexOf(getSymbols().getAttributeStart(), startIndex);
353             if (attrIdx < 0 || attrIdx == startIndex) {
354                 attrIdx = length();
355             }
356 
357             int delIdx = nextDelimiterPos(keyBuffer.toString(), startIndex, attrIdx);
358             if (delIdx < 0) {
359                 delIdx = attrIdx;
360             }
361 
362             endIndex = Math.min(attrIdx, delIdx);
363             return unescapeDelimiters(keyBuffer.substring(startIndex, endIndex));
364         }
365 
366         /**
367          * Removes the current object in the iteration. This method is not supported by this iterator type, so an exception is
368          * thrown.
369          */
370         @Override
371         public void remove() {
372             throw new UnsupportedOperationException("Remove not supported!");
373         }
374     }
375 
376     /** Constant for the initial StringBuffer size. */
377     private static final int INITIAL_SIZE = 32;
378 
379     /**
380      * Helper method for comparing two key parts.
381      *
382      * @param it1 the iterator with the first part
383      * @param it2 the iterator with the second part
384      * @return a flag if both parts are equal
385      */
386     private static boolean partsEqual(final KeyIterator it1, final KeyIterator it2) {
387         return it1.nextKey().equals(it2.nextKey()) && it1.getIndex() == it2.getIndex() && it1.isAttribute() == it2.isAttribute();
388     }
389 
390     /** Stores a reference to the associated expression engine. */
391     private final DefaultExpressionEngine expressionEngine;
392 
393     /** Holds a buffer with the so far created key. */
394     private final StringBuilder keyBuffer;
395 
396     /**
397      * Creates a new instance of {@code DefaultConfigurationKey} and sets the associated expression engine.
398      *
399      * @param engine the expression engine (must not be <strong>null</strong>)
400      * @throws IllegalArgumentException if the expression engine is <strong>null</strong>
401      */
402     public DefaultConfigurationKey(final DefaultExpressionEngine engine) {
403         this(engine, null);
404     }
405 
406     /**
407      * Creates a new instance of {@code DefaultConfigurationKey} and sets the associated expression engine and an initial
408      * key.
409      *
410      * @param engine the expression engine (must not be <strong>null</strong>)
411      * @param key the key to be wrapped
412      * @throws IllegalArgumentException if the expression engine is <strong>null</strong>
413      */
414     public DefaultConfigurationKey(final DefaultExpressionEngine engine, final String key) {
415         if (engine == null) {
416             throw new IllegalArgumentException("Expression engine must not be null!");
417         }
418         expressionEngine = engine;
419         if (key != null) {
420             keyBuffer = new StringBuilder(trim(key));
421         } else {
422             keyBuffer = new StringBuilder(INITIAL_SIZE);
423         }
424     }
425 
426     /**
427      * Appends the name of a property to this key. If necessary, a property delimiter will be added. Property delimiters in
428      * the given string will not be escaped.
429      *
430      * @param property the name of the property to be added
431      * @return a reference to this object
432      */
433     public DefaultConfigurationKey append(final String property) {
434         return append(property, false);
435     }
436 
437     /**
438      * Appends the name of a property to this key. If necessary, a property delimiter will be added. If the boolean argument
439      * is set to <strong>true</strong>, property delimiters contained in the property name will be escaped.
440      *
441      * @param property the name of the property to be added
442      * @param escape a flag if property delimiters in the passed in property name should be escaped
443      * @return a reference to this object
444      */
445     public DefaultConfigurationKey append(final String property, final boolean escape) {
446         String key;
447         if (escape && property != null) {
448             key = escapeDelimiters(property);
449         } else {
450             key = property;
451         }
452         key = trim(key);
453 
454         if (keyBuffer.length() > 0 && !isAttributeKey(property) && !key.isEmpty()) {
455             keyBuffer.append(getSymbols().getPropertyDelimiter());
456         }
457 
458         keyBuffer.append(key);
459         return this;
460     }
461 
462     /**
463      * Appends an attribute to this configuration key.
464      *
465      * @param attr the name of the attribute to be appended
466      * @return a reference to this object
467      */
468     public DefaultConfigurationKey appendAttribute(final String attr) {
469         keyBuffer.append(constructAttributeKey(attr));
470         return this;
471     }
472 
473     /**
474      * Appends an index to this configuration key.
475      *
476      * @param index the index to be appended
477      * @return a reference to this object
478      */
479     public DefaultConfigurationKey appendIndex(final int index) {
480         keyBuffer.append(getSymbols().getIndexStart());
481         keyBuffer.append(index);
482         keyBuffer.append(getSymbols().getIndexEnd());
483         return this;
484     }
485 
486     /**
487      * Extracts the name of the attribute from the given attribute key. This method removes the attribute markers - if any -
488      * from the specified key.
489      *
490      * @param key the attribute key
491      * @return the name of the corresponding attribute
492      */
493     public String attributeName(final String key) {
494         return isAttributeKey(key) ? removeAttributeMarkers(key) : key;
495     }
496 
497     /**
498      * Returns a configuration key object that is initialized with the part of the key that is common to this key and the
499      * passed in key.
500      *
501      * @param other the other key
502      * @return a key object with the common key part
503      */
504     public DefaultConfigurationKey commonKey(final DefaultConfigurationKey other) {
505         if (other == null) {
506             throw new IllegalArgumentException("Other key must no be null!");
507         }
508 
509         final DefaultConfigurationKey result = new DefaultConfigurationKey(getExpressionEngine());
510         final KeyIterator it1 = iterator();
511         final KeyIterator it2 = other.iterator();
512 
513         while (it1.hasNext() && it2.hasNext() && partsEqual(it1, it2)) {
514             if (it1.isAttribute()) {
515                 result.appendAttribute(it1.currentKey());
516             } else {
517                 result.append(it1.currentKey());
518                 if (it1.hasIndex) {
519                     result.appendIndex(it1.getIndex());
520                 }
521             }
522         }
523 
524         return result;
525     }
526 
527     /**
528      * Decorates the given key so that it represents an attribute. Adds special start and end markers. The passed in string
529      * will be modified only if does not already represent an attribute.
530      *
531      * @param key the key to be decorated
532      * @return the decorated attribute key
533      */
534     public String constructAttributeKey(final String key) {
535         if (key == null) {
536             return StringUtils.EMPTY;
537         }
538         if (isAttributeKey(key)) {
539             return key;
540         }
541         final StringBuilder buf = new StringBuilder();
542         buf.append(getSymbols().getAttributeStart()).append(key);
543         if (getSymbols().getAttributeEnd() != null) {
544             buf.append(getSymbols().getAttributeEnd());
545         }
546         return buf.toString();
547     }
548 
549     /**
550      * Returns the &quot;difference key&quot; to a given key. This value is the part of the passed in key that differs from
551      * this key. There is the following relation: {@code other = key.commonKey(other) + key.differenceKey(other)} for an
552      * arbitrary configuration key {@code key}.
553      *
554      * @param other the key for which the difference is to be calculated
555      * @return the difference key
556      */
557     public DefaultConfigurationKey differenceKey(final DefaultConfigurationKey other) {
558         final DefaultConfigurationKey common = commonKey(other);
559         final DefaultConfigurationKey result = new DefaultConfigurationKey(getExpressionEngine());
560 
561         if (common.length() < other.length()) {
562             final String k = other.toString().substring(common.length());
563             // skip trailing delimiters
564             int i = 0;
565             while (i < k.length() && String.valueOf(k.charAt(i)).equals(getSymbols().getPropertyDelimiter())) {
566                 i++;
567             }
568 
569             if (i < k.length()) {
570                 result.append(k.substring(i));
571             }
572         }
573 
574         return result;
575     }
576 
577     /**
578      * Checks if two {@code ConfigurationKey} objects are equal. Two instances of this class are considered equal if they
579      * have the same content (i.e. their internal string representation is equal). The expression engine property is not
580      * taken into account.
581      *
582      * @param obj the object to compare
583      * @return a flag if both objects are equal
584      */
585     @Override
586     public boolean equals(final Object obj) {
587         if (this == obj) {
588             return true;
589         }
590         if (!(obj instanceof DefaultConfigurationKey)) {
591             return false;
592         }
593 
594         final DefaultConfigurationKey c = (DefaultConfigurationKey) obj;
595         return keyBuffer.toString().equals(c.toString());
596     }
597 
598     /**
599      * Escapes the delimiters in the specified string.
600      *
601      * @param key the key to be escaped
602      * @return the escaped key
603      */
604     private String escapeDelimiters(final String key) {
605         return getSymbols().getEscapedDelimiter() == null || !key.contains(getSymbols().getPropertyDelimiter()) ? key
606             : StringUtils.replace(key, getSymbols().getPropertyDelimiter(), getSymbols().getEscapedDelimiter());
607     }
608 
609     /**
610      * Gets the associated default expression engine.
611      *
612      * @return the associated expression engine
613      */
614     public DefaultExpressionEngine getExpressionEngine() {
615         return expressionEngine;
616     }
617 
618     /**
619      * Gets the symbols object from the associated expression engine.
620      *
621      * @return the {@code DefaultExpressionEngineSymbols}
622      */
623     private DefaultExpressionEngineSymbols getSymbols() {
624         return getExpressionEngine().getSymbols();
625     }
626 
627     /**
628      * Returns the hash code for this object.
629      *
630      * @return the hash code
631      */
632     @Override
633     public int hashCode() {
634         return String.valueOf(keyBuffer).hashCode();
635     }
636 
637     /**
638      * Helper method that checks if the specified key starts with a property delimiter.
639      *
640      * @param key the key to check
641      * @return a flag if there is a leading delimiter
642      */
643     private boolean hasLeadingDelimiter(final String key) {
644         return key.startsWith(getSymbols().getPropertyDelimiter())
645             && (getSymbols().getEscapedDelimiter() == null || !key.startsWith(getSymbols().getEscapedDelimiter()));
646     }
647 
648     /**
649      * Helper method that checks if the specified key ends with a property delimiter.
650      *
651      * @param key the key to check
652      * @return a flag if there is a trailing delimiter
653      */
654     private boolean hasTrailingDelimiter(final String key) {
655         return key.endsWith(getSymbols().getPropertyDelimiter())
656             && (getSymbols().getEscapedDelimiter() == null || !key.endsWith(getSymbols().getEscapedDelimiter()));
657     }
658 
659     /**
660      * Tests if the specified key represents an attribute according to the current expression engine.
661      *
662      * @param key the key to be checked
663      * @return <strong>true</strong> if this is an attribute key, <strong>false</strong> otherwise
664      */
665     public boolean isAttributeKey(final String key) {
666         if (key == null) {
667             return false;
668         }
669 
670         return key.startsWith(getSymbols().getAttributeStart()) && (getSymbols().getAttributeEnd() == null || key.endsWith(getSymbols().getAttributeEnd()));
671     }
672 
673     /**
674      * Returns an iterator for iterating over the single components of this configuration key.
675      *
676      * @return an iterator for this key
677      */
678     public KeyIterator iterator() {
679         return new KeyIterator();
680     }
681 
682     /**
683      * Returns the actual length of this configuration key.
684      *
685      * @return the length of this key
686      */
687     public int length() {
688         return keyBuffer.length();
689     }
690 
691     /**
692      * Helper method for removing attribute markers from a key.
693      *
694      * @param key the key
695      * @return the key with removed attribute markers
696      */
697     private String removeAttributeMarkers(final String key) {
698         return key.substring(getSymbols().getAttributeStart().length(),
699             key.length() - (getSymbols().getAttributeEnd() != null ? getSymbols().getAttributeEnd().length() : 0));
700     }
701 
702     /**
703      * Sets the new length of this configuration key. With this method it is possible to truncate the key, for example to return to
704      * a state prior calling some {@code append()} methods. The semantic is the same as the {@code setLength()} method of
705      * {@code StringBuilder}.
706      *
707      * @param len the new length of the key
708      */
709     public void setLength(final int len) {
710         keyBuffer.setLength(len);
711     }
712 
713     /**
714      * Returns a string representation of this object. This is the configuration key as a plain string.
715      *
716      * @return a string for this object
717      */
718     @Override
719     public String toString() {
720         return keyBuffer.toString();
721     }
722 
723     /**
724      * Removes delimiters at the beginning and the end of the specified key.
725      *
726      * @param key the key
727      * @return the key with removed property delimiters
728      */
729     public String trim(final String key) {
730         return trimRight(trimLeft(key));
731     }
732 
733     /**
734      * Removes leading property delimiters from the specified key.
735      *
736      * @param key the key
737      * @return the key with removed leading property delimiters
738      */
739     public String trimLeft(final String key) {
740         if (key == null) {
741             return StringUtils.EMPTY;
742         }
743         String result = key;
744         while (hasLeadingDelimiter(result)) {
745             result = result.substring(getSymbols().getPropertyDelimiter().length());
746         }
747         return result;
748     }
749 
750     /**
751      * Removes trailing property delimiters from the specified key.
752      *
753      * @param key the key
754      * @return the key with removed trailing property delimiters
755      */
756     public String trimRight(final String key) {
757         if (key == null) {
758             return StringUtils.EMPTY;
759         }
760         String result = key;
761         while (hasTrailingDelimiter(result)) {
762             result = result.substring(0, result.length() - getSymbols().getPropertyDelimiter().length());
763         }
764         return result;
765     }
766 
767     /**
768      * Unescapes the delimiters in the specified string.
769      *
770      * @param key the key to be unescaped
771      * @return the unescaped key
772      */
773     private String unescapeDelimiters(final String key) {
774         return getSymbols().getEscapedDelimiter() == null ? key
775             : StringUtils.replace(key, getSymbols().getEscapedDelimiter(), getSymbols().getPropertyDelimiter());
776     }
777 }