CXF Paramètre cookie

De EjnTricks

CXF est capable de prendre en compte les cookies receptionnés pour les utiliser dans les arguments des services. Cette technique a été mise en place pour accéder à un identifiant de session, ajouté automatiquement dans le header des requêtes pour gérer un mode SSO. Cependant, cette utilisation des cookies a posé plusieurs problèmes, méritant une petite explication.

Cet article se base sur la version 2.5.2 de CXF. Les sources de l'article, et d'autres tests, sont disponible sur le SVN


Hand-icon.png Votre avis

Nobody voted on this yet

 You need to enable JavaScript to vote


Bug-icon.png Problématique

Lors de la mise en place d'un webservice Rest consommant un cookie, de nom sessionId, la valeur n'était récupéré que dans deux cas particuliers.

  • C'était le seul cookie disponible, ce qui n'était jamais le cas car il y avait au moins le cookie de session du serveur applicatif.
  • Il se situait en première position dans la chaîne des cookies récupérés.

Dans le cas d'un service de SSO, un identifiant de session était stoqué dans le cookie iPlanetDirectoryPro avec une valeur du type AQIC5wM2LY4SfcwFUUM8WkdG51F7eaIgy1g+1/7Owgx1lO0=@AAJTSQACMDIAAlMxAAIwNA==#. Or seule la partie avant le premier caractère = n'était récupéré, soit AQIC5wM2LY4SfcwFUUM8WkdG51F7eaIgy1g+1/7Owgx1lO0

Cet article se propose de donner des solutions afin d'obtenir correctement la valeur du cookie.


Study icon.png Analyse

CXF étant opensource, l'analyse a été facilitée en effectuant du pas à pas au sein du code. La mise en place des classes de test a été réalisée ainsi.

XML format icon.png Configuration Spring

La configuration Spring permet de définir un service, classe fr.ejn.tutorial.ws.impl.CookieImpl, et un service REST dont l'adresse de base est /cxf/cookieNoIntercept.

	<!-- Class instance Webservice -->
	<bean id="cookieWebService" class="fr.ejn.tutorial.ws.impl.CookieImpl" />
 
	<!-- Webservice -->
	<jaxrs:server id="cookieNoInterceptWsService" address="/cookieNoIntercept">
		<jaxrs:properties>
			<entry key="service-list-path" value="/cxf" />
		</jaxrs:properties>
		<jaxrs:serviceBeans>
			<ref bean="cookieWebService" />
		</jaxrs:serviceBeans>
		<jaxrs:extensionMappings>
			<entry key="xml" value="application/xml" />
		</jaxrs:extensionMappings>
	</jaxrs:server>

Java format icon.png Interface

L'interface définie une seule méthode en mode GET, et acceptant un seul argument pour encapsuler la valeur du cookie à récupérer.

package fr.ejn.tutorial.ws;

import javax.ws.rs.CookieParam;
import javax.ws.rs.GET;
import javax.ws.rs.Path;

import fr.ejn.tutorial.ws.response.ListStringResponseDto;

/**
 * Interface for rest service example for cookie use.
 *
 * @author Etienne Jouvin
 *
 */
public interface Cookie {

	/**
	 * Example to retrieve the a cookie value.
	 *
	 * @param sessionId Cookie value.
	 * @return A response with the cookie name and values.
	 */
	@GET
	@Path("/cookie")
	ListStringResponseDto getCookie(@CookieParam("sessionId") String sessionId);

}

Java format icon.png Classe instance

La classe d'instance réceptionne le contenu dans l'argument sessionId pour le retourner dans une réponse d'instance ListStringResponseDto. L'annotation Path spécialise l'URL d'accès, qui est donc cxf/cookieNoIntercept/cookie.

package fr.ejn.tutorial.ws.impl;

import java.util.Arrays;

import javax.ws.rs.CookieParam;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import fr.ejn.tutorial.ws.Cookie;
import fr.ejn.tutorial.ws.response.ListStringResponseDto;

/**
 * Rest service example for cookie use.
 *
 * @author Etienne Jouvin
 *
 */
