CXF réponse sans noeud root

De EjnTricks
Révision de 5 avril 2012 à 01:06 par Etienne (discussion | contributions)

(diff) ← Version précédente | Voir la version courante (diff) | Version suivante → (diff)

Lors de la construction des réponses, le noeud root est systématique affiché dans la réponse. Or, il est parfois nécessaire de ne pas présenter cette information. L'article illustre les étapes à mettre en place pour obtenir une réponse du type:

{
 first: "firstValue",
 second: "secondValue"
}

La solution présentée a été déduite en effectuant du pas à pas dans les sources de CXF. Le code de cet exemple est disponible sous Tuto CXF.

Configuration Spring

La configuration définie:

  • Un service de classe fr.ejn.tutorial.ws.impl.ResponseWithCustomProviderImpl.
  • Un provider avec la valeur true pour la variable dropRootElement.
  • Un webservice dont l'adresse de base est /responseCustomProvider.
<!-- Class instance Webservice -->
<bean id="responseWithCustomProviderWebService" class="fr.ejn.tutorial.ws.impl.ResponseWithCustomProviderImpl" />

<!-- Custom provider -->
<bean id="jsonNoRootProvider" class="org.apache.cxf.jaxrs.provider.JSONProvider">
	<property name="dropRootElement" value="true" />
</bean>
 
<!-- Webservice -->
<jaxrs:server id="responseCustomProviderWsService" address="/responseCustomProvider">
	<jaxrs:properties>
		<entry key="service-list-path" value="/cxf" />
	</jaxrs:properties>
	<jaxrs:serviceBeans>
		<ref bean="responseWithCustomProviderWebService" />
	</jaxrs:serviceBeans>
	<jaxrs:providers>
		<ref bean="jsonNoRootProvider" />
	</jaxrs:providers>
	<jaxrs:extensionMappings>
		<entry key="json" value="application/json" />
	</jaxrs:extensionMappings>
</jaxrs:server>

Le provider est injecté dans le service REST à l'aide de la balise jaxrs:providers. L'objectif de cet article est alors atteint juste par cette configuration.

Interface

L'interface définie une seule méthode en mode GET sans argument.

package fr.ejn.tutorial.ws;

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.response.NoRootResponse;

/**
 * Rest service example for content download.
 * 
 * @author Etienne Jouvin
 * 
 */
@Path("")
@Produces(MediaType.APPLICATION_JSON)
public interface ResponseWithCustomProvider {

	/**
	 * @return Response to display.
	 */
	@GET
	@Path("/noRoot")
	NoRootResponse noRoot();

}


Classe instance

La classe d'instance utilisée pour le service REST permet de construire une instance de la réponse avec les valeurs:

  • firstValue pour le noeud first dans le JSON de réponse.
  • secondValue pour le noeud second dans le JSON de réponse.
package fr.ejn.tutorial.ws.impl;

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.ResponseWithCustomProvider;
import fr.ejn.tutorial.ws.response.NoRootResponse;

/**
 * @author Etienne Jouvin
 * 
 */
@Path("")
@Produces(MediaType.APPLICATION_JSON)
public class ResponseWithCustomProviderImpl implements ResponseWithCustomProvider {

	/** {@inheritDoc} */
	@GET
	@Path("/noRoot")
	public NoRootResponse noRoot() {
		NoRootResponse responseFirst = new NoRootResponse();

		responseFirst.setFirst("firstValue");
		responseFirst.setSecond("secondValue");
		return responseFirst;
	}

}


Objet réponse

Pour les besoins de ce tutorial, un DTO réponse a été créé avec deux variables exposées avec les getters:

  • getFirst qui produira le noeud first.
  • getSecond qui produira le noeud second.
package fr.ejn.tutorial.ws.response;

import javax.xml.bind.annotation.XmlRootElement;

/**
 * Response used to illustrate the root node name escape in the response.
 * 
 * @author Etienne Jouvin
 * 
 */
@XmlRootElement()
public class NoRootResponse {

	private String first;
	private String second;

	/**
	 * @return the first.
	 */
	public String getFirst() {
		return first;
	}

	/**
	 * @return the second.
	 */
	public String getSecond() {
		return second;
	}

	/**
	 * @param first the first to set.
	 */
	public void setFirst(String first) {
		this.first = first;
	}

	/**
	 * @param second the second to set.
	 */
	public void setSecond(String second) {
		this.second = second;
	}

}


Analyse

La création de la réponse d'un service REST est assez complexe au niveau CXF. Toutes les étapes ne seront pas décrites.

Astuce: pour analyser le fonctionnement de production de la réponse, il a été placé un point d'arrêt dans un des getter de la réponse. Ainsi, il est possible d'identifier les différents appels dans les nombreuses classes.

