
Working with JSON data
Published by Sam Pickard on March 23, 2010I've written a JSON parser. Like the XML parser, it makes calls to your classes to do something useful with the data as it decodes it. Unlike the XML parser, the JSON parser currently requires a complete JSON string, so make sure you've downloaded all the data before you call it. JSON data is typically quite small though.
There are six methods you need to implement to use JSON successfully. They are defined as the JSONListener interface
class JSONListener
{
public:
virtual void header(String& header) = 0;
virtual void data(String& value) = 0;
virtual void arrayStart() = 0;
virtual void arrayEnd() = 0;
virtual void objectStart() = 0;
virtual void objectEnd() = 0;
};
Each of these is called when the appropriate data is found. To use the JSON parser, you need to create a class which inherits from JSONListener. We'll do this shortly.
The Code
This is the source code for the JSON parser. It can also be found on the MoSync forum.
JSONParser.h
/*
* JSONParser.h
*
* Created on: 21 Feb 2010
* Author: sjp
*/
#ifndef JSONPARSER_H_
#define JSONPARSER_H_
#include <MAUtil/String.h>
using namespace MAUtil;
class JSONListener
{
public:
virtual void header(String& header) = 0;
virtual void data(String& value) = 0;
virtual void arrayStart() = 0;
virtual void arrayEnd() = 0;
virtual void objectStart() = 0;
virtual void objectEnd() = 0;
};
class JSONParser
{
public:
JSONParser();
~JSONParser();
void parseString(String& jsondata);
void addListener(JSONListener* listener);
void reset();
private:
String data;
String temp;
int _start;
int _end;
Vector<JSONListener*> _listeners;
void parse();
int findNextQuote(int startPos);
int nextDelimiter(int startPos);
int findPosition(char c, int startPos);
bool gotHeader;
};
#endif /* JSONPARSER_H_ */
JSONParser.cpp
JSONParser.cpp
/*
* JSONParser.cpp
*
* Created on: 21 Feb 2010
* Author: sjp
*/
#include "JSONParser.h"
#include "../std.h"
JSONParser::JSONParser()
{
}
JSONParser::~JSONParser()
{
}
void JSONParser::reset()
{
_start = 0;
data.clear();
temp.clear();
gotHeader = false;
}
void JSONParser::parseString(String& jsondata)
{
data = jsondata;
_start = 0;
parse();
}
void JSONParser::parse()
{
while(_start < data.length())
{
//Move through white space
char firstChar = data.c_str()[_start];
while(firstChar < 33)
firstChar = data.c_str()[++_start];
//Check the first character, if it is an [,],{ or } raise the approriate event
if(firstChar == '[')
{
Vector_each(JSONListener*, itr, _listeners)
(*itr)->arrayStart();
_start++;
gotHeader = false;
}
else if(firstChar == ']')
{
Vector_each(JSONListener*, itr, _listeners)
(*itr)->arrayEnd();
_start++;
gotHeader = false;
}
else if(firstChar == '{')
{
Vector_each(JSONListener*, itr, _listeners)
(*itr)->objectStart();
_start++;
gotHeader = false;
}
else if(firstChar == '}')
{
Vector_each(JSONListener*, itr, _listeners)
(*itr)->objectEnd();
_start++;
gotHeader = false;
}
else if(firstChar == ',')
{
_start++;
gotHeader = false;
}
else if(firstChar == ':')
{
_start++;
gotHeader = true;
}
else if(!gotHeader)
{
//read the header
temp.clear();
_end = data.findFirstOf(':', _start);
_end -= _start; //get the length
if(_end > 0)
{
temp = data.substr(_start, _end);
_start += _end;
Vector_each(JSONListener*, itr, _listeners)
(*itr)->header(temp);
}
}
else
{
//Processing data
if(firstChar == '\"')
{
//Processing a string
_end = findNextQuote(_start + 1) + 1;
_end -= _start;
if(_end > 150)
temp = data.substr(_start + 1, 150); //don't take the leading quote, it won't have a pair
else
{
temp = data.substr(_start, _end);
}
_start += _end;
Vector_each(JSONListener*, itr, _listeners)
(*itr)->data(temp);
}
else
{
_end = nextDelimiter(_start);
_end -= _start;
temp = data.substr(_start, _end);
_start += _end;
Vector_each(JSONListener*, itr, _listeners)
(*itr)->data(temp);
}
}
}
}
int JSONParser::findNextQuote(int startPos)
{
startPos = data.findFirstOf('\"', startPos);
if(data.c_str()[startPos-1] == '\\')
startPos = findNextQuote(startPos + 1); //Ignore any \" pairs in the string data
return startPos;
}
void JSONParser::addListener(JSONListener* listener)
{
_listeners.add(listener);
}
int JSONParser::nextDelimiter(int startPos)
{
int nextComma = findPosition(',', startPos);
int nextSquareBrace = findPosition(']', startPos);
int nextCurlyBrace = findPosition('}', startPos);
int lowest = nextComma < nextSquareBrace ? nextComma : nextSquareBrace;
if(nextCurlyBrace < lowest)
lowest = nextCurlyBrace;
return lowest;
}
int JSONParser::findPosition(char c, int startPos)
{
int cpos = data.findFirstOf(c, startPos);
if(cpos == -1)
cpos = data.length();
return cpos;
}
Example application
Recently, I've wanted to include data from TripSay (www.tripsay.com) into my application. They have two interfaces which are called by formatting a URL, and putting the request data into the querystring, and returning JSON data. The first one lets you query their list of locations where they have data, and the second gets the data.
|
|
To work with this data, I've created a screen with an input box which will take the search string, call the TripSay API, download the list of matches as JSON, and then display this list.
MapSearchScreen.h
/*
* MapSearchScreen.h
*
* Created on: 22 Feb 2010
* Author: sjp
*/
#ifndef MAPSEARCHSCREEN_H_
#define MAPSEARCHSCREEN_H_
#include <MAUI/Screen.h>
#include <MAUI/Layout.h>
#include <MAUI/Image.h>
#include <MAUI/ListBox.h>
#include <MAUI/EditBox.h>
#include "../Widgets/UIBuilder.h"
#include "../Utilities/Util.h"
#include "../Widgets/SoftKeyBar.h"
#include "../Utilities/JSONParser.h"
#include <MAUtil/Downloader.h>
#include <MAP/LonLat.h>
using namespace MAUI;
using namespace MAUtil;
using namespace MAP;
#define STAGE_UNIQUENAME 0
#define STAGE_LAT 1
#define STAGE_LON 2
class MapSearchScreen : public Screen,
public ISoftKeyBarListener,
public JSONListener,
public DownloadListener
{
public:
MapSearchScreen();
~MapSearchScreen();
void keyPressEvent(int keyCode);
void softKeySelected(int buttonID);
//JSON
void header(String& header);
void data(String& value);
void arrayStart();
void arrayEnd();
void objectStart();
void objectEnd();
//Downloader
void notifyProgress(Downloader* downloader, int downloadedBytes, int totalBytes);
bool outOfMemory(Downloader* downloader);
void finishedDownloading(Downloader* downloader, MAHandle data);
void downloadCancelled(Downloader* downloader);
void error(Downloader* downloader, int code);
private:
Layout* layout;
EditBox* criteria;
Image* image;
ListBox* listBox;
Layout* tab;
JSONParser* jsonparser;
int stage;
int objectCounter;
Downloader* dl;
LonLat* curLonLat;
String encodedUrl;
Vector<LonLat*> locations;
};
#endif /* MAPSEARCHSCREEN_H_ */
MapSearchScreen.cpp
/*
* MapSearchScreen.cpp
*
* Created on: 22 Feb 2010
* Author: sjp
*/
#include "MapSearchScreen.h"
#include "../std.h"
#include "../Utilities/Util.h"
#include "../Utilities/Base64.h"
#include "MapScreen.h"
MapSearchScreen::MapSearchScreen()
{
image = (Image*)createMainLayout("search", "back", this);
layout = (Layout*) image->getChildren()[0];
listBox = (ListBox*) layout->getChildren()[1];
Label* title = (Label*) layout->getChildren()[0];
title->setCaption("Map Search");
this->setMain(image);
criteria = createEditBox("london", TEXTFIELD);
tab = createTab("Place to search (e.g. Paris)", criteria);
listBox->add(tab);
criteria->setSelected(true);
jsonparser = NULL;
dl = NULL;
this->setMain(image);
}
MapSearchScreen::~MapSearchScreen()
{
if(jsonparser != NULL)
delete jsonparser;
if(dl != NULL)
delete dl;
}
void MapSearchScreen::softKeySelected(int buttonID)
{
switch(buttonID)
{
case SoftKeyBar::LSK:
if(dl->isDownloading())
return;
if(dl == NULL)
{
dl = new Downloader();
dl->addDownloadListener(this);
}
objectCounter = 0;
char buffer[200];
char* b = buffer;
sprintf(buffer, "http://www.tripsay.com/api/json/uniqueNames?api_key=8eke83j4&query=%s&maxResults=20", criteria->getCaption().c_str());
encodedUrl.clear();
LOG("Encoding '%s'", b);
base64_encode(b, strlen(buffer), encodedUrl);
sprintf(buffer, "http://datilo.net/ImageCache/Transcode.aspx?encpath=%s", encodedUrl.c_str());
LOG("Downloading '%s'", buffer);
dl->beginDownloading(buffer);
break;
case SoftKeyBar::FIRE:
break;
case SoftKeyBar::RSK:
APPCONTROLLER->back();
break;
}
}
void MapSearchScreen::keyPressEvent(int keyCode)
{
switch(keyCode)
{
case MAK_UP:
listBox->selectPreviousItem();
break;
case MAK_FIRE:
if(listBox->getSelectedIndex() > 0)
{
//Set this as the search text
MapScreen* ms = (MapScreen*)APPCONTROLLER->getScreen(MAPSCREEN);
Label* l = (Label*)listBox->getChildren()[listBox->getSelectedIndex()];
curLonLat = locations[listBox->getSelectedIndex() -1];
ms->setLocation(*curLonLat);
ms->setSearchLocation((char*)l->getCaption().c_str());
APPCONTROLLER->back();
}
else
{
softKeySelected(SoftKeyBar::LSK);
}
break;
case MAK_DOWN:
listBox->selectNextItem();
break;
}
}
//JSON handling
void MapSearchScreen::objectEnd()
{
objectCounter--;
if(objectCounter == 0)
locations.add(curLonLat);
}
void MapSearchScreen::objectStart()
{
objectCounter++;
}
void MapSearchScreen::arrayEnd()
{}
void MapSearchScreen::arrayStart()
{}
void MapSearchScreen::header(String& header)
{
trimQuotes(header);
// LOG("Got header '%s'", header.c_str());
if(strncmp(header.c_str(), "uniqueName", 10) == 0)
{
stage = STAGE_UNIQUENAME;
}
else if(strncmp(header.c_str(), "lng", 3) == 0)
{
stage = STAGE_LON;
}
else if(strncmp(header.c_str(), "lat", 3) == 0)
{
stage = STAGE_LAT;
}
else if(strncmp(header.c_str(), "country", 7) == 0)
{
//New entry
curLonLat = new LonLat();
stage = -1;
}
else
{
stage = -1;
}
}
void MapSearchScreen::data(String& data)
{
switch(stage)
{
case STAGE_UNIQUENAME:
trimQuotes(data);
listBox->add(createLabel(data));
break;
case STAGE_LAT:
//LOG("Writing Lat '%s'", data.c_str());
curLonLat->lat = stringToDouble(data);
break;
case STAGE_LON:
//LOG("Writing Lon '%s'", data.c_str());
curLonLat->lon = stringToDouble(data);
break;
}
}
//Downloader
void MapSearchScreen::downloadCancelled(Downloader* downloader)
{}
bool MapSearchScreen::outOfMemory(Downloader* downloader)
{}
void MapSearchScreen::notifyProgress(Downloader* downloader, int d, int t)
{
//LOG("Downloaded %d of %d", d, t);
}
void MapSearchScreen::finishedDownloading(Downloader* downloader, MAHandle data)
{
String json;
int size = maGetDataSize(data);
char* jsonchar = new char[size + 1];
maReadData(data, jsonchar, 0, size);
json.append(jsonchar, size);
delete [] jsonchar;
if(jsonparser == NULL)
{
jsonparser = new JSONParser();
jsonparser->addListener(this);
}
// LOG("Parsing JSON");
jsonparser->parseString(json);
}
void MapSearchScreen::error(Downloader* downloader, int code)
{
LOG("Error: %d", code);
}
To get the JSON data initially in MapSearchScreen, I've also used an MAUtil::Downloader class. I make the request to the URL (the API key is omitted). I've also implemented JSONListener.
Here is a small section of the JSON the TripSay API returns
[
{
"country":"FR",
"lat":48.8566,
"lng":2.35097,
"name":"Paris",
"uniqueName":"Paris,Île-de-France,France"
},
{
"country":"US",
"lat":33.67,
"lng":-95.55,
"name":"Paris",
"uniqueName":"Paris,Texas,USA"
},
{
"country":"CA",
"lat":43.2,
"lng":-80.38,
"name":"Paris",
"uniqueName":"Paris,Canada"
}
]
You can see that it gives me an array of object, with [ and ] denoting the array and the { and } denoting the object. Immediately I can see that I can't simply convert this to XML, as there isn't any root element. I can also see that whenever I've got as many } tags as {, then I've got a complete object and I can process some data.
In my MapSearchScreen code, I've implemented the objectStart and objectEnd methods
void MapSearchScreen::objectEnd()
{
objectCounter--;
if(objectCounter == 0)
locations.add(curLonLat);
}
void MapSearchScreen::objectStart()
{
objectCounter++;
}
So whenever I see { I add one, and whenever I see }, I take one away. When I've got 0, I know I've completed an object. By using this technique, the code is robust to take objects inside objects and not start processing at the wrong time.
When I read the header, I can set a flag to say what the next piece of data is going to be
void MapSearchScreen::header(String& header)
{
trimQuotes(header);
if(strncmp(header.c_str(), "uniqueName", 10) == 0)
stage = STAGE_UNIQUENAME;
else if(strncmp(header.c_str(), "lng", 3) == 0)
stage = STAGE_LON;
else if(strncmp(header.c_str(), "lat", 3) == 0)
stage = STAGE_LAT;
else if(strncmp(header.c_str(), "country", 7) == 0)
{
//New entry
curLonLat = new LonLat();
stage = -1;
}
else
stage = -1;
}
and when I get the data, I can perform the appropriate action
void MapSearchScreen::data(String& data)
{
switch(stage)
{
case STAGE_UNIQUENAME:
trimQuotes(data);
listBox->add(createLabel(data));
break;
case STAGE_LAT:
curLonLat->lat = stringToDouble(data);
break;
case STAGE_LON:
curLonLat->lon = stringToDouble(data);
break;
}
}
In this example, I'm not interested in arrayStart and arrayEnd, so I've left them as empty methods.
void MapSearchScreen::arrayEnd()
{}
void MapSearchScreen::arrayStart()
{}
Whenever I get data for the tag 'uniqueName' I add a label to the listbox, and I save the latitude and longitude data from the JSON. When the label is selected, I can then pass the latitude and longitude to the map screen, so it can show the correct location.
When I show the map, I've got a helper class which turns the JSON data into a UI component, and returns it to the map.
Summary
I've gone from hating JSON data to loving it. This JSON parser is significantly easier to work with than MTXml library, and means I don't have to include the MTXml library if I can have JSON data instead. If you can live with the restriction that you have to download all the data before you start processing it (and you probably can), then you'll find that the JSON parser probably does everything you need.
Share on Facebook
Issue with Hours
Hi Sam are you OK?
First i'm grateful for this code. I'm using your classes in my code, but i have a issue with my JSON message. I get the follow message: {"ReturnCode":200,"Action":800,"Content":[2,"31/01/2011 13:37:22"]}, then call "JSONParser->parseString(string)", but when i capture this value on "void data(String& value)" i see 2,"31/01/2011 14 (without " in the end) not 2,"31/01/2011 13:37:22". Maybe the issue is ":" but i don´t know if its correct.
Can you help me with this problem ?
Thank you!
Marcelo Alves