FileDocCategorySizeDatePackage
UrlQuerySanitizer.javaAPI DocAndroid 1.5 API31293Wed May 06 22:41:54 BST 2009android.net

UrlQuerySanitizer.java

/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.net;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;

/**
 *
 * Sanitizes the Query portion of a URL. Simple example:
 * <code>
 * UrlQuerySanitizer sanitizer = new UrlQuerySanitizer();
 * sanitizer.setAllowUnregisteredParamaters(true);
 * sanitizer.parseUrl("http://example.com/?name=Joe+User");
 * String name = sanitizer.getValue("name"));
 * // name now contains "Joe_User"
 * </code>
 *
 * Register ValueSanitizers to customize the way individual
 * parameters are sanitized:
 * <code>
 * UrlQuerySanitizer sanitizer = new UrlQuerySanitizer();
 * sanitizer.registerParamater("name", UrlQuerySanitizer.createSpaceLegal());
 * sanitizer.parseUrl("http://example.com/?name=Joe+User");
 * String name = sanitizer.getValue("name"));
 * // name now contains "Joe User". (The string is first decoded, which
 * // converts the '+' to a ' '. Then the string is sanitized, which
 * // converts the ' ' to an '_'. (The ' ' is converted because the default
 * unregistered parameter sanitizer does not allow any special characters,
 * and ' ' is a special character.)
 * </code>
 *
 * There are several ways to create ValueSanitizers. In order of increasing
 * sophistication:
 * <ol>
 * <li>Call one of the UrlQuerySanitizer.createXXX() methods.
 * <li>Construct your own instance of
 * UrlQuerySanitizer.IllegalCharacterValueSanitizer.
 * <li>Subclass UrlQuerySanitizer.ValueSanitizer to define your own value
 * sanitizer.
 * </ol>
 *
 */
public class UrlQuerySanitizer {

    /**
     * A simple tuple that holds parameter-value pairs.
     *
     */
    public class ParameterValuePair {
        /**
         * Construct a parameter-value tuple.
         * @param parameter an unencoded parameter
         * @param value an unencoded value
         */
        public ParameterValuePair(String parameter,
                String value) {
            mParameter = parameter;
            mValue = value;
        }
        /**
         * The unencoded parameter
         */
        public String mParameter;
        /**
         * The unencoded value
         */
        public String mValue;
    }

    final private HashMap<String, ValueSanitizer> mSanitizers =
        new HashMap<String, ValueSanitizer>();
    final private HashMap<String, String> mEntries =
        new HashMap<String, String>();
    final private ArrayList<ParameterValuePair> mEntriesList =
        new ArrayList<ParameterValuePair>();
    private boolean mAllowUnregisteredParamaters;
    private boolean mPreferFirstRepeatedParameter;
    private ValueSanitizer mUnregisteredParameterValueSanitizer =
        getAllIllegal();

    /**
     * A functor used to sanitize a single query value.
     *
     */
    public static interface ValueSanitizer {
        /**
         * Sanitize an unencoded value.
         * @param value
         * @return the sanitized unencoded value
         */
        public String sanitize(String value);
    }

