This blog is part of the larger series on all new developer features in SAP HANA SPS 11: SAP HANA SPS 11: New Developer Features
Requirements change over time and so too has XS within SAP HANA. SAP HANA extended application services in SPS 11 represents an evolution of the application server architecture building upon the previous strengths while expanding the technical scope.
Figure 1: Architecture as of SAP HANA SPS 11
We will use standard Cloud Foundry build packs as the basis of the XS language support. Therefore starting with SAP HANA SPS 11, XS Advanced will be delivered with and fully support both Apache TomEE Java and Google V8 JavaScript/Node.js. In both of these runtimes, you will find a pure delivery of the standard runtimes; not an SAP branched or modified version. This greatly supports the easier porting of existing applications onto SAP HANA.
JavaScript has always been the foundation of SAP HANA XS. In XS Classic we use the Mozilla SpiderMonkey virtual machine - the same JavaScript engine which runs the FireFox browser. In today's XS environment we have a single operating system process called XSEngine. Within this process we create a pool of JavaScript Virtual Machines. The are clones of a single runtime version. Therefore there are no options to control which version of JavaScript your application runs against. There are some limited configuration parameters that apply to the entire pool of JavaScript VMs, but no way to configure memory or other scaling parameters per application. Finally as all the VMs live within a single operating system process, if anything goes wrong there is the potential to crash the entire XSEngine process. We also extend the current JavaScript programming model with our $ APIs via JavaScript function wrappers around C/C++ libraries and other JavaScript functions. This creates a situation where there is a deep coupling between the JavaScript VM and the XSEngine codebase.
In the new Cloud Foundry based architecture of XS Advanced, things work very differently. First when you deploy an application or service a copy of the entire Java or Node.js runtime goes with. This way each service is isolated and locked into their version. Even if you upgrade the entire HANA/XS system, already deployed services continue to run with their existing runtime providing better stability over time. It isn't until you would deploy the service again that you could pick up a newer version of the specific runtime. This allows single services to target different runtime versions in parallel.
At the operating system level, we also no longer have one monolithic process hosting all the runtimes. Each runtime instance has its own operating system process. This allows for much better process isolation ensuring that a single poorly written or behaving service can't disrupt other services/applications.
We also will now extend the JavaScript runtime via the standard concept of Node.js modules. In fact even the $ APIs of XSJS are re implemented within Node.js (but more on that later in this blog).
The biggest change for developers however is the change from Mozilla SpiderMonkey to Google V8 as the underlying JavaScript VM. Now you might think that two JavaScript VMs can't be all that different. From a purely JavaScript language aspect, that is certainly true. However with Google V8 we also gain support for Node.js. Node.js has quickly become the defacto standard for server side JavaScript thanks to its Asynchronous / Non-Blocking I/O programming model and the easy of which you can access and manage reusable, open source modules. So for the remainder of this blog we will look at the basics of Node.js development and how SAP will be supplying XS and HANA functionality into the Node.js environment.
But Node.js is hardly an SAP specific technology. It already has a strong community and many excellent resources. So if you want to expand your knowledge of Node.js in general, here are some resources we would recommend:
Even if you don't have access to SAP HANA SPS 11 yet, its easy to get started learning Node.js. You can simply download the Node.js runtime from Download | Node.js and begin writing and executing Node.js applications. Because we use a nearly standard Node.js within XS Advanced (only adding some supportability features), whatever you learn in this local version will prepare you well for developing in XS Advanced in the future. In fact at SAP TechEd 2015 we wanted hands-on exercises to teach people the Node.js programming model and introduce XS Advanced concepts but the actual SPS 11 software wasn't ready yet to be used. Therefore we designed and delivered these hands-on sessions based solely on a local installation of Node.js alone.
The main thing to keep in mind that when you use this local Node.js installation you will be running the Node JavaScript files manually via the command line interface. However once you have SAP HANA XS Advanced you will have to deploy these files to the server and run them as services.
To being with a simple Hello World Node.js application you can simply create a .js file on your local file system and add this code:
console.log("Hello World");
From the command line we can invoke the node.js runtime by issuing the command node. The parameter after the node command tells what JavaScript file to execute.
Therefore issue: node hello.js to run your first exercise.
Figure 2: Execute node hello.js from the command line
Now you can adapt your helloWorld to respond as a Web Server instead of to the console. Node.js has a built-in web server; therefore you can directly interact with the HTTP Request or Response objects. In this exercise create a small service that returns the Hello World text in a web browser. Node.js uses the syntax require to load a dependent library file. Require the built-in http library. Use the createServer API to start an HTTP server and then listen on port 3000 of your localhost (IP 127.0.0.1). The createServer API passes in the request and response objects as parameters. Use res.end to write the body of the response object.
var http = require('http');
http.createServer(function (req, res) {
res.end('Hello World\n');
}).listen(3000, '127.0.0.1');
console.log('Server running at http://127.0.0.1:3000/');
From the node.js command prompt execute the helloWeb.js you just created. Unlike the earlier example, it doesn’t immediately return control to the DOS prompt. Node continues to run because it’s listening for HTTP requests. Open a web browser and navigate to your node.js application which is running on http://localhost:3000. You should see the text of the message you passed back in the response object.
Here is a video detailing the steps of both the console and web based hello world in Node.js:
In the previous example, we created a hello world that runs from the web, but you had to manually open the web browser. Wouldn’t it be nice if the application opened the browser for you? Luckily there are many open source libraries available for Node.js and they are very easy to use and install. We will use one such library to extend the previous exercise to open a web browser for you.
Add the following line of code at the beginning of this file. This require statement gives us access to the opn library. Unlike the http library we used earlier, this isn’t built into node.js and has to be installed in a later step.
var opn = require('opn');
Add this line of code to the end of the file. This will trigger the opening of the default web browser to the web page where our application is listening.
opn('http://127.0.0.1:3000/');
From the node.js command prompt we can install the opn library using the npm tool. First make sure you are in the Exercise2 directory. Then issue the command npm install opn to install the opn library.
Figure 3: NPM Install
NPM is a powerful tool and a strong part of the Node.js's popularity. It makes the installation of external modules very easy. More importantly it manages the dependencies between modules well. When you install a module, all the depend modules are also automatically installed. We've built NPM into the deployment process of XS Advanced. So as you are deploying a new service/application to XS Advanced, NPM is automatically called to also download any dependent modules.
One of the most common tasks in node.js is acting as a web server and handling http requests/responses. express is an open source module that wraps the low level http library and provides many additional services. Therefore it is widely used in Node.js projects in general and more specifically within new XS Advanced applications. We will now adapt our helloWeb to use express instead of the built in http library.
Replace the require statement for the http library with one for the express library.
var express = require('express');
We also want to get a little more sophisticated with our HTTP port assignment. Instead of hard coding an HTTP port, use the built-in object called process to query the PORT which node.js is running under. If none is found fall back to port 3000. This is useful in the XS environment because the controller framework in XS will assign and pass in a distinct port to each application.
var PORT = process.env.PORT || 3000;
Replace the rest of the code in your application with this code. It will create an instance of express and setup a route. Routes are code handlers for certain HTTP URL paths. In our case we will now only respond to GET requests to the /hello path.
var app = express();
//Hello Router
app.route('/hello')
.get(function(req, res) {
res.send('Hello World');
})
// Start the server
var server = app.listen(PORT, function() {
console.log('Listening on http://localhost:'+ PORT +'/hello');
opn('http://localhost:'+ PORT + '/hello');
});
As we get more and more libraries in our node.js applications, we don’t want to have to rely upon someone always manually using the npm install command to include them all. Instead we can create a package.json file that lists all the dependencies and their particular versions.
Figure 4: package.json example
You can now issue the command npm install. This will read the package.json and install all libraries listed in the dependencies section. Express is much larger library; so many more dependencies were installed as well.
One of the additional features of express is that it makes it easy to serve out static HTML content alongside your dynamic services. We can also add a static resource route for the root of our application. Add a line of code before the hello route to redirect requests from the root of the URL to static content in the html sub-directory.
//Home Router
app.use('/', express.static(__dirname + '/html'));
Modules don’t all have to come from central repositories. They can also be a way of modularizing your own code (similar to XSJSLIB files). Let’s now add another route but have the handler for it be contained in a module. Toward the beginning of your file add a require statement that points to a library named myModule which we will create locally in just a few steps.
var myModule = require('./myModule');
Add another route handler for the URL path /module that calls the helloModule function in the myModule library.
//Module Router
app.route('/module')
.get(function(req, res) {
res.send(myModule.helloModule());
})
The myModule.js uses the syntax module.exports. Any function declarations within this block are exposed to the outside. This is all you really need to create such reusable modules.
Here is a video detailing the previous steps:
One of the major differences from Node.js and client side JavaScript or even XSJS is its asynchronous nature. This asynchronous capability allows for non-blocking input and output. This technique is one of the basic things that makes node.js development different from other JavaScript development and also creates one of the reasons for its growing popularity. We will see how these techniques are applied to common operations like HTTP web service calls or even SAP HANA database access.
We want first to look a very simple example that shows the asynchronous nature of node.js. Begin by creating a file named async.js. In this code we will output a start message to the console, then set a timer which will issue a message after 3 seconds. Finally we will issue an ending message to the console.
console.log('Start');
setTimeout(function(){
console.log('Wait Timer Over');
}, 3000);
console.log('End');
What do you expect this code will output? From many other programming languages we would expect sequential processing and therefore the End output wouldn’t come until after the timer expired. However part of the power of node.js is asynchronous non-blocking execution of many core elements. Run the async.js from the command prompt to see the results. You should see the End output is issued before the Wait Timer Over.
Perhaps a timer seemed like an obvious asynchronous operation. However this asynchronous nature of node.js is often used when programs must wait on input or output. When asynchronous processing is applied to these operations, we can keep from blocking execution of other logic while we wait on things like file access, http requests or even database query execution. Let’s first look at the difference between synchronous and asynchronous file operations. Create two text files named file.txt and file2.txt. Place a little text in each file. The actual content isn’t that important. Now create a file named fileSync.js. Using the fs library and the function readFileSync, read each of the text files. Output the content of each file to the console. After each read operation output a console message.
var fs = require('fs');
var text = fs.readFileSync('file.txt','utf8');
console.log(text);
console.log("After First Read\n");
text = fs.readFileSync('file2.txt','utf8');
console.log(text);
console.log("After Second Read\n");
Test your fileSync.js from the node.js command prompt. As you might expect, everything is output in exactly the same order as the lines of code were listed in the application because all operations were synchronous. Program execution didn’t continue until each read operation had completely finished.
Create an additional file named fileAsync.js or copy from the fileSync.js in the previous step. Adjust the logic to use the fs.readFile function. Notice that the console.log(text) now is embedded as an in-line callback function. It doesn’t get executed until the read operation is complete, but the rest of the program flow continues and isn’t blocked by the file operation.
var fs = require('fs');
fs.readFile('file.txt','utf8', function(error, text){
console.log(text);
});
console.log("After First Read\n");
fs.readFile('file2.txt','utf8', function(error, text){
console.log(text);
});
console.log("After Second Read\n");
Now run fileAsync.js from the node.js command prompt. The output of this exercise gives us very different results. Both after comments are output before either of the file contents. Also if the first file had been significantly larger than the second, it’s possible that the second might have finished and output first. This has powerful implications to how we code applications.
Similar to file operations, HTTP requests are another area where our programs must often wait on an external response. In this example let’s see how node.js also makes calling external HTTP services non-blocking. Create a file named httpClient.js. The http library we used in earlier exercises can also be used to make HTTP requests. Use the get function of the http library to call to http://www.loc.gov/pictures/search/?fo=json&q=SAP. This will call the US Library of Congress Image Search (a REST API which requires no authentication or API Key to keep the exercise simple). Issue a console message before and after the HTTP request.
var http = require('http')
console.log("Before HTTP Call\n");
http.get(
{path: "http://www.loc.gov/pictures/search/?fo=json&q=SAP",
host: "proxy.fair.sap.corp",
port: "8080",
headers: {
host: "www.loc.gov"
}},
function (response) {
response.setEncoding('utf8');
response.on('data', function(data){console.log(data.substring(0,100))});
response.on('error', console.error);
});
console.log("After HTTP Call\n");
Test your httpClient.js from the node.js command prompt. Similar to the earlier file exercise, the after http call console message is output before the response from the HTTP request.
Perhaps most interesting to us is that this non-blocking concept can also be extended to database access. This allows us to issue multiple requests to the underlying HANA database in parallel and without stopping the processing flow of the JavaScript application logic. In the next section of this blog we will learn more about making database access to HANA. For this demo we’ve already coded the database requests in a reusable module, so you can concentrate on the asynchronous flow. Create a new file named databaseAsync.js. Issue a message to the console, then call two functions (callHANA1 and callHANA2), then issue another message to the console. This will execute two different queries in the HANA database.
var hana = require('./database');
console.log('Before Database Call');
hana.callHANA1(console.log);
hana.callHANA2(console.log);
console.log("After Database Call");
Test your databaseAsync.js from the node.js command prompt. As you are hopefully learning to expect, the messages you issued after the database requests are actually output first. Only then are the database query results returned. There is also no guarantee that query 1 will finish before query 2.
Figure 5: HANA Database Non-Blocking Access
But what if you want more control over the flow of program execution. Maybe you want several database operations to happen in parallel, but then some logic to execute only after all queries are complete. This is one of things the async module in node.js can make easier. Copy the databaseAsync.js to databaseAsync2.js. Adjust the logic to use the async.parallel function. This allows some of the commands to execute in parallel as before, but then have a sync point once all operations are complete to allow further processing. We will output one final message after everything is done.
var hana = require('./database');
var async = require("async");
async.parallel([
function(cb){console.log('Before Database Call'); cb()},
function(cb){hana.callHANA1(cb, console.log); },
function(cb){hana.callHANA2(cb, console.log); },
function(cb){console.log("After Database Call"); cb();}
], function(err){
setTimeout(function() {
console.log("---Everything's Really Done Now. Go Home!---");
process.exit();
}, 100);
});
Test your databaseAsync2.js from the node.js command prompt. The execution is similar to before, but now we have the final message after all queries are complete. Notice that we don’t have to manually kill the service with Ctrl+C this time either. Because we have a sync point after all parallel execution is complete, we can exit the process safely. Note: We did have to use a timer with a delay of 1/10 of a second otherwise the process would close before the last query console log was done being output making the results look odd.
Here is a video detailing the previous steps:
In this example we will look at how to use the HANA database access library to send queries to the database. The function readTables contains all the most interesting code. We create the HANA client and pass in connection details. These details are read from the default-services.json file. In the final version of XS with node.js integrated; these settings can also be configured centrally in a service broker instead.
The actual database code is very similar to JDBC or XSJS coding. There is a connection, prepared statement and query execution. The result set which is return is a JSON object (just like XSJS $.hdb interface). The major difference is the structure of callbacks for the steps described above due to the asynchronous nature of node.js.
function readTables(cb) {
console.log("starting partner read query ...");
var client = hdb.createClient({
host: options.hana.host,
port: options.hana.port,
user: options.hana.user,
password: options.hana.password
});
client.connect(function(err) {
client.prepare('select * from "SAP_HANA_EPM_NEXT"."sap.hana.democontent.epmNext.data::MD.BusinessPartner" where PARTNERROLE = ?',
function (err, statement){
statement.exec(['01'],
function (err, res) {
if (err)
return cb("ERROR: " + err);
result = JSON.stringify({ PARTNERS: res });
cb(result);
}
)}
)
});
}
One of the most exciting features of node.js on XS is the XSJS backward compatibility. We have the ability to run most XSJS applications without any changes directly from within node.js. We will create a new file called xsjs.js. This will be the node.js starting point that initializes the xsjs library and gives us an access point to then call through to the specific xsjs artifacts. Add the following logic. It brings in the necessary base modules and SAP specific modules that we need. Then we start the XSJS service handler directing it to look in the src folder for our XSJS content. We also set index.xsjs as our default target if nothing is specified by the user. The rest of the code is similar to earlier exercises where we open the web browser for testing.
'use strict';
var os = require('os');
var path = require('path');
var xsjs = require('xsjs');
var xsenv = require('xsenv');
var opn = require('opn');
var port = process.env.PORT || 3000;
var options = xsjs.extend({
rootDir: path.join(__dirname, 'src'),
redirectUrl: '/index.xsjs',
port: port
}, xsenv.getServices());
xsjs(options).listen(port);
console.log('Using HANA on %s:%d', options.hana.host, options.hana.port);
console.log('Server running at http://' + os.hostname() + ':' + port);
opn('http://' + os.hostname() + ':' + port);
But the integration between XSJS and Node.js doesn't end there. From XSJS we add a new $ API called $.require. This new API functions the same as the require keyword in Node.js and gives you full access to Node.js modules from XSJS. This allows you to move freely back and forth from XSJS's simplified synchronous programming mode and Node.js's powerful, but a bit more complex asynchronous/non-blocking programming model. It also means that you have ability to access any pre-built Node.js module managed via NPM from XSJS.
Figure 6: $.require
Here is a video detailing the previous steps:
For the final example in this blog we will look at the Web Sockets functionality of Node.js. One of the often requested features of XS has been Web Sockets support. Luckily this is something that node.js comes with and the asynchronous programming is particularly well suited to the very idea of Web Sockets. In this example we will create a small chat application using Web Sockets.
Using what you’ve already learned, its easy to add code to the chatServer.js that will use express to serve the static content from the html directory on the process.env.PORT or 3000. The module for Web Sockets which we are going to use is ws. Require it and create a new instance of the WebSocketServer on port 3080 (that’s what our UI part is expecting). Then the remainder of the implementation of the Web Sockets functionality is just a few lines of code to both receive and send messages.
wss.broadcast = function (data) {
for (var i in this.clients)
this.clients[i].send(data);
console.log('sent: %s', data);
};
wss.on('connection', function (ws) {
ws.on('message', function (message) {
console.log('received: %s', message);
wss.broadcast(message);
});
ws.send(JSON.stringify({
user: 'XS',
text: 'Hello from Node.js XS Server'
}));
});
From the client side, SAPUI5 has a library for Web Sockets communication. In order to setup the Web Socket connection to our Node.js service we need the following code.
jQuery.sap.require("sap.ui.core.ws.WebSocket"); // WS handling
var connection = new sap.ui.core.ws.WebSocket('ws://localhost:3080');
We can then have event handlers on the client side for both sending and receiving message with this web socket server.
// connection opened
connection.attachOpen(function (oControlEvent) {
sap.m.MessageToast.show('connection opened');
});
// server messages
connection.attachMessage(function (oControlEvent) {
var oModel = sap.ui.getCore().getModel('chatModel');
var result = oModel.getData();
var data = jQuery.parseJSON(oControlEvent.getParameter('data'));
msg = data.user + ': ' + data.text,
lastInfo = result.chat;
if (lastInfo.length > 0) lastInfo += "\r\n";
oModel.setData({chat: lastInfo + msg}, true);
// scroll to textarea bottom to show new messages
$('#app--chatInfo-inner').scrollTop($('#app--chatInfo-inner')[0].scrollHeight);
});
// send message
sendMsg: function() {
var oModel = sap.ui.getCore().getModel('chatModel');
var result = oModel.getData();
var msg = result.chat;
if (msg.length > 0) {
connection.send(JSON.stringify(
{user: result.user, text: result.message}
));
oModel.setData({message: ''}, true);
}
}
Here is a video detailing the previous steps
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
29 | |
13 | |
12 | |
10 | |
9 | |
9 | |
9 | |
7 | |
7 | |
6 |