Self-configuration for Smart Office Scripts

Here is a trivial solution to implement Personalized Scripts for Lawson Smart Office that self-configure based on the program they are being executed in.

Problem

We often implement scripts which functionality can be applied to different M3 programs. For example, a script that makes a phone call based on the phone number displayed on the screen could be applied to any M3 program that has a phone number (Customers, Suppliers, etc.). As another example, a script that shows an address on Google Maps could be applied to any M3 program that has an address (CRS610/E, CRS622/E, etc.).

We could make one copy of the script for each target M3 program, but that would be a maintenance nightmare.

We could make the script re-usable and pass settings to the script, but that would require the installer to manually define the settings, which is time consuming and error prone.

As a general problem, I want to make a script re-usable and with zero configuration so it can be used anywhere in M3 where its functionality is needed.

Solution

The trivial solution is to pre-compute all the possible settings in advance, and apply the corresponding settings at runtime. We dynamically determine in which program we are currently executing the script by reading the HostTitle variable.

I call it a self-configuration script.

Pseudo-code:

HostTitle = controller.RenderEngine.Host.HostTitle
if (HostTitle = X) then { SettingsX }
if (HostTitle = Y) then { SettingsY }
if (HostTitle = Z) then { SettingsZ }

Advantages

The advantage is reduced installation. In some cases we could completely eliminate configuration.

It’s also time saving, and error proof.

And it’s closer to plug’n play and Autonomic Computing.

Example

I had implemented a script that performs address validation.

The script checks the address that is entered by the user with a third-party software that validates if the address is correct or not.

The script needs to get a reference to the address fields: Address line 1 (CUA1), Address line 2 (CUA2), City (TOWN), State (ECAR), etc. In CRS610/E those fields will be WRCUA1, WRCUA2, WRTOWN, and WRECAR. Whereas in CRS622/E those fields will be WWADR1, WWADR2, WWTOWN, and WWECAR.

The fields are different in each M3 program. So I pre-computed the field names of the eight M3 programs and panels where I would be executing the script (CRS610/E, CRS235/E1, CRS300/E, CRS622/E, MNS100/E, OIS002/E, OPS500/I, and SOS005/E), and I hard-coded all the possible values in the script.

Sample source code

Here is part of the source code of my self-configuration script for address validation:

var HostTitle = controller.RenderEngine.Host.HostTitle;
var settings = {};

