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 1158891 2011-08-17 20:08:39Z 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 try
827 {
828 int idx = key.lastIndexOf(getExpressionEngine().getIndexStart());
829 if (idx > 0)
830 {
831 int endidx = key.indexOf(getExpressionEngine().getIndexEnd(),
832 idx);
833
834 if (endidx > idx + 1)
835 {
836 indexValue = Integer.parseInt(key.substring(idx + 1, endidx));
837 current = key.substring(0, idx);
838 result = true;
839 }
840 }
841 }
842 catch (NumberFormatException nfe)
843 {
844 result = false;
845 }
846
847 return result;
848 }
849
850 /**
851 * Returns a flag whether attributes are marked the same way as normal
852 * property keys. We call this the "attribute emulating mode".
853 * When navigating through node hierarchies it might be convenient to
854 * treat attributes the same way than other child nodes, so an
855 * expression engine supports to set the attribute markers to the same
856 * value than the property delimiter. If this is the case, some special
857 * checks have to be performed.
858 *
859 * @return a flag if attributes and normal property keys are treated the
860 * same way
861 */
862 private boolean isAttributeEmulatingMode()
863 {
864 return getExpressionEngine().getAttributeEnd() == null
865 && StringUtils.equals(getExpressionEngine()
866 .getPropertyDelimiter(), getExpressionEngine()
867 .getAttributeStart());
868 }
869 }
870 }