CXF utilisation boolean sur requête

De EjnTricks

Sur les arguments d'un service REST, lors de l'utilisation d'une classe personnalisée contenant des accesseurs à une variable de type boolean, il faut être vigilant à un point particulier. En effet, en utilisant la fonction Source → Generate Getters and Setters, les fonction isXXX et setXXX sont générées. Mais la variable boolean ne sera jamais mis à jour lors de l'appel. Cet article présente l'analyse de ce dysfonctionnement et présente la solution afin de pouvoir manipuler ce type de variable. Le code de cet exemple est disponible sous Tuto CXF.

Définition de l'argument

Une interface, facultative, et la classe d'instance sont créées pour modéliser les paramètres d'appel au service.

  • Interface
package fr.ejn.tutorial.ws.request;

/**
 * @author Etienne Jouvin
 * 
 */
public interface ParameterRequest {

	/**
	 * Require function for CXF.
	 * 
	 * @return isValidated execution
	 */
	boolean getValidated();

	/**
	 * @return the alwaysFalse
	 */
	boolean isAlwaysFalse();

	/**
	 * @return the validated
	 */
	boolean isValidated();

	/**
	 * @param alwaysFalse the alwaysFalse to set
	 */
	void setAlwaysFalse(boolean alwaysFalse);

	/**
	 * @param validated the validated to set
	 */
	void setValidated(boolean validated);

}
  • Classe d'instance.
package fr.ejn.tutorial.ws.request.impl;

import fr.ejn.tutorial.ws.request.ParameterRequest;

/**
 * Parameter bean for boolean parameters example.
 * 
 * @author Etienne Jouvin
 * 
 */
public class ParameterRequestImpl implements ParameterRequest {

	/**
	 * Variable with only getter isAlwaysFalse.
	 */
	private boolean alwaysFalse;
	/**
	 * Variable with both getter isValidated and getValidated.
	 */
	private boolean validated;

	/** {@inheritDoc} */
	public boolean getValidated() {
		return isValidated();
	}

	/** {@inheritDoc} */
	public boolean isAlwaysFalse() {
		return alwaysFalse;
	}

	/** {@inheritDoc} */
	public boolean isValidated() {
		return validated;
	}

	/** {@inheritDoc} */
	public void setAlwaysFalse(boolean alwaysFalse) {
		this.alwaysFalse = alwaysFalse;
	}

	/** {@inheritDoc} */
	public void setValidated(boolean validated) {
		this.validated = validated;
	}

}

Ces définitions vont pouvoir mettre en évidence le dysfonctionnement sur la variable alwaysFalse et la solution mise en oeuvre sur validated.


Définition su service

Le service REST est déclaré à partir de l'interface Parameter.

package fr.ejn.tutorial.ws;

import javax.ws.rs.FormParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import fr.ejn.tutorial.ws.request.impl.ParameterRequestImpl;
import fr.ejn.tutorial.ws.response.ParameterResponseDto;

/**
 * Rest service example for boolean arguments.
 * 
 * @author Etienne Jouvin
 * 
 */
@Path("")
@Produces(MediaType.APPLICATION_JSON)
public interface Parameter {

	/**
	 * Rest service to demonstrate requirements on boolean arguments.
	 * 
	 * @param parameterRequest ParameterRequest.
	 * @return Service response.
	 */
	@POST
	@Path("/validateParameters")
	ParameterResponseDto validateParameters(@FormParam("") ParameterRequestImpl parameterRequest);

}

Sa classe d'instance permet de lire la valeur des arguments et les retourner dans la réponse, permettant de voir les valeurs réceptionnées au niveau du serveur.

package fr.ejn.tutorial.ws.impl;

import javax.ws.rs.FormParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import fr.ejn.tutorial.ws.Parameter;
import fr.ejn.tutorial.ws.request.impl.ParameterRequestImpl;
import fr.ejn.tutorial.ws.response.ParameterResponseDto;

/**
 * Rest service instance for boolean parameter example.
 * 
 * @author Etienne Jouvin
 * 
 */
@Path("")
@Produces(MediaType.APPLICATION_JSON)
public class ParameterImpl implements Parameter {

	/** {@inheritDoc} */
	@POST
	@Path("/validateParameters")
	public ParameterResponseDto validateParameters(@FormParam("") ParameterRequestImpl parameterRequest) {
		/* Build the response with the argument received. */
		ParameterResponseDto parameterResponseDto = new ParameterResponseDto();

		parameterResponseDto.setAlwaysFalse(Boolean.valueOf(parameterRequest.isAlwaysFalse()));
		parameterResponseDto.setValidated(Boolean.valueOf(parameterRequest.isValidated()));

		return parameterResponseDto;
	}

}

Lors de l'utilisation de ce service, quelque soit la valeur envoyée pour alwaysFalse, la valeur retournée est toujours False. Une analyse de la lecture des arguments est nécessaire pour comprendre ce fonctionnement.

Analyse

