Proxyfier vos webservices avec ServiceMix/Camel

servicemix-logo

ServiceMix, ou de l’atelier du soir vers l’application en production.

Tout commence par une présentation interne Ippon de ServiceMix donnée sous forme d’atelier, atelier mené en avril dernier par Jean-Christophe Delmas et Fabien Arrault. Après plusieurs missions, durant lesquelles j’ai été amené à travailler sur différents ESB (JBoss ESB, Aqualogic Service Bus – ex OracleESB) et une formation avancée sur Mule, j’avais déjà acquis une certaine maturité sur les architectures SOA. Ma motivation première était donc de découvrir les notions de base de cet ESB et les principes du pattern EIP avec l’utilisation Camel.

Besoin client (société X)

Puis, dans la même semaine, vient l’expression d’un besoin client de la société X :

Il faudrait en effet remplacer une application développée en interne par X en .NET. Cette application, expose des webservices SOAP stateless qui utilisent un moteur qui parse des pages HTML d’un formulaire développé par une société externe Y, afin de retourner une réponse. Ce système, je vous l’accorde, est un peu “usine à gaz”, car il nécessite de ne surtout pas modifier la structure du formulaire, ou alors, de mettre à jour les webservices à chaque changement de formulaire.

WebServices ancien système

Heureusement, de nouveaux webservices ont été développés par la société Y. L’objectif est alors de les consommer, sans modifier le contrat de services de nos propres webservices existants , eux-mêmes consommés par d’autres clients. Il s’agit donc de développer un Proxy de webservices.

WebServices nouveau système

Mon client ne disposant pas encore d’ESB au sein de son SI, n’ayant pas de contrainte technique majeure, et étant libre de choisir la technologie à employer pour répondre au besoin; mon choix se tourne donc sur la mise en place ServiceMix.

Présentation de ServiceMix

ServiceMix (ou FuseESB) est un ESB (Entreprise Service Bus) open source full Java. Il s’agit d’un conteneur regroupant de nombreux outils, tels que : ActiveMQ, Camel, CXF, ODE, Karaf.

–         ActiveMQ pour gérer les messages au travers de queues JMS,

–         Camel s’appuie sur le pattern EIP (Entreprise Integration) pour faire du routage de messages, et intègre différents types de composants en endpoint (http, cxf, file, etc.),

–         CXF pour interroger des webservices,

–         Karaf nous permet de faire de l’OSGI et d’administrer ServiceMix via une console Shell.

–         ODE (Orchestration DirectorEngine) moteur WS-BPEL.

Un des objectifs d’un ESB est de centraliser tous les services que le S.I. met à disposition ainsi que les services d’applications tierces externes, sur un bus commun à l’ensemble des applications du S.I. Il peut exposer ses services sous différents formats (SOAP, REST, JMS, File) et permet à différentes applications de communiquer entre elles.

On évite ainsi les architectures dites « spaghettis ».

Architecture ESB
(source wikipedia)

Installation de ServiceMix

En pré-requis l’installation de ServiceMix nécessite une JDK 1.6.
Télécharger et dézipper Servicemix sur votre poste de développement et positionner la variable d’environnement

SERVICEMIX_HOME = D:\Java\apache-servicemix-4.4.1-fuse-06-03

Pour lancer ServiceMix, se positionner dans le répertoire bin/ et lancer

servicemix.bat

Celui-ci se lance en mode console.

ServiceMix console

Servicemix.bat server

permet de le lancer en mode serveur.

A terme, nous installerons ServiceMix en tant que Service NT, le client ayant son serveur de production sous windows (Un paragraphe est dédié à cette configuration plus loin). Cette console karaf peut-être accessible à distance via ssh (port 8101 par défaut), ce qui permet de l’administrer à travers le réseau.

Installation des features

Les features sont des modules qui peuvent être installés ou désinstallés en fonction des besoins. Ils permettent d’étendre les fonctionnalités par défaut de ServiceMix.

