- Post History
- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
12-23-2021 05:02 AM - edited 12-24-2022 10:38 PM
One of the most important script type at ServiceNow is the "Script Include". It is like a library where you can put all the business logic of your application in to have one single place for maintenance. An experienced developer first designs a Script Include and then tests it extensively before using its methods in other types of scripts like Business Rules, Reference Qualifiers, Scheduled Jobs, etc. An interesting aspect of Script Includes is that they can be called not only on the server side, but also on the client side via an Ajax-based request. Due to their immense importance, this article presents a generally applicable implementation pattern for Script Includes that covers all invocation variants and also has a standard structure to support clean and maintainable code.
|
|
Challenge
In one of my projects, a new Catalog Item was required where the customer wanted to enter an amount of money and select the related currency. That amount then should be converted to the currency "Euro" on the basis of a daily updated exchange rate. And the conversion should be performed on client-side because of other reasons that do not play a role here for the time being.
Considering these constraints, I was faced with two problems:
- ServiceNow does not offer a numeric data type for catalog variables.
- On the one hand, the currency API accepts only the English notation for numbers - with a dot as decimal separator and commas as grouping separator (1,000.00). On the other hand, we mainly had users who wanted to enter the numbers in the European variant, i.e. with a comma as decimal separator and dots as grouping separator (1.000,00).
Solution
Fortunately the conversion is no problem as ServiceNow provides the proper APIs. And also conversions of number formats based on a locale can be done via that API. Due to the fact that these APIs are offered only on the server side, a Script Include is needed which can be called from the client side. But during the implementation and for testing purposes, I wanted to be able of reusing the business logic on the server side too. In such scenarios, a common approach is creating a "proxy" Script Include which is client callable and redirects all invocations to another Script Include holding the business logic:
You can identify these Script Includes by their name, as they often end in "Ajax":
I am not a fan of this kind of code redundancy because the maintenance efforts are correspondingly high and also the defect potential increases accordingly.
For this reason, I prefer the approach with only one script include, which can not only be invoked from client side but also contains all the business logic.
Usage
On Server Side
In the following example, all possible parameters are passed to the convert() method. With the explicitly given locale de_DE the number format can be interpreted accordingly:
On Client Side
For testing purposes I recommend installing my GlideAjax Test Client. With the help of this Catalog Item you can simply click together the invocation of the method CurrencyUtils.convert() and get the corresponding source code at the end.
But you also can copy the following code block:
var gaRequest = new GlideAjax('CurrencyUtils');
gaRequest.addParam('sysparm_name', 'convert');
gaRequest.addParam('sysparm_source_currency', 'USD');
gaRequest.addParam('sysparm_target_currency', 'EUR');
gaRequest.addParam('sysparm_original_amount', '112,45');
gaRequest.addParam('sysparm_locale', 'de_DE');
gaRequest.getXMLAnswer(function(strAnswer) {
var jsonAnswer = JSON.parse(strAnswer);
if (jsonAnswer.error_message.length > 0) {
alert(jsonAnswer.error_message);
}
else {
alert(
jsonAnswer.original_amount + ' ' + jsonAnswer.source_currency + ' make ' +
jsonAnswer.converted_amount + ' ' + jsonAnswer.target_currency +
' at an exchange rate of ' + jsonAnswer.exchange_rate
);
}
});
Source Code explained
You can download the complete source code of the Script Include right from my public GitHub repo: CurrencyUtils.txt
The referenced LogHelper class is described in detail in my article Just another Log Helper.
(1) Constant Definitions
At the beginning of a Script Include you should define some constants, which will be used later. Constants help to make the code more maintainable and can tell you what the required parameter names should be when you first look at the source code. Constant names should be all uppercase, with words separated by underscores ("_"). In ServiceNow, they also should be prepended with the class name and separated with a dot in order to indicate which class the constants belong to.
var CurrencyUtils = Class.create();
CurrencyUtils.METHOD_CONVERT = 'CurrencyUtils.convert';
CurrencyUtils.METHOD_PARAMETER_SOURCECURRENCY = 'strSourceCurrency';
CurrencyUtils.METHOD_PARAMETER_TARGETCURRENCY = 'strTargetCurrency';
CurrencyUtils.METHOD_PARAMETER_ORIGINALAMOUNT = 'strOriginalAmount';
CurrencyUtils.METHOD_PARAMETER_LOCALE = 'strLocale';
CurrencyUtils.AJAX_PARAMETER_SOURCECURRENCY = 'sysparm_source_currency';
CurrencyUtils.AJAX_PARAMETER_TARGETCURRENCY = 'sysparm_target_currency';
CurrencyUtils.AJAX_PARAMETER_ORIGINALAMOUNT = 'sysparm_original_amount';
CurrencyUtils.AJAX_PARAMETER_LOCALE = 'sysparm_locale';
Note:
In larger projects with many artifacts, it may make sense to create a Script Include that exists exclusively for the purpose of specifying constants. The names of such Script Includes often end in "Const". If you want to learn more regarding that topic, please watch the following video: https://www.youtube.com/watch?v=B94UUQPDyDg
(2) Extending AbstractAjaxProcessor
This definition enables your Script Include to handle client-side calls:
CurrencyUtils.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {
Note:
- Please be aware that you can't define an initialize() method here! As a consequence, when instantiating via the new operator, you have no constructor via which initialization values can be passed. However, you can initialize values as usual via setter methods
- Make sure that also the checkbox "Client callable" in the Script Include record is set:
(3) Public Methods
In this part of the Script Include you define all public methods, i.e. those that can be invoked from outside the class by other scripts.
(3.1) Method definition
According to the "camel case" naming conventions, methods should be verbs, in mixed case with the first letter lowercase and with the first letter of each internal word capitalized. Inside the round brackets the parameter names are listed for which corresponding values must be passed to the method. The parameter names also follow the "camel case" notation. A prefix, which represents the expected data type ("str"), is not mandatory, but helps tremendously when reading the source code.
Note:
The parameter names only matter when calling the method from another server-side script. For a client-side invocation, the parameter passing is done differently, as we will see later.
/**
* Converts a number for a given currency to the value of a target currency with the help of an OOTB
* ServiceNow API. The conversion rates are determined automatically on a daily basis by ServiceNow.
*
* {String} strSourceCurrency
* Original currency of the given amount to be converted as a 3-letter code
* defined by the <a href="https://en.wikipedia.org/wiki/ISO_4217">ISO 4217</a>.
* {String} strTargetCurrency
* Target currency the given amount should be converted to as a 3-letter code
* defined by the <a href="https://en.wikipedia.org/wiki/ISO_4217">ISO 4217</a>.
* @return {JSON}
* Value object containing all information. In casde of errors the property `error_message`
* contains a respective message.
*/
convert: function(strSourceCurrency, strTargetCurrency, strOriginalAmount, strLocale) {
Above the single line of code you can see a comments block which follows a certain notation. This form is based on the standards for commenting Java code. At first glance, this may seem unreadable, but there are ways to have HTML-based documentation generated from these code comments (see https://jsdoc.app/).
Code documentation is extremely important, because it helps the developer to understand how the method is used, which method parameters are passed and which return value is to be expected, and you should follow these conventions.
(3.2) Local constants
Right at the beginning of the method body and outside the following try-catch block, I define the local constants. Of central importance here is the constant IS_CLIENT_CALL, which indicates whether the method invocation was made from the client side or from the server side.
var IS_CLIENT_CALL = JSUtil.notNil(this.getName());
var SESSION_LOCALE = String(GlideLocale.get().getCurrent());
var PARAMETER_SOURCECURRENCY =
IS_CLIENT_CALL ?
CurrencyUtils.AJAX_PARAMETER_SOURCECURRENCY :
CurrencyUtils.METHOD_PARAMETER_SOURCECURRENCY;
var PARAMETER_TARGETCURRENCY =
IS_CLIENT_CALL ?
CurrencyUtils.AJAX_PARAMETER_TARGETCURRENCY :
CurrencyUtils.METHOD_PARAMETER_TARGETCURRENCY;
var PARAMETER_ORGINALAMOUNT =
IS_CLIENT_CALL ?
CurrencyUtils.AJAX_PARAMETER_ORIGINALAMOUNT :
CurrencyUtils.METHOD_PARAMETER_ORIGINALAMOUNT;
var PARAMETER_LOCALE =
IS_CLIENT_CALL ?
CurrencyUtils.AJAX_PARAMETER_LOCALE :
CurrencyUtils.METHOD_PARAMETER_LOCALE;
(3.3) Helper for Building the Return Values
For public methods I always return an object with several values and the local method _getReturnObject() helps to build that object. Depending on whether a client-side or server-side invocation was performed the stringified version or the native object is returned.
Note:
- The LogHelper referenced here is described in detail in my article Just another Log Helper.
- arguments[0], arguments[1] and so on is another way to grab the method parameters.
- With commands like String(arguments[0] || '') it is ensured that a defined String value is returned.
- For local variables and methods I prefer a notation with a leading underscore. That way I can better distinguish between global and local variables.
var _getReturnObject = function() {
var _objReturn = {
error_message: String(arguments[0] || ''),
original_currency: String(arguments[1] || ''),
target_currency: String(arguments[2] || ''),
original_amount: String(arguments[3] || ''),
converted_amount: String(arguments[4] || ''),
exchange_rate: String(arguments[5] || ''),
used_locale: String(arguments[6] || ''),
};
var _strReturn = JSON.stringify(_objReturn);
LogHelper.debug(CurrencyUtils.METHOD_CONVERT, 'Return Values: {0}', _strReturn);
return IS_CLIENT_CALL ? _strReturn : _objReturn;
};
(3.4) Determination of the parameter values
This is the most critical part, because here the parameter values are pulled from the respective source.
Note:
The forced conversion to a string using String() is very important: When passed as a method parameter, in contrast to real high-level languages, no type can be enforced and in case of a client-side call, the invocation of this.getParameter() always returns an object and not a string value as expected.
try {
var _strOriginalCurrency =
String(strOriginalCurrency ||
this.getParameter(CurrencyUtils.AJAX_PARAMETER_ORIGINALCURRENCY)).trim();
var _strTargetCurrency =
String(strTargetCurrency ||
this.getParameter(CurrencyUtils.AJAX_PARAMETER_TARGETCURRENCY)).trim();
var _strOriginalAmount =
String(strOriginalAmount ||
this.getParameter(CurrencyUtils.AJAX_PARAMETER_ORIGINALAMOUNT)).trim();
var _strLocale =
String(strLocale ||
this.getParameter(CurrencyUtils.AJAX_PARAMETER_LOCALE) || _strSessionLocale).trim();
(3.5) Debug logging
Before further processing, it can be helpful to generate a log output in order to better understand which parameter values were passed in case of an error.
LogHelper.debug(
CurrencyUtils.METHOD_CONVERT,
'Entering method with:\n<{0}> = >{1}<\n<{2}> = >{3}<\n<{4}> = >{5}<\n<{6}> = >{7}<',
PARAMETER_SOURCECURRENCY, _strSourceCurrency,
PARAMETER_TARGETCURRENCY, _strTargetCurrency,
PARAMETER_ORGINALAMOUNT, _strOriginalAmount,
PARAMETER_LOCALE, _strLocale
);
(3.6) Plausibility checks
The most extensive part is checking for valid parameter values. The purpose is to cover as many cases as possible that could prevent further processing. This includes not only the check for missing values, but also whether the parameter values are technically correct. In the case of invalid parameter values, it is critical to return clear error messages that can be processed further on the consumer side accordingly.
// source currencey code is not valid
if (!this._isValidCurrencyCode(_strSourceCurrency)) {
return _getReturnObject(
LogHelper.error(
CurrencyUtils.METHOD_CONVERT,
'>{0}< is not a valid currency code at <{1}>!',
_strSourceCurrency, PARAMETER_SOURCECURRENCY
)
);
}
// target currencey code is not valid
if (!this._isValidCurrencyCode(_strTargetCurrency)) {
return _getReturnObject(
LogHelper.error(
CurrencyUtils.METHOD_CONVERT,
'>{0}< is not a valid currency code at <{1}>!',
_strTargetCurrency, PARAMETER_TARGETCURRENCY
)
);
}
// locale has not the required format
if (!new RegExp('^[a-z]{2}_[A-Z]{2}$').test(_strLocale)) {
return _getReturnObject(
LogHelper.error(
CurrencyUtils.METHOD_CONVERT,
'Value >{0}< at <{1}> does not represent a valid format for a locale identifier!',
_strLocale, PARAMETER_LOCALE)
);
}
// normalize amount to english format
var _strNormalizedOriginalAmount = this._getNormalizedNumber(_strOriginalAmount, _strLocale);
// amount is not a valid number for the given locale
if (_strNormalizedOriginalAmount == null) {
return _getReturnObject(
LogHelper.error(
CurrencyUtils.METHOD_CONVERT,
'Value >{0}< at <{1}> does not represent a valid number for the locale >{2}<!',
_strOriginalAmount, PARAMETER_ORGINALAMOUNT, _strLocale)
);
}
The following example of a wrong currency code demonstrates how the return values look like:
(3.7) Currency Conversion
After a long way, the conversion of the given amount into the target currency can finally be performed. And even here, problems can occur and must be handled accordingly.
// preconfigure the currency converter
var gcvConverter = new sn_currency.GlideCurrencyConverter(_strSourceCurrency, _strTargetCurrency);
// set the value to be converted
gcvConverter.setAmount(_strNormalizedOriginalAmount);
// perform currency conversion
var _gcevConversionResult = gcvConverter.convert();
// something went wrong
if (_gcevConversionResult == null) {
return _getReturnObject(
LogHelper.warn(
CurrencyUtils.METHOD_CONVERT,
'Value >{0}< for orignal currency >{1}< could not be converted to target currency >{2}<',
_strOriginalAmount, _strSourceCurrency, _strTargetCurrency
)
);
}
(3.8) Return all values
In case of a successful conversion, all values are bundled by _getReturnObject() and returned to the caller.
return _getReturnObject(
'',
_strSourceCurrency,
_strTargetCurrency,
_strOriginalAmount,
this._getDenormalizedNumber(_gcevConversionResult.getAmount(), _strLocale),
this._getDenormalizedNumber(_gcevConversionResult.getRate(), _strLocale, 6),
_strLocale
);
(3.9) Handling of unexpected errors
The try-catch block is your safety net that catches all the errors you didn't think of during development. And also a proper error handling is necessary to complete the method call successfully.
}
catch (e) {
return _getReturnObject(
LogHelper.fatal(CurrencyUtils.METHOD_CONVERT, 'Unexpected error!', e)
);
}
(4) Private Helper Methods
At the end of a class I always place all methods which are only used internally. Because of the leading underscore "_" these methods cannot be invoked from outside the class and thus are defined as "private".
For this reason, the validation of the method parameters can be omitted, since it is ensured from the internal caller side that the values passed are valid.
_isValidCurrencyCode: function(strCurrencyCode) {
try {
var _gcp = new sn_currency.GlideCurrencyParser();
_gcp.setDefaultCurrencyCode(strCurrencyCode);
_gcp.parse("1");
return true;
}
catch (e) {
return false;
}
},
_getNormalizedNumber: function(strNumber, strLocale) {
var _gcpParser = new sn_currency.GlideCurrencyParser();
var _arrLocale = strLocale.split('_');
try {
_gcpParser.setLocale(_arrLocale[0], _arrLocale[1]);
_gcpParser.setDefaultCurrencyCode("USD");
var _gcvParsedValue = _gcpParser.parse(strNumber);
return _gcvParsedValue.getAmount();
}
catch (e) {
return null;
}
},
_getDenormalizedNumber: function(strNumber, strLocale, numFractionDigits) {
try {
var _gcfCurrencyFormatter = new sn_currency.GlideCurrencyFormatter("%v");
var _arrLocale = strLocale.split('_');
var _parsedInt = parseInt(numFractionDigits, 10);
var _numFractionDigits = isNaN(_parsedInt) ? 2 : Math.abs(_parsedInt);
_gcfCurrencyFormatter.setLocale(_arrLocale[0], _arrLocale[1]);
return _gcfCurrencyFormatter.setMaxFractionDigits(_numFractionDigits).format(strNumber, 'USD');
}
catch (e) {
return null;
}
},
- 6,530 Views
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Maik,
In the source code CurrencyUtils, we have the variable "LogHelper". Where is it declared? or is it an SN object?
Thank you.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Hi
thanks for your question.
I have mentioned in the article that this class is described in my article Just another Log Helper, but it seems I have to promote that fact a bit more. I will check how to modify my article.
Kind regards
Maik
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Thanks Maik. You can put a comment in the CurrencyUtils with a link to the LogHelper.
I like the concept of having single file to handle both ajax and non-ajax calls.