Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

A prior version of the proposal can be found here.


Table of Contents

Introduction

Some functionalities are either not supported or too difficult to express using the DPath expression language. While it is possible to add desired functionality to the codebase, some may be applicable to only a small dataset and won't have widespread use. It also leaves the burden of implementing said functionalities on the Daffodil developers. We would like Daffodil to be able to register and execute external/user defined functions (UDFs) in DPath expressions.

See 

Jira
serverASF JIRA
serverId5aa69414-a9e9-3523-82ec-879b028fb15b
keyDAFFODIL-2186
 for additional information.

Use Cases/Examples

A trivial use case would be adding a replace function callable from a DPath expression. In the DFDL Schema, we might call  something like the below; Where the function will return "Hello_World" if the element resolves to "Hello World". 

...

Code Block
languagejava
@FunctionClassInfo(
	name = "convert_to_hae", 
	namespace = "http://extOther.UDFunction.ElevationConversions.com" 
)
public class MSLConversions {
	public Float evaluate(Integer elevation_msl_raw, Integer hae_adjustment_raw, Float scaling_factor) {
		//implementation..
	}
}

Requirements

  1. The UDF will be defined in a stand-alone library outside of the Daffodil codebase
  2. The UDF must be accessible to and callable from the Daffodil code
  3. Daffodil must be able to process and pass the return value from the UDF back to the Schema
  4. The support of UDFs in the DFDL Schema must be language agnostic and not Java, Scala or Daffodil specific

Proposed Solution

The Daffodil solution will use a combination of JAVA's ServiceLoader and Reflection APIs.

Daffodil Provided Jar

Daffodil will provide a UDFunctionProvider abstract class and an annotation class FunctionClassInfo.

...

The FunctionClassInfo annotation class must be applied and filled in for each function class. It provides name and namespace elements that must be filled out for the function class to be properly identified by the provider when queried by Daffodil .

UDF Implementation

The implementer will be expected to implement at least 2 classes: a provider class and at least one function class.

...

The function classes  will contain the functionality of the UDF embodied in an evaluate method. This class is what will be initialized and returned by the provider class. The function class will be expected to implement an evaluate method as well as apply the Daffodil provided functionClassInfo annotation to the class . Because the parameter types and the return types of the evaluate function are dependent on the functionality, and we really only care about the name, we will not provide an abstract class for it. Each function that the implementer wishes to expose must be defined in a class with an evaluate function, and must be available to the lookup function. See Proposal: User Defined Functions Use Cases/Examples for sample function class.

Daffodil Service Loader

Daffodil will use the ServiceLoader API to poll for UDF Provider classes and return the desired function class on request.

...

Code Block
languagejava
public Object lookupFunctionClass(String namespace, String name) {
	Object funcObj = null;

	try {
		UDFunctionProvider fp = this.functionProviderLookup.get(String.join("_", namespace, name));
		if (fp != null ) {
			funcObj = fp.lookupFunctionClass(namespace, name);
		}
	} catch (Exception e) {
		e.printStackTrace();
	}
	return funcObj;
}

Daffodil DPath Processing

Acquiring the UDFunctionClass

The Internal class referenced above will be instantiated, and its lookup function called with the namespace and name from functionQName object. If it returns null, we'll throw an error, otherwise we'll use Reflection API to query for the parameter type, return type and to find the evaluate function. The below implementation can also be distributed

Code Block
languagescala
val functionClass = functionClassObject.getClass

// infer argument types; this implementation supports overloading evaluate function
val pTypes = argumentsFromDpath.map { o => o.getClass() }
val fEvaluate = functionClass.getMethods.find { p =>
	p.getName == "evaluate" && p.getParameterTypes.sameElements(pTypes)
}

val dResultType = NodeInfo.fromObject(fEvaluate.getReturnType)
val dArgTypes = fEvaluate.getParameterType.map { NodeInfo.fromObject }

Calling the UDFunctionClass

Within the DPath processing code, Daffodil will define 2 case classes, a UDFunctionCallExpr and a UDFunctionCall. The dResultType and dArgTypes arguments for UDFArgsListExpr will be inferred from the functionClass as shown above. The Expression functionObject will be set to the below.

...

Code Block
languagescala
case class UDFunctionCallExpr(nameAsParsed: String, fnQName: RefQName, args: List[Expression],
resultType: NodeInfo.Kind, argsTypes: List[NodeInfo.Kind], constructor: List[CompiledDPath] => RecipeOp)
	extends FunctionCallBase(nameAsParsed, fnQName, args) {

	lazy val argsToArgType = (args zip argsTypes).toMap
	override lazy val inherentType = resultType

	override def targetTypeForSubexpression(childExpr: Expression): NodeInfo.Kind = {
		argsToArgType.get(childExpr) match {
			case Some(argType) => argType
			case None => {
				Assert.invariantFailed("subexpression %s is not an argument.".format(subexp))
			}
		}
	}
	override lazy val compiledDPath = {
		val recipes = args.map { _.compiledDPath }
		val res = new CompiledDPath(constructor(recipes) +: conversions)
		res
	}
}

case class UDFunctionCall(recipes: List[CompiledDPath], functionClassObject: Object, udfEvaluate: Method)
	extends FNArgsList(recipes) {

	override def computeValues(values: List[Any], dstate: DState) = {
		val res = udfEvaluate.invoke(functionClassObject, values: _*) //might need to explicitly cast values to type Object before hand
		res
	}
}

