Exemple d’ API avec WSO2 ESB

Je profite d’une expérience récente pour vous montrer, dans un cas pratique, les qualités de la solution ESB de WSO2. Pour le situer dans les produits de WSO2, ce bus est le coeur de l’ Enterprise Integration (Intermédiations de Systèmes) et de l’ API Management (Plateforme de partage d’API). En quelques mots, ce bus est spécialisé dans les intermédiations de services HTTP.

L’exemple que je vous propose est assez simple et est pour moi représentatif. Vous allez, je n’en doute pas, constater la lisibilité et la clarté des médiations réalisées avec cet ESB.

Le sujet que je vous propose est une API “utilisateur”. Elle présente des profils d’ utilisateurs. Elle puise ses données dans un annuaire Active Directory de Microsoft. Je ne m’attarde ici qu’à la médiation en tant que telle. Les outils WSO2 EI ou API Management portent d’autres fonctionnalités que je détaille pas dans cet article.

La conception de la route tient en 4 étapes:

  1. initialisation de la connexion LDAP,
  2. préparation de la requête,
  3. construction de la réponse.
  4. optionnel: la gestion d’erreurs.

Je vais dans la suite de cet article commenter cette API par étape, par bloc de code. Cela permettra de bien commenter chaque partie.

Avant de démarrer, il faut rappeler que les médiations de WSO2 sont déclaratives et en XML. Le langage XML utilisé est spécifique. Il est une agrégation d’éléments descriptifs du comportement de la médiation. Ces éléments sont appelés mediators.

Quelques exemples de médiators:

  • log: Trace d’un flux
  • send: Envoi d’un message
  • call: Appel d’un service

Une médiation WSO2 tient dans un fichier XML et se lit facilement. Sa compréhension et sa maintenance en sont accrues.

Passons à notre API “utilisateurs”.

Déclaration de l’API

L’API se compose d’un unique fichier XML. Le chemin d’accès racine est déclaré en contexte: “/users/1” de l’ “api”. La valeur est libre. Cela est bien pratique pour gérer les versions au plus tôt 😀 .

Dans notre besoin, une seule opération est prise en charge, la méthode GET sur l’ URI “/user” .

<api context="/users/1" name="User" xmlns="http://ws.apache.org/ns/synapse">
    <resource methods="GET" uri-template="/user">
        <inSequence>
           <!-- Flux d'entrée -->
        </inSequence>
        <outSequence>
            <!-- Flux de sortie -->
        <faultSequence>
            <!-- Flux d'erreur -->
        </faultSequence>
    </resource>
</api>

La médiation débute donc sur l’interception du contenu HTTP sur ce point d’entrée.

Cette méthode sera exposée sur “GET /users/1/user” .

Connexion LDAP

Pour interagir avec un annuaire Active Directory, il faut manipuler le protocole LDAP. Pour cela, WSO2 propose un connecteur LDAP intégré.

Celui-ci expose les opérations principales. J’ai utilisé ci-dessous 2 d’entre elles: init et searchEntry.

Donc, au sein de la séquence d’entrée, inSequence, la 1ère action est la connexion à l’annuaire LDAP.

        <inSequence>
            <!-- initialisation LDAP -->
            <ldap.init>
                <secureConnection>false</secureConnection>
                <disableSSLCertificateChecking>false</disableSSLCertificateChecking>
                <providerUrl>ldap://acme.com:389</providerUrl>
                <securityPrincipal>user</securityPrincipal>
                <securityCredentials>password</securityCredentials>
            </ldap.init>     

Requête LDAP

Nous avons fait le choix d’utiliser l’email, passé en paramètre HTTP, pour faire la recherche de l’utilisateur dans l’annuaire.

Exemple: /users/1/user?email=emmanuel.lesne@middleware-solutions.fr

La valeur du paramètre (query.param.email) est utilisé pour construire le filtre LDAP. Pour intercepter celui-ci et construire le filtre, le médiator script est mis à l’oeuvre. Son support du langage JavaScript est, je trouve, très souple.

 <script language="js"><![CDATA[
// lecture du paramètre email
upn = mc.getProperty("query.param.email");
 // construction du filtre LDAP
 mc.setProperty("filters", "{ 'userPrincipalName':'"+upn+"' }");
]]></script>

Une fois le filtre construit, la recherche est lancée. Le médiator ldap.searchEntry est utilisé pour faire celle-ci.

             <ldap.searchEntry>
                <onlyOneReference>true</onlyOneReference>
                <objectClass>person</objectClass>
                <filters>{$ctx:filters}</filters>
                <dn>DC=corp,DC=acme,DC=com</dn>
                <attributes>userPrincipalName,name,c,co,company,sn,givenName,whenCreated,whenChanged,userAccountControl,mailNickname</attributes>
            </ldap.searchEntry>

            <log level="full"/>     

