Scripting with JavaScript

Elixir Report Designer supports JavaScript 1.7 as standardized by the European Computer Manufacturer’s Association (ECMA) as ECMA Script version 3. This section details some of the features of JavaScript and their use within the Report framework.

In Elixir Report Designer, scripts are always rendered in the order that the bands appear in the output. Within a band, scripts will be rendered first in the following sequence:

  • RenderIf
  • OnRenderBegin
  • OnRenderEnd
  • OnLayout (if available)

For element(s), in each band, scripts of each element will run in the order the elements appear in the “Shapes” tree (Z-order).

JavaScript is a weakly typed, object-based scripting language modeled on Java syntax. It is a case sensitive language and the following keywords are reserved.

THe table below list the keywords, primitive types and compound types.

Types List
Keywords break case catch continue default delete do
else false finally for function if in
instanceof new null return sswitch this throw
true try typeof var void while with
Primitive Types Number Boolean String
Compound Types Object Arrays

For more detailed information on JavaScript, refer to the following:

  • JavaScript Pocket Reference, David Flanagan (O’Reilly)
  • JavaScript: The Definitive Guide, David Flanagan (O’Reilly)
  • ECMAScript Language Specification (ECMA-262) (www.ecma.ch)

Primitive Types

Boolean

The Boolean type has two values: true and false. Boolean can be used as a function: Boolean(x) where the result is true unless x is 0, NaN, null, undefined or "" (the empty string).

Number

Numbers in JavaScript are represented internally using 64-bit floating point values - there is no integer type. Numbers may be written in the one of the following form:

  • Decimal - 10, 20.5, 30,
  • Exponent - 3e-2
  • Hexadecimal - 0xFF

JavaScript supports all conventional mathematical operators, including +, -, *, / and % (modulus). The “Math” object provides additional functionality, such as sqrt, power, sin, cos, tan, etc. Certain operations may yields errors with invalid input, for example, Math.sprt(-1) produces the result NaN - a special value which indicates the result is “Not a Number”.

String

The String type represents a sequence of characters and is delimited with either single or double quotes: “Hello World”, ‘Elixir Report’. Strings can be concatenated with the + symbol:

"Hello" + "World"

Other types are automatically converted to Strings when concatenated with them:

count = 5; message = "Hello" + count

This example also shows that multiple statements are separated by semi-colons. Strings support many additional functions, including: length, charAt, indexOf, match, replace, split, substring, etc.

Strings in JavaScript are immutable - just like in Java - once created they cannot be changed.

Compound Types

Array

Arrays are compound types which contain indexed values of any type. An array is created using the “new” keyword:

a = new Array(10)

This allocates ten slots in the array named a, that are indexed a[0] - a[9]. Arrays can also be created with initial values using an alternate syntax:

