Object-namer launchers

The abstract class ObjectNamerLauncher is the base class for object-namer launchers. An object-namer launcher processes input strings, files, or streams that contain YAML or JSON objects. These can contain expressions written in the ESP scripting language, which is executed in a mode that disables import statements. While the import statement is disabled, the Java classes that can be used (and how they are used) can be configured when an object-namer is created. If a simulation or animation service is provided via a web server, for example, limiting the classes that a user may access has security advantages. Object-namer launchers do not have to be used explicitly: the program yrunner will handle these for the user. The following sections describe the following:

Instantiating an object namer

While a constructor can be used directly, normally and object-namer launcher will be created by using a newInstance method:

  • ObjectNamerLauncher.newInstance(String). This method creates an object-namer launcher, looked up by name, for which scripting-language expressions will allow the use of a specific set of Java classes.
  • ObjectNamerLauncher.newInstance(String,String...). This method creates an object-namer launcher, looked up by names, for which the scripting-language expressions will allow the use of not only the classes associated with the object-namer launcher, but also additional collections of classes specified by the optional arguments and called launcher-data additions.
This is done by the yrunner program, but it is easy to use the newInstance methods directly. A yrunner option will print a table showing the names for launchers and collections of additional classes.

Object namers have a constraint: there can be only one active per thread. This property is enforced by constructors. An object namer is active until its ObjectNamerLauncher.close() method is called. Because object namers implement the AutoCloseable interface, they may be used in a Java try-with-resources block, in which case the call to ObjectNamerLauncher.close() is implicit.

Using an object namer

An object namer processes text in either JSON or YAML format. The methods that process text are

