Scripting basics

This section describes the basic facilities for scripting within Metrici. Later sections describe the various libraries that can be used, how to run services to interact with the application, and differnt scenarios where scripts may be used.

Hello World

You can try out script using the RunScript service, which you can run using the Submit service request screen (you must be signed in to use this screen).

Hello World would be:

<RunScript>
  <script>
    application.put('return','Hello World!');
  </script>
</RunScript>

This returns:

<RunScript>
  <errorNumber>0</errorNumber>
  <return>Hello World!</return>
</RunScript>

See Run Script / Execute Script for full details of the RunScript service.

Application object

All interaction between the script and the application is achieved, directly or indirectly, through the application object.

This application object is of type Script Application. In summary it allows:

  • Storage of attributes that can be passed in and out of the script, or to other scripts.
  • Calling services.
  • Returning values, which can be simple strings or XML messages.
  • Running scripts stored in nodes.
  • Changing the user under which script is running.
  • Retrieving nodes for read or update.
  • Utility functions to handle XML data and URLs.

Errors

The ScriptApplication object holds a set of error fields that are the same as those used by other objects within Metrici. This includes an error number, an error description (system description for error), error message (user description of error) and error fields, and methods to get and set these. See Scripting errors for details.

The error fields are automatically set when errors are detected, and are automatically returned when the script completes. The various places that call scripts all interpret these errors.

To test if there is an error, use application.errorFound(), which returns true if there is an error.

To get a full description of an error, use application.getErrorNumberDescription().

To reset the error state, use application.resetError().

To set an error, use application.setError() or application.mergeError().

Error fields are not automatically reset. You should check, and if necessary reset the errors, after every call.

Returning values

There are four options for returning values:

  • Return nothing. In this case, the error state of the application object will be returned.
  • Return a string. Set the 'return' attribute to a string.
  • Return an XML message. Use the application.newServiceResponse() to create a Service Message Writer object that can be used to set return values. Alternatively:
    • Return an XPath Evaluator object, which will be translated into an XML message provided that the underlying XML node is a document or an element.
    • Use application.put('return',application.parse(xmlString)) to return an arbitrary piece of XML formatted as a string in xmlString.
  • Return values in application attributes. This is only available when calling one script from another script; application attributes are lost when calling a script through the service interface.

If there are errors, other than user errors (105, 108, 109), then the errors will always be returned in preference to other return values.

Timeouts

Scripts have a timeout period, which defaults to 30 seconds. Scripts can extend this timeout period by calling application.extendTimeout(). This is called automatically every time the script calls a service, and therefore should not need to do anything to manage the timeout periods.

As well as the 30 second timeout, there is a unextendable timeout of 1 hour even for long-running scripts.

After the 30 second timeout has been detected, there is a short grace period (an additional timeout) in which you can call application.extendTimeout(). This allows you to extend a timeout after a long-running method call, such as apply().

// Allow for long-running apply
newNode.apply();
application.extendTimeout();

Context

Scripts can run in a context, which describes the node on which the script is operating. The current context node can be queried using application.getContext(). For some types of scripts this is returned as a Script Node Writable Derived or a Script Node Writable Original object which allows changes to the node's derived data. The value returned from application.getContext() is not passed to other scripts executed or called from this script.

A script may also have the context node available in attribute "contextNode". In this case, the context node is only available as a Script Node, not as writable node. It can, however, be passed to other scripts.

Context is not initially set when a script is invoked using include (see below).

Stored scripts

Scripts can be stored as data on nodes.

There are three different methods of invoking a stored script, referred to as include, execute and call. Include is used by the RunScript service, by the run method on node pages, and the application.include() method in scripts.

Execute is used by the ExecuteScript service, by the execute method on node pages, and the application.execute() methods in scripts.

Call is used by the application.call() method in scripts.

The three different methods are fundamentally different.

  Include Execute Call
Purpose Run additional code within the current context. Run a script against a different node. Run additional code in its own context.
Which script is run The script held as the node's member identified by memberType. (If you think of the node as a library, this just runs code from the library.) The script held as the node type's member identified by memberType (If you think of the node type as a class, this runs a method defined on the class.) Like include, the script is held as a node member identified by a member type.
Attributes Read-write access Read-write access Read-write access
Error fields Read-write access Read-write access Read-write access
Global variables and functions Shared Separate Separate
Credentials Shared Copied Copied
Credential changes Passed back Not passed back Not passed back
Context Of the including node Of the executed node Of the including node
Required authority Read node/member type Execute node Read node/member type
If script not found Raise error unless attribute allowNull is set to true Raise error unless attribute allowNull is set to true Raise error unless attribute allowNull is set to true

Call allows a node to invoke a script defined on a node type as if it were defined on its own node. This is useful when using inheritance, for example to invoke the derivation processing of an inherited-from node. It is loosely analogous to the JavaScript call method, which allows a method to be called with a different context.

Returning data to the user

When scripts are called which create data for the user (e.g. a script run from node page), the script should return a message of form:

<Response>
  <redirect>redirectURL</redirect>
  <title>Page title</title>
  <message>User message</message>
  <head>Head matter</head>
  <navigation>Navigation content</navigation>
  <preamble>Preamble content</preamble>
  <content>Main content</content>