Voici ci-dessous, les principaux features à installer dans notre cas.

depuis la console karaf.

features:install webconsole
features:install camel-cxf
features:install camel-http
features:install camel-saxon
features:install servicemix-saxon

A noter que ces installations de features nécessitent d’avoir Maven configuré sur la machine et d’avoir une connexion internet active. Ces installations seront donc faites une seule fois sur le poste de développement puis copiées sur le serveur de production. Mais il est également possible de créer un repository local off-line de features et d’indiquer quelles features on souhaite « booter » au runtime. Une fois cette configuration effectuée, ces features sont mises en cache au premier démarrage du serveur.

La webconsole permet d’administrer la configuration du serveur et de visualiser les bundles installés et de les activer ou désactiver unitairement. L’url d’accès est la suivante :

http://localhost:8181/system/console/bundles

Mot de passe par défaut: smx/smx

ServiceMix webconsole

Il est possible également d’installer une webconsole pour l’administration des queues JMS, via le feature activemq-web-console.

Installation de Saxon

Pour profiter pleinement des fonctionnalités offertes par XSLT 2.0 (fonction tokenize, boucle for, etc.), nous installons l’API Saxon en remplacement de xalan par défaut. Voici la démarche à suivre :

Dans le %SERVICEMIX_HOME%/etc/system.properties, commenter et ajouter les lignes comme ceci :

#javax.xml.transform.TransformerFactory=org.apache.xalan.processor.TransformerFactoryImpl
javax.xml.transform.TransformerFactory=net.sf.saxon.TransformerFactoryImpl

Ajouter les jars suivants dans \lib\endorsed.

saxon-9.1.0.8.jar
saxon-sql-9.1.0.8.jar
saxon-dom-9.1.0.8.jar
saxon-xpath-9.1.0.8.jar

Maven Bundle Plugin

Le plugin maven-bundle-plugin permet de générer un bundle OSGI exploitable par ServiceMix.

 <build>
<plugins>
...
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<version>2.3.4</version>
<extensions>true</extensions>
<configuration>
<instructions>
  <Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
  <Import-Package>*,org.apache.camel.osgi</Import-Package>
  <Private-Package>com.x.esb.booking</Private-Package>
</instructions>
</configuration>
</plugin>
</plugins>
</build>

Les bundles à déployer dans ServiceMix doivent être compilés en JDK1.6.

Nota 1 :

Voici une astuce pour compiler avec la JDK 1.6, lorsque le projet principal Maven utilise une JDK 1.5, ce qui est le cas sur le projet du client. Il suffit de définir un fichier toolchains.xml dans le répertoire <USER_HOME>/.m2 qui définit l’ensemble des jdks installées sur la machine.

<?xml version="1.0" encoding="UTF8"?>
<toolchains>
 <toolchain>
   <type>jdk</type>
   <provides>
   <version><strong>1.5</strong></version>
   <vendor><strong>sun</strong></vendor>
   <id>default</id>
   </provides>
   <configuration>
   <jdkHome>D:/Java/jdk1.5</jdkHome>
   </configuration>
    </toolchain>
    <toolchain>
   <type>jdk</type>
   <provides>
   <version><strong>1.6</strong></version>
   <vendor><strong>sun</strong></vendor>
   <id>jdk16</id>
   </provides>
   <configuration>
   <jdkHome>D:/Java/jdk1.6.0_33</jdkHome>
   </configuration>
    </toolchain>
    <toolchain>
   <type>jdk</type>
   <provides>
   <version><strong>1.7</strong></version>
   <vendor><strong>sun</strong></vendor>
   <id>jdk17</id>
   </provides>
   <configuration>
   <jdkHome>D:/Java/jdk1.7.0_05</jdkHome>
   </configuration>
 </toolchain>
</toolchains>

et au niveau du pom.xml du projet, de spécifier le jdk que l’on souhaite utiliser pour compiler ce sous-module.