Après traitement du service REST, la réponse est construite à partir de la méthode processResponse de la classe org.apache.cxf.jaxrs.interceptor.JAXRSOutInterceptor

    private void processResponse(Message message) {
        
        if (isResponseAlreadyHandled(message)) {
            return;
        }
        
        MessageContentsList objs = MessageContentsList.getContentsList(message);
        if (objs == null || objs.size() == 0) {
            return;
        }
        
        Object responseObj = objs.get(0);
        
        Response response = null;
        if (responseObj instanceof Response) {
            response = (Response)responseObj;
        } else {
            int status = getStatus(message, responseObj != null ? 200 : 204);
            response = Response.status(status).entity(responseObj).build();
        }
        
        Exchange exchange = message.getExchange();
        OperationResourceInfo ori = (OperationResourceInfo)exchange.get(OperationResourceInfo.class
            .getName());

        List<ProviderInfo<ResponseHandler>> handlers = 
            ProviderFactory.getInstance(message).getResponseHandlers();
        for (ProviderInfo<ResponseHandler> rh : handlers) {
            InjectionUtils.injectContextFields(rh.getProvider(), rh, 
                                               message.getExchange().getInMessage());
            InjectionUtils.injectContextFields(rh.getProvider(), rh, 
                                               message.getExchange().getInMessage());
            Response r = rh.getProvider().handleResponse(message, ori, response);
            if (r != null) {
                response = r;
            }
        }
        
        serializeMessage(message, response, ori, true);        
    }


Au niveau de la méthode serializeMessage, l'instance de MessageBodyWriter est recherchée à partir de la configuration mise en place. Celle-ci sera alors utilisée pour construire la réponse renvoyée à l'appelant.

    @SuppressWarnings("unchecked")
    private void serializeMessage(Message message, 
                                  Response response, 
                                  OperationResourceInfo ori,
                                  boolean firstTry) {
        int status = response.getStatus();
        Object responseObj = response.getEntity();
        if (status == 200 && !isResponseNull(responseObj) && firstTry 
            && ori != null && JAXRSUtils.headMethodPossible(ori.getHttpMethod(), 
                (String)message.getExchange().getInMessage().get(Message.HTTP_REQUEST_METHOD))) {
            LOG.info(new org.apache.cxf.common.i18n.Message("HEAD_WITHOUT_ENTITY", BUNDLE).toString());
            responseObj = null;
        }
        if (status == -1) {
            status = isResponseNull(responseObj) ? 204 : 200;
        }
        
        boolean responseHeadersCopied = isResponseHeadersCopied(message);
        setResponseStatus(message, status, responseHeadersCopied);
        
        Map<String, List<Object>> theHeaders = 
            (Map<String, List<Object>>)message.get(Message.PROTOCOL_HEADERS);
        if (firstTry && theHeaders != null) {
            // some headers might've been setup by custom cxf interceptors
            theHeaders.putAll((Map)response.getMetadata());
        } else {
            theHeaders = response.getMetadata();
        }
        MultivaluedMap<String, Object> responseHeaders;
        if (!(theHeaders instanceof MultivaluedMap)) {
            responseHeaders = new MetadataMap<String, Object>(theHeaders);
        } else {
            responseHeaders = (MultivaluedMap)theHeaders;
        }
        message.put(Message.PROTOCOL_HEADERS, responseHeaders);
        
        setResponseDate(responseHeaders, firstTry);
        if (isResponseNull(responseObj)) {
            responseHeaders.putSingle("Content-Length", "0");
            return;
        }
        
        Object ignoreWritersProp = message.getExchange().get(JAXRSUtils.IGNORE_MESSAGE_WRITERS);
        boolean ignoreWriters = 
            ignoreWritersProp == null ? false : Boolean.valueOf(ignoreWritersProp.toString());
        if (ignoreWriters) {
            writeResponseToStream(message.getContent(OutputStream.class), responseObj);
            return;
        }
        
        List<MediaType> availableContentTypes = computeAvailableContentTypes(message, response);  
        
        Method invoked = null;
        if (firstTry) {
            invoked = ori == null ? null : ori.getAnnotatedMethod() == null
                ? ori.getMethodToInvoke() : ori.getAnnotatedMethod();
        }
        
        Class<?> targetType = getRawResponseClass(responseObj);
        Type genericType = getGenericResponseType(ori == null ? null : invoked, responseObj, targetType);
        if (genericType instanceof TypeVariable) {
            genericType = InjectionUtils.getSuperType(ori.getClassResourceInfo().getServiceClass(), 
                                                       (TypeVariable)genericType);
        }
        
        Annotation[] annotations = invoked != null ? invoked.getAnnotations() : new Annotation[]{};
        
        MessageBodyWriter writer = null;
        MediaType responseType = null;
        for (MediaType type : availableContentTypes) { 
            writer = ProviderFactory.getInstance(message)
                .createMessageBodyWriter(targetType, genericType, annotations, type, message);
            
            if (writer != null) {
                responseType = type;
                break;
            }
        }
    
        OutputStream outOriginal = message.getContent(OutputStream.class);
        if (writer == null) {
            message.put(Message.CONTENT_TYPE, "text/plain");
            message.put(Message.RESPONSE_CODE, 500);
            writeResponseErrorMessage(outOriginal, "NO_MSG_WRITER", targetType.getSimpleName());
            return;
        }
        boolean enabled = checkBufferingMode(message, writer, firstTry);
        Object entity = getEntity(responseObj);
        try {
            responseType = checkFinalContentType(responseType);
            if (LOG.isLoggable(Level.FINE)) {
                LOG.fine("Response content type is: " + responseType.toString());
            }
            message.put(Message.CONTENT_TYPE, responseType.toString());
            
            long size = writer.getSize(entity, targetType, genericType, annotations, responseType);
            if (size > 0) {
                LOG.fine("Setting ContentLength to " + size + " as requested by " 
                         + writer.getClass().getName());
                responseHeaders.putSingle(HttpHeaders.CONTENT_LENGTH, Long.toString(size));
            }
            if (LOG.isLoggable(Level.FINE)) {
                LOG.fine("Response EntityProvider is: " + writer.getClass().getName());
            }
            try {
                writer.writeTo(entity, targetType, genericType, 
                               annotations, 
                               responseType, 
                               responseHeaders, 
                               message.getContent(OutputStream.class));
                
                if (isResponseRedirected(message)) {
                    return;
                }
                
                Object newContentType = responseHeaders.getFirst(HttpHeaders.CONTENT_TYPE);
                if (newContentType != null) {
                    message.put(Message.CONTENT_TYPE, newContentType.toString());
                }
                checkCachedStream(message, outOriginal, enabled);
            } finally {
                if (enabled) {
                    message.setContent(OutputStream.class, outOriginal);
                    message.put(XMLStreamWriter.class.getName(), null);
                }
            }
            
        } catch (IOException ex) {
            handleWriteException(message, response, ori, ex, entity, firstTry);
        } catch (Throwable ex) {
            handleWriteException(message, response, ori, ex, entity, firstTry);
        }
    }


