CXF-8089: Build Comma Separated Values in url from Array/List Query Param (#572)

(cherry picked from commit 768c0959b6e1882badeb55387f1d2fc34b57237e)
(cherry picked from commit c3adee999d4464d740f0740e2d5bd497b2b24af2)
diff --git a/.gitignore b/.gitignore
index fdc3d89..4f59107 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,3 +19,5 @@
 *~
 node_modules/
 derby.log
+.pmdruleset.xml
+.sts4-cache/
diff --git a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/UriBuilderImpl.java b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/UriBuilderImpl.java
index fbda422..21c63b0 100644
--- a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/UriBuilderImpl.java
+++ b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/UriBuilderImpl.java
@@ -39,13 +39,15 @@
 import javax.ws.rs.core.UriBuilder;
 import javax.ws.rs.core.UriBuilderException;
 
+import org.apache.cxf.common.util.PropertyUtils;
 import org.apache.cxf.common.util.StringUtils;
 import org.apache.cxf.jaxrs.model.URITemplate;
 import org.apache.cxf.jaxrs.utils.HttpUtils;
 import org.apache.cxf.jaxrs.utils.JAXRSUtils;
 
 public class UriBuilderImpl extends UriBuilder implements Cloneable {
-
+    private static final String EXPAND_QUERY_VALUE_AS_COLLECTION = "expand.query.value.as.collection";
+    
     private String scheme;
     private String userInfo;
     private int port = -1;
@@ -62,6 +64,8 @@
     private Map<String, Object> resolvedTemplatesPathEnc;
     private Map<String, Object> resolvedEncodedTemplates;
     
+    private boolean queryValueIsCollection;
+    
     /**
      * Creates builder with empty URI.
      */
@@ -69,6 +73,13 @@
     }
 
     /**
+     * Creates builder with empty URI and properties
+     */
+    public UriBuilderImpl(Map<String, Object> properties) {
+        queryValueIsCollection = PropertyUtils.isTrue(properties, EXPAND_QUERY_VALUE_AS_COLLECTION);
+    }
+
+    /**
      * Creates builder initialized with given URI.
      * 
      * @param uri initial value for builder
@@ -425,7 +436,8 @@
         builder.schemeSpecificPart = schemeSpecificPart;
         builder.leadingSlash = leadingSlash;
         builder.originalPathEmpty = originalPathEmpty;
-        builder.resolvedEncodedTemplates = 
+        builder.queryValueIsCollection = queryValueIsCollection;
+        builder.resolvedEncodedTemplates =
             resolvedEncodedTemplates == null ? null : new HashMap<String, Object>(resolvedEncodedTemplates);
         builder.resolvedTemplates = 
             resolvedTemplates == null ? null : new HashMap<String, Object>(resolvedTemplates);
@@ -838,27 +850,62 @@
         StringBuilder b = new StringBuilder();
         for (Iterator<Map.Entry<String, List<String>>> it = map.entrySet().iterator(); it.hasNext();) {
             Map.Entry<String, List<String>> entry = it.next();
-            for (Iterator<String> sit = entry.getValue().iterator(); sit.hasNext();) {
-                String val = sit.next();
-                b.append(entry.getKey());
-                if (val != null) {
-                    boolean templateValue = val.startsWith("{") && val.endsWith("}");
-                    if (!templateValue) {
-                        val = HttpUtils.encodePartiallyEncoded(val, isQuery);
-                        if (!isQuery) {
-                            val = val.replaceAll("/", "%2F");
+            
+            // Expand query parameter as "name=v1,v2,v3" 
+            if (isQuery && queryValueIsCollection) {
+                b.append(entry.getKey()).append('=');
+                
+                for (Iterator<String> sit = entry.getValue().iterator(); sit.hasNext();) {
+                    String val = sit.next();
+                    
+                    if (val != null) {
+                        boolean templateValue = val.startsWith("{") && val.endsWith("}");
+                        if (!templateValue) {
+                            val = HttpUtils.encodePartiallyEncoded(val, isQuery);
+                            if (!isQuery) {
+                                val = val.replaceAll("/", "%2F");
+                            }
+                        } else {
+                            val = URITemplate.createExactTemplate(val).encodeLiteralCharacters(isQuery);
                         }
-                    } else {
-                        val = URITemplate.createExactTemplate(val).encodeLiteralCharacters(isQuery);
+                        
+                        if (!val.isEmpty()) {
+                            b.append(val);
+                        }
                     }
-                    b.append('=');
-                    if (!val.isEmpty()) {
-                        b.append(val);
+                    if (sit.hasNext()) {
+                        b.append(',');
                     }
                 }
-                if (sit.hasNext() || it.hasNext()) {
+                
+                if (it.hasNext()) {
                     b.append(separator);
                 }
+            } else {
+                // Expand query parameter as "name=v1&name=v2&name=v3", or use dedicated 
+                // separator for matrix parameters
+                for (Iterator<String> sit = entry.getValue().iterator(); sit.hasNext();) {
+                    String val = sit.next();
+                    b.append(entry.getKey());
+                    if (val != null) {
+                        boolean templateValue = val.startsWith("{") && val.endsWith("}");
+                        if (!templateValue) {
+                            val = HttpUtils.encodePartiallyEncoded(val, isQuery);
+                            if (!isQuery) {
+                                val = val.replaceAll("/", "%2F");
+                            }
+                        } else {
+                            val = URITemplate.createExactTemplate(val).encodeLiteralCharacters(isQuery);
+                        }
+                        b.append('=');
+                        if (!val.isEmpty()) {
+                            b.append(val);
+                        }
+                    }
+                    if (sit.hasNext() || it.hasNext()) {
+                        b.append(separator);
+                    }
+                }
             }
         }
         return b.length() > 0 ? b.toString() : null;
diff --git a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/impl/UriBuilderImplTest.java b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/impl/UriBuilderImplTest.java
index f89533a..43d03a5 100644
--- a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/impl/UriBuilderImplTest.java
+++ b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/impl/UriBuilderImplTest.java
@@ -341,7 +341,7 @@
 
     @Test(expected = IllegalArgumentException.class)
     public void testCtorNull() throws Exception {
-        new UriBuilderImpl(null);
+        new UriBuilderImpl((URI)null);
     }
     
     @Test(expected = IllegalArgumentException.class)
diff --git a/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/ClientProxyImpl.java b/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/ClientProxyImpl.java
index c472f32..5d61c29 100644
--- a/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/ClientProxyImpl.java
+++ b/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/ClientProxyImpl.java
@@ -111,7 +111,17 @@
                            boolean isRoot, 
                            boolean inheritHeaders, 
                            Object... varValues) {
-        this(new LocalClientState(baseURI), loader, cri, isRoot, inheritHeaders, varValues);
+        this(baseURI, loader, cri, isRoot, inheritHeaders, Collections.<String, Object>emptyMap(), varValues);
+    }
+
+    public ClientProxyImpl(URI baseURI,
+            ClassLoader loader,
+            ClassResourceInfo cri,
+            boolean isRoot,
+            boolean inheritHeaders,
+            Map<String, Object> properties,
+            Object... varValues) {
+        this(new LocalClientState(baseURI, properties), loader, cri, isRoot, inheritHeaders, varValues);
     }
     
     public ClientProxyImpl(ClientState initialState,
diff --git a/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/ClientState.java b/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/ClientState.java
index de7a36a..4b64b7e 100644
--- a/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/ClientState.java
+++ b/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/ClientState.java
@@ -19,6 +19,7 @@
 package org.apache.cxf.jaxrs.client;
 
 import java.net.URI;
+import java.util.Map;
 
 import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response;
@@ -115,4 +116,20 @@
     ClientState newState(URI baseURI, 
                          MultivaluedMap<String, String> headers,
                          MultivaluedMap<String, String> templates);
+    
+    /**
+     * The factory method for creating a new state.
+     * Example, proxy and WebClient.fromClient will use this method when creating
+     * subresource proxies and new web clients respectively to ensure thet stay
+     * thread-local if needed
+     * @param baseURI baseURI
+     * @param headers request headers, can be null
+     * @param templates initial templates map, can be null
+     * @param additional properties, could be null
+     * @return client state
+     */
+    ClientState newState(URI baseURI,
+                         MultivaluedMap<String, String> headers,
+                         MultivaluedMap<String, String> templates,
+                         Map<String, Object> properties);
 }