Enfin, le log trace la réponse complète renvoyée par l’annuaire.

Réponse JSON

L’étape finale est la construction de la réponse de l’API, au format JSON.

Le composant LDAP remontant un flux XML, j’ai utilisé un PayloadFactory pour manipuler ce XML et le transformer en JSON. Chaque élément XML est identifié dans la liste des arguments et est utilisé en fonction de sa position 1 à 10 ici.

            <payloadFactory media-type="json">
                <format>
{
  "user": {
    "id":"$11",
    "dn":"$1",
    "email":"$2",    
    "name":"$3",
    "lastName":"$7",
    "firstName":"$8",
    "country": {
        "code":"$4",
        "name":"$5"
    },
    "company":"$6",
    "creationDate":"$9",
    "lastModificationDate":"$10"
    }
}
			      </format>
                <args>
                    <arg evaluator="xml" expression="//ns:dn" xmlns:ns="http://org.wso2.esbconnectors.ldap"/>
                    <arg evaluator="xml" expression="//ns:userPrincipalName" xmlns:ns="http://org.wso2.esbconnectors.ldap"/>
                    <arg evaluator="xml" expression="//ns:name" xmlns:ns="http://org.wso2.esbconnectors.ldap"/>
                    <arg evaluator="xml" expression="//ns:c" xmlns:ns="http://org.wso2.esbconnectors.ldap"/>
                    <arg evaluator="xml" expression="//ns:co" xmlns:ns="http://org.wso2.esbconnectors.ldap"/>
                    <arg evaluator="xml" expression="//ns:company" xmlns:ns="http://org.wso2.esbconnectors.ldap"/>
                    <arg evaluator="xml" expression="//ns:sn" xmlns:ns="http://org.wso2.esbconnectors.ldap"/>
                    <arg evaluator="xml" expression="//ns:givenName" xmlns:ns="http://org.wso2.esbconnectors.ldap"/>
                    <arg evaluator="xml" expression="//ns:whenCreated" xmlns:ns="http://org.wso2.esbconnectors.ldap"/>
                    <arg evaluator="xml" expression="//ns:whenChanged" xmlns:ns="http://org.wso2.esbconnectors.ldap"/>
                    <arg evaluator="xml" expression="//ns:mailNickname" xmlns:ns="http://org.wso2.esbconnectors.ldap"/>
                </args>
            </payloadFactory>

Il convient alors de renvoyer ce flux JSON.

            <respond/>
        </inSequence>

Gestion des erreurs

La gestion d’erreurs est ici basique. Elle est composé de 2 étapes:

  1. tracer dans les logs les informations de l’erreur
  2. transmettre l’erreur au demandeur du service.

Pour la trace dans les logs, le composant log produit une ligne au format clé/valeur avec:

  • messageFaultError: la description de l’erreur interceptée,
  • messageFaultCode: son code technique,
  • messageFaultDetail: son détail,
  • messageFaultException: la pile d’appels.
        <faultSequence>
            <log level="full">
                <property expression="get-property('ERROR_MESSAGE')" name="messageFaultError"/>
                <property expression="get-property('ERROR_CODE')" name="messageFaultCode"/>
                <property expression="get-property('ERROR_EXCEPTION')" name="messageFaultException"/>
                <property expression="get-property('ERROR_DETAIL')" name="messageFaultDetail"/>
            </log>

Ensuite, une réponse au format JSON est construite à partir des mêmes propriétés ERROR_*.

            <payloadFactory media-type="json">
                <format>
               		{
               			"error": {
	               			"code":"$1", 
	               			"message":"$2"
	               		}
               		}
          		</format>
                <args>
                    <arg evaluator="xml" expression="get-property('ERROR_CODE')"/>
                    <arg evaluator="xml" expression="get-property('ERROR_MESSAGE')"/>
                    <arg evaluator="xml" expression="get-property('ERROR_DETAIL')"/>
                    <arg evaluator="xml" expression="get-property('ERROR_EXCEPTION')"/>
                </args>
            </payloadFactory>

Enfin, le type de retour (Content-Type HTTP) est forcé avant renvoi. Comme tous types d’erreurs sont possibles, cela garantie une bonne conversion.

            <property name="messageType" scope="axis2" type="STRING" value="application/json"/>
            <respond/>
        </faultSequence>

Médiation complète

Pour vous permettre de lire la médiation en 1 traite, je vous mets ci-dessous le fichier complet.