La méthode createMessageBodyWriter, de la classe org.apache.cxf.jaxrs.provider.ProviderFactory, cherche l'instance à utiliser en appelant la fonction chooseMessageWriter sur les deux listes internes:

  • messageWriters
  • jaxbWriters
    public <T> MessageBodyWriter<T> createMessageBodyWriter(Class<T> bodyType,
                                                            Type parameterType,
                                                            Annotation[] parameterAnnotations,
                                                            MediaType mediaType,
                                                            Message m) {
        // Try user provided providers
        MessageBodyWriter<T> mw = chooseMessageWriter(messageWriters, 
                                                      bodyType,
                                                      parameterType,
                                                      parameterAnnotations,
                                                      mediaType,
                                                      m);
        
        if (mw == null) {
            mw = chooseMessageWriter(jaxbWriters, 
                                     bodyType,
                                     parameterType,
                                     parameterAnnotations,
                                     mediaType,
                                     m);
        }
        
        if (mw != null || SHARED_FACTORY == this) {
            return mw;
        }
        
        return SHARED_FACTORY.createMessageBodyWriter(bodyType,
                                                  parameterType,
                                                  parameterAnnotations,
                                                  mediaType,
                                                  m);
    }

Attention par défaut la liste messageWriters est vide. Avec la configuration mise en place, ce ne sera plus le cas.


Par défaut, une instance de org.apache.cxf.jaxrs.provider.JSONProvider est retournée. Sur celle-ci, l'appel à la méthode writeTo va entraîner l'exécution de la méthode marshal.

    public void writeTo(Object obj, Class<?> cls, Type genericType, Annotation[] anns,  
        MediaType m, MultivaluedMap<String, Object> headers, OutputStream os)
        throws IOException {
        if (os == null) {
            StringBuilder sb = new StringBuilder();
            sb.append("Jettison needs initialized OutputStream");
            if (getContext() != null && getContext().getContent(XMLStreamWriter.class) == null) {
                sb.append("; if you need to customize Jettison output with the custom XMLStreamWriter"
                          + " then extend JSONProvider or when possible configure it directly.");
            }
            throw new IOException(sb.toString());
        }
        try {
            
            String enc = HttpUtils.getSetEncoding(m, headers, "UTF-8");
            if (Document.class.isAssignableFrom(cls)) {
                XMLStreamWriter writer = createWriter(obj, cls, genericType, enc, os, false);
                copyReaderToWriter(StaxUtils.createXMLStreamReader((Document)obj), writer);
                return;
            }
            if (InjectionUtils.isSupportedCollectionOrArray(cls)) {
                marshalCollection(cls, obj, genericType, enc, os, m, anns);
            } else {
                Object actualObject = checkAdapter(obj, cls, anns, true);
                Class<?> actualClass = obj != actualObject ? actualObject.getClass() : cls;
                if (cls == genericType) {
                    genericType = actualClass;
                }
                
                marshal(actualObject, actualClass, genericType, enc, os);
            }
            
        } catch (JAXBException e) {
            handleJAXBException(e, false);
        } catch (XMLStreamException e) {
            throw new WebApplicationException(e);
        } catch (Exception e) {
            throw new WebApplicationException(e);
        }
    }