<build>
<plugins>
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-toolchains-plugin</artifactId>
  <version>1.0</version>
  <executions>
  <execution>
  <phase>validate</phase>
  <goals>
    <goal>toolchain</goal>
  </goals>
  </execution>
  </executions>
  <configuration>
    <toolchains>
    <jdk>
      <version><strong>1.6</strong></version>
      <vendor><strong>sun</strong></vendor>
    </jdk>
    </toolchains>
  </configuration>
</plugin>
</plugins>
</build>

Commandes utiles

Depuis le shell karaf, afficher les logs

log:tail –n 5

lister les features

features:list

Nota 2 : Service NT

Sous windows, Installer ServiceMix en tant que service NT

karaf@root> features:install wrapper
karaf@root> wrapper:install
Creating file: D:\Java\apache-servicemix-4.4.1-fuse-06-03\bin\karaf-wrapper.exe
Creating file: D:\Java\apache-servicemix-4.4.1-fuse-06-03\etc\karaf-wrapper.conf
Creating file: D:\Java\apache-servicemix-4.4.1-fuse-06-03\bin\karaf-service.bat
Creating file: D:\Java\apache-servicemix-4.4.1-fuse-06-03\lib\wrapper.dll
Creating file: D:\Java\apache-servicemix-4.4.1-fuse-06-03\lib\karaf-wrapper.jar
Creating file: D:\Java\apache-servicemix-4.4.1-fuse-06-03\lib\karaf-wrapper-main.jar
Setup complete.  You may want to tweak the JVM properties in the wrapper configuration file:
D:\Java\apache-servicemix-4.4.1-fuse-06-03\etc\karaf-wrapper.conf
before installing and starting the service.To install the service, run:
C:> D:\Java\apache-servicemix-4.4.1-fuse-06-03\bin\karaf-service.bat installOnce installed, to start the service run:
C:> net start "karaf"Once running, to stop the service run:
C:> net stop "karaf"Once stopped, to remove the installed the service run:
C:> D:\Java\apache-servicemix-4.4.1-fuse-06-03\bin\karaf-service.bat remove

1er cas : Proxy de webservices sans transformation de message

WebServices cas1

Les webservices de la société Y peuvent être exposés via l’ESB de la société X. Il est recommandé d’utiliser le bus de service qui permet de centraliser tous les appels et de rendre transparent l’appel direct via l’url des WebServices Y. Il s’agit d’un Proxy CXF de WebServices. Dans notre cas, sans transformation de message XSLT.

L’autre intérêt est que si les WebServices Y évoluent et que l’on ne souhaite pas modifier la signature existante, il est possible de transformer le message sans impacter l’application cliente. Nous verrons cela dans les cas suivants.

On route ainsi directement les messages au format MESSAGE sur les urls http de la société Y. C’est-à-dire que chaque endpoint cxf exposé par l’ESB de la société X route le message vers le webservice correspondant de la société Y.

Extrait du fichier META-INF/spring/camel-context.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans …">

<osgix:cm-properties id="preProps" persistent-id="com.x.esb.y">
  <prop key="formatMessage">MESSAGE</prop>

  <!-- override com.x.esb.y.cfg in /etc -->
  <prop key="wsXUrlPackage">{{wsXUrl}}/PackageWS</prop>
  <prop key="wsYUrlPackage">{{wsYUrl}}/Package/WebServices/PackageWS</prop>
  <prop key="wsXUrlTravelBooking">{{wsXUrl}}/TravelBookingWS</prop>
  <prop key="wsYUrlTravelBooking">{{wsYUrl}}/TravelBooking/WebServices/TravelBookingWS</prop>
</osgix:cm-properties>

<ctx:property-placeholder properties-ref="preProps" />

<!-- CXF Endpoints -->

