(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";
+}