Web Services and the xTuple Desktop Client

 

davidbeauchamp's picture

While our development team is hard at work creating our very own REST API, I thought it was an appropriate time to highlight some of the Web service tools hidden in plain sight in our scripting toolbox. As an example, I have created a simple JSON object that looks like this, sample raw materials priced by day, and with a little scripting we can pull this data into our screen.

We begin by setting up our new display, using the Display Class introduced in the 3.7 series.

// sets the window title instead of the generic "display"
mywindow.setWindowTitle(qsTr("Web Service Example"));
// sets the label above the list 
mywindow.setListLabel(qsTr("Web Service Example..."));
// bind to the query button
var _query = mywindow.queryAction();
// set list not to populate altId's
mywindow.setUseAltId(false);
 // choose to enable query on start or not
mywindow.setQueryOnStartEnabled(false);
// hide the search box
mywindow.setSearchVisible(false);
 // disables the parameter widget, you can pass those parameters to web service queries but this text file won't accept them
mywindow.setParameterWidgetVisible(false);
// just to make the following easier to read
//var _pw = mywindow.parameterWidget();
//_pw.append(qsTr("Date"), "date", ParameterWidget.Date, mainwindow.dbDate());
// this runs through the defaults we specified making the defaulted filters visible
//_pw.applyDefaultFilterSet();
// bind to the list object
var _list = mywindow.list();
// add columns to list that comes along with our display class
_list.addColumn(qsTr("Date"), -1, Qt.AlignLeft, true, "date");
_list.addColumn(qsTr("Aluminum/LB"), -1, Qt.AlignLeft, true, "aluminum");
_list.addColumn(qsTr("Copper/LB"), -1, Qt.AlignLeft, true, "copper");
_list.addColumn(qsTr("Gold/LB"), -1, Qt.AlignLeft, true, "gold");
_list.addColumn(qsTr("Nickel/LB"), -1, Qt.AlignLeft, true, "nickel");
_list.addColumn(qsTr("Platinum/LB"), -1, Qt.AlignLeft, true, "platinum");
_list.addColumn(qsTr("Silver/LB"), -1, Qt.AlignLeft, true, "silver");
// the network access manager that will be making the network calls
var _netmgr = new QNetworkAccessManager(mywindow);

Nothing too drastic, just setting up the screen and adding the columns for the data we expect. The important line is the very last one instantiating the QNetworkAccessManager, which will do our network communication for us. Even though we aren't using it, I left the commented-out parameter widget code in here as a sample.

Now that we have the network manager at our disposal, it is time to write the code that will send our query to the remote API.

function sSendQuery()
{
  try
  {
    var url = new QUrl("http://pastebin.com/raw.php?i=65SHG0vZ"); //not a real api of course, but it works 

    // sample of how to get the date from our parameter widget
    //var date = mywindow.parameterWidget().parameters().date;

    // if we have it
    //if (date != undefined || date != "")
    //{
      //var query = new Object;
      //query.date = date;
      //url.setQueryItems(query); this adds the ?date= to the URL
    //}

    // prepare the request    
    var netrequest = new QNetworkRequest();
    netrequest.setUrl(url);

    // fetch content of provided url using our network manager
    _netmgr.get(netrequest);
  }
  catch (e) {
    print("sSendQuery - exception at line " + e.lineNumber + ": " + e);
  }
}

A little bit to take in here, but, just as in SQL, we are building our query string and then submitting our request to the server. Except, in this case, it is a Web server instead of a SQL server. Up next is what comes when we actually get an answer to this request, which is asynchronous and handled via a callback method.

function sGetResponse(netreply)
{
  try
  {
    if (netreply.error())
    {
      QMessageBox.warning(mywindow, qsTr("Network Error"),  qsTr("<p>The request for metals prices "
                             + "returned error %1<br>%2").arg(netreply.error()).arg(netreply.errorString()));
      return;
    }

    // get contents of response into a local variable
    var response = netreply.readAll();
    // we don't want to do anything with it if it is empty
    if (response == undefined || response == "") 
    {
      QMessageBox.warning(mywindow, qsTr("Network Error"),  qsTr("<p>The request for metals prices returned an empty response."));
      return;
    }
	
    // try and parse the JSON object into a javascript array
    var metals = eval('(' + response + ')');
    	
    var counter = 0;
    // loop over properties of array
    for (var prop in metals)
    {
        if (metals.hasOwnProperty(prop)) // javascript eccentricity to ensure we're only iterating properties directly in this array, not ones coming from the prototype chain or children
	{   // for each row returned we add one to the _list
	    var row = new XTreeWidgetItem(_list, counter, metals[prop].date, metals[prop].aluminum, 
			metals[prop].gold, metals[prop].copper, metals[prop].nickel, metals[prop].platinum,
			metals[prop].silver);
	    counter++;
        }
    }
  }
  catch (e)
  {
    print("sGetResponse - exception at line " + e.lineNumber + ": " + e);
  }
}
//disconnect query button from the sFillList() slot since we aren't using a metasql query
toolbox.coreDisconnect(_query,"triggered()",mywindow,"sFillList()");
//these two are imperative to the entire operation, the first one links the callback method to the repsonse from the webserver
//the second line links the query button on the screen to our code which makes the request
_netmgr.finished.connect(sGetResponse);
_query.triggered.connect(sSendQuery);