<cxf:cxfEndpoint id="packageService"
        address="${wsXUrlPackage}"
        endpointName="s:PackagePort"
        serviceName="s:PackageService"
        wsdlURL="x/y/wsdl/Package/wsdl/PackageService.wsdl"
        xmlns:s="urn:com:ext:y:pkg:v2"/>

<cxf:cxfEndpoint id="travelBookingService"
        address="${wsXUrlTravelBooking}"
        endpointName="s:TravelBookingPort"
        serviceName="s:TravelBookingService"
        wsdlURL="x/y/wsdl/TravelBooking/wsdl/TravelBookingService.wsdl"
        xmlns:s="urn:com:ext:y:travelbooking:v2"/>
 ...

<osgi:camelContext xmlns="http://camel.apache.org/schema/spring" trace="false">

<propertyPlaceholder id="properties" location="ref:preProps" />

<route>
  <from uri="cxf:bean:packageService?dataFormat={{formatMessage}}"/>

  <convertBodyTo type="java.lang.String" />
  <to uri="log:input"/>

  <to uri="{{wsYUrlPackage}}" />
  <convertBodyTo type="java.lang.String" />
  <to uri="log:output"/>
</route>

<route>
  <from uri="cxf:bean:travelBookingService?dataFormat={{formatMessage}}"/>

  <convertBodyTo type="java.lang.String" />
  <to uri="log:input"/>

  <to uri="{{wsYUrlTravelBooking}}" />
  <convertBodyTo type="java.lang.String" />
  <to uri="log:output"/>
</route>

…
</osgi:camelContext>

</beans>

La route Camel est dans cet exemple écrite au format XML, mais il est possible de le faire en Java en implémentant RouteBuilder.Voir le post suivant : Ayez le reflexe Camel.
La balise osgix:cm-properties permet de récupérer des propriétés de configuration (ex: l’url des webservices appelées) dans le fichier %SERVICE_MIX%/etc/com.x.esb.y.cfg, administrables via la console web.
La balise cxf:cxfEndpoint permet de définir des points d’entrée des webservices exposés.
La balise to:uri permet d’invoquer un composant camel, par exemple le composant http pour invoquer une url WS, ou log pour logger.
Pour chaque webservice/route, nous nous contentons simplement de tracer le message transitant dans le bus de service.

2ème cas : Proxy de webservices avec simple transformation de message

Fonctionnellement, on a un webservice BookingServices dont on expose le WSDL en point d’entrée (WSDL initialement développé en .NET dans l’ancien système). Ce dernier accepte différents types de messages (GetDispo, GetPriceBeforeBooking, MakeBooking).

<cxf:cxfEndpoint id="bookingServices"
        address="${wsBookingUrl}"
        endpointName="s:BookingServicesSoap"
        serviceName="s:BookingServices"
        wsdlURL="x/booking/wsdl/BookingServices.wsdl"
        xmlns:s="http://bookingservices.x.com/"/>

<osgi:camelContext xmlns="http://camel.apache.org/schema/spring" trace="false">

<route>
<from uri="cxf:bean:bookingServices?dataFormat={{formatMessage}}"/>
…
</route>

Il s’agit de router le message en fonction de son contenu (pattern CBR Content Based Route) ou de l’opération appelante sans pour le moment faire de transformation. Pour cela on utilise les balises choice/when et redirige le message sur la route correspondante.

<route>
<from uri="cxf:bean:bookingServices?dataFormat=PAYLOAD"/>

<choice>
  <when>
    <simple>${in.header.SOAPAction} contains 'GetDispo'</simple>
    <to uri="direct:getDispo" />
  </when>
  <when>
    <simple>${in.header.SOAPAction} contains 'GetPriceBeforeBooking'</simple>
    <to uri="direct:getPriceBeforeBooking" />
  </when>
  <when>
    <simple>${in.header.SOAPAction} contains 'MakeBooking'</simple>
    <to uri="direct:makeBooking" />
  </when>
  <otherwise>
  </otherwise>