Au niveau de la méthode marshal, une instance de Marshaller est construite. A ce niveau, c'est le principe standard de JAX-RS qui est utilisé, permettant de sérialiser des objets JAVA vers des formats type XML ou JSON.

    protected void marshal(Object actualObject, Class<?> actualClass, 
                           Type genericType, String enc, OutputStream os) throws Exception {
        
        actualObject = convertToJaxbElementIfNeeded(actualObject, actualClass, genericType);
        if (actualObject instanceof JAXBElement && actualClass != JAXBElement.class) {
            actualClass = JAXBElement.class;
        }
        
        Marshaller ms = createMarshaller(actualObject, actualClass, genericType, enc);
        marshal(ms, actualObject, actualClass, genericType, enc, os, false);
    }

Cette méthode marshal va ensuite permettre de créer un instance de XMLStreamWriter. Attention, malgré son nom, la classe produit bien du JSON.

    protected void marshal(Marshaller ms, Object actualObject, Class<?> actualClass, 
                  Type genericType, String enc, OutputStream os, boolean isCollection) throws Exception {
        OutputStream actualOs = os; 
        
        MessageContext mc = getContext();
        if (mc != null && MessageUtils.isTrue(mc.get(Marshaller.JAXB_FORMATTED_OUTPUT))) {
            actualOs = new CachedOutputStream();    
        }
        XMLStreamWriter writer = createWriter(actualObject, actualClass, genericType, enc, 
                                              actualOs, isCollection);
        ms.marshal(actualObject, writer);
        writer.close();
        if (os != actualOs) {
            StringIndenter formatter = new StringIndenter(
                IOUtils.newStringFromBytes(((CachedOutputStream)actualOs).getBytes()));
            Writer outWriter = new OutputStreamWriter(os, enc);
            IOUtils.copy(new StringReader(formatter.result()), outWriter, 2048);
            outWriter.close();
        }
    }


L'instance de XMLStreamWriter va donc produire la réponse. Durant sa construction, la variable dropRootElement, interne à la classe JSONProvider, est vérifiée afin de construire une instance de QName, initialisée par défaut à partir de l'objet réponse. Si la variable est positionnée à true, la partie localisation est utilisé pour surcharger l'instance.

    protected XMLStreamWriter createWriter(Object actualObject, Class<?> actualClass, 
        Type genericType, String enc, OutputStream os, boolean isCollection) throws Exception {
        
        QName qname = getQName(actualClass, genericType, actualObject, true);
        if (ignoreNamespaces && (isCollection  || dropRootElement)) {        
            qname = new QName(qname.getLocalPart());
        }
        if (BADGER_FISH_CONVENTION.equals(convention)) {
            return JSONUtils.createBadgerFishWriter(os);
        }
        
        Configuration config = 
            JSONUtils.createConfiguration(namespaceMap, 
                                          writeXsiType && !ignoreNamespaces,
                                          attributesToElements,
                                          typeConverter);
        
        XMLStreamWriter writer = JSONUtils.createStreamWriter(os, qname, 
             writeXsiType && !ignoreNamespaces, config, serializeAsArray, arrayKeys,
             isCollection || dropRootElement);
        writer = JSONUtils.createIgnoreMixedContentWriterIfNeeded(writer, ignoreMixedContent);
        writer = JSONUtils.createIgnoreNsWriterIfNeeded(writer, ignoreNamespaces);
        return createTransformWriterIfNeeded(writer, os);
    }

Dès à présent, il est compréhensible qu'il faut positionner true dans cette variable. Comme un setter est disponible, cette valeur est injectée via Spring au niveau de l'instance du provider.


Cette solution est assez élégante, car elle ne nécessite absolument aucune ligne de code. Cependant le provider étant positionné sur la déclaration du service REST, toutes les méthodes de celui-ci seront impactés par cette modification.