There is one more step. In order to use this in your client, you need an initMenu to add the menu entry. Sample one below. Save the above code in the database as webServiceExample, the code below as initMenu, and the entry appears under System > Utilities as Metal Prices.

var utilsMenu	= mainwindow.findChild("menu.sys.utilities");
var action = utilsMenu.addAction(qsTr("Metal Prices"), mainwindow);
action.objectName = "sys.metalPrices";
action.triggered.connect(metals);

function metals()
{
  toolbox.newDisplay("webServiceExample");
}

When you are finished, you should have a display that looks like this:

This isn't so scary. Hopefully, this will give you an idea of the kind of things you can do using the network access manager. I highly recommend reading through the QNetworkAccessManager documentation to learn some of the other things you can do.

AttachmentSize
webserviceexample.png35.98 KB
webserviceexample.txt4.22 KB
initmenu.txt266 bytes
webserviceexample-sample-json-object.png10.61 KB
 
bcwilson's picture
Offline
Joined: 12/30/2008
Nice work, David!

I love a good example! I'm sure I will be using this example at some point to pull outside data into an xTuple display. 

 
malfredo32's picture
Offline
Joined: 05/12/2008
Quite interesting

David,

 

Quite interesting, it open a door to many different services out there, I can quickly think on getting the official exchange rate. In particular in Mexico the Trasury Bank has a web service that publish daily the official exchange rate and actually is entered to xTuple manually. We still need a good way to manage the XML files that web services use send back and forth but it is great in my opinion.

 

 

 
davidbeauchamp's picture
Offline
Joined: 07/20/2008
We can handle XML as well

We include soap2js with the application, and this will let you convert that XML document into a JavaScript object just like the native eval() methods for JSON. Just include it in your script (include("soap2js");) and then in the sGetRespone function, you could do something like:

 var xmlstring = netreply.readAll();
 var jsobject = xml2js(xmlstring);

And now in jsobject you should have the hierarchy from the XML response of the web service. Insert a debugger; statement after the jsobject line so you can inspect the jsobject Object and learn how you could access its children. In my example from the blog post it was array[position].columname, like in the example below:

One last thing to mention is that the network manager supports HTTP PUT, POST and I believe DELETE requests as well as the GET we typically used to retrieve web sites. If the site requires you submit an answer you could still do that in script.

 
sbuttgereit496's picture
Offline
Joined: 04/14/2009
A couple things to note about js2soap.js

Having used these tools recently to make lightweight, pseudo -web service calls to a third party shipping system, I've come across a couple of things that can get in your way if you're not careful or expecting them.

1) The XML created by the js2xml function call in js2soap.js is 'pretty print' style, meaning it produces XML that contains newlines and indents the XML in a way that's nice for humans to read.  Unfortunately, not all systems are forgiving of this (including the one I was working with) so I had to modify the library in order to produce plain, ugly, straight-up XML.  I don't really consider this anything like a bug because I'm pretty sure XML standards indicate that any whitespace characters outside of elements are to be ignored, but standards don't matter unless all parties follow them. 

2) I have opened a bug related to tag generation from a given JSON object.  While all the desired tags get created, it appears as though some of the special members such as xmlattribute not only perform their desired functions (creating an attribute for an element) they also become elements themselves.  http://www.xtuple.org/xtincident/view/bugs/20433  I think the tags I embedded in the issue description may have gotten munged by bug tracker interface, but there is a sample use case.

Having said that, with some tweaks to avoid these issues, these tools work rather well.  Wish QT reported regular HTTP error responses rather than their special error codes, but given that they are basing this on functionality that handles more than HTTP it's understandable.