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