</choice>
</route>

WebServices cas2

Puis, XSLT va nous permettre de transformer le message émis par la société X au format de la société Y. Chaque route fait appel au composant XSLT, une première fois pour transformer le message en Request au format de la société Y, et une seconde fois pour transformer le message en Response au format de la société X, de la manière suivante :

<route>
<from uri="direct:getDispo" />

<!-- Create request -->
<to uri="xslt:findAccommodationServicesRequest.xslt" />
<log message="Request :" />
<convertBodyTo type="java.lang.String" />
<to uri="log:input"/>

<!-- Call WS Company Y -->
<to uri="{{wsYUrlPackage}}?throwExceptionOnFailure=false" />
<log message="Response :" />
<convertBodyTo type="java.lang.String" />
<to uri="log:input"/>

<!-- Create GetDispo response -->
<to uri="xslt:findAccommodationServicesResponse.xslt" />
</route>

Puis vient la transformation du message. Contrairement à la version précédente, XSLT 2.0 propose autant de fonctionnalités que XQuery.
Exemple de transformation simple XSLT 2.0 :

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:book="http://bookingservices.x.com/" >

    <xsl:output indent="yes"/>
    <xsl:template match="/book:GetDispo | /book:MakeBooking | /book:GetPriceBeforeBooking" >

        <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:urn="urn:com:y:pkg:findaccommodationservices:request:v2" xmlns:urn1="urn:com:y:common:commontypes:v2">
            <soapenv:Header/>
            <soapenv:Body>

                <xsl:variable name="trFromDate" select="./book:trFromDate"/>
                <xsl:variable name="trToDate" select="./book:trToDate"/>

                <xsl:variable name="startDate" select="concat(substring($trFromDate,0,5),'-',substring($trFromDate,6,2),'-',substring($trFromDate,9,2))"/>
                <xsl:variable name="endDate" select="concat(substring($trToDate,0,5),'-',substring($trToDate,6,2),'-',substring($trToDate,9,2))"/>

                <urn:findAccommodationServicesRequest code="{./book:agProd_1_Code}" startDate="{$startDate}" endDate="{$endDate}" currency="EUR" >
                    <urn:Header/>
                    <urn:Context employeeId="EMP" customerId="{./book:agAgencyId}" />
                    <urn:AccommodationCriteria roomType="{./book:agProd_1_RoomType}">

                        <xsl:variable name="ageMin" select="book:agProd_1_AgeMin"/>
                        <xsl:variable name="ageMax" select="book:agProd_1_AgeMax"/>

                        <xsl:variable name="nbAdult">
                            <xsl:choose>
                                <xsl:when test="not(./book:trNumAdult_1)">
                                    <xsl:text>1</xsl:text>
                                </xsl:when>
                                <xsl:otherwise>
                                    <xsl:value-of select="./book:trNumAdult_1" />
                                </xsl:otherwise>
                            </xsl:choose>
                        </xsl:variable>

                        <xsl:choose>
                            <xsl:when test="$ageMin &lt; 18 ">
                                <xsl:for-each select="for $i in 1 to $nbAdult return $i">
                                    <urn1:Passenger>
                                        <urn1:NonAdultPassenger age="{$ageMin}"/>
                                    </urn1:Passenger>
                                </xsl:for-each>
                            </xsl:when>
                            <xsl:otherwise>
                                <xsl:for-each select="for $i in 1 to $nbAdult return $i">
                                    <urn1:Passenger>
                                        <urn1:AdultPassenger></urn1:AdultPassenger>
                                    </urn1:Passenger>
                                </xsl:for-each>
                            </xsl:otherwise>
                        </xsl:choose>

                    </urn:AccommodationCriteria>
                </urn:findAccommodationServicesRequest>
            </soapenv:Body>
        </soapenv:Envelope>
    </xsl:template>
</xsl:stylesheet>