    /**
     * Sanitize values based on which characters they contain. Illegal
     * characters are replaced with either space or '_', depending upon
     * whether space is a legal character or not.
     */
    public static class IllegalCharacterValueSanitizer implements
        ValueSanitizer {
        private int mFlags;

        /**
         * Allow space (' ') characters.
         */
        public final static int SPACE_OK =              1 << 0;
        /**
         * Allow whitespace characters other than space. The
         * other whitespace characters are
         * '\t' '\f' '\n' '\r' and '\0x000b' (vertical tab)
         */
        public final static int OTHER_WHITESPACE_OK =  1 << 1;
        /**
         * Allow characters with character codes 128 to 255.
         */
        public final static int NON_7_BIT_ASCII_OK =    1 << 2;
        /**
         * Allow double quote characters. ('"')
         */
        public final static int DQUOTE_OK =             1 << 3;
        /**
         * Allow single quote characters. ('\'')
         */
        public final static int SQUOTE_OK =             1 << 4;
        /**
         * Allow less-than characters. ('<')
         */
        public final static int LT_OK =                 1 << 5;
        /**
         * Allow greater-than characters. ('>')
         */
        public final static int GT_OK =                 1 << 6;
        /**
         * Allow ampersand characters ('&')
         */
        public final static int AMP_OK =                1 << 7;
        /**
         * Allow percent-sign characters ('%')
         */
        public final static int PCT_OK =                1 << 8;
        /**
         * Allow nul characters ('\0')
         */
        public final static int NUL_OK =                1 << 9;
        /**
         * Allow text to start with a script URL
         * such as "javascript:" or "vbscript:"
         */
        public final static int SCRIPT_URL_OK =         1 << 10;

        /**
         * Mask with all fields set to OK
         */
        public final static int ALL_OK =                0x7ff;

        /**
         * Mask with both regular space and other whitespace OK
         */
        public final static int ALL_WHITESPACE_OK =
            SPACE_OK | OTHER_WHITESPACE_OK;


        // Common flag combinations:

        /**
         * <ul>
         * <li>Deny all special characters.
         * <li>Deny script URLs.
         * </ul>
         */
        public final static int ALL_ILLEGAL =
            0;
        /**
         * <ul>
         * <li>Allow all special characters except Nul. ('\0').
         * <li>Allow script URLs.
         * </ul>
         */
        public final static int ALL_BUT_NUL_LEGAL =
            ALL_OK & ~NUL_OK;
        /**
         * <ul>
         * <li>Allow all special characters except for:
         * <ul>
         *  <li>whitespace characters
         *  <li>Nul ('\0')
         * </ul>
         * <li>Allow script URLs.
         * </ul>
         */
        public final static int ALL_BUT_WHITESPACE_LEGAL =
            ALL_OK & ~(ALL_WHITESPACE_OK | NUL_OK);
        /**
         * <ul>
         * <li>Allow characters used by encoded URLs.
         * <li>Deny script URLs.
         * </ul>
         */
        public final static int URL_LEGAL =
            NON_7_BIT_ASCII_OK | SQUOTE_OK | AMP_OK | PCT_OK;
        /**
         * <ul>
         * <li>Allow characters used by encoded URLs.
         * <li>Allow spaces.
         * <li>Deny script URLs.
         * </ul>
         */
        public final static int URL_AND_SPACE_LEGAL =
            URL_LEGAL | SPACE_OK;
        /**
         * <ul>
         * <li>Allow ampersand.
         * <li>Deny script URLs.
         * </ul>
         */
        public final static int AMP_LEGAL =
            AMP_OK;
        /**
         * <ul>
         * <li>Allow ampersand.
         * <li>Allow space.
         * <li>Deny script URLs.
         * </ul>
         */
        public final static int AMP_AND_SPACE_LEGAL =
            AMP_OK | SPACE_OK;
        /**
         * <ul>
         * <li>Allow space.
         * <li>Deny script URLs.
         * </ul>
         */
        public final static int SPACE_LEGAL =
            SPACE_OK;
        /**
         * <ul>
         * <li>Allow all but.
         * <ul>
         *  <li>Nul ('\0')
         *  <li>Angle brackets ('<', '>')
         * </ul>
         * <li>Deny script URLs.
         * </ul>
         */
        public final static int ALL_BUT_NUL_AND_ANGLE_BRACKETS_LEGAL =
            ALL_OK & ~(NUL_OK | LT_OK | GT_OK);

        /**
         *  Script URL definitions
         */

        private final static String JAVASCRIPT_PREFIX = "javascript:";

        private final static String VBSCRIPT_PREFIX = "vbscript:";

        private final static int MIN_SCRIPT_PREFIX_LENGTH = Math.min(
                JAVASCRIPT_PREFIX.length(), VBSCRIPT_PREFIX.length());

        /**
         * Construct a sanitizer. The parameters set the behavior of the
         * sanitizer.
         * @param flags some combination of the XXX_OK flags.
         */
        public IllegalCharacterValueSanitizer(
            int flags) {
            mFlags = flags;
        }
        /**
         * Sanitize a value.
         * <ol>
         * <li>If script URLs are not OK, the will be removed.
         * <li>If neither spaces nor other white space is OK, then
         * white space will be trimmed from the beginning and end of
         * the URL. (Just the actual white space characters are trimmed, not
         * other control codes.)
         * <li> Illegal characters will be replaced with
         * either ' ' or '_', depending on whether a space is itself a
         * legal character.
         * </ol>
         * @param value
         * @return the sanitized value
         */
        public String sanitize(String value) {
            if (value == null) {
                return null;
            }
            int length = value.length();
            if ((mFlags & SCRIPT_URL_OK) != 0) {
                if (length >= MIN_SCRIPT_PREFIX_LENGTH) {
                    String asLower = value.toLowerCase();
                    if (asLower.startsWith(JAVASCRIPT_PREFIX)  ||
                        asLower.startsWith(VBSCRIPT_PREFIX)) {
                        return "";
                    }
                }
            }

            // If whitespace isn't OK, get rid of whitespace at beginning
            // and end of value.
            if ( (mFlags & ALL_WHITESPACE_OK) == 0) {
                value = trimWhitespace(value);
                // The length could have changed, so we need to correct
                // the length variable.
                length = value.length();
            }

            StringBuilder stringBuilder = new StringBuilder(length);
            for(int i = 0; i < length; i++) {
                char c = value.charAt(i);
                if (!characterIsLegal(c)) {
                    if ((mFlags & SPACE_OK) != 0) {
                        c = ' ';
                    }
                    else {
                        c = '_';
                    }
                }
                stringBuilder.append(c);
            }
            return stringBuilder.toString();
        }

        /**
         * Trim whitespace from the beginning and end of a string.
         * <p>
         * Note: can't use {@link String#trim} because {@link String#trim} has a
         * different definition of whitespace than we want.
         * @param value the string to trim
         * @return the trimmed string
         */
        private String trimWhitespace(String value) {
            int start = 0;
            int last = value.length() - 1;
            int end = last;
            while (start <= end && isWhitespace(value.charAt(start))) {
                start++;
            }
            while (end >= start && isWhitespace(value.charAt(end))) {
                end--;
            }
            if (start == 0 && end == last) {
                return value;
            }
            return value.substring(start, end + 1);
        }

        /**
         * Check if c is whitespace.
         * @param c character to test
         * @return true if c is a whitespace character
         */
        private boolean isWhitespace(char c) {
            switch(c) {
            case ' ':
            case '\t':
            case '\f':
            case '\n':
            case '\r':
            case 11: /* VT */
                return true;
            default:
                return false;
            }
        }

        /**
         * Check whether an individual character is legal. Uses the
         * flag bit-set passed into the constructor.
         * @param c
         * @return true if c is a legal character
         */
        private boolean characterIsLegal(char c) {
            switch(c) {
            case ' ' : return (mFlags & SPACE_OK) != 0;
            case '\t': case '\f': case '\n': case '\r': case 11: /* VT */
              return (mFlags & OTHER_WHITESPACE_OK) != 0;
            case '\"': return (mFlags & DQUOTE_OK) != 0;
            case '\'': return (mFlags & SQUOTE_OK) != 0;
            case '<' : return (mFlags & LT_OK) != 0;
            case '>' : return (mFlags & GT_OK) != 0;
            case '&' : return (mFlags & AMP_OK) != 0;
            case '%' : return (mFlags & PCT_OK) != 0;
            case '\0': return (mFlags & NUL_OK) != 0;
            default  : return (c >= 32 && c < 127) ||
                ((c >= 128) && ((mFlags & NON_7_BIT_ASCII_OK) != 0));
            }
        }
    }

