Architectural Principles Functions And Libraries CTL2 functions are grouped and distributed in function libraries. CloverETL engine can be extended with new CTL2 function library plugins extending the ctlfunction extension point. The libraries are also used for visual grouping in CloverETL Designer’s expression editor dialog. Library name is automatically used as the name of the group of functions being displayed. FIXME: Expression Editor Screenshot Functions in interpreted vs. compiled mode The CTL2 can be executed in interpreted or compiled mode. In compiled mode the CTL2 compiler translates CTL2 code directly into Java code. The code is then dynamically compiled and loaded. In the translation process the calls to CTL2 functions are translated directly to a Java method calls from respective library. In the interpreted mode the CTL2 interpreter constructs AST from the CTL2 code. By traversing the AST it evaluates CTL2 expressions. Any calls to CTL2 library functions are executed via objectwrapper of the function. The objec-wrapper is a Java-object representation of the CTL2 function that can be stored in an AST node. The object wrappers contains no logic; it’s puprpose is to retrieve arguments (actual parameters) from interpreter current stack then delegate the call to corresponding Java method implementing the actual function. The default execution mode of CTL2 is interpreted, this means the object-wrappers must be implemented for each newly added function. DateAdd function: Java method with function implementation public static final Date dateAdd(TLFunctionCallContext context, Date lhs, Long shift, DateFieldEnum unit) { Calendar c = ((TLCalendarCache)context.getCache()).getCalendar(); c.setTime(lhs); c.add(unit.asCalendarField(), shift.intValue()); return c.getTime(); } DateAdd function: Object-wrapper delegating to the Java method class DateAddFunction implements TLFunctionPrototype { @Override public void execute(Stack stack, TLFunctionCallContext context) { final DateFieldEnum unit = (DateFieldEnum)stack.pop(); final Long shift = stack.popLong(); final Date lhs = stack.popDate(); stack.push(dateAdd(context, lhs,shift,unit)); } } Function Annotations CTL2 discovers new functions and their signatures automatically via Java reflections andd annotations. Methods that should be treated as CTL2 function implementation must be correctly annotated. Unannotated functions are ignored by the scanning process. The following annotations are recognized by CTL2: org.jetel.ctl.extensions.TLFunctionAnnotation(String functionDescription) : Annotation is used to tag methods which implement the logic of CTL2 functions. Optional String argument carries the description which is automatically displayed in Designer tooltip text for the function Additionally the first parameter of the method must be of TLFunctionCallContext type USEFUL: With debug level TRACE the Engine plugin system reports information about discovered, ignored and erroneously declared methods. Example of DateLib.dateAdd() function declaring implicit context and 3 arguments: @TLFunctionAnnotation("Adds to a component of a date (e.g. month)") public static final Date dateAdd(TLFunctionCallContext context, Date lhs, Long shift, DateFieldEnum unit) { Calendar c = ((TLCalendarCache)context.getCache()).getCalendar(); c.setTime(lhs); c.add(unit.asCalendarField(), shift.intValue()); return c.getTime(); } org.jetel.ctl.extensions.TLFunctionInitAnnotation: No-arg annotation used to tag methods serving as initializers for functions. The initializer methods are called during component’s init() time, before the first call to the function implementation method. Can be used to initialize libraries, backend API and to create function cache (see Context and Cache). Besides the annotation the initializer must respect the following conventions: Method must declare only a single parameter of type org.jetel.extensions.ctl.TLFunctionCallContext. Method name must use „Init“ suffix after function name it belongs to. E.g. method dateAddInit() (note uppercase „I“) is an initializer for the dateAdd() function. Example of DateAddInit caching instance of Calendar: @TLFunctionInitAnnotation public static final void dateAddInit(TLFunctionCallContext context) { context.setCache(new TLCalendarCache()); } org.jetel.ctl.extensions.TLFunctionParametersAnnotation(String[] paramNames): Annotation for specifying names of function parameters as these cannot be determined automatically by Java reflection. Library and Function Lifecycle The function lifecycle has the following steps: 1. Library initialization: a. Scan for available library extensions b. Scan each library for declared functions 2. Function compilation: a. Interpreted mode: instantiate function’s object-wrapper; or b. Compiled mode: generate call to implementing method 3. Function initialization: a. Interpreted mode:call init() on object-wrapper; or b. Compiled mode: call initializer method 4. Execution: a. Interpreted mode: call execute() on object-wrapper; or b. Compiled mode: invoke implementing method 5. End of execution Engine Initialization New libraries are discovered by Engine’s plugin system during Engine initialization. It is done by scanning for extensions on ctlfunction extension point. New libraries should extend the org.jetel.ctl.extensions.TLFunctionLibrary. The default implementation in the TLFunctionLibrary scans all methods in the descendant for presence of TLFunctionAnnotation. All annotated methods are additionally introspected for their parameters via Jav reflections. This information is used by CTL2 type checker. Function compilation During code compilation the CTL2 compiler resolves the name of the function to its implementation. It establishes the binding by searching all libraries and their functions by function name. If no-match is found the compiler generates a compilation error – „Function not declared“. In case multiple matches for the function are found, the correct version to call is determined by CTL2 type checker (see Parameters and Overloading) In interpreted mode after finding the function the CTL2 compiler in instantiates the object-wrapper for the function. This is done by calling by calling getExecutable(String functionName) on function’s library. Compiler creates a new instance is of object-wrapper for each call to the corresponding function. This means two calls to an identical function do NOT share their object-wrappers. This behavior is identical to compiled mode where the compiler generates call to initializer for every call to the CTL2 library function found in the source code. Code initialization In compiled mode the CTL2 compiler generates a call to Java method representing the initializer. In interpreted mode the CTL2 executor invokes the init() method on object-wrapper. The wrapper should delegate this call to corresponding initializer method for the function. This way the initializer logic is localized in the same method. When executed, the initializer is presented with a TLFunctionCallContext instance that can be used to store cached instances of Java objects . The cache should be used to store objects which are expensive to create during repetitive calls to the function during execution (e.g. java.util.regex.Pattern, java.util.Calendar, ...). The cache should be populated and set set via TLFunctionCallContext.setCache(TLCache) method. There is a variety of cache implementations already present (TLCalendarCache,TLRegexpCache, TLObjectCache<T>). Developers who need to implement own cache shoud extend TLCache type. Code execution In compiled mode the CTL2 compiler generates a call to Java method representing the implementation of the function. In interpreted mode the CTL2 interpreter invokes the execute() method on the object-wrapper passing a reference to current stack and TLFunctionCallContext. The responsibility of the objectwrapper is to pop the necessary function arguments, delegate the call to corresponding Java method with function logic and push function’s retrurn value to the stack (for non-void functions). Consider the following when popping function arguments: The number of arguments is either known during development or can be determined via TLFunctionCallContext.getParams().length for vararg functions The arguments on the stack are popped in reverse order; last argument of the function is popped first from the stack End of execution The last record processed by the CTL2 code results in the last invokaction of the function. There is no additional post-execute or cleanup logic. If you require such functionality, consider implementing your logic as custom Component which provides such lifecycle mechanisms. Parameters and Overloading The CTL2 supports the following concepts for functions: Function overloading Vararg functions (aka. 3-dot functions) Passing containers by reference Overloading Overloading allows declaring multiple version of a function with the same name but varying parameters. CTL2 follows the same rules for overloading as in Java language: the versions of the function must differ at least in one parameter (i.e. difference only in return type is insufficient). No specific coding is required from the developer when overloading functions. The CTL2 type checker will generate call to the correct version of the function depending on actual arguments in the function call. Library functions can be additionally overloaded by local declaration in the CTL2 code. In case a local function declares identical parameters as library function the local function takes precedence. No error or warning is issued by the compiler The overloaded versions of the function may be located in different libraries; i.e. it is possible to overload standard library function with own functions. This practice is however not recommended as it may collide with new additions within standard CloverETL product. Vararg functions CTL2 support functions with variable number of arguments. Such functions are automatically recognized and handled by the scanning process. In order to implement a custom vararg function see the existing implementation of the StringLib.concat() or ContainerLib.insert(). The developers implementing avararg function must take care to pop correct number of arguments from the stack in object-wrapper. This number of actual arguments can be retrieved from TLFunctionCallContext.getParams().length Passing containers by reference Container types (map, list, record) are passed to functions by reference. It is therefore possible to modify their state (e.g. insert element to a list, change field value in a record). IMPORTANT: This is also true in case user passes input and output record of the component ($in.0, $out.0)! External Dependencies CTL2 library functions may rely on external libraries. It is necessary to declare such dependencies in library plugin descriptor (plugin.xml) Example: dependencies of StringLib.json2xml <runtime> <library path="bin/"/> <library path="lib/json.jar"/> </runtime> Deprecating Functions Deprecated CTL2 library functions can be annoted by java.lang.Deprecated annotation. This information is used by CloverETL Designer and CTL2 compiler. The compiler will issue a warning about usage of deprecated function. The Designer will render the function in dialogs using strikethrough font. Implementation Implementation of a custom CTL2 function requires the following steps: 1. 2. 3. 4. Implement function library Implement and annotate function method Implement object wrapper (Optional) Implement and annotate function initializer 5. Implement name-to-object lookup 6. Create Engine plugin descriptor Where to start Good starting point are the existing functions from org.jetel.ctl.extensions.StringLib and org.jetel.ctl.extensions.ContainerLib: StringLib.upperCase(): simple function accepting a single paramter and returning a value StringLib.concat(): vararg function accepting any number of String arguments StringLib.find(): function with initializer used to precompile and cache regex patterns ContainerLib.clear(): overloaded function accepting either lists or maps ContainerLib.insert(): vararg function working with lists The plugin descriptor of the StringLib library also shows how to declare external dependencies. Implement function library Create new Eclipse project for your library. Add cloveretl.engine and cloveretl.ctlfunction projects as dependencies The library class should extend the TLFunctionLibrary class Implement and annotate function method Implement your function body as public static method in the library class It is required the first parameter of the method must be of TLFunctionCallContext type. Even in case the actual parameter is not used Use TLFunctionAnnotation(string) to annotate the method as CTL2 function. Provide business description of the function in annotation parameter Implement object wrapper Implement function’s object wrapper as inner class of the library with default access Do NOT declare the inner class as private or static; this prevents CTL2 interpreter to create instances of the object wrapper The object wrapper must extend the TLFunctionPrototype class o Implement the execute() method to retrieve parameters from stack, call corresponding static Java method representing function body and optionally push retrurn value to interpreter stack o Implement the init() method only in case your function uses initializer. In that case call the static Java method representing the initializer and pass it the reference to TLFunctionCallContext from argument (Optional) Implement function initializer Initializer method is not required. Use it in case your function needs special initialization before its execution (e.g. initialize external library, precompile and cache its parameters). See StringLib.find() for reference. Implement the initializer body as public static final method in the library It is required that o The intializer method uses„Init“ suffix after function name it initializes; e.g. findInit() is an initializer method for the find() function o The initializer method declares only single parameter of type TLFunctionCallContext and no return type (void). Use TLFunctionCallContext.setCache() to store cached parameters/references to the context. See descendants of the TLCache class for existing cache implementations. Implement name-to-object lookup Implement the object wrapper lookup in the getExecutable(String) method in your library class: Return an instance of corresponding object wrapper for the function Always return a new instance of the wrapper object Throw IllegalArgumentException(„Unknown function“ + functionName) to handle lookup to non-existent function Create Engine plugin descriptor Your library should be packaged as CloverETL engine: Copy or create a new plugin.xml plugind descriptor in the root of Eclipse project for the library External dependencies o If your library requires external classes it is a good idea to package them as JAR files placed in a „lib“ folder of your project o We recommend placing any 3-rd party to the „lib“ folder as well o Use the <runtime> element to declare all external dependencies of your library o If two custom library function requires two different versions of the same 3rd party library, you have to split them to two CTL2 libraries. All functions withing the same library share the same classpath Extension point o Your must extend the ctlfunction extension point o Provide library name and reference a fully qulified class name to your library as parameters Checking your implementation Is my library and function correctly registered in Engine and Designer? CloverETL Engine reports all discovered plugins during initialization, including the CTL2 function libraries and all introspected functions. In order for the message to appear the DEBUG log level must be enabled. To check if the function is correctly registered in Designer, run eclipsec.exe. This starts Eclipse with console window so that the Engine log is visible. Correctly registered function will appear in the Expression Editor which can be started simply by launching the editor for Filter expression attribute of ExtFilter component. Common errors: Missing annotation on functions Wrong parameters in Java implementation; the first parameter is not of TLFunctionCallContext type Missing or invalid plugin descriptor for the library OK message: DEBUG [main] - Plugin eu.javlin.demolib loaded. id - eu.javlin.demolib version - 0.0.0.devel provider-name - mtomcanyi ctlfunction { libraryName = demo; className = eu.javlin.DemoLibrary; } Error message for missing plugin descriptor: ERROR [main] - IO error occure in plugin manifest reading file:/C:/Projects/CloverETL/workspaces/workspace-librarydemo/cloveretl.demolibrary/plugin.xml. (C:\Projects\CloverETL\workspaces\workspace-librarydemo\cloveretl.demolibrary\plugin.xml (The system cannot find the file specified)) DE