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;
};