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 */ 017package org.apache.commons.configuration2.tree; 018 019import java.util.Iterator; 020import java.util.NoSuchElementException; 021 022import org.apache.commons.lang3.StringUtils; 023 024/** 025 * <p> 026 * A simple class that supports creation of and iteration on configuration keys supported by a 027 * {@link DefaultExpressionEngine} object. 028 * </p> 029 * <p> 030 * For key creation the class works similar to a StringBuffer: There are several {@code appendXXXX()} methods with which 031 * single parts of a key can be constructed. All these methods return a reference to the actual object so they can be 032 * written in a chain. When using this methods the exact syntax for keys need not be known. 033 * </p> 034 * <p> 035 * This class also defines a specialized iterator for configuration keys. With such an iterator a key can be tokenized 036 * into its single parts. For each part it can be checked whether it has an associated index. 037 * </p> 038 * <p> 039 * Instances of this class are always associated with an instance of {@link DefaultExpressionEngine}, from which the 040 * current delimiters are obtained. So key creation and parsing is specific to this associated expression engine. 041 * </p> 042 * 043 * @since 1.3 044 */ 045public class DefaultConfigurationKey { 046 /** 047 * A specialized iterator class for tokenizing a configuration key. This class implements the normal iterator interface. 048 * In addition it provides some specific methods for configuration keys. 049 */ 050 public class KeyIterator implements Iterator<Object>, Cloneable { 051 /** Stores the current key name. */ 052 private String current; 053 054 /** Stores the start index of the actual token. */ 055 private int startIndex; 056 057 /** Stores the end index of the actual token. */ 058 private int endIndex; 059 060 /** Stores the index of the actual property if there is one. */ 061 private int indexValue; 062 063 /** Stores a flag if the actual property has an index. */ 064 private boolean hasIndex; 065 066 /** Stores a flag if the actual property is an attribute. */ 067 private boolean attribute; 068 069 /** 070 * Helper method for checking if the passed key is an attribute. If this is the case, the internal fields will be set. 071 * 072 * @param key the key to be checked 073 * @return a flag if the key is an attribute 074 */ 075 private boolean checkAttribute(final String key) { 076 if (isAttributeKey(key)) { 077 current = removeAttributeMarkers(key); 078 return true; 079 } 080 return false; 081 } 082 083 /** 084 * Helper method for checking if the passed key contains an index. If this is the case, internal fields will be set. 085 * 086 * @param key the key to be checked 087 * @return a flag if an index is defined 088 */ 089 private boolean checkIndex(final String key) { 090 boolean result = false; 091 092 try { 093 final int idx = key.lastIndexOf(getSymbols().getIndexStart()); 094 if (idx > 0) { 095 final int endidx = key.indexOf(getSymbols().getIndexEnd(), idx); 096 097 if (endidx > idx + 1) { 098 indexValue = Integer.parseInt(key.substring(idx + 1, endidx)); 099 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 <b>false</b>, the 138 * attribute markers are stripped from the key; if it is <b>true</b>, 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 (".") is used as delimiter, and a doubled dot ("..") as escaped delimiter, the 179 * escaped delimiter starts at the same position as the delimiter. If the token "\." was used, it would start 180 * one character before the delimiter because the delimiter character "." 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 "attribute 253 * emulating mode". 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 <b>true</b>. 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 <b>false</b>, the attribute markers are stripped from 325 * the key; if it is <b>true</b>, 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 <b>null</b>) 400 * @throws IllegalArgumentException if the expression engine is <b>null</b> 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 <b>null</b>) 411 * @param key the key to be wrapped 412 * @throws IllegalArgumentException if the expression engine is <b>null</b> 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 <b>true</b>, 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 "difference key" 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 <b>true</b> if this is an attribute key, <b>false</b> 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, e.g. 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}