<?xml version="1.0" encoding="UTF-8"?>
<api context="/users/1" name="User" xmlns="http://ws.apache.org/ns/synapse">

 <resource methods="GET" uri-template="/user">

 <inSequence>
 <!-- init LDAP -->
 <ldap.init>
 <secureConnection>false</secureConnection>
 <disableSSLCertificateChecking>false</disableSSLCertificateChecking>
 <providerUrl>ldap://acme.com:389</providerUrl>
 <securityPrincipal>user</securityPrincipal>
 <securityCredentials>password</securityCredentials>
 </ldap.init>
 <script language="js"><![CDATA[upn = mc.getProperty("query.param.email");
 // filtre sur accountName
 mc.setProperty("filters", "{ 'userPrincipalName':'"+upn+"' }");]]></script>
 <!-- property name="filters" scope="default" type="STRING" value="{ 'userPrincipalName':'*Arunas*' }"/ -->
 <ldap.searchEntry>
 <onlyOneReference>true</onlyOneReference>
 <objectClass>person</objectClass>
 <filters>{$ctx:filters}</filters>
 <dn>DC=corp,DC=ACME,DC=com</dn>
 <attributes>userPrincipalName,name,c,co,company,sn,givenName,whenCreated,whenChanged,userAccountControl,mailNickname</attributes>
 </ldap.searchEntry>
 <log level="full"/>
 <payloadFactory media-type="json">
 <format>
{"user": {
 "id":"$11",
 "dn":"$1",
 "email":"$2", 
 "name":"$3",
 "lastName":"$7",
 "firstName":"$8",
 "country": {
 "code":"$4",
 "name":"$5"
 },
 "company":"$6",
 "creationDate":"$9",
 "lastModificationDate":"$10"
 }
}
 </format>
 <args>
 <arg evaluator="xml" expression="//ns:dn" xmlns:ns="http://org.wso2.esbconnectors.ldap"/>
 <arg evaluator="xml" expression="//ns:userPrincipalName" xmlns:ns="http://org.wso2.esbconnectors.ldap"/>
 <arg evaluator="xml" expression="//ns:name" xmlns:ns="http://org.wso2.esbconnectors.ldap"/>
 <arg evaluator="xml" expression="//ns:c" xmlns:ns="http://org.wso2.esbconnectors.ldap"/>
 <arg evaluator="xml" expression="//ns:co" xmlns:ns="http://org.wso2.esbconnectors.ldap"/>
 <arg evaluator="xml" expression="//ns:company" xmlns:ns="http://org.wso2.esbconnectors.ldap"/>
 <arg evaluator="xml" expression="//ns:sn" xmlns:ns="http://org.wso2.esbconnectors.ldap"/>
 <arg evaluator="xml" expression="//ns:givenName" xmlns:ns="http://org.wso2.esbconnectors.ldap"/>
 <arg evaluator="xml" expression="//ns:whenCreated" xmlns:ns="http://org.wso2.esbconnectors.ldap"/>
 <arg evaluator="xml" expression="//ns:whenChanged" xmlns:ns="http://org.wso2.esbconnectors.ldap"/>
 <arg evaluator="xml" expression="//ns:mailNickname" xmlns:ns="http://org.wso2.esbconnectors.ldap"/>
 </args>
 </payloadFactory>
 <respond/>
 </inSequence>

 <outSequence/>

 <faultSequence>
 <log level="full">
 <property expression="get-property('ERROR_MESSAGE')" name="messageFaultError"/>
 <property expression="get-property('ERROR_CODE')" name="messageFaultCode"/>
 <property expression="get-property('ERROR_EXCEPTION')" name="messageFaultException"/>
 <property expression="get-property('ERROR_DETAIL')" name="messageFaultDetail"/>
 </log>
 <payloadFactory media-type="json">
 <format>
 {
 "error": {
 "code":"$1", 
 "message":"$2"
 }
 }
 </format>
 <args>
 <arg evaluator="xml" expression="get-property('ERROR_CODE')"/>
 <arg evaluator="xml" expression="get-property('ERROR_MESSAGE')"/>
 <arg evaluator="xml" expression="get-property('ERROR_DETAIL')"/>
 <arg evaluator="xml" expression="get-property('ERROR_EXCEPTION')"/>
 </args>
 </payloadFactory>
 <property name="messageType" scope="axis2" type="STRING" value="application/json"/>
 <respond/>
 </faultSequence>
 </resource>
</api>

Alors, convaincu par la simplicité et la lisibilité des API réalisées avec l’ESB WSO2 ?

Laisser un commentaire