Ecriture d'un programme pour communiquer avec Trac à l'aide du XML-RPC

Posté le 15. August 2011 dans Programmation

Présentation

Ce billet à pour but d'expliquer comment écrire un programme afin de communiquer en XML-RPC avec Trac. Du à un manque d'API on montrera également comment effectuer l'enregistrement d'un utilisateur sur le gestionnaire de ticket à l'aide de Webkit.

L'exemple sera écrit en C++, à l'aide de Qt.

Le programme permettra à partir d'un programme lancé depuis son poste, de :

  • s'enregistrer
  • créer un ticket
  • attaché des fichiers au ticket

Le programme devra garder en mémoire le login et le mot de passe de l'utilisateur pour pouvoir le ré-utiliser par la suite pour la création d'autres ticket.

Communication XML-RPC

La communication en XML-RPC ce fait au travers de flux XML. Nous allons donc écrire deux classes, l'une dont le travail sera de préparer la question, l'autre dont le but sera de décoder la réponse. Nous allons voir dans une première partie l'ensemble des paramètres utilisable dans la question et de la réponse.

Les paramètres

Dans la question comme dans la réponse, les paramètres sont définit par les noeud XML. Les différents types de paramètres disponibles pour un appel en XML-RPC sont :

Nulle

<nil/>

Entier

<int>42</int>

Double

<double>13.2</double

Boolean

<boolean>1</boolean>

Chaine de caractère

<string>Bonjour monde!</string>

Données

<base64>Qm9uam91ciBtb25kZSAhCkNvbW1lbnQgdmEgbGUgbW9uZGUgIQo=</base64>

Tableau

<array>
    <data>
        <value><int>42</int></value>
        <value><string>Bonjour monde</string></value>
        <value><int>1</int></value>
    </data>
</array>

Structure

<struct>
    <member>
            <name>toto</name>
            <value><int>1</int></value>
    </member>
    <member>
        <name>titi</name>
        <value><int>2</int></value>
    </member>
</struct>

Date/Heure

<dateTime.iso8601>20110805T11:32:51</dateTime.iso8601>

La question

L'encodage de la question se fera au travers de la classe XmlRpcRequest. La structure d'une question au format XML-RPC ressemble à ceci :

<?xml version="1.0"?>
<methodCall>
    <methodName>api.method</methodName>
    <params>
        <param>
            <value><int>42</int></value>
        </param>
    </params>
</methodCall>

La question est donc composé du nom de la méthode à appelé, suivi de un ou plusieurs paramètres en dessous du noeud params.

La classe XmlRpcRequest devra donc générer un flux XML de cette forme à l'aide des différents informations que l'on va lui donner. Pour ce faire on utilisera QXmlStreamWriter qui permet de générer un fichier XML à l'aide de méthode simple en Qt.

class XmlRpcRequest : public QObject
{
    Q_OBJECT
public:
    XmlRpcRequest();
    explicit XmlRpcRequest(const QString & methodName);
    ~XmlRpcRequest();

    void setMethodName(const QString & name);
    const QString & getMethodName() const;

L'objet prend en paramètre à la construction le nom de la méthode à appeler (dans l'exemple ci-dessus api.method). On ajoute évidemment les accesseurs permettant d'accéder et de modifier le nom de la méthode. Ces méthodes n'ont rien de particulier, on ne les détaillera pas.

    const QVariantList & parameters() const;
    void setParameters(const QVariantList & list);
    void addParameters(const QVariant & variant);
    void clearParameters();

La gestion des paramètres se fait au travers de QVariant. Un QVariant est un objet qui peut contenir une valeur indépendamment de ce type. Cela fonctionne à peu pré comme un union en C avec un type indiquant le type de variant et un union contenant les différentes valeurs possible.

Nous allons ainsi à l'aide de 4 méthodes pouvoir ajouter n'importe quel paramètre à notre objet. Bien sur l'objet XmlRpcRequest ne pourra gérer que les type de disponible dans la norme.

Le nom de la méthode, ainsi que la liste des paramètres seront stocké dans deux membre : QString _methode_name et QVariantList _parameters.

Enfin deux dernières méthodes permette de récupérer la question XML-RPC dans un objet QByteArray. Nous allons détailler ces deux méthodes qui contiennent toutes l’intelligence de cet objet.

QByteArray XmlRpcRequest::getRequest() const
{
    QByteArray result;
    QXmlStreamWriter writer(&result);

    writer.writeStartDocument();
    writer.writeStartElement("methodCall");
    writer.writeTextElement("methodName", _methode_name);
    writer.writeStartElement("params");
    foreach(const QVariant & param, _parameters)
    {
        writer.writeStartElement("param");
        createParam(&writer, param);
        writer.writeEndElement();
    }
    writer.writeEndElement();
    writer.writeEndElement();
    writer.writeEndDocument();

    return result;
}

