הלינקייה: מגזין חודשי למפתחים

רוצה לשמוע על כל האירועים, המדריכים, הקורסים והמאמרים שנכתבו החודש ?
הלינקייה הינו מגזין חופשי בעברית שמשאיר אותך בעניינים.
בלי ספאם. בלי שטויות. פעם בחודש אצלך בתיבה.

Consuming a JSON Web Service

As the web grows, thousands of web sites, including facebook, twitter, google maps and many others offer third party developers the ability to interact with their content using a dedicated API.

Standardization also contributed to making it easy to write code that interacts with those sites. As a developer writing desktop applications, using third party APIs can largely enhance your applications with new capabilities that would take too long to develop yourself.

While developing your own global weather database can be difficult, using a web API to get weather report near you is easy. On programmableweb you can find a list of over 6,000 web based APIs to integrate in your applications.

This article will cover the basics of getting a web API integrated in a Qt app. I will first introduce the concepts of a REST web API, then move on to implementing a simple client to consume jokes from an online Chuck Norris jokes database. Full working source code for this article is available on github at: https://github.com/ynonp/qchuck.

1. Web APIs 101

A Web API is a server listening for HTTP requests, usually on port 80. They look just like normal web traffic, so they pass through most firewalls. Web APIs usually use various HTTP methods to perform different actions: a GET request to fetch data, a POST request to create a new record, a PUT request to update an existing record, and a DELETE request to delete a record.

If you're using a Unix or Mac, you can use a tool called curl to test web APIs easily. curl is a command line tool for sending out HTTP requests to any web server. It's also possible to use any normal web browser to send out GET requests.

Try This: in your browser open a new tab and go to: http://api.icndb.com/jokes/random. It's that simple.

A web method can also take parameters. If we're dealing with a GET method, the parameters are passed as part of the url, sometimes after a question mark. Here are two examples:

  • https://graph.facebook.com/btaylor
  • http://api.icndb.com/jokes/random?firstName=bob&lastName=spunge

As you can see, it's the job of the site publishing the API to also publish the documentation explaining how to use it. Both facebook and the Chuck Norris jokes site have one.

POST and PUT requests are a bit more complex, because they can take any data as their input. Refer to the specific API docs for parameter passing details.

For most web APIs, the result of a data fetch is encoded in XML or JSON data format.

Accessing either links mentioned above produced a JSON object. JSON stands for JavaScript Object Notation and it is simply a way of writing an object that JavaScript code will have an easy time understanding. The JSON spec is very simple and includes nested objects, arrays, strings and numbers.

Qt5 has native JSON support, and for Qt4 there's an open source JSON library for Qt called qjson is available on sourceforge. I'll use that library for the example project.

2. Implementing Web API consumer in Qt

In order to consume a web application, we need to do two things:

Be able to send various kinds of HTTP network requests
Parse the response into application-level data
Therefore, the methodology for writing a Qt API consumer is as follows:

Write an API class with all the methods you'll need from the API. Each method in this class is asynchronous — it will send a network request and emit a signal when the reply is received. The class will use QNetworkAccessManager for network traffic.
Write a value class for each meaningful value you expect to get from the API.
This way, you can keep your application and the web API separated.

3. Demo API: Chuck Norris Jokes

Let's create a demo project that uses a web API to display Chuck Norris related jokes. We'll use API methods to get a random Chuck Norris joke, and to get a random joke customized to any name.

3.1 Build Settings for Network Application

Start with a new console project, and add to .pro file the line

QT += network
This adds linking with QtNetwork module, which has QNetworkAccessManager. Another build step you must perform to get the application running is download and compile qjson for your platform according to the instructions on their website. We'll use qjson to parse the JSON response.

3.2 Network Access Code

Now that everything's set, it's safe to start coding. First thing's first, the ChuckNorrisAPI class .h file:

#ifndef CHUCKNORRISAPI_H
#define CHUCKNORRISAPI_H

#include
#include

class ChuckNorrisAPI : public QObject
{
Q_OBJECT

public:
    explicit ChuckNorrisAPI(QObject *parent = 0);

    void randomChuckNorrisJoke();
    void randomJoke( const QString &firstName, const QString &lastName);

private:
    void getRequest( const QString &url );

signals:
    void jokeReady( const QByteArray &jokeAsJSON );
    void networkError( QNetworkReply::NetworkError err );

public slots:
    void parseNetworkResponse( QNetworkReply *finished );

private:
    QNetworkAccessManager m_nam;
};