@Path("")
@Produces(MediaType.APPLICATION_JSON)
public class CookieImpl implements Cookie {

	/**
	 * Build the rest service response.
	 *
	 * @param sessionId Cookie value read.
	 * @return Service response.
	 */
	private ListStringResponseDto buildResponse(String sessionId) {
		ListStringResponseDto res = new ListStringResponseDto();

		res.setValue("sessionId");
		res.setValues(Arrays.asList(sessionId));

		return res;
	}

	/** {@inheritDoc} */
	@Override
	@GET
	@Path("/cookie")
	public ListStringResponseDto getCookie(@CookieParam("sessionId") String sessionId) {
		return this.buildResponse(sessionId);
	}

}

Mimetypes-html-icon.png Formulaire HTML

Afin de tester le service, un simple formulaire HTML est mis en place. ExtJs a été utilisé pour faciliter la manipulation des cookies dans cet exemple.

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Tutorial Téléverser Fichier avec CXF</title>
</head>
<body>

<script language="Javascript">

function submit(formId) {
	var obj = document.getElementById(formId);
	obj.submit();
}

function setCookieAndSubmit(formId) {
	var expireDate = new Date();
	expireDate = expireDate.add("mi", 2);
	
	Ext.util.Cookies.set("name", "Etienne", expireDate);
	Ext.util.Cookies.set("sessionId", "AQIC5wM2LY4SfcwFUUM8WkdG51F7eaIgy1g+1/7Owgx1lO0=@AAJTSQACMDIAAlMxAAIwNA==#", expireDate);
	Ext.util.Cookies.set("surname", "Jouvin", expireDate);

	submit(formId);
}

</script>

<form id="cookieNoIntercept" name="cookieNoIntercept"
	action="cxf/cookieNoIntercept/cookie" method="GET"
	target="_blank">
</form>
<a href="javascript:setCookieAndSubmit('cookieNoIntercept')">Send</a>

</body>
</html>

Le code Javascript permet de spécifier trois cookies avec une durée de rétention de deux minutes, pour limiter les impacts sur le poste lors des tests. L'objectif est de récupérer la valeur AQIC5wM2LY4SfcwFUUM8WkdG51F7eaIgy1g+1/7Owgx1lO0=@AAJTSQACMDIAAlMxAAIwNA==# dans la réponse du webservice REST.


Viewer icon.png Analyse CXF

Run-icon.png Exécution

Lors de la réception d'une requête, le traitement des arguments s'effectue au sein de la méthode createHttpParameterValue de la classe org.apache.cxf.jaxrs.util.JAXRSUtils.

    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;
    }


Ceci n'est qu'un point d'entrée pour appeler la méthode correspondant au type de la source. Pour le cas des cookies, ceci s'effectue à l'aide de la fonction processCookieParam.

    private static Object processCookieParam(Message m, String cookieName, 
                              Class<?> pClass, Type genericType, 
                              Annotation[] paramAnns, String defaultValue) {
        Cookie c = new HttpHeadersImpl(m).getCookies().get(cookieName);
        
        if (c == null && defaultValue != null) {
            c = Cookie.valueOf(cookieName + '=' + defaultValue);
        }
        if (c == null) {
            return null;
        }
        
        if (pClass.isAssignableFrom(Cookie.class)) {
            return c;
        }
        
        return InjectionUtils.handleParameter(c.getValue(), false, pClass, paramAnns, 
                                              ParameterType.COOKIE, m);
    }

La lecture du cookie s'effectue dans l'instruction mise en surbrillance. La classe HttpHeadersImpl n'est utilisée que pour récupérer les données relatives au "header" de la requête, depuis l'instance Message, utilisée par CXF pour trnasporter et compléter les arguments tout au long de l'appel.

    @SuppressWarnings("unchecked")
    public HttpHeadersImpl(Message message) {
        this.message = message;
        this.headers = new MetadataMap<String, String>(
            (Map<String, List<String>>)message.get(Message.PROTOCOL_HEADERS), true, true);
    }

