Scripting in Java 9

We are almost ready with our sample program for this chapter. There is one issue, though it is not professional. When we have a new product that needs a new checker, we have to create a new release of the code.

Programs in professional environments have releases. When the code is modified, bugs are fixed, or a new function is implemented, there are numerous steps that the organization requires before the application can go into production. These steps compose the release process. Some environments have lightweight release processes; others require rigorous and expensive checks. It is not because of the taste of the people in the organization, though. When the cost of a non-working production code is low and it does not matter if there is an outage or wrong functioning in the program, then the release process can be simple. This way, releases get out faster and cheaper. An example can be some chat program that is used for fun by the users. In such a situation, it may be more important to release new fancy features than ensuring bug-free working. On the other end of the palette, if you create code that controls an atomic power plant, the cost of failure can be pretty high. Serious testing and careful checking of all the features, even after the smallest change, can pay off.

In our example, simple checkers may be an area that is not likely to induce serious bugs. It is not impossible but the code is so simple...Yes, I know that such an argument is a bit fishy, but let's assume that these small routines could be changed with less testing and in an easier way than the other parts of the code. How to separate the code for these little scripts, then, so that they do not require a technical release, a new version of the application, and not even restarting the application? We have a new product that needs a new check and we want to have some way to inject this check into the application environment without any service disruption.

The solution we choose is scripting. Java programs can execute scripts written in JavaScript, Groovy, Jython (which is the JVM version of the language Python), and many other languages. Except JavaScript, the language interpreters of these languages are not a part of the JDK, but they all provide a standard interface, which is defined in the JDK. The consequence is that we can implement script execution in our code and the developers, who provide the scripts, are free to choose any of the available languages; we do not need to care to execute a JavaScript code. We will use the same API as to execute Groovy or Jython. The only thing we should know is what language the script is in. This is usually simple: we can guess that from the file extension, and if guessing is not enough, we can demand that the script developers put JavaScript into files with the .js extension, Jython into files with .jy or .py, Groovy into files with .groovy, and so on. It is also important to note that if we want our program to execute one of these languages, we should make sure that the interpreter is on the classpath. In the case of JavaScript, this is given; therefore, as a demonstration in this chapter, we will write our scripts in JavaScript. There will not be a lot; this is a Java book and not a JavaScript book after all.

Scripting is usually a good choice when we want to pass the ability of programmatically configuring or extending our application. This is our case now.

The first thing we have to do is to extend the production information. In case there is a script that checks the consistency of an order that a product is in, we need a field where we can specify the name of the script:

    private String checkScript; 
public String getCheckScript() {
return checkScript;
}
public void setCheckScript(String checkScript) {
this.checkScript = checkScript;
}

We do not want to specify more than one script per product; therefore, we do not need a list of script names. We have only one script specified by the name.

To be honest, the data structure for the checker classes and the annotations, allowing multiple annotations per product and also per checker class, was too complicated. We could not avoid that, though, to have a complex enough structure that could demonstrate the power and capability of stream expressions. Now that we are over that subject, we can go on using simpler data structures focusing on script execution.

We also have to modify the Checker class to not only use the checker classes but also the scripts. We cannot throw away the checker classes because, by the time we realize that we better need scripts for the purpose, we already have a lot of checker classes and we have no financing to rewrite them to be scripts. Well yes, we are in a book and not in real life, but in an enterprise, that would be the case. That is why you should be very careful while designing solutions for a corporate. The structures and the solutions will be there for a long time and it is not easy to throw a piece of code out just because it is technically not the best. If it works and is already there, the business will be extremely reluctant to spend money on code maintenance and refactoring.

Summary: we modify the Checker class. We need a new class that can execute our scripts; thus, the constructor is modified:

private final CheckerScriptExecutor executor; 

public Checker(
@Autowired Collection<ConsistencyChecker> checkers,
@Autowired ProductInformationCollector piCollector,
@Autowired ProductsCheckerCollector pcCollector,
@Autowired CheckerScriptExecutor executor ) {
this.checkers = checkers;
this.piCollector = piCollector;
this.pcCollector = pcCollector;
this.executor = executor;
}

We also have to use this executor in the isConsistent method:

public boolean isConsistent(Order order) { 
final Map<OrderItem, ProductInformation> map =
piCollector.collectProductInformation(order);
if (map == null) {
return false;
}
final Set<Class<? extends Annotation>> annotations =
pcCollector.getProductAnnotations(order);
Predicate<Annotation> annotationIsNeeded = annotation ->
annotations.contains(annotation.annotationType());
Predicate<ConsistencyChecker> productIsConsistent =
checker ->
Arrays.stream(checker.getClass().getAnnotations())
.parallel().unordered()
.filter(annotationIsNeeded)
.anyMatch(
x -> checker.isInconsistent(order));
final boolean checkersSayConsistent = !checkers.stream().
anyMatch(productIsConsistent);
final boolean scriptsSayConsistent =
!map.values().
parallelStream().
map(ProductInformation::getCheckScript).
filter(Objects::nonNull).
anyMatch(s ->
executor.notConsistent(s,order));
return checkersSayConsistent && scriptsSayConsistent;
}