Diagnostics

We intend to supply the user will at least the following errors/warning

  • Warning: No annotation present in function class(es) of \[Provider_ClassName]. Class will be ignored.
  • Error: No User Defined function class with specified name/namespace found. (List all names/namespaces with associated providers)
  • Error: No evaluate function found in function class object

Testing

FocusID
DescriptionTest Data
Expected Result
ServiceLoader API

1
Tests when there are no providers found by the ServiceLoader API due to missing or empty meta-inf fileNo META-INF/Services/org.apache.daffodil.udf.UDFunctionProvider file on classpath of classLoader (CLI Test)
“No user defined functions found.” error
2
Tests when there is an error thrown from ServiceLoader APIMETA-INF/Services/org.apache.daffodil.udf.UDFunctionProvider file contains typo in class name
“ServiceConfigurationError” error
Provider Class

3
Tests when UDF Provider has no function classesUDF with no call to setFunctionClasses initializing functionClasses to array of
classesWarning: Provider Ignored...No function
classes
found
4
Tests when UDF Provider has empty function classUDF with call to setFunctionClasses initializing functionClasses to empty array of classes
Warning: Provider Ignored...No function classes found
Function Class



6
  •  Type your task here, using "@" to assign to a user and "//" to select a due date
5Tests when provider doesn’t implement UDFunctionProviderUDF with Provider that doesn’t extend UDFunctionProvider classProvider not “seen” by DaffodilFunction Class
6
Tests when function classes don’t implement
Serializable class
UserDefinedFunction interfaceUDF with function class that doesn’t
implement Serializable
implement UserDefinedFunction interface
Warning: Provider Ignored...FunctionClass(es) must implement java.io.Serializable
7
Tests when function classes don’t have annotationsUDF with function class that doesn’t
have function class
have UserDefinedFunctionIdentification annotation
Warning: Provider Ignored...Annotations missing for FunctionClass(es)
8
Tests when function classes have empty/invalid annotation fieldsUDF with function class that has annotation function with empty fields
FunctionClass ignored: … “Annotation namespace field is empty or invalid.” and/or “Annotation namespace field is empty or invalid.”
9
Tests when function classes have no evaluate functionUDF with function class whose method isn’t named “evaluate”
“Missing evaluate method for function provided” error
10
Tests when function can’t be foundFunction call from schema with no non either non existent namespace or name
“Function not found” error
Evaluate function




11
Tests when function class have overloaded evaluate functionUDF with
overloaded evaluate function“Only one evaluate method allowed per function class” error
overloaded evaluate function
12
Tests when arguments number incorrectFunction call from schema with incorrect arg number
IllegalArgumentException
13
Tests when argument types incorrectFunction call from schema with incorrect arg type
IllegalArgumentException
14
Tests when argument types unsupportedFunction call from schema with unsupported type (such as Calendar)
“Unsupported object representation type” error
15
Tests when return type unsupportedUDF with unsupported return type such as Array of Arrays
“Unsupported object representation type” error
16
Tests UDF with no argsUDF with no param and static return type
Returns value of static return

17
Tests UDF with no return typeUDF with void return type
Returns null or nil
Primitive Arg/Return Types Testing





18
Tests UDF with primitive int params and returnsUDF with primitive params and return
Return specified type
19
Tests UDF with primitive byte params and returnsUDF with primitive params and return
Return specified type
20
Tests UDF with primitive short params and returnsUDF with primitive params and return
Return specified type
21
Tests UDF with primitive long params and returnsUDF with primitive params and return
Return specified type
22
Tests UDF with primitive double params and returnsUDF with primitive params and return
Return specified type
23
Tests UDF with primitive float params and returnsUDF with primitive params and
returnReturn specified type
return
24
Tests UDF with primitive boolean params and returnsUDF with primitive params and return
Return specified type
Boxed Args/Return Type Testing





25
Tests UDF with Boxed Integer params and returnsUDF with boxed params and return
Return specified type
26
Tests UDF with Boxed Byte params and returnsUDF with boxed params and return
Return specified type
27
Tests UDF with Boxed Short params and returnsUDF with boxed params and return
Return specified type
28
Tests UDF with Boxed Long params and returnsUDF with boxed params and return
Return specified type
29
Tests UDF with Boxed Double params and returnsUDF with boxed params and return
Return specified type
30
Tests UDF with Boxed Float params and returnsUDF with boxed params and return
Return specified type
31
Tests UDF with Boxed Boolean params and returnsUDF with boxed params and return
Return specified type
Other Param/Return Types



32
Tests UDF with Java Big Integer params and returnsUDF with specified params and returns
Return specified type
33
Tests UDF with Java Big Decimal params and returnsUDF with specified params and returns
Return specified type
34
Tests UDF with String params and returnsUDF with specified params and returns
Return specified type
35
Tests UDF with Byte Array params and returnsUDF with specified params and returns
Return specified type
36
  •   
36
Tests UDF with URI params and returnsUDF with specified params and returns
Return specified type

Prototype

UDF Jars: HAEMSLConversions.jar and UDFunctionProviderImpl.jar. Both extend UDFunctionProvider.jar.

...