Enfin il est possible, d’enchaîner plusieurs appels de webservices, comme ci-dessous on vérifie d’abord la disponibilité du produit. Si le produit n’existe pas, on retourne en réponse le message d’erreur, sinon on agrège le résultat de la réponse avec le message initial, afin de construire le message d’entrée au deuxième webservice.
GetPriceBeforeBooking

Le Pattern Enrichment ou aggregationStrategy permet d’enrichir le contenu d’un message courant en l’agrégeant avec la réponse d’un second message.
A la manière de java, il est possible de catcher l’exception lancée lors de l’agrégation à l’aide des balises doTry/doCatch, puis de construire en XSLT le message d’erreur.

       <route>
            <from uri="direct:getPriceBeforeBooking" />

            <doTry>
                <!-- Enrich message header with FindAccommodationServices response -->
                <enrich uri="direct:enrichFindAccommodationServices"
                    strategyRef="findAccommodationServicesAggregationStrategy"/>

                <log message="Enrichment return :" />
                <to uri="log:input?showHeaders=true"/>

                <!-- Create request -->
                <to uri="xslt:computePriceDetailRequest.xslt" />
                <log message="Request :" />
                <convertBodyTo type="java.lang.String" />
                <to uri="log:input?multiline=true"/>

                <!-- Call WS computePriceDetail -->
                <to uri="{{wsYUrlTravelQuote}}?throwExceptionOnFailure=false" />
                <log message="Response :" />
                <convertBodyTo type="java.lang.String" />
                <to uri="log:input"/>

                <!-- Create GetPriceBeforeBooking response -->
                <to uri="xslt:computePriceDetailResponse.xslt" />

                <doCatch>
                    <!-- Handle exception -->
                    <exception>com.x.esb.booking.exception.ProductUnavailableException</exception>
                    <log message="${exception.message}" loggingLevel="WARN" />

                    <setHeader headerName="exceptionMessage">
                        <simple>${exception.message}</simple>
                    </setHeader>

                    <to uri="xslt:getPriceBeforeBookingUnavailable.xslt" />
                </doCatch>
            </doTry>

        </route>

        <route>
            <from uri="direct:enrichFindAccommodationServices" />

            <!-- Create request -->
            <to uri="xslt:findAccommodationServicesRequest.xslt" />
            <log message="Request :" />
            <convertBodyTo type="java.lang.String" />
            <to uri="log:input"/>

            <!-- Call WS findAccommodationServices -->
            <to uri="{{wsYUrlPackage}}?throwExceptionOnFailure=false" />

        </route>

        <bean id="findAccommodationServicesAggregationStrategy" class="com.x.esb.booking.aggregationstrategy.FindAccommodationServicesAggregationStrategy" />

Classe de stratégie agrégation :

class FindAccommodationServicesAggregationStrategy implements AggregationStrategy {
    ...
private final static String xPathResponse = "/env:Envelope/env:Body/resp:findAccommodationServicesResponse";
private final static String xPathPackage = xPathResponse + "/resp:Package";

    private Namespaces namespaces = new Namespaces("env", "http://schemas.xmlsoap.org/soap/envelope/")
                .add("cp", "urn:com:y:pkg:commonpackage:v2")
                .add("resp", "urn:com:y:pkg:findaccommodationservices:response:v2");

    private CamelContext camelContext;

