    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 */
    017package org.apache.commons.configuration2.tree;
    018
    019import java.util.Iterator;
    020import java.util.NoSuchElementException;
    021
    022import org.apache.commons.lang3.StringUtils;
    023
    024/**
    025 * <p>
    026 * A simple class that supports creation of and iteration on configuration keys supported by a
    027 * {@link DefaultExpressionEngine} object.
    028 * </p>
    029 * <p>
    030 * For key creation the class works similar to a StringBuffer: There are several {@code appendXXXX()} methods with which
    031 * single parts of a key can be constructed. All these methods return a reference to the actual object so they can be
    032 * written in a chain. When using this methods the exact syntax for keys need not be known.
    033 * </p>
    034 * <p>
    035 * This class also defines a specialized iterator for configuration keys. With such an iterator a key can be tokenized
    036 * into its single parts. For each part it can be checked whether it has an associated index.
    037 * </p>
    038 * <p>
    039 * Instances of this class are always associated with an instance of {@link DefaultExpressionEngine}, from which the
    040 * current delimiters are obtained. So key creation and parsing is specific to this associated expression engine.
    041 * </p>
    042 *
    043 * @since 1.3
    044 */
    045public class DefaultConfigurationKey {
    046    /**
    047     * A specialized iterator class for tokenizing a configuration key. This class implements the normal iterator interface.
    048     * In addition it provides some specific methods for configuration keys.
    049     */
    050    public class KeyIterator implements Iterator<Object>, Cloneable {
    051        /** Stores the current key name. */
    052        private String current;
    053
    054        /** Stores the start index of the actual token. */
    055        private int startIndex;
    056
    057        /** Stores the end index of the actual token. */
    058        private int endIndex;
    059
    060        /** Stores the index of the actual property if there is one. */
    061        private int indexValue;
    062
    063        /** Stores a flag if the actual property has an index. */
    064        private boolean hasIndex;
    065
    066        /** Stores a flag if the actual property is an attribute. */
    067        private boolean attribute;
    068
    069        /**
    070         * Helper method for checking if the passed key is an attribute. If this is the case, the internal fields will be set.
    071         *
    072         * @param key the key to be checked
    073         * @return a flag if the key is an attribute
    074         */
    075        private boolean checkAttribute(final String key) {
    076            if (isAttributeKey(key)) {
    077                current = removeAttributeMarkers(key);
    078                return true;
    079            }
    080            return false;
    081        }
    082
    083        /**
    084         * Helper method for checking if the passed key contains an index. If this is the case, internal fields will be set.
    085         *
    086         * @param key the key to be checked
    087         * @return a flag if an index is defined
    088         */
    089        private boolean checkIndex(final String key) {
    090            boolean result = false;
    091
    092            try {
    093                final int idx = key.lastIndexOf(getSymbols().getIndexStart());
    094                if (idx > 0) {
    095                    final int endidx = key.indexOf(getSymbols().getIndexEnd(), idx);
    096
    097                    if (endidx > idx + 1) {
    098                        indexValue = Integer.parseInt(key.substring(idx + 1, endidx));
    099                        current = key.substring(0, idx);
    100                        result = true;
    101                    }
    102                }
    103            } catch (final NumberFormatException nfe) {
    104                result = false;
    105            }
    106
    107            return result;
    108        }
    109
    110        /**
    111         * Creates a clone of this object.
    112         *
    113         * @return a clone of this object
    114         */
    115        @Override
    116        public Object clone() {
    117            try {
    118                return super.clone();
    119            } catch (final CloneNotSupportedException cex) {
    120                // should not happen
    121                return null;
    122            }
    123        }
    124
    125        /**
    126         * Returns the current key of the iteration (without skipping to the next element). This is the same key the previous
    127         * {@code next()} call had returned. (Short form of {@code currentKey(false)}.
    128         *
    129         * @return the current key
    130         */
    131        public String currentKey() {
    132            return currentKey(false);
    133        }
    134
    135        /**
    136         * Returns the current key of the iteration (without skipping to the next element). The boolean parameter indicates
    137         * wheter a decorated key should be returned. This affects only attribute keys: if the parameter is <b>false</b>, the
    138         * attribute markers are stripped from the key; if it is <b>true</b>, they remain.
    139         *
    140         * @param decorated a flag if the decorated key is to be returned
    141         * @return the current key
    142         */
    143        public String currentKey(final boolean decorated) {
    144            return decorated && !isPropertyKey() ? constructAttributeKey(current) : current;
    145        }
    146
    147        /**
    148         * Checks if a delimiter at the specified position is escaped. If this is the case, the next valid search position will
    149         * be returned. Otherwise the return value is -1.
    150         *
    151         * @param key the key to check
    152         * @param pos the position where a delimiter was found
    153         * @return information about escaped delimiters
    154         */
    155        private int escapedPosition(final String key, final int pos) {
    156            if (getSymbols().getEscapedDelimiter() == null) {
    157                // nothing to escape
    158                return -1;
    159            }
    160            final int escapeOffset = escapeOffset();
    161            if (escapeOffset < 0 || escapeOffset > pos) {
    162                // No escaping possible at this position
    163                return -1;
    164            }
    165
    166            final int escapePos = key.indexOf(getSymbols().getEscapedDelimiter(), pos - escapeOffset);
    167            if (escapePos <= pos && escapePos >= 0) {
    168                // The found delimiter is escaped. Next valid search position
    169                // is behind the escaped delimiter.
    170                return escapePos + getSymbols().getEscapedDelimiter().length();
    171            }
    172            return -1;
    173        }
    174
    175        /**
    176         * Determines the relative offset of an escaped delimiter in relation to a delimiter. Depending on the used delimiter
    177         * and escaped delimiter tokens the position where to search for an escaped delimiter is different. If, for instance,
    178         * the dot character (&quot;.&quot;) is used as delimiter, and a doubled dot (&quot;..&quot;) as escaped delimiter, the
    179         * escaped delimiter starts at the same position as the delimiter. If the token &quot;\.&quot; was used, it would start
    180         * one character before the delimiter because the delimiter character &quot;.&quot; is the second character in the
    181         * escaped delimiter string. This relation will be determined by this method. For this to work the delimiter string must
    182         * be contained in the escaped delimiter string.
    183         *
    184         * @return the relative offset of the escaped delimiter in relation to a delimiter
    185         */
    186        private int escapeOffset() {
    187            return getSymbols().getEscapedDelimiter().indexOf(getSymbols().getPropertyDelimiter());
    188        }
    189
    190        /**
    191         * Helper method for determining the next indices.
    192         *
    193         * @return the next key part
    194         */
    195        private String findNextIndices() {
    196            startIndex = endIndex;
    197            // skip empty names
    198            while (startIndex < length() && hasLeadingDelimiter(keyBuffer.substring(startIndex))) {
    199                startIndex += getSymbols().getPropertyDelimiter().length();
    200            }
    201
    202            // Key ends with a delimiter?
    203            if (startIndex >= length()) {
    204                endIndex = length();
    205                startIndex = endIndex - 1;
    206                return keyBuffer.substring(startIndex, endIndex);
    207            }
    208            return nextKeyPart();
    209        }
    210
    211        /**
    212         * Gets the index value of the current key. If the current key does not have an index, return value is -1. This
    213         * method can be called after {@code next()}.
    214         *
    215         * @return the index value of the current key
    216         */
    217        public int getIndex() {
    218            return indexValue;
    219        }
    220
    221        /**
    222         * Returns a flag if the current key has an associated index. This method can be called after {@code next()}.
    223         *
    224         * @return a flag if the current key has an index
    225         */
    226        public boolean hasIndex() {
    227            return hasIndex;
    228        }
    229
    230        /**
    231         * Checks if there is a next element.
    232         *
    233         * @return a flag if there is a next element
    234         */
    235        @Override
    236        public boolean hasNext() {
    237            return endIndex < keyBuffer.length();
    238        }
    239
    240        /**
    241         * Returns a flag if the current key is an attribute. This method can be called after {@code next()}.
    242         *
    243         * @return a flag if the current key is an attribute
    244         */
    245        public boolean isAttribute() {
    246            // if attribute emulation mode is active, the last part of a key is
    247            // always an attribute key, too
    248            return attribute || isAttributeEmulatingMode() && !hasNext();
    249        }
    250
    251        /**
    252         * Returns a flag whether attributes are marked the same way as normal property keys. We call this the &quot;attribute
    253         * emulating mode&quot;. When navigating through node hierarchies it might be convenient to treat attributes the same
    254         * way than other child nodes, so an expression engine supports to set the attribute markers to the same value than the
    255         * property delimiter. If this is the case, some special checks have to be performed.
    256         *
    257         * @return a flag if attributes and normal property keys are treated the same way
    258         */
    259        private boolean isAttributeEmulatingMode() {
    260            return getSymbols().getAttributeEnd() == null && StringUtils.equals(getSymbols().getPropertyDelimiter(), getSymbols().getAttributeStart());
    261        }
    262
    263        /**
    264         * Returns a flag whether the current key refers to a property (i.e. is no special attribute key). Usually this method
    265         * will return the opposite of {@code isAttribute()}, but if the delimiters for normal properties and attributes are set
    266         * to the same string, it is possible that both methods return <b>true</b>.
    267         *
    268         * @return a flag if the current key is a property key
    269         * @see #isAttribute()
    270         */
    271        public boolean isPropertyKey() {
    272            return !attribute;
    273        }
    274
    275        /**
    276         * Returns the next object in the iteration.
    277         *
    278         * @return the next object
    279         */
    280        @Override
    281        public Object next() {
    282            return nextKey();
    283        }
    284
    285        /**
    286         * Searches the next unescaped delimiter from the given position.
    287         *
    288         * @param key the key
    289         * @param pos the start position
    290         * @param endPos the end position
    291         * @return the position of the next delimiter or -1 if there is none
    292         */
    293        private int nextDelimiterPos(final String key, final int pos, final int endPos) {
    294            int delimiterPos = pos;
    295            boolean found = false;
    296
    297            do {
    298                delimiterPos = key.indexOf(getSymbols().getPropertyDelimiter(), delimiterPos);
    299                if (delimiterPos < 0 || delimiterPos >= endPos) {
    300                    return -1;
    301                }
    302                final int escapePos = escapedPosition(key, delimiterPos);
    303                if (escapePos < 0) {
    304                    found = true;
    305                } else {
    306                    delimiterPos = escapePos;
    307                }
    308            } while (!found);
    309
    310            return delimiterPos;
    311        }
    312
    313        /**
    314         * Returns the next key part of this configuration key. This is a short form of {@code nextKey(false)}.
    315         *
    316         * @return the next key part
    317         */
    318        public String nextKey() {
    319            return nextKey(false);
    320        }
    321
    322        /**
    323         * Returns the next key part of this configuration key. The boolean parameter indicates wheter a decorated key should be
    324         * returned. This affects only attribute keys: if the parameter is <b>false</b>, the attribute markers are stripped from
    325         * the key; if it is <b>true</b>, they remain.
    326         *
    327         * @param decorated a flag if the decorated key is to be returned
    328         * @return the next key part
    329         */
    330        public String nextKey(final boolean decorated) {
    331            if (!hasNext()) {
    332                throw new NoSuchElementException("No more key parts!");
    333            }
    334
    335            hasIndex = false;
    336            indexValue = -1;
    337            final String key = findNextIndices();
    338
    339            current = key;
    340            hasIndex = checkIndex(key);
    341            attribute = checkAttribute(current);
    342
    343            return currentKey(decorated);
    344        }
    345
    346        /**
    347         * Helper method for extracting the next key part. Takes escaping of delimiter characters into account.
    348         *
    349         * @return the next key part
    350         */
    351        private String nextKeyPart() {
    352            int attrIdx = keyBuffer.toString().indexOf(getSymbols().getAttributeStart(), startIndex);
    353            if (attrIdx < 0 || attrIdx == startIndex) {
    354                attrIdx = length();
    355            }
    356
    357            int delIdx = nextDelimiterPos(keyBuffer.toString(), startIndex, attrIdx);
    358            if (delIdx < 0) {
    359                delIdx = attrIdx;
    360            }
    361
    362            endIndex = Math.min(attrIdx, delIdx);
    363            return unescapeDelimiters(keyBuffer.substring(startIndex, endIndex));
    364        }
    365
    366        /**
    367         * Removes the current object in the iteration. This method is not supported by this iterator type, so an exception is
    368         * thrown.
    369         */
    370        @Override
    371        public void remove() {
    372            throw new UnsupportedOperationException("Remove not supported!");
    373        }
    374    }
    375
    376    /** Constant for the initial StringBuffer size. */
    377    private static final int INITIAL_SIZE = 32;
    378
    379    /**
    380     * Helper method for comparing two key parts.
    381     *
    382     * @param it1 the iterator with the first part
    383     * @param it2 the iterator with the second part
    384     * @return a flag if both parts are equal
    385     */
    386    private static boolean partsEqual(final KeyIterator it1, final KeyIterator it2) {
    387        return it1.nextKey().equals(it2.nextKey()) && it1.getIndex() == it2.getIndex() && it1.isAttribute() == it2.isAttribute();
    388    }
    389
    390    /** Stores a reference to the associated expression engine. */
    391    private final DefaultExpressionEngine expressionEngine;
    392
    393    /** Holds a buffer with the so far created key. */
    394    private final StringBuilder keyBuffer;
    395
    396    /**
    397     * Creates a new instance of {@code DefaultConfigurationKey} and sets the associated expression engine.
    398     *
    399     * @param engine the expression engine (must not be <b>null</b>)
    400     * @throws IllegalArgumentException if the expression engine is <b>null</b>
    401     */
    402    public DefaultConfigurationKey(final DefaultExpressionEngine engine) {
    403        this(engine, null);
    404    }
    405
    406    /**
    407     * Creates a new instance of {@code DefaultConfigurationKey} and sets the associated expression engine and an initial
    408     * key.
    409     *
    410     * @param engine the expression engine (must not be <b>null</b>)
    411     * @param key the key to be wrapped
    412     * @throws IllegalArgumentException if the expression engine is <b>null</b>
    413     */
    414    public DefaultConfigurationKey(final DefaultExpressionEngine engine, final String key) {
    415        if (engine == null) {
    416            throw new IllegalArgumentException("Expression engine must not be null!");
    417        }
    418        expressionEngine = engine;
    419        if (key != null) {
    420            keyBuffer = new StringBuilder(trim(key));
    421        } else {
    422            keyBuffer = new StringBuilder(INITIAL_SIZE);
    423        }
    424    }
    425
    426    /**
    427     * Appends the name of a property to this key. If necessary, a property delimiter will be added. Property delimiters in
    428     * the given string will not be escaped.
    429     *
    430     * @param property the name of the property to be added
    431     * @return a reference to this object
    432     */
    433    public DefaultConfigurationKey append(final String property) {
    434        return append(property, false);
    435    }
    436
    437    /**
    438     * Appends the name of a property to this key. If necessary, a property delimiter will be added. If the boolean argument
    439     * is set to <b>true</b>, property delimiters contained in the property name will be escaped.
    440     *
    441     * @param property the name of the property to be added
    442     * @param escape a flag if property delimiters in the passed in property name should be escaped
    443     * @return a reference to this object
    444     */
    445    public DefaultConfigurationKey append(final String property, final boolean escape) {
    446        String key;
    447        if (escape && property != null) {
    448            key = escapeDelimiters(property);
    449        } else {
    450            key = property;
    451        }
    452        key = trim(key);
    453
    454        if (keyBuffer.length() > 0 && !isAttributeKey(property) && !key.isEmpty()) {
    455            keyBuffer.append(getSymbols().getPropertyDelimiter());
    456        }
    457
    458        keyBuffer.append(key);
    459        return this;
    460    }
    461
    462    /**
    463     * Appends an attribute to this configuration key.
    464     *
    465     * @param attr the name of the attribute to be appended
    466     * @return a reference to this object
    467     */
    468    public DefaultConfigurationKey appendAttribute(final String attr) {
    469        keyBuffer.append(constructAttributeKey(attr));
    470        return this;
    471    }
    472
    473    /**
    474     * Appends an index to this configuration key.
    475     *
    476     * @param index the index to be appended
    477     * @return a reference to this object
    478     */
    479    public DefaultConfigurationKey appendIndex(final int index) {
    480        keyBuffer.append(getSymbols().getIndexStart());
    481        keyBuffer.append(index);
    482        keyBuffer.append(getSymbols().getIndexEnd());
    483        return this;
    484    }
    485
    486    /**
    487     * Extracts the name of the attribute from the given attribute key. This method removes the attribute markers - if any -
    488     * from the specified key.
    489     *
    490     * @param key the attribute key
    491     * @return the name of the corresponding attribute
    492     */
    493    public String attributeName(final String key) {
    494        return isAttributeKey(key) ? removeAttributeMarkers(key) : key;
    495    }
    496
    497    /**
    498     * Returns a configuration key object that is initialized with the part of the key that is common to this key and the
    499     * passed in key.
    500     *
    501     * @param other the other key
    502     * @return a key object with the common key part
    503     */
    504    public DefaultConfigurationKey commonKey(final DefaultConfigurationKey other) {
    505        if (other == null) {
    506            throw new IllegalArgumentException("Other key must no be null!");
    507        }
    508
    509        final DefaultConfigurationKey result = new DefaultConfigurationKey(getExpressionEngine());
    510        final KeyIterator it1 = iterator();
    511        final KeyIterator it2 = other.iterator();
    512
    513        while (it1.hasNext() && it2.hasNext() && partsEqual(it1, it2)) {
    514            if (it1.isAttribute()) {
    515                result.appendAttribute(it1.currentKey());
    516            } else {
    517                result.append(it1.currentKey());
    518                if (it1.hasIndex) {
    519                    result.appendIndex(it1.getIndex());
    520                }
    521            }
    522        }
    523
    524        return result;
    525    }
    526
    527    /**
    528     * Decorates the given key so that it represents an attribute. Adds special start and end markers. The passed in string
    529     * will be modified only if does not already represent an attribute.
    530     *
    531     * @param key the key to be decorated
    532     * @return the decorated attribute key
    533     */
    534    public String constructAttributeKey(final String key) {
    535        if (key == null) {
    536            return StringUtils.EMPTY;
    537        }
    538        if (isAttributeKey(key)) {
    539            return key;
    540        }
    541        final StringBuilder buf = new StringBuilder();
    542        buf.append(getSymbols().getAttributeStart()).append(key);
    543        if (getSymbols().getAttributeEnd() != null) {
    544            buf.append(getSymbols().getAttributeEnd());
    545        }
    546        return buf.toString();
    547    }
    548
    549    /**
    550     * Returns the &quot;difference key&quot; to a given key. This value is the part of the passed in key that differs from
    551     * this key. There is the following relation: {@code other = key.commonKey(other) + key.differenceKey(other)} for an
    552     * arbitrary configuration key {@code key}.
    553     *
    554     * @param other the key for which the difference is to be calculated
    555     * @return the difference key
    556     */
    557    public DefaultConfigurationKey differenceKey(final DefaultConfigurationKey other) {
    558        final DefaultConfigurationKey common = commonKey(other);
    559        final DefaultConfigurationKey result = new DefaultConfigurationKey(getExpressionEngine());
    560
    561        if (common.length() < other.length()) {
    562            final String k = other.toString().substring(common.length());
    563            // skip trailing delimiters
    564            int i = 0;
    565            while (i < k.length() && String.valueOf(k.charAt(i)).equals(getSymbols().getPropertyDelimiter())) {
    566                i++;
    567            }
    568
    569            if (i < k.length()) {
    570                result.append(k.substring(i));
    571            }
    572        }
    573
    574        return result;
    575    }
    576
    577    /**
    578     * Checks if two {@code ConfigurationKey} objects are equal. Two instances of this class are considered equal if they
    579     * have the same content (i.e. their internal string representation is equal). The expression engine property is not
    580     * taken into account.
    581     *
    582     * @param obj the object to compare
    583     * @return a flag if both objects are equal
    584     */
    585    @Override
    586    public boolean equals(final Object obj) {
    587        if (this == obj) {
    588            return true;
    589        }
    590        if (!(obj instanceof DefaultConfigurationKey)) {
    591            return false;
    592        }
    593
    594        final DefaultConfigurationKey c = (DefaultConfigurationKey) obj;
    595        return keyBuffer.toString().equals(c.toString());
    596    }
    597
    598    /**
    599     * Escapes the delimiters in the specified string.
    600     *
    601     * @param key the key to be escaped
    602     * @return the escaped key
    603     */
    604    private String escapeDelimiters(final String key) {
    605        return getSymbols().getEscapedDelimiter() == null || !key.contains(getSymbols().getPropertyDelimiter()) ? key
    606            : StringUtils.replace(key, getSymbols().getPropertyDelimiter(), getSymbols().getEscapedDelimiter());
    607    }
    608
    609    /**
    610     * Gets the associated default expression engine.
    611     *
    612     * @return the associated expression engine
    613     */
    614    public DefaultExpressionEngine getExpressionEngine() {
    615        return expressionEngine;
    616    }
    617
    618    /**
    619     * Gets the symbols object from the associated expression engine.
    620     *
    621     * @return the {@code DefaultExpressionEngineSymbols}
    622     */
    623    private DefaultExpressionEngineSymbols getSymbols() {
    624        return getExpressionEngine().getSymbols();
    625    }
    626
    627    /**
    628     * Returns the hash code for this object.
    629     *
    630     * @return the hash code
    631     */
    632    @Override
    633    public int hashCode() {
    634        return String.valueOf(keyBuffer).hashCode();
    635    }
    636
    637    /**
    638     * Helper method that checks if the specified key starts with a property delimiter.
    639     *
    640     * @param key the key to check
    641     * @return a flag if there is a leading delimiter
    642     */
    643    private boolean hasLeadingDelimiter(final String key) {
    644        return key.startsWith(getSymbols().getPropertyDelimiter())
    645            && (getSymbols().getEscapedDelimiter() == null || !key.startsWith(getSymbols().getEscapedDelimiter()));
    646    }
    647
    648    /**
    649     * Helper method that checks if the specified key ends with a property delimiter.
    650     *
    651     * @param key the key to check
    652     * @return a flag if there is a trailing delimiter
    653     */
    654    private boolean hasTrailingDelimiter(final String key) {
    655        return key.endsWith(getSymbols().getPropertyDelimiter())
    656            && (getSymbols().getEscapedDelimiter() == null || !key.endsWith(getSymbols().getEscapedDelimiter()));
    657    }
    658
    659    /**
    660     * Tests if the specified key represents an attribute according to the current expression engine.
    661     *
    662     * @param key the key to be checked
    663     * @return <b>true</b> if this is an attribute key, <b>false</b> otherwise
    664     */
    665    public boolean isAttributeKey(final String key) {
    666        if (key == null) {
    667            return false;
    668        }
    669
    670        return key.startsWith(getSymbols().getAttributeStart()) && (getSymbols().getAttributeEnd() == null || key.endsWith(getSymbols().getAttributeEnd()));
    671    }
    672
    673    /**
    674     * Returns an iterator for iterating over the single components of this configuration key.
    675     *
    676     * @return an iterator for this key
    677     */
    678    public KeyIterator iterator() {
    679        return new KeyIterator();
    680    }
    681
    682    /**
    683     * Returns the actual length of this configuration key.
    684     *
    685     * @return the length of this key
    686     */
    687    public int length() {
    688        return keyBuffer.length();
    689    }
    690
    691    /**
    692     * Helper method for removing attribute markers from a key.
    693     *
    694     * @param key the key
    695     * @return the key with removed attribute markers
    696     */
    697    private String removeAttributeMarkers(final String key) {
    698        return key.substring(getSymbols().getAttributeStart().length(),
    699            key.length() - (getSymbols().getAttributeEnd() != null ? getSymbols().getAttributeEnd().length() : 0));
    700    }
    701
    702    /**
    703     * Sets the new length of this configuration key. With this method it is possible to truncate the key, e.g. to return to
    704     * a state prior calling some {@code append()} methods. The semantic is the same as the {@code setLength()} method of
    705     * {@code StringBuilder}.
    706     *
    707     * @param len the new length of the key
    708     */
    709    public void setLength(final int len) {
    710        keyBuffer.setLength(len);
    711    }
    712
    713    /**
    714     * Returns a string representation of this object. This is the configuration key as a plain string.
    715     *
    716     * @return a string for this object
    717     */
    718    @Override
    719    public String toString() {
    720        return keyBuffer.toString();
    721    }
    722
    723    /**
    724     * Removes delimiters at the beginning and the end of the specified key.
    725     *
    726     * @param key the key
    727     * @return the key with removed property delimiters
    728     */
    729    public String trim(final String key) {
    730        return trimRight(trimLeft(key));
    731    }
    732
    733    /**
    734     * Removes leading property delimiters from the specified key.
    735     *
    736     * @param key the key
    737     * @return the key with removed leading property delimiters
    738     */
    739    public String trimLeft(final String key) {
    740        if (key == null) {
    741            return StringUtils.EMPTY;
    742        }
    743        String result = key;
    744        while (hasLeadingDelimiter(result)) {
    745            result = result.substring(getSymbols().getPropertyDelimiter().length());
    746        }
    747        return result;
    748    }
    749
    750    /**
    751     * Removes trailing property delimiters from the specified key.
    752     *
    753     * @param key the key
    754     * @return the key with removed trailing property delimiters
    755     */
    756    public String trimRight(final String key) {
    757        if (key == null) {
    758            return StringUtils.EMPTY;
    759        }
    760        String result = key;
    761        while (hasTrailingDelimiter(result)) {
    762            result = result.substring(0, result.length() - getSymbols().getPropertyDelimiter().length());
    763        }
    764        return result;
    765    }
    766
    767    /**
    768     * Unescapes the delimiters in the specified string.
    769     *
    770     * @param key the key to be unescaped
    771     * @return the unescaped key
    772     */
    773    private String unescapeDelimiters(final String key) {
    774        return getSymbols().getEscapedDelimiter() == null ? key
    775            : StringUtils.replace(key, getSymbols().getEscapedDelimiter(), getSymbols().getPropertyDelimiter());
    776    }
    777}