    /**
     * Get the current value sanitizer used when processing
     * unregistered parameter values.
     * <p>
     * <b>Note:</b> The default unregistered parameter value sanitizer is
     * one that doesn't allow any special characters, similar to what
     * is returned by calling createAllIllegal.
     *
     * @return the current ValueSanitizer used to sanitize unregistered
     * parameter values.
     */
    public ValueSanitizer getUnregisteredParameterValueSanitizer() {
        return mUnregisteredParameterValueSanitizer;
    }

    /**
     * Set the value sanitizer used when processing unregistered
     * parameter values.
     * @param sanitizer set the ValueSanitizer used to sanitize unregistered
     * parameter values.
     */
    public void setUnregisteredParameterValueSanitizer(
            ValueSanitizer sanitizer) {
        mUnregisteredParameterValueSanitizer = sanitizer;
    }


    // Private fields for singleton sanitizers:

    private static final ValueSanitizer sAllIllegal =
        new IllegalCharacterValueSanitizer(
                IllegalCharacterValueSanitizer.ALL_ILLEGAL);

    private static final ValueSanitizer sAllButNulLegal =
        new IllegalCharacterValueSanitizer(
                IllegalCharacterValueSanitizer.ALL_BUT_NUL_LEGAL);

    private static final ValueSanitizer sAllButWhitespaceLegal =
        new IllegalCharacterValueSanitizer(
                IllegalCharacterValueSanitizer.ALL_BUT_WHITESPACE_LEGAL);

