(TRINIDAD-2508)
JSF 2.2 @MultipartConfig on FacesServlet breaks Trinidad file upload
Thanks to Andy Schwarz for the partial patch and idea
ensured it's working on jetty as well
diff --git a/pom.xml b/pom.xml
index 5c99ce4..0fedf50 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1048,7 +1048,7 @@
         <jsf-ri.version>2.2.14</jsf-ri.version>
         <jsf-myfaces.version>2.2.12</jsf-myfaces.version>
         <jetty.groupId>org.eclipse.jetty</jetty.groupId>
-        <jetty-plugin.version>9.2.13.v20150730</jetty-plugin.version>
+        <jetty-plugin.version>9.2.21.v20170120</jetty-plugin.version>
       </properties>
     </profile>
 
diff --git a/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/config/upload/UploadRequestManager.java b/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/config/upload/UploadRequestManager.java
index a57d59a..c20900a 100644
--- a/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/config/upload/UploadRequestManager.java
+++ b/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/config/upload/UploadRequestManager.java
@@ -40,12 +40,6 @@
     setRequest(ec);
   }
 
-  public UploadRequestManager(HttpServletRequest req, Map<String, String[]> params)
-  {
-    _params = params;
-    setRequest(req);
-  }
-
   /**
    * Hide the content type so that no one tries to re-download the
    * uploaded files.
diff --git a/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/config/upload/UploadRequestWrapper.java b/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/config/upload/UploadRequestWrapper.java
index 5ed013e..da6b6e8 100644
--- a/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/config/upload/UploadRequestWrapper.java
+++ b/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/config/upload/UploadRequestWrapper.java
@@ -46,15 +46,9 @@
 {
   public UploadRequestWrapper(ExternalContext ec, Map<String, String[]> params)
   {
-    this((HttpServletRequest) ec.getRequest(), params);
-    
-  }
-  
-  public UploadRequestWrapper(HttpServletRequest req, Map<String, String[]> params)
-  {
-    super(req);
-    _manager = new UploadRequestManager(req, params);
-    
+    super((HttpServletRequest) ec.getRequest());
+    _manager = new UploadRequestManager(ec, params);
+
   }
 
   /**
diff --git a/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/util/QueryParams.java b/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/util/QueryParams.java
new file mode 100644
index 0000000..31eb372
--- /dev/null
+++ b/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/util/QueryParams.java
@@ -0,0 +1,196 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.myfaces.trinidadinternal.util;
+
+import java.io.UnsupportedEncodingException;
+
+import java.net.URLDecoder;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Parsed representation of a query string.
+ * 
+ * Although this class is thread-safe, the intention is that instances of QueryParam will be confined to
+ * the current request thread.  (Also, the Servlet API requires that we expose parameter values as arrays, which
+ * are inherently mutable and thus not-thread safe.)
+ */
+abstract public class QueryParams
+{
+  /**
+   * Parses the specified query string.
+   * 
+   * @param queryString the query string to parse
+   * @param encoding a character encoding suitable for use with URLDecoder.decode().
+   * @return the parsed representation of the query string
+   * @throws UnsupportedEncodingException if the encoding is not supported
+   */
+  public static QueryParams parseQueryString(String queryString, String encoding)
+    throws UnsupportedEncodingException
+  {
+    return ((queryString == null) || (queryString.length() == 0))? EmptyQueryParams.getInstance() :
+      new NonEmptyQueryParams(_parseNonEmptyQueryString(queryString, encoding));
+  }
+
+  /**
+   * Returns an immutable map of query parameter name to value. In cases where a particular query parameter
+   * appears multiple times in the query string, only the first valeu will appear in the returned map.
+   * 
+   * Suitable for use as an implementation of ExternalContext.getRequestParameterMap().
+   */  
+  abstract public Map<String, String> getParameterMap();
+
+  /**
+   * Returns an immutable map of query parameter name to (possibly multiple) values.
+   * 
+   * Suitable for use as an implementation of ExternalContext.getRequestParameterValuesMap().
+   */  
+  abstract public Map<String, String[]> getParameterValuesMap();
+
+  private static Map<String, String[]> _parseNonEmptyQueryString(String queryString, String encoding)
+    throws UnsupportedEncodingException
+  {
+    assert(queryString != null);
+    assert(queryString.length() > 0);
+
+    Map<String, String[]> parameterValuesMap = new HashMap<String, String[]>();
+
+    String[] nameValuePairs = queryString.split(_URL_PARAM_SEPERATOR);
+    for (String nameValuePair : nameValuePairs)
+    {
+      String[] nameAndValue = nameValuePair.split(_URL_NAME_VALUE_PAIR_SEPERATOR);
+      String paramName = _decodeParamName(nameAndValue, encoding);
+      String paramValue = _decodeParamValue(nameAndValue, encoding);
+      __addParameterToMap(parameterValuesMap, paramName, paramValue);
+    }
+
+    return parameterValuesMap;
+  }
+
+  // Package-private for use by QueryParamsTest.
+  static void __addParameterToMap(Map<String, String[]> map, String name, String value)
+  {
+    String[] oldValues = map.get(name);
+    String[] newValues;
+
+    if (oldValues == null)
+    {
+      newValues = new String[] { value };
+    }
+    else
+    {
+      List<String> newValuesList = new ArrayList<String>(Arrays.<String>asList(oldValues));
+      newValuesList.add(value);
+      newValues = newValuesList.toArray(new String[oldValues.length + 1]);
+    }
+    
+    map.put(name, newValues);
+  }
+
+  private static String _decodeParamName(String[] nameAndValue, String encoding)
+    throws UnsupportedEncodingException
+  {
+    assert (nameAndValue.length > 0);
+    return URLDecoder.decode(nameAndValue[0], encoding);
+  }
+
+  private static String _decodeParamValue(String[] nameAndValue, String encoding)
+    throws UnsupportedEncodingException
+  {
+    return (nameAndValue.length > 1) ? URLDecoder.decode(nameAndValue[1], encoding) : "";
+  }
+
+  private final static class NonEmptyQueryParams extends QueryParams
+  {
+    public NonEmptyQueryParams(Map<String, String[]> parameterValuesMap)
+    {
+      _parameterValuesMap = Collections.unmodifiableMap(parameterValuesMap);
+      _parameterMap = _toSingleValueParameterMap(parameterValuesMap);
+    }
+
+    @Override
+    public Map<String, String> getParameterMap()
+    {
+      return _parameterMap;
+    }
+
+    @Override
+    public Map<String, String[]> getParameterValuesMap()
+    {
+      return _parameterValuesMap;
+    }
+    
+    private static final Map<String, String> _toSingleValueParameterMap(Map<String, String[]> parameterValuesMap)
+    {
+      Map<String, String> singleValueMap = new HashMap<String, String>();
+      
+      for (Map.Entry<String, String[]> multiValueParam : parameterValuesMap.entrySet())
+      {
+        String[] multiValue = multiValueParam.getValue();
+        assert(multiValue != null);
+        assert(multiValue.length > 0);
+        
+        singleValueMap.put(multiValueParam.getKey(), multiValue[0]);
+      }
+
+      return Collections.unmodifiableMap(singleValueMap);
+    }
+    
+    private final Map<String, String[]> _parameterValuesMap;
+    private final Map<String, String> _parameterMap;
+  }
+
+  private static class EmptyQueryParams extends QueryParams
+  {
+    public static QueryParams getInstance()
+    {
+      return _INSTANCE;
+    }
+
+    @Override
+    public Map<String, String> getParameterMap()
+    {
+      return Collections.emptyMap();
+    }
+
+    @Override
+    public Map<String, String[]> getParameterValuesMap()
+    {
+      return Collections.emptyMap();
+    }
+    
+    private EmptyQueryParams()
+    {
+    }
+    
+    private static final QueryParams _INSTANCE = new EmptyQueryParams();
+  }
+
+  private QueryParams()
+  {
+  }
+
+  private static final String _URL_PARAM_SEPERATOR = "&";
+  private static final String _URL_NAME_VALUE_PAIR_SEPERATOR = "=";
+}
diff --git a/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/webapp/TrinidadFilterImpl.java b/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/webapp/TrinidadFilterImpl.java
index dc292a0..6783a19 100644
--- a/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/webapp/TrinidadFilterImpl.java
+++ b/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/webapp/TrinidadFilterImpl.java
@@ -21,16 +21,18 @@
 import java.io.IOException;
 import java.io.Serializable;
 