La fonction la plus important est getCookies, qui d'après son utilisation permet de récupérer la valeur du cookie dont le nom est passé dans le paramètre cookieName. L'analyse du constructeur de la classe HttpHeadersImpl n'a peu d'intérêt sauf de retenir que les cookies sont potentiellement stockés dans une Map, récupérable depuis le message à l'aide de la clé Message.PROTOCOL_HEADERS.

Le code de la fonction getCookies est le suivant.

    public Map<String, Cookie> getCookies() {
        List<String> values = headers.get(HttpHeaders.COOKIE);
        if (values == null || values.isEmpty()) {
            return Collections.emptyMap();
        }
        
        Map<String, Cookie> cl = new HashMap<String, Cookie>();
        for (String value : values) {
            if (value == null) {
                continue;
            }
            List<String> cs = getHeaderValues(HttpHeaders.COOKIE, value, getCookieSeparator());
            for (String c : cs) {
                Cookie cookie = Cookie.valueOf(c);
                cl.put(cookie.getName(), cookie);
            }
        }
        return cl;
    }

L'analyse prend fin en suivant l'exécution de la fonction getHeaderValues, dont la liste résultat est utilisée pour construire la Map avec le nom du cookie comme clé et l'instance comme valeur. Il est attendu d'avoir une entrée avec la clé sessionId.

    private static final String DEFAULT_SEPARATOR = ",";

    private List<String> getHeaderValues(String headerName, String originalValue, String sep) {
        if (!originalValue.contains(QUOTE)
            || HEADERS_WITH_POSSIBLE_QUOTES.contains(headerName)) {
            String[] ls = originalValue.split(sep);
            if (ls.length == 1) {
                return Collections.singletonList(ls[0].trim());
            } else {
                List<String> newValues = new ArrayList<String>();
                for (String v : ls) {
                    newValues.add(v.trim());
                }
                return newValues;
            }
        }
        if (originalValue.startsWith("\"") && originalValue.endsWith("\"")) {
            String actualValue = originalValue.length() == 2 ? "" 
                : originalValue.substring(1, originalValue.length() - 1);
            return Collections.singletonList(actualValue);
        }
        List<String> values = new ArrayList<String>(4);
        Matcher m = COMPLEX_HEADER_PATTERN.matcher(originalValue);
        while (m.find()) {
            String val = m.group().trim();
            if (val.length() > 0) {
                values.add(val);
            }
        }
        return values;
    }

    private String getCookieSeparator() {
        Object cookiePropValue = message.getContextualProperty(COOKIE_SEPARATOR_PROPERTY);
        if (cookiePropValue != null) {
            return COOKIE_SEPARATOR_CRLF.equals(cookiePropValue.toString()) 
                ? "\r\n" : cookiePropValue.toString();
        } else {
            return DEFAULT_SEPARATOR;
        }
    }

La fonction getCookieSeparator est utilisée pour fournir le troisième argument, qui est généralement "," récupéré depuis DEFAULT_SEPARATOR, à l'appel de getHeaderValues.


Dans le cas de l'exemple, l'instruction headers.get(HttpHeaders.COOKIE); ne retourne une liste avec une seule valeur name=Etienne; sessionId=AQIC5wM2LY4SfcwFUUM8WkdG51F7eaIgy1g+1/7Owgx1lO0=@AAJTSQACMDIAAlMxAAIwNA==#; surname=Jouvin. La propriété contextuelle COOKIE_SEPARATOR_PROPERTY n'étant pas renseignée, le découpage de la chaîne ne donne qu'une seule valeur.

Donc lors de la construction des cookies, boucle sur la variable values, une seule instance de Cookie est mise en place avec le nom name. Toutes les autres valeurs sont perdues.

Warning-icon.png Conclusion

Dans l'état des choses, le fonctionnement standard n'est satisfaisant que dans le cas où le cookie souhaité se situe en première position. C'est donc par chance que cela peut fonctionner , mais surtout inutilisable si il faut récupérer plusieurs cookie.


Examples-icon.png Solutions

Ce paragraphe présente quelques alternatives pour remédier à la problématique soulevées. Au niveau de CXF, il existe la notion d'intercepteur que l'on peut ajouter sur les différentes phases de l'appel et de la réponse. Cette fonctionnalité va être utilisée.

