In addition to the prepared CTL functions, users can build their own CTL functions. To do that, they need to write their own code defining the custom CTL functions and specify its plugin.
Each custom CTL function library must be derived/inherited from org.jetel.interpreter.extensions.TLFunctionLibrary class.
Each custom CTL function must be derived/inhereted from org.jetel.interpreter.extensions.TLFunctionPrototype class.
These classes have some standard operations defined and several abstract methods which need to be defined so as the custom functions may be used.
Within the custom functions code, an existing context must be used or some custom context must be defined. The context serves to store objects when function is to be executed repeatedly, in other words, on more records.
Example of such custom functions code follows:
Code example with comments
package org.mypackage.ctlfunctions; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Collection; import java.util.List; import org.jetel.interpreter.TransformLangExecutorRuntimeException; import org.jetel.interpreter.data.TLNullValue; import org.jetel.interpreter.data.TLNumericValue; import org.jetel.interpreter.data.TLStringValue; import org.jetel.interpreter.data.TLValue; import org.jetel.interpreter.data.TLValueType; import org.jetel.interpreter.extensions.TLContext; import org.jetel.interpreter.extensions.TLFunctionLibrary; import org.jetel.interpreter.extensions.TLFunctionPrototype; public class MyCTLFunctionsLib extends TLFunctionLibrary { private static final String LIBRARY_NAME = "MyCTLFunctions"; enum Function { MY_FUNCTION1("my_function1_name"), MY_FUNCTION2("my_function2_name"); public String name; private Function(String name) { this.name = name; } public static Function fromString(String s) { for(Function function : Function.values()) { if(s.equalsIgnoreCase(function.name) || s.equalsIgnoreCase(LIBRARY_NAME + "." + function.name)) { return function; } } return null; } } public MyCTLFunctionsLib(){ super(); } public TLFunctionPrototype getFunction(String functionName) { switch(Function.fromString(functionName)) { case MY_FUNCTION1: return new MyFunction1(); case MY_FUNCTION2: return new MyFunction2(); default: return null; } } /* (non-Javadoc) * @see org.jetel.interpreter.extensions.ITLFunctionLibrary#getAllFunctions() */ public Collection<TLFunctionPrototype> getAllFunctions() { List<TLFunctionPrototype> ret = new ArrayList<TLFunctionPrototype>(); Function[] fun = Function.values(); for (Function function : fun) { ret.add(getFunction(function.name)); } return ret; } class MyFunction1 extends TLFunctionPrototype { public MyFunction1() { super(LIBRARY_NAME, "my_function1_name", "Description of my function 1", new TLValueType[] { TLValueType.DATE, TLValueType.STRING }, //MyFunction1 has 2 input parameters: 1st is date type,2nd is string type TLValueType.STRING);//MyFunction1 returns string } @Override public TLValue execute(TLValue[] params, TLContext context) {//input parameters and context (the one that is created by createContext() method) //because we use string context, context in context is TLStringValue //we will fulfill the value by the result of our function TLStringValue val = (TLStringValue)context.context; StringBuilder strBuf = (StringBuilder)val.getValue(); strBuf.setLength(0); //checking input parameters' types if (params[0]==TLNullValue.getInstance() || params[1]==TLNullValue.getInstance()) { throw new TransformLangExecutorRuntimeException(params, Function.MY_FUNCTION1.name()+" - NULL value not allowed"); } if (params[0].type!=TLValueType.DATE || params[1].type!=TLValueType.STRING) throw new TransformLangExecutorRuntimeException(params, Function.MY_FUNCTION1.name() + " - wrong type of literal"); String result = "MyFunction1 result"; //TODO fullfil the result strBuf.append(result); //val contains StringBuffer we have just fulfilled return val; } @Override public TLContext createContext() { //You can use one of pre-defined context or create your own context (see next example) // return TLContext.createByteContext(); - contains TLByteArrayValue - with default initial size (INITIAL_BYTE_ARRAY_CAPACITY = 8) // return TLContext.createDateContext(); - contains TLDateValue // return TLContext.createDoubleContext(); - contains TLNumericValue<CloverDouble> - used for storing double values // return TLContext.createIntegerContext(); - contains TLNumericValue<CloverInteger> - used for storing integer values // return TLContext.createListContext(); - contains TLListValue with empty list // return TLContext.createLongContext(); - contains TLNumericValue<CloverLong> - used for storing long values // return TLContext.createNullContext(); - empty context - can be used when there are no object for repeated usage return TLContext.createStringContext();//contains TLStringValue - StringBuffer can be re-used } } class MyFunction2 extends TLFunctionPrototype { public MyFunction2() { super(LIBRARY_NAME, "my_function2_name", "Description of my function 2", new TLValueType[] { TLValueType.DECIMAL, TLValueType.STRING}, //1st input parameter is of type DECIMAL, 2nd (if present - see next line) is of type STRING TLValueType.STRING, 2,1);//MyFunction1 returns string, it accepts 2 or 1 input parameters } @Override public TLValue execute(TLValue[] params, TLContext context) {//input parameters and context (the one that is created by createContext() method) //in context we have TLValue (exactly TLStringValue - see Num2StrContext) and DecimalFormat //- both can be reused in function execution for different records TLValue val = ((Num2StrContext)context.getContext()).value; DecimalFormat format =((Num2StrContext)context.getContext()).format; TLValue input=params[0]; StringBuilder strBuf = (StringBuilder)val.getValue(); strBuf.setLength(0); //checking input parameters if (params[0]==TLNullValue.getInstance()) { throw new TransformLangExecutorRuntimeException(params, Function.MY_FUNCTION2.name()+" - NULL value not allowed"); } Integer radix = null; String formatString = null; //checking number of parameters too if (params.length>1) { if (params[1]==TLNullValue.getInstance()){ throw new TransformLangExecutorRuntimeException(params, Function.MY_FUNCTION2.name()+" - wrong type of literals"); } //if second parameter is numeric we use it as radix if (params[1].getType().isNumeric()) { radix = ((TLNumericValue) params[1]).getInt(); //if second parameter is string we use it as format string }else if (params[1].getType() == TLValueType.STRING) { formatString = params[1].toString(); }else { throw new TransformLangExecutorRuntimeException(params, Function.MY_FUNCTION2.name()+" - wrong type of literals"); } } //body of the function if (radix == null && formatString == null) { radix = 10; } if (radix != null) { if (radix == 10) { strBuf.append(input.toString()); } else { switch(input.type) { case INTEGER: strBuf.append(Integer.toString(((TLNumericValue)input).getInt(),radix)); break; case LONG: strBuf.append(Long.toString(((TLNumericValue)input).getLong(),radix)); break; case NUMBER: strBuf.append(Double.toHexString(((TLNumericValue)input).getDouble())); break; default: throw new TransformLangExecutorRuntimeException(params, Function.MY_FUNCTION2.name()+" - can't convert number to string using specified radix"); } } } else {//radix == null --> formatString != null if (!format.toPattern().equals(formatString)) { format.applyPattern(formatString); } strBuf.append(format.format(((TLNumericValue)input).getDouble())); } //val contains StringBuffer that we have just fulfilled (in the body of the function) return val; } @Override public TLContext createContext() { return Num2StrContext.createContex(); } } } /** * This class demonstrates how to create your own context. * In context you should store objects that can be reused in every execution of your function. * This saves time when executing the same function many times. * * @author Agata Vackova (agata.vackova@javlinconsulting.cz) * (c) Javlin Consulting (www.javlinconsulting.cz) * * @since Feb 25, 2009 */ class Num2StrContext { TLValue value; DecimalFormat format; static TLContext createContex(){ Num2StrContext con=new Num2StrContext(); con.value=TLValue.create(TLValueType.STRING); con.format= new DecimalFormat(); TLContext<Num2StrContext> context=new TLContext<Num2StrContext>(); context.setContext(con); return context; } }
Now the source code needs to be compiled:
javac MyCTLFunctionsLib.javaand received class files can be packed to jar file:
jar cf my_lib.jar *.class
Along with the custom functions code, you also need to define the custom functions plugin. The example follows:
<plugin id="org.mypackage.ctlfunctions" version="1"> <runtime> <library path="my_lib.jar"/> </runtime> <extension point-id="tlfunction"> <parameter id="libraryName" value="MyCTLFunctions"/> <parameter id="className" value="org.mypackage.ctlfunctions.MyCTLFunctionsLib"/> <parameter id="function"> <value>my_function1_name</value> <value>my_function2_name</value> </parameter> </extension> </plugin>
Now you need to put created files to CloverETL plugins directory. It is …\cloverETL\plugins\ for CloverETL Engine or ..\eclipse\plugins\com.cloveretl.gui_x.x.x\lib\plugins\ for CloverETL Designer. Just create new directory and put there plugin.xml and my_lib.jar. If your library requires any other libraries put them in nested lib directory and add them to the runtime tag in plugin.xml file.