What do you need:
You can find the reference implementation of the widget and the operator build in this tutorial in the following github repo:
https://github.com/wirecloud-fiware/quick-start-development-tutorial
We recommend you to see it after completing the tutorial or in case you get stalled.
The first step is to build a blank widget. It must have defined the metadata and a basic HTML. The metadata is written in the config.xml
file, that could be similar to this:
<?xml version='1.0' encoding='UTF-8'?>
<widget xmlns="http://wirecloud.conwet.fi.upm.es/ns/macdescription/1" vendor="CoNWeT" name="basic-chat" version="0.1">
<details>
<title>Basic chat</title>
<homepage>https://github.com/wirecloud-fiware/quick-start-development-tutorial</homepage>
<authors>Miguel Jimenez <mjimenez@fi.upm.es></authors>
<contributors>Álvaro Arranz <aarranz@fi.upm.es></contributors>
<email>wirecloud@conwet.com</email>
<image>images/chat_logo.png</image> <!-- 170x80 -->
<description>Basic chat functionality and WireCloud features demonstrator</description>
<longdescription>README.md</longdescription>
<license>Apache License 2.0</license>
<licenseurl>http://www.apache.org/licenses/LICENSE-2.0.html</licenseurl>
<doc>doc/developer-guide.md</doc>
</details>
<contents src="index.html" useplatformstyle="true"/>
<rendering width="5" height="24"/>
</widget>
That template indicates widget metadata such as author/vendor, together with longer descriptions that can be written using Markdown.
Note the vendor
, name
and version
indicated as attributes of the root element. Please consider that, depending on the WireCloud configuration, it might not admit uploading a widget twice (same version/name/vendor), so you should increase version, subversion or revision number to upload new versions of the widget. Moreover, you must change the vendor, since WireCloud will not upload the widget if another user has uploaded already the same widget (version, name and vendor).
Two remarkable elements are the rendering
element, with basic information about the size of the widget, and the reference to the main HTML file. Such file will be the entry point to the widget, and contains references to JavaScript or CSS files.
A basic HTML, named index.html
as indicated in config.xml
is indicated below. This document contains a basic header for user photo and nickname, a panel for messages, and a basic form for sending messages.
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="application/xhtml+xml; charset=UTF-8"></meta>
<script type="text/javascript" src="js/main.js"></script>
<link rel="stylesheet" type="text/css" href="css/style.css" />
</head>
<body>
<div id="header">
<img id="photo"id="photo" alt="Profile photo" max-height="60" max-width="60" />
<h2 id="username">Username</h2>
</div>
<div id="conversations"></div>
<div id="footer">
<input id="input" type="text" /><button id="send" type="button">Send</button>
</div>
</body>
</html>
This HTML code refers to the js/main.js
JavaScript file, that should contain the necessary code. JavaScript code will be throroughly described below. In addition, a CSS file is referenced, containing basic styling for our chat application.
body { padding: 0;
margin: 0;
font-family: sans-serif;
color: #333;}
#photo {padding: 3px;
border: 2px solid #eaeaea;
max-height: 48px;
max-width: 48px;
float:left;}
.sent, .received {
min-height: 36px;
padding: 2px;
border: 1px solid #eaeaea;
border-radius: 5px;}
.sent { margin: 1px 35px 1px 1px;}
.received{ margin: 1px 1px 1px 35px;
text-align: right;}
.received > p{ display: inline;}
.sent > img{ max-height: 28px;
max-width: 28px;
margin: 2px;
float:left;}
.sent > p{ display:inline;}
.received > img{ max-height: 28px;
max-width: 28px;
margin: 2px;
float:right;}
#username { display: inline;}
#conversations { height: 65%;
overflow: scroll;}
#footer{position: fixed;
bottom: 0;}
#header{height:60px;}
body,html { height: 100%;}
Finally, the referenced js/main.js
is created. Here we include some basic elements such as a closure, an init()
function and two functions to deal with the HTML interface.
(function() {
"use strict";
// object with the data received from gravatar
var userData = null;
function init() {
document.getElementById("send").onclick = sendBtnHandler;
getInfoFromGravatar();
}
function sendBtnHandler(e) {
}
function createMsgDiv(text,imageSrc,received,id){
var newMsgP = document.createElement('p');
var newMsgImg = document.createElement('img');
var newMsgDiv = document.createElement('div');
newMsgP.innerHTML = text;
newMsgP.id = id;
newMsgImg.src = imageSrc;
newMsgImg.alt = 'User profile img';
newMsgDiv.className=(received)?'received':'sent';
newMsgDiv.appendChild(newMsgImg);
newMsgDiv.appendChild(newMsgP);
var conversations = document.getElementById('conversations');
conversations.appendChild(newMsgDiv);
conversations.scrollTop = newMsgDiv.offsetTop;
}
function getInfoFromGravatar() {
// Put the info in userData variable
}
function printUserData(user_data) {
document.getElementById('username').innerHTML = user_data.entry[0].displayName;
document.getElementById('photo').src = user_data.entry[0].thumbnailUrl;
}
document.addEventListener('DOMContentLoaded', init.bind(this), true);
})();
The first feature that we are adding is preferences. Widget preferences must be declared in the config.xml
, and after they're accessed through the WireCloud JavaScript API. The declaration of a preference for getting user gravatar profile would be like this:
<preferences>
<preference name="gravatar" type="text" label="Gravatar URL" description="URL to the gravatar profile of the user" />
</preferences>
This <preferences>
section must be inside the root element <widget>
. Now we can access its value using the name
we've chosen. The following line will be inside the getInfoFromGravatar()
function, so it can make the HTTP request to the specific URL.
var gravatarURL = MashupPlatform.prefs.get('gravatar');
If we did this only, once the user has indicated his profile we would have to reload the widget so as to get the profile from Gravatar. To do things smartly, we're being notified from the platform on any preferences change, and react accordingly. In this case, we will invoke getInfoFromGravatar()
again. Following code goes inside init()
function:
MashupPlatform.prefs.registerCallback(function(new_values) {
if ('gravatar' in new_values) {
getInfoFromGravatar();
}
});
Now it's time to make the HTTP request to gather other information from the user. Gravatar offers a JSON version of the profile on the same URL, appending a '.json' extension, so that will be the base URL for our request.
The basic HTTP request might be like this:
MashupPlatform.http.makeRequest(url, {
method: 'GET',
onSuccess: function(response) {
var user_data;
user_data = JSON.parse(response.responseText);
if (user_data.error) {
// handle error
onError();
} else {
// perform actions
}
},
onError: function() {
onError();
}
});
This structure is invoked with the URL resulted from the concatenation of the obtained user preference gravatarURL
and the string '.json'
. And the actions to be performed are storing the data on the userData
variable, and making it appear on the widget using the previously created printUserData()
function:
var url = gravatarURL + '.json';
...
onSuccess: function(response) {
var user_data;
user_data = JSON.parse(response.responseText);
if (user_data.error) {
onError();
} else {
userData = user_data;
printUserData(user_data);
}
}
The userData
obtained from gravatar has a simple structure that is used for accessin its information, for example on the printUserData()
:
{
"entry": [{
"id": "9508921",
"hash": "61ac3cca6452efd339cb85c7864c147b",
"requestHash": "mjimenezganan",
"profileUrl": "http:\/\/gravatar.com\/mjimenezganan",
"preferredUsername": "mjimenezganan",
"thumbnailUrl": "http:\/\/2.gravatar.com\/avatar\/61ac3cca6452efd339cb85c7864c147b",
"photos": [{
"value": "http:\/\/2.gravatar.com\/avatar\/61ac3cca6452efd339cb85c7864c147b",
"type": "thumbnail"
}],
"name": {
"givenName": "Miguel",
"familyName": "Jim\u00e9nez"
},
"displayName": "mjimenezganan",
"aboutMe": "More info on www.twitter.com\/miguel_jimg",
"currentLocation": "Spain",
"urls": []
}]
}
Chat functionality on the widget is done through the wiring mechanism of WireCloud. First of all, input and output endpoints have to be declared on the config.xml
file:
<wiring>
<outputendpoint name="sendMsg" type="text" label="Send a message" description="The messages sent by the user are sent through this output endpoint" friendcode="message"/>
<inputendpoint name="receiveMsg" type="text" label="Receive a message" description="This is where messages sent by other widgets can be received" friendcode="message" />
</wiring>
Sending messages implies invoking the MashupPlatform.wiring.pushEvent()
function referencing the output endpoint name as declared.
MashupPlatform.wiring.pushEvent('sendMsg', text);
It shall be added to the sendBtnHandler()
. To be able to send messages with metadata (i.e. the Gravatar hash that allows getting sender's image), a JSON serialized object is sent:
function sendBtnHandler(e) {
var msgToSend = {};
msgToSend.msg = document.getElementById("input").value;
if (msgToSend.msg !== "" && userData != null) {
msgToSend.hash= userData.entry[0].hash;
MashupPlatform.wiring.pushEvent('sendMsg', JSON.stringify(msgToSend));
}
}
Now the message is sent through wiring, but the widget needs to receive other widgets' messages and print them on the conversations panel. This is performed registering a callback function on the platform registering for messages received on a specific input endpoint. identified by its name. On the init()
function we would write somethint like this:
MashupPlatform.wiring.registerCallback('receiveMsg', processMsg);
And a processMsg()
function shall be created to indicate the desired behaviour. In our case, creating a message in the conversations panel. Since messages are sent as serialized JSON objects, they are de-serialized for accessing its elements. It is a bit tricky, since it detects my own messages and prints them as sent (different CSS style). This is how the echo is going to work.
function processMsg(event_data) {
var receivedMsg = JSON.parse(event_data);
if (userData != null && receivedMsg.hash !== userData.entry[0].hash) {
createMsgDiv(receivedMsg.msg, 'http://www.gravatar.com/avatar/' + receivedMsg.hash, true, receivedMsg.id);
} else { // My message, echo, mark as sent
createMsgDiv(receivedMsg.msg,'http://www.gravatar.com/avatar/' + receivedMsg.hash, false, receivedMsg.id);
document.getElementById(receivedMsg.id).parentElement.className = 'sent';
}
}
The operator is defined on a config.xml
declarative file, which references one or many JavaScript files containing its behaviour.
Like widgets, the config.xml
has vendor, name and version information that should be updated so as to avoid conflicts or WireCloud rejecting an operator.
<?xml version='1.0' encoding='UTF-8'?>
<operator xmlns="http://wirecloud.conwet.fi.upm.es/ns/macdescription/1"
vendor="CoNWeT" name="ngsi-chat-op" version="0.1.1">
<details>
<title>NGSI chat operator</title>
<homepage>https://github.com/wirecloud-fiware/quick-start-development-tutorial</homepage>
<authors>Miguel Jiménez</authors>
<email>mjimenez@fi.upm.es</email>
<image>images/operator_logo.png</image> <!-- 170x80 -->
<description>Connect to Orion Context Broker through NGSI WireCloud API for chat room</description>
<longdescription>README.md</longdescription>
<license>Apache License 2.0</license>
<licenseurl>http://www.apache.org/licenses/LICENSE-2.0.html</licenseurl>
<doc>doc/developer-guide.md</doc>
</details>
<scripts>
<script src="js/main.js"/>
</scripts>
</operator>
The widget has some preferences for easy indicating the URLs of the NGSI server and the proxy for accessing it from widgets/operators. Moreover, a chatroom
preference has been added.
<preferences>
<preference name="ngsi_server"
type="text" label="NGSI server URL"
description="URL of the Orion Context Broker to use for retrieving entity information"
default="http://orion.lab.fiware.org:10026/"/>
<preference name="ngsi_proxy"
type="text"
label="NGSI proxy URL"
description="URL of the PubSub Context Broker proxy to use for receiving notifications about changes"
default="https://ngsiproxy.lab.fiware.org/"/>
<preference name="chatroom"
type="text"
label="Chat room"
description="Chat room to send and receive messages"
default="Startup Weekends"/>
</preferences>
Wiring is also used in the widget. It sends received messages back to the widget, and forwards user messages to the NGSI API:
<wiring>
<outputendpoint name="toBeReceived"
type="text"
label="Messages from NGSI"
description="Forward a message to a chat widget"
friendcode="message" />
<inputendpoint name="toBeSent"
type="text"
label="Message to NGSI"
description="Receive messages to be sent to the chat room"
friendcode="message" />
</wiring>
Basic JavaScript file is written below, containing initialization function, preferences and wiring subscriptions and some skeletons:
(function() {
"use strict";
var chatroom = null;
var ngsi_connection = null;
function init() {
MashupPlatform.wiring.registerCallback('toBeSent', publishMsg);
MashupPlatform.prefs.registerCallback(function(new_values) {
if ('chatroom' in new_values) {
subscribeChatRoom();
}
});
subscribeChatRoom();
}
function publishMsg(event_data) {
}
function subscribeChatRoom() {
}
function receiveMessage(data){
for(var msg in data.elements){
MashupPlatform.wiring.pushEvent('toBeReceived', JSON.stringify(data.elements[msg]));
}
}
init();
})();
First, NGSI must be declared as a required feature in the config.xml
file:
<requirements>
<feature name="NGSI"/>
</requirements>
An NGSI connection must be stablished so as to make requests over it. It is authenticating using FIWARE tokens from the user logged on WireCloud. I've declared a global (closure) variable to hold it and access from other functions.
ngsi_connection = new NGSI.Connection(MashupPlatform.prefs.get('ngsi_server'), {
use_user_fiware_token: true,
ngsi_proxy_url: MashupPlatform.prefs.get('ngsi_proxy')
});
To create a new entity or modify an existing one, function addAttributes()
of
the connection is used. For the chat application, a new instance of class
ChatMessage
is created, using its hash
and msg
attributes (exactly as the
event sent by wiring). Further information can be found on the NGSI API of
WireCloud documentation or on the Orion Context Broker with WireCloud
tutorial.
The entity to create is like this one, that
[{
entity: {id: MashupPlatform.prefs.get('chatroom') + now.getTime() + msg.hash,
type: 'ChatMessage'},
attributes: [
{
type: 'string',
name: 'hash',
contextValue: msg.hash
},
{
type: 'string',
name: 'msg',
contextValue: msg.msg
}]
}]
Full publishMsg()
definition ends like this:
function publishMsg(event_data) {
var now = new Date();
var msg = JSON.parse(event_data);
ngsi_connection.addAttributes([{
entity: {id: MashupPlatform.prefs.get('chatroom') + now.getTime() + msg.hash,
type: 'ChatMessage'},
attributes: [
{
type: 'string',
name: 'hash',
contextValue: msg.hash
},
{
type: 'string',
name: 'msg',
contextValue: msg.msg
}]
}]);
}
Note that the ID of the entity has been created with a fixed part (a preference indicating the chat room), plus a timestamp and the hash of the user writing.
This operator wants to receive the modification (creation is a kind of
modification) of any instance of type ChatMessage
whose ID starts with the
chat room. If we had previously created the subscription (stored on chatroom
variable), we start cancelling it.
The subcription itself is created invoking the
createSubscription()
method. It requires several
parameters (like the id of the entities to monitorize, the list of attribute
your interested on, the duration of the subscription, etc), in the next slide
you will see an example of use for our scenario.
function subscribeChatRoom() {
if (chatroom != null) {
NGSI.cancelSubscription(chatroom);
}
var entityIdList = [
{type: 'ChatMessage', id: MashupPlatform.prefs.get('chatroom') + '.*', isPattern: true}
];
var attributeList = null;
var duration = 'PT30M';
var throttling = null;
var notifyConditions = [{
type: 'ONCHANGE',
condValues: ['hash', 'msg']
}];
var options = {
flat: true,
onNotify: receiveMessage,
onSuccess: function (data) {
chatroom = data.subscriptionId;
}
};
ngsi_connection.createSubscription(entityIdList, attributeList, duration, throttling, notifyConditions, options);
}
The subscription duration is set to 30 minutes, and should be renewed (or the page reloaded) before that time. How to renew the subscription will be addressed in future versions of this tutorial (in the meantime, you can take a look to the Orion Context Broker with WierCloud tutorial for a generic tutorial of how to do it).
Table of Contents | t |
---|---|
Exposé | ESC |
Full screen slides | e |
Presenter View | p |
Source Files | s |
Slide Numbers | n |
Toggle screen blanking | b |
Show/hide slide context | c |
Notes | 2 |
Help | h |