Share-icon.png Modification du Header

La première solution possible, mais la plus coûteuse en développement, est de modifier la valeur dans le Header pour que l'instruction headers.get(HttpHeaders.COOKIE), dans la fonction getCookies retourne une liste avec une entrée pour chacun des cookies. Une classe héritant de l'interface org.apache.cxf.transport.http.AbstractHTTPDestination est écrite avec l'implémentation suivante de la fonction handleMessage.

	public void handleMessage(Message message) throws Fault {

		Object headers = message.get(Message.PROTOCOL_HEADERS);
		Object request = message.get(AbstractHTTPDestination.HTTP_REQUEST);
		if (request instanceof HttpServletRequest && headers instanceof Map) {
			HttpServletRequest httpServletRequest = (HttpServletRequest) request;

			/* Get cookie values from the headers and build the key / value pair according it. */
			/* This will be used during the next execution, because cookies with = in the value will be cut, when request from the HttpServletRequest instance. */
			Map<String, String> cookieValues = new HashMap<String, String>();
			Cookie tmpCookie = null;
			for (Enumeration<?> enumeration = httpServletRequest.getHeaders(HEADER_COOKIE_PARAM); enumeration.hasMoreElements();) {
				/* Get the cookie value... */
				String val = (String) enumeration.nextElement();
				if (null != val) {
					String[] values = val.split(";");
					for (String value : values) {
						tmpCookie = Cookie.valueOf(value);
						cookieValues.put(tmpCookie.getName(), tmpCookie.getValue());
					}
				}
			}

			/* Rebuild the cookie argument to fix a problem with separator. */
			javax.servlet.http.Cookie[] cookies = httpServletRequest.getCookies();
			if (null != cookies) {
				List<String> headerCookies = new ArrayList<String>(cookies.length);
				String value;
				for (javax.servlet.http.Cookie cookie : cookies) {
					/* Prefer to get the value previously read. */
					/* For a cookie with a value like AQIC5wM2LY4SfcwFUUM8WkdG51F7eaIgy1g+1/7Owgx1lO0=@AAJTSQACMDIAAlMxAAIwNA==# */
					/* the cookie in the HTTPServletRequest has the value AQIC5wM2LY4SfcwFUUM8WkdG51F7eaIgy1g+1/7Owgx1lO0= */
					value = cookieValues.get(cookie.getName());
					if (null == value) {
						value = cookie.getValue();
					}
					tmpCookie = new Cookie(cookie.getName(), value, cookie.getPath(), cookie.getDomain(), cookie.getVersion());
					headerCookies.add(tmpCookie.toString());
				}

				Map<Object, Object> requestHeaders = (Map<Object, Object>) headers;
				requestHeaders.put(HttpHeaders.COOKIE, headerCookies);
			}
		}
	}

Le principe est de travailler directement sur la requête HTTP pour en récupérer la vraie liste des cookie, httpServletRequest.getCookies(). Chacun de ces valeurs est alors parcourue pour être placée comme entrée dans une Map, elle même injectée dans le message sous la clé Message.PROTOCOL_HEADERS.

Cependant, dans le cas où le cookie contient le caractère =, la valeur est tronquée et une perte d'information en résulte. C'est pourquoi un pré traitement est réalisée à partir de la valeur récupérée depuis httpServletRequest avec la clé HEADER_COOKIE_PARAM. Cette valeur est découpée en fonction du caractère ;, donnant une liste de cookie. Cette dernière est parcouru pour avoir temporairement un mapping entre le nom et la valeur. Ce mapping est utilisé préférentiellement lors du parcours des cookies.