    @Override
    public Exchange aggregate(Exchange oldExchange, Exchange newExchange) {
        if (oldExchange == null) {
            return newExchange;
        }

        // Get Camel Context
        this.camelContext = oldExchange.getContext();

        // first message body
        String oldBody = oldExchange.getIn().getBody(String.class);

        // response soap from findAccommodationResponse
        String newBody = newExchange.getIn().getBody(String.class);

        // if not available then throw Exception
        Error error = getError(newBody);
        if(error != null){
            oldExchange.setException(new ProductUnavailableException(error.getErrorMessage()));
            oldExchange.getIn().setBody(oldBody);
            return oldExchange;
        }

        // Get Package Uid/Code and add to header
        String packageUid = getPackageAttribute(newBody, PACKAGE_UID);
        String packageCode = getPackageAttribute(newBody, PACKAGE_CODE);
        String currency = getPackageAttribute(newBody, CURRENCY);
        oldExchange.getIn().setHeader("packageUid", packageUid);
        oldExchange.getIn().setHeader("packageCode", packageCode);
        oldExchange.getIn().setHeader("currency", currency);

        ...
        oldExchange.getIn().setBody(oldBody);

        return oldExchange;
    }

    private String getPackageAttribute(String newBody, String attribute) throws DOMException {
        Node packageNode = XPathBuilder.xpath(xPathPackage, Node.class).namespaces(namespaces).evaluate(camelContext, newBody, Node.class);
        return (String)packageNode.getAttributes().getNamedItem(attribute).getNodeValue();
    }
}

Ceci permet de récupérer la réponse de l’appel au WS, de pousser les informations nécessaires par la suite dans le header du message, afin de pouvoir les récupérer avant transformation. Cela donne la possibilité de transmettre des variables tout au long du cheminement du message dans le bus de service, car les variables présentes dans le header sont récupérables automatiquement depuis XSLT, en les définissant comme paramètres ex :

<xsl:stylesheet ...>
<xsl:param name="packageUid"/>

<xsl:template ...>
  <urn:Package uid="{$packageUid}" ...>
</xsl:template>

3eme cas : Proxy de webservices avec multiples transformations de messages

D’un point de vue fonctionnel, pour réaliser un des webservices du projet de la société X, celui-ci nécessite l’appel de plusieurs enchaînements de transformation XSLT et d’appels webservices externes de la société Y dont de nombreux champs, dans la réponse, sont utilisés en entrée du webservice suivant. Il va donc falloir jongler entre les messages header et le body, par l’intermédiaire de processor.

MakeBooking

Les classes header…Processor permettent de pousser des informations dans le header message soap. Ces informations peuvent être des paramètres, mais aussi un message xml complet.

<route>
    <from uri="direct:makeBooking" />

    <!-- Add MakeBooking to header -->
    <process ref="headerMakeBookingProcessor"/>

    <doTry>
        <!-- Enrich message header with FindAccommodationServices response -->
        <enrich uri="direct:enrichFindAccommodationServices"
                strategyRef="findAccommodationServicesAggregationStrategy"/>

        <log message="Enrichment return :" />
        <to uri="log:input?showHeaders=true"/>

        <!-- Create request -->
        <to uri="xslt:createTravelBookingRequest.xslt" />

        <!-- Call WS CreateTravelBooking -->
        <to uri="{{wsYUrlTravelBooking}}?throwExceptionOnFailure=false" />

        <!-- Add TravelBookingNumber to header -->
        <process ref="headerTravelBookingNumberProcessor"/>

        <!-- Create request -->
        <to uri="xslt:getTravelBookingSummaryRequest.xslt" />

        <!-- Call WS getTravelBookingSummary -->
        <to uri="{{wsYUrlTravelBooking}}?throwExceptionOnFailure=false" />

        <process ref="headerMakeBookingResponseProcessor"/>

        <log message="Composite Message :" />
        <convertBodyTo type="java.lang.String" />
        <to uri="log:input?showHeaders=true"/>

        <!-- Create MakeBooking response -->
        <to uri="xslt:getTravelBookingSummaryResponse.xslt" />

        <doCatch>
            <!-- Handle exception -->
            <exception>com.x.esb.booking.exception.ProductUnavailableException</exception>
            <log message="${exception.message}" loggingLevel="WARN" />

            <setHeader headerName="exceptionMessage">
                <simple>${exception.message}</simple>
            </setHeader>

            <to uri="xslt:makeBookingUnavailable.xslt" />
        </doCatch>
    </doTry>