Note that in this code, we use parallel streams because, why not? Whenever it is possible, we can use parallel streams, even unordered, to tell the underlying system and also to the programmer fellows maintaining the code that order is not important.

We also modify one of our product JSON files to reference a script instead of a checker class through some annotation:

{ 
"id" : "124",
"title": "Desk Lamp",
"checkScript" : "powered_device",
"description": "this is a lamp that stands on my desk",
"weight": "600",
"size": [ "300", "20", "2" ]
}

Even JSON is simpler. Note that as we decided to use JavaScript, we do not need to specify the file name extension when we name the script.

We may later consider further development when we will allow the product checker script maintainers to use different scripting languages. In such a case, we may still require that they specify the extension, and in case there is no extension, it will be added by our program as .js. In our current solution, we do not check that, but we may devote a few seconds to think about it to be sure that the solution can be further developed. It is important that we do not develop extra code for the sake of further development. Developers are not fortunetellers and cannot tell reliably what the future needs will be. That is the task of the business people.

We put the script into the resource directory of our project under the scripts directory. The name of the file has to be powered_device.js because this is the name we specified in the JSON file:

function isInconsistent(order){ 
isConsistent = false
items = order.getItems()
for( i in items ){
item = items[i]
print( item )
if( item.getProductId() == "126" ||
item.getProductId() == "127" ||
item.getProductId() == "128" ){
isConsistent = true
}
}
return ! isConsistent
}

This is an extremely simple JavaScript program. As a side note, when you iterate over a list or an array in JavaScript, the loop variable will iterate over the indexes of the collection or the array. Since I rarely program in JavaScript, I fell into this trap and it took me more than half an hour to debug the error I made.

We have prepared everything we need to call the script. We still have to invoke it. To do so, we use the JDK scripting API. First, we need a ScriptEngineManager. This manager is used to get access to the JavaScript engine. Although the JavaScript interpreter has been a part of the JDK since Java 7, it is still managed in an abstract way. It is one of the many possible interpreters that a Java program can use to execute script. It just happens to be there in the JDK, so we do not need to add the interpreter JAR to the classpath. ScriptEngineManager discovers all the interpreters that are on the classpath and registers them.

It does so using the Service Provider specification, which has been a part of the JDK for a long time, and by Java 9, it also got extra support in module handling. This requires the script interpreters to implement the ScriptEngineFactory interface and also to list the class that does it in the META-INF/services/javax.script.ScriptEngineFactory file. These files, from all the JAR files that are part of the classpath, are read as resources by ScriptEngineManager, and through this, it knows which classes implement script interpreters. The ScriptEngineFactory interface requires that the interpreters provide methods such as getNames, getExtensions, and getMimeTypes. The manager calls these methods to collect the information about the interpreters. When we ask a JavaScript interpreter, the manager will return the one created by the factory that said that one of its names is JavaScript.

To get access to the interpreters through the name, file name extension or mime-type is only one of the functions of ScriptEngineManager. The other one is to manage Bindings.

When we execute a script from within the Java code, we don't do it because we want to increase our dopamine levels. In the case of scripts, it does not happen. We want some results. We want to pass parameters and after the execution of the script, we want values back from the script that we can use in the Java code. This can happen in two ways. One is by passing parameters to a method or function implemented in the script and getting the return value from the script. This usually works, but it may even happen that some scripting language does not even have the notion of the function or method. In such a case, it is not a possibility. What is possible is to pass some environment to the script and read values from the environment after the script is executed. This environment is represented by Bindings.

Bindings is a map that has String keys and Object values.

In the case of most scripting languages, for example, in JavaScript, Bindings is connected to global variables in the script we execute. In other words, if we execute the following command in our Java program before invoking the script, then the JavaScript global variable, globalVariable, will reference the myObject object:

myBindings.put("globalVariable",myObject)

We can create Bindings and pass it to ScriptEngineManager but just as well we can use the one that it creates automatically, and we can call the put method on the engine object directly.

There are two Bindings when we execute scripts. One is set on the ScriptEngineManager level. This is named global binding. There is also one managed by ScriptEngine itself. This is the local Bindings. From the script point of view, there is no difference. From the embedding side, there is some difference. In case we use the same ScriptEngineManager to create multiple ScriptEngine instances, then the global bindings are shared by them. If one gets a value, all of them see the same value; if one sets a value, all others will later see that changed value. The local binding is specific to the engine it is managed by. Since we only introduce Java scripting API in this book, we do not get into more details and we will not use Bindings. We are good with invoking a JavaScript function and to get the result from it.

The class that implements the script invocation is CheckerScriptExecutor:

package packt.java9.by.example.mybusiness.bulkorder.services; 

import ...