Il faut ensuite référencer l'intercepteur dans la configuration Spring.

	<!-- Class instance Webservice -->
	<bean id="cookieWebService" class="fr.ejn.tutorial.ws.impl.CookieImpl" />

	<!-- Interceptor -->
	<bean id="cookieInterceptor" class="fr.ejn.tutorial.cxf.phase.CookieInterceptor">
		<constructor-arg value="receive" type="java.lang.String" />
	</bean>
 
	<!-- Webservice -->
	<jaxrs:server id="cookieInterceptWsService" address="/cookieIntercept">
		<jaxrs:inInterceptors>
			<ref bean="cookieInterceptor"/>
		</jaxrs:inInterceptors>
		<jaxrs:properties>
			<entry key="service-list-path" value="/cxf" />
		</jaxrs:properties>
		<jaxrs:serviceBeans>
			<ref bean="cookieWebService" />
		</jaxrs:serviceBeans>
		<jaxrs:extensionMappings>
			<entry key="xml" value="application/xml" />
		</jaxrs:extensionMappings>
	</jaxrs:server>

	<jaxrs:server id="cookieNoInterceptWsService" address="/cookieNoIntercept">
		<jaxrs:properties>
			<entry key="service-list-path" value="/cxf" />
		</jaxrs:properties>
		<jaxrs:serviceBeans>
			<ref bean="cookieWebService" />
		</jaxrs:serviceBeans>
		<jaxrs:extensionMappings>
			<entry key="xml" value="application/xml" />
		</jaxrs:extensionMappings>
	</jaxrs:server>

Icon-Configuration-Settings.png Modification paramètre contextuel

Lors de l'analyse, il a été constaté que les cookies sont récupérés après avoir découpé une chaîne de caractère en fonction d'un séparateur. Ce séparateur est récupéré depuis un paramètre contextuel trouvé dans le message qui est véhiculé de couche en couche. Or lors de l'exécution en pas à pas, il est constaté que ce paramètre n'est pas renseigné et c'est la valeur par défaut, soit ,, qui est utilisée. Il suffit de renseigner ce paramètre dans le message, lors de l'exécution d'un interceptor.

Le code de celui-ci est particulièrement simple.

	/** {@inheritDoc} */
	@Override
	public void handleMessage(Message message) throws Fault {
		/* Change the default cookie separator used by CXF when dealing with cookies. */
		message.setContextualProperty("org.apache.cxf.http.cookie.separator", ";");
	}

A noter que le nom du paramètre est mis en dur et non récupéré depuis une constante comme dans la fonction getCookieSeparator.

    private String getCookieSeparator() {
        Object cookiePropValue = message.getContextualProperty(COOKIE_SEPARATOR_PROPERTY);
        if (cookiePropValue != null) {
            return COOKIE_SEPARATOR_CRLF.equals(cookiePropValue.toString()) 
                ? "\r\n" : cookiePropValue.toString();
        } else {
            return DEFAULT_SEPARATOR;
        }
    }

La variable DEFAULT_SEPARATOR est définie comme privée et ne peut pas être utilisée.

Warning-icon.png Il existe une petite subtilité dans cette implémentation. En effet, la variable message est une instance de org.apache.cxf.message.XMLMessage. Le code de la fonction setContextualProperty est.

    public void setContextualProperty(String key, Object v) {
        message.setContextualProperty(key, v);
    }

La méthode setContextualProperty de la variable message, instance de org.apache.cxf.message.MessageImpl, effectue avant tout un contrôle sur une variable interne.

    public void setContextualProperty(String key, Object v) {
        if (contextCache != null && !containsKey(key)) {
            contextCache.put(key, v);
        }
    }

Il est donc nécessaire que la variable contextCache soit déjà instanciée afin de mémoriser le paramètre contextuel. Cette contrainte va orienter la phase durant laquelle l'intercepteur doit être exécuté. Cette variable est initialisée lors du constructeur ou au premier appel de getContextualProperty.

    public MessageImpl(Message m) {
        super(m);
        if (m instanceof MessageImpl) {
            MessageImpl impl = (MessageImpl)m;
            exchange = impl.getExchange();
            id = impl.id;
            interceptorChain = impl.interceptorChain;
            defaultContents = impl.defaultContents;
            contents = impl.contents;
            contextCache = impl.contextCache;
        } else {
            throw new RuntimeException("Not a MessageImpl! " + m.getClass());
        }
    }

    public Object getContextualProperty(String key) {
        if (contextCache == null) {
            calcContextCache();
        }
        return contextCache.get(key);
    }

Lors de l'exécution en pas à pas, il est constaté que la fonction getContextualProperty est appelée lors de l'exécution de CertConstraintsInterceptor, dont la phase est pre-stream.

