Creating custom classes in automation scripts

September 5, 2023

Introduction

We have spent the last few years progressively building tools and processes for managing and developing automation scripts. Since automation scripts are usually positioned as a configuration, the question arises, why not just use Migration Manager and the other tools provided with Maximo? The simple answer is that automation scripts are not configuration, but rather are customizations that should be treated with the rigor and seriousness of any development activity. That the language is JavaScript or Jython and is editable in a text box in Maximo instead of being compiled to a Java class merely provides the dangerous illusion of configuration.

If you are not using source control to store and control the release of your automation scripts, combined with tight procedures for migration and promotion, this is a situation far worse than having Java customizations. Automation scripts allow full access to every function in Maximo, the database and in many cases the underlying operating system. This means that anyone with access to automation scripts also has full, unaudited access to the entire system since with full database and operating system access, audit tables and other such measures are easily bypassed.

I am not arguing that automation scripts are bad. There are many great things about automation scripts. You can quickly build, test and deploy solutions without downtime or system disruptions and generally the skill level needed to do so is much lower, making Maximo develop accessible to more people. However, they are still a customization and need to be taken as seriously as any Java customization. If there are lingering doubts that automation scripts are the same as Java, automation scripts even allow you to implement custom Java classes.

To demonstrate this point, in this post we are going to explore how to implement custom Java classes in an automation script. It's true, custom Java classes never went away, they have just been cleverly disguised and repackaged in a more trendy form.

Extending classes

For our first example we will extend the java.io.InputStream abstract Java class. To keep the example simple we will create a new class named IncrementInputStream that returns an ever incrementing number every time the read() function is called. We will then create a loop to print 10 consecutive numbers to the log.

JavaScript

To extend a Java class in JavaScript use the the built in Java.extend function. The extend function takes a Java type object as the first argument and the class implementations in JavaScript as the second. Here we have extended the InputStream and defined a member variable of number and the necessary read() function to return the incrementing number member variable.

Note that in JavaScript, all objects are a map of properties and as such the number variable and read() function are properties of the object map.

// Import the Java type InputStream
InputStream = Java.type("java.io.InputStream");
// Extend the InputStream Java type object and implement the read function.
IncrementInputStream = Java.extend(InputStream, {
number: 0,
read: function () {
return this.number++;
},
});
// Call the main function
main();
// The main function as a defined script entry point.
function main() {
// Create a new instance of the custom IncrementInputStream.
var inputStream = new IncrementInputStream();
// Loop 10 times, printing the output to the log.
for (var i = 0; i < 10; i++) {
// we are logging to the error log to make ensure it shows up in the log.
// the prefix of "" ensures that the read byte is automatically cast to a String.
service.log_error("" + inputStream.read());
}
}
var scriptConfig = {
"autoscript": "INCREMENT",
"description": "Print an incrementing number to the log.",
"version": "1.0.0",
"active": true,
"logLevel": "ERROR",
};

Python

In Python the custom class is subclassed as any other Python class would be, the only difference being that in this case the object being extended is a Java class. You define a class with the new class name that takes an argument of the class to extend. The new class implements the __init__(self) function, which is called when the class is created and allows us to define the number member variable on the self object, which is then provided by the Jython platform. The read(self) function likewise provides the self parameter, that allows us to access the number variable.

# Import the Java type InputStream
from java.io import InputStream
class IncrementInputStream(InputStream):
def __init__(self):
self.number = 0
def read(self):
currentNumber = self.number
self.number += 1
return currentNumber
# The main function as a defined script entry point.
def main():
# Create a new instance of the custom IncrementInputStream.
inputStream = IncrementInputStream()
i = 0
# Loop 10 times, printing the output to the log.
while i < 10:
# we are logging to the error log to make ensure it shows up in the log.
service.log_error(str(inputStream.read()))
i += 1
# Call the main function
main()
scriptConfig = """{
"autoscript": "INCREMENT",
"description": "Print an incrementing number to the log.",
"version": "1.0.0",
"active": true,
"logLevel": "ERROR"
}"""

