com.flexive.shared.XPathElement.java Source code

Java tutorial

Introduction

Here is the source code for com.flexive.shared.XPathElement.java

Source

/***************************************************************
 *  This file is part of the [fleXive](R) framework.
 *
 *  Copyright (c) 1999-2014
 *  UCS - unique computing solutions gmbh (http://www.ucs.at)
 *  All rights reserved
 *
 *  The [fleXive](R) project is free software; you can redistribute
 *  it and/or modify it under the terms of the GNU Lesser General Public
 *  License version 2.1 or higher as published by the Free Software Foundation.
 *
 *  The GNU Lesser General Public License can be found at
 *  http://www.gnu.org/licenses/lgpl.html.
 *  A copy is found in the textfile LGPL.txt and important notices to the
 *  license from the author are found in LICENSE.txt distributed with
 *  these libraries.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  For further information about UCS - unique computing solutions gmbh,
 *  please see the company website: http://www.ucs.at
 *
 *  For further information about [fleXive](R), please see the
 *  project website: http://www.flexive.org
 *
 *
 *  This copyright notice MUST APPEAR in all copies of the file!
 ***************************************************************/
package com.flexive.shared;

import com.flexive.shared.content.FxPK;
import com.flexive.shared.exceptions.FxInvalidParameterException;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.apache.commons.lang.StringUtils;