diff --git a/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/JAXRSClientFactory.java b/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/JAXRSClientFactory.java
index 167ae73..302d9e3 100644
--- a/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/JAXRSClientFactory.java
+++ b/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/JAXRSClientFactory.java
@@ -22,6 +22,7 @@
 import java.net.URI;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 
 import javax.ws.rs.core.MultivaluedMap;
 
@@ -88,6 +89,19 @@
     
     /**
      * Creates a proxy
+     * @param baseAddress baseAddres
+     * @param cls resource class, if not interface then a CGLIB proxy will be created
+     * @param properties additional properties
+     * @return typed proxy
+     */
+    public static <T> T create(String baseAddress, Class<T> cls, Map<String, Object> properties) {
+        JAXRSClientFactoryBean bean = getBean(baseAddress, cls, null);
+        bean.setProperties(properties);
+        return bean.create(cls);
+    }
+
+    /**
+     * Creates a proxy
      * @param baseAddress baseAddress
      * @param cls resource class, if not interface then a CGLIB proxy will be created
      * @param configLocation classpath location of the configuration resource
@@ -135,10 +149,24 @@
      * @return typed proxy
      */
     public static <T> T create(String baseAddress, Class<T> cls, List<?> providers, boolean threadSafe) {
+        return create(baseAddress, cls, providers, Collections.<String, Object>emptyMap(), threadSafe);
+    }
+    /**
+     * Creates a thread safe proxy
+     * @param baseAddress baseAddress
+     * @param cls proxy class, if not interface then a CGLIB proxy will be created
+     * @param providers list of providers
+     * @param threadSafe if true then a thread-safe proxy will be created
+     * @param properties additional properties
+     * @return typed proxy
+     */
+    public static <T> T create(String baseAddress, Class<T> cls, List<?> providers, 
+            Map<String, Object> properties, boolean threadSafe) {
         JAXRSClientFactoryBean bean = getBean(baseAddress, cls, null);
         bean.setProviders(providers);
+        bean.setProperties(properties);
         if (threadSafe) {
-            bean.setInitialState(new ThreadLocalClientState(baseAddress));
+            bean.setInitialState(new ThreadLocalClientState(baseAddress, properties));
         }
         return bean.create(cls);
     }
