基于 Qt 的 XML-RPC 客户端:解析请求/响应体

上一节我们已经详细了解了 XML-RPC 协议的规范。从上节的内容可以看出,解析 XML-RPC,主要就是对 XML-RPC 协议中请求/响应体进行解析。而这些请求/响应体都是 XML 格式的,因此,我们需要使用 Qt 的 XML API 来完成这个工作。

我们的解析工作需要从两个方面进行:将实际数据转换成 XML 格式,以及将 XML 格式的数据转换成实际数据。

首先,我们需要解决的问题是,所谓的实际数据应当是什么格式。我们可以找到一个很适合做这种实际数据类型的 Qt 类:QVariant。这个类的作用就是将具体数据封装为统一的类型,类似于提供一种单根机制。几乎所有 Qt 能够识别出的数据类型都可以存入QVariant,因此,我们只需提供QVariant和 XML 的转换即可。

QVariant -> XML

QVariant到 XML 的转换函数名定义为toXML。这个函数接受一个QVariant作为参数,返回值是QDomElement类型。下面我们给出这个函数的具体代码:

QDomElement XmlRpcValue::toXml(const QVariant &var)
{
    QDomDocument doc;
    QDomElement tagValue = doc.createElement("value");

    switch(var.type()) {
    case QVariant::Int: // <int>
    {
        QDomElement tagInt = doc.createElement("int");
        tagInt.appendChild(doc.createTextNode(QString::number(var.toInt())));
        tagValue.appendChild(tagInt);
    }
        break;
    case QVariant::Bool: // <boolean>
    {
        QDomElement tagBoolean = doc.createElement("boolean");
        tagBoolean.appendChild(doc.createTextNode(var.toBool() ? "1" : "0"));
        tagValue.appendChild(tagBoolean);
    }
        break;
    case QVariant::String: // <string>
    {
        QDomElement tagString = doc.createElement("string");
        tagString.appendChild(doc.createTextNode(var.toString()));
        tagValue.appendChild(tagString);
    }
        break;
    case QVariant::Double: // <double>
    {
        QDomElement tagDouble = doc.createElement("double");
        tagDouble.appendChild(doc.createTextNode(QString::number(var.toDouble())));
        tagValue.appendChild(tagDouble);
    }
        break;
    case QVariant::DateTime: // <dateTime.iso8601>
    {
        QDomElement tagDateTime = doc.createElement("datetime.iso8601");
        tagDateTime.appendChild(doc.createTextNode(
                                        var.toDateTime()
                                           .toString("yyyyMMddThh:mm:ss")));
        tagValue.appendChild(tagDateTime);
    }
        break;
    case QVariant::ByteArray: // <base64>
    {
        QDomElement tagBase64 = doc.createElement("base64");
        tagBase64.appendChild(doc.createTextNode(var.toByteArray().toBase64()));
        tagValue.appendChild(tagBase64);
    }
        break;
    case QVariant::List: // <array>
    case QVariant::StringList: // <array>
    {
        QDomElement tagArray = doc.createElement("array");
        QDomElement tagData = doc.createElement("data");
        foreach(QVariant v, var.toList()) {
            tagData.appendChild(toXml(v));
        }
        tagArray.appendChild(tagData);
        tagValue.appendChild(tagArray);
    }
        break;
    case QVariant::Map: // <struct>
    {
        QDomElement tagStruct = doc.createElement("struct");
        QDomElement member;
        QDomElement name;

        QMap<QString, QVariant> map = var.toMap();
        QMapIterator<QString, QVariant> i(map);
        while(i.hasNext()) {
            i.next();

            name = doc.createElement("name"); // <name>
            name.appendChild(doc.createTextNode(i.key()));

            member = doc.createElement("member"); // <member>
            member.appendChild(name);
            member.appendChild(toXml(i.value()));

            tagStruct.appendChild(member);
        }
        tagValue.appendChild(tagStruct);
    }
        break;
    default:
    {
        LOG_WARN(QString("Failed to marshal unknown variant type: %1."
                         "Use string instead.").arg(var.type()));
        QDomElement tagString = doc.createElement("string");
        tagString.appendChild(doc.createTextNode(var.toString()));
        tagValue.appendChild(tagString);
    }
    }

    return tagValue;
}

上面代码就是将一个QVariant类型的数据转换成其对应的 XML 格式。代码很长,但是思路很简单。按照 XML-RPC 规范的要求,我们首先判断这个QVariant是什么类型的,但是将其转换成对应的 XML 格式。注意,对于一些可以嵌套的类型,比如 struct,是递归调用的。而对于任何不能识别的类型,我们在 default 分支里面当做 string 进行处理。

XML ->QVariant

我们将 XML 到QVariant的转换函数名定义为 fromXML。这个函数接受一个QDomElement 作为参数,返回值是QVariant类型。下面我们给出这个函数的具体代码:

QVariant XmlRpcValue::fromXml(const QDomElement &xml)
{
    if(xml.tagName().toLower() != "value") {
        return QVariant();
    }

    // If no type is indicated, the type is string.
    if(!xml.firstChild().isElement()) {
        return QVariant(xml.text());
    }

    const QDomElement typeElement = xml.firstChild().toElement();
    const QString typeName = typeElement.tagName().toLower();

    if(typeName == "string") {
        return QVariant(typeElement.text());
    } else if(typeName == "i4" || typeName == "int") {
        return QVariant(typeElement.text().toInt());
    } else if(typeName == "double") {
        return QVariant(typeElement.text().toDouble());
    } else if (typeName == "boolean") {
        return QVariant(typeElement.text().toLower() == "true"
                           || typeElement.text() == "1");
    } else if(typeName == "base64") {
        return QVariant(QByteArray::fromBase64(typeElement.text().toLatin1()));
    } else if(typeName == "datetime" || typeName == "datetime.iso8601") {
        return QVariant(QDateTime::fromString(typeElement.text(),
                                              "yyyyMMddThh:mm:ss"));
    } else if(typeName == "nil") {
        // Non-standard extension
        // http://ontosys.com/xml-rpc/extensions.php
        return QVariant();
    } else if (typeName == "array") {
        QList values;
        QDomNode valueNode = typeElement.firstChild().firstChild();
        while(!valueNode.isNull()) {
            values << fromXml(valueNode.toElement());
            valueNode = valueNode.nextSibling();
        }
        return QVariant(values);
    } else if ( typeName == "struct" ) {
        QMap map;
        QDomNode memberNode = typeElement.firstChild();
        while(!memberNode.isNull())	{
            const QString key = memberNode.toElement().elementsByTagName("name")
                                          .item(0).toElement().text();
            const QVariant data = fromXml(memberNode.toElement()
                                                    .elementsByTagName("value")
                                                    .item(0).toElement());
            map[key] = data;
            memberNode = memberNode.nextSibling();
        }
        return QVariant(map);
    } else {
        LOG_WARN(QString("Cannot demarshal unknown type: %1.")
                     .arg(typeElement.tagName().toLower()));
    }

    return QVariant();
}

这个函数正好与前面的toXML()相反。我们需要从QDomElement获取每一个标签名字,然后与 XML-RPC 标准进行比对。这里,我们所需注意的是,值需要符合标准提供的格式,比如时间等。而在函数中,我们增加了一个 nil 类型,这个类型是对标准的一个扩展。而对于不能识别的类型,则返回一个空的QVariant

虽然代码比较长,但是思路和写法都很简单,这里我们就不详细介绍。对于数据的解析是 XML-RPC 的基础之一。在后面的章节中,我们将基于这些代码,继续实现一个 XML-RPC 客户端。

Leave a Reply