Accueil Nos publications Blog [WP7] – Utiliser une API XMLRPC, partie 1

[WP7] – Utiliser une API XMLRPC, partie 1

Il y a quelques temps, j’avais cherché pour une application windows phone, un moyen de pouvoir récupérer quelques informations tirées d’un blog, greffé lui-même sur un forum. Certes, une version mobile du site existe, mais, les données qu’elle propose d’exploiter sont trop faibles et à la longue, il deviendrait difficile de maintenir cette application WP7. Je vous propose dans cet article de découvrir les moyens mis en œuvre pour résoudre ces problèmes rencontrés, grâce à XmlRpc.

Besoin

Je souhaite afficher des news et des commentaires associés à ces news dans mon application windows phone.

Contexte

Le site web monsmartphone en est une superbe illustration. A la vue de tous, le point d’entrée est un blog comme beaucoup d’autres. Mais en fait, c’est un peu plus que cela. Outre le développement de fonctionnalités uniques, il s’agit d’un forum de type PHPBB. Les informations que je souhaite récupérer et exploiter sont non seulement les news en elles-mêmes, mais, également les commentaires (et les informations relatives à leurs auteurs). Dans le principe de PHPBB, les news, et donc, les commentaires qui en découlent sont des « threads » (ou des séries de « posts »). Une news et ses commentaires constituent un « thread ».
Quelques petites recherches me révèleront que, contrairement à WordPress, PHPBB n’implémente pas d’API XMLRPC.
Plusieurs solutions s’offrent à moi :
– Implémenter moi-même alors le client et le serveur XMLRPC
– Implémenter le client en trouvant quelque part le serveur.
Vous vous doutez bien que c’est la 2ième solution pour laquelle j’ai optée (PHPBB étant une trop grosse usine à gaz, implémenter le serveur n’aurait pas été si facile). C’est ainsi donc que j’ai découvert TAPATALK.
Avant que nous continuions, arrêtons-nous un peu sur ce qu’est XmlRPC.

Qu’est-ce que XMLRPC ?

XML-RPC est composé de 2 mots, XML et RPC. RPC pour définir le protocole réseau et XML pour le format des données. C’est un protocole utilisant HTTP pour transporter des données au format XML. Pour plus d’informations, vous pouvez lire la définition sur Wikipedia ou consulter le site officiel. Je vous invite à les lire pour vraiment en savoir plus, comprendre les formats des appels, des réponses, et les types.

Serveur XMLRPC

TAPATALK est un client permettant d’utiliser au sein d’une application mobile (iPhone ou Android) un forum quelconque parmi une longue (très longue) liste de forums existants. Une version existe pour Windows Phone (Board Express).

Comment fonctionne ce client ?

Un addon ou plugin a été créé par eux. Selon le type de forum pour lequel vous souhaitez activer la possibilité d’utiliser XMLRPC, vous devrez télécharger un package spécifique du nom de mobiquo. Ce package doit simplement être déposé sur votre serveur. Aucune manipulation supplémentaire n’est requise sauf peut-être que, pour activer ce plugin, vous devrez vous inscrire sur leur site. L’avantage de cette inscription est que vous aurez des statistiques quant à l’utilisation de ce plugin XmlRpc.

Notre serveur est donc prêt (oui oui, et nous allons pouvoir utiliser XmlRpc au sein de notre application).

Frameworks XMLRPC

Il existe un framework en .NET dédié à XMLRPC et très bien réalisé. Il s’agit de XmlRpc.NET. Au moment où j’avais implémenté l’approche dont il est question dans cet article, aucune version pour WindowsPhone n’existait. Un autre article suivra donc pour montrer les différences entre les 2 approches (et à quel point c’est plus simple avec ce dernier).

Mise en place du client

Les modèles XmlRpc

Les modèles sont les objets qui nous permettront de pouvoir effectuer nos appels, récupérer les réponses, faire transiter nos données, spécifiquement pour XmlRpc.
En suivant les spécifications du site, voici la liste de modèles que nous obtenons :


using System;
using System.Collections.Generic;

namespace WP7XmlRpc.Models
{
    ///
    /// Xml Rpc Method Call
    ///
    public class MethodCall
    {
        public MethodCall()
        {
            Params = new List();
        }
        public string MethodName { get; set; }
        public List Params { get; private set; }
    }

    ///
    /// Xml Rpc Method Response
    ///
    public class MethodResponse
    {
        public MethodResponse() { }
        public List Params { get; set; }
    }

    ///
    /// Xml Rpc Param
    ///
    public class Param
    {
        public Value Value { get; set; }
    }

    ///
    /// Xml Rpc Value
    ///
    public class Value
    {
        public String Base64 { get; set; }
        public String String { get; set; }
        public String Int { get; set; }
        public String I4 { get; set; }
        public String Boolean { get; set; }
        public String Double { get; set; }
        public List Struct { get; set; }
        public Array Array { get; set; }
    }

