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