Implementing Interfaces

Automation scripts can also implement Java interfaces. For our second example will implement the java.lang.Runnable interface to create a custom runnable object that can be run in a background thread. The new BackgroundRunnable object will write a message to the log every 10 seconds for a minute after the script has been invoked to demonstrate that it is running in the background.

Note that this process starts when the script it called and continues to run in the background for one minute after the script was invoked. While this example completes after a minute, this could be used to run a background process indefinitely if desired.

JavaScript

JavaScript handles implementing interfaces in the same way as extending a class, the built in Java.extend function is again used. In this case the Java java.lang.Runnable type is provided as the first argument and the interface implementation as the second. Interface implementations have access to the script local variables, but do not have access to the script's implicit variables such as the service implicit variable. For this reason we assign the service variable to the local localService variable so that the interface implementation has access to the service object after the script has executed.

Runnable = Java.type("java.lang.Runnable");
Thread = Java.type("java.lang.Thread");
main();
function main() {
var localService = service;
BackgroundRunnable = Java.extend(Runnable, {
run: function () {
for (var i = 0; i < 6; i++) {
localService.log_error("Printing message from background thread.");
Thread.sleep(10000);
}
},
});
// Create a new thread with a new instance of the BackgroundRunnable and then start the process.
new Thread(new BackgroundRunnable()).start();
}
var scriptConfig = {
"autoscript": "BACKGROUND",
"description": "A background thread that prints a log message every 10 seconds for a minute.",
"version": "",
"active": true,
"logLevel": "ERROR",
};

Python

The implementation of the java.lang.Runnable interface in Python is also the same as extending a class. In this case we are add the service and Thread objects to the class __init__ function so they can be passed from the script's context to the new BackgroundRunnable class. After defining the new class, we create a new instance of the BackgroundRunnable object with a reference to the implicit service variable and script scoped Thread import, pass that to a new Thread object and start the thread running in the background.

from java.lang import Runnable, Thread
class BackgroundRunnable(Runnable):
def __init__(self, service, Thread):
self.service = service
self.Thread = Thread
def run(self):
i = 0
while i < 6:
self.service.log_error("Printing message from background thread.")
self.Thread.sleep(10000)
i += 1
def main():
(Thread(BackgroundRunnable(service, Thread))).start()
main()
scriptConfig = """{
"autoscript": "BACkGROUND2",
"description": "",
"version": "",
"active": true,
"logLevel": "ERROR"
}"""

Final Thoughts

In this post we explored how to extend Java classes and implement Java interfaces in an automation script. We did this to highlight that automation scripts are as much a customization as a Java class is and to draw attention to the need to take automation scripting seriously.

Sharptree has spent several years developing our VS Code extension (https://marketplace.visualstudio.com/items?itemName=sharptree.maximo-script-deploy) and corresponding command line utility (https://www.npmjs.com/package/maximo-dev-tools) to enable proper development practices, including automating deployments with DevOps pipelines and enabling release controls. We make these tools freely available to give back to the Maximo community and to provide a solution to this significant problem.

It is past time to stop fooling ourselves that automation scripts are not customizations, any such suggestion probably started as IBM marketing. It is my opinion that any Maximo professional that treats an automation script something less than a customization is doing their employer or clients and the community a significant disservice.

Maximo developers need to use proper tools and stop hacking away, cutting and pasting code from Notepad++ into Maximo and from one instance to another as a migration procedure. Actions and behavior are rooted in beliefs and the only way actions change is if beliefs change. As long we continue believing that automation scripts aren't the same as Java development, automation scripts will continue to be developed with sloppy and amateur practices.

Hopefully this post helps illuminate why it is so important to rethink commonly held beliefs about automation scripts.

If you have any questions, comments or have suggestions for topics you would like to see us cover, please reach out to us at [email protected]

In the time it took you to read this blog post...

You could have deployed Opqo, our game-changing mobile solution for Maximo.

Opqo is simple to acquire, simple to deploy and simple to use, with clear transparent monthly pricing that is flexible to your usage.