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 {@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 */
054 public 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 }