#endif // CHUCKNORRISAPI_H

As you can see, I created two public methods for each API method exposed. I chose to use the same signals for both methods, but for more complex APIs you may want to use separate signals.

A QNetworkAccessManager member object is responsible for all networking code. It has methods to send GET, POST, PUT and DELETE requests, and signals for both completion and errors. The QNetworkAccessManager is meant to be used as a singleton, so it's a good idea to create one as a member in the API class, or for more complex applications to initialize one in main method and pass it as an argument when constructing the API class.

Moving on in the code, here's the constructor which connects signals to slots, and the getRequest method that sends a GET request to a specific url. Both public methods use it. Remember full source code is available on github.

// Fragments from ChuckNorrisAPI.cpp
ChuckNorrisAPI::ChuckNorrisAPI(QObject *parent) :
    QObject(parent)
{
    QObject::connect(&m_nam, SIGNAL(finished(QNetworkReply*)),
                     this, SLOT(parseNetworkResponse(QNetworkReply*)));
}

void ChuckNorrisAPI::getRequest( const QString &urlString )
{
    QUrl url ( urlString );
    QNetworkRequest req ( url );
    m_nam.get( req );
}

QNetworkAccessManager is asynchronous, so calling m_nam.get returns immediately. When the request finishes, the network access manager will emit finished(...) signal. Here's the code that handles that signal:


void ChuckNorrisAPI::parseNetworkResponse( QNetworkReply *finished )
{
    if ( finished->error() != QNetworkReply::NoError )
    {
        // A communication error has occurred
        emit networkError( finished->error() );
        return;
    }

    // QNetworkReply is a QIODevice. So we read from it just like it was a file
    QByteArray data = finished->readAll();
    emit jokeReady( data );
}

Note: The Chuck Norris API is very simple, so any network reply arriving is a Chuck Norris joke. In more complex cases you'll probably have multiple kinds of requests. If this is the case, use QObject::connect inside the request handling method, and bind the relevant slot according to the request.

3.3 Parsing the JSON string

When you have a JSON string at hand, the next step is to parse it. Using qjson, we can convert a JSON string into a QVariant. Since JSON objects can have multiple data types, they are mapped to Qt's variant data type and fetched according to the following table (assuming obj is the QVariant holding the JSON value):

Data Type Extraction Method
String obj.toString()
Number obj.toInt() or obj.toFloat()
Nested object obj.toMap()
Array obj.toList()

let's examine the code in Joke.cpp that parses a JSON string and initializes a joke object:

    QJson::Parser parser;
    bool ok;
    QVariantMap result = parser.parse (jokeAsJSON, &ok).toMap();

    if ( ok )
    {
        QVariantMap jokeData = result["value"].toMap();

        m_id         = jokeData["id"].toInt();
        m_text       = jokeData["joke"].toString();
        m_categories = variantListToStringList(jokeData["categories"].toList());
        m_isValid    = true;
    }

Some things to notice:

A value object parsed from network data can have an invalid value. This is because network data can be corrupted or not what you expect. I use a boolean member to identify invalid objects.
toList() returns a QVariantList. If you know that all objects in the list are of the same type, it's possible to create a new list with a stricter type. This is what variantListToStringList function does — it iterates the variant list and creates a new QStringList from it.
4. Consuming Web APIs Takeaways

In this article we've covered the basics of consuming web APIs. Qt's network access manager is indeed an awesome tool for developers wishing to use external network APIs in their applications.

Web APIs can enhance your application, or create cool mashups. See programmableweb.com for mashup and API ideas

QNetworkAccessManager is not perfect, though. It's biggest flaw is its inability to set network timeouts, which forces developers to write their own code to handle network timeouts.

When writing network code, you also need to pay close attentions to errors, both transient and permanent. Using web APIs exposes your application to risks of API deprecation, of the external services going down, or users running your application on offline machines. Try to structure your application in a way that it can still provide some functionality to users even if they are offline or the Web API is not available.
For example, if you're working on a weather tool, every time you got a forecast try to save that data locally in QSettings or in a file. That way, if a user runs the application while she's offline, she can still see previously fetched forecasts.

Are you using a Web API to enhance a Qt application ? Tell me about it in the comments below.