major
#29993
Configuration: Each configured function can be implemented in either Java or TL-Script, using the same configuration syntax (including in the React configuration editor)
Context
Many configuration properties refer to a function, such as a filter, a mapping, or a label provider (PolymorphicConfiguration<F>). Until now, such properties could only be populated with a Java class. Conversely, properties of type Expr only accept TL-Script.
While properties such as ScriptFunction0 through ScriptFunction4 did allow both, they had their own syntax (<label expression="..."/>) that did not match either Expr (label="...") or a Java implementation (label="com.foo.MyLabel"). If a property was changed from Expr to ScriptFunctionN, every existing configuration had to be adjusted. For this reason, ScriptFunctionN was rarely used. Furthermore, the default implementation of ScriptFunctionN (entry under <defaults>) did not take effect within the application configuration (see #29742).
In the generic configuration editor of the React interface (tl-layout-configedit, e.g., in the model editor for annotations), properties of type ScriptFunctionN did not have an input field at all. Example: React model editor, Attribute -> "+ Customizations" -> "Dynamic Label" (DynamicLabel): the "Label" section (a ScriptFunction3) was empty.
Improvement
Any configuration property that references an implementation of an interface can optionally be assigned either a Java implementation or a TL-Script.
Interfaces with exactly one abstract method
For a function (e.g., Filter, Mapping, LabelProvider, Comparator, Function0... FunctionN, IFunctionN), the syntax remains the same as before for the respective variant:
{{{#!xml <-- Java implementation (unchanged) --> <filter class="com.foo.MyFilter" my-option="..."/> <x filter="com.foo.MyFilter"/>
<-- TL script, as with a property of type Expr --> <x filter="x -> $x.get(my.module:MyType#done)"/> <filter><![CDATA[x -> $x.get(my.module:MyType#done)]]></filter> }}}
The TL-Script expression receives the method’s arguments in the order they are provided. Arguments and the result are converted as with TL-Script functions from TLScriptFunctions (including @ScriptConversion on the method). If the script returns a value of the wrong type, a clear error message is displayed.
This allows a property of type ` Expr ` to be converted to a function type later on without changing existing configurations.
Interfaces with Multiple Methods
An interface with multiple methods (e.g., ListModelBuilder) is implemented in TL-Script by specifying each method as its own property—just like any other configuration property—either as an element or as an attribute. The name of the property is the method name written with hyphens or, if the method has an @Name annotation, its value:
{{{#!xml <builder> <get-model>...</get-model> <supports-model>...</supports-model> <retrieve-model-from-list-element>...</retrieve-model-from-list-element> <supports-list-element>...</supports-list-element> </builder>
<builder get-model="..." supports-model="..." retrieve-model-from-list-element="..."/> }}}
- Every abstract method must be specified. A default method can be specified; if not specified, its Java implementation applies.
- The element form is also available for interfaces with a method: <filter><accept>...</accept></filter> is equivalent to filter="...".
- The @Name annotation on a method determines the property name, just as it does for a property of a configuration interface. If the method is annotated multiple times in the interface hierarchy, the annotation from the most specific interface takes precedence. Overloaded methods with different @Name values are separate properties and can therefore be implemented.
- An interface whose abstract methods are generic, overloaded without different @Name values, or whose property names conflict cannot be implemented in TL-Script (configuration error). Overloaded or generic default methods are not provided and retain their Java implementation (for example, ` Comparator ` remains implementable).
Methods Without Parameters
A method without parameters whose return type is a simple configuration value (primitive types and their wrappers, String, enums, Class, Date, types with their own format such as ResKey) is not implemented by a script but by a value of that type. Example DefaultProvider:
{{{#!xml <provider is-computed-in-transaction="true" create-default="context -> attribute -> 42"/> }}}
An unset value for a default method returns its Java implementation; a missing value for an abstract method constitutes a configuration error. Methods without parameters that have a different return type (Object, type variable, interface) remain scripts.
ScriptFunctionN
ScriptFunction0... ScriptFunction4 are examples of this mechanism. The previous syntax <label expression="..."/> remains valid; label="..." now also works, even within the application configuration. The result is converted to the result type instead of being cast without verification.
Editors
In the configuration editors (React and classic), such a property offers a choice of implementation: the corresponding Java classes and “TL-Script.” This also applies when the selection is calculated by an @Options function(e.g., “Default Value” in the Model Editor). With “TL-Script,” a TL-Script code editor appears for each method; for a method without parameters and with a simple result type, the corresponding input field appears (e.g., the “Calculated in Transaction” checkbox). A configured expression opens with “TL-Script” and the expression in the editor.
- If the script implements the only abstract method, the code editor displays the name of the implemented property (i.e., “Name” for “Dynamic Name” and “Default Value” for “Default Value”). If there are multiple methods, each editor displays the name of its respective method.
- The tooltip displays the description of the property and the method, followed by the arguments with their name, type, and description, and the result with its type and description; for example, for the default value: "Arguments: context: Object – The context in which …; attribute: TLStructuredTypePart – The attribute for which the default value is generated. Result: Object – Default value for the specified attribute."
- When switching to "TL-Script," a lambda is pre-populated with the method’s parameter names, e.g., context -> attribute -> null (without known names: arg1 -> arg2 -> null).
- The selection includes only classes whose generic type matches the property. For “Dynamic Name,” therefore, not all IFunction3 implementations in the system are offered anymore.
In-App Documentation for Implementable Interfaces
The TLDoclet exports the JavaDoc of an interface F` to the `messages_*.properties of its module if a configuration property of that same module is of type ` PolymorphicConfiguration<F> ` (directly, via a subinterface such as ` ScriptFunction3`, or as an element of a list or map). Interfaces with type parameters (IFunctionN, Mapping) are not exported; in those cases, the property describes the script. Keys: <Interface>.<method> (name), .tooltip, .param.<name>.tooltip, .return. A reference {@link #otherMethod()} without text appears as the name of the linked method. For these methods, the doclet does not issue warnings about missing documentation.
Fixed bug in the React configuration editor
If the last entry in a list was removed (e.g., the only annotation for an attribute in the Model Editor), the form was not considered modified, and clicking “Save” would discard the removal. Adding, removing, reordering, and changing types are now always considered modifications.
Solution
- com.top_logic.basic defines the ScriptImplementationProvider interface. It is found via ServiceLoader so that it takes effect as soon as the application configuration is read. A PolymorphicConfiguration<F> property with an interface F implicitly receives a text format if the interface has a method. As a result, the distinction applies equally to attribute and element notation, JSON, and write-back.
- An attribute value in the form of a qualified Java class name (a.b.C, a.b.C$D) is an implementation class (an error occurs if it does not exist). Any other value is a TL-Script expression (or the value of a method without parameters). A mere qualified name is not a valid TL-Script expression; both previous notations remain valid.
- A TL-Script implementation is written back without a ` class` attribute and without a type specification, and then immediately read back in.
- com.top_logic.model.search provides the TL-Script implementation (TLScriptImplementationProvider): a proxy for the interface that executes the expressions or returns the values. For each implemented type, a configuration type (DeclarativeConfigDescriptor) is created at runtime, whose properties are the methods of the interface (MethodProperty, which also determines the property name from @Name ). The proxy maps a call based on the method’s signature, not its name.
- The conversions of arguments and results from ` TLScriptMethod ` are outsourced to the shared class ` ScriptConversions `.
- ScriptImplementationLabels determines the label, tooltip, arguments, and result of a script or value editor for both editors jointly from the exported interface documentation. The argument names are the parameter names of the method (the engine is translated using -parameters ).
- The redundant override of the label for TLDefaultValue.provider (“Provider”) in ElementMessages_*.properties is no longer necessary; the property is now called “Default Value.”
- The entries for ScriptFunctionN under <defaults> in modelSearchConf.config.xml, the ScriptFunctionEditor text editor with its java: prefix, and the associated editor customizations are no longer needed. Explicitly specifying the implementation class in contactConf.config.xml (#29742) is no longer necessary.
Test
- TestScriptImplementation, TestMethodProperty, TestJavaTypeUtil, TestJavaTypeFormat, TestConfigUtil (basic), TestScriptConversions, TestTLScriptImplementation, TestScriptImplOptionMapping, TestTLScriptEditorControls (model.search), TestScriptImplementationConfigEditor (model.search.react), TestConfigEditorControl, TestConfigFormControl (configedit), TestAnnotationsFieldControlProvider (layout.view), TestImplementableInterfaceDoc (tl-build_doclet), Scripted Test TestRegression28606.
- React demo tl-demo-react: Administration -> Model -> "demo.tickets" module -> "name" attribute of "Ticket" -> Edit -> "+ Customizations" -> "Dynamic Label": "Label" offers "TL-Script"; the code editor is named "Label," is pre-populated with arg1 -> arg2 -> arg3 -> null, and displays the description and arguments in the tooltip; an expression is saved and displayed again; removing the single annotation is saved.
- Classic demo tl-demo: Administration -> Development -> Model Editor -> Demo A -> " dateTime " attribute -> Edit -> Customizations -> "Dynamic Label": "Label" offers LabelWithTimeZone and "TL-Script"; use "TL-Script" as above.
- Default value in both demos (React: as above, "Default Value"; classic: as above, "Default Value"): "Default Value" offers "TL-Script"; the code editor is pre-populated with context -> attribute -> null, the tooltip displays the description, named arguments, and result; the "Calculated in transaction" checkbox is saved and displayed again.