1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.configuration.tree;
18
19 import java.util.Iterator;
20 import java.util.NoSuchElementException;
21
22 import org.apache.commons.lang.StringUtils;
23
24 /***
25 * <p>
26 * A simple class that supports creation of and iteration on configuration keys
27 * supported by a <code>{@link DefaultExpressionEngine}</code> object.
28 * </p>
29 * <p>
30 * For key creation the class works similar to a StringBuffer: There are several
31 * <code>appendXXXX()</code> methods with which single parts of a key can be
32 * constructed. All these methods return a reference to the actual object so
33 * they can be written in a chain. When using this methods the exact syntax for
34 * keys need not be known.
35 * </p>
36 * <p>
37 * This class also defines a specialized iterator for configuration keys. With
38 * such an iterator a key can be tokenized into its single parts. For each part
39 * it can be checked whether it has an associated index.
40 * </p>
41 * <p>
42 * Instances of this class are always associated with an instance of
43 * <code>{@link DefaultExpressionEngine}</code>, from which the current
44 * delimiters are obtained. So key creation and parsing is specific to this
45 * associated expression engine.
46 * </p>
47 *
48 * @since 1.3
49 * @author Oliver Heger
50 * @version $Id: DefaultConfigurationKey.java 439648 2006-09-02 20:42:10Z oheger $
51 */
52 public class DefaultConfigurationKey
53 {
54 /*** Constant for the initial StringBuffer size. */
55 private static final int INITIAL_SIZE = 32;
56
57 /*** Stores a reference to the associated expression engine. */
58 private DefaultExpressionEngine expressionEngine;
59
60 /*** Holds a buffer with the so far created key. */
61 private StringBuffer keyBuffer;
62
63 /***
64 * Creates a new instance of <code>DefaultConfigurationKey</code> and sets
65 * the associated expression engine.
66 *
67 * @param engine the expression engine
68 */
69 public DefaultConfigurationKey(DefaultExpressionEngine engine)
70 {
71 keyBuffer = new StringBuffer(INITIAL_SIZE);
72 setExpressionEngine(engine);
73 }
74
75 /***
76 * Creates a new instance of <code>DefaultConfigurationKey</code> and sets
77 * the associated expression engine and an initial key.
78 *
79 * @param engine the expression engine
80 * @param key the key to be wrapped
81 */
82 public DefaultConfigurationKey(DefaultExpressionEngine engine, String key)
83 {
84 setExpressionEngine(engine);
85 keyBuffer = new StringBuffer(trim(key));
86 }
87
88 /***
89 * Returns the associated default expression engine.
90 *
91 * @return the associated expression engine
92 */
93 public DefaultExpressionEngine getExpressionEngine()
94 {
95 return expressionEngine;
96 }
97
98 /***
99 * 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
583
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
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
652 while (startIndex < length()
653 && hasLeadingDelimiter(keyBuffer.substring(startIndex)))
654 {
655 startIndex += getExpressionEngine().getPropertyDelimiter()
656 .length();
657 }
658
659
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
748 return -1;
749 }
750 int escapeOffset = escapeOffset();
751 if (escapeOffset < 0 || escapeOffset > pos)
752 {
753
754 return -1;
755 }
756
757 int escapePos = key.indexOf(getExpressionEngine()
758 .getEscapedDelimiter(), pos - escapeOffset);
759 if (escapePos <= pos && escapePos >= 0)
760 {
761
762
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 }