</Response>

All sections are optional. If the script returns nothing, the node is simply redisplayed.

If message is set, then the user message for the next screen display is set.

If redirect is set, then the user is redirected to the given URL.

If redirect is not set, and content is set, then the HTML in head, navigation and content are written to the head area, navigation area and main content area respectively. These should contain fully marked-up HTML in string form, e.g.

<content>&lt;p&gt;Hello World&lt;/p&gt;</content>

Escaping of the HTML is carried out by the put() method on the ServiceMessageWriter, i.e. you would output the above with something like.

var response = application.newServiceResponse('Response');
response.put('content','<p>Hello World</p>');

<preamble> is similar to content, and writes to the top of the content page, before the tabs. It is only supported by extension scripts.

${rootPath} and ${nodeURL} in redirect, navigation and content will be replaced with suitable values.

In extension scripts, the high-level element should be set to Extension, rather than Response.

title, if given, is used as both the title for the page and as the value of the h1 element.

HTML in the content, preamble, navigation and head areas will be filtered before it is returned to the user. This will mend any broken HTML, and remove potentially dangerous content such as JavaScript.

User credentials

A script always runs under the authority of a user. This is generally the user who ran the service that, directly or indirectly, called the script. This user authority controls what can be seen when objects are retrieved using the newNode() method, and the authority passed in service requests with the run() method.

There are times when the user needs to be changed:

  • A script called by a general user may need to run with higher authority, to access data that the user would not normally see. For example, a summarisation script called by a user might need to summarise data from other users, although the user can not see the detail of that data.
  • A script that creates a new user may need to switch to using the the new user's credentials in order to complete the setup.

The script application object offers two different mechanisms for changing the user under which the script runs.

The application.setCredentials() method sets the credentials to be used for service requests, but does not modify the underlying authority of the script. The application.resetCredentials() method reverts the credentials to the underlying authority.

application.runAsOwner(), which is not passed any parameters, allows the script to be run as the node owner. This is always permitted if the context is set.

When node and node member objects are created, they are created under the authority of a user. When a script is passed nodes in attributes, these will not have the correct authority after a runAs() has been invoked, and will need to be re-retrieved.

User credentials are passed to any scripts called by application.include(), application.execute() or application.call(). However, any credential changes made in an executed script are reset on return to the caller. Executing an authorised script does not make the calling script authorised.

Transaction control

Scripts should make no assumptions about the transaction control mechanisms in place. Specifically, they should cope with two extremes:

  • Dirty reads - a script should assume that any updates made by any transaction may become immediately available for read anywhere else, irrespective of commits, but must not rely on this behaviour.
  • Consistent reads - a script should assume that any updates made by any transaction are not available to any other transaction that has started, even after the transaction making the update has committed, but must not rely on this behaviour. (See http://dev.mysql.com/doc/refman/5.1/en/innodb_consistent_read in the MySQL manual for a description of this behaviour.)

In practice, this means:

  • Scripts must be very cautious when updating a node that may be updated simultaneously by other scripts, especially when scripts are triggered to run in the background. Any concurrent access is likely to lead to locking. Be particularly cautious of dependency processing for packages. Set the package content change behaviour of packages into which automatically generated nodes are inserted so that they do not constantly update and set out of date the package (use the update nothing or update dependents options so that the package itself is not put out of date).
  • To give called services visibility of recently made changes, scripts must commit before calling services.
  • To gain visibility of changes made by services, scripts must themselves also commit after the service call. (This is an impact of consistent reads - changes made by different database connections in services are not available to the script until the script commits.)

Bindings

It is bad practice to hard code node references with scripts, except for system-defined nodes. It makes both maintenance and change control harder.

To avoid hard coding node references, each node on which scripts are coded should have a Bindings (system.BINDINGS) member type. This allows names to be associated with nodes. These nodes can then be retrieved by the script using application.getBinding(), which is passed the name of the binding.

The bindings must be defined on the same node as the script itself, or more precisely on the node that is the source when the getBinding is called. This even applies to included scripts, even though they otherwise share the scope of the script that calls them. However, if a function contains getBinding(), it will be the source node when the function is called that is used.

Calling the getBinding() method on a node looks for a binding on the node, and, optionally, if it is not there, on its parent package. This can be used to create dynamic bindings that are resolved at run time. The type Dynamic Bindings (library.parts.DynamicBindingsType) can be used to create a set of free-standing bindings that can be inherited by the package at the root of the solution. This can then be referenced using dynamic binding, allowing scripts to locate different parts of the solution.

Structure

A typical script will resolve the context node, include other scripts referenced in the bindings, retrieve the identity of bound nodes and return results. The following code illustrates an effective program structure that achieves this. Putting all substantive processing in functions that are only called if there is no error ensures that errors in included scripts are detected properly.

/**
* Example script
*/
var context = application.getContext();
application.include(application.getBinding('scriptOther'));
var boundNode = application.getBinding('boundNode');

function main() {
var response = application.newServiceResponse('Response');
response.put('content',getContent());
}

function getContent() {
var s = '';
s += 'Hello World';
return s;
}

if (!application.errorFound()){
main();
}