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