a = ["Hello}, "World", 123 ]

This creates an array named a with three slots, where a[0] is the string “Hello”, a[1] is the string “World” and a[2] is the number 123.

Object

Objects are compound types because they can contain multiple properties (named values of any type). Elixir Report Designer provides objects which represent all of the components that can be manipulated through the Report Designer. Most use of scripts in Elixir Report Designer consists of manipulating the properties of these objects during the report rendering process.

Where JavaScript is used to access Java objects, it is possible to use two alternate forms of access. The properties can be accessed through the traditional “get” and “set” methods, or access the properties directly, which JavaScript then translates into the appropriate “get” and “set” operations. For example, these two lines have identical behaviour:

component.setBackgroundColor("Red");
component.backgroundColor = "Red";

A similar situation applies for “get”:

c = component.getBackgroundColor();
c = component.backgroundColor;

Objects loaded from a datasource may sometimes have field names that are not valid names in JavaScript. Such fields can still be accessed by using a named lookup of the value. For example, if the field name is 2000 (which obviously is not a valid variable name) you can access the value with:

this["2000"]

Scriptlets

Elixir Report Designer allows small scripts to be executed to control or interact with the rendering process. Within each script, the keyword this identifies the current template component - which might be a ReportHeader, PageHeader, Field, Image, etc. The this keyword is optional in the scripts:

this.setFontColor("Red")

is equivalent to:

setFontColor("Red")

Each part of a report - from the Report object itself, down to Sections, Report Bands (e.g. a Group Header), and every component can have up to four scripts:

  • RenderIf
  • OnRenderBegin
  • OnRenderEnd
  • OnLayout

The scripts will be executed based on the order the objects are rendered. For report bands, this is document order, for components it is the order they appear in the shape tree (Z-order, from back to front). OnRenderEnd and OnLayout will occur after all child components have been rendered, so Report OnRenderBegin will be the first script to run and Report OnRenderEnd the last.

RenderIf -- except for Report
OnRenderBegin
-- any child components rendered here
OnRenderEnd
[OnLayout] -- only applies to report bands

RenderIf

RenderIf is evaluated to determine whether a component should be rendered. If the script evaluates to true, the component is rendered normally. If it evaluates to false, the component is not rendered and occupies no space in the output. Here’s a sample RenderIf script:

Parameters.get("user") == "Elixir"

This script returns true if the value of the dynamic parameter user is equal to the value Elixir, otherwise it returns false. Thus the component will only be rendered if the user is Elixir.

If no RenderIf script is defined, the default is true, then the component is rendered.

Elixir Data Designer supports the RenderIf script at the cell level. You can use the RenderIf script to control which cells will display, and the remaining cells will automatically expand to fill the gap. The cells include those in Horizontal Boxes and Vertical Boxes. To control the visibility of cells, run the Render Wizard, and then make your selections in the Dynamic Parameters window.

OnRenderBegin

OnRenderBegin is executed before each component is rendered. For example, if the report has ten details, OnRenderBegin will be called on each component in the detail section ten times, once per render of the component. The keyword this identifies s the component being rendered. Here is a sample OnRenderBegin script:

if (count%2==1)
{
    setBackgroundColor("Yellow");
}

This script checks the count variable. If the number is odd (1, 3, 5, etc.) the background colour of the component is changed to “yellow”. Note that this modified template component will be used for all further renderings, so all subsequent even renderings (2, 4, 6, etc.) will be yellow as well. That’s probably not what we intended, so it would often be advisable to include:

else
{
    setBackgroundColor("White");
}

to ensure changes to the template are not propagated to subsequent details.

OnRenderEnd

OnRenderEnd is executed after each component is rendered. This script is useful for incrementing counters and modifying the results of rendering (e.g., formatting) before the report is finalized. The keyword this still refers to the template component used to render the output. Another variable result now references the rendered component. Here’s a sample onRenderEnd script:

field = result.getLogicalElement(0);
if (field.getText() <0) result.setBackgroundColor("Red");

This script extracts the text from a field, checks whether the value is less than zero. If it is, the background colour gets set to red. Notice that JavaScript will automatically turn the field text, which is a string into a number for comparison. In this case, there is no need to provide an alternate colour for a positive result as the change is to the result element, not the template element. Changes to this affect the rest of the rendering from this template, whereas changes to result only affect the generated report.

Here is another example:

field = result.getLogicalElement(0);
field.setText(Field.getText().toUpperCase());

This script extracts the text from a field component and changes the formatted text to upper case. This cannot be done in OnRenderBegin, because the formatting is only performed during rendering. The call to getLogicalElement is required to conform to the logical RML structure.

OnLayout

OnLayout is only applicable for bands in the report. When a particular band, e.g., a Detail, has been rendered the OnRenderIf script is called. Following this, the band may be paginated, even split into multiple chunks so that it can fit within the page boundaries. As this task is done, immediately after each chunk has been added to a page, the OnLayout script in invoked. At this point, you can access this, result (the same as OnRenderEnd) and page object on which this chunk has been put.

OnLayout is useful for controlling page-based information. For example, a page total that is displayed in the page footer. It is only at the point of pagination, after OnRenderEnd, that we know what page (or pages) a particular band will be assigned to. Hence, this is the time to update page totals. The OnLayout for a chunk, will be invoked before the OnRenderBegin of the Page Footer belonging to the Page that it is placed on.

Here is an example of the sequence of scripts when the Detail band fits at the bottom of the page:

  1. Detail RenderIf (assume returns true)
  2. Detail OnRenderBegin
  3. Detail OnRenderEnd
  4. Detail OnLayout (because it fits the page)
  5. PageFooter RenderIf (assume returns true)
  6. PageFooter OnRenderBegin
  7. PageFooter OnRenderEnd

Here is the sane sequence when Detail band does not fit on the page:

  1. Detail RenderIf (assume returns true)
  2. Detail OnRenderBegin
  3. Detail OnRenderEnd (by the end of this, we know how big the detail is, and can see it would not fit)
  4. PageFooter RenderIf (assume returns true)
  5. PageFooter OnRenderBegin
  6. PageFooter OnRenderEnd
  7. PageHeader RenderIf (next page now, assume returns true)
  8. PageHeader OnRenderBegin
  9. PageHeader OnRenderEnd
  10. Detail OnLayout (delayed to here because the detail did not fit before)

Script Editor

To assist in editing scripts, the Functions panel includes a Script Editor.

The Script Editor provides access to the complete set of JavaScript objects and functions that are installed in the Repertoire release. This is a comprehensive set of APIs, which you probably only need to refer to 5% of them.

The Script Editor shows Categories, SubCategories (which are often objects) and Functions in losts acrss the bottom. Choose an item from each list, starting from the left to explore the available functions. Once the desired function or Object to use is located, double-click on it to add it to the editor at the current cursor location. The cursor will then advance to the next logical place for text entry.

Another useful feature is the “Validate” button on the right. This allows the JAvaScript syntax in the editor to be validated. As JavaScript is a weakly typed language, the check is for syntax only, not semantics. For example, by netering Data.getCount() would not remind you that getCount expects a parameter, but entering data..getCount("Name") will warn you tht you have two ‘.’ characters. This is a syntax error, whereas spellings and parameters are semantics. When validate is successful, a tick will appear next to the button, which will now say “Valid”. Any text editing will return the button to it’s original “Validate” state, allowing you to check again. Upon a validation failure, you will be given a message from the JavaScript compiler and the cursor will be placed at the point the error was detected.

Security

Several components of Elixir Repertoire such as data source and report template may embed Javascript to generate dynamic effects during report rendering.

Javascript is a powerful tool and may perform dangerous actions that will cause undesirable effects if not used properly. As a result, a user may want to control what the scripts can do. For example, javaScripts should not be allowed to write any file on the system or establish any network connection to an unknown network.

Elixir Runtime enables to user to set the exact security permissions to the Javascript. This is based on the standard Java Security Architecture. Java platform supports a policy-based, easily configurable and fine-grained access control model. Following this model, security permissions for JavaScripts can be specified in a policy file, which is enforced by Java at runtime.

The steps to protect users from malicious JavaScripts are:

  1. Find out the requirements of the application’s security.
  2. Configure the security permissions given to the application, Repertoire Runtime and JavaScripts embedded in the report templates.
  3. Run the Java application with the Security Manager.
  4. Verify that the security policies are taking effect.

To find out the application’s security requirements, check the following:

  • Security permissions that should be given to the application
    Once Security Manager is installed with the Java application, all Java classes are guarded by security policies, including your own codes. Therefore, if the application needs to perform security sensitive operations, you will need to grant those permissions explicitly.

  • Security permissions that should be given to Repertoire Runtime
    Enough permissions should be given so that report templates can be loaded from the file system, write rendered reports to hard disk, etc. successfully.

  • Security permissions that should be given to JavaScripts
    This will depend on the functionality of the scripts. For example, if one of the scripts needs to write the output to a file, you might need to grant write permission on the file (to the script) explicitly.

Configure Security Permissions

The Java Security Manager checks a file for security permissions. By default, the file is named “java.policy” under the Java runtime installation folder (${JRE_HOME}/lib/security/java.policy). You can also save your policies in a different file, but you will need to tell Security Manager the location to find the file. The file must follow certain syntax so that Security Manager can work properly.

The policy file in the “/config” directory of Elixir Repertoire (java2.policy) grants all security permissions to Java classes loaded from “/bin”, “/ext” and “/lib” directory of Elixir Repertoire. These classes are either Repertoire’s built-in classes or put in by system administrators, which makes it safe to trust (by default).

In the default policy file, JavaScripts written by the end users are allowed to:

  • Read all Java system properties, such as “java.version”
  • Read files in Repertoire installation directory
  • Read, write and delete files in OS’s “temp” directory

If the scripts try to do other security related actions (such as writing a system file import for your operating system), your scripts will fail to run and some error logs will be generated.

On Repertoire Report Designer UI, you can view console logs which are generated as you render your reports. Logs are organized into different categories and one of them is “JavascriptLog”. When your scripts fail to run for some reason, you can always check this console log to see the reason of failing.

When your script fails because it does not have certain permissions to complete the task, the log entry should look something like:

Error evaluating script: java.security.AccessControlException: access denied (<permission required>)

Verify Effects of Security Policies

The best way to verify security polocies is to write some JavaScript. To try do some operations that are not allowed and make sure the script fails.

Below is a simple example.

When designing a report template with Repertoire Report Designer, you can insert Javascript in several places.

Add the line of script into the report.

Generate the report. In the console, error logs will appear indicating that the file is unable to write to the system.

This is an expected behavior as the settings in our default policy file (java2.policy) do not allow user scripts to write a file to the system and the script is trying to write a file named “test” into the system.

JavaScript API Reference

Each object is described in terms of functions and properties. The Property identifier is used to indicate the ability to both get and set a value. For example:

Property: A : String

Indicates that the object has both String get.A() and void setA(Stringa) functions. If the return typ is a Boolean, then the naming follows the JAva convention of using “is” in place of “Get”, Boolean isA and void setA(Boolean a).

Where objects are identified as X extends Y, this indicates that all functions of Y are applicable to each X object.

Where colours are required to be provided as Strings, Elixir Report Designer supports all CSS standard names, including “Black”, “Blue”, “Cyan”, “Magenta”, etc.

this.setBackgroundColor("Red");

Alternatively, an RGB value may be supplied, with an optional alpha transparency value. Values should be in the range 0-255.

this.setBackgroundColor("rgb(255,0,0)"); // red,green,blue
this.setBackgroundColor("rgb(255,0,0,128)"); // red,green,blue,alpha

Elixir Utility Functions

To avoid possible conflicts with other libraries in future, these functions which were previously global, have been moved to a namespace elxfn. For backwards compatibility they are still also provided as global functions, but these will eventually be withdrawn. A JavaScript warning will be logged for each use of a deprecated function.

Core Object, Number and String

  • Boolean elxfn.isNull(a)
  • Boolean elxfn.isNumber(a)
  • Boolean elxfn.isString(a)
  • Boolean elxfn.isDateString(str,pattern)
    The pattern parameter is optional, default is “yyyy-MM-dd” if it is not defined.
  • Boolean elxfn.isNumberString(str)
    Indicates whether a given string contains a valid number (parsed by Java).
    An alternative pure Javascript approach is to use new Number(str)!=NaN.
  • Number elxfn.asNumber(str)
  • String elxfn.trim(str)
  • String elxfn.strip(str, ch)
  • String elxfn.leftTrim(str)
  • String elxfn.rightTrim(str)
  • String elxfn.terminate(code,message)
    It provides a scripted way to abort report rendering.
    For example, if you want the report to only render when there are data records available, you can adda script to the first SEction Header OnRenderBegin:
    if (Data.getRecordCount() ==0)
    elxfn.terminate(101,"No records available");
    The status code (101) will be parsed back to the caller through the “JobInfo”. Values less than 100 are reserved for internal use. You could also use terminate to test for valid combination of parameters.

Date

  • Date elxfn.offsetDays(origDate, numDays)
  • Date elxfn.offsetMonths(origDate, numMonths)
  • Date elxfn.offsetYears(origDate, numYears)
  • Date elxfn.offsetDate(origDate, numYears, numMonths, numDays)
  • Number elxfn.dateDiff(DateA, DateB, interval)
    The allowed interval values are: s, m, h, d (seconds, minutes, hours and days).

Utility Objects

Format Object

The format object provides functions for manipulating and formatting text. The pattern definitions are described in the Java API.

  • String padLeftAligned(String s,int width)
  • String padRightAligned(String s,int width)
  • String padCenterAligned(String s,int width)
  • String spaces(int len)
  • String formatNumber(Number n, int decimalPlaces)
  • String formatNumber(Number n, int decimalPlaces, String locale)
  • String formatNumber(Number n, String pattern)
  • String formatCurrency(Number n)
  • String formatCurrency(Number n, String locale)
  • String formatPercent(Number n, int decimalPlaces)
  • String formatPercent(Number n, String locale, int decimalPlaces)
  • String formatDate(Date d, String pattern)
  • String formatDate(Date d, String pattern, String locale)

Log Object

The Log object interfaces with the Log4J logging mechanism used throughout Elixir Report Designer.

  • void error(String msg)
  • void warn(String msg)
  • void info(String msg)
  • void debug(String msg)

Properties Object

The Parameters object provides a helper function to access report parameters. If this function is called from within a subreport it will return the subreport value for the property, but if not found there will return the parent report value instead. To set the parameter value you need to choose which report should hold the value and use the setParameter method of that RawReport (which could be either a master or a subreport). Similarly you can get a parameter from a report directly (ignoring master-subreport inheritance) by using the RawReport function getParameterValue.

Properties is deprecated and is kept for backward compatibility.

  • String get(String name)

Renderer Object

The Renderer object provides an interface to the render engine. There are two functions (eval and exec) for r invoking the JavaScript engine. These are only needed if you construct JavaScript expressions within JavaScript itself, and want to run them. Use eval when you expect a return value. Use exec when no return is needed.

  • Object eval(String src, String lang, String code)
    • src: Name of code being evaluated, displayed in error message
    • lang: Script language used, must be “javascript” at present
    • code: Code to be evaluated
    • return: Result of evaluating the script
  • void exec(String src, String lang, String code)
    • src: Name of code being evaluated, displayed in error message
    • lang: Script language used, must be “javascript” at present
    • code: Code to be evaluated
    • return: None
  • LogicalElement getLogicalElementByName()
  • LogicalReport getLogicalReport()
  • String getMimeType()
  • RawElement getRawElementByName()
  • RawReport getRawReport()
  • void pageBreak()
  • void pageBreak(boolean resetPageCount)

Data Objects

Data Object

The Data object represents the master datasource during section rendering. There are also helper methods for performing operations on named datasources. A number of the operations here return Function objects. The Function can then be applied to the desired record scope. See the Function object described below for more details.

  • int getRecordIndex()
  • int getRecordCount()
  • Object getCount(String)
  • String getString(String fieldName)
  • Object getObject(String fieldName)
  • Function getAverage(String fieldName)
  • Function getMax(String fieldName)
  • Function getMin(String fieldName)
  • Function getStandardDeviation(String fieldName)
  • Function getSum(String fieldName)
  • Function getVariance(String fieldName)
  • Function getAverage(String ds,String fieldName)
  • Function getMax(String ds,String fieldName)
  • Function getMin(String ds,String fieldName)
  • Function getStandardDeviation(String ds,String fieldName)
  • Function getSum(String ds,String fieldName)
  • Function getVariance(String ds,String fieldName)

DataCache Object

Each DataCache object holds a data table in memory along with an index to the current record in the table. A DataCache is created by the DataCacheManager or by filtering an existing DataCache.

  • void reset()
  • void reset(Properties props)
  • int getRecordCount()
  • int getRecordIndex()
  • boolean hasNext()
  • void next()
  • int getColumnCount()
  • String getColumnName(int index)
  • int getColumnIndex(String fieldname)
  • Object getObject(int rowIndex, String fieldname)
  • Object getObject(int rowIndex, int columnIndex)
  • void moveToRow(int rowIndex)
  • Object getObject(String fieldname)
  • String getString(String fieldname)
  • int seekTo(String fieldname, Object value)
  • DataCache filter(String fieldname, Object value)

DataCacheManager Object

The DataCacheManager provides an interface to load or discard DataCaches from memory.

  • DataCache loadCache(String dsnname, Properties props)
  • DataCache loadCache(String dsnname, Object cacheName, Properties props)
  • boolean hasCache(Object cacheName)
  • DataCache getCache(Object cacheName)
  • void putCache(Object newCacheName, DataCache cache)
  • void removeCache(Object cacheName)

Function Object

A Function is returned by the Data object to represent the different operations that can be performed on data, e.g., getSum(fieldName). Given the function, you can execute it by specifying the range of records you want to be included in the result. For example,

Data.getSum("Salary").getValueOverall();
  • Object getValueOverAll()
  • Object getValueOverGroup()
  • Object getRunningValueOverAll()
  • Object getRunningValueOverGroup()

GroupNode Object

A GroupNode represents a range of records. typically as a result of a grouping operation. GroupNodes form a tree structure with a single root which encompasses the whole datasource, which is then subdivided into child groups recursively, based on the section grouping configuration.

  • boolean hasChildren()
  • Iterator getChildIterator() // child GroupNodes
  • int getStart() // first index in group
  • int getStop() // last index in group
  • GroupNode getParent() // null if topmost group

Raw Report Objects

The current raw object is accessed as this in the RenderIf, OnRenderBegin, OnRenderEnd and OnLayout scriptlets.

Logical Report Objects

Logical Report objects are only accessible in onRenderEnd and onLayout. They are identified using the result variable. Most raw elements are converted to more than one logical element. For example, a raw Field is rendered as a logical Rectangle which contains one or more logical Text objects, one for each line of output. Accessing the Text within the result is illustrated (in the OnRenderEnd example above) like this:

field = result.getLogicalElement(0);

Where field is the first line of text in the result rectangle. You can query the number of child elements with:

count = result.getLogicalElementCount();

Wherever possible work with the raw objects, rather than the logical ones, as the dealing with logical objects requires an intimate knowledge of the rendering process and logical objects are more likely to change as new features are added.