L'intercepteur personnalisé doit être placé après, par exemple durant la phase unmarshal. La configuration Spring mise en place est la suivante.

	<!-- Class instance Webservice -->
	<bean id="cookieWebService" class="fr.ejn.tutorial.ws.impl.CookieImpl" />

	<!-- Interceptor -->
	<bean id="cookieInterceptor" class="fr.ejn.tutorial.cxf.phase.CookieInterceptor">
		<constructor-arg value="receive" type="java.lang.String" />
	</bean>
 
	<bean id="cookieSeparatorInterceptor" class="fr.ejn.tutorial.cxf.phase.CookieSeparatorInterceptor">
		<constructor-arg value="unmarshal" type="java.lang.String" />
	</bean>

	<!-- Webservice -->
	<jaxrs:server id="cookieInterceptWsService" address="/cookieIntercept">
		<jaxrs:inInterceptors>
			<ref bean="cookieInterceptor"/>
		</jaxrs:inInterceptors>
		<jaxrs:properties>
			<entry key="service-list-path" value="/cxf" />
		</jaxrs:properties>
		<jaxrs:serviceBeans>
			<ref bean="cookieWebService" />
		</jaxrs:serviceBeans>
		<jaxrs:extensionMappings>
			<entry key="xml" value="application/xml" />
		</jaxrs:extensionMappings>
	</jaxrs:server>

	<jaxrs:server id="cookieSeparatorInterceptorWsService" address="/cookieSeparatorIntercept">
		<jaxrs:inInterceptors>
			<ref bean="cookieSeparatorInterceptor"/>
		</jaxrs:inInterceptors>
		<jaxrs:properties>
			<entry key="service-list-path" value="/cxf" />
		</jaxrs:properties>
		<jaxrs:serviceBeans>
			<ref bean="cookieWebService" />
		</jaxrs:serviceBeans>
		<jaxrs:extensionMappings>
			<entry key="xml" value="application/xml" />
		</jaxrs:extensionMappings>
	</jaxrs:server>

	<jaxrs:server id="cookieNoInterceptWsService" address="/cookieNoIntercept">
		<jaxrs:properties>
			<entry key="service-list-path" value="/cxf" />
		</jaxrs:properties>
		<jaxrs:serviceBeans>
			<ref bean="cookieWebService" />
		</jaxrs:serviceBeans>
		<jaxrs:extensionMappings>
			<entry key="xml" value="application/xml" />
		</jaxrs:extensionMappings>
	</jaxrs:server>


Bug-icon.png Encodage des cookies

Study icon.png Analyse

Sur le code source fourni en exemple, la manipulation des cookies s'effectue avec ExtJs. Ce framework encode la valeur du cookie pour échapper certain caractère comme =. Ainsi la valeur AQIC5wM2LY4SfcwFUUM8WkdG51F7eaIgy1g+1/7Owgx1lO0=@AAJTSQACMDIAAlMxAAIwNA==# est transmise ainsi AQIC5wM2LY4SfcwFUUM8WkdG51F7eaIgy1g+1/7Owgx1lO0%3D@AAJTSQACMDIAAlMxAAIwNA%3D%3D%23.

Or la fonction processCookieParam, dans la classe org.apache.cxf.jaxrs.utils.JAXRSUtils ne force pas le décodage de la valeur.

    private static Object processCookieParam(Message m, String cookieName, 
                              Class<?> pClass, Type genericType, 
                              Annotation[] paramAnns, String defaultValue) {
        Cookie c = new HttpHeadersImpl(m).getCookies().get(cookieName);
        
        if (c == null && defaultValue != null) {
            c = Cookie.valueOf(cookieName + '=' + defaultValue);
        }
        if (c == null) {
            return null;
        }
        
        if (pClass.isAssignableFrom(Cookie.class)) {
            return c;
        }
        
        return InjectionUtils.handleParameter(c.getValue(), false, pClass, paramAnns, 
                                              ParameterType.COOKIE, m);
    }