@@ -362,7 +390,7 @@
             }
         } else {
             MultivaluedMap<String, String> headers = inheritHeaders ? client.getHeaders() : null;
-            bean.setInitialState(clientState.newState(client.getCurrentURI(), headers, null));
+            bean.setInitialState(clientState.newState(client.getCurrentURI(), headers, null, bean.getProperties()));
             proxy = bean.create(cls);
         }
         WebClient.copyProperties(WebClient.client(proxy), client);
diff --git a/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/JAXRSClientFactoryBean.java b/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/JAXRSClientFactoryBean.java
index 7936733..1c75173 100644
--- a/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/JAXRSClientFactoryBean.java
+++ b/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/JAXRSClientFactoryBean.java
@@ -219,7 +219,7 @@
             Endpoint ep = createEndpoint();
             this.getServiceFactory().sendEvent(FactoryBeanListener.Event.PRE_CLIENT_CREATE, ep);
             ClientState actualState = getActualState();
-            WebClient client = actualState == null ? new WebClient(getAddress())
+            WebClient client = actualState == null ? new WebClient(getAddress(), getProperties())
                 : new WebClient(actualState);
             initClient(client, ep, actualState == null);
     
@@ -242,11 +242,11 @@
     
     private ClientState getActualState() {
         if (threadSafe) {
-            initialState = new ThreadLocalClientState(getAddress(), timeToKeepState);
+            initialState = new ThreadLocalClientState(getAddress(), timeToKeepState, getProperties());
         }
         if (initialState != null) {
-            return headers != null
-                ? initialState.newState(URI.create(getAddress()), headers, null) : initialState;
+            return headers != null 
+                ? initialState.newState(URI.create(getAddress()), headers, null, getProperties()) : initialState;
         } else {
             return null;
         }
@@ -339,10 +339,10 @@
                                                 ClientState actualState, Object[] varValues) {
         if (actualState == null) {
             return new ClientProxyImpl(URI.create(getAddress()), proxyLoader, cri, isRoot,
-                                    inheritHeaders, varValues);
+                                    inheritHeaders, getProperties(), varValues);
         } else {
             return new ClientProxyImpl(actualState, proxyLoader, cri, isRoot,
-                                    inheritHeaders, varValues);
+                                    inheritHeaders, getProperties(), varValues);
         }
     }
 
diff --git a/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/LocalClientState.java b/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/LocalClientState.java
index 217244f..5aca35b 100644
--- a/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/LocalClientState.java
+++ b/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/LocalClientState.java
@@ -19,6 +19,9 @@
 package org.apache.cxf.jaxrs.client;
 
 import java.net.URI;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 
 import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response;
@@ -41,19 +44,38 @@
     private Response response;
     private URI baseURI;
     private UriBuilder currentBuilder;
-    
+    private Map<String, Object> properties;
+
     public LocalClientState() {
         
     }
     
     public LocalClientState(URI baseURI) {
+        this(baseURI, Collections.<String, Object>emptyMap());
+    }
+    
+    public LocalClientState(URI baseURI, Map<String, Object> properties) {
         this.baseURI = baseURI;
-        resetCurrentUri();
+        
+        if (properties != null) {
+            this.properties = new HashMap<>(properties);
+        }
+        
+        resetCurrentUri(properties);
     }
     
     public LocalClientState(URI baseURI, URI currentURI) {
+        this(baseURI, currentURI, Collections.<String, Object>emptyMap()); 
+    }
+
+    public LocalClientState(URI baseURI, URI currentURI, Map<String, Object> properties) {
         this.baseURI = baseURI;
-        this.currentBuilder = new UriBuilderImpl().uri(currentURI);
+        
+        if (properties != null) {
+            this.properties = new HashMap<>(properties);
+        }
+        
+        this.currentBuilder = new UriBuilderImpl(properties).uri(currentURI);
     }
     
     public LocalClientState(LocalClientState cs) {
@@ -63,13 +85,14 @@
         
         this.baseURI = cs.baseURI;
         this.currentBuilder = cs.currentBuilder != null ? cs.currentBuilder.clone() : null;
+        this.properties = cs.properties;
     }
