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 <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             // 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 (&quot;.&quot;) is
777          * used as delimiter, and a doubled dot (&quot;..&quot;) as escaped
778          * delimiter, the escaped delimiter starts at the same position as the
779          * delimiter. If the token &quot;\.&quot; was used, it would start one
780          * character before the delimiter because the delimiter character
781          * &quot;.&quot; 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 &quot;attribute emulating mode&quot;.
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 }