La fonction handleParameter exécute l'instruction decodeValue(value, decoded, pType), value étant le deuxième argument de la fonction soit false dans ce cas. Cela passe dans la focntion decodeValue de la classe org.apache.cxf.jaxrs.utils.InjectionUtils.

    public static String decodeValue(String value, boolean decode, ParameterType param) {
        if (!decode) {
            return value;
        }
        if (param == ParameterType.PATH || param == ParameterType.MATRIX) {
            return HttpUtils.pathDecode(value);
        } else {
            return HttpUtils.urlDecode(value);
        }
    }

Il n'existe donc aucune configuration possible et l'utilisation de l'annotation Encoded n'aura aucun effet.

Examples-icon.png Solutions

Plusieurs solutions sont possibles pour y remédier. Dans tous les cas, cela passe par l'appel à la fonction urlDecode de la classe org.apache.cxf.common.util.UrlUtils.

    /**
     * Decodes using URLDecoder - use when queries or form post values are decoded
     * @param value value to decode
     * @param enc encoding
     * @return
     */
    public static String urlDecode(String value, String enc) {
        try {
            value = URLDecoder.decode(value, enc);
        } catch (UnsupportedEncodingException e) {
            LOG.warning("UTF-8 encoding can not be used to decode " + value);          
        }
        return value;
    }
    
    public static String urlDecode(String value) {
        return urlDecode(value, "UTF-8");
    }

Share-icon.png Modification valeur

Il est possible de modifier la valeur directement dans la classe d'instance du Webservice ou bien en utilisant le premier intercepteur étudié. Ces solutions ne sont pas proposées dans le cadre de cet article, pour privilégier la deuxième solution.

Icon-Configuration-Settings.png Utilisation Adapter

Il est possible de décorer les paramètres des fonction avec l'annotation XmlJavaTypeAdapter pour laquelle il faut spécifier l'instance utilisée. Celle-ci est alors consommée durant la fonction handleParameter de la classe org.apache.cxf.jaxrs.utils.InjectionUtils.

    public static Object handleParameter(String value, 
                                         boolean decoded,
                                         Class<?> pClass,
                                         Annotation[] paramAnns,
                                         ParameterType pType,
                                         Message message) {
        
        if (value == null) {
            return null;
        }
        
        if (pType == ParameterType.PATH) {
            if (PathSegment.class.isAssignableFrom(pClass)) {
                return new PathSegmentImpl(value, decoded);   
            } else {
                value = new PathSegmentImpl(value, false).getPath();                 
            }
        }
        
        value = decodeValue(value, decoded, pType);
        
        
        if (pClass.isPrimitive()) {
            try {
                return PrimitiveUtils.read(value, pClass);
            } catch (NumberFormatException nfe) {
                //
                //  For path, query & matrix parameters this is 404,
                //  for others 400...
                //
                if (pType == ParameterType.PATH || pType == ParameterType.QUERY
                    || pType == ParameterType.MATRIX) {
                    throw new WebApplicationException(nfe, Response.Status.NOT_FOUND);
                }
                throw new WebApplicationException(nfe, Response.Status.BAD_REQUEST);
            }
        }
        
        boolean adapterHasToBeUsed = false;
        Class<?> valueType = JAXBUtils.getValueTypeFromAdapter(pClass, pClass, paramAnns);
        if (valueType != pClass) {
            pClass = valueType;
            adapterHasToBeUsed = true;
        }
        
        Object result = instantiateFromParameterHandler(value, pClass, message);
        if (result != null) {
            return result;
        }
        // check constructors accepting a single String value
        try {
            Constructor<?> c = pClass.getConstructor(new Class<?>[]{String.class});
            result = c.newInstance(new Object[]{value});
        } catch (NoSuchMethodException ex) {
            // try valueOf
        } catch (WebApplicationException ex) {
            throw ex;
        } catch (Exception ex) {
            result = createFromParameterHandler(value, pClass, message);
            if (result == null) {
                LOG.severe(new org.apache.cxf.common.i18n.Message("CLASS_CONSTRUCTOR_FAILURE", 
                                                                   BUNDLE, 
                                                                   pClass.getName()).toString());
                throw new WebApplicationException(ex, HttpUtils.getParameterFailureStatus(pType));
            }
        }
        if (result == null) {
            // check for valueOf(String) static methods
            String[] methodNames = pClass.isEnum() 
                ? new String[] {"fromString", "fromValue", "valueOf"} 
                : new String[] {"valueOf", "fromString"};
            for (String mName : methodNames) {   
                result = evaluateFactoryMethod(value, pClass, pType, mName);
                if (result != null) {
                    break;
                }
            }
        }
        
        if (result == null) {
            result = createFromParameterHandler(value, pClass, message);
        }
        
        if (result != null && adapterHasToBeUsed) {
            // as the last resort, try XmlJavaTypeAdapters
            try {
                result = JAXBUtils.convertWithAdapter(result, paramAnns);
            } catch (Throwable ex) {
                result = null; 
            }
        }
        
        if (result == null) {
            reportServerError("WRONG_PARAMETER_TYPE", pClass.getName());
        }
        
        return result;
    }