+import java.io.UnsupportedEncodingException;
+
 import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
-import javax.faces.FactoryFinder;
 import javax.faces.component.UIViewRoot;
 import javax.faces.context.ExternalContext;
+import javax.faces.context.ExternalContextWrapper;
 import javax.faces.context.FacesContext;
-import javax.faces.context.FacesContextFactory;
 import javax.servlet.Filter;
 import javax.servlet.FilterChain;
 import javax.servlet.FilterConfig;
@@ -60,6 +62,8 @@
 import org.apache.myfaces.trinidadinternal.context.external.ServletExternalContext;
 import org.apache.myfaces.trinidadinternal.renderkit.core.CoreRenderKit;
 import org.apache.myfaces.trinidadinternal.renderkit.core.xhtml.XhtmlConstants;
+import org.apache.myfaces.trinidadinternal.share.util.MultipartFormHandler;
+import org.apache.myfaces.trinidadinternal.util.QueryParams;
 import org.apache.myfaces.trinidadinternal.webapp.wrappers.BasicHTMLBrowserRequestWrapper;
 
 /**
@@ -161,27 +165,21 @@
     }
     
     // potentially wrap the ServletContext in order to check managed bean HA
-    ExternalContext externalContext = new ServletExternalContext(
-                                        _getPotentiallyWrappedServletContext(request),
-                                        request,
-                                        response);
+    ExternalContext externalContext = _createExternalContext(request, response);
 
     // provide a (Pseudo-)FacesContext for configuration tasks
     PseudoFacesContext facesContext = new PseudoFacesContext(externalContext);
     facesContext.setAsCurrentInstance();
 
-    // for error handling
-    boolean isPartialRequest=false;
-
     GlobalConfiguratorImpl config;
     try
     {
-      isPartialRequest = CoreRenderKit.isPartialRequest(externalContext);
       config = GlobalConfiguratorImpl.getInstance();
       config.beginRequest(externalContext);
     }
     catch(Throwable t)
     {
+      boolean isPartialRequest = CoreRenderKit.isPartialRequest(externalContext);
       facesContext.release();
       _handleException(externalContext, isPartialRequest, t);
       return;
@@ -196,6 +194,15 @@
         request = (ServletRequest)externalContext.getRequest();
         response = (ServletResponse)externalContext.getResponse();
 
+        //To maintain backward compatibilty, wrap the request at the filter level
+        Map<String, String[]> addedParams = FileUploadConfiguratorImpl.getAddedParameters(externalContext);
+
+        if(addedParams != null)
+        {
+          FileUploadConfiguratorImpl.apply(externalContext);
+          request = new UploadRequestWrapper(externalContext, addedParams);
+        }
+
         String noJavaScript = request.getParameter(XhtmlConstants.NON_JS_BROWSER);
 
         // Wrap the request only for Non-javaScript browsers
@@ -205,16 +212,6 @@
           request = new BasicHTMLBrowserRequestWrapper((HttpServletRequest)request);
         }
 
-        //To maintain backward compatibilty, wrap the request at the filter level
-         Map<String, String[]> addedParams = FileUploadConfiguratorImpl.getAddedParameters(externalContext);
-
-        if(addedParams != null)
-        {
-          FileUploadConfiguratorImpl.apply(externalContext);
-          request = new UploadRequestWrapper((HttpServletRequest)request, addedParams);
-          isPartialRequest = CoreRenderKit.isPartialRequest(addedParams);
-        }
-
         responseComplete = facesContext.getResponseComplete();
       }
       finally
@@ -231,6 +228,7 @@
     }
     catch (Throwable t)
     {
+      boolean isPartialRequest = CoreRenderKit.isPartialRequest(externalContext);
       _handleException(externalContext, isPartialRequest, t);
     }
     finally
@@ -241,11 +239,36 @@
       }
       catch(Throwable t)
       {
+        boolean isPartialRequest = CoreRenderKit.isPartialRequest(externalContext);
         _handleException(externalContext, isPartialRequest, t);
       }
     }
   }
 
+  private ExternalContext _createExternalContext(ServletRequest request, ServletResponse response)
+  {
+    // potentially wrap the ServletContext in order to check managed bean HA
+    ExternalContext externalContext = new ServletExternalContext(
+                                        _getPotentiallyWrappedServletContext(request),
+                                        request,
+                                        response);
+
+    if (_isMultipartHttpServletRequest(externalContext))
+    {
+      // We need to wrap the ExternalContext for multipart form posts in order to avoid
+      // premature reads on the request input stream. See MultipartExternalContextWrapper
+      // class doc for details.
+      //
+      // Note: we only appply this fix for HttpServlet use cases, as the fix requires access
+      // to the query string, and I don't see how to get at the query string for portlet
+      // requests.  It is possible that this means that trinidad file uploads may still be broken
+      // for portlets when running against JSF 2.2.
+      externalContext = new MultipartExternalContextWrapper(externalContext);
+    }
+    
+    return externalContext;
+  }
+
   /**
    * For PPR errors, handle the request specially
    */