    private static final ValueSanitizer sURLLegal =
        new IllegalCharacterValueSanitizer(
                IllegalCharacterValueSanitizer.URL_LEGAL);

    private static final ValueSanitizer sUrlAndSpaceLegal =
        new IllegalCharacterValueSanitizer(
                IllegalCharacterValueSanitizer.URL_AND_SPACE_LEGAL);

    private static final ValueSanitizer sAmpLegal =
        new IllegalCharacterValueSanitizer(
                IllegalCharacterValueSanitizer.AMP_LEGAL);

    private static final ValueSanitizer sAmpAndSpaceLegal =
        new IllegalCharacterValueSanitizer(
                IllegalCharacterValueSanitizer.AMP_AND_SPACE_LEGAL);

    private static final ValueSanitizer sSpaceLegal =
        new IllegalCharacterValueSanitizer(
                IllegalCharacterValueSanitizer.SPACE_LEGAL);

    private static final ValueSanitizer sAllButNulAndAngleBracketsLegal =
        new IllegalCharacterValueSanitizer(
                IllegalCharacterValueSanitizer.ALL_BUT_NUL_AND_ANGLE_BRACKETS_LEGAL);

    /**
     * Return a value sanitizer that does not allow any special characters,
     * and also does not allow script URLs.
     * @return a value sanitizer
     */
    public static final ValueSanitizer getAllIllegal() {
        return sAllIllegal;
    }

    /**
     * Return a value sanitizer that allows everything except Nul ('\0')
     * characters. Script URLs are allowed.
     * @return a value sanitizer
     */
    public static final ValueSanitizer getAllButNulLegal() {
        return sAllButNulLegal;
    }
    /**
     * Return a value sanitizer that allows everything except Nul ('\0')
     * characters, space (' '), and other whitespace characters.
     * Script URLs are allowed.
     * @return a value sanitizer
     */
    public static final ValueSanitizer getAllButWhitespaceLegal() {
        return sAllButWhitespaceLegal;
    }
    /**
     * Return a value sanitizer that allows all the characters used by
     * encoded URLs. Does not allow script URLs.
     * @return a value sanitizer
     */
    public static final ValueSanitizer getUrlLegal() {
        return sURLLegal;
    }
    /**
     * Return a value sanitizer that allows all the characters used by
     * encoded URLs and allows spaces, which are not technically legal
     * in encoded URLs, but commonly appear anyway.
     * Does not allow script URLs.
     * @return a value sanitizer
     */
    public static final ValueSanitizer getUrlAndSpaceLegal() {
        return sUrlAndSpaceLegal;
    }
    /**
     * Return a value sanitizer that does not allow any special characters
     * except ampersand ('&'). Does not allow script URLs.
     * @return a value sanitizer
     */
    public static final ValueSanitizer getAmpLegal() {
        return sAmpLegal;
    }
    /**
     * Return a value sanitizer that does not allow any special characters
     * except ampersand ('&') and space (' '). Does not allow script URLs.
     * @return a value sanitizer
     */
    public static final ValueSanitizer getAmpAndSpaceLegal() {
        return sAmpAndSpaceLegal;
    }
    /**
     * Return a value sanitizer that does not allow any special characters
     * except space (' '). Does not allow script URLs.
     * @return a value sanitizer
     */
    public static final ValueSanitizer getSpaceLegal() {
        return sSpaceLegal;
    }
    /**
     * Return a value sanitizer that allows any special characters
     * except angle brackets ('<' and '>') and Nul ('\0').
     * Allows script URLs.
     * @return a value sanitizer
     */
    public static final ValueSanitizer getAllButNulAndAngleBracketsLegal() {
        return sAllButNulAndAngleBracketsLegal;
    }