La méthode commence par créer le document, suivit de la méthode. Enfin pour chaque paramètre (de type QVariant), la méthode appelle XmlRpcRequest::createParam.

Cette méthode sera celle qui ajoutera les noeuds en dessous de param en fonction du type de QVariant.

void XmlRpcRequest::createParam(QXmlStreamWriter* writer, const QVariant & param) const
{
    switch(param.type())
    {
    case QVariant::Int:
        writer->writeTextElement("int", QString::number(param.toInt()));
        break;
    case QVariant::Bool:
        writer->writeTextElement("boolean", QString::number((int)param.toBool()));
        break;
    case QVariant::String:
        writer->writeTextElement("string", param.toString());
        break;
    case QVariant::Double:
        writer->writeTextElement("double", QString::number(param.toDouble()));
        break;
    case QVariant::DateTime:
        writer->writeTextElement("dateTime.iso8601", param.toDateTime().toString("YYYYMMDDThhmmZ"));
        break;
    case QVariant::ByteArray:
        writer->writeTextElement("base64", param.toByteArray().toBase64());
        break;
    case QVariant::List:
        {
            writer->writeStartElement("array");
            writer->writeStartElement("data");
            QVariantList list = param.toList();
            foreach(const QVariant & var, list)
            {
                writer->writeStartElement("value");
                createParam(writer, var);
                writer->writeEndElement();
            }
            writer->writeEndElement();
            writer->writeEndElement();
        }
        break;
    case QVariant::Map:
        {
            writer->writeStartElement("struct");
            QVariantMap map = param.toMap();
            QMapIterator<QString,QVariant> it(map);
            while(it.hasNext())
            {
                it.next();
                writer->writeStartElement("member");
                writer->writeTextElement("name", it.key());
                writer->writeStartElement("value");
                createParam(writer, it.value());
                writer->writeEndElement();
                writer->writeEndElement();
            }
            writer->writeEndElement();
        }
        break;
    case QVariant::Invalid:
        writer->writeStartElement("nil");
        writer->writeEndElement();
        break;
    default:
        break;
    }
}

Pour chaque type de variant dont on connait une transformation on ajout le noeud XML suivit de la valeur du noeud. Pour les type de variant QVariant::List, QVariant::Map, nous allons appeler récursivement les méthode XmlRpcRequest::createParams pour gérer les structures complexe. Comme une structure C struct ne peut être renseigné dans un QVariant, nous utilisons une map (dont le contenu est trié) pour définir le contenu de la structure. Cela signifie que l'on a pas le contrôle sur l'odre des des champs dans la map, mais cela ne devrait pas poser de problème.

La réponse

Après avoir posé la question, forcément on attend une réponse, cette réponse peut avoir plusieurs forme.

  • réponse normal
<?xml version="1.0"?>
<methodResponse>
    <params>
        <param>
                <value><string>Ma chaine de caractère</string></value>
        </param>
    </params>
</methodResponse>
  • réponse avec erreur
<?xml version="1.0"?>
<methodResponse>
    <fault>
        <value>
            <struct>
                <member>
                    <name>faultCode</name>
                    <value><int>4</int></value>
                </member>
                <member>
                    <name>faultString</name>
                    <value><string>Wrong type of character.</string></value>
                </member>
            </struct>
        </value>
    </fault>
</methodResponse>

La lecture de cette réponse sera fait à l'aide de la classe XmlRpcResponse dont voici la définition :

class XmlRpcResponse : public QObject
{
    Q_OBJECT
public:
    XmlRpcResponse();
        XmlRpcResponse(const QByteArray & datas);

    void setResponse(const QByteArray & data);

    int faultCode() const;
    const QString & faultString() const;
    const QVariantList & parameters() const;
private:
    void readResponse(QXmlStreamReader * reader);
    void readFault(QXmlStreamReader * reader);
    void readParams(QXmlStreamReader * reader);
    void readParam(QXmlStreamReader * reader);
    QVariant readVariable(QXmlStreamReader * reader);
    void readListData(QXmlStreamReader* reader, QVariantList & list);
    void readListValue(QXmlStreamReader * reader, QVariantList & list);
    void readMapMembers(QXmlStreamReader* reader, QVariantMap & list);
    void readMapMember(QXmlStreamReader* reader, QVariantMap & list);
    QString readMapName(QXmlStreamReader * reader);

    int _fault_code;
    QString _fault_string;
    QVariantList _parameters;
};

L'interface Trac / XML-RPC

Connection

L'interface graphique