cxf-rt-rs-client changes since 3.4.3
diff --git a/transform/src/patch/java/org/apache/cxf/jaxrs/client/ClientProxyImpl.java b/transform/src/patch/java/org/apache/cxf/jaxrs/client/ClientProxyImpl.java
new file mode 100644
index 0000000..8937909
--- /dev/null
+++ b/transform/src/patch/java/org/apache/cxf/jaxrs/client/ClientProxyImpl.java
@@ -0,0 +1,1149 @@
+/**
+ * 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.jaxrs.client;
+
+import java.io.Closeable;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Type;
+import java.net.URI;
+import java.security.AccessController;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.ResourceBundle;
+import java.util.function.Predicate;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+import javax.ws.rs.BeanParam;
+import javax.ws.rs.CookieParam;
+import javax.ws.rs.FormParam;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.MatrixParam;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.ProcessingException;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.client.InvocationCallback;
+import javax.ws.rs.container.AsyncResponse;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+
+import org.apache.cxf.Bus;
+import org.apache.cxf.BusFactory;
+import org.apache.cxf.common.classloader.ClassLoaderUtils;
+import org.apache.cxf.common.classloader.ClassLoaderUtils.ClassLoaderHolder;
+import org.apache.cxf.common.i18n.BundleUtils;
+import org.apache.cxf.common.logging.LogUtils;
+import org.apache.cxf.common.util.PrimitiveUtils;
+import org.apache.cxf.common.util.PropertyUtils;
+import org.apache.cxf.common.util.ReflectionUtil;
+import org.apache.cxf.common.util.StringUtils;
+import org.apache.cxf.endpoint.Endpoint;
+import org.apache.cxf.helpers.CastUtils;
+import org.apache.cxf.interceptor.Fault;
+import org.apache.cxf.interceptor.InterceptorProvider;
+import org.apache.cxf.jaxrs.ext.multipart.Attachment;
+import org.apache.cxf.jaxrs.ext.multipart.Multipart;
+import org.apache.cxf.jaxrs.impl.MetadataMap;
+import org.apache.cxf.jaxrs.impl.ResponseImpl;
+import org.apache.cxf.jaxrs.model.ClassResourceInfo;
+import org.apache.cxf.jaxrs.model.OperationResourceInfo;
+import org.apache.cxf.jaxrs.model.Parameter;
+import org.apache.cxf.jaxrs.model.ParameterType;
+import org.apache.cxf.jaxrs.utils.AnnotationUtils;
+import org.apache.cxf.jaxrs.utils.FormUtils;
+import org.apache.cxf.jaxrs.utils.InjectionUtils;
+import org.apache.cxf.jaxrs.utils.JAXRSUtils;
+import org.apache.cxf.message.Exchange;
+import org.apache.cxf.message.Message;
+
+/**
+ * Proxy-based client implementation
+ *
+ */
+public class ClientProxyImpl extends AbstractClient implements
+    InvocationHandlerAware, InvocationHandler, Closeable {
+
+    protected static final Logger LOG = LogUtils.getL7dLogger(ClientProxyImpl.class);
+    protected static final ResourceBundle BUNDLE = BundleUtils.getBundle(ClientProxyImpl.class);
+    protected static final String SLASH = "/";
+    protected static final String BUFFER_PROXY_RESPONSE = "buffer.proxy.response";
+    protected static final String PROXY_METHOD_PARAM_BODY_INDEX = "proxy.method.parameter.body.index";
+
+    protected ClassResourceInfo cri;
+    protected ClassLoader proxyLoader;
+    protected boolean inheritHeaders;
+    protected boolean isRoot;
+    protected Map<String, Object> valuesMap = Collections.emptyMap();
+    protected BodyWriter bodyWriter = new BodyWriter();
+    protected Client proxy;
+    public ClientProxyImpl(URI baseURI,
+                           ClassLoader loader,
+                           ClassResourceInfo cri,
+                           boolean isRoot,
+                           boolean inheritHeaders,
+                           Object... varValues) {
+        this(baseURI, loader, cri, isRoot, inheritHeaders, Collections.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,
+                           ClassLoader loader,
+                           ClassResourceInfo cri,
+                           boolean isRoot,
+                           boolean inheritHeaders,
+                           Object... varValues) {
+        super(initialState);
+        this.proxyLoader = loader;
+        this.cri = cri;
+        this.isRoot = isRoot;
+        this.inheritHeaders = inheritHeaders;
+        initValuesMap(varValues);
+        cfg.getInInterceptors().add(new ClientAsyncResponseInterceptor());
+    }
+
+    void setProxyClient(Client client) {
+        this.proxy = client;
+    }
+
+    private void initValuesMap(Object... varValues) {
+        if (isRoot) {
+            List<String> vars = cri.getURITemplate().getVariables();
+            valuesMap = new LinkedHashMap<>();
+            for (int i = 0; i < vars.size(); i++) {
+                if (varValues.length > 0) {
+                    if (i < varValues.length) {
+                        valuesMap.put(vars.get(i), varValues[i]);
+                    } else {
+                        org.apache.cxf.common.i18n.Message msg = new org.apache.cxf.common.i18n.Message(
+                             "ROOT_VARS_MISMATCH", BUNDLE, vars.size(), varValues.length);
+                        LOG.info(msg.toString());
+                        break;
+                    }
+                } else {
+                    valuesMap.put(vars.get(i), "");
+                }
+            }
+        }
+    }
+
+    private static class WrappedException extends Exception {
+        private static final long serialVersionUID = 1183890106889852917L;
+
+        final Throwable wrapped;
+        WrappedException(Throwable wrapped) {
+            this.wrapped = wrapped;
+        }
+        Throwable getWrapped() {
+            return wrapped;
+        }
+    }
+
+    private static Object invokeDefaultMethod(Class<?> declaringClass, Object o, Method m, Object[] params)
+        throws Throwable {
+
+        try {
+            return AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
+                @Override
+                public Object run() throws Exception {
+                    try {
+                        final MethodHandles.Lookup lookup = MethodHandles
+                                .publicLookup()
+                                .in(declaringClass);
+                        // force private access so unreflectSpecial can invoke the interface's default method
+                        Field f;
+                        try { 
+                            f = MethodHandles.Lookup.class.getDeclaredField("allowedModes");
+                        } catch (NoSuchFieldException nsfe) {
+                            // IBM and OpenJ9 JDKs use a different field name
+                            f = MethodHandles.Lookup.class.getDeclaredField("accessMode");
+                            m.setAccessible(true);
+                        }
+                        final int modifiers = f.getModifiers();
+                        if (Modifier.isFinal(modifiers)) {
+                            final Field modifiersField = Field.class.getDeclaredField("modifiers");
+                            modifiersField.setAccessible(true);
+                            modifiersField.setInt(f, modifiers & ~Modifier.FINAL);
+                            f.setAccessible(true);
+                            f.set(lookup, MethodHandles.Lookup.PRIVATE);
+                        }
+                        MethodHandle mh = lookup.unreflectSpecial(m, declaringClass).bindTo(o);
+                        return params != null && params.length > 0 ? mh.invokeWithArguments(params) : mh.invoke();
+                    } catch (Throwable t) {
+                        try { // try using built-in JDK 9+ API for invoking default method
+                            return invokeDefaultMethodUsingPrivateLookup(declaringClass, o, m, params);
+                        } catch (final NoSuchMethodException ex) {
+                            throw new WrappedException(t);
+                        }
+                    }
+                }
+            });
+        } catch (PrivilegedActionException pae) {
+            Throwable wrapped = pae.getCause();
+            if (wrapped instanceof WrappedException) {
+                throw ((WrappedException)wrapped).getWrapped();
+            }
+            throw wrapped;
+        }
+    }
+
+    /**
+     * For JDK 9+, we could use MethodHandles.privateLookupIn, which is not 
+     * available in JDK 8.
+     */
+    private static Object invokeDefaultMethodUsingPrivateLookup(Class<?> declaringClass, Object o, Method m, 
+            Object[] params) throws WrappedException, NoSuchMethodException {
+        try {
+            final Method privateLookup = MethodHandles
+                .class
+                .getDeclaredMethod("privateLookupIn", Class.class, MethodHandles.Lookup.class);
+            
+            return ((MethodHandles.Lookup)privateLookup
+                .invoke(null, declaringClass, MethodHandles.lookup()))
+                .unreflectSpecial(m, declaringClass)
+                .bindTo(o)
+                .invokeWithArguments(params);
+        } catch (NoSuchMethodException t) {
+            throw t;
+        } catch (Throwable t) {
+            throw new WrappedException(t);
+        }
+    }
+
+    /**
+     * Updates the current state if Client method is invoked, otherwise
+     * does the remote invocation or returns a new proxy if subresource
+     * method is invoked. Can throw an expected exception if ResponseExceptionMapper
+     * is registered
+     */
+    @Override
+    public Object invoke(Object o, Method m, Object[] params) throws Throwable {
+        checkClosed();
+        Class<?> declaringClass = m.getDeclaringClass();
+        if (Client.class == declaringClass || InvocationHandlerAware.class == declaringClass
+            || Object.class == declaringClass || Closeable.class == declaringClass
+            || AutoCloseable.class == declaringClass) {
+            return m.invoke(this, params);
+        }
+        resetResponse();
+        OperationResourceInfo ori = cri.getMethodDispatcher().getOperationResourceInfo(m);
+        if (ori == null) {
+            if (m.isDefault()) {
+                return invokeDefaultMethod(declaringClass, o, m, params);
+            }
+            reportInvalidResourceMethod(m, "INVALID_RESOURCE_METHOD");
+        }
+
+        MultivaluedMap<ParameterType, Parameter> types = getParametersInfo(m, params, ori);
+        List<Parameter> beanParamsList = getParameters(types, ParameterType.BEAN);
+
+        int bodyIndex = getBodyIndex(types, ori);
+
+        List<Object> pathParams = getPathParamValues(m, params, types, beanParamsList, ori, bodyIndex);
+
+        UriBuilder builder = getCurrentBuilder().clone();
+        if (isRoot) {
+            addNonEmptyPath(builder, ori.getClassResourceInfo().getURITemplate().getValue());
+        }
+        addNonEmptyPath(builder, ori.getURITemplate().getValue());
+
+        handleMatrixes(m, params, types, beanParamsList, builder);
+        handleQueries(m, params, types, beanParamsList, builder);
+
+        URI uri = builder.buildFromEncoded(pathParams.toArray()).normalize();
+
+        MultivaluedMap<String, String> headers = getHeaders();
+        MultivaluedMap<String, String> paramHeaders = new MetadataMap<>();
+        handleHeaders(m, params, paramHeaders, beanParamsList, types);
+        handleCookies(m, params, paramHeaders, beanParamsList, types);
+
+        if (ori.isSubResourceLocator()) {
+            ClassResourceInfo subCri = cri.getSubResource(m.getReturnType(), m.getReturnType());
+            if (subCri == null) {
+                reportInvalidResourceMethod(m, "INVALID_SUBRESOURCE");
+            }
+
+            MultivaluedMap<String, String> subHeaders = paramHeaders;
+            if (inheritHeaders) {
+                subHeaders.putAll(headers);
+            }
+
+            ClientState newState = getState().newState(uri, subHeaders,
+                 getTemplateParametersMap(ori.getURITemplate(), pathParams));
+            ClientProxyImpl proxyImpl =
+                new ClientProxyImpl(newState, proxyLoader, subCri, false, inheritHeaders);
+            proxyImpl.setConfiguration(getConfiguration());
+            return JAXRSClientFactory.createProxy(m.getReturnType(), proxyLoader, proxyImpl);
+        }
+        headers.putAll(paramHeaders);
+
+        getState().setTemplates(getTemplateParametersMap(ori.getURITemplate(), pathParams));
+
+        Object body = null;
+        if (bodyIndex != -1) {
+            body = params[bodyIndex];
+            if (body == null) {
+                bodyIndex = -1;
+            }
+        } else if (types.containsKey(ParameterType.FORM))  {
+            body = handleForm(m, params, types, beanParamsList);
+        } else if (types.containsKey(ParameterType.REQUEST_BODY))  {
+            body = handleMultipart(types, ori, params);
+        } else if (hasFormParams(params, beanParamsList)) {
+            body = handleForm(m, params, types, beanParamsList);
+        }
+        
+        setRequestHeaders(headers, ori, types.containsKey(ParameterType.FORM),
+            body == null ? null : body.getClass(), m.getReturnType());
+
+        try {
+            return doChainedInvocation(uri, headers, ori, params, body, bodyIndex, null, null);
+        } finally {
+            resetResponseStateImmediatelyIfNeeded();
+        }
+
+    }
+
+    protected void addNonEmptyPath(UriBuilder builder, String pathValue) {
+        if (!SLASH.equals(pathValue)) {
+            builder.path(pathValue);
+        }
+    }
+
+    protected MultivaluedMap<ParameterType, Parameter> getParametersInfo(Method m,
+        Object[] params, OperationResourceInfo ori) {
+        MultivaluedMap<ParameterType, Parameter> map = new MetadataMap<>();
+
+        List<Parameter> parameters = ori.getParameters();
+        if (parameters.isEmpty()) {
+            return map;
+        }
+        int requestBodyParam = 0;
+        int multipartParam = 0;
+        for (Parameter p : parameters) {
+            if (isIgnorableParameter(m, p)) {
+                continue;
+            }
+            if (p.getType() == ParameterType.REQUEST_BODY) {
+                requestBodyParam++;
+                if (getMultipart(ori, p.getIndex()) != null) {
+                    multipartParam++;
+                }
+            }
+            map.add(p.getType(), p);
+        }
+
+        if (map.containsKey(ParameterType.REQUEST_BODY)) {
+            if (requestBodyParam > 1 && requestBodyParam != multipartParam) {
+                reportInvalidResourceMethod(ori.getMethodToInvoke(), "SINGLE_BODY_ONLY");
+            }
+            if (map.containsKey(ParameterType.FORM)) {
+                reportInvalidResourceMethod(ori.getMethodToInvoke(), "ONLY_FORM_ALLOWED");
+            }
+        }
+        return map;
+    }
+
+    protected boolean isIgnorableParameter(Method m, Parameter p) {
+        if (p.getType() == ParameterType.CONTEXT) {
+            return true;
+        }
+        return p.getType() == ParameterType.REQUEST_BODY
+            && m.getParameterTypes()[p.getIndex()] == AsyncResponse.class;
+    }
+
+    protected static int getBodyIndex(MultivaluedMap<ParameterType, Parameter> map,
+                                    OperationResourceInfo ori) {
+        List<Parameter> list = map.get(ParameterType.REQUEST_BODY);
+        int index = list == null || list.size() > 1 ? -1 : list.get(0).getIndex();
+        if (ori.isSubResourceLocator() && index != -1) {
+            reportInvalidResourceMethod(ori.getMethodToInvoke(), "NO_BODY_IN_SUBRESOURCE");
+        }
+        return index;
+    }
+
+    protected void checkResponse(Method m, Response r, Message inMessage) throws Throwable {
+        Throwable t = null;
+        int status = r.getStatus();
+
+        if (status >= 300) {
+            Class<?>[] exTypes = m.getExceptionTypes();
+            if (exTypes.length == 0) {
+                exTypes = new Class<?>[]{WebApplicationException.class};
+            }
+            for (Class<?> exType : exTypes) {
+                ResponseExceptionMapper<?> mapper = findExceptionMapper(inMessage, exType);
+                if (mapper != null) {
+                    t = mapper.fromResponse(r);
+                    if (t != null) {
+                        throw t;
+                    }
+                }
+            }
+
+            if ((t == null) && (m.getReturnType() == Response.class) && (m.getExceptionTypes().length == 0)) {
+                return;
+            }
+
+            t = convertToWebApplicationException(r);
+
+            if (inMessage.getExchange().get(Message.RESPONSE_CODE) == null) {
+                throw t;
+            }
+
+            Endpoint ep = inMessage.getExchange().getEndpoint();
+            inMessage.getExchange().put(InterceptorProvider.class, getConfiguration());
+            inMessage.setContent(Exception.class, new Fault(t));
+            inMessage.getInterceptorChain().abort();
+            if (ep.getInFaultObserver() != null) {
+                ep.getInFaultObserver().onMessage(inMessage);
+            }
+
+            throw t;
+
+        }
+    }
+
+    protected static ResponseExceptionMapper<?> findExceptionMapper(Message message, Class<?> exType) {
+        ClientProviderFactory pf = ClientProviderFactory.getInstance(message);
+        return pf.createResponseExceptionMapper(message, exType);
+    }
+
+    protected MultivaluedMap<String, String> setRequestHeaders(MultivaluedMap<String, String> headers,
+                                                             OperationResourceInfo ori,
+                                                             boolean formParams,
+                                                             Class<?> bodyClass,
+                                                             Class<?> responseClass) {
+        if (headers.getFirst(HttpHeaders.CONTENT_TYPE) == null) {
+            if (formParams || bodyClass != null && MultivaluedMap.class.isAssignableFrom(bodyClass)) {
+                headers.putSingle(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED);
+            } else {
+                String ctType = null;
+                List<MediaType> consumeTypes = ori.getConsumeTypes();
+                if (!consumeTypes.isEmpty() && !consumeTypes.get(0).equals(MediaType.WILDCARD_TYPE)) {
+                    ctType = JAXRSUtils.mediaTypeToString(ori.getConsumeTypes().get(0));
+                }
+                if (ctType != null) {
+                    headers.putSingle(HttpHeaders.CONTENT_TYPE, ctType);
+                }
+            }
+        }
+
+        List<MediaType> accepts = getAccept(headers);
+        if (accepts == null) {
+            if (responseClass == Void.class || responseClass == Void.TYPE) {
+                accepts = Collections.singletonList(MediaType.WILDCARD_TYPE);
+            } else {
+                List<MediaType> produceTypes = ori.getProduceTypes();
+                boolean produceWildcard = produceTypes.isEmpty()
+                    || produceTypes.get(0).equals(MediaType.WILDCARD_TYPE);
+                if (produceWildcard) {
+                    accepts = InjectionUtils.isPrimitive(responseClass)
+                        ? Collections.singletonList(MediaType.TEXT_PLAIN_TYPE)
+                        : Collections.singletonList(MediaType.APPLICATION_XML_TYPE);
+                } else {
+                    accepts = produceTypes;
+                }
+            }
+
+            for (MediaType mt : accepts) {
+                headers.add(HttpHeaders.ACCEPT, JAXRSUtils.mediaTypeToString(mt));
+            }
+        }
+
+        return headers;
+    }
+
+    protected List<MediaType> getAccept(MultivaluedMap<String, String> allHeaders) {
+        List<String> headers = allHeaders.get(HttpHeaders.ACCEPT);
+        if (headers == null || headers.isEmpty()) {
+            return null;
+        }
+        return headers.stream().
+                flatMap(header -> JAXRSUtils.parseMediaTypes(header).stream()).collect(Collectors.toList());
+    }
+
+    protected List<Object> getPathParamValues(Method m,
+                                            Object[] params,
+                                            MultivaluedMap<ParameterType, Parameter> map,
+                                            List<Parameter> beanParams,
+                                            OperationResourceInfo ori,
+                                            int bodyIndex) {
+        List<Object> list = new ArrayList<>();
+
+        List<String> methodVars = ori.getURITemplate().getVariables();
+        List<Parameter> paramsList = getParameters(map, ParameterType.PATH);
+        Map<String, BeanPair> beanParamValues = new HashMap<>(beanParams.size());
+        beanParams.forEach(p -> {
+            beanParamValues.putAll(getValuesFromBeanParam(params[p.getIndex()], PathParam.class));
+        });
+        if (!beanParamValues.isEmpty() && !methodVars.containsAll(beanParamValues.keySet())) {
+            List<String> classVars = ori.getClassResourceInfo().getURITemplate().getVariables();
+            classVars.forEach(classVar -> {
+                BeanPair pair = beanParamValues.get(classVar);
+                if (pair != null) {
+                    Object paramValue = convertParamValue(pair.getValue(), pair.getAnns());
+                    if (isRoot) {
+                        valuesMap.put(classVar, paramValue);
+                    } else {
+                        list.add(paramValue);
+                    }
+                }
+            });
+        }
+        if (isRoot) {
+            list.addAll(valuesMap.values());
+        }
+
+
+        Map<String, Parameter> paramsMap = new LinkedHashMap<>();
+        paramsList.forEach(p -> {
+            if (p.getName().isEmpty()) {
+                MultivaluedMap<String, Object> values = InjectionUtils.extractValuesFromBean(params[p.getIndex()], "");
+                methodVars.forEach(var -> {
+                    list.addAll(values.get(var));
+                });
+            } else {
+                paramsMap.put(p.getName(), p);
+            }
+        });
+
+        Object requestBody = bodyIndex == -1 ? null : params[bodyIndex];
+        methodVars.forEach(varName -> {
+            Parameter p = paramsMap.remove(varName);
+            if (p != null) {
+                list.add(convertParamValue(params[p.getIndex()],
+                        m.getParameterTypes()[p.getIndex()],
+                        getParamAnnotations(m, p)));
+            } else if (beanParamValues.containsKey(varName)) {
+                BeanPair pair = beanParamValues.get(varName);
+                list.add(convertParamValue(pair.getValue(), pair.getAnns()));
+            } else if (requestBody != null) {
+                try {
+                    Method getter = requestBody.getClass().
+                            getMethod("get" + StringUtils.capitalize(varName), new Class<?>[]{});
+                    list.add(getter.invoke(requestBody, new Object[]{}));
+                } catch (Exception ex) {
+                    // continue
+                }
+            }
+        });
+
+        for (Parameter p : paramsMap.values()) {
+            if (valuesMap.containsKey(p.getName())) {
+                int index = 0;
+                for (Iterator<String> it = valuesMap.keySet().iterator(); it.hasNext(); index++) {
+                    if (it.next().equals(p.getName()) && index < list.size()) {
+                        list.set(index, convertParamValue(params[p.getIndex()], null));
+                        break;
+                    }
+                }
+            }
+        }
+
+
+        return list;
+    }
+
+    protected static Annotation[] getParamAnnotations(Method m, Parameter p) {
+        return m.getParameterAnnotations()[p.getIndex()];
+    }
+
+    protected static List<Parameter> getParameters(MultivaluedMap<ParameterType, Parameter> map,
+                                           ParameterType key) {
+        return map.get(key) == null ? Collections.emptyList() : map.get(key);
+    }
+
+    protected void handleQueries(Method m,
+                               Object[] params,
+                               MultivaluedMap<ParameterType, Parameter> map,
+                               List<Parameter> beanParams,
+                               UriBuilder ub) {
+        List<Parameter> qs = getParameters(map, ParameterType.QUERY);
+        qs.stream().
+                filter(p -> params[p.getIndex()] != null).
+                forEachOrdered(p -> {
+                    addMatrixQueryParamsToBuilder(ub, p.getName(), ParameterType.QUERY,
+                            getParamAnnotations(m, p), params[p.getIndex()]);
+                });
+        beanParams.stream().
+                map(p -> getValuesFromBeanParam(params[p.getIndex()], QueryParam.class)).
+                forEachOrdered(values -> {
+                    values.forEach((key, value) -> {
+                        if (value != null) {
+                            addMatrixQueryParamsToBuilder(ub, key, ParameterType.QUERY,
+                                    value.getAnns(), value.getValue());
+                        }
+                    });
+                });
+    }
+
+    protected Map<String, BeanPair> getValuesFromBeanParam(Object bean, Class<? extends Annotation> annClass) {
+        Map<String, BeanPair> values = new HashMap<>();
+        getValuesFromBeanParam(bean, annClass, values);
+        return values;
+    }
+
+    protected Map<String, BeanPair> getValuesFromBeanParam(Object bean,
+                                                         Class<? extends Annotation> annClass,
+                                                         Map<String, BeanPair> values) {
+        boolean completeFieldIntrospectionNeeded = false;
+        for (Method m : bean.getClass().getMethods()) {
+            if (m.getName().startsWith("set")) {
+                try {
+                    String propertyName = m.getName().substring(3);
+                    Annotation methodAnnotation = m.getAnnotation(annClass);
+                    boolean beanParam = m.getAnnotation(BeanParam.class) != null;
+                    if (methodAnnotation != null || beanParam) {
+                        Method getter = bean.getClass().getMethod("get" + propertyName, new Class<?>[]{});
+                        Object value = getter.invoke(bean, new Object[]{});
+                        if (value != null) {
+                            if (methodAnnotation != null) {
+                                String annotationValue = AnnotationUtils.getAnnotationValue(methodAnnotation);
+                                values.put(annotationValue, new BeanPair(value, m.getParameterAnnotations()[0]));
+                            } else {
+                                getValuesFromBeanParam(value, annClass, values);
+                            }
+                        }
+                    } else {
+                        String fieldName = StringUtils.uncapitalize(propertyName);
+                        Field f = InjectionUtils.getDeclaredField(bean.getClass(), fieldName);
+                        if (f == null) {
+                            completeFieldIntrospectionNeeded = true;
+                            continue;
+                        }
+                        boolean jaxrsParamAnnAvailable = getValuesFromBeanParamField(bean, f, annClass, values);
+                        if (!jaxrsParamAnnAvailable && f.getAnnotation(BeanParam.class) != null) {
+                            Object value = ReflectionUtil.accessDeclaredField(f, bean, Object.class);
+                            if (value != null) {
+                                getValuesFromBeanParam(value, annClass, values);
+                            }
+                        }
+                    }
+                } catch (Throwable t) {
+                    // ignore
+                }
+            }
+            if (completeFieldIntrospectionNeeded) {
+                for (Field f : bean.getClass().getDeclaredFields()) {
+                    boolean jaxrsParamAnnAvailable = getValuesFromBeanParamField(bean, f, annClass, values);
+                    if (!jaxrsParamAnnAvailable && f.getAnnotation(BeanParam.class) != null) {
+                        Object value = ReflectionUtil.accessDeclaredField(f, bean, Object.class);
+                        if (value != null) {
+                            getValuesFromBeanParam(value, annClass, values);
+                        }
+                    }
+                }
+            }
+        }
+        return values;
+    }
+
+    protected boolean getValuesFromBeanParamField(Object bean,
+                                                Field f,
+                                                Class<? extends Annotation> annClass,
+                                                Map<String, BeanPair> values) {
+        boolean jaxrsParamAnnAvailable = false;
+        Annotation fieldAnnotation = f.getAnnotation(annClass);
+        if (fieldAnnotation != null) {
+            jaxrsParamAnnAvailable = true;
+            Object value = ReflectionUtil.accessDeclaredField(f, bean, Object.class);
+            if (value != null) {
+                String annotationValue = AnnotationUtils.getAnnotationValue(fieldAnnotation);
+                values.put(annotationValue, new BeanPair(value, f.getAnnotations()));
+            }
+        }
+        return jaxrsParamAnnAvailable;
+    }
+
+    protected void handleMatrixes(Method m,
+                                Object[] params,
+                                MultivaluedMap<ParameterType, Parameter> map,
+                                List<Parameter> beanParams,
+                                UriBuilder ub) {
+        List<Parameter> mx = getParameters(map, ParameterType.MATRIX);
+        mx.stream().
+                filter(p -> params[p.getIndex()] != null).
+                forEachOrdered(p -> {
+                    addMatrixQueryParamsToBuilder(ub, p.getName(), ParameterType.MATRIX,
+                            getParamAnnotations(m, p), params[p.getIndex()]);
+                });
+        beanParams.stream().
+                map(p -> getValuesFromBeanParam(params[p.getIndex()], MatrixParam.class)).
+                forEachOrdered(values -> {
+                    values.forEach((key, value) -> {
+                        if (value != null) {
+                            addMatrixQueryParamsToBuilder(ub, key, ParameterType.MATRIX,
+                                    value.getAnns(), value.getValue());
+                        }
+                    });
+                });
+    }
+
+    protected MultivaluedMap<String, String> handleForm(Method m,
+                                                      Object[] params,
+                                                      MultivaluedMap<ParameterType, Parameter> map,
+                                                      List<Parameter> beanParams) {
+
+        MultivaluedMap<String, String> form = new MetadataMap<>();
+
+        List<Parameter> fm = getParameters(map, ParameterType.FORM);
+        fm.forEach(p -> {
+            addFormValue(form, p.getName(), params[p.getIndex()], getParamAnnotations(m, p));
+        });
+        beanParams.stream().
+                map(p -> getValuesFromBeanParam(params[p.getIndex()], FormParam.class)).
+                forEachOrdered(values -> {
+                    values.forEach((key, value) -> {
+                        addFormValue(form, key, value.getValue(), value.getAnns());
+                    });
+                });
+
+        return form;
+    }
+
+    protected void addFormValue(MultivaluedMap<String, String> form, String name, Object pValue, Annotation[] anns) {
+        if (pValue != null) {
+            if (InjectionUtils.isSupportedCollectionOrArray(pValue.getClass())) {
+                Collection<?> c = pValue.getClass().isArray()
+                    ? Arrays.asList((Object[]) pValue) : (Collection<?>) pValue;
+                for (Iterator<?> it = c.iterator(); it.hasNext();) {
+                    FormUtils.addPropertyToForm(form, name, convertParamValue(it.next(), anns));
+                }
+            } else {
+                FormUtils.addPropertyToForm(form, name, name.isEmpty()
+                                            ? pValue : convertParamValue(pValue, anns));
+            }
+
+        }
+
+    }
+
+    protected List<Attachment> handleMultipart(MultivaluedMap<ParameterType, Parameter> map,
+                                             OperationResourceInfo ori,
+                                             Object[] params) {
+        List<Parameter> fm = getParameters(map, ParameterType.REQUEST_BODY);
+        List<Attachment> atts = new ArrayList<>(fm.size());
+        fm.forEach(p -> {
+            Multipart part = getMultipart(ori, p.getIndex());
+            if (part != null) {
+                Object partObject = params[p.getIndex()];
+                if (partObject != null) {
+                    atts.add(new Attachment(part.value(), part.type(), partObject));
+                }
+            }
+        });
+        return atts;
+    }
+
+    protected void handleHeaders(Method m,
+                               Object[] params,
+                               MultivaluedMap<String, String> headers,
+                               List<Parameter> beanParams,
+                               MultivaluedMap<ParameterType, Parameter> map) {
+        List<Parameter> hs = getParameters(map, ParameterType.HEADER);
+        hs.stream().
+                filter(p -> params[p.getIndex()] != null).
+                forEachOrdered(p -> {
+                    headers.add(p.getName(), convertParamValue(params[p.getIndex()], getParamAnnotations(m, p)));
+                });
+        beanParams.stream().
+                map(p -> getValuesFromBeanParam(params[p.getIndex()], HeaderParam.class)).
+                forEachOrdered(values -> {
+                    values.forEach((key, value) -> {
+                        if (value != null) {
+                            headers.add(key, convertParamValue(value.getValue(), value.getAnns()));
+                        }
+                    });
+                });
+    }
+
+    protected static Multipart getMultipart(OperationResourceInfo ori, int index) {
+        Method aMethod = ori.getAnnotatedMethod();
+        return aMethod != null ? AnnotationUtils.getAnnotation(
+            aMethod.getParameterAnnotations()[index], Multipart.class) : null;
+    }
+
+    protected void handleCookies(Method m,
+                               Object[] params,
+                               MultivaluedMap<String, String> headers,
+                               List<Parameter> beanParams,
+                               MultivaluedMap<ParameterType, Parameter> map) {
+        List<Parameter> cs = getParameters(map, ParameterType.COOKIE);
+        cs.stream().
+                filter(p -> params[p.getIndex()] != null).
+                forEachOrdered(p -> {
+                    headers.add(HttpHeaders.COOKIE,
+                            p.getName() + '='
+                            + convertParamValue(params[p.getIndex()].toString(), getParamAnnotations(m, p)));
+                });
+        beanParams.stream().
+                map(p -> getValuesFromBeanParam(params[p.getIndex()], CookieParam.class)).
+                forEachOrdered(values -> {
+                    values.forEach((key, value) -> {
+                        if (value != null) {
+                            headers.add(HttpHeaders.COOKIE,
+                                    key + "=" + convertParamValue(value.getValue(), value.getAnns()));
+                        }
+                    });
+                });
+    }
+
+    protected Message createMessage(Object body,
+                                    OperationResourceInfo ori,
+                                    MultivaluedMap<String, String> headers,
+                                    URI currentURI,
+                                    Exchange exchange,
+                                    Map<String, Object> invocationContext,
+                                    boolean isProxy) {
+        return createMessage(body, ori.getHttpMethod(), headers, currentURI,
+                             exchange, invocationContext, isProxy);
+    }
+
+    //CHECKSTYLE:OFF
+    protected Object doChainedInvocation(URI uri,
+                                       MultivaluedMap<String, String> headers,
+                                       OperationResourceInfo ori,
+                                       Object[] methodParams,
+                                       Object body,
+                                       int bodyIndex,
+                                       Exchange exchange,
+                                       Map<String, Object> invocationContext) throws Throwable {
+    //CHECKSTYLE:ON
+        Bus configuredBus = getConfiguration().getBus();
+        Bus origBus = BusFactory.getAndSetThreadDefaultBus(configuredBus);
+        ClassLoaderHolder origLoader = null;
+        try {
+            ClassLoader loader = configuredBus.getExtension(ClassLoader.class);
+            if (loader != null) {
+                origLoader = ClassLoaderUtils.setThreadContextClassloader(loader);
+            }
+            Message outMessage = createMessage(body, ori, headers, uri, exchange, invocationContext, true);
+            if (bodyIndex != -1) {
+                outMessage.put(Type.class, ori.getMethodToInvoke().getGenericParameterTypes()[bodyIndex]);
+            }
+            outMessage.getExchange().setOneWay(ori.isOneway());
+            setSupportOnewayResponseProperty(outMessage);
+            outMessage.setContent(OperationResourceInfo.class, ori);
+            setPlainOperationNameProperty(outMessage, ori.getMethodToInvoke().getName());
+            outMessage.getExchange().put(Method.class, ori.getMethodToInvoke());
+
+            outMessage.put(Annotation.class.getName(),
+                           getMethodAnnotations(ori.getAnnotatedMethod(), bodyIndex));
+
+            outMessage.getExchange().put(Message.SERVICE_OBJECT, proxy);
+            if (methodParams != null) {
+                outMessage.put(List.class, Arrays.asList(methodParams));
+            }
+            if (body != null) {
+                outMessage.put(PROXY_METHOD_PARAM_BODY_INDEX, bodyIndex);
+            }
+            outMessage.getInterceptorChain().add(bodyWriter);
+
+            Map<String, Object> reqContext = getRequestContext(outMessage);
+            reqContext.put(OperationResourceInfo.class.getName(), ori);
+            reqContext.put(PROXY_METHOD_PARAM_BODY_INDEX, bodyIndex);
+
+            // execute chain
+            InvocationCallback<Object> asyncCallback = checkAsyncCallback(ori, reqContext, outMessage);
+            if (asyncCallback != null) {
+                return doInvokeAsync(ori, outMessage, asyncCallback);
+            }
+            doRunInterceptorChain(outMessage);
+
+            Object[] results = preProcessResult(outMessage);
+            if (results != null && results.length == 1) {
+                return results[0];
+            }
+
+            try {
+                return handleResponse(outMessage, ori.getClassResourceInfo().getServiceClass());
+            } finally {
+                completeExchange(outMessage.getExchange(), true);
+            }
+
+        } finally {
+            if (origLoader != null) {
+                origLoader.reset();
+            }
+            if (origBus != configuredBus) {
+                BusFactory.setThreadDefaultBus(origBus);
+            }
+        }
+
+    }
+
+    protected InvocationCallback<Object> checkAsyncCallback(OperationResourceInfo ori,
+                                                            Map<String, Object> reqContext,
+                                                            Message outMessage) {
+        Object callbackProp = reqContext.get(InvocationCallback.class.getName());
+        if (callbackProp != null) {
+            if (callbackProp instanceof Collection) {
+                @SuppressWarnings("unchecked")
+                Collection<InvocationCallback<Object>> callbacks = (Collection<InvocationCallback<Object>>)callbackProp;
+                for (InvocationCallback<Object> callback : callbacks) {
+                    if (doCheckAsyncCallback(ori, callback) != null) {
+                        return callback;
+                    }
+                }
+            } else {
+                @SuppressWarnings("unchecked")
+                InvocationCallback<Object> callback = (InvocationCallback<Object>)callbackProp;
+                return doCheckAsyncCallback(ori, callback);
+            }
+        }
+        return null;
+    }
+
+    protected InvocationCallback<Object> doCheckAsyncCallback(OperationResourceInfo ori,
+                                                            InvocationCallback<Object> callback) {
+        Type callbackOutType = getCallbackType(callback);
+        Class<?> callbackRespClass = getCallbackClass(callbackOutType);
+
+        Class<?> methodReturnType = ori.getMethodToInvoke().getReturnType();
+        if (Object.class == callbackRespClass
+            || callbackRespClass.isAssignableFrom(methodReturnType)
+            || PrimitiveUtils.canPrimitiveTypeBeAutoboxed(methodReturnType, callbackRespClass)) {
+            return callback;
+        }
+        return null;
+    }
+
+    protected Object doInvokeAsync(OperationResourceInfo ori, 
+                                   Message outMessage,
+                                   InvocationCallback<Object> asyncCallback) {
+        outMessage.getExchange().setSynchronous(false);
+        setAsyncMessageObserverIfNeeded(outMessage.getExchange());
+        JaxrsClientCallback<?> cb = newJaxrsClientCallback(asyncCallback, outMessage,
+            ori.getMethodToInvoke().getReturnType(), ori.getMethodToInvoke().getGenericReturnType());
+        outMessage.getExchange().put(JaxrsClientCallback.class, cb);
+        doRunInterceptorChain(outMessage);
+
+        return null;
+    }
+
+    protected JaxrsClientCallback<?> newJaxrsClientCallback(InvocationCallback<Object> asyncCallback,
+                                                            Message outMessage,
+                                                            Class<?> responseClass,
+                                                            Type outGenericType) {
+        return new JaxrsClientCallback<>(asyncCallback, responseClass, outGenericType);
+    }
+
+    @Override
+    protected Object retryInvoke(URI newRequestURI,
+                                 MultivaluedMap<String, String> headers,
+                                 Object body,
+                                 Exchange exchange,
+                                 Map<String, Object> invContext) throws Throwable {
+
+        Map<String, Object> reqContext = CastUtils.cast((Map<?, ?>)invContext.get(REQUEST_CONTEXT));
+        int bodyIndex = body != null ? (Integer)reqContext.get(PROXY_METHOD_PARAM_BODY_INDEX) : -1;
+        OperationResourceInfo ori =
+            (OperationResourceInfo)reqContext.get(OperationResourceInfo.class.getName());
+        return doChainedInvocation(newRequestURI, headers, ori, null,
+                                   body, bodyIndex, exchange, invContext);
+    }
+
+    protected Object handleResponse(Message outMessage, Class<?> serviceCls)
+        throws Throwable {
+        try {
+            Response r = setResponseBuilder(outMessage, outMessage.getExchange()).build();
+            ((ResponseImpl)r).setOutMessage(outMessage);
+            getState().setResponse(r);
+
+            Method method = outMessage.getExchange().get(Method.class);
+            checkResponse(method, r, outMessage);
+            if (method.getReturnType() == Void.class || method.getReturnType() == Void.TYPE) {
+                return null;
+            }
+            if (method.getReturnType() == Response.class
+                && (r.getEntity() == null || InputStream.class.isAssignableFrom(r.getEntity().getClass())
+                    && ((InputStream)r.getEntity()).available() == 0)) {
+                return r;
+            }
+            if (PropertyUtils.isTrue(super.getConfiguration().getResponseContext().get(BUFFER_PROXY_RESPONSE))) {
+                r.bufferEntity();
+            }
+
+            Class<?> returnType = getReturnType(method, outMessage);
+            Type genericType = getGenericReturnType(serviceCls, method, returnType);
+            
+            returnType = InjectionUtils.updateParamClassToTypeIfNeeded(returnType, genericType);
+            return readBody(r,
+                            outMessage,
+                            returnType,
+                            genericType,
+                            method.getDeclaredAnnotations());
+        } finally {
+            ClientProviderFactory.getInstance(outMessage).clearThreadLocalProxies();
+        }
+    }
+
+    protected Type getGenericReturnType(Class<?> serviceCls, Method method, Class<?> returnType) {
+        return InjectionUtils.processGenericTypeIfNeeded(serviceCls, returnType, method.getGenericReturnType());
+    }
+
+    protected Class<?> getReturnType(Method method, Message outMessage) {
+        return method.getReturnType();
+    }
+
+    public Object getInvocationHandler() {
+        return this;
+    }
+
+    protected static void reportInvalidResourceMethod(Method m, String name) {
+        org.apache.cxf.common.i18n.Message errorMsg =
+            new org.apache.cxf.common.i18n.Message(name,
+                                                   BUNDLE,
+                                                   m.getDeclaringClass().getName(),
+                                                   m.getName());
+        LOG.severe(errorMsg.toString());
+        throw new ProcessingException(errorMsg.toString());
+    }
+
+    protected static Annotation[] getMethodAnnotations(Method aMethod, int bodyIndex) {
+        return aMethod == null || bodyIndex == -1 ? new Annotation[0]
+            : aMethod.getParameterAnnotations()[bodyIndex];
+    }
+    
+    /**
+     * Checks if @BeanParam object has at least one @FormParam declaration.
+     * @param params parameter values
+     * @param beanParams bean parameters
+     * @return "true" @BeanParam object has at least one @FormParam, "false" otherwise
+     */
+    private boolean hasFormParams(Object[] params, List<Parameter> beanParams) {
+        return beanParams
+            .stream()
+            .map(p -> getValuesFromBeanParam(params[p.getIndex()], FormParam.class))
+            .anyMatch(((Predicate<Map<String, BeanPair>>) Map::isEmpty).negate());
+    }
+
+    protected class BodyWriter extends AbstractBodyWriter {
+
+        @Override
+        protected void doWriteBody(Message outMessage,
+                                   Object body,
+                                   Type bodyType,
+                                   Annotation[] customAnns,
+                                   OutputStream os) throws Fault {
+
+
+            OperationResourceInfo ori = outMessage.getContent(OperationResourceInfo.class);
+            if (ori == null) {
+                return;
+            }
+
+            Method method = ori.getMethodToInvoke();
+            int bodyIndex = (Integer)outMessage.get(PROXY_METHOD_PARAM_BODY_INDEX);
+
+            Annotation[] anns = customAnns != null ? customAnns
+                : getMethodAnnotations(ori.getAnnotatedMethod(), bodyIndex);
+            try {
+                Class<?>[] parameterTypes = method.getParameterTypes();
+                if (bodyIndex >= 0 && bodyIndex < parameterTypes.length) {
+                    Class<?> paramClass = parameterTypes[bodyIndex];
+                    Class<?> bodyClass =
+                        paramClass.isAssignableFrom(body.getClass()) ? paramClass : body.getClass();
+                    Type genericType = bodyType;
+                    if (genericType == null) {
+                        Type[] genericParameterTypes = method.getGenericParameterTypes();
+                        if (bodyIndex < genericParameterTypes.length) {
+                            genericType = genericParameterTypes[bodyIndex];
+                        }
+                    }
+                    genericType = InjectionUtils.processGenericTypeIfNeeded(
+                        ori.getClassResourceInfo().getServiceClass(), bodyClass, genericType);
+                    bodyClass = InjectionUtils.updateParamClassToTypeIfNeeded(bodyClass, genericType);
+                    writeBody(body, outMessage, bodyClass, genericType, anns, os);
+                } else {
+                    Type paramType = body.getClass();
+                    if (bodyType != null) {
+                        paramType = bodyType;
+                    }
+                    writeBody(body, outMessage, body.getClass(), paramType,
+                              anns, os);
+                }
+            } catch (Exception ex) {
+                throw new Fault(ex);
+            }
+
+        }
+
+    }
+
+    protected static class BeanPair {
+        protected Object value;
+        protected Annotation[] anns;
+        BeanPair(Object value, Annotation[] anns) {
+            this.value = value;
+            this.anns = anns;
+        }
+        public Object getValue() {
+            return value;
+        }
+        public Annotation[] getAnns() {
+            return anns;
+        }
+    }
+    class ClientAsyncResponseInterceptor extends AbstractClientAsyncResponseInterceptor {
+        @Override
+        protected void doHandleAsyncResponse(Message message, Response r, JaxrsClientCallback<?> cb) {
+            try {
+                Object entity = handleResponse(message.getExchange().getOutMessage(),
+                                               cb.getResponseClass());
+                cb.handleResponse(message, new Object[] {entity});
+            } catch (Throwable t) {
+                cb.handleException(message, t);
+            } finally {
+                completeExchange(message.getExchange(), false);
+                closeAsyncResponseIfPossible(r, message, cb);
+            }
+        }
+    }
+}
diff --git a/transform/src/patch/java/org/apache/cxf/jaxrs/client/JAXRSClientFactory.java b/transform/src/patch/java/org/apache/cxf/jaxrs/client/JAXRSClientFactory.java
new file mode 100644
index 0000000..1e487ab
--- /dev/null
+++ b/transform/src/patch/java/org/apache/cxf/jaxrs/client/JAXRSClientFactory.java
@@ -0,0 +1,414 @@
+/**
+ * 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.jaxrs.client;
+
+import java.lang.reflect.InvocationHandler;
+import java.net.URI;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import javax.ws.rs.core.MultivaluedMap;
+
+import org.apache.cxf.common.util.ProxyHelper;
+import org.apache.cxf.feature.Feature;
+import org.apache.cxf.jaxrs.model.UserResource;
+
+/**
+ * Factory for creating proxy clients.
+ *
+ */
+public final class JAXRSClientFactory {
+
+    private JAXRSClientFactory() {
+
+    }
+
+    /**
+     * Creates a proxy
+     * @param baseAddress baseAddress
+     * @param cls resource class, if not interface then a CGLIB proxy will be created
+     * @return typed proxy
+     */
+    public static <T> T create(String baseAddress, Class<T> cls) {
+        return create(URI.create(baseAddress), cls);
+    }
+
+    /**
+     * Creates a proxy using a custom class loader
+     * @param baseAddress baseAddress
+     * @param loader class loader
+     * @param cls resource class, if not interface then a CGLIB proxy will be created
+     * @return typed proxy
+     */
+    public static <T> T create(String baseAddress, Class<T> cls, ClassLoader loader) {
+        JAXRSClientFactoryBean bean = getBean(baseAddress, cls, null);
+        bean.setClassLoader(loader);
+        return bean.create(cls);
+    }
+
+    /**
+     * Creates a proxy
+     * @param baseURI baseURI
+     * @param cls resource class, if not interface then a CGLIB proxy will be created
+     * @return typed proxy
+     */
+    public static <T> T create(URI baseURI, Class<T> cls) {
+        return create(baseURI, cls, false);
+    }
+
+    /**
+     * Creates a proxy
+     * @param baseURI baseURI
+     * @param cls resource class, if not interface then a CGLIB proxy will be created
+     * @param inheritHeaders if true then existing proxy headers will be inherited by
+     *        subresource proxies if any
+     * @return typed proxy
+     */
+    public static <T> T create(URI baseURI, Class<T> cls, boolean inheritHeaders) {
+        JAXRSClientFactoryBean bean = getBean(baseURI.toString(), cls, null);
+        bean.setInheritHeaders(inheritHeaders);
+        return bean.create(cls);
+    }
+
+    /**
+     * 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
+     * @return typed proxy
+     */
+    public static <T> T create(String baseAddress, Class<T> cls, String configLocation) {
+        JAXRSClientFactoryBean bean = getBean(baseAddress, cls, configLocation);
+        return bean.create(cls);
+    }
+
+    /**
+     * Creates a proxy
+     * @param baseAddress baseAddress
+     * @param cls resource class, if not interface then a CGLIB proxy will be created
+     *        This class is expected to have a root JAXRS Path annotation containing
+     *        template variables, for ex, "/path/{id1}/{id2}"
+     * @param configLocation classpath location of the configuration resource
+     * @param varValues values to replace root Path template variables
+     * @return typed proxy
+     */
+    public static <T> T create(String baseAddress, Class<T> cls, String configLocation,
+                               Object... varValues) {
+        JAXRSClientFactoryBean bean = getBean(baseAddress, cls, configLocation);
+        return bean.create(cls, varValues);
+    }
+
+
+    /**
+     * Creates a proxy
+     * @param baseAddress baseAddress
+     * @param cls proxy class, if not interface then a CGLIB proxy will be created
+     * @param providers list of providers
+     * @return typed proxy
+     */
+    public static <T> T create(String baseAddress, Class<T> cls, List<?> providers) {
+        return create(baseAddress, cls, providers, null);
+    }
+
+    /**
+     * 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
+     * @return typed proxy
+     */
+    public static <T> T create(String baseAddress, Class<T> cls, List<?> providers, boolean threadSafe) {
+        return create(baseAddress, cls, providers, Collections.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, properties));
+        }
+        return bean.create(cls);
+    }
+
+    /**
+     * Creates a thread safe proxy and allows to specify time to keep state.
+     * @param baseAddress baseAddress
+     * @param cls proxy class, if not interface then a CGLIB proxy will be created
+     * @param providers list of providers
+     * @param timeToKeepState how long to keep this state
+     * @return typed proxy
+     */
+    public static <T> T create(String baseAddress, Class<T> cls, List<?> providers, long timeToKeepState) {
+        JAXRSClientFactoryBean bean = getBean(baseAddress, cls, null);
+        bean.setProviders(providers);
+        bean.setInitialState(new ThreadLocalClientState(baseAddress, timeToKeepState));
+        return bean.create(cls);
+    }
+
+    /**
+     * Creates a proxy
+     * @param baseAddress baseAddress
+     * @param cls proxy class, if not interface then a CGLIB proxy will be created
+     * @param providers list of providers
+     * @param configLocation classpath location of the configuration resource
+     * @return typed proxy
+     */
+    public static <T> T create(String baseAddress, Class<T> cls, List<?> providers, String configLocation) {
+        JAXRSClientFactoryBean bean = getBean(baseAddress, cls, configLocation);
+        bean.setProviders(providers);
+        return bean.create(cls);
+    }
+
+    /**
+     * Creates a proxy
+     * @param baseAddress baseAddress
+     * @param cls proxy class, if not interface then a CGLIB proxy will be created
+     * @param providers list of providers
+     * @param features the features which will be applied to the client
+     * @param configLocation classpath location of the configuration resource
+     * @return typed proxy
+     */
+    public static <T> T create(String baseAddress, Class<T> cls, List<?> providers,
+                               List<Feature> features,
+                               String configLocation) {
+        JAXRSClientFactoryBean bean = getBean(baseAddress, cls, configLocation);
+        bean.setProviders(providers);
+        bean.setFeatures(features);
+        return bean.create(cls);
+    }
+
+    /**
+     * Creates a proxy which will do basic authentication
+     * @param baseAddress baseAddress
+     * @param cls proxy class, if not interface then a CGLIB proxy will be created
+     * @param username username
+     * @param password password
+     * @param configLocation classpath location of the configuration resource
+     * @return typed proxy
+     */
+    public static <T> T create(String baseAddress, Class<T> cls, String username,
+                               String password, String configLocation) {
+        JAXRSClientFactoryBean bean = getBean(baseAddress, cls, configLocation);
+        bean.setUsername(username);
+        bean.setPassword(password);
+        return bean.create(cls);
+    }
+
+    /**
+     * Creates a proxy which will do basic authentication
+     * @param baseAddress baseAddress
+     * @param cls proxy class, if not interface then a CGLIB proxy will be created
+     * @param providers list of providers
+     * @param username username
+     * @param password password
+     * @param configLocation classpath location of the configuration resource
+     * @return typed proxy
+     */
+    public static <T> T create(String baseAddress, Class<T> cls, List<?> providers,
+                               String username, String password, String configLocation) {
+        JAXRSClientFactoryBean bean = getBean(baseAddress, cls, configLocation);
+        bean.setUsername(username);
+        bean.setPassword(password);
+        bean.setProviders(providers);
+        return bean.create(cls);
+    }
+
+    /**
+     * Creates a proxy using user resource model
+     * @param baseAddress baseAddress
+     * @param cls proxy class, if not interface then a CGLIB proxy will be created
+     * @param modelRef model location
+     * @return typed proxy
+     */
+    public static <T> T createFromModel(String baseAddress, Class<T> cls, String modelRef,
+                                        String configLocation) {
+        return createFromModel(baseAddress, cls, modelRef, Collections.emptyList(), configLocation);
+    }
+
+    /**
+     * Creates a proxy using user resource model
+     * @param baseAddress baseAddress
+     * @param cls proxy class, if not interface then a CGLIB proxy will be created
+     * @param modelRef model location
+     * @param providers list of providers
+     * @param configLocation classpath location of the configuration resource
+     * @return typed proxy
+     */
+    public static <T> T createFromModel(String baseAddress, Class<T> cls, String modelRef,
+                               List<?> providers, String configLocation) {
+        JAXRSClientFactoryBean bean = WebClient.getBean(baseAddress, configLocation);
+        bean.setProviders(providers);
+        bean.setModelRef(modelRef);
+        bean.setServiceClass(cls);
+        return bean.create(cls);
+    }
+
+    /**
+     * Creates a thread safe proxy using user resource model
+     * @param baseAddress baseAddress
+     * @param cls proxy class, if not interface then a CGLIB proxy will be created
+     * @param modelRef model location
+     * @param providers list of providers
+     * @param threadSafe if true then thread-safe proxy will be created
+     * @return typed proxy
+     */
+    public static <T> T createFromModel(String baseAddress, Class<T> cls, String modelRef,
+                                        List<?> providers, boolean threadSafe) {
+        JAXRSClientFactoryBean bean = WebClient.getBean(baseAddress, null);
+        bean.setProviders(providers);
+        bean.setModelRef(modelRef);
+        bean.setServiceClass(cls);
+        if (threadSafe) {
+            bean.setInitialState(new ThreadLocalClientState(baseAddress));
+        }
+        return bean.create(cls);
+    }
+
+    /**
+     * Creates a thread safe proxy using user resource model and allows to
+     * specify time to keep state.
+     * @param baseAddress baseAddress
+     * @param cls proxy class, if not interface then a CGLIB proxy will be created
+     * @param modelRef model location
+     * @param providers list of providers
+     * @param timeToKeepState how long to keep this state
+     * @return typed proxy
+     */
+    public static <T> T createFromModel(String baseAddress, Class<T> cls, String modelRef,
+                                        List<?> providers, long timeToKeepState) {
+        JAXRSClientFactoryBean bean = WebClient.getBean(baseAddress, null);
+        bean.setProviders(providers);
+        bean.setModelRef(modelRef);
+        bean.setServiceClass(cls);
+        bean.setInitialState(new ThreadLocalClientState(baseAddress, timeToKeepState));
+        return bean.create(cls);
+    }
+
+    /**
+     * Creates a proxy using user resource model
+     * @param baseAddress baseAddress
+     * @param cls proxy class, if not interface then a CGLIB proxy will be created
+     * @param modelBeans model beans
+     * @param configLocation classpath location of the configuration resource
+     * @return typed proxy
+     */
+    public static <T> T createFromModel(String baseAddress, Class<T> cls, List<UserResource> modelBeans,
+                               String configLocation) {
+        return createFromModel(baseAddress, cls, modelBeans, Collections.emptyList(), configLocation);
+    }
+
+    /**
+     * Creates a proxy using user resource model
+     * @param baseAddress baseAddress
+     * @param cls proxy class, if not interface then a CGLIB proxy will be created
+     * @param modelBeans model beans
+     * @param providers list of providers
+     * @param configLocation classpath location of the configuration resource
+     * @return typed proxy
+     */
+    public static <T> T createFromModel(String baseAddress, Class<T> cls, List<UserResource> modelBeans,
+                               List<?> providers, String configLocation) {
+        JAXRSClientFactoryBean bean = WebClient.getBean(baseAddress, configLocation);
+
+        bean.setProviders(providers);
+        bean.setModelBeans(modelBeans);
+        bean.setServiceClass(cls);
+        return bean.create(cls);
+    }
+
+    /**
+     * Creates a proxy, baseURI will be set to Client currentURI
+     *
+     * @param client Client instance
+     * @param cls proxy class, if not interface then a CGLIB proxy will be created
+     * @return typed proxy
+     */
+    public static <T> T fromClient(Client client, Class<T> cls) {
+        return fromClient(client, cls, false);
+    }
+
+    /**
+     * Creates a proxy, baseURI will be set to Client currentURI
+     * @param client Client instance
+     * @param cls proxy class, if not interface then a CGLIB proxy will be created
+     * @param inheritHeaders if true then existing Client headers will be inherited by new proxy
+     *        and subresource proxies if any
+     * @return typed proxy
+     */
+    public static <T> T fromClient(Client client, Class<T> cls, boolean inheritHeaders) {
+        JAXRSClientFactoryBean bean = getBean(client.getCurrentURI().toString(), cls, null);
+        bean.setInheritHeaders(inheritHeaders);
+
+        ClientState clientState = WebClient.getClientState(client);
+
+        final T proxy;
+        if (clientState == null) {
+            proxy = bean.create(cls);
+            if (inheritHeaders) {
+                WebClient.client(proxy).headers(client.getHeaders());
+            }
+        } else {
+            MultivaluedMap<String, String> headers = inheritHeaders ? client.getHeaders() : null;
+            bean.setInitialState(clientState.newState(client.getCurrentURI(), headers, null, bean.getProperties()));
+            proxy = bean.create(cls);
+        }
+        WebClient.copyProperties(WebClient.client(proxy), client);
+        return proxy;
+    }
+
+    static <T> T createProxy(Class<T> cls, ClassLoader loader, InvocationHandler handler) {
+
+        return cls.cast(ProxyHelper.getProxy(loader == null ? cls.getClassLoader() : loader,
+                                             new Class[]{Client.class, InvocationHandlerAware.class, cls},
+                                             handler));
+    }
+
+    private static JAXRSClientFactoryBean getBean(String baseAddress, Class<?> cls, String configLocation) {
+        JAXRSClientFactoryBean bean = WebClient.getBean(baseAddress, configLocation);
+        bean.setServiceClass(cls);
+        return bean;
+    }
+
+
+}
diff --git a/transform/src/patch/java/org/apache/cxf/jaxrs/client/LocalClientState.java b/transform/src/patch/java/org/apache/cxf/jaxrs/client/LocalClientState.java
new file mode 100644
index 0000000..477e5d3
--- /dev/null
+++ b/transform/src/patch/java/org/apache/cxf/jaxrs/client/LocalClientState.java
@@ -0,0 +1,185 @@
+/**
+ * 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.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;
+import javax.ws.rs.core.UriBuilder;
+
+import org.apache.cxf.common.util.StringUtils;
+import org.apache.cxf.jaxrs.impl.MetadataMap;
+import org.apache.cxf.jaxrs.impl.UriBuilderImpl;
+
+/**
+ * Keeps the client state such as the baseURI, currentURI, requestHeaders, current response
+ *
+ */
+public class LocalClientState implements ClientState {
+    private static final String HTTP_SCHEME = "http";
+    private static final String WS_SCHEME = "ws";
+
+    private MultivaluedMap<String, String> requestHeaders = new MetadataMap<>(false, true);
+    private MultivaluedMap<String, String> templates;
+    private Response response;
+    private URI baseURI;
+    private UriBuilder currentBuilder;
+    private Map<String, Object> properties;
+
+    public LocalClientState() {
+
+    }
+
+    public LocalClientState(URI baseURI) {
+        this(baseURI, Collections.emptyMap());
+    }
+    
+    public LocalClientState(URI baseURI, Map<String, Object> properties) {
+        this.baseURI = baseURI;
+        
+        if (properties != null) {
+            this.properties = new HashMap<>(properties);
+        }
+        
+        resetCurrentUri(properties);
+    }
+
+    public LocalClientState(URI baseURI, URI currentURI) {
+        this(baseURI, currentURI, Collections.emptyMap()); 
+    }
+
+    public LocalClientState(URI baseURI, URI currentURI, Map<String, Object> properties) {
+        this.baseURI = baseURI;
+        
+        if (properties != null) {
+            this.properties = new HashMap<>(properties);
+        }
+        
+        this.currentBuilder = new UriBuilderImpl(properties).uri(currentURI);
+    }
+
+    public LocalClientState(LocalClientState cs) {
+        this.requestHeaders = new MetadataMap<>(cs.requestHeaders);
+        this.templates = cs.templates == null ? null : new MetadataMap<String, String>(cs.templates);
+        this.response = cs.response;
+
+        this.baseURI = cs.baseURI;
+        this.currentBuilder = cs.currentBuilder != null ? cs.currentBuilder.clone() : null;
+        this.properties = cs.properties;
+    }
+
+    private void resetCurrentUri(Map<String, Object> props) {
+        if (isSupportedScheme(baseURI)) {
+            this.currentBuilder = new UriBuilderImpl(props).uri(baseURI);
+        } else {
+            this.currentBuilder = new UriBuilderImpl(props).uri("/");
+        }
+    }
+
+    public void setCurrentBuilder(UriBuilder currentBuilder) {
+        this.currentBuilder = currentBuilder;
+    }
+
+    public UriBuilder getCurrentBuilder() {
+        return currentBuilder;
+    }
+
+    public void setBaseURI(URI baseURI) {
+        this.baseURI = baseURI;
+        resetCurrentUri(Collections.emptyMap());
+    }
+
+    public URI getBaseURI() {
+        return baseURI;
+    }
+
+    public void setResponse(Response r) {
+        this.response = r;
+    }
+
+    public Response getResponse() {
+        return response;
+    }
+
+    public void setRequestHeaders(MultivaluedMap<String, String> requestHeaders) {
+        this.requestHeaders = requestHeaders;
+    }
+
+    public MultivaluedMap<String, String> getRequestHeaders() {
+        return requestHeaders;
+    }
+
+    public MultivaluedMap<String, String> getTemplates() {
+        return templates;
+    }
+
+    public void setTemplates(MultivaluedMap<String, String> map) {
+        if (templates == null) {
+            this.templates = map;
+        } else if (map != null) {
+            templates.putAll(map);
+        } else {
+            templates = null;
+        }
+    }
+
+    public void reset() {
+        requestHeaders.clear();
+        response = null;
+        currentBuilder = new UriBuilderImpl(properties).uri(baseURI);
+        templates = null;
+    }
+    
+    public ClientState newState(URI currentURI, MultivaluedMap<String, String> headers,
+            MultivaluedMap<String, String> templatesMap, Map<String, Object> props) {
+        final ClientState state;
+        if (isSupportedScheme(currentURI)) {
+            state = new LocalClientState(currentURI, props);
+        } else {
+            state = new LocalClientState(baseURI, currentURI, props);
+        }
+        if (headers != null) {
+            state.setRequestHeaders(headers);
+        }
+        // we need to carry the template parameters forward
+        MultivaluedMap<String, String> newTemplateParams = templates;
+        if (newTemplateParams != null && templatesMap != null) {
+            newTemplateParams.putAll(templatesMap);
+        } else {
+            newTemplateParams = templatesMap;
+        }
+        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/transform/src/patch/java/org/apache/cxf/jaxrs/client/WebClient.java b/transform/src/patch/java/org/apache/cxf/jaxrs/client/WebClient.java
new file mode 100644
index 0000000..eaedd4b
--- /dev/null
+++ b/transform/src/patch/java/org/apache/cxf/jaxrs/client/WebClient.java
@@ -0,0 +1,1349 @@
+/**
+ * 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.jaxrs.client;
+
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.ProcessingException;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.client.AsyncInvoker;
+import javax.ws.rs.client.CompletionStageRxInvoker;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.InvocationCallback;
+import javax.ws.rs.client.RxInvoker;
+import javax.ws.rs.client.RxInvokerProvider;
+import javax.ws.rs.client.SyncInvoker;
+import javax.ws.rs.core.Cookie;
+import javax.ws.rs.core.EntityTag;
+import javax.ws.rs.core.Form;
+import javax.ws.rs.core.GenericEntity;
+import javax.ws.rs.core.GenericType;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.PathSegment;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.ResponseBuilder;
+import javax.ws.rs.core.UriBuilder;
+
+import org.apache.cxf.Bus;
+import org.apache.cxf.BusFactory;
+import org.apache.cxf.bus.spring.SpringBusFactory;
+import org.apache.cxf.common.classloader.ClassLoaderUtils;
+import org.apache.cxf.common.classloader.ClassLoaderUtils.ClassLoaderHolder;
+import org.apache.cxf.common.util.ClassHelper;
+import org.apache.cxf.common.util.PropertyUtils;
+import org.apache.cxf.feature.Feature;
+import org.apache.cxf.helpers.CastUtils;
+import org.apache.cxf.interceptor.Fault;
+import org.apache.cxf.jaxrs.client.spec.ClientImpl.WebTargetImpl;
+import org.apache.cxf.jaxrs.client.spec.InvocationBuilderImpl;
+import org.apache.cxf.jaxrs.impl.ResponseImpl;
+import org.apache.cxf.jaxrs.impl.UriBuilderImpl;
+import org.apache.cxf.jaxrs.model.ParameterType;
+import org.apache.cxf.jaxrs.model.URITemplate;
+import org.apache.cxf.jaxrs.utils.HttpUtils;
+import org.apache.cxf.jaxrs.utils.JAXRSUtils;
+import org.apache.cxf.jaxrs.utils.ParameterizedCollectionType;
+import org.apache.cxf.message.Exchange;
+import org.apache.cxf.message.Message;
+
+
+/**
+ * Http-centric web client
+ *
+ */
+public class WebClient extends AbstractClient {
+    private static final String REQUEST_CLASS = "request.class";
+    private static final String REQUEST_TYPE = "request.type";
+    private static final String REQUEST_ANNS = "request.annotations";
+    private static final String RESPONSE_CLASS = "response.class";
+    private static final String RESPONSE_TYPE = "response.type";
+    private static final String WEB_CLIENT_OPERATION_REPORTING = "enable.webclient.operation.reporting";
+    private BodyWriter bodyWriter = new BodyWriter();
+    protected WebClient(String baseAddress) {
+        this(convertStringToURI(baseAddress), Collections.emptyMap());
+    }
+    
+    protected WebClient(String baseAddress, Map<String, Object> properties) {
+        this(convertStringToURI(baseAddress), properties);
+    }
+
+    protected WebClient(URI baseURI) {
+        this(baseURI, Collections.emptyMap());
+    }
+
+    protected WebClient(URI baseURI, Map<String, Object> properties) {
+        this(new LocalClientState(baseURI, properties));
+    }
+
+    protected WebClient(ClientState state) {
+        super(state);
+        cfg.getInInterceptors().add(new ClientAsyncResponseInterceptor());
+    }
+
+
+
+
+    /**
+     * Creates WebClient
+     * @param baseAddress baseAddress
+     */
+    public static WebClient create(String baseAddress) {
+        return create(baseAddress, Collections.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();
+    }
+
+    /**
+     * Creates WebClient
+     * @param baseURI baseURI
+     */
+    public static WebClient create(URI baseURI) {
+        return create(baseURI.toString());
+    }
+
+    /**
+     * Creates WebClient
+     * @param baseURI baseURI
+     */
+    public static WebClient create(String baseURI, boolean threadSafe) {
+        return create(baseURI, Collections.emptyList(), threadSafe);
+    }
+
+    /**
+     * Creates WebClient
+     * @param baseAddress baseURI
+     * @param providers list of providers
+     */
+    public static WebClient create(String baseAddress, List<?> providers) {
+        return create(baseAddress, providers, null);
+    }
+
+    /**
+     * Creates WebClient
+     * @param baseAddress baseURI
+     * @param providers list of providers
+     * @param threadSafe if true ThreadLocalClientState is used
+     */
+    public static WebClient create(String baseAddress, List<?> providers, boolean threadSafe) {
+        return create(baseAddress, providers, Collections.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, properties));
+        }
+        return bean.createWebClient();
+    }
+
+    /**
+     * Creates a thread safe WebClient
+     * @param baseAddress baseURI
+     * @param providers list of providers
+     * @param timeToKeepState time to keep this thread safe state.
+     */
+    public static WebClient create(String baseAddress, List<?> providers, long timeToKeepState) {
+        JAXRSClientFactoryBean bean = getBean(baseAddress, null);
+        bean.setProviders(providers);
+        bean.setInitialState(new ThreadLocalClientState(baseAddress, timeToKeepState));
+        return bean.createWebClient();
+    }
+
+    /**
+     * Creates WebClient
+     * @param baseAddress baseAddress
+     * @param providers list of providers
+     * @param configLocation classpath location of the configuration resource, can be null
+     * @return WebClient instance
+     */
+    public static WebClient create(String baseAddress, List<?> providers, String configLocation) {
+        JAXRSClientFactoryBean bean = getBean(baseAddress, configLocation);
+        bean.setProviders(providers);
+        return bean.createWebClient();
+    }
+
+    /**
+     * Creates WebClient with a list of custom features
+     * @param baseAddress baseAddress
+     * @param providers list of providers
+     * @param features the features which will be applied to the client
+     * @param configLocation classpath location of the configuration resource, can be null
+     * @return WebClient instance
+     */
+    public static WebClient create(String baseAddress,
+                                   List<?> providers,
+                                   List<? extends Feature> features,
+                                   String configLocation) {
+        JAXRSClientFactoryBean bean = getBean(baseAddress, configLocation);
+        bean.setProviders(providers);
+        bean.setFeatures(features);
+        return bean.createWebClient();
+    }
+
+    /**
+     * Creates WebClient
+     * @param baseAddress baseAddress
+     * @param configLocation classpath location of the configuration resource, can be null
+     * @return WebClient instance
+     */
+    public static WebClient create(String baseAddress, String configLocation) {
+        JAXRSClientFactoryBean bean = getBean(baseAddress, configLocation);
+
+        return bean.createWebClient();
+    }
+
+    /**
+     * Creates WebClient which will do basic authentication
+     * @param baseAddress baseAddress
+     * @param username username
+     * @param password password
+     * @param configLocation classpath location of the configuration resource, can be null
+     * @return WebClient instance
+     */
+    public static WebClient create(String baseAddress, String username, String password,
+                                         String configLocation) {
+        JAXRSClientFactoryBean bean = getBean(baseAddress, configLocation);
+
+        bean.setUsername(username);
+        bean.setPassword(password);
+
+        return bean.createWebClient();
+    }
+
+    /**
+     * Creates WebClient which will do basic authentication
+     * @param baseAddress baseAddress
+     * @param providers list of providers
+     * @param username username
+     * @param password password
+     * @param configLocation classpath location of the configuration resource, can be null
+     * @return WebClient instance
+     */
+    public static WebClient create(String baseAddress, List<?> providers,
+                                   String username, String password, String configLocation) {
+        JAXRSClientFactoryBean bean = getBean(baseAddress, configLocation);
+
+        bean.setUsername(username);
+        bean.setPassword(password);
+        bean.setProviders(providers);
+        return bean.createWebClient();
+    }
+
+    /**
+     * Creates WebClient, baseURI will be set to Client currentURI
+     * @param object existing client object
+     */
+    public static WebClient fromClientObject(Object object) {
+        Client client = client(object);
+        return client == null ? null : fromClient(client, false);
+    }
+
+    /**
+     * Creates WebClient, baseURI will be set to Client currentURI
+     * @param client existing client
+     */
+    public static WebClient fromClient(Client client) {
+        return fromClient(client, false);
+    }
+
+    /**
+     * Creates WebClient, baseURI will be set to Client currentURI
+     * @param client existing client
+     * @param inheritHeaders  if existing Client headers can be inherited by new client
+     */
+    public static WebClient fromClient(Client client, boolean inheritHeaders) {
+
+        final WebClient webClient;
+
+        ClientState clientState = getClientState(client);
+        if (clientState == null) {
+            webClient = create(client.getCurrentURI());
+            if (inheritHeaders) {
+                webClient.headers(client.getHeaders());
+            }
+        } else {
+            MultivaluedMap<String, String> headers = inheritHeaders ? client.getHeaders() : null;
+            webClient = new WebClient(clientState.newState(client.getCurrentURI(), headers, null));
+        }
+        copyProperties(webClient, client);
+        return webClient;
+    }
+
+    /**
+     * Converts object to Client
+     * @param object the object
+     * @return Client object converted to Client
+     */
+    public static Client client(Object object) {
+        if (object instanceof Client) {
+            return (Client)object;
+        }
+        return null;
+    }
+
+    /**
+     * Retrieves ClientConfiguration
+     * @param client proxy or http-centric Client
+     * @return underlying ClientConfiguration instance
+     */
+    public static ClientConfiguration getConfig(Object client) {
+        if (client instanceof WebTargetImpl) {
+            client = ((WebTargetImpl)client).getWebClient();
+        } else if (client instanceof InvocationBuilderImpl) {
+            client = ((InvocationBuilderImpl)client).getWebClient();
+        }
+
+        if (client instanceof Client) {
+            if (client instanceof WebClient) {
+                return ((AbstractClient)client).getConfiguration();
+            } else if (client instanceof InvocationHandlerAware) {
+                Object handler = ((InvocationHandlerAware)client).getInvocationHandler();
+                return ((AbstractClient)handler).getConfiguration();
+            }
+        }
+        throw new IllegalArgumentException("Not a valid Client");
+    }
+
+    /**
+     * Does HTTP invocation
+     * @param httpMethod HTTP method
+     * @param body request body, can be null
+     * @return JAXRS Response, entity may hold a string representaion of
+     *         error message if client or server error occured
+     */
+    public Response invoke(String httpMethod, Object body) {
+        return doInvoke(httpMethod, body, null, Response.class, Response.class);
+    }
+
+    /**
+     * Does HTTP POST invocation
+     * @param body request body, can be null
+     * @return JAXRS Response
+     */
+    public Response post(Object body) {
+        return invoke(HttpMethod.POST, body);
+    }
+
+    /**
+     * Does HTTP PUT invocation
+     * @param body request body, can be null
+     * @return JAXRS Response
+     */
+    public Response put(Object body) {
+        return invoke(HttpMethod.PUT, body);
+    }
+
+    /**
+     * Does HTTP GET invocation
+     * @return JAXRS Response
+     */
+    public Response get() {
+        return invoke(HttpMethod.GET, null);
+    }
+
+    /**
+     * Does HTTP HEAD invocation
+     * @return JAXRS Response
+     */
+    public Response head() {
+        return invoke(HttpMethod.HEAD, null);
+    }
+
+    /**
+     * Does HTTP OPTIONS invocation
+     * @return JAXRS Response
+     */
+    public Response options() {
+        return invoke(HttpMethod.OPTIONS, null);
+    }
+
+    /**
+     * Does HTTP DELETE invocation
+     * @return JAXRS Response
+     */
+    public Response delete() {
+        return invoke(HttpMethod.DELETE, null);
+    }
+
+    /**
+     * Posts form data
+     * @param values form values
+     * @return JAXRS Response
+     */
+    public Response form(Map<String, List<Object>> values) {
+        type(MediaType.APPLICATION_FORM_URLENCODED);
+        return doInvoke(HttpMethod.POST, values, null, Response.class, Response.class);
+    }
+
+    /**
+     * Posts form data
+     * @param form form values
+     * @return JAXRS Response
+     */
+    public Response form(Form form) {
+        type(MediaType.APPLICATION_FORM_URLENCODED);
+        return doInvoke(HttpMethod.POST, form.asMap(), null, Response.class, Response.class);
+    }
+
+    /**
+     * Does HTTP invocation and returns types response object
+     * @param httpMethod HTTP method
+     * @param body request body, can be null
+     * @param responseType generic response type
+     * @return typed object, can be null. Response status code and headers
+     *         can be obtained too, see Client.getResponse()
+     */
+    public <T> T invoke(String httpMethod, Object body, GenericType<T> responseType) {
+        @SuppressWarnings("unchecked")
+        Class<T> responseClass = (Class<T>)responseType.getRawType();
+        Response r = doInvoke(httpMethod, body, null, responseClass, responseType.getType());
+        return castResponse(r, responseClass);
+    }
+
+    /**
+     * Does HTTP invocation and returns types response object
+     * @param httpMethod HTTP method
+     * @param body request body, can be null
+     * @param responseClass expected type of response object
+     * @return typed object, can be null. Response status code and headers
+     *         can be obtained too, see Client.getResponse()
+     */
+    public <T> T invoke(String httpMethod, Object body, Class<T> responseClass) {
+        Response r = doInvoke(httpMethod, body, null, responseClass, responseClass);
+        return castResponse(r, responseClass);
+    }
+
+    /**
+     * Does HTTP invocation and returns types response object
+     * @param httpMethod HTTP method
+     * @param body request body, can be null
+     * @param requestClass request body class
+     * @param responseClass expected type of response object
+     * @return typed object, can be null. Response status code and headers
+     *         can be obtained too, see Client.getResponse()
+     */
+    public <T> T invoke(String httpMethod, Object body, Class<?> requestClass, Class<T> responseClass) {
+        Response r = doInvoke(httpMethod, body, requestClass, null, responseClass, responseClass);
+        return castResponse(r, responseClass);
+    }
+
+    @SuppressWarnings("unchecked")
+    private <T> T castResponse(Response r, Class<T> responseClass) {
+        return (T)(responseClass == Response.class ? r : r.getEntity());
+    }
+    /**
+     * Does HTTP POST invocation and returns typed response object
+     * @param body request body, can be null
+     * @param responseClass expected type of response object
+     * @return typed object, can be null. Response status code and headers
+     *         can be obtained too, see Client.getResponse()
+     */
+    public <T> T post(Object body, Class<T> responseClass) {
+        return invoke(HttpMethod.POST, body, responseClass);
+    }
+
+    /**
+     * Does HTTP POST invocation and returns typed response object
+     * @param body request body, can be null
+     * @param responseType generic response type
+     * @return typed object, can be null. Response status code and headers
+     *         can be obtained too, see Client.getResponse()
+     */
+    public <T> T post(Object body, GenericType<T> responseType) {
+        return invoke(HttpMethod.POST, body, responseType);
+    }
+
+    /**
+     * Does HTTP Async POST invocation and returns Future.
+     * Shortcut for async().post(Entity, InvocationCallback)
+     * @param callback invocation callback
+     * @return the future
+     */
+    public <T> Future<T> post(Object body, InvocationCallback<T> callback) {
+        return doInvokeAsyncCallback(HttpMethod.POST, body, body.getClass(), null, callback);
+    }
+
+    /**
+     * Does HTTP PUT invocation and returns typed response object
+     * @param body request body, can be null
+     * @param responseClass expected type of response object
+     * @return typed object, can be null. Response status code and headers
+     *         can be obtained too, see Client.getResponse()
+     */
+    public <T> T put(Object body, Class<T> responseClass) {
+        return invoke(HttpMethod.PUT, body, responseClass);
+    }
+
+
+    /**
+     * Does HTTP PUT invocation and returns typed response object
+     * @param body request body, can be null
+     * @param responseType generic response type
+     * @return typed object, can be null. Response status code and headers
+     *         can be obtained too, see Client.getResponse()
+     */
+    public <T> T put(Object body, GenericType<T> responseType) {
+        return invoke(HttpMethod.PUT, body, responseType);
+    }
+
+    /**
+     * Does HTTP Async PUT invocation and returns Future.
+     * Shortcut for async().put(Entity, InvocationCallback)
+     * @param callback invocation callback
+     * @return the future
+     */
+    public <T> Future<T> put(Object body, InvocationCallback<T> callback) {
+        return doInvokeAsyncCallback(HttpMethod.PUT, body, body.getClass(), null, callback);
+    }
+
+    /**
+     * Does HTTP invocation and returns a collection of typed objects
+     * @param httpMethod HTTP method
+     * @param body request body, can be null
+     * @param memberClass expected type of collection member class
+     * @return typed collection
+     */
+    public <T> Collection<? extends T> invokeAndGetCollection(String httpMethod, Object body,
+                                                    Class<T> memberClass) {
+        Response r = doInvoke(httpMethod, body, null,
+                              Collection.class, new ParameterizedCollectionType(memberClass));
+        return CastUtils.cast((Collection<?>)r.getEntity(), memberClass);
+    }
+
+    /**
+     * Posts a collection of typed objects
+     * @param collection request body
+     * @param memberClass type of collection member class
+     * @return JAX-RS Response
+     */
+    public <T> Response postCollection(Object collection, Class<T> memberClass) {
+        return doInvoke(HttpMethod.POST, collection, new ParameterizedCollectionType(memberClass),
+                        Response.class, Response.class);
+    }
+
+    /**
+     * Posts a collection of typed objects
+     * @param collection request body
+     * @param memberClass type of collection member class
+     * @param responseClass expected type of response object
+     * @return JAX-RS Response
+     */
+    public <T1, T2> T2 postCollection(Object collection, Class<T1> memberClass,
+                                            Class<T2> responseClass) {
+        Response r = doInvoke(HttpMethod.POST, collection, new ParameterizedCollectionType(memberClass),
+                              responseClass, responseClass);
+        return responseClass.cast(responseClass == Response.class ? r : r.getEntity());
+    }
+
+    /**
+     * Posts collection of typed objects and returns a collection of typed objects
+     * @param collection request body
+     * @param memberClass type of collection member class
+     * @param responseClass expected type of response object
+     * @return JAX-RS Response
+     */
+    public <T1, T2> Collection<? extends T2> postAndGetCollection(Object collection,
+                                                                  Class<T1> memberClass,
+                                                                  Class<T2> responseClass) {
+        Response r = doInvoke(HttpMethod.POST, collection, new ParameterizedCollectionType(memberClass),
+                              Collection.class, new ParameterizedCollectionType(responseClass));
+        return CastUtils.cast((Collection<?>)r.getEntity(), responseClass);
+    }
+
+    /**
+     * Posts the object and returns a collection of typed objects
+     * @param body request body
+     * @param responseClass expected type of response object
+     * @return JAX-RS Response
+     */
+    public <T> Collection<? extends T> postObjectGetCollection(Object body,
+                                                                  Class<T> responseClass) {
+        Response r = doInvoke(HttpMethod.POST, body, null, Collection.class,
+                              new ParameterizedCollectionType(responseClass));
+        return CastUtils.cast((Collection<?>)r.getEntity(), responseClass);
+    }
+
+    /**
+     * Posts request body and returns a collection of typed objects
+     * @param body request body, can be null
+     * @param memberClass expected type of collection member class
+     * @return typed collection
+     */
+    public <T> Collection<? extends T> postAndGetCollection(Object body, Class<T> memberClass) {
+        return invokeAndGetCollection(HttpMethod.POST, body, memberClass);
+    }
+
+    /**
+     * Does HTTP GET invocation and returns a collection of typed objects
+     * @param memberClass expected type of collection member class
+     * @return typed collection
+     */
+    public <T> Collection<? extends T> getCollection(Class<T> memberClass) {
+        return invokeAndGetCollection(HttpMethod.GET, null, memberClass);
+    }
+
+    /**
+     * Does HTTP GET invocation and returns typed response object
+     * @param responseClass expected type of response object
+     * @return typed object, can be null. Response status code and headers
+     *         can be obtained too, see Client.getResponse()
+     */
+    public <T> T get(Class<T> responseClass) {
+        return invoke(HttpMethod.GET, null, responseClass);
+    }
+
+
+    /**
+     * Does HTTP GET invocation and returns typed response object
+     * @param responseType generic response type
+     * @return typed object, can be null. Response status code and headers
+     *         can be obtained too, see Client.getResponse()
+     */
+    public <T> T get(GenericType<T> responseType) {
+        return invoke(HttpMethod.GET, null, responseType);
+    }
+
+    /**
+     * Does HTTP Async GET invocation and returns Future.
+     * Shortcut for async().get(InvocationCallback)
+     * @param callback invocation callback
+     * @return the future
+     */
+    public <T> Future<T> get(InvocationCallback<T> callback) {
+        return doInvokeAsyncCallback(HttpMethod.GET, null, null, null, callback);
+    }
+
+    /**
+     * Updates the current URI path
+     * @param path new relative path segment
+     * @return updated WebClient
+     */
+    public WebClient path(Object path) {
+        getCurrentBuilder().path(convertParamValue(path, null));
+
+        return this;
+    }
+
+    /**
+     * Updates the current URI path with path segment which may contain template variables
+     * @param path new relative path segment
+     * @param values template variable values
+     * @return updated WebClient
+     */
+    public WebClient path(String path, Object... values) {
+        URI u = new UriBuilderImpl().uri(URI.create("http://tempuri")).path(path).buildFromEncoded(values);
+        getState().setTemplates(getTemplateParametersMap(new URITemplate(path), Arrays.asList(values)));
+        return path(u.getRawPath());
+    }
+
+    @Override
+    public WebClient query(String name, Object ...values) {
+        return (WebClient)super.query(name, values);
+    }
+
+    /**
+     * Updates the current URI matrix parameters
+     * @param name matrix name
+     * @param values matrix values
+     * @return updated WebClient
+     */
+    public WebClient matrix(String name, Object ...values) {
+        addMatrixQueryParamsToBuilder(getCurrentBuilder(), name, ParameterType.MATRIX, null, values);
+        return this;
+    }
+
+    /**
+     * Updates the current URI fragment
+     * @param name fragment name
+     * @return updated WebClient
+     */
+    public WebClient fragment(String name) {
+        getCurrentBuilder().fragment(name);
+        return this;
+    }
+
+    /**
+     * Moves WebClient to a new baseURI or forwards to new currentURI
+     * @param newAddress new URI
+     * @param forward if true then currentURI will be based on baseURI
+     * @return updated WebClient
+     */
+    public WebClient to(String newAddress, boolean forward) {
+        getState().setTemplates(null);
+        if (forward) {
+            if (!newAddress.startsWith("/")
+                && !newAddress.startsWith(getBaseURI().toString())) {
+                throw new IllegalArgumentException("Base address can not be preserved");
+            }
+            resetCurrentBuilder(URI.create(newAddress));
+        } else {
+            resetBaseAddress(URI.create(newAddress));
+        }
+        return this;
+    }
+
+    /**
+     * Goes back
+     * @param fast if true then goes back to baseURI otherwise to a previous path segment
+     * @return updated WebClient
+     */
+    public WebClient back(boolean fast) {
+        getState().setTemplates(null);
+        if (fast) {
+            getCurrentBuilder().replacePath(getBaseURI().getPath());
+        } else {
+            URI uri = getCurrentURI();
+            if (uri == getBaseURI()) {
+                return this;
+            }
+            List<PathSegment> segments = JAXRSUtils.getPathSegments(uri.getPath(), false);
+            getCurrentBuilder().replacePath(null);
+            for (int i = 0; i < segments.size() - 1; i++) {
+                getCurrentBuilder().path(HttpUtils.fromPathSegment(segments.get(i)));
+            }
+
+        }
+        return this;
+    }
+
+    /**
+     * Replaces the current path with the new value.
+     * @param path new path value. If it starts from "/" then all the current
+     * path starting from the base URI will be replaced, otherwise only the
+     * last path segment will be replaced. Providing a null value is equivalent
+     * to calling back(true)
+     * @return updated WebClient
+     */
+    public WebClient replacePath(String path) {
+        if (path == null) {
+            return back(true);
+        }
+        back(path.startsWith("/") ? true : false);
+        return path(path);
+    }
+
+    /**
+     * Resets the current query
+     * @return updated WebClient
+     */
+    public WebClient resetQuery() {
+        return replaceQuery(null);
+    }
+
+    /**
+     * Replaces the current query with the new value.
+     * @param queryString the new value, providing a null is
+     *        equivalent to calling resetQuery().
+     * @return updated WebClient
+     */
+    public WebClient replaceQuery(String queryString) {
+        getCurrentBuilder().replaceQuery(queryString);
+        return this;
+    }
+
+    /**
+     * Replaces the header value with the new values.
+     * @param headerName headerValues
+     * @param value new values, null is equivalent to removing the header
+     * @return updated WebClient
+     */
+    public WebClient replaceHeader(String headerName, Object value) {
+        MultivaluedMap<String, String> headers = getState().getRequestHeaders();
+        headers.remove(headerName);
+        if (value != null) {
+            super.header(headerName, value);
+        }
+        return this;
+    }
+
+    /**
+     * Replaces the current query with the new value.
+     * @param queryParam query param name
+     * @param value the new value, providing a null is
+     *        equivalent to calling resetQuery().
+     * @return updated WebClient
+     */
+    public WebClient replaceQueryParam(String queryParam, Object... value) {
+        getCurrentBuilder().replaceQueryParam(queryParam, value);
+        return this;
+    }
+
+    @Override
+    public WebClient type(MediaType ct) {
+        return (WebClient)super.type(ct);
+    }
+
+    @Override
+    public WebClient type(String type) {
+        return (WebClient)super.type(type);
+    }
+
+    @Override
+    public WebClient accept(MediaType... types) {
+        return (WebClient)super.accept(types);
+    }
+
+    @Override
+    public WebClient accept(String... types) {
+        return (WebClient)super.accept(types);
+    }
+
+    @Override
+    public WebClient language(String language) {
+        return (WebClient)super.language(language);
+    }
+
+    @Override
+    public WebClient acceptLanguage(String ...languages) {
+        return (WebClient)super.acceptLanguage(languages);
+    }
+
+    @Override
+    public WebClient encoding(String encoding) {
+        return (WebClient)super.encoding(encoding);
+    }
+
+    @Override
+    public WebClient acceptEncoding(String ...encodings) {
+        return (WebClient)super.acceptEncoding(encodings);
+    }
+
+    @Override
+    public WebClient match(EntityTag tag, boolean ifNot) {
+        return (WebClient)super.match(tag, ifNot);
+    }
+
+    @Override
+    public WebClient modified(Date date, boolean ifNot) {
+        return (WebClient)super.modified(date, ifNot);
+    }
+
+    @Override
+    public WebClient cookie(Cookie cookie) {
+        return (WebClient)super.cookie(cookie);
+    }
+
+    @Override
+    public WebClient authorization(Object auth) {
+        return (WebClient)super.authorization(auth);
+    }
+
+    @Override
+    public WebClient header(String name, Object... values) {
+        return (WebClient)super.header(name, values);
+    }
+
+    @Override
+    public WebClient headers(MultivaluedMap<String, String> map) {
+        return (WebClient)super.headers(map);
+    }
+
+    @Override
+    public WebClient reset() {
+        //clearTemplates();
+        return (WebClient)super.reset();
+    }
+
+    protected Response doInvoke(String httpMethod,
+                                Object body,
+                                Type inGenericType,
+                                Class<?> responseClass,
+                                Type outGenericType) {
+        return doInvoke(httpMethod, body, body == null ? null : body.getClass(), inGenericType,
+            responseClass, outGenericType);
+    }
+
+
+
+    protected Response doInvoke(String httpMethod,
+                                Object body,
+                                Class<?> requestClass,
+                                Type inGenericType,
+                                Class<?> responseClass,
+                                Type outGenericType) {
+        Annotation[] inAnns = null;
+        if (body instanceof Entity) {
+            Entity<?> entity = (Entity<?>)body;
+            setEntityHeaders(entity);
+            body = entity.getEntity();
+            requestClass = body.getClass();
+            inGenericType = body.getClass();
+            inAnns = entity.getAnnotations();
+        }
+        if (body instanceof GenericEntity) {
+            GenericEntity<?> genericEntity = (GenericEntity<?>)body;
+            body = genericEntity.getEntity();
+            requestClass = genericEntity.getRawType();
+            inGenericType = genericEntity.getType();
+        }
+        MultivaluedMap<String, String> headers = prepareHeaders(responseClass, body);
+        resetResponse();
+        final Response r;
+        try {
+            r = doChainedInvocation(httpMethod, headers, body, requestClass, inGenericType,
+                                             inAnns, responseClass, outGenericType, null, null);
+        } finally {
+            resetResponseStateImmediatelyIfNeeded();
+        }
+
+        int status = r.getStatus();
+        if (status != 304 && status >= 300 && responseClass != Response.class) {
+            throw convertToWebApplicationException(r);
+        }
+        return r;
+    }
+
+    protected <T> Future<T> doInvokeAsyncCallback(String httpMethod,
+                                                  Object body,
+                                                  Class<?> requestClass,
+                                                  Type inType,
+                                                  InvocationCallback<T> callback) {
+
+        Type outType = getCallbackType(callback);
+        Class<?> respClass = getCallbackClass(outType);
+        return doInvokeAsync(httpMethod, body, requestClass, inType, respClass, outType, callback);
+    }
+
+    protected <T> Future<T> doInvokeAsync(String httpMethod,
+                                          Object body,
+                                          Class<?> requestClass,
+                                          Type inType,
+                                          Class<?> respClass,
+                                          Type outType,
+                                          InvocationCallback<T> callback) {
+        JaxrsClientCallback<T> cb = new JaxrsClientCallback<>(callback, respClass, outType);
+        prepareAsyncClient(httpMethod, body, requestClass, inType, respClass, outType, cb);
+        return cb.createFuture();
+    }
+
+    protected void prepareAsyncClient(String httpMethod,
+                                   Object body,
+                                   Class<?> requestClass,
+                                   Type inType,
+                                   Class<?> respClass,
+                                   Type outType,
+                                   JaxrsClientCallback<?> cb) {
+        Annotation[] inAnns = null;
+        if (body instanceof Entity) {
+            Entity<?> entity = (Entity<?>)body;
+            setEntityHeaders(entity);
+            body = entity.getEntity();
+            requestClass = body.getClass();
+            inType = body.getClass();
+            inAnns = entity.getAnnotations();
+        }
+        if (body instanceof GenericEntity) {
+            GenericEntity<?> genericEntity = (GenericEntity<?>)body;
+            body = genericEntity.getEntity();
+            requestClass = genericEntity.getRawType();
+            inType = genericEntity.getType();
+        }
+
+        MultivaluedMap<String, String> headers = prepareHeaders(respClass, body);
+        resetResponse();
+
+        Message m = finalizeMessage(httpMethod, headers, body, requestClass, inType,
+                                  inAnns, respClass, outType, null, null);
+
+        m.getExchange().setSynchronous(false);
+        setAsyncMessageObserverIfNeeded(m.getExchange());
+        m.getExchange().put(JaxrsClientCallback.class, cb);
+
+        doRunInterceptorChain(m);
+    }
+
+
+    private MultivaluedMap<String, String> prepareHeaders(Class<?> responseClass, Object body) {
+        MultivaluedMap<String, String> headers = getHeaders();
+        if (headers.getFirst(HttpHeaders.CONTENT_TYPE) == null && body instanceof Form) {
+            headers.putSingle(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED);
+        }
+
+        if (responseClass != null && responseClass != Response.class
+            && headers.getFirst(HttpHeaders.ACCEPT) == null) {
+            headers.putSingle(HttpHeaders.ACCEPT, MediaType.WILDCARD);
+        }
+        return headers;
+    }
+
+    class ClientAsyncResponseInterceptor extends AbstractClientAsyncResponseInterceptor {
+        @Override
+        protected void doHandleAsyncResponse(Message message, Response r, JaxrsClientCallback<?> cb) {
+            if (r == null) {
+                try {
+                    r = handleResponse(message.getExchange().getOutMessage(),
+                                       cb.getResponseClass(),
+                                       cb.getOutGenericType());
+                } catch (Throwable t) {
+                    cb.handleException(message, t);
+                    return;
+                } finally {
+                    completeExchange(message.getExchange(), false);
+                }
+            }
+            if (cb.getResponseClass() == null || Response.class.equals(cb.getResponseClass())) {
+                cb.handleResponse(message, new Object[] {r});
+            } else if (r.getStatus() >= 300) {
+                cb.handleException(message, convertToWebApplicationException(r));
+            } else {
+                cb.handleResponse(message, new Object[] {r.getEntity()});
+                closeAsyncResponseIfPossible(r, message, cb);
+            }
+        }
+    }
+
+
+
+    //TODO: retry invocation will not work in case of async request failures for the moment
+    @Override
+    protected Object retryInvoke(URI newRequestURI,
+                                 MultivaluedMap<String, String> headers,
+                                 Object body,
+                                 Exchange exchange,
+                                 Map<String, Object> invContext) throws Throwable {
+
+        Map<String, Object> reqContext = CastUtils.cast((Map<?, ?>)invContext.get(REQUEST_CONTEXT));
+        String httpMethod = (String)reqContext.get(Message.HTTP_REQUEST_METHOD);
+        Class<?> requestClass = (Class<?>)reqContext.get(REQUEST_CLASS);
+        Type inType = (Type)reqContext.get(REQUEST_TYPE);
+        Annotation[] inAnns = (Annotation[])reqContext.get(REQUEST_ANNS);
+        Class<?> respClass = (Class<?>)reqContext.get(RESPONSE_CLASS);
+        Type outType = (Type)reqContext.get(RESPONSE_TYPE);
+        return doChainedInvocation(httpMethod, headers, body, requestClass, inType,
+                                   inAnns, respClass, outType, exchange, invContext);
+    }
+    //CHECKSTYLE:OFF
+    protected Response doChainedInvocation(String httpMethod, //NOPMD
+                                           MultivaluedMap<String, String> headers,
+                                           Object body,
+                                           Class<?> requestClass,
+                                           Type inType,
+                                           Annotation[] inAnns,
+                                           Class<?> respClass,
+                                           Type outType,
+                                           Exchange exchange,
+                                           Map<String, Object> invContext) {
+    //CHECKSTYLE:ON
+        Bus configuredBus = getConfiguration().getBus();
+        Bus origBus = BusFactory.getAndSetThreadDefaultBus(configuredBus);
+        ClassLoaderHolder origLoader = null;
+        try {
+            ClassLoader loader = configuredBus.getExtension(ClassLoader.class);
+            if (loader != null) {
+                origLoader = ClassLoaderUtils.setThreadContextClassloader(loader);
+            }
+            Message m = finalizeMessage(httpMethod, headers, body, requestClass, inType,
+                                        inAnns, respClass, outType, exchange, invContext);
+            doRunInterceptorChain(m);
+            return doResponse(m, respClass, outType);
+        } finally {
+            if (origLoader != null) {
+                origLoader.reset();
+            }
+            if (origBus != configuredBus) {
+                BusFactory.setThreadDefaultBus(origBus);
+            }
+        }
+    }
+
+    //CHECKSTYLE:OFF
+    private Message finalizeMessage(String httpMethod, //NOPMD
+                                   MultivaluedMap<String, String> headers,
+                                   Object body,
+                                   Class<?> requestClass,
+                                   Type inGenericType,
+                                   Annotation[] inAnns,
+                                   Class<?> responseClass,
+                                   Type outGenericType,
+                                   Exchange exchange,
+                                   Map<String, Object> invContext) {
+   //CHECKSTYLE:ON
+        URI uri = getCurrentURI();
+        Message m = createMessage(body, httpMethod, headers, uri, exchange,
+                invContext, false);
+        setSupportOnewayResponseProperty(m);
+        if (inAnns != null) {
+            m.put(Annotation.class.getName(), inAnns);
+        }
+        Map<String, Object> reqContext = getRequestContext(m);
+        reqContext.put(Message.HTTP_REQUEST_METHOD, httpMethod);
+        reqContext.put(REQUEST_CLASS, requestClass);
+        reqContext.put(REQUEST_TYPE, inGenericType);
+        reqContext.put(REQUEST_ANNS, inAnns);
+        reqContext.put(RESPONSE_CLASS, responseClass);
+        reqContext.put(RESPONSE_TYPE, outGenericType);
+
+        if (body != null) {
+            m.put(Type.class, inGenericType);
+        }
+        m.getInterceptorChain().add(bodyWriter);
+
+        setWebClientOperationProperty(m, httpMethod);
+
+        return m;
+    }
+
+    private void setWebClientOperationProperty(Message m, String httpMethod) {
+        Object prop = m.getContextualProperty(WEB_CLIENT_OPERATION_REPORTING);
+        // Enable the operation reporting by default
+        if (prop == null || PropertyUtils.isTrue(prop)) {
+            UriBuilder absPathUri = super.getCurrentBuilder().clone();
+            absPathUri.replaceQuery(null);
+            setPlainOperationNameProperty(m, httpMethod + ":" + absPathUri.build().toString());
+        }
+
+    }
+
+    protected Response doResponse(Message m,
+                                  Class<?> responseClass,
+                                  Type outGenericType) {
+        try {
+            Object[] results = preProcessResult(m);
+            if (results != null && results.length == 1) {
+                return (Response)results[0];
+            }
+        } catch (WebApplicationException | ProcessingException ex) {
+            throw ex;
+        } catch (Exception ex) {
+            throw new ProcessingException(ex);
+        }
+
+        try {
+            return handleResponse(m, responseClass, outGenericType);
+        } finally {
+            completeExchange(m.getExchange(), false);
+        }
+    }
+
+    protected Response handleResponse(Message outMessage, Class<?> responseClass, Type genericType) {
+        try {
+            ResponseBuilder rb = setResponseBuilder(outMessage, outMessage.getExchange());
+            Response currentResponse = rb.clone().build();
+            ((ResponseImpl)currentResponse).setOutMessage(outMessage);
+
+            Object entity = readBody(currentResponse, outMessage, responseClass, genericType,
+                                     new Annotation[]{});
+
+            if (entity == null) {
+                int status = currentResponse.getStatus();
+                if (status >= 400) {
+                    entity = currentResponse.getEntity();
+                }
+            }
+            rb = JAXRSUtils.fromResponse(currentResponse, false);
+
+            rb.entity(entity instanceof Response
+                      ? ((Response)entity).getEntity() : entity);
+
+            Response r = rb.build();
+            getState().setResponse(r);
+            ((ResponseImpl)r).setOutMessage(outMessage);
+            return r;
+        } catch (ProcessingException ex) {
+            throw ex;
+        } catch (Throwable ex) {
+            throw new ProcessingException(ex);
+        } finally {
+            ClientProviderFactory.getInstance(outMessage).clearThreadLocalProxies();
+        }
+    }
+
+
+    private class BodyWriter extends AbstractBodyWriter {
+
+        protected void doWriteBody(Message outMessage,
+                                   Object body,
+                                   Type bodyType,
+                                   Annotation[] customAnns,
+                                   OutputStream os) throws Fault {
+
+            Map<String, Object> requestContext = WebClient.this.getRequestContext(outMessage);
+            Class<?> requestClass = null;
+            Type requestType = null;
+            if (requestContext != null) {
+                requestClass = (Class<?>)requestContext.get(REQUEST_CLASS);
+                requestType = (Type)requestContext.get(REQUEST_TYPE);
+            }
+            if (bodyType != null) {
+                requestType = bodyType;
+            }
+
+            Annotation[] anns = customAnns != null ? customAnns : new Annotation[]{};
+            boolean isAssignable = requestClass != null && requestClass.isAssignableFrom(body.getClass());
+            try {
+                writeBody(body, outMessage,
+                          requestClass == null || !isAssignable ? body.getClass() : requestClass,
+                          requestType == null || !isAssignable ? body.getClass() : requestType,
+                          anns, os);
+            } catch (Exception ex) {
+                throw new Fault(ex);
+            }
+        }
+    }
+
+    static void copyProperties(Client toClient, Client fromClient) {
+        AbstractClient newClient = toAbstractClient(toClient);
+        AbstractClient oldClient = toAbstractClient(fromClient);
+        newClient.setConfiguration(oldClient.getConfiguration());
+    }
+
+    private static AbstractClient toAbstractClient(Object client) {
+
+        if (client instanceof AbstractClient) {
+            return (AbstractClient)client;
+        } else if (client instanceof InvocationHandlerAware) {
+            return (AbstractClient)((InvocationHandlerAware)client).getInvocationHandler();
+        } else {
+            Object realObject = ClassHelper.getRealObject(client);
+            if (realObject instanceof AbstractClient) {
+                return (AbstractClient)realObject;
+            }
+        }
+        return null;
+    }
+
+    static JAXRSClientFactoryBean getBean(String baseAddress, String configLocation) {
+        JAXRSClientFactoryBean bean = new JAXRSClientFactoryBean();
+
+        if (configLocation != null) {
+            SpringBusFactory bf = new SpringBusFactory();
+            Bus bus = bf.createBus(configLocation);
+            bean.setBus(bus);
+        }
+        bean.setAddress(baseAddress);
+        return bean;
+    }
+
+    static ClientState getClientState(Client client) {
+        AbstractClient newClient = toAbstractClient(client);
+        if (newClient == null) {
+            return null;
+        }
+        return newClient.getState();
+    }
+
+    static URI convertStringToURI(String baseAddress) {
+        try {
+            return URI.create(baseAddress);
+        } catch (RuntimeException ex) {
+            // no need to check "https" scheme or indeed ':'
+            // as the relative address will not work as the base address
+            if (baseAddress.startsWith(HTTP_SCHEME)) {
+                return new UriBuilderImpl().uriAsTemplate(baseAddress).build();
+            }
+            throw ex;
+        }
+    }
+
+    // Link to JAX-RS 2.0 AsyncInvoker
+    public AsyncInvoker async() {
+        return new AsyncInvokerImpl(this);
+    }
+
+    // Link to JAX-RS 2.0 SyncInvoker
+    public SyncInvoker sync() {
+        return new SyncInvokerImpl(this);
+    }
+
+    // Link to JAX-RS 2.1 CompletionStageRxInvoker
+    public CompletionStageRxInvoker rx() {
+        return rx(lookUpExecutorService());
+    }
+
+    public CompletionStageRxInvoker rx(ExecutorService ex) {
+        return new CompletionStageRxInvokerImpl(this, ex);
+    }
+    // Link to JAX-RS 2.1 RxInvoker extensions
+    @SuppressWarnings("rawtypes")
+    public <T extends RxInvoker> T rx(Class<T> rxCls) {
+        return rx(rxCls, (ExecutorService)null);
+    }
+
+    @SuppressWarnings({
+        "rawtypes", "unchecked"
+    })
+    public <T extends RxInvoker> T rx(Class<T> rxCls, ExecutorService executorService) {
+        if (CompletionStageRxInvoker.class.isAssignableFrom(rxCls)) {
+            return (T)rx(executorService);
+        }
+        ClientProviderFactory pf =
+            ClientProviderFactory.getInstance(WebClient.getConfig(this).getEndpoint());
+        RxInvokerProvider rxProvider = pf.getRxInvokerProvider();
+        if (rxProvider != null && rxProvider.isProviderFor(rxCls)) {
+            return (T)rxProvider.getRxInvoker(sync(), executorService);
+        }
+        throw new IllegalStateException("Provider for " + rxCls.getName() + " is not available");
+    }
+
+    private void setEntityHeaders(Entity<?> entity) {
+        type(entity.getMediaType());
+        if (entity.getLanguage() != null) {
+            language(entity.getLanguage().toString());
+        }
+        if (entity.getEncoding() != null) {
+            encoding(entity.getEncoding());
+        }
+    }
+
+    private ExecutorService lookUpExecutorService() {
+        try {
+            javax.naming.InitialContext ic = new javax.naming.InitialContext();
+            Object execService = ic.lookup("java:comp/DefaultManagedExecutorService");
+            if (execService != null) {
+                return (ExecutorService)execService;
+            }
+        } catch (Throwable ex) {
+            // ignore
+        }
+        return null;
+    }
+}
diff --git a/transform/src/patch/java/org/apache/cxf/jaxrs/client/spec/ClientRequestContextImpl.java b/transform/src/patch/java/org/apache/cxf/jaxrs/client/spec/ClientRequestContextImpl.java
new file mode 100644
index 0000000..cddf5c6
--- /dev/null
+++ b/transform/src/patch/java/org/apache/cxf/jaxrs/client/spec/ClientRequestContextImpl.java
@@ -0,0 +1,186 @@
+/**
+ * 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.jaxrs.client.spec;
+
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.net.URI;
+import java.util.List;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientRequestContext;
+import javax.ws.rs.core.Configuration;
+import javax.ws.rs.core.GenericEntity;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+
+import org.apache.cxf.jaxrs.client.ClientProviderFactory;
+import org.apache.cxf.jaxrs.impl.AbstractRequestContextImpl;
+import org.apache.cxf.jaxrs.utils.HttpUtils;
+import org.apache.cxf.jaxrs.utils.InjectionUtils;
+import org.apache.cxf.jaxrs.utils.JAXRSUtils;
+import org.apache.cxf.message.Message;
+import org.apache.cxf.message.MessageContentsList;
+
+
+public class ClientRequestContextImpl extends AbstractRequestContextImpl
+    implements ClientRequestContext {
+
+    public ClientRequestContextImpl(Message m,
+                                    boolean responseContext) {
+        super(m, responseContext);
+    }
+
+    @Override
+    public MediaType getMediaType() {
+        if (!hasEntity()) {
+            return null;
+        }
+        Object mt = HttpUtils.getModifiableHeaders(m).getFirst(HttpHeaders.CONTENT_TYPE);
+        return mt instanceof MediaType ? (MediaType)mt : JAXRSUtils.toMediaType(mt.toString());
+    }
+
+    @Override
+    public Client getClient() {
+        return (Client)m.getContextualProperty(Client.class.getName());
+    }
+
+    @Override
+    public Configuration getConfiguration() {
+        ClientProviderFactory cpf = ClientProviderFactory.getInstance(m);
+        return cpf.getConfiguration(m);
+    }
+
+    private Object getMessageContent() {
+        MessageContentsList objs = MessageContentsList.getContentsList(m);
+        if (objs == null || objs.isEmpty()) {
+            return null;
+        }
+        return objs.get(0);
+    }
+
+    @Override
+    public Object getEntity() {
+        return getMessageContent();
+    }
+
+    @Override
+    public Annotation[] getEntityAnnotations() {
+        Annotation[] anns = (Annotation[])m.get(Annotation.class.getName());
+        return anns == null ? new Annotation[] {} : anns;
+    }
+
+    @Override
+    public Class<?> getEntityClass() {
+        Object entity = getEntity();
+        return entity == null ? null : entity.getClass();
+    }
+
+    @Override
+    public Type getEntityType() {
+        Type t = m.get(Type.class);
+        return t != null ? t : getEntityClass();
+    }
+
+    @Override
+    public OutputStream getEntityStream() {
+        return m.getContent(OutputStream.class);
+    }
+
+    @Override
+    public boolean hasEntity() {
+        return getEntity() != null;
+    }
+
+    @Override
+    public void setEntity(Object entity, Annotation[] anns, MediaType mt) {
+        if (mt != null) {
+            MultivaluedMap<String, Object> headers = getHeaders();
+            headers.putSingle(HttpHeaders.CONTENT_TYPE, mt);
+            m.put(Message.CONTENT_TYPE, mt.toString());
+        }
+        if (anns != null) {
+            m.put(Annotation.class.getName(), anns);
+        }
+        doSetEntity(entity);
+    }
+
+    @Override
+    public void setEntity(Object entity) {
+        doSetEntity(entity);
+    }
+
+    private void doSetEntity(Object entity) {
+        Object actualEntity = InjectionUtils.getEntity(entity);
+        m.setContent(List.class, actualEntity == null ? new MessageContentsList()
+            : new MessageContentsList(actualEntity));
+        if (entity != null) {
+            final Type type;
+            if (GenericEntity.class.isAssignableFrom(entity.getClass())) {
+                type = ((GenericEntity<?>)entity).getType();
+            } else {
+                type = entity.getClass();
+            }
+            m.put(Type.class, type);
+            m.remove("org.apache.cxf.empty.request");
+        }
+
+    }
+
+    @Override
+    public URI getUri() {
+        String requestURI = (String)m.get(Message.REQUEST_URI);
+        if (requestURI  == null) {
+            return null;
+        }
+        if (requestURI.startsWith("/")) {
+            String endpointAddress = (String)m.get(Message.ENDPOINT_ADDRESS);
+            requestURI = requestURI.length() == 1 ? endpointAddress : endpointAddress + requestURI;
+        }
+        return URI.create(requestURI);
+    }
+
+    @Override
+    public void setEntityStream(OutputStream os) {
+        m.setContent(OutputStream.class, os);
+
+    }
+
+    @Override
+    public void setUri(URI requestURI) {
+        m.put(Message.ENDPOINT_ADDRESS, requestURI.toString());
+        m.put(Message.REQUEST_URI, requestURI.toString());
+
+    }
+
+    @Override
+    public MultivaluedMap<String, Object> getHeaders() {
+        h = null;
+        return HttpUtils.getModifiableHeaders(m);
+    }
+
+    @Override
+    public MultivaluedMap<String, String> getStringHeaders() {
+        h = null;
+        return HttpUtils.getModifiableStringHeaders(m);
+    }
+
+}
diff --git a/transform/src/patch/java/org/apache/cxf/jaxrs/client/spring/JAXRSClientFactoryBeanDefinitionParser.java b/transform/src/patch/java/org/apache/cxf/jaxrs/client/spring/JAXRSClientFactoryBeanDefinitionParser.java
new file mode 100644
index 0000000..6543ddc
--- /dev/null
+++ b/transform/src/patch/java/org/apache/cxf/jaxrs/client/spring/JAXRSClientFactoryBeanDefinitionParser.java
@@ -0,0 +1,187 @@
+/**
+ * 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.jaxrs.client.spring;
+
+import java.io.IOException;
+import java.lang.annotation.Annotation;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import javax.ws.rs.Path;
+import javax.ws.rs.ext.Provider;
+import javax.xml.namespace.QName;
+
+import org.w3c.dom.Element;
+
+import org.apache.cxf.bus.spring.BusWiringBeanFactoryPostProcessor;
+import org.apache.cxf.common.util.ClasspathScanner;
+import org.apache.cxf.configuration.spring.AbstractFactoryBeanDefinitionParser;
+import org.apache.cxf.jaxrs.client.JAXRSClientFactoryBean;
+import org.apache.cxf.jaxrs.model.UserResource;
+import org.apache.cxf.jaxrs.utils.ResourceUtils;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.BeanCreationException;
+import org.springframework.beans.factory.BeanDefinitionStoreException;
+import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
+import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.xml.ParserContext;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+
+
+
+
+
+public class JAXRSClientFactoryBeanDefinitionParser extends AbstractFactoryBeanDefinitionParser {
+
+    public JAXRSClientFactoryBeanDefinitionParser() {
+        super();
+        setBeanClass(Object.class);
+    }
+
+    @Override
+    protected Class<?> getFactoryClass() {
+        return JAXRSSpringClientFactoryBean.class;
+    }
+
+    @Override
+    protected String getFactoryIdSuffix() {
+        return ".proxyFactory";
+    }
+
+    @Override
+    protected String getSuffix() {
+        return ".jaxrs-client";
+    }
+
+    @Override
+    protected void mapAttribute(BeanDefinitionBuilder bean, Element e, String name, String val) {
+        if ("serviceName".equals(name)) {
+            QName q = parseQName(e, val);
+            bean.addPropertyValue(name, q);
+        } else if ("basePackages".equals(name)) {
+            bean.addPropertyValue("basePackages", ClasspathScanner.parsePackages(val));
+        } else {
+            mapToProperty(bean, name, val);
+        }
+    }
+
+    @Override
+    protected void mapElement(ParserContext ctx, BeanDefinitionBuilder bean, Element el, String name) {
+        if ("properties".equals(name) || "headers".equals(name)) {
+            Map<?, ?> map = ctx.getDelegate().parseMapElement(el, bean.getBeanDefinition());
+            bean.addPropertyValue(name, map);
+        } else if ("executor".equals(name)) {
+            setFirstChildAsProperty(el, ctx, bean, "serviceFactory.executor");
+        } else if ("binding".equals(name)) {
+            setFirstChildAsProperty(el, ctx, bean, "bindingConfig");
+        } else if ("inInterceptors".equals(name) || "inFaultInterceptors".equals(name)
+            || "outInterceptors".equals(name) || "outFaultInterceptors".equals(name)) {
+            List<?> list = ctx.getDelegate().parseListElement(el, bean.getBeanDefinition());
+            bean.addPropertyValue(name, list);
+        } else if ("features".equals(name) || "providers".equals(name)
+                   || "schemaLocations".equals(name) || "modelBeans".equals(name)) {
+            List<?> list = ctx.getDelegate().parseListElement(el, bean.getBeanDefinition());
+            bean.addPropertyValue(name, list);
+        } else if ("model".equals(name)) {
+            List<UserResource> resources = ResourceUtils.getResourcesFromElement(el);
+            bean.addPropertyValue("modelBeans", resources);
+        } else {
+            setFirstChildAsProperty(el, ctx, bean, name);
+        }
+    }
+
+    public static class JAXRSSpringClientFactoryBean extends JAXRSClientFactoryBean
+        implements ApplicationContextAware {
+
+        private List<String> basePackages;
+
+        public JAXRSSpringClientFactoryBean() {
+            super();
+        }
+
+        public void setBasePackages(List<String> basePackages) {
+            this.basePackages = basePackages;
+        }
+
+        public void setApplicationContext(ApplicationContext ctx) throws BeansException {
+            try {
+                if (basePackages != null) {
+                    final Map< Class< ? extends Annotation >, Collection< Class< ? > > > classes =
+                        ClasspathScanner.findClasses(basePackages, Path.class, Provider.class);
+
+                    if (classes.get(Path.class).size() > 1) {
+                        throw new NoUniqueBeanDefinitionException(Path.class, classes.get(Path.class).size(),
+                            "More than one service class (@Path) has been discovered");
+                    }
+                    AutowireCapableBeanFactory beanFactory = ctx.getAutowireCapableBeanFactory();
+                    for (final Class< ? > providerClass: classes.get(Provider.class)) {
+                        Object bean;
+                        try {
+                            bean = beanFactory.createBean(providerClass,
+                                                   AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true);
+                        } catch (Exception ex) {
+                            bean = beanFactory.createBean(providerClass);
+                        }
+                        setProvider(bean);
+                    }
+
+                    for (final Class< ? > serviceClass: classes.get(Path.class)) {
+                        setServiceClass(serviceClass);
+                    }
+                }
+            } catch (IOException ex) {
+                throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
+            } catch (ClassNotFoundException ex) {
+                throw new BeanCreationException("Failed to create bean from classfile", ex);
+            }
+
+            if (bus == null) {
+                setBus(BusWiringBeanFactoryPostProcessor.addDefaultBus(ctx));
+            }
+        }
+    }
+
+    static Class<?> getServiceClass(Collection<Class<?>> rootClasses) {
+        for (Class<?> cls : rootClasses) {
+            if (cls.isInterface()) {
+                return cls;
+            }
+        }
+        return rootClasses.iterator().next();
+    }
+    static List<Object> getProviders(ApplicationContext context, Collection<Class<?>> providerClasses) {
+        List<Object> providers = new LinkedList<>();
+        AutowireCapableBeanFactory beanFactory = context.getAutowireCapableBeanFactory();
+        for (final Class< ? > providerClass: providerClasses) {
+            Object bean;
+            try {
+                bean = beanFactory.createBean(providerClass,
+                                       AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true);
+            } catch (Exception ex) {
+                bean = beanFactory.createBean(providerClass);
+            }
+            providers.add(bean);
+        }
+        return providers;
+    }
+}