    /**
     * Constructs a UrlQuerySanitizer.
     * <p>
     * Defaults:
     * <ul>
     * <li>unregistered parameters are not allowed.
     * <li>the last instance of a repeated parameter is preferred.
     * <li>The default value sanitizer is an AllIllegal value sanitizer.
     * <ul>
     */
    public UrlQuerySanitizer() {
    }

    /**
     * Constructs a UrlQuerySanitizer and parse a URL.
     * This constructor is provided for convenience when the
     * default parsing behavior is acceptable.
     * <p>
     * Because the URL is parsed before the constructor returns, there isn't
     * a chance to configure the sanitizer to change the parsing behavior.
     * <p>
     * <code>
     * UrlQuerySanitizer sanitizer = new UrlQuerySanitizer(myUrl);
     * String name = sanitizer.getValue("name");
     * </code>
     * <p>
     * Defaults:
     * <ul>
     * <li>unregistered parameters <em>are</em> allowed.
     * <li>the last instance of a repeated parameter is preferred.
     * <li>The default value sanitizer is an AllIllegal value sanitizer.
     * <ul>
     */
    public UrlQuerySanitizer(String url) {
        setAllowUnregisteredParamaters(true);
        parseUrl(url);
    }

    /**
     * Parse the query parameters out of an encoded URL.
     * Works by extracting the query portion from the URL and then
     * calling parseQuery(). If there is no query portion it is
     * treated as if the query portion is an empty string.
     * @param url the encoded URL to parse.
     */
    public void parseUrl(String url) {
        int queryIndex = url.indexOf('?');
        String query;
        if (queryIndex >= 0) {
            query = url.substring(queryIndex + 1);
        }
        else {
            query = "";
        }
        parseQuery(query);
    }

    /**
     * Parse a query. A query string is any number of parameter-value clauses
     * separated by any non-zero number of ampersands. A parameter-value clause
     * is a parameter followed by an equal sign, followed by a value. If the
     * equal sign is missing, the value is assumed to be the empty string.
     * @param query the query to parse.
     */
    public void parseQuery(String query) {
        clear();
        // Split by '&'
        StringTokenizer tokenizer = new StringTokenizer(query, "&");
        while(tokenizer.hasMoreElements()) {
            String attributeValuePair = tokenizer.nextToken();
            if (attributeValuePair.length() > 0) {
                int assignmentIndex = attributeValuePair.indexOf('=');
                if (assignmentIndex < 0) {
                    // No assignment found, treat as if empty value
                    parseEntry(attributeValuePair, "");
                }
                else {
                    parseEntry(attributeValuePair.substring(0, assignmentIndex),
                            attributeValuePair.substring(assignmentIndex + 1));
                }
            }
        }
    }

    /**
     * Get a set of all of the parameters found in the sanitized query.
     * <p>
     * Note: Do not modify this set. Treat it as a read-only set.
     * @return all the parameters found in the current query.
     */
    public Set<String> getParameterSet() {
        return mEntries.keySet();
    }

    /**
     * An array list of all of the parameter value pairs in the sanitized
     * query, in the order they appeared in the query. May contain duplicate
     * parameters.
     * <p class="note"><b>Note:</b> Do not modify this list. Treat it as a read-only list.</p>
     */
    public List<ParameterValuePair> getParameterList() {
        return mEntriesList;
    }

    /**
     * Check if a parameter exists in the current sanitized query.
     * @param parameter the unencoded name of a parameter.
     * @return true if the paramater exists in the current sanitized queary.
     */
    public boolean hasParameter(String parameter) {
        return mEntries.containsKey(parameter);
    }