    ///
    /// Xml Rpc Member
    ///
    public class Member
    {
        public string Name { get; set; }
        public Value Value { get; set; }
    }

    ///
    /// Xml Rpc Array
    ///
    public class Array
    {
        public List Data { get; set; }
    }

    ///
    /// Xml Rpc Data
    ///
    public class Data
    {
        public Value Value { get; set; }
    }
}

J’avoue que je n’ai pas cherché à typer les paramètres pour éviter de perdre du temps et de créer un système de sérialisation / désérialisation complexe.

Un Helper Utils XmlRpc

Les réponses XmlRpc peuvent parfois être difficiles à lire, certains champs peuvent être encodés en base64 (ou doivent l’être pour les appels XmlRpc). J’ai donc créé un helper avec quelques méthodes utiles pour pouvoir au mieux exploiter mes données. Le code est simple et commenté pour aider à sa compréhension.


using System;
using System.Linq;
using System.Text;
using System.Xml.Linq;

namespace WP7XmlRpc.Helpers
{
    public class XmlRpcMethodsHelper
    {
        #region Encrypt / Decrypt
        ///
        /// Encodes a string into a base64 string
        ///
        public static String EncodeToBase64String(String input)
        {
            byte[] toEncodeAsBytes = StringToAscii(input);
            string returnValue = Convert.ToBase64String(toEncodeAsBytes);
            return returnValue;
        }

        ///
        /// Encodes a string into a base 64 bytes
        ///
        public static byte[] EncodeToBase64Bytes(string input)
        {
            byte[] toEncodeAsBytes = StringToAscii(input);
            return toEncodeAsBytes;
        }

        ///
        /// Decodes a base64 string
        ///
        public static String DecodeFromBase64String(string input)
        {
            byte[] decbuff = Convert.FromBase64String(input);
            return Encoding.UTF8.GetString(decbuff, 0, decbuff.Length);
        }

        ///
        /// Gets String to Ascii
        ///
        private static byte[] StringToAscii(string s)
        {
            byte[] retval = new byte[s.Length];
            for (int i = 0; i < s.Length; ++i)
            {
                char ch = s[i];
                if (ch <= 0x7f) retval[i] = (byte)ch;
                else retval[i] = (byte)'?';
            }
            return retval;
        }

        #endregion

        #region

        ///
        /// Gets XmlRpc Member value
        ///
        /// parent node (struct or data)
        /// name element value
        /// xml rpc value type
        /// decrypt base 64 value
        ///
        public static String GetXmlRpcMemberValue(XElement parent, string name, string valueType, bool decryptBase64value = false)
        {
            var value = parent
                            .Descendants("member")
                                .FirstOrDefault(e => e.Element("name").Value == name)
                                    .Element("value")
                                        .Element(valueType).Value;

            if (decryptBase64value)
                value = DecodeFromBase64String(value);

            return value;
        }

        ///
        /// Gets XmlRpc Response Struct Member Value
        ///
        /// parent node
        /// name element value
        /// xml rpc value type
        /// decrypt base 64 value
        ///
        public static String GetXmlRpcResponseStructMemberValue(XElement parent, string name, string valueType, bool decryptBase64value = false)
        {
            var value = parent
                              .Element("params")
                                .Element("param")
                                    .Element("value")
                                        .Element("struct")
                                            .Descendants("member")
                                                .First(n => n.Element("name").Value == name)
                                                    .Element("value").Element(valueType).Value;

            if (decryptBase64value)
                value = DecodeFromBase64String(value);

            return value;
        }

        #endregion
    }
}

Maintenant, nous avons tous les outils nécessaires pour mieux exploiter XmlRpc au sein de notre application. Voyons comment les utiliser.

Créer son service

Je prendrai l’exemple du service de récupération des news et des commentaires que j’ai implémenté dans la prochaine version de notre application monsmartphone.
Commençons d’abord par décrire notre contrat :


    ///
    /// Defines base post service contracts
    ///
    public interface IPostService
    {
        ///
        /// Gets post
        ///
        /// thread id
        /// callback
        void GetPost(id id, Action callback);

        ///
        /// Gets Posts
        ///
 /// thread id
        /// first post position
        /// last post position
        /// callback
        void GetPosts(string id, string firstPostPosition, int lastPostPosition, Action callback);
    }

Voici maintenant à quoi ressemble notre service en lui-même :


using RestSharp;
using System;
using System.Collections.Generic;
using WP7XmlRpc.Models;
using WP7XmlRpc.Helpers;

namespace monsmartphone.Services
{
    ///
    ///
    ///
    public class PostService : BaseService, IUserService
    {
        public PostService() : base() { }

        #region IPostService Members
        ///
        /// Gets a specific post
        ///
        /// topic id
        /// callback
        public void GetPost(int id, Action callback)
        {
            GetPosts(id, 0, 1, callback);
        }