@@ -521,9 +544,143 @@
     return _servletContext;
   }
 
+  private static boolean _isMultipartHttpServletRequest(ExternalContext externalContext)
+  {
+    return (MultipartFormHandler.isMultipartRequest(externalContext) &&
+             (externalContext.getRequest() instanceof HttpServletRequest));
+  }
+
+  /**
+   * With the addition of a standard multipart form processing solution in Java EE, we now
+   * have contention between Trinidad and Java EE for who will a) read the input stream for
+   * multiparm form posts and b) manage uploaded files.  This contention was exacerbated by
+   * the addition of the @MultipartConfig annotation to the FacesServlet in JSF 2.2.  As a
+   * result of this addition, any attempt to get a request parameter (either a query or post
+   * parameter) will cause the servlet engine to read the input stream.  If this happens before
+   * Trinidad's FileUploadConfiguratorImpl is invoked, FileUploadConfiguratorImpl won't be
+   * able to read the request input stream and as a result Trinidad file upload processing
+   * is broken.
+   * 
+   * As a temporary workaround until we can devise a better integration strategy with Java EE's
+   * multipart support, we want to avoid request parameter lookups before FileUploadConfiguratorImpl
+   * gets a crack at the input stream.  This ExternalContextWrapper helps with this, by suppressing
+   * access to the underlying request parameter maps while Configurators are invoked.  We do, however,
+   * need to provide access to query parameters, so these (and not post parameters) are exposed via the
+   * getRequestParameter* methods.
+   * 
+   * This simulates the behavior prior to JSF 2.2 (ie. prior to the additional @MultipartConfig):
+   * query parameter lookups are successful, but post parameter lookups return null until after the
+   * request input stream is processed.
+   */
+  private static class MultipartExternalContextWrapper extends ExternalContextWrapper
+  {
+    private MultipartExternalContextWrapper(ExternalContext wrapped)
+    {
+      assert(wrapped.getRequest() instanceof HttpServletRequest);
+
+      _wrapped = wrapped;
+      _queryParams = _parseQueryParameters(wrapped);
+    }
+
+    @Override
+    public Map<String, String> getRequestParameterMap()
+    {
+      return _queryParams.getParameterMap();
+    }
+
+    @Override
+    public Iterator<String> getRequestParameterNames()
+    {
+      return _queryParams.getParameterMap().keySet().iterator();
+    }
+
+    @Override
+    public Map<String, String[]> getRequestParameterValuesMap()
+    {
+      return _queryParams.getParameterValuesMap();
+    }
+
+    @Override
+    public ExternalContext getWrapped()
+    {
+      return _wrapped;
+    }
+    
+    private static QueryParams _parseQueryParameters(ExternalContext externalContext)
+    {
+      String queryString = _getQueryString(externalContext);
+      String encoding = _getQueryStringEncoding(externalContext);
+      
+      try
+      {
+        return QueryParams.parseQueryString(queryString, encoding);
+      }
+      catch (UnsupportedEncodingException e)
+      {
+        _LOG.warning(e);
+      }
+
+      // Retry, forcing encoding to UTF-8
+      try
+      {
+        return QueryParams.parseQueryString(queryString, _UTF8);
+      }
+      catch (UnsupportedEncodingException e)
+      {
+        _LOG.severe(e);
+        throw new IllegalStateException(e);
+      }
+    }
+    
+    private static String _getQueryString(ExternalContext externalContext)
+    {
+      return ((HttpServletRequest)externalContext.getRequest()).getQueryString();
+    }
+
+    private static String _getQueryStringEncoding(ExternalContext externalContext)
+    {
+      String encoding = externalContext.getRequestCharacterEncoding();
+      
+      // The request character encoding should already have been initialized
+      // by ServletExternalContext._initHttpServletRequest().  If the request
+      // character encoding is null, we could:
+      //
+      // 1.  Try to derive the document's character encoding.
+      // 2.  Default to UTF-8.
+      // 3.  Fail.
+      //
+      // I don't know that #1 is possible at this point in the request lifecycle.  A null
+      // encoding here means that the encoding is not specified via the request's content
+      // type, and is not available via the ViewHandler.CHARACTER_ENCODING_KEY session
+      // attribute.  Not sure where else to look.
+      //
+      // I prefer #2 over #3, especially as the javadoc for java.net.URLDecoder states the following:
+      //
+      //   Note: The World Wide Web Consortium Recommendation states that UTF-8 should be used.
+      //   Not doing so may introduce incompatibilites.
+      //
+      // Also note that the encoding that we use to decode the query string does not impct subsequent
+      // request parameter/payload decoding.  This encoding will only be applied to query parameters
+      // that are retrieved by Configurators during multipart pst processing, which is a very small subset
+      // of the request paramters.
+      if (encoding == null)
+      {
+        _LOG.info("No request character encoding found.  Parsing query string with encoding " +
+                  _UTF8);
+        encoding = _UTF8;
+      }
+
+      return encoding;
+    }
+    
+    private final ExternalContext _wrapped;
+    private final QueryParams _queryParams;
+  }
+
   private ServletContext _servletContext;
   private List<Filter> _filters = null;
 
