macchina.io

macchina.io JavaScript Programming Guide

Introduction

The macchina.io JavaScript programming environment provides access to various global objects and functions, which give scripts access to the features of macchina.io.

These are described in the following, along with example code.

Furthermore, macchina.io supports a simple module system for JavaScript that allows a script to import a "module", typically containing re-usable code. The module system is based on the modules specification from CommonJS.

This document assumes you are already familiar with JavaScript and its concepts.

JavaScript Bundles

An important question is how to actually get JavaScript code into macchina.io. In general, all JavaScript code executed within macchina.io must be contained in bundles. There are different ways to create a bundle and deploy it into a macchina.io server. The easiest way is to use the Playground app in the web user interface. It provides a browser-based editor that allows you to create and edit JavaScript scripts, as well as to execute them in macchina.io. This is great for trying out things very quickly. The Playground can also be used to create and download a complete bundle containing the script. This bundle can then be deployed to a macchina.io based device, either by placing it on the filesystem (in one of the directories configured as bundle repositories), or by installing them using the web based Bundle utility.

To learn more about bundles, please refer to the Open Service Platform documentation, specifically the OSP Overview and OSP Bundles documents.

In macchina.io, there are two ways that JavaScript code can be executed. The first is by placing one or more scripts in a bundle, and by creating one or more JavaScript extensions in the bundle. The extension basically tells macchina.io which scripts to automatically run when the bundle is started. Every script referenced in a JavaScript extension runs in its own thread. Different scripts can also share code, by placing code in modules and importing them via the require() function. See the following section on modules for more information.

The other way to execute JavaScript is via the web server. Any files served by the web server with the extension ".jss" or ".jssp" will be treated as JavaScript servlets, or server pages, and executed when requested by a client.

The JavaScript Extension Point

The JavaScript extension point in macchina.io tells the macchina.io server which scripts (contained in the bundle) to automatically execute when the bundle starts. Each script is executed in its own thread. Optionally, the extension point can also specify a limit to the JavaScript heap, thus preventing the script from using too much memory. A list of search paths can also be given.

By convention, extensions are specified using an XML file named "extensions.xml", located in the root directory of a bundle.

Here's an example what the "extensions.xml" looks like to run a script in a file named "main.js", also located in the root directory of the bundle.

<extensions>
    <extension point="com.appinf.osp.js" script="main.js"/>
</extensions>

The script will run when the bundle is started, and it will be stopped when the bundle is stopped.

To set a memory limit for the script, use the memoryLimit attribute to specify the allowed amount of memory in bytes. Example:

<extensions>
    <extension point="com.appinf.osp.js" script="main.js" memoryLimit="500000"/>
</extensions>

A comma or semicolon-separated list of search paths/URIs for imported modules can be specified using the searchPaths attribute.

There are actually no special tools required to create a bundle. In the simplest form, a bundle is just a directory with specific contents, located in one of the directories specified as bundle repositories. Since directories are hard to deploy, such a bundle directory is usually stored in a ZIP archive, using the ".bndl" extension. This is called a bundle file.

Creating a JavaScript Bundle From Scratch

Let's now create a bundle from scratch. Every bundle needs to contain some meta information that tells macchina.io (or more specifically, the underlying Open Service Platform) something about the bundle (e.g., its name, its creator, its dependencies, etc.). This information is contained in a text file named "manifest.mf", which must be located in a special directory named "META-INF", located in the root of the bundle directory.

The directory hierarchy of a bundle containing a script so basically looks like this:

/ (bundle root directory)
    META-INF/
        manifest.mf
    extensions.xml
    main.js

The "manifest.mf" file for a simple bundle looks as follows:

Manifest-Version: 1.0
Bundle-Name: My First Bundle
Bundle-SymbolicName: com.my-company.my-first-bundle
Bundle-Version: 1.0.0
Bundle-Vendor: My Company
Bundle-Copyright: (c) 2015, My Company
Bundle-RunLevel: 900
Bundle-LazyStart: false