if (HostTitle.IndexOf('CRS610/E') > 0) {
     // Customer. Open - CRS610/E
     settings = {
         FirmName: 'WRCUNM',
         AddressLine1: 'WRCUA1',
         AddressLine2: 'WRCUA2',
         AddressLine3: 'WRCUA3',
         AddressLine4: 'WRCUA4',
         City: 'WRTOWN',
         State: 'WRECAR',
         PostalCode: 'WRPONO',
         Country: 'WRCSCD',
         Latitude: '',
         Longitude: ''
     };
} else if (HostTitle.IndexOf('CRS622/E') > 0) {
     // Supplier. Connect Address - CRS622/E
     settings = {
         FirmName: 'WWSUNM',
         AddressLine1: 'WWADR1',
         AddressLine2: 'WWADR2',
         AddressLine3: 'WWADR3',
         AddressLine4: 'WWADR4',
         City: 'WWTOWN',
         State: 'WWECAR',
         PostalCode: 'WWPONO',
         Country: 'WWCSCD',
         Latitude: 'WEGEOY',
         Longitude: 'WEGEOX'
     };
} else if (HostTitle.IndexOf('OIS002/E') > 0) {
     // Customer. Connect Addresses - OIS002/E
     settings = {
         FirmName: 'WRCUNM',
         AddressLine1: 'WRCUA1',
         AddressLine2: 'WRCUA2',
         AddressLine3: 'WRCUA3',
         AddressLine4: 'WRCUA4',
         City: 'WRTOWN',
         State: 'WRECAR',
         PostalCode: 'WRPONO',
         Country: 'WRCSCD',
         Latitude: '',
         Longitude: ''
     };
} else if (HostTitle.IndexOf('CRS235/E1') > 0) {
     // Internal Address. Open - CRS235/E1
     settings = {
         FirmName: 'WWCONM',
         AddressLine1: 'WWADR1',
         AddressLine2: 'WWADR2',
         AddressLine3: 'WWADR3',
         AddressLine4: 'WWADR4',
         City: 'WWTOWN',
         State: 'WWECAR',
         PostalCode: 'WWPONO',
         Country: 'WWCSCD',
         Latitude: 'WEGEOY',
         Longitude: 'WEGEOX'
     };
} else if (HostTitle.IndexOf('MNS100/E') > 0) {
     // Company. Connect Division - MNS100/E
     settings = {
         FirmName: 'WWCONM',
         AddressLine1: 'WWCOA1',
         AddressLine2: 'WWCOA2',
         AddressLine3: 'WWCOA3',
         AddressLine4: 'WWCOA4',
         City: 'WWTOWN',
         State: 'WWECAR',
         PostalCode: 'WWPONO',
         Country: 'WWCSCD',
         Latitude: '',
         Longitude: ''
     };
} else if (HostTitle.IndexOf('CRS300/E') > 0) {
     // Ship-Via Address. Open - CRS300/E
     settings = {
         FirmName: 'WWCONM',
         AddressLine1: 'WWADR1',
         AddressLine2: 'WWADR2',
         AddressLine3: 'WWADR3',
         AddressLine4: 'WWADR4',
         City: 'WWTOWN',
         State: 'WWECAR',
         PostalCode: 'WWPONO',
         Country: 'WWCSCD',
         Latitude: '',
         Longitude: ''
     };
} else if (HostTitle.IndexOf('SOS005/E') > 0) {
     // Service Order. Connect Delivery Address - SOS005/E
     settings = {
         FirmName: 'WPCONM',
         AddressLine1: 'WPADR1',
         AddressLine2: 'WPADR2',
         AddressLine3: 'WPADR3',
         AddressLine4: 'WPADR4',
         City: 'WPTOWN',
         State: 'WPECAR',
         PostalCode: 'WPPONO',
         Country: 'WPCSCD',
         Latitude: '',
         Longitude: ''
     };
} else if (HostTitle.IndexOf('OPS500/I') > 0) {
     // Shop. Open - OPS500/I
     settings = {
         FirmName: 'LBL_L21T2',
         AddressLine1: 'WICUA1',
         AddressLine2: 'WICUA2',
         AddressLine3: '',
         AddressLine4: '',
         City: 'WICUA3',
         State: '',
         PostalCode: '',
         Country: 'WICUA4',
         Latitude: '',
         Longitude: ''
     };
} else {
     // M3 panel not supported
}

This has been tested in Lawson Smart Client (LSC), and in Lawson Smart Office (LSO).

Additionally, you can discriminate LSC vs. LSO with:

if (Application.Current.MainWindow.Title == 'Lawson Smart Client') {
 // running in LSC (not LSO)
}

That’s it!

UPDATE

UPDATE 2012-07-24: We can also use controller.RenderEngine.PanelHeader to get just the program and panel (for example: CRS610/B1) instead of the entire host title; the result is a shorter syntax:

var PanelHeader = controller.RenderEngine.PanelHeader;
if (PanelHeader == 'CRS610/E') {
   // CRS610/E
} else if (PanelHeader == 'CRS622/E') {
   // CRS622/E
} else if (PanelHeader == 'OIS002/E') {
   // OIS002/E
} else if (PanelHeader == 'CRS235/E1') {
   // CRS235/E1
} else {
   // not supported
}

How to pass settings to a script

In this post I discuss several techniques to pass settings to a Personalized Script for Lawson Smart Office.

Suppose we have three settings: fieldX, and Y that we want to pass to a script with the respective values WRPHNO, 33, and 17. The question is how do we pass those settings and their values to the script? I can think of five techniques: 1) hard-code the settings in the source code, 2) use comma separated values and String.Split, 3) use JSON, 4) use XML, or 5) use Java Properties or .NET Resource files.

1. Hard-code the settings

The first technique is to hard-code the settings as Fields in the class declaration of the script:

var field = "WRPHNO";
var X = 33;
var Y = 17;

Here is an example:

Or we can use an Array:

