TL-Script can easily be extended with own functions. There are two mechanisms – pick the simplest one that fits the function:

Simple functions via static methods

The simplest way to add functions is to create a subclass of com.top_logic.model.search.expr.config.operations.TLScriptFunctions and add public static methods. Every public static method automatically becomes a TL-Script function – there is no registration in the application configuration, because all subclasses are discovered automatically at start-up. A subclass may live in any module that depends on com.top_logic.model.search.

@ScriptPrefix("math")
public class MathFunctions extends TLScriptFunctions {
   /**
    * Returns the absolute value of a number.
    *
    * @param a
    *        The value whose absolute value is to be determined.
    * @return The absolute value of the argument.
    */
   @Label("Absolute value of a number")
   @SideEffectFree
   public static double abs(@Mandatory double a) {
      return Math.abs(a);
   }
}

The function name is the prefix followed by the capitalized method name, joined with no separator (e.g. math + absmathAbs()). Always annotate the class with @ScriptPrefix to define this prefix: it groups the functions under a common namespace, prevents name conflicts with built-in or future functions, and lets users find them by prefix in the editor's auto-completion. Without the annotation the prefix defaults to the class's simple name, which is discouraged. The suffix of an individual function can be overridden with @Name on the method.

Each method parameter becomes a TL-Script parameter (positionally). Mark required parameters with @Mandatory; provide a default for a primitive parameter via the matching annotation (@StringDefault, @LongDefault, …); for a type that TL-Script cannot convert natively, annotate the parameter with @ScriptConversion naming a ValueConverter. The method's JavaDoc becomes the function description and each parameter is described by its @param tag; @Label overrides the generated UI label, and @SideEffectFree declares a side-effect-free function. Function names must be globally unique across all subclasses; a clash is reported as a configuration error at start-up. Functions defined this way need no separate documentation page – their description is generated from the JavaDoc.

Functions with full control

The TL-Script function is implemented in a derivative of com.top_logic.model.search.expr.GenericMethod. The actual function is implemented in the overridden method com.top_logic.model.search.expr.Info.eval(Object, Object[], EvalContext). The method gets the self argument as first parameter and all other arguments in the arguments array. As a result, the method must return the function result of the TL-Script function.

Additionally, a builder for the function class must be created. This is created as a derivative of com.top_logic.model.search.expr.config.operations.AbstractSimpleMethodBuilder<I>. This builder creates an instance of the function implementation from above. The builder implementation is registered in the application configuration.

A minimal implementation of a TL-Script function that rounds off a number might look like this:

public class Floor extends SimpleGenericMethod {
   protected Floor(String name, SearchExpression self, SearchExpression[] arguments) {
      super(name, self, arguments);
   }

   @Override
   public GenericMethod copy(SearchExpression self, SearchExpression[] arguments) {
      return new Floor(getName(), self, arguments);
   }

   @Override
   public TLType getType(TLType selfType, List<TLType> argumentTypes) {
      return selfType;
   }

   @Override
   public Object eval(Object self, Object[] arguments) {
      return Math.floor(asDouble(self));
   }

   public static final class Builder extends AbstractSimpleMethodBuilder<Floor> {
      public Builder(InstantiationContext context, Config<?> config) {
         super(context, config);
      }

      @Override
      public Floor build(Expr expr, SearchExpression self, SearchExpression[] args)
            throws ConfigurationException {
         return new Floor(getConfig().getName(), self, args);
      }
   }
}

Naming the arguments

If the function takes arguments, describe them with a constant ArgumentDescriptor by overriding descriptor() in the builder. This enables calling the function with named arguments (e.g. my_fun(value: 42)), documents the signature and validates the number of arguments.

private static final ArgumentDescriptor DESCRIPTOR = ArgumentDescriptor.builder()
   .mandatory("value")
   // .optional("digits", 0)
   .build();

@Override
public ArgumentDescriptor descriptor() {
   return DESCRIPTOR;
}

Note: If a single expression class is reused for several registrations (parameterized by the builder configuration), override getId() on both the builder and the expression and return a value that is unique across all builders (e.g. the pair of the expression class and a configured discriminator). Otherwise the compiled form of the expression may be re-created by the wrong builder.

Configuration

The builder for the function implementation is registered in the application configuration in the section com.top_logic.model.search.expr.config.SearchBuilder. The above function could be registered as my_floor as follows:

<config service-class="com.top_logic.model.search.expr.config.SearchBuilder">
   <instance>
      <methods>
         <method name="my_floor" class="my.package.Floor$Builder"/>
      </methods>
   </instance>
</config>

Usage

Like built-in functions, custom functions can be called using the name assigned in the configuration. To prevent name conflicts with future updates, it is recommended to use a name prefix like my_ in the configuration example above.

The function registered above can then be called as follows:

my_floor(4.2)

The expected result would then be 4.

Documentation

This applies to functions registered via a MethodBuilder; functions defined as static methods on a TLScriptFunctions subclass are documented through their JavaDoc, as described above.

A MethodBuilder-based TL-Script function is only complete once it is documented – otherwise users cannot discover or understand it. Provide a documentation page for every such function. Built-in engine functions are documented under src/main/webapp/doc/{en,de}/DeveloperGuide/TLScript/<Section>/<functionName>/, with one directory per function (named exactly like the registered <method name>) containing:

Pages are discovered by directory, so there is no central index to maintain. Copy an existing page as a template.