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