var settings = ["WRPHNO", 33, 17];
settings[0] // field
settings[1] // X
settings[2] // Y

Here is an example with the Array:

2. Use comma separated values and String.Split

The second technique is to pass a comma separated list of values like WRPHNO,33,17 as the argument of the script, and to access the values as an array after a String.Split:

var settings = args.Split(",");
settings[0] // field
settings[1] // X
settings[2] // Y

Here is an example:

We can also read the values from a file:

var settings = System.IO.File.ReadAllText("C:\\path\\settings.txt").Split(",");

3. Use JSON

The third technique is to use JSON. With JSON we write our settings in object literal notation like this:

{ "field": "WRPHNO",
 "X": 33,
 "Y": 17 }

We set the JSON text as the argument of the script, surrounded by parenthesis, and we call eval(JSON) from the script. The settings become a JScript object that we can access with settings.field, settings.X, and settings.Y. Here is an example:

We can also read the JSON text from a file:

var JSON = System.IO.File.ReadAllText("C:\\path\\settings.txt");
var settings = eval("("+JSON+")");

4. Use XML

The fourth technique is to pass XML as the argument of the script, and to use XPath in SelectSingleNode to access the values:

var doc = new XmlDocument();
doc.LoadXml(args);
doc.SelectSingleNode("/settings/field").InnerText
doc.SelectSingleNode("/settings/X").InnerText
doc.SelectSingleNode("/settings/Y").InnerText

Here is an example:

We can also load the XML from a file:

doc.Load("file://hostname/settings.xml");

5. Use Properties or Resource files

The fifth technique is to use Java Properties files or .NET Resource files, but I couldn’t find a concise solution that fits in only a couple of lines of code; it seems to require many lines of code.

UPDATE 2012-12-14: I would copy/paste my properties file to the Smart Office installation point folder on the web server, and refer to it from the script as http://smartoffice/LSO/thibaud.properties using standard .NET classes to read files. If you have an example let me know and I can post it here.

Discussion

The hard-coded values are simple to implement for the developer, but it makes the script non reusable by nature, and it invites a risk of corruption when the administrator has to manually edit the source code to change the settings.

The comma separated list of values and String.Split is also simple to implement for the developer, but maintenance is inversely proportional to scalability: the more settings we add the harder it becomes to identify which value is located where in the string. Also, if by mistake a comma goes missing the whole settings are compromised.

Using JSON is great because of the possibilities to validate the JSON object with JSlint and JSONLint. Also, it’s scalable: we can store a large number of settings in complex structures like in sub-settings or in a tree of settings and still maintain readability.

Using XML is great for all the advantages of XML, like editing tools and semantic validation.

Settings page

I encourage implementing a settings page as an HTML form for the administrators to easily configure the script and save the settings in the XML Customization file in Smart Office.

In our example, and with JSON, we would have the following HTML form:

<input id="field"/>

We create a JSON string in JavaScript like this:

var settings = {};
settings.field = document.getElementById("field").value;
settings.X = document.getElementById("X").value;
settings.Y = document.getElementById("Y").value;
var JSONtext = JSON.stringify(settings);

The JSON object is provided as a JS file by Douglas Crockford here, and it is also natively supported in modern web browsers like Microsoft Internet Explorer 8 and in Google Chrome.

Our settings page would look like this:

The source code for that simple settings page is:

// <![CDATA[
javascript" src="https://raw.github.com/douglascrockford/JSON-js/master/json2.js">
// ]]>// Field  X  Y  

Here are two sample web pages that use JSON for passing plenty of settings to a script to be stored in the XML Customization file:

Sample 1: http://ibrix.info/AddressValidation/settings/

Sample 2: http://ibrix.info/skype/settings/

Customization file

Once the settings are generated, either as a comma separated list of values, or as JSON text, or as XML, we can save the result in the argument attribute of the Customization file in Smart Office. The value must be XML escaped with for example this online tool.

Here is an example of an XML Customization file for a script AddressValidationM3 for CRS610/E with plenty of XML-escaped JSON as the argument:

For more information on XML Customization files in Smart Office, refer to the Chapter Managing M3 MForms Personalizations of the Lawson Smart Office Administration Guide: