content: /*
   * 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.Locale;
  import java.util.Set;
  import java.util.StringTokenizer;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  
  /**
   *
   * 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, they 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(Locale.ROOT);
                      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 parses 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 parameter 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 unsanitized 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 parameters 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.
       */
      private static final Pattern plusOrPercent = Pattern.compile("[+%]");
      public String unescape(String string) {
          final Matcher matcher = plusOrPercent.matcher(string);
          if (!matcher.find()) return string;
          final int firstEscape = matcher.start();
  
          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();
      }
  }
  
  