-    
-    private void resetCurrentUri() {
+
+    private void resetCurrentUri(Map<String, Object> props) {
         if (isSupportedScheme(baseURI)) {
-            this.currentBuilder = new UriBuilderImpl().uri(baseURI);
+            this.currentBuilder = new UriBuilderImpl(props).uri(baseURI);
         } else {
-            this.currentBuilder = new UriBuilderImpl().uri("/");
+            this.currentBuilder = new UriBuilderImpl(props).uri("/");
         }
     }
     
@@ -83,7 +106,7 @@
     
     public void setBaseURI(URI baseURI) {
         this.baseURI = baseURI;
-        resetCurrentUri();
+        resetCurrentUri(Collections.<String, Object>emptyMap());
     }
     
     public URI getBaseURI() {
@@ -123,18 +146,17 @@
     public void reset() {
         requestHeaders.clear();
         response = null;
-        currentBuilder = new UriBuilderImpl().uri(baseURI);
+        currentBuilder = new UriBuilderImpl(properties).uri(baseURI);
         templates = null;
     }
     
-    public ClientState newState(URI currentURI, 
-                                MultivaluedMap<String, String> headers,
-                                MultivaluedMap<String, String> templatesMap) {
+    public ClientState newState(URI currentURI, MultivaluedMap<String, String> headers,
+            MultivaluedMap<String, String> templatesMap, Map<String, Object> props) {
         ClientState state = null;
         if (isSupportedScheme(currentURI)) {
-            state = new LocalClientState(currentURI);
+            state = new LocalClientState(currentURI, props);
         } else {
-            state = new LocalClientState(baseURI, currentURI);
+            state = new LocalClientState(baseURI, currentURI, props);
         }
         if (headers != null) {
             state.setRequestHeaders(headers);
@@ -149,7 +171,13 @@
         state.setTemplates(newTemplateParams);
         return state;
     }
-    
+
+    public ClientState newState(URI currentURI,
+                                MultivaluedMap<String, String> headers,
+                                MultivaluedMap<String, String> templatesMap) {
+        return newState(currentURI, headers, templatesMap, properties);
+    }
+
     private static boolean isSupportedScheme(URI uri) {
         return !StringUtils.isEmpty(uri.getScheme()) 
             && (uri.getScheme().startsWith(HTTP_SCHEME) || uri.getScheme().startsWith(WS_SCHEME));
diff --git a/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/ThreadLocalClientState.java b/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/ThreadLocalClientState.java
index d6b4313..be7d72b 100644
--- a/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/ThreadLocalClientState.java
+++ b/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/ThreadLocalClientState.java
@@ -43,11 +43,19 @@
     private long timeToKeepState;
     
     public ThreadLocalClientState(String baseURI) {
-        this.initialState = new LocalClientState(URI.create(baseURI));
+        this(baseURI, Collections.<String, Object>emptyMap());
+    }
+    
+    public ThreadLocalClientState(String baseURI, Map<String, Object> properties) {
+        this.initialState = new LocalClientState(URI.create(baseURI), properties);
     }
     
     public ThreadLocalClientState(String baseURI, long timeToKeepState) {
-        this.initialState = new LocalClientState(URI.create(baseURI));
+        this(baseURI, timeToKeepState, Collections.<String, Object>emptyMap());
+    }
+
+    public ThreadLocalClientState(String baseURI, long timeToKeepState, Map<String, Object> properties) {
+        this.initialState = new LocalClientState(URI.create(baseURI), properties);
         this.timeToKeepState = timeToKeepState;
     }
     
@@ -107,6 +115,16 @@
         return new ThreadLocalClientState(ls, timeToKeepState);
     }
     
+
+    @Override
+    public ClientState newState(URI currentURI,
+            MultivaluedMap<String, String> headers,
+            MultivaluedMap<String, String> templates,
+            Map<String, Object> properties) {
+        LocalClientState ls = (LocalClientState)getState().newState(currentURI, headers, templates, properties);
+        return new ThreadLocalClientState(ls, timeToKeepState);
+    }
+
     private void removeThreadLocalState(Thread t) {
         state.remove(t);
         if (checkpointMap != null) {
diff --git a/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/WebClient.java b/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/WebClient.java
index 4f8d1aa..312e589 100644
--- a/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/WebClient.java
+++ b/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/WebClient.java
@@ -86,11 +86,19 @@
     private static final String WEB_CLIENT_OPERATION_REPORTING = "enable.webclient.operation.reporting";
     private BodyWriter bodyWriter = new BodyWriter();
     protected WebClient(String baseAddress) {
-        this(convertStringToURI(baseAddress));
+        this(convertStringToURI(baseAddress), Collections.<String, Object>emptyMap());
+    }
+    
+    protected WebClient(String baseAddress, Map<String, Object> properties) {
+        this(convertStringToURI(baseAddress), properties);
     }
     
     protected WebClient(URI baseURI) {
-        this(new LocalClientState(baseURI));
+        this(baseURI, Collections.<String, Object>emptyMap());
+    }
+
+    protected WebClient(URI baseURI, Map<String, Object> properties) {
+        this(new LocalClientState(baseURI, properties));
     }
     
     protected WebClient(ClientState state) {
@@ -106,8 +114,17 @@
      * @param baseAddress baseAddress
      */
     public static WebClient create(String baseAddress) {
+        return create(baseAddress, Collections.<String, Object>emptyMap());
+    }
+
+    /**
+     * Creates WebClient
+     * @param baseAddress baseAddress
+     */
+    public static WebClient create(String baseAddress, Map<String, Object> properties) {
         JAXRSClientFactoryBean bean = new JAXRSClientFactoryBean();
         bean.setAddress(baseAddress);
+        bean.setProperties(properties);
         return bean.createWebClient();
     }
     
@@ -143,10 +160,23 @@
      * @param threadSafe if true ThreadLocalClientState is used
      */
     public static WebClient create(String baseAddress, List<?> providers, boolean threadSafe) {
+        return create(baseAddress, providers, Collections.<String, Object>emptyMap(), threadSafe);
+    }
+    
+    /**
+     * Creates WebClient
+     * @param baseAddress baseURI
+     * @param providers list of providers
+     * @param threadSafe if true ThreadLocalClientState is used
+     * @param properties additional properties
+     */
+    public static WebClient create(String baseAddress, List<?> providers, 
+            Map<String, Object> properties, boolean threadSafe) {
         JAXRSClientFactoryBean bean = getBean(baseAddress, null);
         bean.setProviders(providers);
+        bean.setProperties(properties);
         if (threadSafe) {
-            bean.setInitialState(new ThreadLocalClientState(baseAddress));
+            bean.setInitialState(new ThreadLocalClientState(baseAddress, properties));
         }
         return bean.createWebClient();        
     }
diff --git a/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/spec/ClientImpl.java b/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/spec/ClientImpl.java
index 85d5e62..5efb0b1 100644
--- a/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/spec/ClientImpl.java
+++ b/rt/rs/client/src/main/java/org/apache/cxf/jaxrs/client/spec/ClientImpl.java
@@ -345,6 +345,7 @@
             if (targetClient == null) {
                 JAXRSClientFactoryBean bean = new JAXRSClientFactoryBean();
                 bean.setAddress(uri.toString());
+                bean.setProperties(configProps);
                 Boolean threadSafe = getBooleanValue(configProps.get(THREAD_SAFE_CLIENT_PROP));
                 if (threadSafe == null) {
                     threadSafe = DEFAULT_THREAD_SAFETY_CLIENT_STATUS;
diff --git a/rt/rs/client/src/test/java/org/apache/cxf/jaxrs/client/spring/JAXRSClientFactoryBeanTest.java b/rt/rs/client/src/test/java/org/apache/cxf/jaxrs/client/spring/JAXRSClientFactoryBeanTest.java
index 2bdb107..2d9a28d 100644
--- a/rt/rs/client/src/test/java/org/apache/cxf/jaxrs/client/spring/JAXRSClientFactoryBeanTest.java
+++ b/rt/rs/client/src/test/java/org/apache/cxf/jaxrs/client/spring/JAXRSClientFactoryBeanTest.java
@@ -21,6 +21,7 @@
 import javax.xml.namespace.QName;
 
 import org.apache.cxf.BusFactory;
+import org.apache.cxf.jaxrs.client.Client;
 import org.apache.cxf.jaxrs.client.JAXRSClientFactoryBean;
 
 import org.junit.After;
@@ -29,6 +30,11 @@
 
 import org.springframework.context.support.ClassPathXmlApplicationContext;
 
+import static org.hamcrest.CoreMatchers.endsWith;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
 public class JAXRSClientFactoryBeanTest extends Assert {
 
     @After
@@ -64,8 +70,29 @@
         assertEquals("Get a wrong map size", cfb.getHeaders().size(), 1);
         assertEquals("Get a wrong username", cfb.getUsername(), "username");
         assertEquals("Get a wrong password", cfb.getPassword(), "password");
+        
+        bean = ctx.getBean("client2.proxyFactory");
+        assertNotNull(bean);
+        cfb = (JAXRSClientFactoryBean) bean;
+        assertNotNull(cfb.getProperties());
+        assertEquals("Get a wrong map size", cfb.getProperties().size(), 1);
+
         ctx.close();
-
     }
-
+    
+    @Test
+    public void testClientProperties() throws Exception {
+        try (ClassPathXmlApplicationContext ctx =
+                new ClassPathXmlApplicationContext(new String[] {"/org/apache/cxf/jaxrs/client/spring/clients.xml"})) {
+            Client bean = (Client) ctx.getBean("client2");
+            assertNotNull(bean);
+            assertThat(bean.query("list", "1").query("list", "2").getCurrentURI().toString(),
+                endsWith("?list=1,2"));
+            
+            bean = (Client) ctx.getBean("client1");
+            assertNotNull(bean);
+            assertThat(bean.query("list", "1").query("list", "2").getCurrentURI().toString(),
+                endsWith("?list=1&list=2"));
+        }
+    }
 }
diff --git a/rt/rs/client/src/test/java/org/apache/cxf/jaxrs/client/spring/clients.xml b/rt/rs/client/src/test/java/org/apache/cxf/jaxrs/client/spring/clients.xml
index 74ae059..6815916 100644
--- a/rt/rs/client/src/test/java/org/apache/cxf/jaxrs/client/spring/clients.xml
+++ b/rt/rs/client/src/test/java/org/apache/cxf/jaxrs/client/spring/clients.xml
@@ -45,4 +45,9 @@
             <entry key="Accept" value="text/xml"/>
         </jaxrs:headers>
     </jaxrs:client>
-</beans>
+    <jaxrs:client id="client2" serviceClass="org.apache.cxf.jaxrs.resources.BookStore" address="http://localhost:9000/foo">
+        <jaxrs:properties>
+            <entry key="expand.query.value.as.collection" value="true" />
+        </jaxrs:properties>
+    </jaxrs:client>
+</beans>
\ No newline at end of file
diff --git a/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/BookStore.java b/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/BookStore.java
index 5362094..89fea53 100644
--- a/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/BookStore.java
+++ b/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/BookStore.java
@@ -184,9 +184,13 @@
         String dStr = dBuilder.toString();
         if (!lStr.equals(dStr)) {
             throw new InternalServerErrorException();
+        } else if ("".equalsIgnoreCase(lStr)) {
+            lStr = "0";
         }
+        
         return new Book("cxf", Long.parseLong(lStr));
     }
+    
     @GET
     @Path("/")
     public Book getBookRoot() {
@@ -395,6 +399,11 @@
         return new BookStoreSub(this);
     }
     
+    @Path("/querysub")
+    public BookStoreQuerySub getQuerySub() {
+        return new BookStoreQuerySub();
+    }
+
     @GET
     @Path("/twoBeanParams/{id}")
     @Produces("application/xml")
@@ -2188,6 +2197,26 @@
             return bookStore.getBeanParamBook(bean);
         }
     }
+    
+    public static class BookStoreQuerySub {
+        @GET
+        @Path("/listofstrings")
+        @Produces("text/xml")
+        public Book getBookFromListStrings(@QueryParam("value") List<String> value) {
+            final StringBuilder builder = new StringBuilder();
+            
+            for (String v : value) {
+                if (builder.length() > 0) {
+                    builder.append(' ');
+                }
+                
+                builder.append(v);
+            }
+            
+            return new Book(builder.toString(), 0L);
+        }
+    }
+
 }
 
 
diff --git a/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/JAXRSClientServerQueryParamBookTest.java b/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/JAXRSClientServerQueryParamBookTest.java
new file mode 100644
index 0000000..42faaff
--- /dev/null
+++ b/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/JAXRSClientServerQueryParamBookTest.java
@@ -0,0 +1,234 @@
+/**
+ * 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.cxf.systest.jaxrs;
+
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Response;
+
+import org.apache.cxf.jaxrs.client.JAXRSClientFactory;
+import org.apache.cxf.jaxrs.client.WebClient;
+import org.apache.cxf.jaxrs.ext.xml.XMLSource;
+import org.apache.cxf.jaxrs.model.AbstractResourceInfo;
+import org.apache.cxf.testutil.common.AbstractBusClientServerTestBase;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.utils.URIBuilder;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+import static org.hamcrest.CoreMatchers.endsWith;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(Parameterized.class)
+public class JAXRSClientServerQueryParamBookTest extends AbstractBusClientServerTestBase {
+    public static final String PORT = BookServer.PORT;
+    private final Boolean threadSafe;
+    
+    public JAXRSClientServerQueryParamBookTest(Boolean threadSafe) {
+        this.threadSafe = threadSafe;
+    }
+    
+    @Parameters(name = "Client is thread safe = {0}")
+    public static Collection<Boolean> data() {
+        return Arrays.asList(new Boolean[] {null, true, false});
+    }
+
+    @BeforeClass
+    public static void startServers() throws Exception {
+        AbstractResourceInfo.clearAllMaps();
+        assertTrue("server did not launch correctly",
+                launchServer(BookServer.class, true));
+        createStaticBus();
+    }
+
+    @Test
+    public void testListOfLongAndDoubleQuery() throws Exception {
+        BookStore client = createClient();
+        Book book = client.getBookFromListOfLongAndDouble(Arrays.<Long>asList(1L, 2L, 3L), Arrays.<Double>asList());
+        assertEquals(123L, book.getId());
+    }
+    
+    @Test
+    public void testListOfLongAndDoubleQueryWebClient() throws Exception {
+        WebClient wc = createWebClient();
+        
+        Response r = wc
+                .path("/bookstore/listoflonganddouble")
+                .query("value", Arrays.asList(1L, 2L, 3L))
+                .accept("text/xml")
+                .get();
+
+        assertThat(wc.getCurrentURI().toString(), endsWith("value=1&value=2&value=3"));
+        try (InputStream is = (InputStream)r.getEntity()) {
+            XMLSource source = new XMLSource(is);
+            assertEquals(123L, Long.parseLong(source.getValue("Book/id")));
+        }
+    }
+
+    @Test
+    public void testListOfLongAndDoubleQueryAsManyWebClient() throws Exception {
+        WebClient wc = createWebClient();
+        
+        Response r = wc
+                .path("/bookstore/listoflonganddouble")
+                .query("value", "1")
+                .query("value", "2")
+                .query("value", "3")
+                .accept("text/xml")
+                .get();
+
+        assertThat(wc.getCurrentURI().toString(), endsWith("value=1&value=2&value=3"));
+        try (InputStream is = (InputStream)r.getEntity()) {
+            XMLSource source = new XMLSource(is);
+            assertEquals(123L, Long.parseLong(source.getValue("Book/id")));
+        }
+    }
+    
+    @Test
+    public void testListOfLongAndDoubleQueryAsString() throws Exception {
+        final URIBuilder builder = new URIBuilder("http://localhost:" + PORT + "/bookstore/listoflonganddouble");
+        builder.setCustomQuery("value=1,2,3");
+
+        final CloseableHttpClient client = HttpClientBuilder.create().build();
+        HttpGet get = new HttpGet(builder.build());
+        get.addHeader("Accept", "text/xml");
+
+        try (CloseableHttpResponse response = client.execute(get)) {
+            // should not succeed since "parse.query.value.as.collection" contextual property is not set
+            assertEquals(404, response.getStatusLine().getStatusCode());
+        }
+    }
+    
+    @Test
+    public void testListOfLongAndDoubleQueryEmptyWebClient() throws Exception {
+        WebClient wc = createWebClient();
+        
+        Response r = wc
+                .path("/bookstore/listoflonganddouble")
+                .query("value", "")
+                .accept("text/xml")
+                .get();
+
+        assertThat(wc.getCurrentURI().toString(), endsWith("value="));
+        try (InputStream is = (InputStream)r.getEntity()) {
+            XMLSource source = new XMLSource(is);
+            assertEquals(0L, Long.parseLong(source.getValue("Book/id")));
+        }
+    }
+    
+    @Test
+    public void testListOfLongAndDoubleQueryEmpty() throws Exception {
+        BookStore client = createClient();
+        Book book = client.getBookFromListOfLongAndDouble(Arrays.<Long>asList(), Arrays.<Double>asList());
+        assertEquals(0L, book.getId());
+    }
+
+    @Test
+    public void testListOfStringsWebClient() throws Exception {
+        WebClient wc = createWebClient();
+        
+        Response r = wc
+                .path("/bookstore/querysub/listofstrings")
+                .query("value", "this is")
+                .query("value", "the book")
+                .query("value", "title")
+                .accept("text/xml")
+                .get();
+
+        assertThat(wc.getCurrentURI().toString(), endsWith("value=this+is&value=the+book&value=title"));
+        try (InputStream is = (InputStream)r.getEntity()) {
+            XMLSource source = new XMLSource(is);
+            assertEquals("this is the book title", source.getValue("Book/name"));
+        }
+    }
+    
+    @Test
+    public void testListOfStringsJaxrsClient() throws Exception {
+        WebTarget client = createJaxrsClient();
+        
+        Response r = client
+                .path("/bookstore/querysub/listofstrings")
+                .queryParam("value", "this is")
+                .queryParam("value", "the book")
+                .queryParam("value", "title")
+                .request()
+                .accept("text/xml")
+                .get();
+
+        try (InputStream is = (InputStream)r.getEntity()) {
+            XMLSource source = new XMLSource(is);
+            assertEquals("this is the book title", source.getValue("Book/name"));
+        }
+    }
+
+    @Test
+    public void testListOfStrings() throws Exception {
+        BookStore client = createClient();
+        
+        Book book = client.getQuerySub().getBookFromListStrings(
+            Arrays.asList("this is", "the book", "title"));
+
+        assertEquals("this is the book title", book.getName());
+    }
+    
+    private WebClient createWebClient() {
+        if (threadSafe == null) {
+            return WebClient.create("http://localhost:" + PORT);
+        } else {
+            return WebClient.create("http://localhost:" + PORT, Collections.emptyList(), threadSafe);
+        }
+    }
+    
+    private BookStore createClient() {
+        if (threadSafe == null) {
+            return JAXRSClientFactory.create("http://localhost:" + PORT, BookStore.class);
+        } else {
+            return JAXRSClientFactory.create("http://localhost:" + PORT, BookStore.class, 
+                Collections.emptyList(), threadSafe);
+        }
+    }
+    
+    private WebTarget createJaxrsClient() {
+        if (threadSafe == null) {
+            return ClientBuilder
+                .newClient()
+                .target("http://localhost:" + PORT);
+        } else {
+            return ClientBuilder
+                .newClient()
+                .property("thread.safe.client", threadSafe)
+                .target("http://localhost:" + PORT);
+        }
+    }
+}
\ No newline at end of file
diff --git a/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/JAXRSClientServerQueryParamCollectionBookTest.java b/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/JAXRSClientServerQueryParamCollectionBookTest.java
new file mode 100644
index 0000000..0a8e6f4
--- /dev/null
+++ b/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/JAXRSClientServerQueryParamCollectionBookTest.java
@@ -0,0 +1,244 @@
+/**
+ * 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.cxf.systest.jaxrs;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Response;
+
+import org.apache.cxf.jaxrs.client.JAXRSClientFactory;
+import org.apache.cxf.jaxrs.client.WebClient;
+import org.apache.cxf.jaxrs.ext.xml.XMLSource;
+import org.apache.cxf.jaxrs.model.AbstractResourceInfo;
+import org.apache.cxf.testutil.common.AbstractBusClientServerTestBase;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.utils.URIBuilder;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.util.EntityUtils;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+import static org.hamcrest.CoreMatchers.endsWith;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(Parameterized.class)
+public class JAXRSClientServerQueryParamCollectionBookTest extends AbstractBusClientServerTestBase {
+    public static final String PORT = BookServer.PORT;
+    private final Boolean threadSafe;
+    
+    public JAXRSClientServerQueryParamCollectionBookTest(Boolean threadSafe) {
+        this.threadSafe = threadSafe;
+    }
+
+    @Parameters(name = "Client is thread safe = {0}")
+    public static Collection<Boolean> data() {
+        return Arrays.asList(new Boolean[] {null, true, false});
+    }
+    
+    @BeforeClass
+    public static void startServers() throws Exception {
+        AbstractResourceInfo.clearAllMaps();
+        assertTrue("server did not launch correctly", 
+            launchServer(new BookServer(Collections.singletonMap("parse.query.value.as.collection", "true"))));
+        createStaticBus();
+    }
+
+    @Test
+    public void testListOfLongAndDoubleQuery() throws Exception {
+        BookStore client = createClient();
+        Book book = client.getBookFromListOfLongAndDouble(Arrays.<Long>asList(1L, 2L, 3L), Arrays.<Double>asList());
+        assertEquals(123L, book.getId());
+    }
+
+    @Test
+    public void testListOfLongAndDoubleQueryEmpty() throws Exception {
+        BookStore client = createClient();
+        Book book = client.getBookFromListOfLongAndDouble(Arrays.<Long>asList(), Arrays.<Double>asList());
+        assertEquals(0L, book.getId());
+    }
+
+    @Test
+    public void testListOfLongAndDoubleQueryWebClient() throws Exception {
+        WebClient wc = createWebClient();
+        
+        Response r = wc
+            .path("/bookstore/listoflonganddouble")
+            .query("value", Arrays.asList(1L, 2L, 3L))
+            .accept("text/xml")
+            .get();
+
+        assertThat(wc.getCurrentURI().toString(), endsWith("value=1,2,3"));
+        try (InputStream is = (InputStream)r.getEntity()) {
+            XMLSource source = new XMLSource(is);
+            assertEquals(123L, Long.parseLong(source.getValue("Book/id")));
+        }
+    }
+
+    @Test
+    public void testListOfLongAndDoubleQueryAsManyWebClient() throws Exception {
+        WebClient wc = createWebClient();
+        
+        Response r = wc
+                .path("/bookstore/listoflonganddouble")
+                .query("value", "1")
+                .query("value", "2")
+                .query("value", "3")
+                .accept("text/xml")
+                .get();
+
+        assertThat(wc.getCurrentURI().toString(), endsWith("value=1,2,3"));
+        try (InputStream is = (InputStream)r.getEntity()) {
+            XMLSource source = new XMLSource(is);
+            assertEquals(123L, Long.parseLong(source.getValue("Book/id")));
+        }
+    }
+    
+    @Test
+    public void testListOfStringsWebClient() throws Exception {
+        WebClient wc = createWebClient();
+        
+        Response r = wc
+                .path("/bookstore/querysub/listofstrings")
+                .query("value", "this is")
+                .query("value", "the book")
+                .query("value", "title")
+                .accept("text/xml")
+                .get();
+
+        assertThat(wc.getCurrentURI().toString(), endsWith("value=this+is,the+book,title"));
+        try (InputStream is = (InputStream)r.getEntity()) {
+            XMLSource source = new XMLSource(is);
+            assertEquals("this is the book title", source.getValue("Book/name"));
+        }
+    }
+
+    @Test
+    public void testListOfStringsJaxrsClient() throws Exception {
+        WebTarget client = createJaxrsClient();
+        
+        Response r = client
+                .path("/bookstore/querysub/listofstrings")
+                .queryParam("value", "this is")
+                .queryParam("value", "the book")
+                .queryParam("value", "title")
+                .request()
+                .accept("text/xml")
+                .get();
+
+        try (InputStream is = (InputStream)r.getEntity()) {
+            XMLSource source = new XMLSource(is);
+            assertEquals("this is the book title", source.getValue("Book/name"));
+        }
+    }
+
+    @Test
+    public void testListOfStrings() throws Exception {
+        BookStore bookStore = createClient();
+        
+        Book book = bookStore.getQuerySub().getBookFromListStrings(
+            Arrays.asList("this is", "the book", "title"));
+
+        assertEquals("this is the book title", book.getName());
+    }
+
+    @Test
+    public void testListOfLongAndDoubleQueryEmptyWebClient() throws Exception {
+        WebClient wc = createWebClient();
+        
+        Response r = wc
+                .path("/bookstore/listoflonganddouble")
+                .query("value", "")
+                .accept("text/xml")
+                .get();
+
+        assertThat(wc.getCurrentURI().toString(), endsWith("value="));
+        try (InputStream is = (InputStream)r.getEntity()) {
+            XMLSource source = new XMLSource(is);
+            assertEquals(0L, Long.parseLong(source.getValue("Book/id")));
+        }
+    }
+    
+    @Test
+    public void testListOfLongAndDoubleQueryAsString() throws Exception {
+        final URIBuilder builder = new URIBuilder("http://localhost:" + PORT + "/bookstore/listoflonganddouble");
+        builder.setCustomQuery("value=1,2,3");
+
+        final CloseableHttpClient client = HttpClientBuilder.create().build();
+        HttpGet get = new HttpGet(builder.build());
+        get.addHeader("Accept", "text/xml");
+
+        try (CloseableHttpResponse response = client.execute(get)) {
+            final byte[] content = EntityUtils.toByteArray(response.getEntity());
+            try (InputStream is = new ByteArrayInputStream(content)) {
+                XMLSource source = new XMLSource(is);
+                assertEquals(123L, Long.parseLong(source.getValue("Book/id")));
+            }
+        }
+    }
+    
+    private WebClient createWebClient() {
+        if (threadSafe == null) {
+            return WebClient.create("http://localhost:" + PORT, 
+                Collections.<String, Object>singletonMap("expand.query.value.as.collection", "true"));
+        } else {
+            return WebClient.create("http://localhost:" + PORT, Collections.emptyList(),
+                Collections.<String, Object>singletonMap("expand.query.value.as.collection", "true"), true);
+        }
+    }
+    
+    private BookStore createClient() {
+        if (threadSafe == null) {
+            return JAXRSClientFactory.create("http://localhost:" + PORT, BookStore.class,
+                Collections.<String, Object>singletonMap("expand.query.value.as.collection", "true"));
+        } else {
+            return JAXRSClientFactory.create("http://localhost:" + PORT, BookStore.class, Collections.emptyList(),
+                Collections.<String, Object>singletonMap("expand.query.value.as.collection", "true"), threadSafe);
+        }
+    }
+    
+    private WebTarget createJaxrsClient() {
+        if (threadSafe == null) {
+            return ClientBuilder
+                .newClient()
+                .property("expand.query.value.as.collection", "true")
+                .target("http://localhost:" + PORT);
+        } else {
+            return ClientBuilder
+                .newClient()
+                .property("expand.query.value.as.collection", "true")
+                .property("thread.safe.client", threadSafe)
+                .target("http://localhost:" + PORT);
+        }
+    }
+}
\ No newline at end of file