    /**
     * Get the value for a parameter in the current sanitized query.
     * Returns null if the parameter does not
     * exit.
     * @param parameter the unencoded name of a parameter.
     * @return the sanitized unencoded value of the parameter,
     * or null if the parameter does not exist.
     */
    public String getValue(String parameter) {
        return mEntries.get(parameter);
    }

    /**
     * Register a value sanitizer for a particular parameter. Can also be used
     * to replace or remove an already-set value sanitizer.
     * <p>
     * Registering a non-null value sanitizer for a particular parameter
     * makes that parameter a registered parameter.
     * @param parameter an unencoded parameter name
     * @param valueSanitizer the value sanitizer to use for a particular
     * parameter. May be null in order to unregister that parameter.
     * @see #getAllowUnregisteredParamaters()
     */
    public void registerParameter(String parameter,
            ValueSanitizer valueSanitizer) {
        if (valueSanitizer == null) {
            mSanitizers.remove(parameter);
        }
        mSanitizers.put(parameter, valueSanitizer);
    }

    /**
     * Register a value sanitizer for an array of parameters.
     * @param parameters An array of unencoded parameter names.
     * @param valueSanitizer
     * @see #registerParameter
     */
    public void registerParameters(String[] parameters,
            ValueSanitizer valueSanitizer) {
        int length = parameters.length;
        for(int i = 0; i < length; i++) {
            mSanitizers.put(parameters[i], valueSanitizer);
        }
    }

    /**
     * Set whether or not unregistered parameters are allowed. If they
     * are not allowed, then they will be dropped when a query is sanitized.
     * <p>
     * Defaults to false.
     * @param allowUnregisteredParamaters true to allow unregistered parameters.
     * @see #getAllowUnregisteredParamaters()
     */
    public void setAllowUnregisteredParamaters(
            boolean allowUnregisteredParamaters) {
        mAllowUnregisteredParamaters = allowUnregisteredParamaters;
    }

    /**
     * Get whether or not unregistered parameters are allowed. If not
     * allowed, they will be dropped when a query is parsed.
     * @return true if unregistered parameters are allowed.
     * @see #setAllowUnregisteredParamaters(boolean)
     */
    public boolean getAllowUnregisteredParamaters() {
        return mAllowUnregisteredParamaters;
    }

    /**
     * Set whether or not the first occurrence of a repeated parameter is
     * preferred. True means the first repeated parameter is preferred.
     * False means that the last repeated parameter is preferred.
     * <p>
     * The preferred parameter is the one that is returned when getParameter
     * is called.
     * <p>
     * defaults to false.
     * @param preferFirstRepeatedParameter True if the first repeated
     * parameter is preferred.
     * @see #getPreferFirstRepeatedParameter()
     */
    public void setPreferFirstRepeatedParameter(
            boolean preferFirstRepeatedParameter) {
        mPreferFirstRepeatedParameter = preferFirstRepeatedParameter;
    }

    /**
     * Get whether or not the first occurrence of a repeated parameter is
     * preferred.
     * @return true if the first occurrence of a repeated parameter is
     * preferred.
     * @see #setPreferFirstRepeatedParameter(boolean)
     */
    public boolean getPreferFirstRepeatedParameter() {
        return mPreferFirstRepeatedParameter;
    }

    /**
     * Parse an escaped parameter-value pair. The default implementation
     * unescapes both the parameter and the value, then looks up the
     * effective value sanitizer for the parameter and uses it to sanitize
     * the value. If all goes well then addSanitizedValue is called with
     * the unescaped parameter and the sanitized unescaped value.
     * @param parameter an escaped parameter
     * @param value an unsanitzied escaped value
     */
    protected void parseEntry(String parameter, String value) {
        String unescapedParameter = unescape(parameter);
         ValueSanitizer valueSanitizer =
            getEffectiveValueSanitizer(unescapedParameter);

        if (valueSanitizer == null) {
            return;
        }
        String unescapedValue = unescape(value);
        String sanitizedValue = valueSanitizer.sanitize(unescapedValue);
        addSanitizedEntry(unescapedParameter, sanitizedValue);
    }