import java.io.Serializable;
import java.lang.ref.WeakReference;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * A single XPath element (alias and multiplicity).
 *
 * @author Markus Plesser (markus.plesser@flexive.com), UCS - unique computing solutions gmbh (http://www.ucs.at)
 */
public class XPathElement implements Serializable {
    private static final long serialVersionUID = 2037392183607142045L;
    private static final String PK = "@(pk|PK)=(NEW|\\d*\\.(LIVE|MAX|\\d*))";
    /**
     * First element must start with a "/",
     * an XPath element must start with a letter or underscore followed by an optional letter/number/underscore combination
     * and may end with an optional multiplicity like [x] where x is a number
     */
    //    private static final Pattern XPathPattern = Pattern.compile("([A-Z_][A-Z_0-9 _]*(\\[" + PK + "\\])?)?(/[A-Z][A-Z_0-9]*(\\[[0-9]+\\])?)+");
    private static final Pattern PKPattern = Pattern.compile(PK);
    private static final Pattern doubleSlashPattern = Pattern.compile("/{2,}");
    private static final List<XPathElement> EMPTY = Collections.unmodifiableList(new ArrayList<XPathElement>(0));

    // lookup cache for read-only XPath element mappings (thread-local cache)
    private static final ThreadLocal<Map<String, ImmutableList<XPathElement>>> CACHED_SPLIT_PATHS = new ThreadLocal<Map<String, ImmutableList<XPathElement>>>() {
        @Override
        protected Map<String, ImmutableList<XPathElement>> initialValue() {
            return Maps.newHashMapWithExpectedSize(500);
        }
    };

    // shared canonical lookup cache
    private static final WeakHashMap<String, ImmutableList<XPathElement>> SHARED_SPLIT_PATHS = new WeakHashMap<String, ImmutableList<XPathElement>>(
            500);
    private static final WeakHashMap<XPathElement, WeakReference<XPathElement>> SHARED_ELEMENTS = new WeakHashMap<XPathElement, WeakReference<XPathElement>>(
            1000);

    private final String alias;
    private int index;
    private final boolean indexDefined;
    private final boolean immutable;

    /**
     * Ctor
     *
     * @param alias        alias to use
     * @param index        multiplicity to apply
     * @param indexDefined was the multiplicity explicitly defined?
     */
    public XPathElement(String alias, int index, boolean indexDefined) {
        this(alias, index, indexDefined, false);
    }

    public XPathElement(String alias, int index, boolean indexDefined, boolean immutable) {
        this.alias = alias;
        this.index = index;
        this.indexDefined = indexDefined;
        this.immutable = immutable;
    }

    /**
     * Getter for the alias
     *
     * @return alias
     */
    public String getAlias() {
        return alias;
    }

    /**
     * Getter for the multiplicity
     *
     * @return multiplicity
     */
    public int getIndex() {
        return index;
    }

    /**
     * Setter for the multiplicity
     *
     * @param index the multiplicity to apply
     */
    public void setIndex(int index) {
        if (immutable) {
            throw new IllegalStateException("This XPathElement may not be modified");
        }
        this.index = index;
    }

    /**
     * Was the multiplicity explicitly defined?
     *
     * @return multiplicity explicitly defined
     */
    public boolean isIndexDefined() {
        return indexDefined;
    }

    /**
     * get FQN of the alias
     *
     * @return FQ alias
     */
    @Override
    public String toString() {
        return alias + "[" + index + "]";
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof XPathElement)) {
            return false;
        }
        final XPathElement other = (XPathElement) obj;
        return equalData(other) && indexDefined == other.indexDefined;
    }

    /**
     * Compare alias and index to another XPath element.
     *
     * @param other    the other XPath element
     * @return         true when alias and index are equal (ignoring whether the index was defined explicitly)
     */
    public boolean equalData(XPathElement other) {
        return alias.equals(other.alias) && index == other.index;
    }

    @Override
    public int hashCode() {
        int result = alias.hashCode();
        result = 31 * result + index;
        result = 31 * result + (indexDefined ? 1 : 0);
        return result;
    }

    /**
     * Split an XPath into its elements
     *
     * <p>For performance reasons, this method expects that the xPath is already in upper case form.</p>
     *
     * @param XPath the XPath
     * @return XPathElement array
     */
    public static List<XPathElement> split(final String XPath) {
        if (StringUtils.isEmpty(XPath))
            return EMPTY;

        // get thread-local cache instance
        final Map<String, ImmutableList<XPathElement>> cache = CACHED_SPLIT_PATHS.get();

        ImmutableList<XPathElement> elements = cache.get(XPath);
        if (elements == null) {
            synchronized (XPathElement.class) {
                elements = SHARED_SPLIT_PATHS.get(XPath);

                if (elements == null) {
                    if (!isValidXPath(XPath)) {
                        throw new FxInvalidParameterException("XPATH", "ex.xpath.invalid", XPath)
                                .asRuntimeException();
                    }
                    final int length = XPath.length();
                    List<XPathElement> values = new ArrayList<XPathElement>(10);
                    int pos = XPath.indexOf('/') + 1; //skip first '/' to avoid empty entries
                    if (pos == 0) {
                        throw new FxInvalidParameterException("XPATH", "ex.xpath.invalid", XPath)
                                .asRuntimeException();
                    }
                    while (pos < length) {
                        int nextPos = XPath.indexOf('/', pos);
                        if (nextPos == -1) {
                            nextPos = length;
                        }
                        values.add(toElementImmutable(XPath, pos, nextPos));
                        pos = nextPos + 1;
                    }

                    elements = ImmutableList.copyOf(values);

                    if (!SHARED_SPLIT_PATHS.containsKey(XPath)) {
                        SHARED_SPLIT_PATHS.put(XPath, elements);
                    }
                }
            }

            // cache in current thread
            cache.put(XPath, elements);
        }

        return elements;
    }

    /**
     * Split an XPath into its elements and return new XPathElements (for updating the index).
     * Usually the faster {@link #split(String)} method should be used, which caches access to frequently
     * used XPaths, but returns immutable instances.
     *
     * <p>For performance reasons, this method expects that the xPath is already in upper case form.</p>
     *
     * @param XPath the XPath
     * @return XPathElement array
     * @since 3.2.1
     */
    public static List<XPathElement> splitNew(String XPath) {
        final List<XPathElement> elements = split(XPath);
        final List<XPathElement> result = Lists.newArrayListWithCapacity(elements.size());
        for (XPathElement element : elements) {
            result.add(new XPathElement(element.getAlias(), element.getIndex(), element.isIndexDefined()));
        }
        return result;
    }

    /**
     * Get the last (rightmost) element of an XPath.
     *
     * <p>For performance reasons, this method expects that the xPath is already in upper case form.</p>
     *
     * @param XPath the XPath
     * @return last (rightmost) element of an XPath
     */
    public static XPathElement lastElement(String XPath) {
        if (StringUtils.isEmpty(XPath) || !isValidXPath(XPath))
            throw new FxInvalidParameterException("XPATH", "ex.xpath.invalid", XPath).asRuntimeException();
        return toElement(XPath, XPath.substring(XPath.lastIndexOf('/') + 1));
    }

    /**
     * Convert an alias of an XPath to an element.
     *
     * <p>For performance reasons, this method expects that the xPath is already in upper case form.</p>
     *
     * @param XPath full XPath, only used if exception is thrown
     * @param alias alias to convert to an XPathElement
     * @return XPathElement
     */
    public static XPathElement toElement(String XPath, String alias) {
        if (StringUtils.isEmpty(alias) || alias.indexOf('/') >= 0)
            throw new FxInvalidParameterException("XPATH", "ex.xpath.element.invalid", alias, XPath)
                    .asRuntimeException();
        try {
            final int indexPos = alias.indexOf('[');
            if (indexPos == -1) {
                return new XPathElement(alias, 1, false);
            }
            final int index = Integer.parseInt(alias.substring(indexPos + 1, alias.length() - 1));
            return new XPathElement(alias.substring(0, indexPos), index, true);
        } catch (Exception e) {
            throw new FxInvalidParameterException("XPATH", "ex.xpath.element.invalid", alias, XPath)
                    .asRuntimeException();
        }
    }

    private static XPathElement toElementImmutable(String xpath, int elemStart, int elemEnd) {
        final int indexPos = xpath.indexOf('[', elemStart);
        final XPathElement elem;
        if (indexPos == -1 || indexPos > elemEnd) {
            elem = new XPathElement(xpath.substring(elemStart, elemEnd), 1, false, true);
        } else {
            final int index = Integer.parseInt(xpath.substring(indexPos + 1, elemEnd - 1));
            elem = new XPathElement(xpath.substring(elemStart, indexPos), index, true);
        }
        // use canonical instances (xpath elements of nested types contain a lot of duplication)
        final WeakReference<XPathElement> ref = SHARED_ELEMENTS.get(elem);
        XPathElement cachedElem = ref != null ? ref.get() : null;
        if (ref == null || cachedElem == null) {
            SHARED_ELEMENTS.put(elem, new WeakReference<XPathElement>(elem));
            cachedElem = elem;
        }
        return cachedElem;
    }

    /**
     * Check if this XPath is valid.
     *
     * <p>For performance reasons, this method expects that the xPath is already in upper case form.</p>
     * 
     * @param XPath the XPath
     * @return valid or not
     */
    public static boolean isValidXPath(String XPath) {
        try {
            //            slow version using reqular expressions:
            //            return "/".equals(XPath) || !StringUtils.isEmpty(XPath) && XPathPattern.matcher(XPath).matches();
            if (XPath == null)
                return false;
            if (XPath.length() == 1 && XPath.charAt(0) == '/')
                return true;
            final char[] xp = XPath.toCharArray();
            int pos = -1;
            final int length = xp.length;
            if (xp[0] != '/') {
                //check for correct type
                boolean inBr = false; //in bracket
                boolean hadBr = false; //already had a bracket
                while (++pos < length) {
                    final char ch = xp[pos];
                    if (ch == '/')
                        break;//end of type
                    if (ch >= '0' && ch <= '9' && pos > 1 && !inBr) //first letter must not be a number
                        continue;
                    if (((ch >= 'A' && ch <= 'Z') || ch == '_' || ch == ' ') && !inBr) //only A-Z, underscore and space allowed in name
                        continue;
                    if (ch == '[') {
                        if (inBr || hadBr || xp[pos + 1] != '@') //in type bracket has to be followed by "@"
                            return false;
                        inBr = true;
                        hadBr = true;
                        continue;
                    }
                    if (ch == '@') {
                        switch (xp[++pos]) {
                        case 'p':
                        case 'P':
                            break;
                        default:
                            return false;
                        }
                        switch (xp[++pos]) {
                        case 'k':
                        case 'K':
                            break;
                        default:
                            return false;
                        }
                        if (xp[++pos] != '=')
                            return false;
                        //@pk=NEW
                        if (xp[pos + 1] == 'N' && xp[pos + 2] == 'E' && xp[pos + 3] == 'W' && xp[pos + 4] == ']') {
                            pos += 3;
                            continue;
                        }
                        boolean hasNum = false;
                        //@pk=<number>.
                        while (xp[++pos] >= '0' && xp[pos] <= '9') {
                            hasNum = true;
                        }
                        if (!hasNum)
                            return false;
                        if (xp[pos] != '.')
                            return false;
                        if (xp[pos + 1] >= '0' && xp[pos + 1] <= '9') {
                            //@pk=<number>.<number>
                            while (xp[++pos] >= '0' && xp[pos] <= '9') {
                            }
                            --pos; //one back as we reached ']
                            continue;
                        }
                        if (xp[pos + 1] == 'L' && xp[pos + 2] == 'I' && xp[pos + 3] == 'V' && xp[pos + 4] == 'E') {
                            pos += 4;
                            continue;
                        }
                        if (xp[pos + 1] == 'M' && xp[pos + 2] == 'A' && xp[pos + 3] == 'X') {
                            pos += 3;
                            continue;
                        }
                        return false;
                    }
                    if (ch == ']') {
                        if (!inBr)
                            return false;
                        inBr = false;
                        continue;
                    }
                    return false;
                }
                if (inBr)
                    return false;
                pos--;
            } else //end type check
                pos = -1;
            if ((pos + 1) == length) //empty or name only is not valid
                return false;
            while (++pos < length) {
                if (xp[pos] == '/') { //element start
                    if (pos == length)
                        return false; //may not end with '/'
                    if (!(xp[pos + 1] >= 'A' && xp[pos + 1] <= 'Z'))
                        return false; //element must start with A-Z
                    pos++;
                    char ch = xp[pos];
                    while ((ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_') {
                        if ((pos + 1) == length)
                            return true;
                        pos++;
                        ch = xp[pos];
                    }
                    if (pos == length)
                        return true;
                    if (ch == '[') { //index is optional and may only exist here
                        boolean hasNum = false;
                        //@pk=<number>.
                        while (xp[++pos] >= '0' && xp[pos] <= '9') {
                            hasNum = true;
                        }
                        if (!hasNum)
                            return false;
                        if (xp[pos] != ']')
                            return false; //index has to end with '['
                    } else
                        pos--;
                } else
                    return false; //expected an element start
            }
            //element check, allowed is only [A-Z] as first letter followed by [A-Z0-9] and an optional index
            return true;
        } catch (ArrayIndexOutOfBoundsException e) {
            return false;
        }
    }

    /**
     * Get the XPath of an array of XPathElements with multiplicities
     *
     * @param xpe list containing XPathElement
     * @return XPath
     */
    public static String toXPath(List<XPathElement> xpe) {
        if (xpe == null || xpe.size() == 0)
            return "/";
        StringBuilder XPath = new StringBuilder(100);
        for (XPathElement xp : xpe) {
            XPath.append('/').append(xp.getAlias()).append('[').append(xp.getIndex()).append(']');
        }
        return XPath.toString();
    }

    /**
     * Get the XPath of an array of XPathElements without multiplicities
     *
     * @param xpe list containing XPathElement
     * @return XPath
     */
    public static String toXPathNoMult(List<XPathElement> xpe) {
        if (xpe == null || xpe.size() == 0)
            return "/";
        StringBuilder XPath = new StringBuilder(100);
        for (XPathElement xp : xpe) {
            XPath.append('/').append(xp.getAlias());
        }
        return XPath.toString();
    }

    /**
     * Get the given XPath with full multiplicity information
     *
     * @param XPath XPath
     * @return XPath with full multiplicity information
     */
    public static String toXPathMult(String XPath) {
        if (StringUtils.isEmpty(XPath) || "/".equals(XPath))
            return "/";

        int start = 0;
        final int xpLength = XPath.length();
        StringBuilder xpc = new StringBuilder(xpLength + 20);
        final int typeSepIdx = XPath.indexOf('/');
        if (typeSepIdx > 0) {
            //we have a full qualified XPath with type name that needs to be stripped temporarily
            for (int i = 0; i < typeSepIdx; i++) {
                xpc.append(xpCharToUpperCase(XPath.charAt(i)));
            }
            start = typeSepIdx;
        }
        for (int i = start + 1; i < xpLength; i++) { // skip first '/'
            xpc.append('/');
            char ch;
            boolean hasIndex = false;
            while (i < xpLength && (ch = XPath.charAt(i)) != '/') {
                xpc.append(xpCharToUpperCase(ch));
                if (ch == '[') {
                    hasIndex = true;
                }
                i++;
            }
            if (!hasIndex) {
                xpc.append("[1]");
            }
        }
        return xpc.toString();
    }

    /**
     * Get the given XPath with full multiplicity information.
     * Please note that no checks are performed and that the XPath is assumed to be uppercase and valid
     *
     * @param XPath   valid XPath without type information and without indices
     * @param indices indices to apply (comma separated)
     * @return XPath with full multiplicity information
     */
    public static String toXPathMult(String XPath, String indices) {
        if (XPath == null || "/".equals(XPath) || XPath.length() == 0)
            return "/";
        if (XPath.charAt(0) != '/')
            XPath = XPath.substring(XPath.indexOf('/'));
        String[] ind = indices.split(",");
        StringBuilder xp = new StringBuilder(XPath.length() + ind.length * 3);
        int curr = -1;
        try {
            for (char c : XPath.toCharArray()) {
                if (c == '/') {
                    if (curr >= 0)
                        xp.append('[').append(ind[curr]).append(']');
                    curr++;
                }
                xp.append(c);
            }
            xp.append('[').append(ind[curr]).append(']');
        } catch (ArrayIndexOutOfBoundsException e) {
            throw new FxInvalidParameterException("XPATH", "ex.xpath.invalid", XPath).asRuntimeException();
        }
        return xp.toString();
    }

    /**
     * Get the given XPath with no indices
     *
     * @param XPath XPath with indices
     * @return XPath with indices stripped, in upper case
     */
    public static String toXPathNoMult(String XPath) {
        if (StringUtils.isEmpty(XPath) || "/".equals(XPath))
            return "/";
        XPath = xpToUpperCase(XPath);
        if (!isValidXPath(XPath))
            throw new FxInvalidParameterException("XPATH", "ex.xpath.invalid", XPath).asRuntimeException();
        if (XPath.indexOf('[') == -1) {
            // fast path for the case where we don't have to do anything because the XPath does not contain
            // a type parameter (PK) or multiplicities
            return XPath;
        }
        String type = null;
        final int firstSep = XPath.indexOf('/');
        if (XPath.charAt(0) != '/' && firstSep > 0) {
            //we have a full qualified XPath with type name that needs to be stripped temporarily
            type = XPath.substring(0, firstSep);
            final int typeParamSep = type.indexOf('[');
            if (typeParamSep > 0)
                type = type.substring(0, typeParamSep);
            XPath = XPath.substring(firstSep);
        }
        final String[] xp = XPath.substring(1).split("\\/"); //skip first '/' to avoid empty entries
        final StringBuilder xpc = new StringBuilder(XPath.length() + 10);
        for (String xpcurr : xp) {
            xpc.append('/');
            final int multSep = xpcurr.indexOf('[');
            if (multSep > 0)
                xpc.append(xpcurr.substring(0, multSep));
            else
                xpc.append(xpcurr);
        }
        if (type != null)
            return type + xpc.toString();
        return xpc.toString();
    }

    /**
     * Get the FQ indices of an XPath as an int array.
     * This method is optimized for performance and does not check for validity!
     *
     * @param XPath the xpath to examine
     * @return FQ indices of an XPath as an int array
     */
    public static int[] getIndices(String XPath) {
        int[] mult = new int[10];
        byte curr = 0;
        int lastOpen = -1;
        char[] c = XPath.toCharArray();
        for (int i = 0; i < c.length; i++) {
            switch (c[i]) {
            case '/':
                if (i == 0)
                    break;
                if (c[i - 1] == ']')
                    break;
                mult[curr++] = 1;
                if (mult.length <= curr) {
                    //resize mult
                    int[] tmp = new int[mult.length + 5];
                    System.arraycopy(mult, 0, tmp, 0, mult.length);
                    mult = tmp;
                }
                break;
            case '[':
                lastOpen = i;
                break;
            case ']':
                int m = 1;
                for (int w = (i - 1); w > lastOpen; w--) {
                    mult[curr] += (c[w] - (byte) '0') * m;
                    m *= 10;
                }
                curr++;
                if (mult.length <= curr) {
                    //resize mult
                    int[] tmp = new int[mult.length + 5];
                    System.arraycopy(mult, 0, tmp, 0, mult.length);
                    mult = tmp;
                }
            }
        }
        if (c[c.length - 1] != ']' && c[c.length - 1] != '/') {
            if (mult.length < curr) {
                //resize mult
                int[] tmp = new int[mult.length + 5];
                System.arraycopy(mult, 0, tmp, 0, mult.length);
                mult = tmp;
            }
            mult[curr++] = 1;
        }
        if (curr == 0)
            return new int[0];
        int start = (c[0] == '/') ? 0 : 1;
        int[] ret = new int[curr - start];
        System.arraycopy(mult, start, ret, 0, curr - start);
        return ret;
    }

    /**
     * Build an XPath from the given elements
     *
     * @param leadingSlash prepend a leading slash character?
     * @param elements     elements that build the XPath
     * @return XPath
     */
    public static String buildXPath(boolean leadingSlash, String... elements) {
        StringBuilder XPath = new StringBuilder(100);
        for (String element : elements) {
            if (element == null)
                continue;
            XPath.append('/');
            if (element.length() > 0 && element.charAt(0) == '/')
                XPath.append(element.substring(1));
            else
                XPath.append(element);
            if (XPath.length() > 1 && XPath.charAt(XPath.length() - 1) == '/')
                XPath.deleteCharAt(XPath.length() - 1);
        }
        if (XPath.length() > 0) {
            if (XPath.charAt(0) == '/' && !leadingSlash)
                XPath.deleteCharAt(0);
        } else if (XPath.length() == 0 && leadingSlash)
            XPath.append('/');
        return xpToUpperCase(doubleSlashPattern.matcher(XPath).replaceAll("/"));
    }

    /**
     * Strip leading types from an XPath if present, and return
     * the XPath in upper case.
     *
     * @param XPath the XPath
     * @return XPath without leading type
     */
    public static String stripType(String XPath) {
        final int pos = XPath.indexOf('/');
        return pos != -1 ? xpToUpperCase(pos == 0 ? XPath : XPath.substring(pos)) : "";
    }

    /**
     * Strip the last element (usually property) from an XPath
     *
     * @param XPath the XPath
     * @return XPath without the last element
     */
    public static String stripLastElement(String XPath) {
        if (XPath == null)
            throw new FxInvalidParameterException("XPATH", "ex.xpath.invalid", "null").asRuntimeException();
        if (XPath.lastIndexOf('/') == 0)
            return "/";
        XPath = xpToUpperCase(XPath);
        if (!isValidXPath(XPath))
            throw new FxInvalidParameterException("XPATH", "ex.xpath.invalid", XPath).asRuntimeException();
        return XPath.substring(0, XPath.lastIndexOf('/'));
    }

    /**
     * Extract the primary key stored in the given XPath. If no PK is contained
     * in the XPath, a FxRuntimeException is thrown.
     *
     * @param xPath the xpath
     * @return the primary key stored in the given XPath
     * @throws com.flexive.shared.exceptions.FxRuntimeException
     *          if the given xpath is invalid or contains no PK
     */
    public static FxPK getPK(String xPath) {
        FxSharedUtils.checkParameterEmpty(xPath, "xpath");
        final Matcher matcher = PKPattern.matcher(xPath);
        if (!matcher.find()) {
            //noinspection ThrowableInstanceNeverThrown
            throw new FxInvalidParameterException("xpath", "ex.xpath.element.noPk", xPath).asRuntimeException();
        }
        return FxPK.fromString(matcher.group(2));
    }

    /**
     * Change the index of an xpath (requires an xpath with all explicit indices set!)
     * This method is optimized for performance and does not check for validity!
     *
     * @param XPath the xpath with all indices set
     * @param pos   position of the element (0-based)
     * @param index the new index to apply
     * @return xpath with the new index at the requested position
     * @since 3.1.5
     */
    public static String changeIndex(String XPath, int pos, int index) {
        byte curr = 0;
        int lastOpen = -1;
        final int xpLength = XPath.length();
        for (int i = 0; i < xpLength; i++) {
            switch (XPath.charAt(i)) {
            case '[':
                lastOpen = i;
                break;
            case ']':
                if (curr++ != pos)
                    break;
                // very often the right index is already set, so we skip the new string in that case
                final String oldIndex = XPath.substring(lastOpen + 1, i);
                final String newIndex = String.valueOf(index);
                if (!oldIndex.equals(newIndex)) {
                    return XPath.substring(0, lastOpen + 1) + newIndex + XPath.substring(i);
                } else {
                    // index not changed
                    return XPath;
                }
            }
        }
        return XPath; //not found, return original
    }

    /**
     * Optimized uppercase method for XPaths (characters are limited to a-z).
     * 
     * @param xpath the XPath
     * @return      the uppercased XPath
     * @since 3.2.0
     */
    public static String xpToUpperCase(String xpath) {
        if (xpath == null) {
            return null;
        }
        StringBuilder out = null;
        final int len = xpath.length();
        for (int i = 0; i < len; i++) {
            final char ch = xpath.charAt(i);
            if (ch >= 'a' && ch <= 'z') {
                if (out == null) {
                    out = new StringBuilder(len);
                    out.append(xpath.substring(0, i));
                }
                out.append((char) (ch - 32));
            } else if (out != null) {
                out.append(ch);
            }
        }
        return out != null ? out.toString() : xpath;
    }

    private static char xpCharToUpperCase(char ch) {
        if (ch >= 'a' && ch <= 'z') {
            return (char) (ch - 32);
        } else {
            return ch;
        }
    }

    /**
     * Return the depth of the XPath element (= the number of slashes)
     *
     * @param xpath    the xpath
     * @return         the depth (min. 1 for valid xpaths)
     * @since 3.2.0
     */
    public static int getDepth(String xpath) {
        int depth = 0;
        for (int i = 0; i < xpath.length(); i++) {
            if (xpath.charAt(i) == '/') {
                depth++;
            }
        }
        return depth;
    }

    /**
     * Add default indices (= 1) to a string representation.
     *
     * @param xmult    the existing list of indices, comma-separated (may be empty)
     * @param count    the number of indices that should be added
     * @return  the new xmult string
     * @since 3.2.0
     */
    public static String addDefaultIndices(String xmult, int count) {
        // add missing indices after the base group - TODO: optimize?
        final StringBuilder indices = new StringBuilder(xmult.length() + count * 2);
        indices.append(xmult);
        for (int i = 0; i < count; i++) {
            if (indices.length() > 0) {
                indices.append(',');
            }
            indices.append('1');
        }
        return indices.toString();
    }

    /**
     * Internal method to clear the thread-based XPath lookup cache.
     */
    public static void clearRequestCache() {
        CACHED_SPLIT_PATHS.remove();
    }

}