La lecture des arguments, depuis la requête HTTP, s'effectue à travers la fonction createHttpParameterValue de la classe org.apache.cxf.jaxrs.utils.JAXRSUtils. L'argument étant déclaré avec l'annotation @FormParam, il sera lu à l'aide de la fonction processFormParam.

    public static Object createHttpParameterValue(Parameter parameter, 
                                            Class<?> parameterClass, 
                                            Type genericParam,
                                            Annotation[] paramAnns,
                                            Message message,
                                            MultivaluedMap<String, String> values,
                                            OperationResourceInfo ori) {
       
        boolean isEncoded = parameter.isEncoded() || ori != null && ori.isEncodedEnabled();
        String defaultValue = parameter.getDefaultValue();
        if (defaultValue == null && ori != null) {
            defaultValue = ori.getDefaultParameterValue();
        }
        
        Object result = null;
        
        if (parameter.getType() == ParameterType.PATH) {
            result = readFromUriParam(message, parameter.getName(), parameterClass, genericParam,
                                      paramAnns, values, defaultValue, !isEncoded);
        } 
        
        if (parameter.getType() == ParameterType.QUERY) {
            result = readQueryString(parameter.getName(), parameterClass, genericParam, 
                                     paramAnns, message, defaultValue, !isEncoded);
        }
        
        if (parameter.getType() == ParameterType.MATRIX) {
            result = processMatrixParam(message, parameter.getName(), parameterClass, genericParam,
                                        paramAnns, defaultValue, !isEncoded);
        }
        
        if (parameter.getType() == ParameterType.FORM) {
            result = processFormParam(message, parameter.getName(), parameterClass, genericParam, 
                                      paramAnns, defaultValue, !isEncoded);
        }
        
        if (parameter.getType() == ParameterType.COOKIE) {
            result = processCookieParam(message, parameter.getName(), parameterClass, genericParam,
                                        paramAnns, defaultValue);
        } 
        
        if (parameter.getType() == ParameterType.HEADER) {
            result = processHeaderParam(message, parameter.getName(), parameterClass, genericParam,
                                        paramAnns, defaultValue);
        } 

        return result;
    }

Le contenu de la fonction processFormParam ne permet pas encore de trouver la solution. Cependant, elle permet d'identifier un appel à InjectionUtils.handleBean dans le cas où la clé de l'argument est une chaîne vide.

    private static Object processFormParam(Message m, String key, 
                                           Class<?> pClass, Type genericType,
                                           Annotation[] paramAnns,
                                           String defaultValue,
                                           boolean decode) {
        
        MessageContext mc = new MessageContextImpl(m);
        MediaType mt = mc.getHttpHeaders().getMediaType();
        
        @SuppressWarnings("unchecked")
        MultivaluedMap<String, String> params = 
            (MultivaluedMap<String, String>)m.get(FormUtils.FORM_PARAM_MAP); 
        
        if (params == null) {
            params = new MetadataMap<String, String>();
            m.put(FormUtils.FORM_PARAM_MAP, params);
        
            if (mt == null || mt.isCompatible(MediaType.APPLICATION_FORM_URLENCODED_TYPE)) {
                String enc = HttpUtils.getEncoding(mt, "UTF-8");
                String body = FormUtils.readBody(m.getContent(InputStream.class), enc);
                HttpServletRequest request = (HttpServletRequest)m.get(AbstractHTTPDestination.HTTP_REQUEST);
                FormUtils.populateMapFromString(params, (String)body, enc, decode, request);
            } else {
                if (mt != null && "multipart".equalsIgnoreCase(mt.getType()) 
                    && MediaType.MULTIPART_FORM_DATA_TYPE.isCompatible(mt)) {
                    MultipartBody body = AttachmentUtils.getMultipartBody(mc);
                    FormUtils.populateMapFromMultipart(params, body, decode);
                } else {
                    org.apache.cxf.common.i18n.Message errorMsg = 
                        new org.apache.cxf.common.i18n.Message("WRONG_FORM_MEDIA_TYPE", 
                                                               BUNDLE, 
                                                               mt == null ? "*/*" : mt.toString());
                    LOG.warning(errorMsg.toString());
                    throw new WebApplicationException(415);
                }
            }
        }
        
        if ("".equals(key)) {
            return InjectionUtils.handleBean(pClass, paramAnns, params, ParameterType.FORM, m, false);
        } else {
            List<String> results = params.get(key);
    
            return InjectionUtils.createParameterObject(results, 
                                                        pClass, 
                                                        genericType,
                                                        paramAnns,
                                                        defaultValue,
                                                        false,
                                                        ParameterType.FORM,
                                                        m);
             
        }
    }