    /**
     * Record a sanitized parameter-value pair. Override if you want to
     * do additional filtering or validation.
     * @param parameter an unescaped parameter
     * @param value a sanitized unescaped value
     */
    protected void addSanitizedEntry(String parameter, String value) {
        mEntriesList.add(
                new ParameterValuePair(parameter, value));
        if (mPreferFirstRepeatedParameter) {
            if (mEntries.containsKey(parameter)) {
                return;
            }
        }
        mEntries.put(parameter, value);
    }

    /**
     * Get the value sanitizer for a parameter. Returns null if there
     * is no value sanitizer registered for the parameter.
     * @param parameter the unescaped parameter
     * @return the currently registered value sanitizer for this parameter.
     * @see #registerParameter(String, android.net.UrlQuerySanitizer.ValueSanitizer)
     */
    public ValueSanitizer getValueSanitizer(String parameter) {
        return mSanitizers.get(parameter);
    }

    /**
     * Get the effective value sanitizer for a parameter. Like getValueSanitizer,
     * except if there is no value sanitizer registered for a parameter, and
     * unregistered paramaters are allowed, then the default value sanitizer is
     * returned.
     * @param parameter an unescaped parameter
     * @return the effective value sanitizer for a parameter.
     */
    public ValueSanitizer getEffectiveValueSanitizer(String parameter) {
        ValueSanitizer sanitizer = getValueSanitizer(parameter);
        if (sanitizer == null && mAllowUnregisteredParamaters) {
            sanitizer = getUnregisteredParameterValueSanitizer();
        }
        return sanitizer;
    }

    /**
     * Unescape an escaped string.
     * <ul>
     * <li>'+' characters are replaced by
     * ' ' characters.
     * <li>Valid "%xx" escape sequences are replaced by the
     * corresponding unescaped character.
     * <li>Invalid escape sequences such as %1z", are passed through unchanged.
     * <ol>
     * @param string the escaped string
     * @return the unescaped string.
     */
    public String unescape(String string) {
        // Early exit if no escaped characters.
        int firstEscape = string.indexOf('%');
        if ( firstEscape < 0) {
            firstEscape = string.indexOf('+');
            if (firstEscape < 0) {
                return string;
            }
        }

        int length = string.length();

        StringBuilder stringBuilder = new StringBuilder(length);
        stringBuilder.append(string.substring(0, firstEscape));
        for (int i = firstEscape; i < length; i++) {
            char c = string.charAt(i);
            if (c == '+') {
                c = ' ';
            }
            else if ( c == '%' && i + 2 < length) {
                char c1 = string.charAt(i + 1);
                char c2 = string.charAt(i + 2);
                if (isHexDigit(c1) && isHexDigit(c2)) {
                    c = (char) (decodeHexDigit(c1) * 16 + decodeHexDigit(c2));
                    i += 2;
                }
            }
            stringBuilder.append(c);
        }
        return stringBuilder.toString();
    }

    /**
     * Test if a character is a hexidecimal digit. Both upper case and lower
     * case hex digits are allowed.
     * @param c the character to test
     * @return true if c is a hex digit.
     */
    protected boolean isHexDigit(char c) {
        return decodeHexDigit(c) >= 0;
    }

    /**
     * Convert a character that represents a hexidecimal digit into an integer.
     * If the character is not a hexidecimal digit, then -1 is returned.
     * Both upper case and lower case hex digits are allowed.
     * @param c the hexidecimal digit.
     * @return the integer value of the hexidecimal digit.
     */

    protected int decodeHexDigit(char c) {
        if (c >= '0' && c <= '9') {
            return c - '0';
        }
        else if (c >= 'A' && c <= 'F') {
            return c - 'A' + 10;
        }
        else if (c >= 'a' && c <= 'f') {
            return c - 'a' + 10;
        }
        else {
            return -1;
        }
    }

    /**
     * Clear the existing entries. Called to get ready to parse a new
     * query string.
     */
    protected void clear() {
        mEntries.clear();
        mEntriesList.clear();
    }
}