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 (&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    }