</route>

HeaderMakeBooking pousse le message initial complet dans son propre header.

public class HeaderMakeBookingProcessor implements Processor {

    @Override
    public void process(Exchange exchange) throws Exception {

        String body = exchange.getIn().getBody(String.class);
        exchange.getIn().setHeader("MakeBookingRequest", body);

    }
}

Utilisation de messageHeader compositeMessage

Dans ce cas plus complexe, il est utile de pouvoir récupérer des informations du message d’entrée, or ce dernier a été modifié suite à la première transformation XSLT. L’idée initiale consiste donc à utiliser un processor qui va stocker ce message dans le header, puis de récupérer ce message dans la feuille XSLT.

Or XSLT 2.0 ne permet pas de parser à l’aide d’XPath une variable dynamique de type xs:string contenant un flux XML (et XSLT 3.0 est toujours en cours de spécification). L’astuce employée consiste donc à passer par un message composé. Celui-ci contient le message en entrée concaténé du payload courant. Un processor va donc nous permettre de récupérer le message en entrée présent dans le header pour le rajouter dans le body.

compositeMessage

    public class <strong>HeaderMakeBookingResponseProcessor</strong> implements Processor {

	private Namespaces namespaces = new Namespaces("env", "http://schemas.xmlsoap.org/soap/envelope/").add("resp", "urn:com:ext:y:travelbooking:gettravelbookingsummary:reponse:v2");

	private final static String xPathSoapEnv = "/env:Envelope/env:Body/resp:getTravelBookingSummaryResponse";

	@Override
	public void process(Exchange exchange) throws Exception {
		CamelContext camelContext = exchange.getContext();
		String makeBookingRequest = exchange.getIn().getHeader("MakeBookingRequest", String.class);

		// remove soap envelop
		String body = exchange.getIn().getBody(String.class);
		String bodyExtracted = XPathBuilder.xpath(xPathSoapEnv).namespaces(namespaces).evaluate(camelContext, body, String.class);

		// preserve header
		exchange.getOut().setHeaders(exchange.getIn().getHeaders());

		//build composite message
		exchange.getOut().setBody("<compositeMessage>" + bodyExtracted + makeBookingRequest + "</compositeMessage>");
	}

}

Enfin il reste à parser en XSLT correctement le flux en prenant en compte la balise compositeMessage.

<xsl:template match="//compositeMessage" >
  <xsl:for-each select="./resp:getTravelBookingSummaryResponse">
  ...
     <xsl:for-each select="../book:MakeBooking/book:options/book:Option">
     ...

Conclusion

L’objectif de ce post était multiple.

– Dans un premier temps présenter l’ESB ServiceMix et l’utilisation de Camel;
– Puis dans un second temps, présenter les difficultés rencontrées au travers d’un retour d’expérience d’un cas concret de Proxy Webservices mise en place chez un client Ippon;
– Et enfin surtout de mettre en exergue l’intérêt des ateliers mis en place par Ippon.

Remerciements à Emmanuel Remy pour son camel-context qui m’a inspiré.

Tweet about this on TwitterShare on FacebookGoogle+Share on LinkedIn

Une réflexion au sujet de « Proxyfier vos webservices avec ServiceMix/Camel »

  1. Excellent article qui met une bonne claque ! Quand on voit parfois les usines à gaz qui sont montées pour solutionner ce type de problèmes… Ca c’est du concret, des idées toutes simples parfaitement illustrées, comme de mettre le payload entrant dans un header ou de changer le parser XSLT (mais celui de XALAN a aussi des avantages – notamment de pouvoir appeler des fonctions statiques java directement depuis la transformation comme si c’était des fonction xslt). Sans compter que la lecture donne aussi une approche de Camel un peu différente et plus évoluée de celles habituellement rencontrées (intégration dans ServiceMix, Enricher, …) Merci Sébastien !

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *


*