These, of course, are not used explicitly if the yrunner command is used. Regardless, the input will a text file using the format specified by YAML or JSON, with a UTF-8 character encoding. For the YAML case, tabs are not allowed. The text file should contain an object, or an array whose elements are objects (the parser actually allows arrays of arrays, but the values that are not arrays must be objects). Each object (whether represented with JSON or YAML syntax) may have the following properties:
  • execute. The value may be an object or a list of objects with types supported by JSON or YAML. When a value is a string using the syntax specified for ExpressionParser, the string is interpreted as follows:
    • a statement starting with "=" will be evaluated.
    • a "var" statement will set a variable with a specified name, and its variant using "?=" will provide a default values if the variable is not set externally. The variant using "??=" will provide a default value if the variable is either not defined or has the value null.
    • a "function" or "synchronized function" statement will define a function, giving it a name.
    If a string that could be confused with an ExpressionParser statement is desired, there are two ways of handling it:
    • For YAML, once can force the value to be an string by using "!!str" tag. For example
      
      execute: !!str var foo = 10
                
      If the string is a quoted string (delimited by double quotes) or a YAML multi-line string, it is assumed to be a string. To make such a string an expression, on may use a tag:
      
      execute: !bzdev!esp "var foo = 10"
                
    • For JSON, one can use "=" with a quoted string. For example
      
      execute: "= \"var foo = 10\""
                
    Such a conflict should occur rarely, if at all, in practice.
  • factories. The value is either an object or a sequence of objects. Each of these objects contains a "context" property and a sequence of properties whose names are variable names and whose values are the simple class names of factories. The value for a context is a list with two elements. The first element is a variable name for an object namer. The second is the package in which the factories provided by that context's factories are located.
  • define. This section allows one to provide YAML anchors to avoid repetition and is otherwise ignored. This is needed because the variable provided by an expression parser cannot be used to define a common subtree for part of JSON or YAML object: a YAML anchor and alias is used instead.
  • create. The value is either an object or a list of objects. For each object, there are four properties:
    • var. The value is the variable storing the object that will be created.
    • name. The value is name of the object.
    • factory. The value is the variable storing the factory that will be used to create the object.
    • configuration. The value is either another object or a list of objects. This value is factory dependent. The syntax for an object follows that provided in the documentation for NamedObjectFactory (that documentation describes the use of ECMAScript, but ECMAScript is similar to JSON syntactically and YAML is (approximately) a superset of JSON.

It is sometimes necessary to manipulate ESP variables. The methods

can be used for this purpose. The yrunner command, for example, has options that set scripting-language variables and some of these methods are used by yrunner for that purpose.

Subclassing an object-namer launcher

A subclass of ObjectNamerLauncher is expected to have two constructors: one with no arguments and one with a single argument, an instance of JSObject. The directory containing the source code for the subclass should also contain a YAML file using the syntax shown below. If LAUNCHER is the class name of an object-namer launcher and LAUNCHER_SUPERCLASS is its immediate superclass, then class definition for LAUNCHER (with these two identifiers replaced with their actual values) is


 public class LAUNCHER extends LAUNCHER_SUPERCLASS {

    public static InputStream getResourceStream() {
        return LAUNCHER.class.getResourceAsStream("LAUNCHER.yaml");
    }

    public LAUNCHER(JSObject initializer)
        throws ClassNotFoundException, IOException, IllegalAccessException
    {
        super(combine(loadFromStream(LAUNCHER.class,
                                     LAUNCHER.getResourceAsStream(),
                                     8),
                        initializer));
    }

    public LAUNCHER()
        throws ClassNotFoundException, IOException, IllegalAccessException
    {
        this(null);
    }
 }
  
The methods will parse YAML files and combine the results to produce a single object. In all or almost all cases, this design pattern should be used. The constant 8 indicates that tabs will be converted to spaces with the assumption that there are 8 spaces per tab. Allowing tabs is convenient when using some text editors (e.g., emacs).

Configuring object-namer launchers

The YAML file loaded by a constructor lists classes and how they can be used in an object-namer launcher's scripting environment. A a more detailed description is provided by the API documentation for ExpressionParser, but that documentation does not describe a YAML representation of the same data.

The YAML file, generally a resource in a JAR file, specifies an a YAML object with the following properties:

  • argumentTypes - a JSArray providing a list of strings giving the fully qualified class names for arguments used by constructors, functions, and methods. The types String, int, double, Integer, or Double should not be used, as these are allowed by default.
  • fieldClasses - a JSArray providing a list of strings giving the fully qualified class names for classes containing fields that can be used. The types of the fields that will be included are boolean, int, long, double, String, or an enumeration.
  • functionClasses - a JSArray providing a list of strings the fully qualified class names for classes whose public, static methods returning an allowable type have a fixed number of arguments whose types are boolean, int, long, double, or a type provided by the argumentTypes property.
  • methodClasses - a JSArray providing a list of strings giving the fully qualified class names classes whose instance methods returning an allowable type have a fixed number of arguments with types int, double, long, boolean, String, or a type provided by the argumentTypes property.
  • returnTypes - a JSArray providing a list of strings giving the fully qualified class names for objects that the parser can return or can construct. The constructors that will be provided are those with a fixed number of arguments whose types are int, long, double, boolean, String, or a type provided by the argumentTypes property.
  • define - this property (actually any property other than the ones listed above) is ignored. If provided it should appear first, and should be used merely to provide anchors so that YAML aliases can be used to minimize the replication of class names.

The SPIs for object-namer launchers

Both yrunner and the newInstance methods will look up object-namer launchers, and collections of classes, by name. A name is provided by an SPI (Service Provider Interface). There are two that are applicable: Methods common to both and that must be implemented are:

The interface ONLauncherProvider adds one additional method: {ONLauncherProvider.onlClass(). This class provides the class name of the object-namer launcher that matches the name provided by ONLauncherData.getName().

Generally, the SPIs should not be in the same directory as the classes they provide: otherwise the SPI classes might appear in the API documentation generated with the javadoc command. These SPIs typically have trivial implementations. For example, the SPI for the org.bzdev.math package, an implementation of ONLauncherData, is only a few lines of code:


package org.bzdev.providers.math;
import java.io.InputStream;
import java.util.ResourceBundle;
import org.bzdev.lang.spi.ONLauncherData;

public class MathLauncherData implements ONLauncherData {

    public MathLauncherData() {}

    public String getName() {
        return "math";
    }

    public InputStream getInputStream() {
        return getClass().getResourceAsStream("MathLauncherData.yaml");
    }

    @Override
    public String description() {
        return ResourceBundle
            .getBundle("org.bzdev.providers.math.lpack.MathLauncherData")
            .getString("description");
    }
}
  
A file in the same directory named MathLauncherData.yaml contains a list of classes that this service provider makes accessible. The use of a resource bundle allows the description to be localized so that multiple languages can be supported.

Similarly for the org.bzdev.devqsim package the implantation of ONLauncherProvider. is also only a few lines of code long:


package org.bzdev.providers.devqsim;
import java.io.InputStream;
import java.util.ResourceBundle;
import org.bzdev.obnaming.spi.ONLauncherProvider;
import org.bzdev.devqsim.SimulationLauncher;

public class SimulationLauncherProvider implements ONLauncherProvider {
    @Override
    public String getName() {
        return "devqsim";
    }

    @Override
    public Class onlClass() {
        return SimulationLauncher.class;
    }

    @Override
    public InputStream getInputStream() {
        return SimulationLauncher.getResourceStream();
    }

    @Override
    public String description() {
        return ResourceBundle
            .getBundle("org.bzdev.providers.devqsim.lpack.SimulationLauncher")
            .getString("description");
    }
}
  
In this case, SimulationLauncher is placed in the org.bzdev.devqsim package along with the resource SimulationLauncher.yaml, primarily because Java 11 makes it difficult to read resources from a different package when Java modules are used.

The module-info.jar file for org.bzdev.math contains the statement


    provides org.bzdev.lang.spi.ONLauncherData with
        org.bzdev.providers.math.MathLauncherData,
        ...
  
and the file META-INF/services/org.bzdev.lang.spi.ONLaucherData will contain the line

org.bzdev.providers.math.MathLauncherData
  

Similarly the module-info.jar file for org.bzdev.devqsim contains the statement


    provides org.bzdev.obnaming.spi.ONLauncherProvider with
        org.bzdev.providers.devqsim.SimulationLauncherProvider;
  
and the file META-INF/services/org.bzdev.obnaming.spi.ONLauncherProvider contains the line

org.bzdev.providers.devqsim.SimulationLauncherProvider
  

Providing both a META-INF/services file and an entry in a module-info.jar file should be redundant, but some releases of Java, or at least of openjdk, need both if the service provider is to work as expected whether or not Java modules are used.

Generating class documentation

The class ObjectNamerLauncher contains methods for querying its expression parser to recover information about the classes it supports. While one can use the following classes to generate documentation, the program yrunner already provides this capability. The methods described below can be used to generate documentation in a different format than that provided by yrunner.

The following describes the lower-level methods used to generate such documentation or extract other information about the Java classes that can be used.

Several methods return lists of classes based on their usage:

The method ObjectNamerLauncher.createAPIMap(List) will set up a map that associated class names with a URL for their API documentation. It's argument is a list of URLs that point to the top-level directories for API documentation generated by javadoc. Several methods return a TemplateProcessor.KeyMapList that will contain key maps for each class:

The documentation for each of these methods includes a description of the key maps that these produce. The class TemplateProcessor can then be used to generate documentation.

Finally, several methods,

provide information about the names the SPIs use to look up launchers and launcher-data additions. The method ObjectNamerLauncher.getProviderKeyMap() will provide key maps that include a name and a description of each launcher or launcher-data addition, and can be used to generate formatted documentation.