External communication between client and server (ServerComm)
Last publication:23/02/2018at 2:43 PMCédricDamioli
Introduction
This document explains how the clients and server communicate with each other.
The communication is centralized in a single component named ServerComm : this component's main role is to queue up messages, in order to send them grouped to the server and save bandwidth.
Using the ServerComm component follows the scheme :
first of all you create a message (ServerMessage) by choosing an url to call, the parameters to send, a callback function (as communications are mainly asynchronous), a priority and a response type.
then you send it (and show an hourglass in the interface)
when the response is back you are notified (and can then hide the hourglass)
The big picture
(1) Each component sends messages.
(2) The messages are queued on the client side and are sent to the server regularly, all grouped in a single request.
(3) When messages arrive on the server they go through the dispatcher, that dispatches them to their destination.
(4) Each server side component answers
(5) The server then aggregates the answers and sends them back to the browser (meaning that if a request takes a long time to be processed, all the responses in the group will take a long time to be sent back to the client).
(6) On the client side, the ServerComm then properly calls each callback method with each xml response.
Sample code
Sending a message :
Oops !
Copy to clipboard failed. Open the code and copy it manually.
var serverMessage = new org.ametys.servercomm.ServerMessage(
this.getPluginName(), // The plugin that will receive the message or null for the workspace
'search/search.xml?' + args, // The url required in the plugin sitemap
null, // A JSon object given to the url
org.ametys.servercomm.ServerComm.PRIORITY_MAJOR, // A priority for the message
this.searchCallback, // A callback method
this, // The instance of the object having the callback method
null, // The arguments give to the callback (in adition to the response of course)
"xml"); // The response type (xml, text or xml2text)
org.ametys.servercomm.ServerComm.getInstance().send(serverMessage);
var serverMessage = new org.ametys.servercomm.ServerMessage(
this.getPluginName(), // The plugin that will receive the message or null for the workspace
'search/search.xml?' + args, // The url required in the plugin sitemap
null, // A JSon object given to the url
org.ametys.servercomm.ServerComm.PRIORITY_MAJOR, // A priority for the message
this.searchCallback, // A callback method
this, // The instance of the object having the callback method
null, // The arguments give to the callback (in adition to the response of course)
"xml"); // The response type (xml, text or xml2text)
org.ametys.servercomm.ServerComm.getInstance().send(serverMessage);
var serverMessage = new org.ametys.servercomm.ServerMessage(
this.getPluginName(), // The plugin that will receive the message or null for the workspace
'search/search.xml?' + args, // The url required in the plugin sitemap
null, // A JSon object given to the url
org.ametys.servercomm.ServerComm.PRIORITY_MAJOR, // A priority for the message
this.searchCallback, // A callback method
this, // The instance of the object having the callback method
null, // The arguments give to the callback (in adition to the response of course)
"xml"); // The response type (xml, text or xml2text)
org.ametys.servercomm.ServerComm.getInstance().send(serverMessage);
As you can see here you are free to send arguments directly in the url using the GET way or through a JSON object. The second way is much more powerful but harder to debug.
Callback method :
Imagine the server answer is the following :
Oops !
Copy to clipboard failed. Open the code and copy it manually.
Copy to clipboard failed. Open the code and copy it manually.
MyObject.prototype.callback = function(response, args)
{
if (org.ametys.servercomm.ServerComm.handleBadResponse("An error occured", response, "MyObject"))
{
// Handle error
return;
}
var results = response.selectNodes("search/result");
for (var i=0; i < results.length; i++)
{
var result = results[i][org.ametys.servercomm.ServerComm.xmlTextContent];
// Handle good result
}
}
MyObject.prototype.callback = function(response, args)
{
if (org.ametys.servercomm.ServerComm.handleBadResponse("An error occured", response, "MyObject"))
{
// Handle error
return;
}
var results = response.selectNodes("search/result");
for (var i=0; i < results.length; i++)
{
var result = results[i][org.ametys.servercomm.ServerComm.xmlTextContent];
// Handle good result
}
}
MyObject.prototype.callback = function(response, args)
{
if (org.ametys.servercomm.ServerComm.handleBadResponse("An error occured", response, "MyObject"))
{
// Handle error
return;
}
var results = response.selectNodes("search/result");
for (var i=0; i < results.length; i++)
{
var result = results[i][org.ametys.servercomm.ServerComm.xmlTextContent];
// Handle good result
}
}
Message on the client side
Structure
A message is composed of :
A plugin name : the plugin wich will be called on the server side. Can be null to go to the root urls.
An url : this url will be called relatively to the plugin. The url can also have GET parameters. "search/search.xml?keyword=test" but you have to encode them manually.
JSON parameters : these are simple objects like Maps or Arrays of Booleans, Integers, Doubles or Strings.
Oops !
Copy to clipboard failed. Open the code and copy it manually.
Priority : the priority is an important parameter. See the next paragraph.
Callback method : the method handling the response.
Callback scope (ussually "this" if the callback method is defined in the current object scope)
Callback arguments : an array of arguments that will also be given to the callback method.
Response type : can be "xml", "text" or "xml2text". See the paragraph on Response handling.
The class used is ServerMessage. All the parameters above are in the ServerMessage constructor.
See the API of org.ametys.servercomm.ServerMessage for further information.
Priority
The priorioty is a constant which can have the following values :
major : for a fast departure. The message will leave in a few milliseconds (the time to wait for other messages that could be sent in the same JS cycle). The most part of the messages should use this priority.
normal : for a delayed departure. The message will leave in the 10 following seconds. For really non urgent messages like updating secondary data that are not visible ; or if you have a js cache, you may want to update it.
minor : for a really delayed departure. The message will leave in the 40 following seconds. For background messages like ping or saving preferences.
synchronous : see below.
long request : see below.
When the messages are queued, the bigger priority will win.
For example, if only three "normal" messages are queued, they will wait for 10 seconds.
But if you send a major message in between, all the messages will leave at once.
The constants are available as :
Oops !
Copy to clipboard failed. Open the code and copy it manually.
The JSON way is much more powerfull sinceit allows to receive typed objects, Map and List. The null js value will, for example, be converted as null in the java code.
To make it easy for you, the root json parameters are converted automatically to request parameters (if there are no GET parameters).
So you can send the following json object:
Oops !
Copy to clipboard failed. Open the code and copy it manually.
{ test: 'foo', subtest: { type: 1, arg: 'foo2' }}
{ test: 'foo', subtest: { type: 1, arg: 'foo2' }}
{ test: 'foo', subtest: { type: 1, arg: 'foo2' }}
and catch the "simple" values directly using
Oops !
Copy to clipboard failed. Open the code and copy it manually.
request.getParameter("test")
request.getParameter("test")
request.getParameter("test")
but you cannot get complex types (like subtest in this example) this way.
This makes it also available in the sitemap using the request parameter inputdata (always for simple types only) :
Oops !
Copy to clipboard failed. Open the code and copy it manually.
{request-param:test}
{request-param:test}
{request-param:test}
You can serialize your pipeline in whatever format you want : xml, json ...
Response handling on the client side
Callback method
When the response is back, your callback method is called to handle it.
The first thing to do is to handle errors.
The generic ServerComm.handleBadResponse , allows you to test the HTTP return code and display an error message
Oops !
Copy to clipboard failed. Open the code and copy it manually.
Copy to clipboard failed. Open the code and copy it manually.
var searchNode = response.selectSingleNode("search"); // Do not use absolute xpath
var maxResults = searchNode.getAttribute("results");
alert("There are " + maxResults + " results");
var results = searchNode.selectNode("result[@id]")
for (var i = 0; i < results.length; i++)
{
var result = results[i];
alert("Result n°" + i + "/" + maxResults + ": " + results.selectSingleNode("name")[org.ametys.servercomm.ServerComm.xmlTextContent]);
}
var searchNode = response.selectSingleNode("search"); // Do not use absolute xpath
var maxResults = searchNode.getAttribute("results");
alert("There are " + maxResults + " results");
var results = searchNode.selectNode("result[@id]")
for (var i = 0; i < results.length; i++)
{
var result = results[i];
alert("Result n°" + i + "/" + maxResults + ": " + results.selectSingleNode("name")[org.ametys.servercomm.ServerComm.xmlTextContent]);
}
var searchNode = response.selectSingleNode("search"); // Do not use absolute xpath
var maxResults = searchNode.getAttribute("results");
alert("There are " + maxResults + " results");
var results = searchNode.selectNode("result[@id]")
for (var i = 0; i < results.length; i++)
{
var result = results[i];
alert("Result n°" + i + "/" + maxResults + ": " + results.selectSingleNode("name")[org.ametys.servercomm.ServerComm.xmlTextContent]);
}
Text
This reponse type send the response of the server as a text node.
If you are using json serializer, use this.
Oops !
Copy to clipboard failed. Open the code and copy it manually.
The main mecanism of the ServerComm is asynchronous and you should use it 99% of the time.
But sometimes you need synchronous requests (that stops the js while waiting for the answer).
Use the SYNCHRONOUS priority and get the response directly from the send method.
Here is the code of the PageMessageTargetFactory
Oops !
Copy to clipboard failed. Open the code and copy it manually.
org.ametys.messagebus.bus.impl.PageMessageTargetFactory.prototype.createTarget = function (type, parameters)
{
var target = new org.ametys.messagebus.message.MessageTarget(type, parameters, []);
var serverMessage = new org.ametys.servercomm.ServerMessage("web", "/repository/page-info", {id: parameters['id']}, org.ametys.servercomm.ServerComm.PRIORITY_SYNCHRONOUS, null, this, null);
var response = org.ametys.servercomm.ServerComm.getInstance().send(serverMessage);
if (org.ametys.servercomm.ServerComm.handleBadResponse("An error occured", response, "org.ametys.messagebus.bus.impl.PageMessageTargetFactory"))
{
throw "org.ametys.messagebus.bus.impl.PageMessageTargetFactory request failed";
}
var contents = response.selectNodes("page/contents/content");
for (var i=0; i < contents.length; i++)
{
var contentName = contents[i].selectSingleNode("name")[org.ametys.servercomm.ServerComm.xmlTextContent];
var subTarget = org.ametys.messagebus.bus.MessageBuilder.getInstance().createTarget("content", {'id': contentName});
target.getSubTargets().push(subTarget);
}
return target;
}
org.ametys.messagebus.bus.impl.PageMessageTargetFactory.prototype.createTarget = function (type, parameters)
{
var target = new org.ametys.messagebus.message.MessageTarget(type, parameters, []);
var serverMessage = new org.ametys.servercomm.ServerMessage("web", "/repository/page-info", {id: parameters['id']}, org.ametys.servercomm.ServerComm.PRIORITY_SYNCHRONOUS, null, this, null);
var response = org.ametys.servercomm.ServerComm.getInstance().send(serverMessage);
if (org.ametys.servercomm.ServerComm.handleBadResponse("An error occured", response, "org.ametys.messagebus.bus.impl.PageMessageTargetFactory"))
{
throw "org.ametys.messagebus.bus.impl.PageMessageTargetFactory request failed";
}
var contents = response.selectNodes("page/contents/content");
for (var i=0; i < contents.length; i++)
{
var contentName = contents[i].selectSingleNode("name")[org.ametys.servercomm.ServerComm.xmlTextContent];
var subTarget = org.ametys.messagebus.bus.MessageBuilder.getInstance().createTarget("content", {'id': contentName});
target.getSubTargets().push(subTarget);
}
return target;
}
org.ametys.messagebus.bus.impl.PageMessageTargetFactory.prototype.createTarget = function (type, parameters)
{
var target = new org.ametys.messagebus.message.MessageTarget(type, parameters, []);
var serverMessage = new org.ametys.servercomm.ServerMessage("web", "/repository/page-info", {id: parameters['id']}, org.ametys.servercomm.ServerComm.PRIORITY_SYNCHRONOUS, null, this, null);
var response = org.ametys.servercomm.ServerComm.getInstance().send(serverMessage);
if (org.ametys.servercomm.ServerComm.handleBadResponse("An error occured", response, "org.ametys.messagebus.bus.impl.PageMessageTargetFactory"))
{
throw "org.ametys.messagebus.bus.impl.PageMessageTargetFactory request failed";
}
var contents = response.selectNodes("page/contents/content");
for (var i=0; i < contents.length; i++)
{
var contentName = contents[i].selectSingleNode("name")[org.ametys.servercomm.ServerComm.xmlTextContent];
var subTarget = org.ametys.messagebus.bus.MessageBuilder.getInstance().createTarget("content", {'id': contentName});
target.getSubTargets().push(subTarget);
}
return target;
}
Long requests
Keep in mind that all request will come back only when they are all handled.
So if you have a request that you know it will be long, you have to set the LongRequest priority flag in order to :
Send the request 'alone' (it will not be grouped) to avoid that others short requests wait for you
Set a longer timeout (10x longer compared to normal)
Warning
If your have to send several messages at once, you can "pause" the communication with the server. Do not forget to "unpause" it.