        ///
        /// Gets posts
        ///
        /// topic id
        /// first comment position
        /// last comment position
        /// callback
        public void GetPosts(int id, int firstPostPosition, int lastPostPosition, Action callback)
        {
            var request = new RestRequest { Resource = RESOURCE_URL, Method = Method.POST, RequestFormat = DataFormat.Xml };

            MethodCall methodCall = new MethodCall();
            methodCall.MethodName = "get_thread";
            methodCall.Params.Add(new Param { Value = new Value { String = id.ToString() } });
            methodCall.Params.Add(new Param { Value = new Value { I4 = firstPostPosition.ToString() } });
            methodCall.Params.Add(new Param { Value = new Value { I4 = lastPostPosition.ToString() } });
            methodCall.Params.Add(new Param { Value = new Value { Boolean = "1" } });

            request.AddBody(methodCall);
            Client.ExecuteAsync(request, callback);
        }

        #endregion
    }
}

Dans le corps de la méthode GetPosts de notre service, on peut voir comment créer un appel XmlRpc.
La méthode AddBody de l’objet RestRequest (avec lequel vous êtes sûrement déjà familiers puisque j’ai déjà parlé ici de RestSharp :-D) va sérialiser au format Xml notre objet methodCall.
C’est donc au format suivant que nous aurons notre appel effectué :


<?xml version="1.0"?>
<methodCall>
   <methodName>get_thread</methodName>
   <params>
      <param><value><string>topic_id</string></value></param>
     <param><value><i4>first_post_position</i4></value></param>
     <param><value><i4>last_post_position</i4></value></param>
    <param><value><boolean>1</boolean></value></param>
  </params>
 </methodCall>

Pour les entités liées aux forums, Les méthodes, les paramètres et leur type sont définies sur le site de tapatalk.

Utiliser le service

Pour utiliser mon service (dans mon VueModel par exemple), je commence généralement par créer le callback correspondant aux méthodes que je souhaite utiliser.
Prenons mon service PostService et la méthode GetPosts(int id, Action callback).
Quand j’indique une RestReponse sans paramètre de type T, je veux dire que je veux récupérer ma réponse au format string via la propriété Content de l’objet de réponse. (Quand la réponse est typée, c’est via la propriété Data que l’on récupère l’objet typé).
Pourquoi ne pas avoir typé ma réponse directement au format MethodResponse (RestResponse) ?

Il est vrai que j’aurai pu procéder ainsi, mais, je souhaite pouvoir moi-même transformer la réponse et effectuer dessus quelques traitements.
Voyons mon callback de plus près :


///
        /// Post Process CallBack
        ///
        ///
        private void GetPostCallback(RestResponse response)
        {
            if (response.ResponseStatus == ResponseStatus.Error) {
                DisplayErrorMessage(); //message indiquant un problème de connexion
                return;
            }

            var result = response.Content;
            if (!String.IsNullOrEmpty(result))
            {
                DispatcherHelper.CheckBeginInvokeOnUI(() =>
                {
                    var post = EntityConverterHelper.ConvertToPost(result);
                    //any job you want
                });
            }
        }

Et notre méthode ConvertToPost :


///
/// Converts a response into a list of Post
///
/// response
///
public static List ConvertToPosts(string response)
{
 try {
       List posts = new List();

              var postElements = XElement.Parse(response);
              if (postElements != null) {
                    var xPosts = postElements
                                   .Element("params")
                                      .Element("param")
                                         .Element("value")
                                              .Element("struct")
                                                   .Descendants("member")
                                                       .First(n => n.Element("name").Value == "posts")
                                                                .Element("value")
                                                                    .Element("array")
                                                                        .Element("data")                                                                            .Descendants("value")                                                                .Descendants("struct");

if (xPosts != null) {
var total_post_num = XmlRpcMethodsHelper.GetXmlRpcResponseStructMemberValue(postElements, "total_post_num", "int");
      var topic_id = XmlRpcMethodsHelper.GetXmlRpcResponseStructMemberValue(postElements, "topic_id", "string");

      foreach (XElement xpost in xPosts) {
          Post post = ConvertToPost(xpost);
          if (!string.IsNullOrEmpty(total_post_num)) {
               post.TotalPostNum = int.Parse(total_post_num);
          }
          if (!string.IsNullOrEmpty(topic_id)) {
               post.TopicId = int.Parse(topic_id);
          }
                            posts.Add(post);
                        }
                        return posts;
                    }
                }
                return null;
            }
            catch (Exception ex) {
                throw;
            }
        }

Conclusion

Voilà donc pour cette première approche avec XmlRpc. Les premiers coups d’oeil jetés à XmlRpc.NET montreraient qu’il serait assez simple de les utiliser et de pouvoir remplacer le système actuel. A suivre donc …