major
#29993
Konfiguration: Jede konfigurierte Funktion wahlweise in Java oder TL-Script implementierbar, mit derselben Konfigurationssyntax (auch im React-Konfigurationseditor)
Kontext
Viele Konfigurationseigenschaften verweisen auf eine Funktion, z. B. einen Filter, eine Abbildung oder einen Label-Provider (PolymorphicConfiguration<F>). Solche Eigenschaften konnten bisher nur mit einer Java-Klasse belegt werden. Umgekehrt nehmen Eigenschaften vom Typ Expr nur TL-Script auf.
Mit ScriptFunction0 ... ScriptFunction4 gab es zwar Eigenschaften, die beides erlauben. Sie hatten aber eine eigene Syntax (<label expression="..."/>), die weder zu Expr (label="...") noch zu einer Java-Implementierung (label="com.foo.MyLabel") passt. Wurde eine Eigenschaft von Expr auf ScriptFunctionN umgestellt, musste jede bestehende Konfiguration angepasst werden. Deshalb wurde ScriptFunctionN kaum verwendet. Außerdem griff die Standardimplementierung von ScriptFunctionN (Eintrag unter <defaults>) innerhalb der Anwendungskonfiguration nicht (siehe #29742).
Im generischen Konfigurationseditor der React-Oberfläche (tl-layout-configedit, z. B. im Modelleditor für Annotationen) hatten Eigenschaften vom Typ ScriptFunctionN gar kein Eingabefeld. Beispiel: React-Modelleditor, Attribut -> "+ Anpassungen" -> "Dynamische Bezeichnung" (DynamicLabel): der Abschnitt "Bezeichnung" (eine ScriptFunction3) war leer.
Verbesserung
Jede Konfigurationseigenschaft, die auf eine Implementierung eines Interfaces verweist, kann wahlweise mit einer Java-Implementierung oder mit TL-Script belegt werden.
Interfaces mit genau einer abstrakten Methode
Für eine Funktion (z. B. Filter, Mapping, LabelProvider, Comparator, Function0 ... FunctionN, IFunctionN) ist die Syntax dieselbe wie bisher für die jeweilige Variante:
{{{#!xml <-- Java-Implementierung (unverändert) --> <filter class="com.foo.MyFilter" my-option="..."/> <x filter="com.foo.MyFilter"/>
<-- TL-Script, wie bei einer Eigenschaft vom Typ Expr --> <x filter="x -> $x.get(my.module:MyType#done)"/> <filter><![CDATA[x -> $x.get(my.module:MyType#done)]]></filter> }}}
Der TL-Script-Ausdruck bekommt die Argumente der Methode in ihrer Reihenfolge. Argumente und Ergebnis werden wie bei TL-Script-Funktionen aus TLScriptFunctions umgewandelt (inklusive @ScriptConversion an der Methode). Liefert das Skript einen Wert vom falschen Typ, gibt es eine verständliche Fehlermeldung.
Damit kann eine Eigenschaft vom Typ Expr später auf einen Funktionstyp umgestellt werden, ohne dass sich bestehende Konfigurationen ändern.
Interfaces mit mehreren Methoden
Ein Interface mit mehreren Methoden (z. B. ListModelBuilder) wird in TL-Script implementiert, indem jede Methode als eigene Eigenschaft angegeben wird, wie jede andere Konfigurationseigenschaft wahlweise als Element oder als Attribut. Der Name der Eigenschaft ist der Methodenname in Bindestrich-Schreibweise oder, wenn die Methode eine @Name-Annotation trägt, deren Wert:
{{{#!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="..."/> }}}
- Jede abstrakte Methode muss angegeben werden. Eine Default-Methode kann angegeben werden; ohne Angabe gilt ihre Java-Implementierung.
- Die Elementform gibt es auch für Interfaces mit einer Methode: <filter><accept>...</accept></filter> ist gleichbedeutend mit filter="...".
- @Name an einer Methode bestimmt den Eigenschaftsnamen wie bei einer Eigenschaft eines Konfigurations-Interfaces. Ist die Methode in der Interface-Hierarchie mehrfach annotiert, gilt die Annotation des speziellsten Interfaces. Überladene Methoden mit unterschiedlichen @Name-Werten sind getrennte Eigenschaften und damit implementierbar.
- Ein Interface, dessen abstrakte Methoden generisch sind, überladen ohne unterschiedliche @Name-Werte, oder deren Eigenschaftsnamen kollidieren, kann nicht in TL-Script implementiert werden (Konfigurationsfehler). Überladene oder generische Default-Methoden werden nicht angeboten und behalten ihre Java-Implementierung (so bleibt z. B. Comparator implementierbar).
Methoden ohne Parameter
Eine Methode ohne Parameter, deren Ergebnistyp ein einfacher Konfigurationswert ist (primitive Typen und ihre Wrapper, String, Aufzählungen, Class, Date, Typen mit eigenem Format wie ResKey), wird nicht durch ein Skript, sondern durch einen Wert dieses Typs implementiert. Beispiel DefaultProvider:
{{{#!xml <provider is-computed-in-transaction="true" create-default="context -> attribute -> 42"/> }}}
Ein nicht gesetzter Wert einer Default-Methode liefert deren Java-Implementierung, ein fehlender Wert einer abstrakten Methode ist ein Konfigurationsfehler. Methoden ohne Parameter mit einem anderen Ergebnistyp (Object, Typvariable, Interface) bleiben Skripte.
ScriptFunctionN
ScriptFunction0 ... ScriptFunction4 sind ein Fall dieses Mechanismus. Die bisherige Schreibweise <label expression="..."/> bleibt gültig, label="..." funktioniert jetzt ebenfalls, auch innerhalb der Anwendungskonfiguration. Das Ergebnis wird in den Ergebnistyp umgewandelt statt ungeprüft gecastet.
Editoren
In den Konfigurationseditoren (React und klassisch) bietet eine solche Eigenschaft eine Auswahl der Implementierung an: die passenden Java-Klassen und "TL-Script". Das gilt auch, wenn die Auswahl von einer @Options-Funktion berechnet wird (z. B. "Standardwert" im Modelleditor). Mit "TL-Script" erscheint je Methode ein TL-Script-Code-Editor, für eine Methode ohne Parameter mit einfachem Ergebnistyp das passende Eingabefeld (z. B. die Checkbox "In Transaktion berechnet"). Ein konfigurierter Ausdruck öffnet mit "TL-Script" und dem Ausdruck im Editor.
- Implementiert das Skript die einzige abstrakte Methode, trägt der Code-Editor die Bezeichnung der implementierten Eigenschaft (bei "Dynamische Bezeichnung" also "Bezeichnung", beim Standardwert "Standardwert"). Bei mehreren Methoden trägt jeder Editor die Bezeichnung seiner Methode.
- Der Tooltip zeigt die Beschreibung der Eigenschaft und der Methode, dann die Argumente mit Name, Typ und Beschreibung und das Ergebnis mit Typ und Beschreibung, z. B. beim Standardwert: "Argumente: context: Object – Der Kontext, in dem …; attribute: TLStructuredTypePart – Das Attribut, für das der Standardwert erzeugt wird. Ergebnis: Object – Standardwert für das angegebene Attribut."
- Beim Wechsel auf "TL-Script" wird ein Lambda mit den Parameternamen der Methode vorbelegt, z. B. context -> attribute -> null (ohne bekannte Namen arg1 -> arg2 -> null).
- Die Auswahl enthält nur Klassen, deren generischer Typ zur Eigenschaft passt. Für "Dynamische Bezeichnung" werden also nicht mehr alle IFunction3-Implementierungen des Systems angeboten.
In-App-Dokumentation implementierbarer Interfaces
Der TLDoclet exportiert die JavaDoc eines Interfaces F` in die `messages_*.properties seines Moduls, wenn eine Konfigurationseigenschaft desselben Moduls vom Typ PolymorphicConfiguration<F> ist (direkt, über ein Sub-Interface wie ScriptFunction3 oder als Element einer Liste bzw. Map). Interfaces mit Typparametern (IFunctionN, Mapping) werden nicht exportiert, dort beschreibt die Eigenschaft das Skript. Schlüssel: <Interface>.<methode> (Bezeichnung), .tooltip, .param.<name>.tooltip, .return. Ein Verweis {@link #andereMethode()} ohne Text erscheint als Bezeichnung der verlinkten Methode. Für diese Methoden gibt der Doclet keine Warnungen über fehlende Dokumentation aus.
Behobener Fehler im React-Konfigurationseditor
Wurde der letzte Eintrag einer Liste entfernt (z. B. die einzige Annotation eines Attributs im Modelleditor), galt das Formular nicht als geändert, und "Speichern" verwarf das Entfernen. Hinzufügen, Entfernen, Umsortieren und Typwechsel gelten jetzt immer als Änderung.
Lösung
- com.top_logic.basic definiert die Schnittstelle ScriptImplementationProvider. Sie wird per ServiceLoader gefunden, damit sie schon beim Lesen der Anwendungskonfiguration greift. Eine Eigenschaft PolymorphicConfiguration<F> mit einem Interface F erhält bei einem Interface mit einer Methode implizit ein Textformat. Dadurch gilt die Unterscheidung für Attribut- und Elementschreibweise, JSON und das Zurückschreiben gleichermaßen.
- Ein Attributwert in der Form eines qualifizierten Java-Klassennamens (a.b.C, a.b.C$D) ist eine Implementierungsklasse (Fehler, wenn sie nicht existiert). Jeder andere Wert ist ein TL-Script-Ausdruck (bzw. der Wert einer Methode ohne Parameter). Ein bloßer qualifizierter Name ist kein gültiger TL-Script-Ausdruck, beide bisherigen Schreibweisen bleiben gültig.
- Eine TL-Script-Implementierung wird ohne class-Attribut und ohne Typangabe zurückgeschrieben und gleich wieder eingelesen.
- com.top_logic.model.search stellt die TL-Script-Implementierung bereit (TLScriptImplementationProvider): einen Proxy für das Interface, der die Ausdrücke ausführt bzw. die Werte liefert. Für jeden implementierten Typ entsteht zur Laufzeit ein Konfigurationstyp (DeclarativeConfigDescriptor), dessen Eigenschaften die Methoden des Interfaces sind (MethodProperty, das auch den Eigenschaftsnamen aus @Name bestimmt). Der Proxy ordnet einen Aufruf über die Signatur der Methode zu, nicht über ihren Namen.
- Die Umwandlungen von Argumenten und Ergebnissen aus TLScriptMethod sind in die gemeinsame Klasse ScriptConversions ausgelagert.
- Bezeichnung, Tooltip, Argumente und Ergebnis eines Skript- oder Wert-Editors bestimmt ScriptImplementationLabels für beide Editoren gemeinsam aus der exportierten Interface-Dokumentation. Die Argumentnamen sind die Parameternamen der Methode (die Engine wird mit -parameters übersetzt).
- Die überflüssige Übersteuerung der Bezeichnung von TLDefaultValue.provider ("Provider") in ElementMessages_*.properties entfällt; die Eigenschaft heißt "Standardwert".
- Die Einträge für ScriptFunctionN unter <defaults> in modelSearchConf.config.xml, der Texteditor ScriptFunctionEditor mit seinem java:-Präfix und die zugehörigen Editor-Anpassungen entfallen. Die explizite Angabe der Implementierungsklasse in contactConf.config.xml (#29742) ist nicht mehr nötig.
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 -> Modell -> Modul "demo.tickets" -> Attribut "name" von "Ticket" -> Bearbeiten -> "+ Anpassungen" -> "Dynamische Bezeichnung": "Bezeichnung" bietet "TL-Script" an; der Code-Editor heißt "Bezeichnung", ist mit arg1 -> arg2 -> arg3 -> null vorbelegt und zeigt im Tooltip Beschreibung und Argumente; ein Ausdruck wird gespeichert und wieder angezeigt; das Entfernen der einzigen Annotation wird gespeichert.
- Klassische Demo tl-demo: Administration -> Development -> Modell Editor -> Demo A -> Attribut dateTime -> Bearbeiten -> Anpassungen -> "Dynamische Bezeichnung": "Bezeichnung" bietet LabelWithTimeZone und "TL-Script" an; mit "TL-Script" wie oben.
- Standardwert in beiden Demos (React: wie oben, "Standard Wert"; klassisch: wie oben, "Standard Wert"): "Standardwert" bietet "TL-Script" an; der Code-Editor ist mit context -> attribute -> null vorbelegt, der Tooltip zeigt Beschreibung, benannte Argumente und Ergebnis; die Checkbox "In Transaktion berechnet" wird gespeichert und wieder angezeigt.