@Component
public class CheckerScriptExecutor {
private static final Logger log = ...

private final ScriptEngineManager manager =
new ScriptEngineManager();

public boolean notConsistent(String script, Order order) {

try {
final Reader scriptReader = getScriptReader(script);
final Object result =
evalScript(script, order, scriptReader);
assertResultIsBoolean(script, result);
log.info("Script {} was executed and returned {}",
script, result);
return (boolean) result;

} catch (Exception wasAlreadyHandled) {
return true;
}
}

The only public method, notConsistent, gets the name of the script to execute and also order. The latter has to be passed to the script. First it gets Reader, which can read the script text, evaluates it, and finally returns the result in case it is boolean or can at least be converted to boolean. If any of the methods invoked from here that we implemented in this class is erroneous, it will throw an exception, but only after appropriately logging it. In such cases, the safe way is to refuse an order.

Actually, this is something that the business should decide. If there is a check script that cannot be executed, it is clearly an erroneous situation. In this case, accepting an order and later handling the problems manually has certain costs. Refusing an order or confirmation because of some internal bug is also not a happy path of the order process. We have to check which approach causes the least damage to the company. It is certainly not the duty of the programmer. In our situation, we are in an easy situation.

We assume that the business representatives said that the order in such a situation should be refused. In real life, similar decisions are many times refused by the business representatives saying that it just should not happen and the IT department has to ensure that the program and the whole operation is totally bug free. There is a psychological reason for such a response, but this really leads us extremely far from Java programming.

Engines can execute a script passed through Reader or as String. Because now we have the script code in a resource file, it seems to be a better idea to let the engine read the resource instead of reading it to a String:

 
private Reader getScriptReader(String script)
throws IOException {
final Reader scriptReader;
try {
final InputStream scriptIS = new ClassPathResource(
"scripts/" + script + ".js").getInputStream();
scriptReader = new InputStreamReader(scriptIS);
} catch (IOException ioe) {
log.error("The script {} is not readable", script);
log.error("Script opening exception", ioe);
throw ioe;
}
return scriptReader;
}

To read the script from a resource file, we use the Spring ClassPathResource class. The name of the script is prepended with the scripts directory and appended by the.js extension. The rest is fairly standard and nothing we have not seen in this book. The next method that evaluates the script is more interesting:

        private Object evalScript(String script, 
Order order,
Reader scriptReader)
throws ScriptException, NoSuchMethodException {
final Object result;
final ScriptEngine engine =
manager.getEngineByName("JavaScript");
try {
engine.eval(scriptReader);
Invocable inv = (Invocable) engine;
result = inv.invokeFunction("isInconsistent", order);
} catch (ScriptException | NoSuchMethodException se) {
log.error("The script {} thruw up", script);
log.error("Script executing exception", se);
throw se;
}
return result;
}

To execute the method in the script, first of all, we need a script engine that is capable of handling JavaScript. We get the engine from the manager by its name. If it is not JavaScript, we should check that the returned engine is not null. In the case of JavaScript, the interpreter is part of the JDK and checking that the JDK conforms to the standard would be paranoid.

If ever we want to extend this class to handle not only JavaScript but also other types of scripts, this check has to be done, and also the script engine should probably be requested from the manager by the file name extension, which we do not have access to in this private method. But that is future development, not in this book.

When we have the engine, we have to evaluate the script. This will define the function in the script so that we can invoke it afterwards. To invoke it, we need some Invocable object. In the case of JavaScript, the engine also implements an Invocable interface. Not all script engines implement this interface. Some scripts do not have functions or methods, and there is nothing to invoke in them. Again, this is something to do later, when we want to allow not only JavaScript scripting but also other types of scripting.

To invoke the function, we pass its name to the invokeFunction method and also the arguments that we want to pass on. In this case, this is the order. In the case of JavaScript, the integration between the two languages is fairly developed. As in our example, we can access the field and the methods of the Java objects that are passed as arguments and the returned JavaScript true or false value is also converted to Boolean magically. There are some situations when the access is not that simple though:

 
private void assertResultIsBoolean(String script,
Object result) {
if (!(result instanceof Boolean)) {
log.error("The script {} returned non boolean",
script);
if (result == null) {
log.error("returned value is null");
} else {
log.error("returned type is {}",
result.getClass());
}
throw new IllegalArgumentException();
}
}
}

The last method of the class checks that the returned value, which can be anything since this is a script engine, is convertible to boolean.

It is important to note that the fact that some of the functionality is implemented in script does not guarantee that the application works seamlessly. There may be several issues and scripts may affect the inner working of the entire application. Some scripting engines provide special ways to protect the application from bad scripts, others do not. The fact that we do not pass but order to the script does not guarantee that a script cannot access other objects. Using reflection, static methods, and other techniques there can be ways to access just anything inside our Java program. We may be a bit easier with the testing cycle when only a script changes in our code base, but it does not mean that we should blindly trust any script.

In our example, it probably would be a very bad idea to let the producers of the products upload scripts to our system. They may provide their check scripts, but these scripts have to be reviewed from the security point of view before being deployed into the system. If this is properly done, then scripting is an extremely powerful extension to the Java ecosystem, giving great flexibility to our programs.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.143.205.27