Cette dernière fonction analysée va permettre d'identifier la cause du dysfonctionnement.

   public static Object handleBean(Class<?> paramType, Annotation[] paramAnns, 
                                    MultivaluedMap<String, String> values,
                                    ParameterType pType, Message message, boolean decoded) {
        Object bean = null;
        try {
            if (paramType.isInterface()) {
                paramType = org.apache.cxf.jaxrs.utils.JAXBUtils.getValueTypeFromAdapter(paramType,
                                                                                         paramType, 
                                                                                         paramAnns);
            }
            bean = paramType.newInstance();
        } catch (IllegalAccessException ex) {
            reportServerError("CLASS_ACCESS_FAILURE", paramType.getName());
        } catch (Exception ex) {
            reportServerError("CLASS_INSTANTIATION_FAILURE", paramType.getName());
        }    
        
        Map<String, MultivaluedMap<String, String>> parsedValues =
            new HashMap<String, MultivaluedMap<String, String>>();
        for (Map.Entry<String, List<String>> entry : values.entrySet()) {
            String memberKey = entry.getKey();
            String beanKey = null;

            int idx = memberKey.indexOf('.');
            if (idx == -1) {
                beanKey = "." + memberKey;
            } else {
                beanKey = memberKey.substring(0, idx);
                memberKey = memberKey.substring(idx + 1);
            }

            MultivaluedMap<String, String> value = parsedValues.get(beanKey);
            if (value == null) {
                value = new MetadataMap<String, String>();
                parsedValues.put(beanKey, value);
            }
            value.put(memberKey, entry.getValue());
        }

        if (parsedValues.size() > 0) {
            for (Map.Entry<String, MultivaluedMap<String, String>> entry : parsedValues.entrySet()) {
                String memberKey = entry.getKey();

                boolean isbean = !memberKey.startsWith(".");
                if (!isbean) {
                    memberKey = memberKey.substring(1);
                }

                Object setter = null;
                Object getter = null;
                for (Method m : paramType.getMethods()) {
                    if (m.getName().equalsIgnoreCase("set" + memberKey)
                        && m.getParameterTypes().length == 1) {
                        setter = m;
                    } else if (m.getName().equalsIgnoreCase("get" + memberKey)
                        && m.getReturnType() != Void.TYPE) {
                        getter = m;
                    }
                    if (setter != null && getter != null) {
                        break;
                    }
                }
                if (setter == null) {
                    for (Field f : paramType.getFields()) {
                        if (f.getName().equalsIgnoreCase(memberKey)) {
                            setter = f;
                            getter = f;
                            break;
                        }
                    }
                }

                if (setter != null && getter != null) {
                    Class<?> type = null;
                    Type genericType = null;
                    Object paramValue = null;
                    if (setter instanceof Method) {
                        type = Method.class.cast(setter).getParameterTypes()[0];
                        genericType = Method.class.cast(setter).getGenericParameterTypes()[0];
                        paramValue = InjectionUtils.extractFromMethod(bean, (Method) getter);
                    } else {
                        type = Field.class.cast(setter).getType();
                        genericType = Field.class.cast(setter).getGenericType();
                        paramValue = InjectionUtils.extractFieldValue((Field) getter, bean);
                    }

                    List<MultivaluedMap<String, String>> processedValuesList =
                        processValues(type, genericType, entry.getValue(), isbean);

                    for (MultivaluedMap<String, String> processedValues : processedValuesList) {
                        if (InjectionUtils.isSupportedCollectionOrArray(type)) {
                            Object appendValue = InjectionUtils.injectIntoCollectionOrArray(type,
                                                            genericType, paramAnns, processedValues,
                                                            isbean, true,
                                                            pType, message);
                            paramValue = InjectionUtils.mergeCollectionsOrArrays(paramValue, appendValue,
                                                            genericType);
                        } else if (isSupportedMap(genericType)) {
                            Object appendValue = InjectionUtils.injectIntoMap(
                                type, genericType, paramAnns, processedValues, true, pType, message);
                            paramValue = InjectionUtils.mergeMap(paramValue, appendValue, genericType);

                        } else if (isbean) {
                            paramValue = InjectionUtils.handleBean(type, paramAnns, processedValues,
                                                            pType, message, decoded);
                        } else {
                            paramValue = InjectionUtils.handleParameter(
                                processedValues.values().iterator().next().get(0), 
                                decoded, type, paramAnns, pType, message);
                        }

                        if (paramValue != null) {
                            if (setter instanceof Method) {
                                InjectionUtils.injectThroughMethod(bean, (Method) setter, paramValue);
                            } else {
                                InjectionUtils.injectFieldValue((Field) setter, bean, paramValue);
                            }
                        }
                    }
                }
            }
        }
        
        return bean;
    }

L'analyse complète de la fonction n'est pas l'objectif principal. Après lecture des arguments depuis la requête HTTP, les accesseurs sont recherchés, pour chacun d'entre eux, à partir des méthodes déclarées dans la classe de l'argument, ParameterRequestImpl dans le cadre de cet article. En reprenant la variable alwaysFalse, les méthodes suivantes sont recherchées:

  • setAlwaysFalse
  • getAlwaysFalse

La fonction getAlwaysFalse n'étant pas trouvée, le traitement d'injection n'est pas pris en compte.


Solution

La solution a mettre en oeuvre est donc déduite facilement de l'analyse. Le getter doit être disponible sous la forme getXXX, même sur le type boolean pour lequel il est plus courant de voir la fonction isXXX. C'est pourquoi le fonctionnement est correct pour l'argument validated.