+  private static final String _UTF8 = "UTF-8";
   private static final String _LAUNCH_KEY = "_dlgDta";
   private static final String _IS_RETURNING_KEY =
     "org.apache.myfaces.trinidadinternal.webapp.AdfacesFilterImpl.IS_RETURNING";
diff --git a/trinidad-impl/src/test/java/org/apache/myfaces/trinidadinternal/util/QueryParamsTest.java b/trinidad-impl/src/test/java/org/apache/myfaces/trinidadinternal/util/QueryParamsTest.java
new file mode 100644
index 0000000..b985ee0
--- /dev/null
+++ b/trinidad-impl/src/test/java/org/apache/myfaces/trinidadinternal/util/QueryParamsTest.java
@@ -0,0 +1,147 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.myfaces.trinidadinternal.util;
+
+
+import java.io.UnsupportedEncodingException;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Unit tests for QueryParams
+ */
+public class QueryParamsTest
+{
+  @Test
+  public void testNullQueryString()
+    throws UnsupportedEncodingException
+  {
+    _testNullOrEmpty(null);
+  }
+
+  @Test
+  public void testEmptyQueryString()
+    throws UnsupportedEncodingException
+  {
+    _testNullOrEmpty("");
+  }
+  
+  @Test
+  public void testNonEmptyQueryStrings()
+    throws UnsupportedEncodingException
+  {
+    _parseAndCompare("foo=bar", "foo", "bar");
+    _parseAndCompare("foo=bar1&foo=bar2", "foo", "bar1", "foo", "bar2");
+    _parseAndCompare("a=1&a=2&b=3&b=4&c=5", "a", "1", "a", "2", "b", "3", "b", "4", "c", "5");
+    _parseAndCompare("foo", "foo", "");
+    _parseAndCompare("foo=", "foo", "");
+  }
+  
+  @Test
+  public void testEncodedQueryStrings()
+    throws UnsupportedEncodingException
+  {
+    _parseAndCompare("foo=bar1+bar2", "foo", "bar1 bar2");
+    _parseAndCompare("foo+bar=baz", "foo bar", "baz");
+    _parseAndCompare("foo=%C3%A9", "foo", "\u00e9");
+  }
+
+  @Test(expected=UnsupportedEncodingException.class)
+  public void testUnsupportedEncoding()
+    throws UnsupportedEncodingException
+  {
+    QueryParams.parseQueryString("foo=%C3%A9", "UTF-FOO");
+  }
+
+  private void _testNullOrEmpty(String nullOrEmpty)
+    throws UnsupportedEncodingException
+  {
+    QueryParams params = QueryParams.parseQueryString(nullOrEmpty, _UTF8);
+    Assert.assertNotNull(params);
+    Assert.assertTrue(params.getParameterMap().isEmpty());    
+  }
+  
+  private void _parseAndCompare(String queryString, String... expectedNameValues)
+    throws UnsupportedEncodingException
+  {
+    QueryParams params = QueryParams.parseQueryString(queryString, _UTF8);
+    Map<String, String[]> expectedParamValuesMap = _toParameterValuesMap(expectedNameValues);
+    _compareParamValuesMaps(params.getParameterValuesMap(), expectedParamValuesMap);
+    _checkSingleValueParamMap(params.getParameterMap(), expectedParamValuesMap);
+  }
+  
+  private static Map<String, String[]> _toParameterValuesMap(String... nameValues)
+  {
+    Map<String, String[]> map = new HashMap<String, String[]>();
+    
+    if ((nameValues.length % 2) != 0)
+    {
+      throw new IllegalArgumentException("nameValues must contain an even number of entries");
+    }
+    
+    for (int i = 0; i < nameValues.length; i += 2)
+    {
+      String paramName = nameValues[i];
+      String paramValue = nameValues[i + 1];
+      QueryParams.__addParameterToMap(map, paramName, paramValue);
+    }
+
+    return map;
+  }
+  
+  private static void _compareParamValuesMaps(
+    Map<String, String[]> sourceMap,
+    Map<String, String[]> targetMap
+    )
+  {
+    Assert.assertNotNull(sourceMap);
+    Assert.assertNotNull(targetMap);
+    Assert.assertEquals(sourceMap.size(), targetMap.size());
+    
+    for (Map.Entry<String, String[]> entry : sourceMap.entrySet())
+    {
+      String[] sourceValues = entry.getValue();
+      String[] targetValues = targetMap.get(entry.getKey());
+      
+      Assert.assertArrayEquals(sourceValues, targetValues);
+    }
+  }
+  
+  private static void _checkSingleValueParamMap(
+    Map<String, String> singleValueMap,
+    Map<String, String[]> multiValueMap
+    )
+  {
+    Assert.assertTrue(singleValueMap.size() == multiValueMap.size());
+    
+    for (Map.Entry<String, String> param : singleValueMap.entrySet())
+    {      
+      String[] multiValue = multiValueMap.get(param.getKey());
+      Assert.assertNotNull(multiValue);
+      Assert.assertTrue(multiValue.length > 0);      
+      Assert.assertEquals(param.getValue(), multiValue[0]);
+    }
+  }
+
+  private static final String _UTF8 = "UTF-8";
+}