When creating your own bundle, make sure not to change the values for Manifest-Version, Bundle-RunLevel, or Bundle-LazyStart (unless you are already familiar enough with OSP to know what you're doing).

By convention, the symbolic name of a bundle (the name used internally) is written in reverse domain name notation, to ensure bundle names are unique, even if bundles come from different vendors.

For more detailed information regarding the bundle manifest, please refer to the OSP Bundles manual.

We've already shown the content of the "extensions.xml" file for the bundle. Here it is again for reference:

<extensions>
    <extension point="com.appinf.osp.js" script="main.js"/>
</extensions>

Finally, here is a simple "Hello, world!" script for "main.js":

logger.information("Hello, world!");

The easiest way to get such a bundle file is to use the Playground's "Export Bundle" button. This will download a ".bndl" file. You can use an unzip tool like unzip on Linux or macOS to extract the files contained in the bundle. Use this as a starting point for your own bundles.

Please make sure that when creating a bundle manually using the zip tool that there is no top-level directory in the Zip file. The "META-INF" directory and "extensions.xml" file must be at the top-level of the Zip archive. Furthermore, the name of the bundle file must conform to the convention "<symbolic-name>_<version>.bndl", e.g. "com.my-company.my-first-bundle_1.0.0.bndl".

One way to ensure this is to cd into the bundle directory, then invoke zip as in the following:

$ zip -r ../com.my-company.my-first-bundle_1.0.0.bndl *

Creating a JavaScript Bundle With The BundleCreator Tool

For creating bundles, macchina.io provides the Bundle Creator (bundle) tool from the Open Service Platform. While creating bundles manually using zip is fine for simple bundles, using the bundle tool makes it easier to create more complex bundles that have dependencies on other bundles, and that need to collect files from different source directories.

For the Bundle Creator tool, it's necessary to describe the metadata and contents of the bundle in a so-called bundle specification file (".bndlspec"). Furthermore, it's good practice to not invoke the Bundle Creator directly, but through a Makefile.

The recommended directory layout for a JavaScript bundle is thus:

/ (bundle project root directory)
    Makefile
    MyFirstBundle.bndlspec
    bundle/
    	main.js

The Bundle Specification File

The bundle specification file (MyFirstBundle.bndlspec) looks like this:

<?xml version="1.0"?>
<bundlespec>
  <manifest>
    <name>My First Bundle</name>
    <symbolicName>com.my-company.my-first-bundle</symbolicName>
    <version>1.0.0</version>
    <vendor>My Company</vendor>
    <copyright>(c) 2015, My Company</copyright>
    <lazyStart>false</lazyStart>
    <runLevel>900</runLevel>
    <dependency>
      <symbolicName>com.appinf.osp.js</symbolicName>
      <version>[1.0.0,2.0.0)</version>
    </dependency>
  </manifest>
  <code>
  </code>
  <files>
    bundle/*
  </files>
</bundlespec>

Note that the bundle specification declares a dependency on the com.appinf.osp.js bundle, which contains the JavaScript runtime.

For a detailed description of the bundle specification file format, please see the Bundle Creator documentation.

The Makefile

The Makefile for the bundle makes use of macchina.io build system based on GNU Make. It looks as follows:

.PHONY: all

include $(POCO_BASE)/build/rules/global
include $(POCO_BASE)/OSP/BundleCreator/BundleCreator.make

all:
	$(SET_LD_LIBRARY_PATH) $(BUNDLE_TOOL) MyFirstBundle.bndlspec

It first includes the global build system settings, and then definitions for invoking the Bundle Creator tool. Finally, it defines a target that builds the bundle by running the Bundle Creator.

Before invoking the Makefile via GNU Make, the environment variables POCO_BASE and PROJECT_BASE must be defined. POCO_BASE must contain the absolute path of the platform directory within the macchina.io source tree. PROJECT_BASE must contain the absolute path to the parent directory of your project. If your MyFirstBundle directory is in /home/user/projects/MyFirstBundle, then PROJECT_BASE must be set to /home/user/projects.

Building

Assuming you are in the your MyFirstBundle project directory, run the following commands to build the bundle:

$ export POCO_BASE=/path/to/macchina.io/platform
$ export PROJECT_BASE=/home/user/projects
$ make

After running make, the bundle file com.my-company.my-first-bundle_1.0.0.bndl will be created in the current directory.

JavaScript Servlets and Server Pages

Scripts can also be executed in order to serve web requests. macchina.io uses the Open Service Platform Web Server, which gets all content it serves from bundles. To make the web server execute server-side JavaScript, that script must be in a file with extension ".jss" or ".jssp", and the file must be accessible through the web server. There is an important difference between ".jss" and ".jssp" files. The former contain a so-called "JavaScript Servlet", while the latter contain a "JavaScript Server Page".

Servlets

A JavaScript Servlet is a script that will be invoked whenever a client requests the respective file from the web server. The script has access to global request and response objects which are used to process the HTTP request and send an appropriate HTTP response. There are also form and session objects available, which are used for HTML form processing, as well as user authentication. These objects are described in detail later in this document.

Here's a little sample servlet that generates a "Hello, world!" JSON document:

var hello = {
    message: "Hello, world!"
};

response.contentType = 'application/json';
response.write(JSON.stringify(hello));
response.send();

Server Pages

A JavaScript Server Page looks like an ordinary HTML document, except that it can have embedded JavaScript code using special directives. Such a server page will be compiled into a servlet script when first requested, and then works exactly like a servlet.

Here's a simple example for a server page:

<%
    var now = new DateTime();
%>
<html>
  <head>
    <title>DateTime Sample</title>
  </head>
  <body>
    <h1>DateTime Sample</h1>
    <p><%= now.toString() %></p>
  </body>
</html>

As you can see, the special directive:

<% <script> %>

is used to enclose JavaScript code.

The special directive:

<%= <expression> %>

is used to embed a JavaScript expression. The result of evaluating the expression is directly inserted into the resulting document.

JavaScript Server Page Syntax

The following special tags are supported in a JavaScript server page (JSSP) file.

Hidden Comment

A hidden comment documents the JSSP file, but is not sent to the client.

<%-- <comment> --%>

Expression

The result of any valid JavaScript expression can be directly inserted into the page, provided the result can be written to an output stream. Note that the expression must not end with a semicolon.

<%= <expression> %>

Scriptlet

Arbitrary JavaScript code fragments can be included using the Scriptlet directive.

<%
    <statement>
    ...
%>

Include Directive

Another JSSP file or resource can be included into the current file using the Include Directive.

<%@ include file="<uri>" %>

Alternatively, this can also be written as:

<%@ include page="<uri>" %>

Page Directive

The Page Directive allows the definition of attributes that control various aspects of JavaScript servlet code generation.

<%@ page <attr>="<value>" ... %>

Currently, only the contentType attribute is supported, which allows setting the value of the Content-Type HTTP response header.

<%@ page contentType="application/json" %>

Modules

macchina.io supports JavaScript modules. This allows a script to import another script, with the imported script being encapsulated within a "module" and thus not polluting the global namespace.

A script can import another module using the require() function, specifying either a relative path to the script (i.e., in the same bundle, or to a list of search paths/URIs) or a complete URI. This may even be used to download scripts from a web server, using a HTTP URI.

For require() to work, a module must follow certain conventions:

  • A module may define global variables. However, these will not be available outside of the module, and thus not pollute the global scope.
  • If a module wishes to export an object or function, it has to do so by assigning that object or function to its exports or module.exports object. The module.exports object is the only object available to importers of the module.
  • The name of the resource containing the module must have the extension ".js".

Here's a quick example. Say we want to write a function that looks for a temperature sensor, and returns the sensor object, if one if found:

function findTemperatureSensor()
{
    var temperatureRefs = serviceRegistry.find('io.macchina.physicalQuantity == "temperature"');
    if (temperatureRefs.length > 0)
        return temperatureRefs[0].instance();
    else
        return null;
}

You find that this is a useful function, and you want to use it in different scripts. Now you could copy and paste the function into every script where you need it. For obvious reasons, code sharing by copy-and-paste is a bad idea, so you decide to make the function reusable in the form of a module.

To create a re-usable module that exports the findTemperatureSensor() function, the code needs to be changed as follows:

exports.findTemperatureSensor = function()
    {
        var temperatureRefs = serviceRegistry.find('io.macchina.physicalQuantity == "temperature"');
        if (temperatureRefs.length > 0)
            return temperatureRefs[0].instance();
        else
            return null;
    };

What we've essentially done is we've added the function to the module's exports object. Assuming we've put the above function in a file named "sensors.js", whenever we need this function in a script, we can write:

var sensors = require("sensors");
var temperatureSensor = sensors.findTemperatureSensor();

Instead of adding properties to the exports object, we can also export an entire object. However, this cannot be done by assigning an object to the exports object. The exports object is basically just a reference to the exports property in the global module object. If we assign an object directly to exports, the global exports variable will now reference the object we've assigned. However, the exports property in the module object will still have its old value. Since it is the exports property in the module object that counts, this will not work. We can, however, directly assign an object to the exports property of the module object, in order to export an entire object.

Say, we now want to extend our "sensors.js" module to include support for an ambient light sensor. We can do this as follows:

module.exports = {
    findTemperatureSensor: function()
        {
            var temperatureRefs = serviceRegistry.find('io.macchina.physicalQuantity == "temperature"');
            if (temperatureRefs.length > 0)
                return temperatureRefs[0].instance();
            else
                return null;
        },

    findAmbientLightSensor: function()
        {
            var ambientLightRefs = serviceRegistry.find('io.macchina.physicalQuantity == "illuminance"');
            if (ambientLightRefs.length > 0)
                return ambientLightRefs[0].instance();
            else
                return null;
        }
};

Note that require() will make sure that every module is actually loaded and executed at most once, even if it is imported by multiple modules in an import hierarchy.

require() can be used to import modules from other bundles, or even from the web. To import a module from another bundle, use the "bndl://" URI scheme and specify the symbolic name of the bundle, as well as the file name (and path) of the module in the bundle.

Example:

var module = require("bndl://com.mycompany.mybundle/mymodule.js");

Finding Modules

Modules imported with require() will be searched in the following locations and order:

  1. If a fully qualified URI is specified, the module is loaded from that URI.
  2. The given (relative) path will be resolved against the URI of the importing module.
  3. The given (relative) path will be resolved against the search paths specified for the script (using the searchPaths extension point attribute).
  4. The given (relative) path will be resolved against the global module search paths.

Global Module Search Paths

A global search path for JavaScript modules can be set in the application's configuration file, using the osp.js.moduleSearchPaths configuration property. Multiple paths must be separated with a comma or semicolon.

The application Object

The global application object contains read-only properties that allow a script to obtain information about the application it's executing in.

The following properties are available:

name

The name of the application. In macchina.io, this is "macchina".

startTime

The date and time the application was started, as a DateTime object.

uptime

The time in seconds the application has been running, with millisecond accuracy. Therefore a floating-point number.

config

The application's configuration, as a Configuration object.

See the description of Configuration objects for more information.

Example:

var osName = application.config.getString("system.osName");

logger

The application's logger object. This can be used to write log messages. Note that in macchina.io, each bundle also has its own logger object, which should be preferred to the application's logger.

application.logger.information("Hello, world!");

See the description of Logger objects for more information.

The system Object

The global system object contains read-only properties that allow a script to obtain information about the environment it's executing in.

The following read-only properties and methods are available:

osName

This property contains the name of the operating system, typically "Linux" or "Darwin" (macOS).

osDisplayName

On platforms supported by macchina.io, this is the same as osName.

osArchitecture

The CPU architecture, e.g. "x86_64" or "armv7l".

osVersion

The operating system version, e.g. "3.18.7+".

nodeName

The computer's configured host name.

nodeId

The Ethernet address of the first Ethernet adapter in the system, e.g. "3c:07:54:0d:6e:ef". Note that for systems with multiple adapters, the order of the adapters may not be fixed and thus the node ID may change after a reboot.

processorCount

The number of CPU cores available on the system.

clock

Returns the number of seconds (including fractional seconds) elapsed since system startup.

has(name)

This function returns true if an environment variable with the given name exists.

get(name [, default])

Returns the value of the environment variable with the given name. If the environment variable does not exist and a default value is given, the default value is returned. Otherwise, an exception is thrown.

Example:

var username = system.get("LOGNAME");

set(name, value)

Sets the environment variable with the given name to the given value.

exec(command)

Executes the given shell command using the system's default shell. Returns the output of the command as a String object. The exit status of the command is available via the returned object's exitStatus property.

Example:

var files = system.exec('ls');

Note: be careful when passing strings obtained from HTML forms or other external inputs to system.exec() as command arguments. Always check such strings for validity before using their values to avoid potential command injection attacks.

sleep(milliseconds)

Sleeps the current thread for the given period, in milliseconds.

The uri Object

The global uri object allows a script to load a string or Buffer from a URI. The URI can refer to a local file, a bundle resource ("bndl://"), a HTTP(S) or FTP server. It provides two methods, loadString() and loadBuffer().

loadString(uri)

Loads a string from the given URI. The retrieved content must be UTF-8 encoded text, otherwise the results of converting the content to a string may be undefined.

Example:

var page = uri.loadString("http://macchina.io");

loadBuffer(uri)

Loads a Buffer from the given URI. This can be used to retrieve binary data or non-UTF8 text from a resource.

Example:

var page = uri.loadBuffer("http://macchina.io");

The console Object

The global console object allows a script to write diagnostic output using the application's logging infrastructure. It provides similar methods to the console object available in most web browsers. A script's console object is always connected to its bundle's logger. The following methods are supported:

trace()

Outputs a stack trace to the bundle's logger using debug log level.

assert(condition, message [, arg]...)

Writes an error-level log message, as well as a debug-level stack trace to the bundle's logger if the given condition evaluates to false. The message can contain formatting specifiers. See the Logger object for more information.

debug(message [, arg]...)

Writes a debug-level log message to the bundle's logger. The message can contain formatting specifiers. See the Logger object for more information.

log(message [, arg]...)

Writes an information-level log message to the bundle's logger. The message can contain formatting specifiers. See the Logger object for more information.

info(message [, arg]...)

Writes an information-level log message to the bundle's logger. The message can contain formatting specifiers. See the Logger object for more information.

warn(message [, arg]...)

Writes an warning-level log message to the bundle's logger. The message can contain formatting specifiers. See the Logger object for more information.

error(message [, arg]...)

Writes an error-level log message to the bundle's logger. The message can contain formatting specifiers. See the Logger object for more information.

dump(message, object [, level])

Dumps an object (or Buffer) to the bundle's logger. If the given object is a Buffer, a hex dump of its contents will be written. Otherwise, a pretty-printed JSON representation (JSON.stringify()) of the object will be written. If no log level is specified, debug log level will be used.

Configuration Objects

Configuration objects provide access to the configuration mechanism provided by the POCO Util library and are used for application configuration, bundle properties, etc. The following methods are supported:

getInt(key [, default])

Returns the configuration value for the given key as integer. If a default value is specified, the default value is returned if the configuration key has not been found. An exception is thrown if the key cannot be found and no default value is given, or if the configuration value is not a valid integer.

getDouble(key [, default])

Returns the configuration value for the given key as floating-point number. If a default value is specified, the default value is returned if the configuration key has not been found. An exception is thrown if the key cannot be found and no default value is given, or if the configuration value is not a valid floating-point number.

getBool(key [, default])

Returns the configuration value for the given key as boolean. If a default value is specified, the default value is returned if the configuration key has not been found. An exception is thrown if the key cannot be found and no default value is given, or if the configuration value is not a boolean (true/false).

getString(key [, default])

Returns the configuration value for the given key as string. If a default value is specified, the default value is returned if the configuration key has not been found. An exception is thrown if the key cannot be found and no default value is given.

getObject(key [, default])

Returns the configuration value for the given key as object. If a default value is specified, the default value is returned if the configuration key has not been found. The configuration value must be valid JSON. An exception is thrown if the key cannot be found and no default value is given, or if the configuration value is not valid JSON.

has(key)

Returns true if the given configuration key exists, otherwise false.

set(key, value)

Sets the configuration value for the given key. Value can be a string, integer, floating-point number, boolean or any object that can be JSON-formatted with JSON.stringify(). An exception is thrown if setting the value for the given key fails (e.g., because the configuration is read-only).

keys([key])

Returns an array containing all subkeys of the given key, or root-level keys if no or an empty key is given.

Logger Objects

Logger objects are used to write diagnostic or log messages to a logger. A JavaScript Logger object is always connected to a Logger object from the POCO C++ Libraries. JavaScript scripts in macchina.io have access to two Logger objects. The global application logger, via the application.logger object, and a bundle-specific logger, via the logger object.

Example:

var i = 42;
logger.information("The answer to live, the universe and everything: ", i);

Creating Logger Objects

In addition to the bundle-specific and application-global logger objects available through the logger and application.logger variables, a script can use the Logger constructor to use other logger instances.

Logger(name [, configuration [, reconfigure]])

Creates a new Logger object, using a logger with the given name obtained from the POCO C++ Libraries logging framework. If a logger with the given name does not exist, a new logger instance is created. Note that the underlying logger instances are shared application-wide.

If a new logger instance is created, the given configuration object is used to configure the logger. If reconfigure is true (default is false), the configuration is also applied to an already existing logger. Otherwise, the configuration is ignored and the configuration of an already existing logger is not changed. The reconfigure option should be used judiciously, as reconfiguring an existing logger instance used by other parts of the application may interfere with the workings of the application.

The configuration object can have the following properties:

  • level: specifies the log level; see Log Levels below.
  • pattern: format pattern; if specified, a FormattingChannel with a PatternFormatter is created for the logger. See Format Pattern below for the format of the pattern.
  • channel: either a string containing the name of a globally configured logging channel (through the application's configuration files), or an object containing properties for creating and configuring a new channel. See Channel Configuration below.

Log Levels

The level can either be an integer from 1 to 8, where 1 corresponds to fatal error message and 8 corresponds to a trace message, or it can be one of the following strings, representing a priority:

  • fatal (1)
  • critical (2)
  • error (3)
  • warning (4)
  • notice (5)
  • information (6)
  • debug (7)
  • trace (8)

Numerical log levels are also available as properties of the Logger constructor, named FATAL, CRITICAL, ERROR, WARNING, NOTICE, INFORMATION, DEBUG and TRACE.

Format Pattern

The format pattern is used as a template to format the message and is copied character by character except for the following special characters, which are replaced by the corresponding value.

  • %s - message source
  • %t - message text
  • %l - message priority level (1 .. 7)
  • %p - message priority (Fatal, Critical, Error, Warning, Notice, Information, Debug, Trace)
  • %q - abbreviated message priority (F, C, E, W, N, I, D, T)
  • %P - message process identifier
  • %T - message thread name
  • %I - message thread identifier (numeric)
  • %N - node or host name
  • %U - message source file path (empty string if not set)
  • %u - message source line number (0 if not set)
  • %w - message date/time abbreviated weekday (Mon, Tue, ...)
  • %W - message date/time full weekday (Monday, Tuesday, ...)
  • %b - message date/time abbreviated month (Jan, Feb, ...)
  • %B - message date/time full month (January, February, ...)
  • %d - message date/time zero-padded day of month (01 .. 31)
  • %e - message date/time day of month (1 .. 31)
  • %f - message date/time space-padded day of month ( 1 .. 31)
  • %m - message date/time zero-padded month (01 .. 12)
  • %n - message date/time month (1 .. 12)
  • %o - message date/time space-padded month ( 1 .. 12)
  • %y - message date/time year without century (70)
  • %Y - message date/time year with century (1970)
  • %H - message date/time hour (00 .. 23)
  • %h - message date/time hour (00 .. 12)
  • %a - message date/time am/pm
  • %A - message date/time AM/PM
  • %M - message date/time minute (00 .. 59)
  • %S - message date/time second (00 .. 59)
  • %i - message date/time millisecond (000 .. 999)
  • %c - message date/time centisecond (0 .. 9)
  • %F - message date/time fractional seconds/microseconds (000000 - 999999)
  • %z - time zone differential in ISO 8601 format (Z or +NN.NN)
  • %Z - time zone differential in RFC format (GMT or +NNNN)
  • %L - convert time to local time (must be specified before any date/time specifier; does not itself output anything)
  • %E - epoch time (UTC, seconds since midnight, January 1, 1970)
  • %v[width] - the message source (%s) but text length is padded/cropped to 'width'
  • %[name] - the value of the message parameter with the given name
  • %% - percent sign

Channel Configuration

Logging channels can be configured using a configuration object containing channel-specific properties. The class property specifies the class name of the channel, and can be one of the standard channel classes supported by the POCO C++ Libraries, as well as the name of any custom channel registered with the logging framework. The supported channel class names are:

Any other properties of the configuration object are passed directly to the newly created channel, and are specific to the respective channel. See the documentation of the respective logging channel (Poco::Channel) subclass for supported configuration properties.

Example:

var testLogger = new Logger("TestLogger", {
    level: "debug",
    pattern: "%Y-%m-%d %H:%M:%S.%i [%p] %s<%I>: %t",
    channel: {
        class: "FileChannel",
        path: "test.log"
    }
});

Checking for Logger Objects

The Logger.isLogger() function can be used to check whether a given object is a Logger object.

Logger.isLogger(object)

Returns true if the given object is a Logger object, otherwise false.

Logger Methods

The following methods are available:

trace(message [, arg]...)

Writes a trace log message. Trace log messages have the lowest priority. The message can contain formatting specifiers.

debug(message [, arg]...)

Writes a debug log message. The message can contain formatting specifiers.

information(message [, arg]...)

Writes an informational log message. The message can contain formatting specifiers.

notice(message [, arg]...)

Writes a notice log message. The message can contain formatting specifiers.

warning(message [, arg]...)

Writes a warning log message. The message can contain formatting specifiers.

error(message [, arg]...)

Writes an error log message. The message can contain formatting specifiers.

critical(message [, arg]...)

Writes a critical error log message. The message can contain formatting specifiers.

fatal(message [, arg]...)

Writes a fatal error log message. Fatal log messages have the highest priority. The message can contain formatting specifiers.

log(priority, message [, arg]...)

Writes a log message with the specified priority or log level. The log level can either be an integer from 1 to 8, where 1 corresponds to fatal error message and 8 corresponds to a trace message, or it can be one of the following strings, representing a priority:

  • fatal (1)
  • critical (2)
  • error (3)
  • warning (4)
  • notice (5)
  • information (6)
  • debug (7)
  • trace (8)

Numerical log levels are also available as properties of the Logger constructor, named FATAL, CRITICAL, ERROR, WARNING, NOTICE, INFORMATION, DEBUG and TRACE.

The message can contain formatting specifiers.

Example:

logger.log(7, "A debug message");
logger.log("error", "An error message");
logger.log(Logger.WARNING, "A warning message");

dump(message, object [, level])

Dumps an object (or Buffer) to the bundle's logger. If the given object is a Buffer, a hex dump of its contents will be written. Otherwise, a pretty-printed JSON representation (JSON.stringify()) of the object will be written. If no log level is specified, debug log level will be used.

Message Formatting

The first message argument in a logging method can contain formatting specifiers. Additional arguments are formatted according to these specifiers. If more arguments than formatting specifiers are present, the remaining arguments are simply appended to the log message.

The following formatting specifiers are supported:

  • %d, %i: The argument is formatted as an integer.
  • %f: The argument is formatted as a floating-point number.
  • %s: The argument is formatted as a string.
  • %o: The argument is formatted with JSON.stringify().
  • %O: The argument is formatted with JSON.stringify(), using indentation.

If the type of the argument does not match the formatting specifier, a simple string representation of the object is output instead.

Examples:

logger.information("The answer to %s, the %s and everything: %d", "live", "universe", 42);

Timeouts and Intervals

macchina.io supports the setTimeout() and setInterval() functions known from browser-based JavaScript, as well as setImmediate() known from node.js.

setTimeout(function, milliseconds [, arg]...)

To execute a function after a given time period, use the setTimeout() function.

Example:

setTimeout(
    function() {
        logger.information("Timeout!");
    },
    2000);

Any additional arguments passed after the milliseconds will be passed to the callback function.

setInterval(function, milliseconds [, arg]...)

To execute a function periodically in a specific interval (given in milliseconds), use the setInterval() function.

Example:

setInterval(
    function() {
        logger.information("Timeout!");
    },
    2000);

Example (with argument to callback):

setInterval(
    function(arg) {
        logger.information("Timeout! " + arg);
    },
    2000,
    42);

setInterval(function, milliseconds)

To execute a function periodically in a specific interval (given in milliseconds), use the setInterval() function.

Example:

setInterval(
    function() {
        logger.information("Timeout!");
    },
    2000);

Any additional arguments passed after the milliseconds will be passed to the callback function.

setImmediate(function [, arg]...)

To schedule a function for execution as soon as the current script or callback function finishes, use the setImmediate() function.

Example:

setImmediate(
    function() {
        logger.information("Callback!");
    });

Any additional arguments passed after the function will be passed to the callback function. setImmediate() is equivalent to calling setTimeout() with 0 milliseconds.

setTimeout(), setInterval() and setImmediate() return a timer object which exposes a boolean property named cancelled, as well as a cancel() method to cancel an active timer.

Example:

var count = 0;
var timer = setInterval(
    function() {
        count++;
        logger.information("Timeout ", count);
        if (count == 3)
        {
            timer.cancel();
            logger.information("Done");
        }
    },
    2000);

Note: Since JavaScript execution is single threaded per script, timer callback functions will only run when the script is idle and not currently executing any code.

For compatibility with Browser-based JavaScript and node.js, macchina.io also provides clearTimeout(), clearInterval() and clearImmediate().

clearTimeout(timeout)

Cancels the given timeout, which must be an object returned from setTimeout().

clearInterval(interval)

Cancels the given interval, which must be an object returned from setInterval().

clearImmediate(immediate)

Cancels the given immediate, which must be an object returned from setImmediate().

DateTime and LocalDateTime Objects

DateTime and LocalDateTime objects are similar to JavaScript Date objects, however they provide a few more capabilities, including better support for formatting.

DateTime is UTC-based, while LocalDateTime is based on the system's local time zone.

Creating DateTime and LocalDateTime Objects

DateTime and LocalDateTime objects can be created by using their respective constructor functions, passing a variable number of arguments:

  • No arguments: a DateTime/LocalDateTime object for the current system time is created.
  • A single string argument: the date and time is parsed from the given string, which must contain a date and time in one of the standard representations: asctime, HTTP, ISO 8601, various RFC formats or sortable format (YYYY-MM-DD HH:MM:SS).
  • Two string arguments: the date and time is parsed from the first string, using the format given in the second string. See the Poco::DateTimeFormatter class for a description of the format string.
  • A JavaScript Date object: date and time are taken from that object.
  • A number representing Julian date.
  • Three to six numbers: year, month, day, and optional hour, minute, second. Second may contain fractional seconds.

Examples:

var dt1 = new DateTime("2015-03-09 11:55:33"); var dt2 = new DateTime(2015, 3, 9, 11, 55, 33);

Checking for DateTime and LocalDateTime Objects

The DateTime.isDateTime() and LocalDateTime.isLocalDateTime() functions can be used to check whether a given object is a DateTime, or LocalDateTime, respectively, object.

DateTime.isDateTime(object)

Returns true if the given object is a DateTime object, otherwise false.

LocalDateTime.isLocalDateTime(object)

Returns true if the given object is a LocalDateTime object, otherwise false.

DateTime and LocalDateTime Properties

The following properties are supported by DateTime and LocalDateTime.

year

Returns the year, e.g. 2015.

month

Returns the month, range 1 to 12.

day

Returns the day, range 1 to 31.

hour

Returns the hour, range 0 to 23.

hour12

Returns the hour, range 0 to 12

am and pm

Returns true or false, indicating whether the hour reported by hour12 is AM or PM.

minute

Returns the minute, range 0 to 59.

second

Returns the second, range 0 to 59.

dayOfWeek

Returns the weekday (0 to 6, where 0 = Sunday, 1 = Monday, ..., 6 = Saturday).

dayOfYear

Returns the number of the day in the year. January 1 is 1, February 1 is 32, etc.

julian

Returns the julian day as a number. The fractional part represents the time.

timestamp

Returns the number of microseconds between January 1, 1970 and the date/time in the DateTime/LocalDateTime object.

epoch

Returns the number of seconds between January 1, 1970 and the date/time in the DateTime/LocalDateTime object.

tzd

Supported by LocalDateTime only. Returns the time zone differential, which is the number of seconds between UTC and the system's local time.

DateTime and LocalDateTime Methods

The following methods are supported by DateTime and LocalDateTime.

daysOfMonth([year [, month]])

Returns the number of days in the specified month (1 to 12). If no date or year is given, the year and date from the DateTime/LocalDateTime object are used.

isLeapYear([year])

Returns true if the given year (or the year in the DateTime/LocalDateTime object) is a leap year, otherwise false.

addSeconds(seconds)

Adds the given number of seconds to the DateTime/LocalDateTime.

addHours(hours)

Adds the given number of hours to the DateTime/LocalDateTime.

addDays(days)

Adds the given number of days to the DateTime/LocalDateTime.

utc()

Supported by LocalDateTime only. Returns a DateTime object corresponding to the date/time in UTC.

local()

Supported by DateTime only. Returns a LocalDateTime object corresponding to the date/time in the system's timezone.

format([format]) and toString([format])

Convert the DateTime/LocalDateTime to a string, using a format string. If no format string is given, uses ISO 8601 format.

See the Poco::DateTimeFormatter class for a description of the format string.

Instead of format placeholders, the format string can also be one of the following fixed strings for standard formats:

  • "sortable": 2015-01-03 12:00:00
  • "iso8601frac": 2015-01-03T12:00:00.000000+01:00
  • "iso8601": 2015-01-03T12:00:00+01:00
  • "asctime": Sat Jan 3 12:00:00 2015
  • "http": Sat, 03 Jan 2015 12:00:00 +0100
  • "rfc1036": Saturday, 3 Jan 15 12:00:00 +0100
  • "rfc1123": Sat, 3 Jan 2015 12:00:00 +0100
  • "rfc850": Saturday, 3-Jan-15 12:00:00 +0100
  • "rfc822": Sat, 3 Jan 15 12:00:00 +0100

toDate()

Returns a JavaScript Date object for the stored date/time.

Buffer Objects

Buffer objects are used to work efficiently with binary data in JavaScript. Standard (browser-based) JavaScript has no efficient way for handling binary data. macchina.io provides Buffer objects to handle continuous blocks of memory containing arbitrary (binary) data.

A Buffer object contains a variable-size, continuous block of memory, located on the (C++) heap. The size of the block can change during the lifetime of a Buffer object. In order to avoid frequent resizing and re-allocation of the memory block, the Buffer object differentiates between the physical and logical size of the block. The physical size, or capacity, of the block is the number of bytes actually allocated on the heap. The logical size, or length, is the number of bytes in use. The length is therefore always smaller than or equal to the capacity. The capacity of a Buffer can be 0 — in this case no heap memory will be allocated (and consequently, the Buffer cannot contain any data). Both capacity and length of a Buffer can change during the lifetime of a Buffer object.

The contents of a Buffer can be accessed via indexed properties. In this regard, a Buffer provides an interface very similar to a JavaScript array. Each byte in the buffer can be addressed by its offset from the beginning of the Buffer's memory block. Buffer objects also support the methods concat(), slice(), push() and pop() known from JavaScript Array.

For debugging purposes, the contents of a Buffer can be written to the application log as a hex dump, using logger.dump() or console.dump().

Creating Buffer Objects

A Buffer object can be created empty, from a JavaScript Array (containing byte values), from a JavaScript String, or from (a part of) another Buffer. The Buffer() constructor can be used to create a Buffer object.

Buffer()

Creates an empty, zero-capacity (and zero-size) Buffer. No memory for the buffer's content will be allocated.

Buffer(capacity)

Creates an empty (zero-size) Buffer with the given capacity in bytes. A block of memory with size given in capacity will be allocated.

Buffer(string [, encoding])

Creates a Buffer from the given string. The (optional) encoding parameter specifies how the contents of the string will be store in the buffer:

  • Buffer.BASE64: The string contains binary data encoded using Base64 encoding. The Base64 data will be decoded and the raw binary data will be stored in the Buffer. This is the default if no encoding is given.
  • Buffer.UTF8: The string will be stored in the Buffer using UTF-8 encoding.
  • Buffer.UTF16: The string will be stored in the Buffer using UTF-16 encoding (in the hosts native byte order).
  • Buffer.ASCII: The string will be stored in the Buffer using ASCII encoding.

In addition to the above values, encoding can also be a string containing the name of one of the text encodings supported by the POCO C++ Libraries:

  • "UTF-8", "UTF8"
  • "UTF-16", "UTF16"
  • "UTF-32", "UTF32"
  • "ISO-8859-1", "Latin-1", "Latin1"
  • "ISO-8859-2", "Latin-2", "Latin2"
  • "ISO-8859-15", "Latin-9", "Latin9"
  • "Windows-1250", "windows-1250", "cp1250", "CP1250"
  • "Windows-1251", "windows-1251", "cp1251", "CP1251"
  • "Windows-1252", "windows-1252", "cp1252", "CP1252"

If any additional text encodings have been registered using Poco::TextEncoding::add(), these can be used as well.

Buffer(array)

Creates a buffer from a JavaScript Array containing byte values.

Buffer(buffer [, begin [, end]])

Creates a buffer by copying the contents of another buffer. An optional range can be given, with begin specifying the offset of the first byte to copy and end specifying the offset of the byte following the last byte to copy.

Example:

var newBuffer = new Buffer(buffer, 0, buffer.length);

will copy the entire buffer, while

var newBuffer = new Buffer(buffer, 2, 4);

will copy two bytes, starting at offset 2.

The given offsets can also be negative, meaning relative to the length of the Buffer.

Example:

var newBuffer = new Buffer(buffer, -2, buffer.length);

will copy the last two bytes of the Buffer.

Checking for Buffer Objects

The Buffer.isBuffer() function can be used to check whether a given object is a Buffer object.

Buffer.isBuffer(object)

Returns true if the given object is a Buffer object, otherwise false.

Packing and Unpacking Structured Data

Buffer objects support packing and unpacking of structured data via the pack() and unpack() methods. The interface of these objects, specifically the format string, follows the struct.pack() and struct.unpack() functions known from the Python programming language. However, one important difference is that Buffer's pack() and unpack() do not support automatic alignment of values.

pack() and unpack() are controlled via a format string.

Format String

A format string begins with an optional byte order specifier. The following specifiers are supported:

  • "=" - host's native byte order (default)
  • "<" - little endian byte order
  • ">" - big endian byte order
  • "!" - network byte order (big endian)

The byte order specifier is followed by one or more type specifiers. A type specifier can be preceded by an optional repeat count. For atomic types, the repeat count specifies the number of values that will be packed according to the following type specifier. For strings ("c" and "s"), the repeat count specifies the number of bytes reserved for the packed string. If a string exceeds the given length, it will be truncated. Unused bytes will be zero-filled.

Whitespace characters (Space, TAB, CR, LF) in the format string are ignored. However, there must not be a whitespace character between length and type specifier.

The following type specifiers are supported:

Specifier    Native C/C++ Type              Size    Notes
-------------------------------------------------------------------------------------
x            -                              1       Padding Byte (set to 0 in pack())
c, s         char[repeat]                   repeat  UTF-8 encoded
b            int8_t (signed char)           1
B            uint8_t (unsigned char)        1
?            bool                           1       0 = false, 1 = true
h            int16_t (short)                2
H            uint16_t (unsigned short)      2
i, l         int32_t (int)                  4
I, L         uint32_t (unsigned)            4
q            int64_t (long long)            8
Q            uint64_t (unsigned long long)  8
f            float                          4
d            double                         8

Buffer Properties

Buffer objects support the following properties:

length (r/w)

The length of the buffer, in terms of actually used bytes (as opposed to the capacity). Can be changed dynamically; existing contents will be preserved, but may be truncated.

capacity (r/w)

The capacity of the buffer in bytes, or how much memory is allocated. Can be changed dynamically; existing contents will be preserved, but may be truncated.

Buffer Methods

Buffer objects support the following methods:

concat(buffer)

Appends the contents of the given Buffer to this Buffer. Capacity and length of the Buffer will be adjusted as necessary.

slice([begin [, end]])

Creates a new Buffer by copying the contents of an existing Buffer. An optional range can be given, with begin specifying the offset of the first byte to copy and end specifying the offset of the byte following the last byte to copy.

push(byte)

Appends a single byte to the Buffer. The given byte value must be an integer in range 0 - 255. Length will be incremented. Capacity will be adjusted if necessary (implying a re-allocation of the buffer).

pop()

Returns and removes the last byte in the buffer. The length of the buffer will be decremented by one.

toBase64([lineLength])

Returns a String containing the Base64-encoded content of the Buffer. An optional line length for the Base64 data can be given. If no line length, or a zero line length is given, there will be no line breaks in the Base64 data. For best interoperability, a line length of 72 should be used if Base64 data is to be sent over a network or otherwise exchanged with other parties.

fromBase64(string)

Decodes the given Base64-encoded string and stores the resulting data in the buffer.

decodeString([encoding])

Returns a JavaScript String from the contents of the buffer, assuming the given encoding, or UTF-8 if no encoding is given. See the Buffer constructor documentation for a list of supported encodings.

encodeString(string [, encoding])

Encodes the given JavaScript string and stores the result in the Buffer, using the given encoding, or UTF-8 if no encoding is given. See the Buffer constructor documentation for a list of supported encodings. Note that the default encoding (UTF-8) is different from the Buffer constructor (Base64).

pack(format, array)

Packs the values in the given array into the buffer, using the byte order and types found in the format string.

Example:

buffer.pack("!4BH", {192, 168, 1, 2, 8080});

This will fill the buffer with the following content:

Offset   Byte
-------------
0        192
1        168
2        1
3        2
4        31
5        144

unpack(format)

Unpacks values from the buffer according to the byte order and types found in the format string. Returns an Array containing the unpacked values.

Example:

var buffer = new Buffer({192, 168, 1, 2, 31, 44});
var values = buffer.unpack("!4BH"); // {192, 168, 1, 2, 8080}

HTTPRequest and HTTPResponse Objects

HTTPRequest objects are used to either send a HTTP or HTTPS request to a web server, or to obtain information about the HTTP request in a servlet or server page.

As of the 0.7.0 release, the HTTPRequest object has been moved into a separate net module, available through the com.appinf.osp.js.net bundle.

You'll have to import the net module in your script before being able to use the HTTPRequest object.

To send a HTTP request, create a new HTTPRequest object, using the HTTPRequest constructor function. The constructor takes three optional arguments, a request method (e.g., "GET"), a URI (e.g., "http://macchina.io") and the HTTP version string (e.g., "HTTP/1.1"). All these values can be set at a later time as well, via properties.

HTTP requests can be sent in blocking or asynchronous mode. In blocking mode, the script has to wait until the HTTP request completes. In asynchronous mode, the HTTP request is sent in a separate thread, and the script is notified of the result via a callback function.

Here is an example for a synchronous request:

var net = require('net');
var request = new net.HTTPRequest('GET', 'http://macchina.io');
var response = request.send();
logger.information(response.status, " ", response.reason);
logger.information(response.contentType);
logger.information(response.content);

And here's the same example, with the request sent asynchronously:

var net = require('net');
var request = new net.HTTPRequest('GET', 'http://macchina.io');
var response = request.send(
    function (result) {
        if (result.error)
        {
             logger.error(result.error);
        }
        else
        {
            logger.information(result.response.status, " ", result.response.reason);
            logger.information(result.response.contentType);
            logger.information(result.response.content);
        }
    }
);

Of course, HTTPRequest can also be used to send POST or PUT requests, as well as requests using any other HTTP methods.

Here's a sample how to post some JSON to a Hue base station to control a smart bulb:

var net = require('net');
var request = new net.HTTPRequest('PUT', 'http://192.168.178.151/api/newdeveloper/lights/1/state');
request.content = JSON.stringify({on: true, bri: 100, hue: 46920, sat: 255});
request.contentType = 'application/json';
request.timeout = 10.0;
request.send(
    function(result)
    {
        if (result.error)
        {
            logger.error(result.error);
        }
        else
        {
            logger.information(result.response.status, " ", result.response.reason);
        }
    }
);

HTTPRequest Properties

HTTPRequest objects support the following properties:

method (r/w)

The request method, e.g. "GET", "POST", etc.

uri (r/w)

The request URI or path, e.g. "/index.html".

version (r/w)

The HTTP version string, usually "HTTP/1.1".

contentType (r/w)

The request content type, e.g. "application/json".

content (r/w)

The request content, as string.

Only Unicode UTF-8 encoded text content is supported. For other types of content, use the buffer property.

buffer (r/w)

The request content, as Buffer object. Using the buffer property, content that is not UTF-8 text can be handled.

Note: setting the buffer property will also affect the content property (and vice-versa), since both use the same underlying data.

timeout (r/w)

The request timeout in seconds.

cookies (ro)

An object containing all cookies sent with the request, with cookie name as key and cookie value as string value.

credentials (ro)

If the request has been sent with HTTP Basic Authentication information, returns an object with a "username" and a "password" property containing the credentials.

If no valid authentication information has been sent, returns null.

HTTPRequest Methods

The following methods are supported by HTTPRequest objects:

has(name)

Returns true if the request has a header with the given name, false otherwise.

get(name [, default])

Returns the value of the request header with the given name, or the given default value if the header is not present. If no default is given, returns an empty string if the header is not present.

set(name, value)

Sets the request header with the given name to the given value.

authenticate(username, password)

Adds a HTTP Basic Authentication header to the request, using the given credentials.

send([callback])

Sends the request. If a callback function is given, the function will return immediately and will not return anything. Success or failure will be reported via the callback function. The callback function must accept a single argument, which will be an object containing an "error" or a "response" property. If the request failed, the "error" property will contain an error message. Otherwise, the "response" property will contain a HTTPResponse object. Note that even if no "error" property is set, the request may still have failed, although on the HTTP level. Check the HTTP response status, using the result.response.status property.

If no callback function is given, the request is sent and the function will block until the complete response is received, or an error has occurred. If an error occurred, an exception will be raised. Otherwise, a HTTPResponse object will be returned.

HTTPResponse Properties

HTTPResponse object support the following properties:

status (r/w)

The HTTP status code, e.g. 200.

reason (r/w)

The HTTP status message, e.g. "OK" (for a 200 status).

version (r/w)

The HTTP version string, e.g. "HTTP/1.1".

contentType

The response body content type, e.g. "text/html".

content (r/w)

The response content, as string.

Only Unicode UTF-8 encoded text content is supported. For other types of content, use the buffer property.

buffer (r/w)

The response content, as Buffer object. Using the buffer property, content that is not UTF-8 text can be handled.

Note: setting the buffer property will also affect the content property (and vice-versa), since both use the same underlying data.

HTTPResponse Methods

has(name)

Returns true if the response has a header with the given name, false otherwise.

get(name [, default])

Returns the value of the response header with the given name, or the given default value if the header is not present. If no default is given, returns an empty string if the header is not present.

set(name, value)

Sets the response header with the given name to the given value.

setStatus(status)

Sets the response status code, e.g. 200.

write(text) and writeln(text)

Appends text to the response body.

Note: currently only Unicode UTF-8 encoded text content is supported.

writeln() adds a newline after the text.

writeHTML(text)

Writes text to the response body. All HTML reserved characters will be escaped properly.

htmlize(text)

Returns a copy of text with all HTML reserved characters properly escaped.

send()

Sends the response. Can be used in servlets or server pages on the global response object only.

The form Object

The global form object is available in servlets and server pages. It gives convenient access to any HTML form data sent by the browser.

The form object has two methods, has() and get().

Additionally, the form object has shortcut accessors for form fields. Instead of writing:

var value = form.get('field');

one can also write:

var value = form.field;

has(name)

Returns true if the form has a field with the given name, false otherwise.

get(name [, default])

Returns the value of the form field with the given name. If the field is not present, returns the default value. If no default value has been given, returns an empty string if the form field does not exist.

The session Object

The global session object is available in servlets and server pages. It gives access to the session object for the current request.

Note: Currently there is no way to create a session object from JavaScript. In macchina.io, a session object is created when a user logs in to the web interface.

session Properties

The session object supports the following read-only properties:

id

The internal ID of the session object.

username

The name of the signed-in user. Only available if a user is signed in and the session is authenticated.

authenticated

Returns true if the session is authenticated, otherwise false. A session is authenticated if the user has successfully signed in.

csrfToken

A random string that can be used as a CSRF synchronizer token in forms, to prevent CSRF (Cross-Site Request Forgery) attacks.

clientAddress

The IP address and port number of the HTTP client.

session Methods

setInt(name, integer)

Adds or updates a an integer property with the given name to the session.

getInt(name [, default])

Returns the integer value of the property with the given name.

setString(name, string)

Adds or updates a string property with the given name to the session.

getString(name [, default])

Returns the string value of the property with the given name.

erase(name)

Erases the property with the given name.

authorize(permission)

Returns true if the currently signed in user has the given permission, by checking with the OSP authentication service, using the username from the session.

The bundle Object

The global bundle object gives a script (including servlet or server page) access to some properties of its bundle.

bundle Properties

The bundle object has the following read-only properties:

name

The name of the bundle.

symbolicName

The symbolic name of the bundle.

version

The version of the bundle.

path

The path of the bundle.

state

The state of the bundle as string. Possible values are:

  • installed
  • uninstalled
  • resolved
  • starting
  • active
  • stopping

temporaryDirectory

The path to the bundle's directory for storing temporary data. This directory is cleared whenever the bundle stops.

persistentDirectory

The path to the bundle's directory for storing persistent data.

properties

A Configuration object for accessing the bundle's properties defined in the bundles' "bundle.properties" file.

bundle Methods

The bundle object has the following methods:

getResourceString(path)

Returns a string containing the contents of the file stored in the bundle with the given path. Will only work for files containing text, as the contents of the file are stored in a string.

getResourceBuffer(path)

Returns a Buffer containing the contents of the file stored in the bundle with the given path.

getLocalizedResourceString(path [, language])

Returns a string containing the contents of the localized file stored in the bundle with the given path. Will only work for files containing text, as the contents of the file are stored in a string. An optional RFC 1766 language tag string (e.g., "en-US") can be specified. See the Poco::OSP::LanguageTag class for more information.

getLocalizedResourceBuffer(path [, language])

Returns a Buffer containing the contents of the localized file stored in the bundle with the given path. An optional RFC 1766 language tag string (e.g., "en-US") can be specified. See the Poco::OSP::LanguageTag class for more information.

The serviceRegistry Object

The global serviceRegistry object gives access to the OSP Service Registry. In macchina.io, all sensors and devices are available as services through the service registry.

serviceRegistry Methods

The serviceRegistry object supports the following methods:

find(query)

Looks up the service with the properties specified in the query. Returns an array of ServiceRef objects containing references to all services matching the query.

To obtain the actual service object, the instance() method of the corresponding ServiceRef object must be called.

The query language is similar to JavaScript or C++ expressions and supports comparison (==, !=, <, <=, >, >=), regular expression matching (=~) and logical and/or operations (&&, ||). Subexpressions can be grouped with parenthesis. The data types string, integer, float and boolean are supported. The simplified syntax for the query language is given in the following.

expr          ::= andExpr ["||" andExpr]
andExpr       ::= relExpr ["&&" relExpr]
relExpr       ::= ["!"] (id [relOp value | "=~" matchExpr]) | subExpr
subExpr       ::= "(" expr ")"
relOp         ::= "==" | "!=" | "<" | "<=" | ">" | ">="
value         ::= numLiteral | boolLiteral | stringLiteral
numLiteral    ::= [sign] digit*"."digit*["E"["+" | "-"]digit*]
boolLiteral   ::= "true" | "false"
stringLiteral ::= '"' char* '"'
matchExpr     ::= stringLiteral | regExpr
regExpr       ::= delim char+ delim /* valid Perl regular expression,
                                       enclosed in delim */
delim         ::= "/" | "#"

Examples for valid queries are given in the following:

  • name == "com.appinf.osp.sample.service" — a simple string comparison for equality.
  • majorVersion > 1 && minorVersion >= 5 — numeric comparisons and logical AND.
  • name =~ "com.appinf.osp.*" && someProperty — simple pattern matching and test for existence of someProperty.
  • someProperty =~ /[0-9]+/ — regular expression matching.

findByName(name)

Looks up the service with the given name. If found, returns a ServiceRef object for it, otherwise returns null.

To obtain the actual service object, the instance() method of the corresponding ServiceRef object must be called.

createListener(query, registeredCallback, unregisteredCallback)

Creates and returns a new ServiceListener object that listens for status changes of services matching the given query. The query language is the same as for find(). Two callback functions must be provided as arguments. The first one will be called when a new service matching the query is registered. It will also be called after calling createListener() for already registered services matching the query. The second callback function will be called when a service matching the query is unregistered. Both callback functions will receive a ServiceRef object representing the respective service as argument.

ServiceListener Objects

ServiceListener objects only expose a single method, dispose(). It can be used to tell the ServiceListener that it's no longer needed and should stop listening for service status changes. After calling dispose(), the callback functions will no longer be called.

ServiceRef Objects

ServiceRef objects have two methods. The first one, instance(), returns the underlying service object. Note that not all service objects are accessible from JavaScript. Only service objects implemented using the Remoting-based bridging mechanism are accessible from JavaScript. An attempt to call instance() for a service object not accessible from JavaScript will result in an exception. The second method is equals(), which takes nother ServiceRef object as argument and returns true if both refer to the same service, otherwise false.

Database Session and RecordSet Objects

Database Session and RecordSet objects provide access to the SQLite database engine that comes with macchina.io.

As of the 0.7.0 release, the Database Session and RecordSet objects have been moved into a separate data module which is available through the com.appinf.osp.js.data bundle.

You'll have to import the data module in your script before being able to use the Database Session and RecordSet objects.

Here's a short example showing the Database Session and RecordSet objects being used to periodically log sensor data in a SQLite database.

var temperatureSensor;
var temperatureRefs = serviceRegistry.find('io.macchina.physicalQuantity == "temperature"');
if (temperatureRefs.length > 0)
{
    temperatureSensor = temperatureRefs[0].instance();
}

var data = require('data');
var dbSession = new data.Session('SQLite', bundle.persistentDirectory + 'temperature.db');

dbSession.execute('CREATE TABLE IF NOT EXISTS temperature ( \
    timestamp TIMESTAMP, \
    temperature FLOAT \
    )');

setInterval(
    function() {
        dbSession.execute(
            'INSERT INTO temperature VALUES (?, ?)',
            new Date(),
            temperatureSensor.value()
        );
    },
    60000);

This sample shows how to create a Database Session object connected to a SQLite database, create a table if it does not yet exist, and how to insert rows into the table.

Note: there is a subtle, but important difference in JavaScript between:

var date = Date();

and:

var date = new Date();

The first expression will result in a string object ("Mon Mar 09 2015 21:30:50 GMT+0100 (CET)"), while the second expression will result in a Date object, as one would expect. When storing Date objects in an SQLite database make sure you're using a Date object and not a "stringified" date. Otherwise extracting dates from the database will produce wrong results, as the JavaScript-produced date string is not supported by SQLite, which internally uses ISO 8601.

Once you have data in a database, you may also want to get it out again. Following sample shows the code for a servlet that returns the last 100 data samples as a JSON document.

var data = require('data');
var dbSession = new data.Session('SQLite', bundle.persistentDirectory + 'temperature.db');
dbSession.pageSize = 100;

var recordSet = dbSession.execute('SELECT timestamp, temperature FROM temperature ORDER BY timestamp DESC');
response.contentType = 'application/json';
response.write(recordSet.toJSON());
response.send();

Creating a Database Session

A Database Session object is created by calling the constructor function with two arguments. The first argument is the connector name, which for macchina.io always is "SQLite". The second argument is the path to the SQLite database file.

Database Session Properties

Database Session objects have the following properties:

connector (ro)

The name of the database connector. For macchina.io, only "SQLite" is supported.

connectionString (ro)

The connection string. For macchina.io, this is always the path to the SQLite database file.

isConnected (ro)

Returns true if the session is connected to the database. For macchina.io and SQLite, this returns true, unless the session has been closed by calling close().

isTransaction (ro)

Returns true if a transaction is active, otherwise false.

pageSize (r/w)

The result page size for queries.

The page size specifies how many rows are transferred at a time when delivering a query result. The larger the page size, the more memory is required.

Defaults to 256. Setting this to 0 will result in an unlimited page size, which is not recommended.

Database Session Methods

The following methods are supported by Database Session objects:

begin()

Starts a new transaction.

commit()

Commits the current transaction.

rollback()

Rolls back the current transaction.

close()

Closes the session. After the session has been closed, it must no longer be used.

executeNonQuery(statement [, args]...)

Execute a non-query SQL statement (e.g., INSERT or UPDATE). The statement can contain placeholders ('?'), which will be replaced by the given arguments using the underlying database's data binding mechanism.

Returns the number of rows affected by the statement.

execute(statement [, args]...)

Execute a SQL query statement. The statement can contain placeholders ('?'), which will be replaced by the given arguments using the underlying database's data binding mechanism.

Returns a RecordSet object containing the query result.

RecordSet Objects

RecordSet objects are returned by the execute() method of Database Session objects. A RecordSet contains the result of a SQL query. For non-query statements, the record set will be empty.

RecordSet Properties

RecordSet objects support the following properties:

columnCount (ro)

Returns the number of columns in the query result.

rowCount (ro)

Returns the number of rows in the record set. The number of rows in the record set is never larger than the page size configured in the session.

statement (ro)

Returns the statement or query that produced this record set.

RecordSet Methods

RecordSet objects support the following methods:

getValue(column [, row])

Returns the value stored in the given column, for the given row. If no row is given, the current row is used (see moveNext(), etc.).

The column can be specified by name, or by numerical index.

getName(index)

Returns the name of the column with the given column index.

getType(column)

Returns the data type name of the column with the given column name or numerical index.

The following type names can be reported:

  • bool
  • Int8
  • Int16
  • Int32
  • Int64
  • UInt8
  • UInt16
  • UInt32
  • UInt64
  • float
  • double
  • string
  • wstring
  • DateTime
  • Date
  • Time
  • BLOB
  • CLOB

getLength(column)

Returns the length of the column with the given column name or index.

getPrecision(column)

Return the precision of the column with the given column name or index.

moveFirst()

Sets the current row to the first row in the record set. Returns true if the record set contains at least one row, false otherwise.

moveLast()

Sets the current row to the last row in the record set. Returns true if the record set contains at least one row, false otherwise.

moveNext()

Moves the current row to the next row in the record set. Returns true if successful, or false if no more rows are available.

movePrevious()

Moves the current row to the previous row in the record set. Returns true if successful, or false if the current row already is the first row.

fetchNextPage()

Fetches the next results page from the database.

close()

Closes the record set. After being closed, the record set must not be used anymore.

toJSON()

Converts the rows in the record set into a JSON document containing an array of objects, each one representing a row. Column names are used as property names in the objects.