L'objectif est donc d'avoir un adaptateur qui traite soit capable de fournir une valeur de la classe du paramètre, soit String dans le cas présent, mais qui prend en entrée une instance d'une classe autre. Cette logique se situe dans la classe JAXBUtils.

Des tentatives d’instanciation de la classe d'entrée seront réalisées à partir du contructeur acceptant une chaîne de caractère comme paramètre, ou des fonction du type fromString / valueOf.

Pour finir, il suffit donc de créer une classe étendant fr.ejn.tutorial.xml.bind.annotation.adapters.CookieValueAdapter pour effectuer le décodage.

package fr.ejn.tutorial.xml.bind.annotation.adapters;

import javax.xml.bind.annotation.adapters.XmlAdapter;

import org.apache.cxf.common.util.UrlUtils;

import com.sun.xml.bind.v2.runtime.RuntimeUtil;

/**
 * Value adaptor used with CookieParam in order to decode the value. Inspired from the static class ToStringAdapter in RuntimeUtil.
 *
 *
 * @author Etienne Jouvin
 * @see RuntimeUtil.ToStringAdapter
 *
 */
public class CookieValueAdapter extends XmlAdapter<StringBuffer, String> {

	/** {@inheritDoc} */
	@Override
	public StringBuffer marshal(String o) throws Exception {
		throw new UnsupportedOperationException();
	}

	/** {@inheritDoc} */
	@Override
	public String unmarshal(StringBuffer v) throws Exception {
		String res = null;

		if (null != v) {
			res = UrlUtils.urlDecode(v.toString());
		}

		return res;
	}

}


Cette implémentation est ensuite utilisée dans la déclaration de la méthode du webservice REST.

package fr.ejn.tutorial.ws.impl;

import java.util.Arrays;

import javax.ws.rs.CookieParam;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

import fr.ejn.tutorial.ws.Cookie;
import fr.ejn.tutorial.ws.response.ListStringResponseDto;
import fr.ejn.tutorial.xml.bind.annotation.adapters.CookieValueAdapter;

/**
 * Rest service example for cookie use.
 *
 * @author Etienne Jouvin
 *
 */
@Path("")
@Produces(MediaType.APPLICATION_JSON)
public class CookieImpl implements Cookie {

	/**
	 * Build the rest service response.
	 *
	 * @param sessionId Cookie value read.
	 * @return Service response.
	 */
	private ListStringResponseDto buildResponse(String sessionId) {
		ListStringResponseDto res = new ListStringResponseDto();

		res.setValue("sessionId");
		res.setValues(Arrays.asList(sessionId));

		return res;
	}

	/** {@inheritDoc} */
	@Override
	@GET
	@Path("/cookie")
	public ListStringResponseDto getCookie(@CookieParam("sessionId") String sessionId) {
		return this.buildResponse(sessionId);
	}

	/** {@inheritDoc} */
	@Override
	@GET
	@Path("/cookieAdapter")
	public ListStringResponseDto getCookieWithAdapter(@XmlJavaTypeAdapter(CookieValueAdapter.class) @CookieParam("sessionId") String sessionId) {
		return this.buildResponse(sessionId);
	}

}