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; 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 * Constructs a new instance. 071 */ 072 public KeyIterator() { 073 // empty 074 } 075 076 /** 077 * Helper method for checking if the passed key is an attribute. If this is the case, the internal fields will be set. 078 * 079 * @param key the key to be checked 080 * @return a flag if the key is an attribute 081 */ 082 private boolean checkAttribute(final String key) { 083 if (isAttributeKey(key)) { 084 current = removeAttributeMarkers(key); 085 return true; 086 } 087 return false; 088 } 089 090 /** 091 * Helper method for checking if the passed key contains an index. If this is the case, internal fields will be set. 092 * 093 * @param key the key to be checked 094 * @return a flag if an index is defined 095 */ 096 private boolean checkIndex(final String key) { 097 boolean result = false; 098 099 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}