Hi,
I am integrating the Apache CXF 2.2.3 jaxrs frontend into the FraSCAti
platform (an SCA implementation).
During this work, I found some issues in the ClientProxyImpl class. See
the attached patched ClientProxyImpl class and a diff file.
The patch in method setRequestHeaders() around line 255 is for the case
a method is annotated with @Produces. I think that produce types if
defined must be put in request headers else text/plain or
application/xml must be put.
I am not sure but perhaps the loop in line 268 must be moved outside the
if statement.
The patch in method handleForm() at line 354 deals with the case where
the type of parameters is not String, e.g. an Integer, a Char, etc.
The last patch in method doChainedInvocation() at line 398 calls the
handleForm() method when required.
I hope these patches are adequate and could be integrated into a future
release of Apache CXF.
Best regards and A+
Philippe Merle
/**
* 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.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.URI;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.logging.Logger;
import javax.ws.rs.WebApplicationException;
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.common.i18n.BundleUtils;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.interceptor.AbstractOutDatabindingInterceptor;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.jaxrs.impl.MetadataMap;
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.model.URITemplate;
import org.apache.cxf.jaxrs.provider.ProviderFactory;
import org.apache.cxf.jaxrs.utils.FormUtils;
import org.apache.cxf.jaxrs.utils.InjectionUtils;
import org.apache.cxf.message.Message;
import org.apache.cxf.message.MessageContentsList;
import org.apache.cxf.phase.Phase;
import org.apache.cxf.transport.http.HTTPConduit;
/**
* Proxy-based client implementation
*
*/
public class ClientProxyImpl extends AbstractClient implements
InvocationHandlerAware, InvocationHandler {
private static final Logger LOG =
LogUtils.getL7dLogger(ClientProxyImpl.class);
private static final ResourceBundle BUNDLE =
BundleUtils.getBundle(ClientProxyImpl.class);
private ClassResourceInfo cri;
private boolean inheritHeaders;
private boolean isRoot;
private Map<String, Object> valuesMap;
public ClientProxyImpl(URI baseURI, URI currentURI, ClassResourceInfo cri,
boolean isRoot,
boolean inheritHeaders, Object... varValues) {
super(baseURI, currentURI);
this.cri = cri;
this.isRoot = isRoot;
this.inheritHeaders = inheritHeaders;
initValuesMap(varValues);
}
private void initValuesMap(Object... varValues) {
if (isRoot && varValues.length != 0) {
valuesMap = new LinkedHashMap<String, Object>();
List<String> vars = cri.getURITemplate().getVariables();
for (int i = 0; i < vars.size(); i++) {
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 = Collections.emptyMap();
}
}
/**
* 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
*/
public Object invoke(Object o, Method m, Object[] params) throws Throwable {
Class<?> declaringClass = m.getDeclaringClass();
if (Client.class == declaringClass || InvocationHandlerAware.class ==
declaringClass
|| Object.class == declaringClass) {
return m.invoke(this, params);
}
resetResponse();
OperationResourceInfo ori =
cri.getMethodDispatcher().getOperationResourceInfo(m);
if (ori == null) {
reportInvalidResourceMethod(m, "INVALID_RESOURCE_METHOD");
}
MultivaluedMap<ParameterType, Parameter> types = getParametersInfo(ori);
List<Object> pathParams = getPathParamValues(types, params, ori);
int bodyIndex = getBodyIndex(types, ori);
UriBuilder builder = getCurrentBuilder().clone();
if (isRoot) {
builder.path(ori.getClassResourceInfo().getURITemplate().getValue());
}
builder.path(ori.getURITemplate().getValue());
handleMatrixes(types, params, builder);
handleQueries(types, params, builder);
URI uri = builder.buildFromEncoded(pathParams.toArray()).normalize();
MultivaluedMap<String, String> headers = getHeaders();
MultivaluedMap<String, String> paramHeaders = new MetadataMap<String,
String>();
handleHeaders(paramHeaders, types, params);
handleCookies(paramHeaders, types, params);
if (ori.isSubResourceLocator()) {
ClassResourceInfo subCri = cri.getSubResource(m.getReturnType(),
m.getReturnType());
if (subCri == null) {
reportInvalidResourceMethod(m, "INVALID_SUBRESOURCE");
}
ClientProxyImpl proxyImpl = new ClientProxyImpl(getBaseURI(), uri,
subCri, false, inheritHeaders);
proxyImpl.setConfiguration(getConfiguration());
Object proxy = JAXRSClientFactory.create(m.getReturnType(),
proxyImpl);
if (inheritHeaders) {
WebClient.client(proxy).headers(headers);
}
WebClient.client(proxy).headers(paramHeaders);
return proxy;
}
headers.putAll(paramHeaders);
setRequestHeaders(headers, ori, types.containsKey(ParameterType.FORM),
bodyIndex == -1 ? null : params[bodyIndex].getClass(),
m.getReturnType());
return doChainedInvocation(uri, headers, ori, params, bodyIndex, types,
pathParams);
}
private static MultivaluedMap<ParameterType, Parameter>
getParametersInfo(OperationResourceInfo ori) {
MultivaluedMap<ParameterType, Parameter> map =
new MetadataMap<ParameterType, Parameter>();
List<Parameter> parameters = ori.getParameters();
if (parameters.size() == 0) {
return map;
}
for (Parameter p : parameters) {
if (p.getType() == ParameterType.CONTEXT) {
reportInvalidResourceMethod(ori.getMethodToInvoke(),
"NO_CONTEXT_PARAMETERS");
}
map.add(p.getType(), p);
}
if (map.containsKey(ParameterType.REQUEST_BODY)) {
if (map.get(ParameterType.REQUEST_BODY).size() > 1) {
reportInvalidResourceMethod(ori.getMethodToInvoke(),
"SINGLE_BODY_ONLY");
}
if (map.containsKey(ParameterType.FORM)) {
reportInvalidResourceMethod(ori.getMethodToInvoke(),
"ONLY_FORM_ALLOWED");
}
}
return map;
}
private static int getBodyIndex(MultivaluedMap<ParameterType, Parameter>
map,
OperationResourceInfo ori) {
List<Parameter> list = map.get(ParameterType.REQUEST_BODY);
int index = list == null ? -1 : list.get(0).getIndex();
if (ori.isSubResourceLocator() && index != -1) {
reportInvalidResourceMethod(ori.getMethodToInvoke(),
"NO_BODY_IN_SUBRESOURCE");
}
return index;
}
private static void checkResponse(Method m, Response r, Message message)
throws Throwable {
int status = r.getStatus();
if (status >= 400) {
if (m.getReturnType() == Response.class &&
m.getExceptionTypes().length == 0) {
return;
}
ResponseExceptionMapper<?> mapper = findExceptionMapper(m, message);
if (mapper != null) {
Throwable t = mapper.fromResponse(r);
if (t != null) {
throw t;
}
}
throw new WebApplicationException(r);
}
}
private static ResponseExceptionMapper<?> findExceptionMapper(Method m,
Message message) {
ProviderFactory pf = ProviderFactory.getInstance(message);
for (Class<?> exType : m.getExceptionTypes()) {
ResponseExceptionMapper<?> mapper =
pf.createResponseExceptionMapper(exType);
if (mapper != null) {
return mapper;
}
}
return null;
}
private 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 cType =
bodyClass != null && InjectionUtils.isPrimitive(bodyClass)
? MediaType.TEXT_PLAIN :
ori.getConsumeTypes().isEmpty()
|| ori.getConsumeTypes().get(0).equals(MediaType.WILDCARD)
? MediaType.APPLICATION_XML :
ori.getConsumeTypes().get(0).toString();
headers.putSingle(HttpHeaders.CONTENT_TYPE, cType);
}
}
List<MediaType> accepts = getAccept();
if (accepts == null) {
/* TODO: report this patch to Apache CXF
accepts = InjectionUtils.isPrimitive(responseClass)
? Collections.singletonList(MediaType.TEXT_PLAIN_TYPE)
: ori.getProduceTypes().size() == 0
|| ori.getConsumeTypes().get(0).equals(MediaType.WILDCARD_TYPE)
? Collections.singletonList(MediaType.APPLICATION_XML_TYPE) :
ori.getProduceTypes();
*/
accepts = ori.getProduceTypes().size() > 0
? ori.getProduceTypes()
: InjectionUtils.isPrimitive(responseClass)
? Collections.singletonList(MediaType.TEXT_PLAIN_TYPE)
: Collections.singletonList(MediaType.APPLICATION_XML_TYPE);
// TODO: Must the following loop be put outside this if statement?
for (MediaType mt : accepts) {
headers.add(HttpHeaders.ACCEPT, mt.toString());
}
}
return headers;
}
private List<Object> getPathParamValues(MultivaluedMap<ParameterType,
Parameter> map,
Object[] params,
OperationResourceInfo ori) {
List<Parameter> paramsList = getParameters(map, ParameterType.PATH);
List<Object> list = new LinkedList<Object>();
if (isRoot) {
list.addAll(valuesMap.values());
}
List<String> vars = ori.getURITemplate().getVariables();
// TODO : unfortunately, UriBuilder will lose a method-scoped parameter
// if a same name variable exists in a class scope which is an api bug.
// It's a rare case but we might want just to use UriBuilderImpl()
directly
// on the client side and tell it to choose the last variable value
for (Parameter p : paramsList) {
if (valuesMap.containsKey(p.getName()) &&
!vars.contains(p.getName())) {
int index = 0;
for (Iterator<String> it = valuesMap.keySet().iterator();
it.hasNext(); index++) {
if (it.next().equals(p.getName())) {
list.remove(index);
list.add(index, params[p.getIndex()]);
break;
}
}
} else {
String paramName = p.getName();
if (!"".equals(paramName)) {
list.add(params[p.getIndex()]);
} else {
MultivaluedMap<String, Object> values =
InjectionUtils.extractValuesFromBean(params[p.getIndex()], "");
for (String var : vars) {
list.addAll(values.get(var));
}
}
}
}
return list;
}
@SuppressWarnings("unchecked")
private static List<Parameter> getParameters(MultivaluedMap<ParameterType,
Parameter> map,
ParameterType key) {
return map.get(key) == null ? Collections.EMPTY_LIST : map.get(key);
}
private static void handleQueries(MultivaluedMap<ParameterType, Parameter>
map,
Object[] params,
UriBuilder ub) {
List<Parameter> qs = getParameters(map, ParameterType.QUERY);
for (Parameter p : qs) {
if (params[p.getIndex()] != null) {
addParametersToBuilder(ub, p.getName(), params[p.getIndex()],
ParameterType.QUERY);
}
}
}
private static void handleMatrixes(MultivaluedMap<ParameterType, Parameter>
map, Object[] params,
UriBuilder ub) {
List<Parameter> mx = getParameters(map, ParameterType.MATRIX);
for (Parameter p : mx) {
if (params[p.getIndex()] != null) {
addParametersToBuilder(ub, p.getName(), params[p.getIndex()],
ParameterType.MATRIX);
}
}
}
private MultivaluedMap<String, Object>
handleForm(MultivaluedMap<ParameterType, Parameter> map,
Object[] params) {
MultivaluedMap<String, Object> form = new MetadataMap<String, Object>();
List<Parameter> fm = getParameters(map, ParameterType.FORM);
for (Parameter p : fm) {
if (params[p.getIndex()] != null) {
// TODO: following must be reported to Apache CXF
// FormUtils.addPropertyToForm(form, p.getName(),
params[p.getIndex()]);
FormUtils.addPropertyToForm(form, p.getName(),
params[p.getIndex()].toString());
}
}
return form;
}
private void handleHeaders(MultivaluedMap<String, String> headers,
MultivaluedMap<ParameterType, Parameter> map,
Object[] params) {
List<Parameter> hs = getParameters(map, ParameterType.HEADER);
for (Parameter p : hs) {
if (params[p.getIndex()] != null) {
headers.add(p.getName(), params[p.getIndex()].toString());
}
}
}
private void handleCookies(MultivaluedMap<String, String> headers,
MultivaluedMap<ParameterType, Parameter> map,
Object[] params) {
List<Parameter> cs = getParameters(map, ParameterType.COOKIE);
for (Parameter p : cs) {
if (params[p.getIndex()] != null) {
headers.add(HttpHeaders.COOKIE, p.getName() + '=' +
params[p.getIndex()].toString());
}
}
}
private Object doChainedInvocation(URI uri, MultivaluedMap<String, String>
headers,
OperationResourceInfo ori, Object[] params, int
bodyIndex,
MultivaluedMap<ParameterType, Parameter> types,
List<Object> pathParams) throws Throwable {
Message m = createMessage(ori.getHttpMethod(), headers, uri);
if (pathParams.size() != 0) {
List<String> vars = ori.getURITemplate().getVariables();
MultivaluedMap<String, String> templatesMap = new
MetadataMap<String, String>(vars.size());
for (int i = 0; i < vars.size(); i++) {
if (i < pathParams.size()) {
templatesMap.add(vars.get(i), pathParams.get(i).toString());
}
}
m.put(URITemplate.TEMPLATE_PARAMETERS, templatesMap);
}
// TODO: Report to Apache CXF
// if (bodyIndex != -1 || types.containsKey(ParameterType.FORM)) {
boolean isForm = types.containsKey(ParameterType.FORM);
if (bodyIndex != -1 || isForm) {
m.setContent(OperationResourceInfo.class, ori);
m.put("BODY_INDEX", bodyIndex);
// TODO: Report to Apache CXF
// Object body = bodyIndex != -1 ? params[bodyIndex] :
handleForm(types, params);
Object body = isForm ? handleForm(types, params) :
params[bodyIndex];
MessageContentsList contents = new MessageContentsList(new
Object[]{body});
m.setContent(List.class, contents);
m.getInterceptorChain().add(new BodyWriter());
}
// execute chain
try {
m.getInterceptorChain().doIntercept(m);
} catch (Throwable ex) {
// we'd like a user to get the whole Response anyway if needed
}
// TODO : this needs to be done in an inbound chain instead
HttpURLConnection connect =
(HttpURLConnection)m.get(HTTPConduit.KEY_HTTP_CONNECTION);
return handleResponse(connect, m, ori);
}
protected Object handleResponse(HttpURLConnection connect, Message
outMessage, OperationResourceInfo ori)
throws Throwable {
Response r = setResponseBuilder(connect,
outMessage.getExchange().getInMessage()).clone().build();
Method method = ori.getMethodToInvoke();
checkResponse(method, r, outMessage);
if (method.getReturnType() == Void.class) {
return null;
}
if (method.getReturnType() == Response.class
&& (r.getEntity() == null ||
InputStream.class.isAssignableFrom(r.getEntity().getClass())
&& ((InputStream)r.getEntity()).available() == 0)) {
return r;
}
return readBody(r, connect, outMessage, method.getReturnType(),
method.getGenericReturnType(),
method.getDeclaredAnnotations());
}
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 WebApplicationException(405);
}
// TODO : what we really need to do is to refactor JAXRSOutInterceptor so
that
// it can handle both client requests and server responses - it may need to
be split into
// several interceptors - in fact we need to do the same for
JAXRSInInterceptor so that we can do
// on onMessage() properly
private class BodyWriter extends AbstractOutDatabindingInterceptor {
public BodyWriter() {
super(Phase.WRITE);
}
@SuppressWarnings("unchecked")
public void handleMessage(Message m) throws Fault {
OperationResourceInfo ori =
m.getContent(OperationResourceInfo.class);
OutputStream os = m.getContent(OutputStream.class);
if (os == null || ori == null) {
return;
}
MessageContentsList objs = MessageContentsList.getContentsList(m);
if (objs == null || objs.size() == 0) {
return;
}
MultivaluedMap<String, String> headers =
(MultivaluedMap)m.get(Message.PROTOCOL_HEADERS);
Method method = ori.getMethodToInvoke();
int bodyIndex = (Integer)m.get("BODY_INDEX");
Method aMethod = ori.getAnnotatedMethod();
Annotation[] anns = aMethod == null || bodyIndex == -1 ? new
Annotation[0]
:
aMethod.getParameterAnnotations()[bodyIndex];
Object body = objs.get(0);
try {
if (bodyIndex != -1) {
writeBody(body, m, body.getClass(),
method.getGenericParameterTypes()[bodyIndex],
anns, headers, os);
} else {
writeBody(body, m, body.getClass(), body.getClass(),
anns, headers, os);
}
os.flush();
} catch (Exception ex) {
throw new Fault(ex);
}
}
}
}
254a255
> /* TODO: report this patch to Apache CXF
259a261,268
> */
> accepts = ori.getProduceTypes().size() > 0
> ? ori.getProduceTypes()
> : InjectionUtils.isPrimitive(responseClass)
> ? Collections.singletonList(MediaType.TEXT_PLAIN_TYPE)
> : Collections.singletonList(MediaType.APPLICATION_XML_TYPE);
>
> // TODO: Must the following loop be put outside this if statement?
343c352,354
< FormUtils.addPropertyToForm(form, p.getName(),
params[p.getIndex()]);
---
> // TODO: following must be reported to Apache CXF
> // FormUtils.addPropertyToForm(form, p.getName(),
> params[p.getIndex()]);
> FormUtils.addPropertyToForm(form, p.getName(),
> params[p.getIndex()].toString());
386c397,400
< if (bodyIndex != -1 || types.containsKey(ParameterType.FORM)) {
---
> // TODO: Report to Apache CXF
> // if (bodyIndex != -1 || types.containsKey(ParameterType.FORM)) {
> boolean isForm = types.containsKey(ParameterType.FORM);
> if (bodyIndex != -1 || isForm) {
389c403,406
< Object body = bodyIndex != -1 ? params[bodyIndex] :
handleForm(types, params);
---
> // TODO: Report to Apache CXF
> // Object body = bodyIndex != -1 ? params[bodyIndex] :
> handleForm(types, params);
> Object body = isForm ? handleForm(types, params) :
> params[bodyIndex];
>
453c470
<
---
>