View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
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 {@link DefaultExpressionEngine} object.
28   * </p>
29   * <p>
30   * For key creation the class works similar to a StringBuffer: There are several
31   * {@code appendXXXX()} 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   * {@link DefaultExpressionEngine}, 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 <a
50   * href="http://commons.apache.org/configuration/team-list.html">Commons
51   * Configuration team</a>
52   * @version $Id: DefaultConfigurationKey.java 1231724 2012-01-15 18:40:31Z oheger $
53   */
54  public class DefaultConfigurationKey
55  {
56      /** Constant for the initial StringBuffer size. */
57      private static final int INITIAL_SIZE = 32;
58  
59      /** Stores a reference to the associated expression engine. */
60      private DefaultExpressionEngine expressionEngine;
61  
62      /** Holds a buffer with the so far created key. */
63      private StringBuilder keyBuffer;
64  
65      /**
66       * Creates a new instance of {@code DefaultConfigurationKey} and sets
67       * the associated expression engine.
68       *
69       * @param engine the expression engine
70       */
71      public DefaultConfigurationKey(DefaultExpressionEngine engine)
72      {
73          keyBuffer = new StringBuilder(INITIAL_SIZE);
74          setExpressionEngine(engine);
75      }
76  
77      /**
78       * Creates a new instance of {@code DefaultConfigurationKey} and sets
79       * the associated expression engine and an initial key.
80       *
81       * @param engine the expression engine
82       * @param key the key to be wrapped
83       */
84      public DefaultConfigurationKey(DefaultExpressionEngine engine, String key)
85      {
86          setExpressionEngine(engine);
87          keyBuffer = new StringBuilder(trim(key));
88      }
89  
90      /**
91       * Returns the associated default expression engine.
92       *
93       * @return the associated expression engine
94       */
95      public DefaultExpressionEngine getExpressionEngine()
96      {
97          return expressionEngine;
98      }
99  
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 (&quot;.&quot;) is
783          * used as delimiter, and a doubled dot (&quot;..&quot;) as escaped
784          * delimiter, the escaped delimiter starts at the same position as the
785          * delimiter. If the token &quot;\.&quot; was used, it would start one
786          * character before the delimiter because the delimiter character
787          * &quot;.&quot; 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 &quot;attribute emulating mode&quot;.
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 }