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