This is an automated email from the ASF dual-hosted git repository.
gitgabrio pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-kie-drools.git
The following commit(s) were added to refs/heads/main by this push:
new fbb6c4c7a2 [incubator-kie-issues#1370] DMN: refactor BaseFEELFunction
getCandidateMethod (#6023)
fbb6c4c7a2 is described below
commit fbb6c4c7a2fd8735bf51fd5dceed871e87c9fe09
Author: Gabriele Cardosi <[email protected]>
AuthorDate: Wed Jul 17 09:51:22 2024 +0200
[incubator-kie-issues#1370] DMN: refactor BaseFEELFunction
getCandidateMethod (#6023)
* [incubator-kie-issues#1370] Clean up signature/unused code
* [incubator-kie-issues#1370] WIP. Implemented first unit test
* [incubator-kie-issues#1370] WIP. Implemented unit tests
* [incubator-kie-issues#1370] WIP. Further refactoring with unit tests.
Fully working
* [incubator-kie-issues#1370] WIP. Begin ScorerHelper implementation. Fully
working
* [incubator-kie-issues#1370] WIP. Implemented ScorerHelper with test.
Fully working
* [incubator-kie-issues#1370] WIP. TODO: fix score logic - corner cases not
covered. Issues with singleton list of null element
* [incubator-kie-issues#1370] Working. Implemented tests.
* [incubator-kie-issues#1370] Fully working. Fixing corner cases with
Object parameters
* [incubator-kie-issues#1370] Implemented CustomFunction invocation test
* [incubator-kie-issues#1370] Improvement based on PR suggestion
* [incubator-kie-issues#1370] Fix formatting
* [incubator-kie-issues#1370] Benchmark improving - refactoring
---------
Co-authored-by: Gabriele-Cardosi <[email protected]>
---
.../dmn/feel/runtime/functions/AllFunction.java | 1 -
.../feel/runtime/functions/BaseFEELFunction.java | 389 ++++---------
.../runtime/functions/BaseFEELFunctionHelper.java | 318 +++++++++++
.../dmn/feel/runtime/functions/ScoreHelper.java | 237 ++++++++
.../java/org/kie/dmn/feel/util/CoerceUtil.java | 18 +
.../functions/BaseFEELFunctionHelperTest.java | 378 +++++++++++++
.../runtime/functions/BaseFEELFunctionTest.java | 328 +++++++++++
.../feel/runtime/functions/ScorerHelperTest.java | 610 +++++++++++++++++++++
.../java/org/kie/dmn/feel/util/CoerceUtilTest.java | 41 ++
.../kie-dmn-feel/src/test/resources/logback.xml | 1 +
.../feel/runtime/functions/AvgFunction.java | 2 +-
.../feel/runtime/functions/ConcatFunction.java | 7 +-
12 files changed, 2056 insertions(+), 274 deletions(-)
diff --git
a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/AllFunction.java
b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/AllFunction.java
index fb6bc4cdda..1ae19f9ceb 100644
---
a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/AllFunction.java
+++
b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/AllFunction.java
@@ -67,7 +67,6 @@ public class AllFunction
// Arrays.asList does not accept null as parameter
return FEELFnResult.ofError(new
InvalidParametersEvent(Severity.ERROR, "b", "cannot be null"));
}
-
return invoke( Arrays.asList( list ) );
}
}
diff --git
a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/BaseFEELFunction.java
b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/BaseFEELFunction.java
index 7dcb0a5b26..33922d3450 100644
---
a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/BaseFEELFunction.java
+++
b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/BaseFEELFunction.java
@@ -7,7 +7,7 @@
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
@@ -18,15 +18,11 @@
*/
package org.kie.dmn.feel.runtime.functions;
-import java.lang.annotation.Annotation;
-import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
-import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@@ -43,23 +39,20 @@ import org.kie.dmn.feel.runtime.FEELFunction;
import org.kie.dmn.feel.runtime.events.FEELEventBase;
import org.kie.dmn.feel.runtime.events.InvalidParametersEvent;
import org.kie.dmn.feel.util.Either;
-import org.kie.dmn.feel.util.NumberEvalHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import static org.kie.dmn.feel.util.CoerceUtil.coerceParams;
-
public abstract class BaseFEELFunction
implements FEELFunction {
- private final Logger logger = LoggerFactory.getLogger( getClass() );
+ private final Logger logger = LoggerFactory.getLogger(getClass());
private String name;
private Symbol symbol;
public BaseFEELFunction(String name) {
this.name = name;
- this.symbol = new FunctionSymbol( name, this );
+ this.symbol = new FunctionSymbol(name, this);
}
@Override
@@ -69,7 +62,7 @@ public abstract class BaseFEELFunction
public void setName(String name) {
this.name = name;
- ((FunctionSymbol) this.symbol).setId( name );
+ ((FunctionSymbol) this.symbol).setId(name);
}
@Override
@@ -82,59 +75,71 @@ public abstract class BaseFEELFunction
// use reflection to call the appropriate invoke method
try {
boolean isNamedParams = params.length > 0 && params[0] instanceof
NamedParameter;
- if ( !isCustomFunction() ) {
- List<String> available = null;
- if ( isNamedParams ) {
- available = Stream.of( params ).map( p ->
((NamedParameter) p).getName() ).collect( Collectors.toList() );
- }
+ if (!isCustomFunction()) {
+ CandidateMethod cm = getCandidateMethod(ctx, params,
isNamedParams);
- CandidateMethod cm = getCandidateMethod( ctx, params,
isNamedParams, available );
+ if (cm != null) {
+ Object result = cm.actualMethod.invoke(this,
cm.actualParams);
- if ( cm != null ) {
- Object result = cm.apply.invoke( this, cm.actualParams );
-
- if ( result instanceof Either ) {
+ if (result instanceof Either) {
@SuppressWarnings("unchecked")
Either<FEELEvent, Object> either = (Either<FEELEvent,
Object>) result;
return getEitherResult(ctx,
either,
- () ->
Stream.of(cm.apply.getParameters()).map(p ->
p.getAnnotation(ParameterName.class).value()).collect(Collectors.toList()),
- () ->
Arrays.asList(cm.actualParams));
+ () ->
Stream.of(cm.actualMethod.getParameters()).map(p ->
p.getAnnotation(ParameterName.class).value()).collect(Collectors.toList()),
+ () ->
Arrays.asList(cm.actualParams));
}
return result;
} else {
- // CandidateMethod cm could be null also if reflection
failed on Platforms not supporting getClass().getDeclaredMethods()
+ // CandidateMethod cm could be null also if reflection
failed on Platforms not supporting
+ // getClass().getDeclaredMethods()
String ps = getClass().toString();
- logger.error( "Unable to find function '" + getName() + "(
" + ps.substring( 1, ps.length() - 1 ) + " )'" );
- ctx.notifyEvt(() -> new FEELEventBase(Severity.ERROR,
"Unable to find function '" + getName() + "( " + ps.substring(1, ps.length() -
1) + " )'", null));
+ logger.error("Unable to find function '" + getName() + "(
" + ps.substring(1, ps.length() - 1) +
+ " )'");
+ ctx.notifyEvt(() -> new FEELEventBase(Severity.ERROR,
"Unable to find function '" + getName() +
+ "( " + ps.substring(1, ps.length() - 1) + " )'",
null));
}
} else {
- if ( isNamedParams ) {
- params = rearrangeParameters(params,
this.getParameters().get(0).stream().map(Param::getName).collect(Collectors.toList()));
+ if (isNamedParams) {
+ // This is inherently frail because it expects that, if,
the first parameter is NamedParameter
+ // and the function is a CustomFunction, then all
parameters are NamedParameter
+ NamedParameter[] namedParams =
+
Arrays.stream(params).map(NamedParameter.class::cast).toArray(NamedParameter[]::new);
+ params =
BaseFEELFunctionHelper.rearrangeParameters(namedParams,
+
this.getParameters().get(0).stream().map(Param::getName).collect(Collectors.toList()));
}
- Object result = invoke( ctx, params );
- if ( result instanceof Either ) {
+ Object result = invoke(ctx, params);
+ if (result instanceof Either) {
@SuppressWarnings("unchecked")
Either<FEELEvent, Object> either = (Either<FEELEvent,
Object>) result;
final Object[] usedParams = params;
Object eitherResult = getEitherResult(ctx,
either,
- () -> IntStream.of(
0, usedParams.length ).mapToObj( i -> "arg" + i ).collect( Collectors.toList()
),
- () -> Arrays.asList(
usedParams ));
- return normalizeResult( eitherResult );
+ () ->
IntStream.of(0, usedParams.length).mapToObj(i -> "arg"
+ +
i).collect(Collectors.toList()),
+ () ->
Arrays.asList(usedParams));
+ return
BaseFEELFunctionHelper.normalizeResult(eitherResult);
}
- return normalizeResult( result );
+ return BaseFEELFunctionHelper.normalizeResult(result);
}
- } catch ( Exception e ) {
- logger.error( "Error trying to call function " + getName() + ".",
e );
- ctx.notifyEvt( () -> new FEELEventBase( Severity.ERROR, "Error
trying to call function " + getName() + ".", e ));
+ } catch (Exception e) {
+ logger.error("Error trying to call function " + getName() + ".",
e);
+ ctx.notifyEvt(() -> new FEELEventBase(Severity.ERROR, "Error
trying to call function " + getName() + ".",
+ e));
}
return null;
}
+ @Override
+ public List<List<Param>> getParameters() {
+ // TODO: we could implement this method using reflection, just for
consistency,
+ // but it is not used at the moment
+ return Collections.emptyList();
+ }
+
/**
* this method should be overriden by custom function implementations that
should be invoked reflectively
* @param ctx
@@ -142,274 +147,120 @@ public abstract class BaseFEELFunction
* @return
*/
public Object invoke(EvaluationContext ctx, Object[] params) {
- throw new RuntimeException( "This method should be overriden by
classes that implement custom feel functions" );
- }
-
- private Object getEitherResult(EvaluationContext ctx, Either<FEELEvent,
Object> source, Supplier<List<String>> parameterNamesSupplier,
- Supplier<List<Object>>
parameterValuesSupplier) {
- return source.cata((left) -> {
- ctx.notifyEvt(() -> {
- if (left instanceof InvalidParametersEvent
invalidParametersEvent) {
-
invalidParametersEvent.setNodeName(getName());
-
invalidParametersEvent.setActualParameters(parameterNamesSupplier.get(),
-
parameterValuesSupplier.get());
- }
- return left;
- }
- );
- return null;
- }, Function.identity());
+ throw new RuntimeException("This method should be overriden by classes
that implement custom feel functions");
}
- private Object[] rearrangeParameters(Object[] params, List<String> pnames)
{
- if ( pnames.size() > 0 ) {
- Object[] actualParams = new Object[pnames.size()];
- for ( int i = 0; i < actualParams.length; i++ ) {
- for ( int j = 0; j < params.length; j++ ) {
- if ( ((NamedParameter) params[j]).getName().equals(
pnames.get( i ) ) ) {
- actualParams[i] = ((NamedParameter)
params[j]).getValue();
- break;
- }
+ /**
+ * @param ctx
+ * @param originalInput
+ * @param isNamedParams <code>true</code> if the parameter refers to value
to be retrieved inside
+ * <code>ctx</code>; <code>false</code> if the parameter is the actual
value
+ * @return
+ */
+ protected CandidateMethod getCandidateMethod(EvaluationContext ctx,
Object[] originalInput, boolean isNamedParams) {
+ CandidateMethod toReturn = null;
+ for (Method method : getClass().getDeclaredMethods()) {
+ if (Modifier.isPublic(method.getModifiers()) &&
method.getName().equals("invoke")) {
+ CandidateMethod candidateMethod = getCandidateMethod(ctx,
originalInput, isNamedParams, method);
+ if (candidateMethod == null) {
+ continue;
+ }
+ if (toReturn == null) {
+ toReturn = candidateMethod;
+ } else if (candidateMethod.score > toReturn.score) {
+ toReturn = candidateMethod;
+ } else if (candidateMethod.score == toReturn.score) {
+ toReturn = getBestScoredCandidateMethod(originalInput,
candidateMethod, toReturn);
}
}
- params = actualParams;
}
- return params;
+ return toReturn;
}
- private CandidateMethod getCandidateMethod(EvaluationContext ctx, Object[]
params, boolean isNamedParams, List<String> available) {
- CandidateMethod candidate = null;
- // first, look for exact matches
- for ( Method m : getClass().getDeclaredMethods() ) {
- if ( !Modifier.isPublic(m.getModifiers()) || !m.getName().equals(
"invoke" ) ) {
- continue;
- }
-
- Object[] actualParams;
- boolean injectCtx = Arrays.stream( m.getParameterTypes()
).anyMatch( p -> EvaluationContext.class.isAssignableFrom( p ) );
- if( injectCtx ) {
- actualParams = new Object[ params.length + 1 ];
- int j = 0;
- for (int i = 0; i < m.getParameterCount(); i++) {
- if( EvaluationContext.class.isAssignableFrom(
m.getParameterTypes()[i] ) ) {
- if( isNamedParams ) {
- actualParams[i] = new NamedParameter( "ctx", ctx );
- } else {
- actualParams[i] = ctx;
- }
- } else if (j < params.length) {
- actualParams[i] = params[j];
- j++;
- }
- }
- } else {
- actualParams = params;
- }
- if( isNamedParams ) {
- actualParams = calculateActualParams( ctx, m, actualParams,
available );
- if( actualParams == null ) {
- // incompatible method
- continue;
- }
- }
- CandidateMethod cm = new CandidateMethod( actualParams );
-
- Class<?>[] parameterTypes = m.getParameterTypes();
- if (!isNamedParams && actualParams.length > 0) {
- // if named parameters, then it has been adjusted already in
the calculateActualParams method,
- // otherwise adjust here
- adjustForVariableParameters( cm, parameterTypes );
- }
-
- if ( parameterTypes.length != cm.getActualParams().length ) {
- continue;
- }
+ private CandidateMethod getCandidateMethod(EvaluationContext ctx, Object[]
originalInput,
+ boolean isNamedParams, Method
m) {
+ Object[] adaptedInput =
BaseFEELFunctionHelper.getAdjustedParametersForMethod(ctx, originalInput,
+
isNamedParams, m);
+ if (adaptedInput == null) {
+ // incompatible method
+ return null;
+ }
- boolean found = true;
- for ( int i = 0; i < parameterTypes.length; i++ ) {
- Class<?> currentIdxActualParameterType =
cm.getActualClasses()[i];
- Class<?> expectedParameterType = parameterTypes[i];
- if ( currentIdxActualParameterType != null &&
!expectedParameterType.isAssignableFrom( currentIdxActualParameterType ) ) {
- Optional<Object[]> coercedParams =
coerceParams(currentIdxActualParameterType, expectedParameterType,
actualParams, i);
- if (coercedParams.isPresent()) {
- cm.setActualParams(coercedParams.get());
- continue;
- }
- found = false;
- break;
- }
- }
- if ( found ) {
- cm.setApply( m );
- if (candidate == null) {
- candidate = cm;
- } else {
- if (cm.getScore() > candidate.getScore()) {
- candidate = cm;
- } else if (cm.getScore() == candidate.getScore()) {
- if (isNamedParams &&
nullCount(cm.actualParams)<nullCount(candidate.actualParams)) {
- candidate = cm; // `cm` narrower for named
parameters without need of passing nulls.
- } else if
(candidate.getApply().getParameterTypes().length == 1
- && cm.getApply().getParameterTypes().length ==
1
- &&
candidate.getApply().getParameterTypes()[0].equals(Object.class)
- &&
!cm.getApply().getParameterTypes()[0].equals(Object.class)) {
- candidate = cm; // `cm` is more narrowed, hence
reflect `candidate` to be now `cm`.
- }
- } else {
- // do nothing.
- }
- }
- }
+ Class<?>[] parameterTypes = m.getParameterTypes();
+ if (parameterTypes.length != adaptedInput.length) {
+ return null;
}
- return candidate;
- }
-
- private static long nullCount(Object[] params) {
- return Stream.of(params).filter(x -> x == null).count();
- }
- @Override
- public List<List<Param>> getParameters() {
- // TODO: we could implement this method using reflection, just for
consistency,
- // but it is not used at the moment
- return Collections.emptyList();
+ ScoreHelper.Compares compares = new
ScoreHelper.Compares(originalInput, adaptedInput, parameterTypes);
+ return new CandidateMethod(m, ScoreHelper.grossScore(compares),
adaptedInput);
}
/**
- * Adjust CandidateMethod considering var args signature.
+ * Returns the <b>left</b> <code>CandidateMethod</code> if its
<b>fineScore</b> is greater than the <b>right</b> one,
+ * otherwise returns the <b>right</b> <code>CandidateMethod</code>
+ * @param originalInput
+ * @param left
+ * @param right
+ * @return
*/
- private void adjustForVariableParameters(CandidateMethod cm, Class<?>[]
parameterTypes) {
- if ( parameterTypes.length > 0 && parameterTypes[parameterTypes.length
- 1].isArray() ) {
- // then it is a variable parameters function call
- Object[] newParams = new Object[parameterTypes.length];
- if ( newParams.length > 1 ) {
- System.arraycopy( cm.getActualParams(), 0, newParams, 0,
newParams.length - 1 );
- }
- Object[] remaining = new Object[cm.getActualParams().length -
parameterTypes.length + 1];
- newParams[newParams.length - 1] = remaining;
- System.arraycopy( cm.getActualParams(), parameterTypes.length - 1,
remaining, 0, remaining.length );
- cm.setActualParams( newParams );
- }
- }
-
- private Object[] calculateActualParams(EvaluationContext ctx, Method m,
Object[] params, List<String> available) {
- Annotation[][] pas = m.getParameterAnnotations();
- List<String> names = new ArrayList<>( m.getParameterCount() );
- for ( int i = 0; i < m.getParameterCount(); i++ ) {
- for ( int p = 0; p < pas[i].length; i++ ) {
- if ( pas[i][p] instanceof ParameterName ) {
- names.add( ((ParameterName) pas[i][p]).value() );
- break;
- }
- }
- if ( names.get( i ) == null ) {
- // no name found
- return null;
- }
- }
- Object[] actualParams = new Object[names.size()];
- boolean isVariableParameters = m.getParameterCount() > 0 &&
m.getParameterTypes()[m.getParameterCount()-1].isArray();
- String variableParamPrefix = isVariableParameters ? names.get(
names.size()-1 ) : null;
- List<Object> variableParams = isVariableParameters ? new ArrayList<>(
) : null;
- for ( Object o : params ) {
- NamedParameter np = (NamedParameter) o;
- if( names.contains( np.getName() ) ) {
- actualParams[names.indexOf( np.getName() )] = np.getValue();
- } else if( isVariableParameters ) {
- // check if it is a variable parameters method
- if( np.getName().matches( variableParamPrefix + "\\d+" ) ) {
- int index = Integer.parseInt( np.getName().substring(
variableParamPrefix.length() ) ) - 1;
- if( variableParams.size() <= index ) {
- for( int i = variableParams.size(); i < index; i++ ) {
- // need to add nulls in case the user skipped
indexes
- variableParams.add( null );
- }
- variableParams.add( np.getValue() );
- } else {
- variableParams.set( index, np.getValue() );
- }
- } else {
- // invalid parameter, method is incompatible
- return null;
- }
- } else {
- // invalid parameter, method is incompatible
- return null;
- }
- }
- if( isVariableParameters ) {
- actualParams[ actualParams.length - 1 ] = variableParams.toArray();
- }
-
- return actualParams;
+ private CandidateMethod getBestScoredCandidateMethod(Object[]
originalInput, CandidateMethod left,
+ CandidateMethod
right) {
+
+ ScoreHelper.Compares compares = new
ScoreHelper.Compares(originalInput, left.getActualParams(),
+
left.getParameterTypes());
+ int leftScore = ScoreHelper.fineScore(compares);
+ compares = new ScoreHelper.Compares(originalInput,
right.getActualParams(), right.getParameterTypes());
+ int rightScore = ScoreHelper.fineScore(compares);
+ return leftScore > rightScore ? left : right;
}
- private Object normalizeResult(Object result) {
- // this is to normalize types returned by external functions
- if (result != null && result.getClass().isArray()) {
- List<Object> objs = new ArrayList<>();
- for (int i = 0; i < Array.getLength(result); i++) {
- objs.add(NumberEvalHelper.coerceNumber(Array.get(result, i)));
- }
- return objs;
- } else {
- return NumberEvalHelper.coerceNumber(result);
- }
+ private Object getEitherResult(EvaluationContext ctx, Either<FEELEvent,
Object> source,
+ Supplier<List<String>>
parameterNamesSupplier,
+ Supplier<List<Object>>
parameterValuesSupplier) {
+ return source.cata((left) -> {
+ ctx.notifyEvt(() -> {
+ if (left instanceof InvalidParametersEvent
invalidParametersEvent) {
+
invalidParametersEvent.setNodeName(getName());
+
invalidParametersEvent.setActualParameters(parameterNamesSupplier.get(),
+
parameterValuesSupplier.get());
+ }
+ return left;
+ }
+ );
+ return null;
+ }, Function.identity());
}
protected boolean isCustomFunction() {
return false;
}
- private static class CandidateMethod {
- private Method apply = null;
+ protected static class CandidateMethod {
+
+ private Method actualMethod = null;
private Object[] actualParams;
- private Class[] actualClasses = null;
private int score;
- public CandidateMethod(Object[] actualParams) {
+ public CandidateMethod(Method actualMethod, int score, Object[]
actualParams) {
+ this.actualMethod = actualMethod;
+ this.score = score;
this.actualParams = actualParams;
- populateActualClasses();
- }
-
- private void calculateScore() {
- if ( actualClasses.length > 0 &&
actualClasses[actualClasses.length - 1] != null &&
actualClasses[actualClasses.length - 1].isArray() ) {
- score = 1;
- } else {
- score = 10;
- }
}
- public Method getApply() {
- return apply;
+ public Method getActualMethod() {
+ return actualMethod;
}
- public void setApply(Method apply) {
- this.apply = apply;
- calculateScore();
+ public int getScore() {
+ return score;
}
public Object[] getActualParams() {
return actualParams;
}
- public void setActualParams(Object[] actualParams) {
- this.actualParams = actualParams;
- populateActualClasses();
- }
-
- private void populateActualClasses() {
- this.actualClasses = Stream.of( this.actualParams ).map( p -> p !=
null ? p.getClass() : null ).toArray( Class[]::new );
- }
-
- public Class[] getActualClasses() {
- return actualClasses;
+ public Class<?>[] getParameterTypes() {
+ return actualMethod.getParameterTypes();
}
-
- public int getScore() {
- return score;
- }
-
}
-
}
diff --git
a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/BaseFEELFunctionHelper.java
b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/BaseFEELFunctionHelper.java
new file mode 100644
index 0000000000..327f3fb98c
--- /dev/null
+++
b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/BaseFEELFunctionHelper.java
@@ -0,0 +1,318 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.kie.dmn.feel.runtime.functions;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Array;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import org.kie.dmn.feel.lang.EvaluationContext;
+import org.kie.dmn.feel.lang.impl.NamedParameter;
+import org.kie.dmn.feel.util.NumberEvalHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.kie.dmn.feel.util.CoerceUtil.coerceParams;
+
+public class BaseFEELFunctionHelper {
+
+ private final static Logger logger =
LoggerFactory.getLogger(BaseFEELFunctionHelper.class);
+
+ static Object[] getAdjustedParametersForMethod(EvaluationContext ctx,
Object[] params, boolean isNamedParams,
+ Method m) {
+ logger.trace("getAdjustedParametersForMethod {} {} {} {}", ctx,
params, isNamedParams, m);
+ Object[] toReturn = addCtxParamIfRequired(ctx, params, isNamedParams,
m);
+ Class<?>[] parameterTypes = m.getParameterTypes();
+ if (isNamedParams) {
+ // This is inherently frail because it expects that, if, the first
parameter is NamedParameter and the
+ // function is a CustomFunction, then all parameters are
NamedParameter
+ NamedParameter[] namedParams =
+
Arrays.stream(toReturn).map(NamedParameter.class::cast).toArray(NamedParameter[]::new);
+ toReturn = BaseFEELFunctionHelper.calculateActualParams(m,
namedParams);
+ if (toReturn == null) {
+ // incompatible method
+ return null;
+ }
+ } else if (toReturn.length > 0) {
+ // if named parameters, then it has been adjusted already in the
calculateActualParams method,
+ // otherwise adjust here
+ toReturn = adjustForVariableParameters(toReturn, parameterTypes);
+ }
+ toReturn = adjustByCoercion(parameterTypes, toReturn);
+ return toReturn;
+ }
+
+ /**
+ * This method check if the input parameters, set inside the given
<code>CandidateMethod</code>,
+ * could match the given <code>parameterTypes</code>, eventually
<b>coerced</b>.
+ * In case of match with coercion, a new <code>Object[]</code> with
coerced values is returned.
+ * @param parameterTypes
+ * @param actualParams
+ * @return an <code>Object[]</code>, with values eventually coerced, or
<code>null</code> for incompatible and not coercible values
+ */
+ static Object[] adjustByCoercion(Class<?>[] parameterTypes, Object[]
actualParams) {
+ logger.trace("adjustByCoercion {} {}", parameterTypes, actualParams);
+ Object[] toReturn = actualParams;
+ int counter = Math.min(parameterTypes.length, actualParams.length);
+ for (int i = 0; i < counter; i++) {
+ Class<?> expectedParameterType = parameterTypes[i];
+ Optional<Object[]> coercedParams;
+ Object actualParam = actualParams[i];
+ if (actualParam != null) {
+ Class<?> currentIdxActualParameterType =
actualParam.getClass();
+ if
(expectedParameterType.isAssignableFrom(currentIdxActualParameterType)) {
+ // not null object assignable to expected type: no need to
coerce
+ coercedParams = Optional.of(toReturn);
+ } else {
+ // attempt to coerce
+ coercedParams = coerceParams(currentIdxActualParameterType,
+ expectedParameterType,
toReturn, i);
+ }
+ } else {
+ // null object - no need to coerce
+ coercedParams = Optional.of(toReturn);
+ }
+ if (coercedParams.isPresent()) {
+ toReturn = coercedParams.get();
+ continue;
+ }
+ return null;
+ }
+ return toReturn;
+ }
+
+ /**
+ * This method insert <code>context</code> reference inside the given
parameters, if the given
+ * <code>Method</code> signature include it.
+ * Depending on the <code>isNamedParams</code>, the reference could be the
given <code>EvaluationContext</code>
+ * itself, or a <code>NamedParameter</code>
+ * @param ctx
+ * @param params
+ * @param isNamedParams
+ * @param m
+ * @return
+ */
+ static Object[] addCtxParamIfRequired(EvaluationContext ctx, Object[]
params, boolean isNamedParams, Method m) {
+ logger.trace("addCtxParamIfRequired {} {} {} {}", ctx, params,
isNamedParams, m);
+ Object[] actualParams;
+ // Here, we check if any of the parameters is an EvaluationContext
+ boolean injectCtx =
Arrays.stream(m.getParameterTypes()).anyMatch(EvaluationContext.class::isAssignableFrom);
+ if (injectCtx) {
+ actualParams = new Object[params.length + 1];
+ int j = 0;
+ for (int i = 0; i < m.getParameterCount(); i++) {
+ if
(EvaluationContext.class.isAssignableFrom(m.getParameterTypes()[i])) {
+ if (isNamedParams) {
+ actualParams[i] = new NamedParameter("ctx", ctx);
+ } else {
+ actualParams[i] = ctx;
+ }
+ } else if (j < params.length) {
+ actualParams[i] = params[j];
+ j++;
+ }
+ }
+ } else {
+ actualParams = params;
+ }
+ return actualParams;
+ }
+
+ /**
+ * Method to retrieve the actual parameters from the given
<code>NamedParameter[]</code>
+ * It returns <code>null</code> if the actual parameters does not match
with the <code>Method</code> ones
+ * @param m
+ * @param params
+ * @return an <code>Object[]</code> with mapped values, or
<code>null</code> if the mapping has not been possible
+ * for all <code>params</code>
+ */
+ static Object[] calculateActualParams(Method m, NamedParameter[] params) {
+ logger.trace("calculateActualParams {} {}", m, params);
+ List<String> names = getParametersNames(m);
+ Object[] actualParams = new Object[names.size()];
+ boolean isVariableParameters =
+ m.getParameterCount() > 0 &&
m.getParameterTypes()[m.getParameterCount() - 1].isArray();
+ String variableParamPrefix = isVariableParameters ?
names.get(names.size() - 1) : null;
+ List<Object> variableParams = isVariableParameters ? new ArrayList<>()
: null;
+ for (NamedParameter np : params) {
+ if (!calculateActualParam(np, names, actualParams,
isVariableParameters, variableParamPrefix,
+ variableParams)) {
+ return null;
+ }
+ }
+ if (isVariableParameters) {
+ actualParams[actualParams.length - 1] = variableParams.toArray();
+ }
+ return actualParams;
+ }
+
+ /**
+ * Method to populate the given <code>actualParams</code> or
<code>variableParams</code> with values extracted
+ * from <code>NamedParameter</code>
+ * @param np
+ * @param names
+ * @param actualParams
+ * @param isVariableParameters
+ * @param variableParamPrefix
+ * @param variableParams
+ * @return <code>true</code> if a mapping has been found,
<code>false</code> otherwise
+ */
+ static boolean calculateActualParam(NamedParameter np, List<String> names,
Object[] actualParams,
+ boolean isVariableParameters, String
variableParamPrefix,
+ List<Object> variableParams) {
+ logger.trace("calculateActualParam {} {} {} {} {} {}", np, names,
actualParams, isVariableParameters, variableParamPrefix, variableParams);
+ if (names.contains(np.getName())) {
+ actualParams[names.indexOf(np.getName())] = np.getValue();
+ return true;
+ } else if (isVariableParameters) {
+ return calculateActualParamVariableParameters(np,
variableParamPrefix, variableParams);
+ } else {
+ // invalid parameter, method is incompatible
+ return false;
+ }
+ }
+
+ /**
+ * Method to populate the given <code>variableParams</code> with values
extracted from <code>NamedParameter</code>
+ * @param np
+ * @param variableParamPrefix
+ * @param variableParams
+ * @return <code>true</code> if a mapping has been found,
<code>false</code> otherwise
+ */
+ static boolean calculateActualParamVariableParameters(NamedParameter np,
String variableParamPrefix,
+ List<Object>
variableParams) {
+ logger.trace("calculateActualParamVariableParameters {} {} {}", np,
variableParamPrefix, variableParams);
+ // check if it is a variable parameters method
+ if (np.getName().matches(variableParamPrefix + "\\d+")) {
+ int index =
Integer.parseInt(np.getName().substring(variableParamPrefix.length())) - 1;
+ if (variableParams.size() <= index) {
+ for (int i = variableParams.size(); i < index; i++) {
+ // need to add nulls in case the user skipped indexes
+ variableParams.add(null);
+ }
+ variableParams.add(np.getValue());
+ } else {
+ variableParams.set(index, np.getValue());
+ }
+ } else {
+ // invalid parameter, method is incompatible
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Retrieves the names of the parameters from the given
<code>Method</code>,
+ * from the ones annotated with <code>ParameterName</code>
+ * @param m
+ * @return
+ */
+ static List<String> getParametersNames(Method m) {
+ logger.trace("getParametersNames {}", m);
+ Annotation[][] pas = m.getParameterAnnotations();
+ List<String> toReturn = new ArrayList<>(m.getParameterCount());
+ for (int i = 0; i < m.getParameterCount(); i++) {
+ for (int p = 0; p < pas[i].length; i++) {
+ if (pas[i][p] instanceof ParameterName) {
+ toReturn.add(((ParameterName) pas[i][p]).value());
+ break;
+ }
+ }
+ if (toReturn.get(i) == null) {
+ // no name found
+ return null;
+ }
+ }
+ return toReturn;
+ }
+
+ /**
+ * Method invoked by <code>CustomFunction</code>.
+ * It refactors the input parameters to match the order defined in the
<code>CustomFunction</code>,
+ * returning the actual value of the given <code>params</code>
+ * @param params
+ * @param pnames the parameters defined in the <code>CustomFunction</code>
+ * @return
+ */
+ static Object[] rearrangeParameters(NamedParameter[] params, List<String>
pnames) {
+ logger.trace("rearrangeParameters {} {}", params, pnames);
+ if (pnames.isEmpty()) {
+ return params;
+ } else {
+ Object[] actualParams = new Object[pnames.size()];
+ for (int i = 0; i < actualParams.length; i++) {
+ for (int j = 0; j < params.length; j++) {
+ if (params[j].getName().equals(pnames.get(i))) {
+ actualParams[i] = params[j].getValue();
+ break;
+ }
+ }
+ }
+ return actualParams;
+ }
+ }
+
+ /**
+ * Adjust CandidateMethod considering var args signature.
+ * It converts a series of object to an array, if the last parameter type
is an array.
+ * It is needed to differentiate function(list) from function(n0...nx),
e.g.
+ * sum([1,2,3]) = 6
+ * sum(1,2,3) = 6
+ */
+ static Object[] adjustForVariableParameters(Object[] actualParams,
Class<?>[] parameterTypes) {
+ logger.trace("adjustForVariableParameters {} {}", actualParams,
parameterTypes);
+ if (parameterTypes.length > 0 && parameterTypes[parameterTypes.length
- 1].isArray()) {
+ // then it is a variable parameters function call
+ Object[] toReturn = new Object[parameterTypes.length];
+ if (toReturn.length > 1) {
+ System.arraycopy(actualParams, 0, toReturn, 0, toReturn.length
- 1);
+ }
+ Object[] remaining = new Object[actualParams.length -
parameterTypes.length + 1];
+ toReturn[toReturn.length - 1] = remaining;
+ System.arraycopy(actualParams, parameterTypes.length - 1,
remaining, 0, remaining.length);
+ return toReturn;
+ } else {
+ return actualParams;
+ }
+ }
+
+ /**
+ * This method apply the <code>NumberEvalHelper.coerceNumber</code> to
the given result or,
+ * if it is an array, recursively to all its elements
+ * @param result
+ * @return
+ */
+ static Object normalizeResult(Object result) {
+ logger.trace("normalizeResult {}", result);
+ // this is to normalize types returned by external functions
+ if (result != null && result.getClass().isArray()) {
+ List<Object> objs = new ArrayList<>();
+ for (int i = 0; i < Array.getLength(result); i++) {
+ objs.add(NumberEvalHelper.coerceNumber(Array.get(result, i)));
+ }
+ return objs;
+ } else {
+ return NumberEvalHelper.coerceNumber(result);
+ }
+ }
+}
diff --git
a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/ScoreHelper.java
b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/ScoreHelper.java
new file mode 100644
index 0000000000..1b8b0ac566
--- /dev/null
+++
b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/ScoreHelper.java
@@ -0,0 +1,237 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.kie.dmn.feel.runtime.functions;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.ToIntFunction;
+import java.util.stream.Stream;
+
+import org.kie.dmn.feel.lang.EvaluationContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Class used to evaluate <code>Method</code> score based on the given inputs.
+ * It compares the original input with the "adapted" one to match a given
<code>Method</code>.
+ * For each condition, a value is provided. The top score is obtained when all
the conditions are met.
+ * <p>
+ * Conditions considered (from most to less relevant):
+ * Condition Score
+ * 1. last input not array -> 100000
+ * 1. last parameter not array -> 10000
+ * 2. number of parameters -> 1000
+ * 3. type identity of all parameters -> -> weighted value of matching
parameters and values 0-1000
+ * 4. coerced to varargs -> -10
+ * 4. null counts -> null objects * -1
+ */
+public class ScoreHelper {
+
+ private final static Logger logger =
LoggerFactory.getLogger(ScoreHelper.class);
+
+ private static final List<ToIntFunction<Compares>> GROSS_SCORER_LIST;
+ private static final List<ToIntFunction<Compares>> FINE_SCORER_LIST;
+
+ static int lastInputNotArrayNotArrayScore = 100000;
+ static int lastParameterNotArrayScore = 10000;
+ static int numberOfParametersScore = 1000;
+ static int coercedToVarargsScore = -10;
+
+ static final ToIntFunction<Compares> numberOfParameters = compares -> {
+ int toReturn = compares.originalInput.length ==
compares.parameterTypes.length ? numberOfParametersScore : 0;
+ logger.trace("numberOfParameters {} -> {}", compares, toReturn);
+ return toReturn;
+ };
+
+ static final ToIntFunction<Compares> typeIdentityOfParameters = compares
-> {
+ int index = Math.min(compares.originalInput.length,
compares.parameterTypes.length);
+ boolean automaticallyAddedEvaluationContext =
compares.parameterTypes.length > 0 &&
+ compares.parameterTypes[0] != null &&
+ compares.parameterTypes[0].equals(EvaluationContext.class) &&
+ compares.originalInput.length > 0 &&
+ compares.originalInput[0] != null &&
+ !(compares.originalInput[0] instanceof EvaluationContext);
+ int counter = 0;
+ int matchedEvaluationContext = 0;
+ for (int i = 0; i < index; i++) {
+ if (compares.parameterTypes[i].equals(EvaluationContext.class) &&
+ compares.originalInput[i] != null &&
+ compares.originalInput[i] instanceof EvaluationContext) {
+ // Do not consider EvaluationContext for score
+ matchedEvaluationContext += 1;
+ continue;
+ }
+ // In this case, we switch the parameter comparison, ignoring the
first parameterType
+ int inputIndex = automaticallyAddedEvaluationContext ? i + 1 : i;
+ Class<?> expectedType = compares.parameterTypes[inputIndex];
+ Object originalValue = compares.originalInput[i];
+ Object adaptedValue = compares.adaptedInput != null &&
compares.adaptedInput.length > i ? compares.adaptedInput[i] : null;
+ if (expectedType.equals(Object.class)) {
+ // parameter type is Object
+ counter += 1;
+ }
+ if (originalValue == null) {
+ // null value has a potential match
+ counter += 1;
+ } else if (!(expectedType.isInstance(originalValue)) &&
!expectedType.isInstance(adaptedValue)) {
+ // do not count it
+ continue;
+ } else if (!(expectedType.equals(Object.class))) {
+ if (adaptedValue != null &&
+ (expectedType.equals(adaptedValue.getClass()) ||
+
expectedType.isAssignableFrom(adaptedValue.getClass()))) {
+ counter += 2;
+ } else if (expectedType.equals(originalValue.getClass()) ||
+
expectedType.isAssignableFrom(originalValue.getClass())) {
+ counter += 3;
+ }
+ }
+ logger.trace("typeIdentityOfParameters {} {} -> {}", expectedType,
originalValue, counter);
+ }
+ int elementsToConsider = index - matchedEvaluationContext;
+ int toReturn = counter > 0 ? Math.round(((float) counter /
elementsToConsider) * 500) : 0;
+ logger.trace("typeIdentityOfParameters {} -> {}", compares, toReturn);
+ return toReturn;
+ };
+
+ static final ToIntFunction<Compares> lastInputNotArray =
+ compares -> {
+ int toReturn = isLastInputArray(compares.adaptedInput) ? 0 :
+ lastInputNotArrayNotArrayScore;
+ logger.trace("lastInputNotArray {} -> {}", compares, toReturn);
+ return toReturn;
+ };
+
+ static boolean isLastInputArray(Object[] adaptedInput) {
+ return adaptedInput != null &&
+ adaptedInput.length > 0 &&
+ adaptedInput[adaptedInput.length - 1] != null &&
+ adaptedInput[adaptedInput.length - 1].getClass().isArray();
+ }
+
+ static final ToIntFunction<Compares> lastParameterNotArray =
+ compares -> {
+ int toReturn = isLastParameterArray(compares.parameterTypes) ?
0 :
+ lastParameterNotArrayScore;
+ logger.trace("lastParameterNotArray {} -> {}", compares,
toReturn);
+ return toReturn;
+ };
+
+ static boolean isLastParameterArray(Class<?>[] parameterTypes) {
+ return parameterTypes != null &&
+ parameterTypes.length > 0 &&
+ parameterTypes[parameterTypes.length - 1] != null &&
+ parameterTypes[parameterTypes.length - 1].isArray();
+ }
+
+ static final ToIntFunction<Compares> coercedToVarargs =
+ compares -> {
+ Object[] amendedOriginalInput = compares.originalInput != null
? Arrays.stream(compares.originalInput)
+ .filter(o -> !(o instanceof
EvaluationContext)).toArray() : new Object[0];
+ Object[] amendedAdaptedInput = compares.adaptedInput != null ?
Arrays.stream(compares.adaptedInput)
+ .filter(o -> !(o instanceof
EvaluationContext)).toArray() : new Object[0];
+ int toReturn = 0;
+ if (amendedOriginalInput.length >= amendedAdaptedInput.length
&&
+ amendedAdaptedInput.length == 1 &&
+
isCoercedToVarargs(amendedOriginalInput[amendedOriginalInput.length - 1],
+ amendedAdaptedInput[0])) {
+ toReturn = coercedToVarargsScore;
+ }
+ logger.trace("coercedToVarargs {} -> {}", compares, toReturn);
+ return toReturn;
+ };
+
+ static boolean isCoercedToVarargs(Object originalInput, Object
adaptedInput) {
+ boolean isOriginalInputCandidate =
+ originalInput == null ||
!originalInput.getClass().equals(Object.class.arrayType());
+ boolean isAdaptedInputCandidate =
+ adaptedInput != null &&
adaptedInput.getClass().equals(Object.class.arrayType());
+ return isOriginalInputCandidate && isAdaptedInputCandidate;
+ }
+
+ static final ToIntFunction<Compares> nullCounts =
+ compares -> {
+ int toReturn = nullCount(compares.adaptedInput) * -1;
+ logger.trace("nullCounts {} -> {}", compares, toReturn);
+ return toReturn;
+ };
+
+ static {
+ GROSS_SCORER_LIST = new ArrayList<>();
+ GROSS_SCORER_LIST.add(lastInputNotArray);
+ GROSS_SCORER_LIST.add(lastParameterNotArray);
+
+ FINE_SCORER_LIST = new ArrayList<>();
+ FINE_SCORER_LIST.add(numberOfParameters);
+ FINE_SCORER_LIST.add(typeIdentityOfParameters);
+ FINE_SCORER_LIST.add(coercedToVarargs);
+ FINE_SCORER_LIST.add(nullCounts);
+ }
+
+ static int grossScore(Compares toScore) {
+ int toReturn = GROSS_SCORER_LIST.stream()
+ .mapToInt(comparesIntegerFunction ->
+ comparesIntegerFunction.applyAsInt(toScore))
+ .sum();
+ logger.trace("grossScore {} -> {}", toScore, toReturn);
+ return toReturn;
+ }
+
+ static int fineScore(Compares toScore) {
+ int toReturn = FINE_SCORER_LIST.stream()
+ .mapToInt(comparesIntegerFunction ->
+ comparesIntegerFunction.applyAsInt(toScore))
+ .sum();
+ logger.trace("fineScore {} -> {}", toScore, toReturn);
+ return toReturn;
+ }
+
+ static int nullCount(Object[] params) {
+ int toReturn = params != null ? (int)
Stream.of(params).filter(Objects::isNull).count() : 0;
+ logger.trace("nullCount {} -> {}", params, toReturn);
+ return toReturn;
+ }
+
+ private ScoreHelper() {
+ }
+
+ static class Compares {
+
+ private final Object[] originalInput;
+ private final Object[] adaptedInput;
+ private final Class<?>[] parameterTypes;
+
+ public Compares(Object[] originalInput, Object[] adaptedInput,
Class<?>[] parameterTypes) {
+ this.originalInput = originalInput;
+ this.adaptedInput = adaptedInput;
+ this.parameterTypes = parameterTypes;
+ }
+
+ @Override
+ public String toString() {
+ return "Compares{" +
+ "originalInput=" + Arrays.toString(originalInput) +
+ ", adaptedInput=" + Arrays.toString(adaptedInput) +
+ ", parameterTypes=" + Arrays.toString(parameterTypes) +
+ '}';
+ }
+ }
+}
\ No newline at end of file
diff --git
a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/CoerceUtil.java
b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/CoerceUtil.java
index 421f728249..fa8779609f 100644
--- a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/CoerceUtil.java
+++ b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/CoerceUtil.java
@@ -21,6 +21,7 @@ package org.kie.dmn.feel.util;
import java.time.LocalDate;
import java.time.ZonedDateTime;
import java.util.Collection;
+import java.util.Collections;
import java.util.Optional;
import org.kie.dmn.feel.lang.Type;
@@ -49,6 +50,10 @@ public class CoerceUtil {
static Optional<Object> coerceParam(Class<?>
currentIdxActualParameterType, Class<?> expectedParameterType,
Object actualObject) {
+ /* 10.3.2.9.4 Type conversions
+ from singleton list:
+ When the type of the expression is List<T>, the value of the
expression is a singleton list and the target
+ type is T, the expression is converted by unwrapping the first
element. */
if (Collection.class.isAssignableFrom(currentIdxActualParameterType)) {
Collection<?> valueCollection = (Collection<?>) actualObject;
if (valueCollection.size() == 1) {
@@ -62,6 +67,19 @@ public class CoerceUtil {
}
}
}
+ /* to singleton list:
+ When the type of the expression is T and the target type is List<T>
the expression is converted to a
+ singleton list. */
+ if (!Collection.class.isAssignableFrom(currentIdxActualParameterType)
&&
+ Collection.class.isAssignableFrom(expectedParameterType)) {
+ Object singletonValue = coerceParam(currentIdxActualParameterType,
currentIdxActualParameterType, actualObject)
+ .orElse(actualObject);
+ return Optional.of(Collections.singletonList(singletonValue));
+ }
+ /* from date to date and time
+ When the type of the expression is date and the target type is
date and time, the expression is converted
+ to a date time value in which the time of day is UTC midnight
(00:00:00)
+ */
if (actualObject instanceof LocalDate localDate &&
ZonedDateTime.class.isAssignableFrom(expectedParameterType)) {
Object coercedObject =
DateTimeEvalHelper.coerceDateTime(localDate);
diff --git
a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/BaseFEELFunctionHelperTest.java
b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/BaseFEELFunctionHelperTest.java
new file mode 100644
index 0000000000..39106d29d1
--- /dev/null
+++
b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/BaseFEELFunctionHelperTest.java
@@ -0,0 +1,378 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.kie.dmn.feel.runtime.functions;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.IntStream;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.kie.dmn.feel.codegen.feel11.CodegenTestUtil;
+import org.kie.dmn.feel.lang.EvaluationContext;
+import org.kie.dmn.feel.lang.impl.NamedParameter;
+import org.kie.dmn.feel.runtime.FEELFunction;
+import org.kie.dmn.feel.util.NumberEvalHelper;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class BaseFEELFunctionHelperTest {
+
+ private EvaluationContext ctx;
+
+ @BeforeEach
+ public void setUp() {
+ ctx = CodegenTestUtil.newEmptyEvaluationContext();
+ }
+
+ @Test
+ void getAdjustedParametersForMethod() throws NoSuchMethodException,
InvocationTargetException, IllegalAccessException {
+ // StddevFunction.invoke(@ParameterName( "list" ) List<?> list)
+ Method method = StddevFunction.class.getMethod("invoke", List.class);
+ assertNotNull(method);
+ Object actualValue = Arrays.asList(2, 4, 7, 5);
+ Object[] parameters = {new NamedParameter("list", actualValue)};
+
+ Object[] retrieved =
BaseFEELFunctionHelper.getAdjustedParametersForMethod(ctx, parameters, true,
method);
+ assertNotNull(retrieved);
+ assertEquals(parameters.length, retrieved.length);
+ assertEquals(actualValue, retrieved[0]);
+ }
+
+ @Test
+ void adjustByCoercion() {
+ // no coercion needed
+ Object actualParam = List.of(true, false);
+ Class<?>[] parameterTypes = new Class[]{List.class};
+ Object[] actualParams = {actualParam};
+ Object[] retrieved =
BaseFEELFunctionHelper.adjustByCoercion(parameterTypes, actualParams);
+ assertEquals(actualParams, retrieved);
+
+ actualParam = "StringA";
+ parameterTypes = new Class[]{String.class};
+ actualParams = new Object[]{actualParam};
+ retrieved = BaseFEELFunctionHelper.adjustByCoercion(parameterTypes,
actualParams);
+ assertEquals(actualParams, retrieved);
+
+ // coercing more objects to different types: fails
+ parameterTypes = new Class[]{String.class, Integer.class};
+ actualParams = new Object[]{"String", 34 };
+ retrieved = BaseFEELFunctionHelper.adjustByCoercion(parameterTypes,
actualParams);
+ assertEquals(actualParams, retrieved);
+
+ // not coercing null value to not-list type
+ actualParam = null;
+ actualParams = new Object[]{actualParam};
+ retrieved = BaseFEELFunctionHelper.adjustByCoercion(parameterTypes,
actualParams);
+ assertEquals(actualParams, retrieved);
+
+ // not coercing null value to singleton list
+ parameterTypes = new Class[]{List.class};
+ retrieved = BaseFEELFunctionHelper.adjustByCoercion(parameterTypes,
actualParams);
+ assertEquals(actualParams, retrieved);
+
+ // coercing not-null value to singleton list
+ actualParam = "StringA";
+ actualParams = new Object[]{actualParam};
+ retrieved = BaseFEELFunctionHelper.adjustByCoercion(parameterTypes,
actualParams);
+ assertNotNull(retrieved);
+ assertNotEquals(actualParams, retrieved);
+ assertEquals(1, retrieved.length);
+ assertNotNull(retrieved[0]);
+ assertThat(retrieved[0]).isInstanceOf(List.class);
+ List retrievedList = (List) retrieved[0];
+ assertEquals(1, retrievedList.size());
+ assertEquals(actualParam, retrievedList.get(0));
+
+ // coercing null value to array: fails
+ parameterTypes = new Class[]{Object.class.arrayType()};
+ retrieved = BaseFEELFunctionHelper.adjustByCoercion(parameterTypes,
actualParams);
+ assertNull(retrieved);
+
+ // coercing one object to different type: fails
+ actualParam = 45;
+ parameterTypes = new Class[]{String.class};
+ actualParams = new Object[]{actualParam};
+ retrieved = BaseFEELFunctionHelper.adjustByCoercion(parameterTypes,
actualParams);
+ assertNull(retrieved);
+
+ // coercing more objects to different types: fails
+ parameterTypes = new Class[]{String.class, Integer.class};
+ actualParams = new Object[]{"String", "34" };
+ retrieved = BaseFEELFunctionHelper.adjustByCoercion(parameterTypes,
actualParams);
+ assertNull(retrieved);
+
+ }
+
+ @Test
+ void addCtxParamIfRequired() throws NoSuchMethodException {
+ // AllFunction.invoke(@ParameterName( "list" ) List list)
+ Method method = AllFunction.class.getMethod("invoke", List.class);
+ assertNotNull(method);
+ Object[] parameters = {List.of(true, false)};
+
+ Object[] retrieved = BaseFEELFunctionHelper.addCtxParamIfRequired(ctx,
parameters, true, method);
+ assertNotNull(retrieved);
+ assertEquals(parameters.length, retrieved.length);
+ for (int i = 0; i < parameters.length; i++) {
+ assertEquals(parameters[i], retrieved[i]);
+ }
+
+ // SortFunction.invoke(@ParameterName( "ctx" ) EvaluationContext ctx,
+ // @ParameterName("list")
List list,
+ //
@ParameterName("precedes") FEELFunction function)
+ method = SortFunction.class.getMethod("invoke",
EvaluationContext.class, List.class, FEELFunction.class);
+ assertNotNull(method);
+ parameters = new Object[]{List.of(1, 2), AllFunction.INSTANCE};
+ // direct reference to ctx
+ retrieved = BaseFEELFunctionHelper.addCtxParamIfRequired(ctx,
parameters, false, method);
+ assertNotNull(retrieved);
+ assertEquals(parameters.length + 1, retrieved.length);
+ assertEquals(ctx, retrieved[0]);
+ for (int i = 0; i < parameters.length; i++) {
+ assertEquals(parameters[i], retrieved[i + 1]);
+ }
+
+ // NamedParameter reference to ctx
+ retrieved = BaseFEELFunctionHelper.addCtxParamIfRequired(ctx,
parameters, true, method);
+ assertNotNull(retrieved);
+ assertEquals(parameters.length + 1, retrieved.length);
+ assertEquals(NamedParameter.class, retrieved[0].getClass());
+ NamedParameter retrievedNamedParameter = (NamedParameter) retrieved[0];
+ assertEquals("ctx", retrievedNamedParameter.getName());
+ assertEquals(ctx, retrievedNamedParameter.getValue());
+ for (int i = 0; i < parameters.length; i++) {
+ assertEquals(parameters[i], retrieved[i + 1]);
+ }
+ }
+
+ @Test
+ void calculateActualParams() throws NoSuchMethodException {
+ // CeilingFunction.invoke(@ParameterName( "n" ) BigDecimal n)
+ Method m = CeilingFunction.class.getMethod("invoke", BigDecimal.class);
+ assertNotNull(m);
+ NamedParameter[] parameters = {new NamedParameter("n",
BigDecimal.valueOf(1.5))};
+ Object[] retrieved = BaseFEELFunctionHelper.calculateActualParams(m,
parameters);
+ assertNotNull(retrieved);
+ assertEquals(parameters.length, retrieved.length);
+ assertEquals(parameters[0].getValue(), retrieved[0]);
+
+ parameters = new NamedParameter[]{new NamedParameter("undefined",
BigDecimal.class)};
+ retrieved = BaseFEELFunctionHelper.calculateActualParams(m,
parameters);
+ assertNull(retrieved);
+ }
+
+ @Test
+ void calculateActualParam() {
+ // populate by NamedParameter value
+ NamedParameter np = new NamedParameter("n", BigDecimal.valueOf(1.5));
+ List<String> names = Collections.singletonList("n");
+ Object[] actualParams = new Object[1];
+ boolean isVariableParameters = false;
+ String variableParamPrefix = null;
+ List<Object> variableParams = null;
+ assertTrue(BaseFEELFunctionHelper.calculateActualParam(np, names,
actualParams, isVariableParameters,
+
variableParamPrefix, variableParams));
+ assertEquals(np.getValue(), actualParams[0]);
+
+ np = new NamedParameter("undefined", BigDecimal.valueOf(1.5));
+ actualParams = new Object[1];
+ assertFalse(BaseFEELFunctionHelper.calculateActualParam(np, names,
actualParams, isVariableParameters,
+
variableParamPrefix, variableParams));
+
+ // populate by variableparameters
+ variableParamPrefix = "varPref";
+ int varIndex = 12;
+ np = new NamedParameter(variableParamPrefix + varIndex,
BigDecimal.valueOf(1.5));
+ names = Collections.singletonList("n");
+ actualParams = new Object[1];
+ isVariableParameters = true;
+ variableParams = new ArrayList();
+ assertTrue(BaseFEELFunctionHelper.calculateActualParam(np, names,
actualParams, isVariableParameters,
+
variableParamPrefix, variableParams));
+ assertEquals(varIndex, variableParams.size());
+ for (int i = 0; i < varIndex - 1; i++) {
+ assertNull(variableParams.get(i));
+ }
+ assertEquals(np.getValue(), variableParams.get(varIndex - 1));
+ }
+
+ @Test
+ void calculateActualParamVariableParameters() {
+ // populate by variableparameters
+ String variableParamPrefix = "varPref";
+ int varIndex = 12;
+ NamedParameter np = new NamedParameter(variableParamPrefix + varIndex,
BigDecimal.valueOf(1.5));
+ List<Object> variableParams = new ArrayList<>();
+
assertTrue(BaseFEELFunctionHelper.calculateActualParamVariableParameters(np,
variableParamPrefix,
+
variableParams));
+ assertEquals(varIndex, variableParams.size());
+ for (int i = 0; i < varIndex - 1; i++) {
+ assertNull(variableParams.get(i));
+ }
+ assertEquals(np.getValue(), variableParams.get(varIndex - 1));
+
+ np = new NamedParameter("variableParamPrefix",
BigDecimal.valueOf(1.5));
+ variableParams = new ArrayList<>();
+
assertFalse(BaseFEELFunctionHelper.calculateActualParamVariableParameters(np,
variableParamPrefix,
+
variableParams));
+ }
+
+ @Test
+ void getParametersNames() throws NoSuchMethodException {
+ // SumFunction.invoke(@ParameterName("n") Object[] list)
+ Method m = SumFunction.class.getMethod("invoke",
Object.class.arrayType());
+ assertNotNull(m);
+ List<String> retrieved = BaseFEELFunctionHelper.getParametersNames(m);
+ assertNotNull(retrieved);
+ int counter = 0;
+ Annotation[][] pas = m.getParameterAnnotations();
+ for (Annotation[] annotations : pas) {
+ for (Annotation annotation : annotations) {
+ if (annotation instanceof ParameterName parameterName) {
+ assertEquals(parameterName.value(),
retrieved.get(counter));
+ counter++;
+ }
+ }
+ }
+
+ // DateAndTimeFunction.invoke(@ParameterName( "year" ) Number year,
@ParameterName( "month" ) Number month,
+ // @ParameterName( "day" ) Number day,
+ // @ParameterName(
"hour" ) Number hour, @ParameterName(
+ // "minute" ) Number
minute, @ParameterName( "second" )
+ // Number second,
+ // @ParameterName(
"hour offset" ) Number hourOffset )
+ m = DateAndTimeFunction.class.getMethod("invoke", Number.class,
+ Number.class,
+ Number.class,
+ Number.class,
+ Number.class,
+ Number.class,
+ Number.class);
+ assertNotNull(m);
+ retrieved = BaseFEELFunctionHelper.getParametersNames(m);
+ assertNotNull(retrieved);
+ counter = 0;
+ pas = m.getParameterAnnotations();
+ for (Annotation[] annotations : pas) {
+ for (Annotation annotation : annotations) {
+ if (annotation instanceof ParameterName parameterName) {
+ assertEquals(parameterName.value(),
retrieved.get(counter));
+ counter++;
+ }
+ }
+ }
+ }
+
+ @Test
+ void rearrangeParameters() {
+ NamedParameter[] params = {new NamedParameter("fake", new Object())};
+ Object[] retrieved =
BaseFEELFunctionHelper.rearrangeParameters(params, Collections.emptyList());
+ assertNotNull(retrieved);
+ assertEquals(params, retrieved);
+
+ List<String> pnames = IntStream.range(0, 3)
+ .mapToObj(i -> "Parameter_" + i)
+ .toList();
+
+ // single param in correct position
+ params = new NamedParameter[]{new NamedParameter(pnames.get(0), new
Object())};
+ retrieved = BaseFEELFunctionHelper.rearrangeParameters(params, pnames);
+ assertNotNull(retrieved);
+ assertEquals(pnames.size(), retrieved.length);
+ for (int i = 0; i < retrieved.length; i++) {
+ if (i == 0) {
+ assertEquals(params[0].getValue(), retrieved[i]);
+ } else {
+ assertNull(retrieved[i]);
+ }
+ }
+
+ // single param in wrong position
+ params = new NamedParameter[]{new NamedParameter(pnames.get(2), new
Object())};
+ retrieved = BaseFEELFunctionHelper.rearrangeParameters(params, pnames);
+ assertNotNull(retrieved);
+ assertEquals(pnames.size(), retrieved.length);
+ for (int i = 0; i < retrieved.length; i++) {
+ if (i == 2) {
+ assertEquals(params[0].getValue(), retrieved[i]);
+ } else {
+ assertNull(retrieved[i]);
+ }
+ }
+
+ // reverting the whole order
+ params = new NamedParameter[]{new NamedParameter(pnames.get(2), new
Object()),
+ new NamedParameter(pnames.get(1), new Object()),
+ new NamedParameter(pnames.get(0), new Object())};
+ retrieved = BaseFEELFunctionHelper.rearrangeParameters(params, pnames);
+ assertNotNull(retrieved);
+ assertEquals(pnames.size(), retrieved.length);
+ for (int i = 0; i < retrieved.length; i++) {
+ switch (i) {
+ case 0:
+ assertEquals(params[2].getValue(), retrieved[i]);
+ break;
+ case 1:
+ assertEquals(params[1].getValue(), retrieved[i]);
+ break;
+ case 2:
+ assertEquals(params[0].getValue(), retrieved[i]);
+ break;
+ }
+ }
+ }
+
+ @Test
+ void normalizeResult() {
+ List<Object> originalResult = List.of(3, "4", 56);
+ Object result = originalResult.toArray();
+ Object retrieved = BaseFEELFunctionHelper.normalizeResult(result);
+ assertNotNull(retrieved);
+ assertInstanceOf(List.class, retrieved);
+ List<Object> retrievedList = (List<Object>) retrieved;
+ assertEquals(originalResult.size(), retrievedList.size());
+ for (int i = 0; i < originalResult.size(); i++) {
+ assertEquals(NumberEvalHelper.coerceNumber(originalResult.get(i)),
retrievedList.get(i));
+ }
+
+ result = 23;
+ retrieved = BaseFEELFunctionHelper.normalizeResult(result);
+ assertNotNull(retrieved);
+ assertEquals(NumberEvalHelper.coerceNumber(result), retrieved);
+
+ result = "23";
+ retrieved = BaseFEELFunctionHelper.normalizeResult(result);
+ assertNotNull(retrieved);
+ assertEquals(NumberEvalHelper.coerceNumber(result), retrieved);
+ }
+}
\ No newline at end of file
diff --git
a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/BaseFEELFunctionTest.java
b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/BaseFEELFunctionTest.java
new file mode 100644
index 0000000000..dde34f0d62
--- /dev/null
+++
b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/BaseFEELFunctionTest.java
@@ -0,0 +1,328 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.kie.dmn.feel.runtime.functions;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Parameter;
+import java.math.BigDecimal;
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.temporal.TemporalAccessor;
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.kie.dmn.feel.codegen.feel11.CodegenTestUtil;
+import org.kie.dmn.feel.lang.EvaluationContext;
+import org.kie.dmn.feel.lang.ast.BaseNode;
+import org.kie.dmn.feel.lang.ast.InfixOpNode;
+import org.kie.dmn.feel.lang.ast.InfixOperator;
+import org.kie.dmn.feel.lang.ast.NameRefNode;
+import org.kie.dmn.feel.lang.ast.NullNode;
+import org.kie.dmn.feel.lang.ast.NumberNode;
+import org.kie.dmn.feel.lang.impl.NamedParameter;
+import org.kie.dmn.feel.lang.types.BuiltInType;
+import org.kie.dmn.feel.runtime.FEELFunction;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class BaseFEELFunctionTest {
+
+ private EvaluationContext ctx;
+
+ @BeforeEach
+ public void setUp() {
+ ctx = CodegenTestUtil.newEmptyEvaluationContext();
+ }
+
+ @Test
+ void invokeReflectiveCustomFunction() {
+ List<FEELFunction.Param> parameters = List.of(new
FEELFunction.Param("foo", BuiltInType.UNKNOWN),
+ new
FEELFunction.Param("person's age", BuiltInType.UNKNOWN));
+
+ BaseNode left = new InfixOpNode(InfixOperator.EQ,
+ new NameRefNode(BuiltInType.UNKNOWN,
"foo"),
+ new NullNode(""),
+ "foo = null");
+ BaseNode right = new InfixOpNode(InfixOperator.LT,
+ new NameRefNode(BuiltInType.UNKNOWN,
"person's age"),
+ new NumberNode(BigDecimal.valueOf(18),
"18"),
+ "person's age < 18");
+ BaseNode body = new InfixOpNode(InfixOperator.AND, left, right, "foo =
null and person's age < 18");
+ BaseFEELFunction toTest = new CustomFEELFunction("<anonymous>",
+ parameters,
+ body,
+ ctx);
+ Object[] params = {new NamedParameter("foo", null),
+ new NamedParameter("person's age", 16)};
+ Object retrieved = toTest.invokeReflectively(ctx, params);
+ assertNotNull(retrieved);
+ assertInstanceOf(Boolean.class, retrieved);
+ assertTrue((Boolean) retrieved);
+
+ params = new Object[]{new NamedParameter("foo", null),
+ new NamedParameter("person's age", 19)};
+ retrieved = toTest.invokeReflectively(ctx, params);
+ assertNotNull(retrieved);
+ assertInstanceOf(Boolean.class, retrieved);
+ assertFalse((Boolean) retrieved);
+ }
+
+ @Test
+ void getAllFunctionCandidateMethod() {
+ BaseFEELFunction toTest = AllFunction.INSTANCE;
+
+ // invoke(@ParameterName( "list" ) List list)
+ Object[] parameters = {List.of(true, false)};
+ BaseFEELFunction.CandidateMethod candidateMethodRetrieved =
toTest.getCandidateMethod(ctx, parameters, false);
+ assertNotNull(candidateMethodRetrieved);
+ Method retrieved = candidateMethodRetrieved.getActualMethod();
+ assertNotNull(retrieved);
+ assertTrue(Modifier.isPublic(retrieved.getModifiers()));
+ assertEquals("invoke", retrieved.getName());
+ Parameter[] parametersRetrieved = retrieved.getParameters();
+ assertNotNull(parametersRetrieved);
+ assertEquals(1, parametersRetrieved.length);
+ assertEquals(List.class, parametersRetrieved[0].getType());
+
+ // invoke(@ParameterName( "b" ) Object[] list)
+ parameters = new Object[]{true, false};
+ candidateMethodRetrieved = toTest.getCandidateMethod(ctx, parameters,
false);
+ assertNotNull(candidateMethodRetrieved);
+ retrieved = candidateMethodRetrieved.getActualMethod();
+ assertNotNull(retrieved);
+ assertTrue(Modifier.isPublic(retrieved.getModifiers()));
+ assertEquals("invoke", retrieved.getName());
+ parametersRetrieved = retrieved.getParameters();
+ assertNotNull(parametersRetrieved);
+ assertEquals(1, parametersRetrieved.length);
+ assertEquals(Object.class.arrayType(),
parametersRetrieved[0].getType());
+ }
+
+ @Test
+ void getDateAndTimeFunctionCandidateMethod() {
+ BaseFEELFunction toTest = DateAndTimeFunction.INSTANCE;
+
+ // invoke(@ParameterName( "from" ) String val)
+ Object[] parameters = {"2017-09-07T10:20:30"};
+ BaseFEELFunction.CandidateMethod candidateMethodRetrieved =
toTest.getCandidateMethod(ctx, parameters, false);
+ assertNotNull(candidateMethodRetrieved);
+ Method retrieved = candidateMethodRetrieved.getActualMethod();
+ assertNotNull(retrieved);
+ assertTrue(Modifier.isPublic(retrieved.getModifiers()));
+ assertEquals("invoke", retrieved.getName());
+ Parameter[] parametersRetrieved = retrieved.getParameters();
+ assertNotNull(parametersRetrieved);
+ assertEquals(1, parametersRetrieved.length);
+ assertEquals(String.class, parametersRetrieved[0].getType());
+
+ // invoke(@ParameterName( "date" ) TemporalAccessor date,
@ParameterName( "time" ) TemporalAccessor time)
+ parameters = new Object[]{LocalDate.of(2017, 6, 12), LocalTime.of(10,
6, 20)};
+ candidateMethodRetrieved = toTest.getCandidateMethod(ctx, parameters,
false);
+ assertNotNull(candidateMethodRetrieved);
+ retrieved = candidateMethodRetrieved.getActualMethod();
+ assertNotNull(retrieved);
+ assertTrue(Modifier.isPublic(retrieved.getModifiers()));
+ assertEquals("invoke", retrieved.getName());
+ parametersRetrieved = retrieved.getParameters();
+ assertNotNull(parametersRetrieved);
+ assertEquals(2, parametersRetrieved.length);
+ Arrays.stream(parametersRetrieved).forEach(parameter ->
assertEquals(TemporalAccessor.class,
+
parameter.getType()));
+
+// invoke(@ParameterName( "year" ) Number year, @ParameterName( "month"
) Number month, @ParameterName( "day"
+// ) Number day,
+// @ParameterName( "hour" ) Number hour, @ParameterName(
"minute" ) Number minute, @ParameterName(
+// "second" ) Number second )
+ parameters = new Object[]{2017, 6, 12, 10, 6, 20};
+ candidateMethodRetrieved = toTest.getCandidateMethod(ctx, parameters,
false);
+ assertNotNull(candidateMethodRetrieved);
+ retrieved = candidateMethodRetrieved.getActualMethod();
+ assertNotNull(retrieved);
+ assertTrue(Modifier.isPublic(retrieved.getModifiers()));
+ assertEquals("invoke", retrieved.getName());
+ parametersRetrieved = retrieved.getParameters();
+ assertNotNull(parametersRetrieved);
+ assertEquals(6, parametersRetrieved.length);
+ Arrays.stream(parametersRetrieved).forEach(parameter ->
assertEquals(Number.class, parameter.getType()));
+
+// invoke(@ParameterName( "year" ) Number year, @ParameterName( "month"
) Number month, @ParameterName( "day"
+// ) Number day,
+// @ParameterName( "hour" ) Number hour, @ParameterName(
"minute" ) Number minute, @ParameterName(
+// "second" ) Number second,
+// @ParameterName( "hour offset" ) Number hourOffset )
+ parameters = new Object[]{2017, 6, 12, 10, 6, 20, 2};
+ candidateMethodRetrieved = toTest.getCandidateMethod(ctx, parameters,
false);
+ assertNotNull(candidateMethodRetrieved);
+ retrieved = candidateMethodRetrieved.getActualMethod();
+ assertNotNull(retrieved);
+ assertTrue(Modifier.isPublic(retrieved.getModifiers()));
+ assertEquals("invoke", retrieved.getName());
+ parametersRetrieved = retrieved.getParameters();
+ assertNotNull(parametersRetrieved);
+ assertEquals(7, parametersRetrieved.length);
+ Arrays.stream(parametersRetrieved).forEach(parameter ->
assertEquals(Number.class, parameter.getType()));
+
+// invoke(@ParameterName( "year" ) Number year, @ParameterName( "month"
) Number month, @ParameterName( "day"
+// ) Number day,
+// @ParameterName( "hour" ) Number hour, @ParameterName(
"minute" ) Number minute, @ParameterName(
+// "second" ) Number second,
+// @ParameterName( "timezone" ) String timezone )
+ parameters = new Object[]{2017, 6, 12, 10, 6, 20, "Europe/Paris"};
+ candidateMethodRetrieved = toTest.getCandidateMethod(ctx, parameters,
false);
+ assertNotNull(candidateMethodRetrieved);
+ retrieved = candidateMethodRetrieved.getActualMethod();
+ assertNotNull(retrieved);
+ assertTrue(Modifier.isPublic(retrieved.getModifiers()));
+ assertEquals("invoke", retrieved.getName());
+ parametersRetrieved = retrieved.getParameters();
+ assertNotNull(parametersRetrieved);
+ assertEquals(7, parametersRetrieved.length);
+ for (int i = 0; i < 6; i++) {
+ assertEquals(Number.class, parametersRetrieved[i].getType());
+ }
+ assertEquals(String.class, parametersRetrieved[6].getType());
+ }
+
+ @Test
+ void getExtendedTimeFunctionCandidateMethod() {
+ BaseFEELFunction toTest =
org.kie.dmn.feel.runtime.functions.extended.TimeFunction.INSTANCE;
+
+ // invoke(@ParameterName( "from" ) String val)
+ Object[] parameters = {"10:20:30"};
+ BaseFEELFunction.CandidateMethod candidateMethodRetrieved =
toTest.getCandidateMethod(ctx, parameters, false);
+ assertNotNull(candidateMethodRetrieved);
+ Method retrieved = candidateMethodRetrieved.getActualMethod();
+ assertNotNull(retrieved);
+ assertTrue(Modifier.isPublic(retrieved.getModifiers()));
+ assertEquals("invoke", retrieved.getName());
+ Parameter[] parametersRetrieved = retrieved.getParameters();
+ assertNotNull(parametersRetrieved);
+ assertEquals(1, parametersRetrieved.length);
+ assertEquals(String.class, parametersRetrieved[0].getType());
+
+// invoke(
+// @ParameterName("hour") Number hour, @ParameterName("minute")
Number minute,
+// @ParameterName("second") Number seconds)
+ parameters = new Object[]{10, 6, 20};
+ candidateMethodRetrieved = toTest.getCandidateMethod(ctx, parameters,
false);
+ assertNotNull(candidateMethodRetrieved);
+ retrieved = candidateMethodRetrieved.getActualMethod();
+ assertNotNull(retrieved);
+ assertTrue(Modifier.isPublic(retrieved.getModifiers()));
+ assertEquals("invoke", retrieved.getName());
+ parametersRetrieved = retrieved.getParameters();
+ assertNotNull(parametersRetrieved);
+ assertEquals(3, parametersRetrieved.length);
+ Arrays.stream(parametersRetrieved).forEach(parameter ->
assertEquals(Number.class, parameter.getType()));
+
+// invoke(
+// @ParameterName("hour") Number hour, @ParameterName("minute")
Number minute,
+// @ParameterName("second") Number seconds,
@ParameterName("offset") Duration offset)
+ parameters = new Object[]{10, 6, 20, Duration.ofHours(3)};
+ candidateMethodRetrieved = toTest.getCandidateMethod(ctx, parameters,
false);
+ assertNotNull(candidateMethodRetrieved);
+ retrieved = candidateMethodRetrieved.getActualMethod();
+ assertNotNull(retrieved);
+ assertTrue(Modifier.isPublic(retrieved.getModifiers()));
+ assertEquals("invoke", retrieved.getName());
+ parametersRetrieved = retrieved.getParameters();
+ assertNotNull(parametersRetrieved);
+ assertEquals(4, parametersRetrieved.length);
+ for (int i = 0; i < 3; i++) {
+ assertEquals(Number.class, parametersRetrieved[i].getType());
+ }
+ assertEquals(Duration.class, parametersRetrieved[3].getType());
+
+// invoke(@ParameterName("from") TemporalAccessor date
+ parameters = new Object[]{LocalTime.of(10, 6, 20)};
+ candidateMethodRetrieved = toTest.getCandidateMethod(ctx, parameters,
false);
+ assertNotNull(candidateMethodRetrieved);
+ retrieved = candidateMethodRetrieved.getActualMethod();
+ assertNotNull(retrieved);
+ assertTrue(Modifier.isPublic(retrieved.getModifiers()));
+ assertEquals("invoke", retrieved.getName());
+ parametersRetrieved = retrieved.getParameters();
+ assertNotNull(parametersRetrieved);
+ assertEquals(1, parametersRetrieved.length);
+ assertEquals(TemporalAccessor.class, parametersRetrieved[0].getType());
+ }
+
+ @Test
+ void getSortFunctionCandidateMethod() {
+ BaseFEELFunction toTest = SortFunction.INSTANCE;
+
+ // invoke(@ParameterName( "ctx" ) EvaluationContext ctx,
+ // @ParameterName("list")
List list,
+ //
@ParameterName("precedes") FEELFunction function
+ Object[] parameters = {List.of(1, 2), AllFunction.INSTANCE};
+ BaseFEELFunction.CandidateMethod candidateMethodRetrieved =
toTest.getCandidateMethod(ctx, parameters, false);
+ assertNotNull(candidateMethodRetrieved);
+ Method retrieved = candidateMethodRetrieved.getActualMethod();
+ assertNotNull(retrieved);
+ assertTrue(Modifier.isPublic(retrieved.getModifiers()));
+ assertEquals("invoke", retrieved.getName());
+ Parameter[] parametersRetrieved = retrieved.getParameters();
+ assertNotNull(parametersRetrieved);
+ assertEquals(3, parametersRetrieved.length);
+ assertEquals(EvaluationContext.class,
parametersRetrieved[0].getType());
+ assertEquals(List.class, parametersRetrieved[1].getType());
+ assertEquals(FEELFunction.class, parametersRetrieved[2].getType());
+
+ // invoke(@ParameterName("list") List list)
+ parameters = new Object[]{List.of(1, 3, 5)};
+ candidateMethodRetrieved = toTest.getCandidateMethod(ctx, parameters,
false);
+ assertNotNull(candidateMethodRetrieved);
+ retrieved = candidateMethodRetrieved.getActualMethod();
+ assertNotNull(retrieved);
+ assertTrue(Modifier.isPublic(retrieved.getModifiers()));
+ assertEquals("invoke", retrieved.getName());
+ parametersRetrieved = retrieved.getParameters();
+ assertNotNull(parametersRetrieved);
+ assertEquals(1, parametersRetrieved.length);
+ assertEquals(List.class, parametersRetrieved[0].getType());
+ }
+
+ @Test
+ void getStddevFunctionCandidateMethod() {
+ BaseFEELFunction toTest = StddevFunction.INSTANCE;
+
+ // invoke(@ParameterName("list") List<?> list)
+ Object actualValue = Arrays.asList(2, 4, 7, 5);
+ Object[] parameters = {new NamedParameter("list", actualValue)};
+ BaseFEELFunction.CandidateMethod candidateMethodRetrieved =
toTest.getCandidateMethod(ctx, parameters, false);
+ assertNotNull(candidateMethodRetrieved);
+ Method retrieved = candidateMethodRetrieved.getActualMethod();
+ assertNotNull(retrieved);
+ assertTrue(Modifier.isPublic(retrieved.getModifiers()));
+ assertEquals("invoke", retrieved.getName());
+ Parameter[] parametersRetrieved = retrieved.getParameters();
+ assertNotNull(parametersRetrieved);
+ assertEquals(1, parametersRetrieved.length);
+ assertEquals(List.class, parametersRetrieved[0].getType());
+ }
+
+}
\ No newline at end of file
diff --git
a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/ScorerHelperTest.java
b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/ScorerHelperTest.java
new file mode 100644
index 0000000000..396e0a0356
--- /dev/null
+++
b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/ScorerHelperTest.java
@@ -0,0 +1,610 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.kie.dmn.feel.runtime.functions;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+
+import org.junit.jupiter.api.Test;
+import org.kie.dmn.feel.codegen.feel11.CodegenTestUtil;
+import org.kie.dmn.feel.lang.EvaluationContext;
+import org.kie.dmn.feel.lang.impl.NamedParameter;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static
org.kie.dmn.feel.runtime.functions.ScoreHelper.coercedToVarargsScore;
+import static
org.kie.dmn.feel.runtime.functions.ScoreHelper.lastInputNotArrayNotArrayScore;
+import static
org.kie.dmn.feel.runtime.functions.ScoreHelper.lastParameterNotArrayScore;
+import static
org.kie.dmn.feel.runtime.functions.ScoreHelper.numberOfParametersScore;
+
+class ScorerHelperTest {
+
+ @Test
+ void grossScore() {
+ Object[] originalInput = new Object[] { "String" };
+ Class<?>[] parameterTypes = new Class<?>[] { String.class };
+ Object[] adaptedInput = new Object[] { "String" };
+ ScoreHelper.Compares compares = new
ScoreHelper.Compares(originalInput, adaptedInput, parameterTypes);
+ int retrieved = ScoreHelper.grossScore(compares);
+ // 10000 (lastParameterNotArray)
+ // 100000 (lastInputNotArray)
+ int expected = lastInputNotArrayNotArrayScore +
lastParameterNotArrayScore;
+ assertEquals(expected, retrieved);
+
+ originalInput = new Object[] { "String", "34" };
+ parameterTypes = new Class<?>[] { String.class, Integer.class };
+ adaptedInput = new Object[] { "String", null };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrieved = ScoreHelper.grossScore(compares);
+ // 10000 (lastParameterNotArray)
+ // 100000 (lastInputNotArray)
+ expected = lastInputNotArrayNotArrayScore + lastParameterNotArrayScore;
+ assertEquals(expected, retrieved);
+
+ originalInput = new Object[] { "String", "34" };
+ parameterTypes = new Class<?>[] { String.class, Integer.class };
+ adaptedInput = null;
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrieved = ScoreHelper.grossScore(compares);
+ // 10000 (lastParameterNotArray)
+ // 100000 (lastInputNotArray)
+ expected = lastInputNotArrayNotArrayScore + lastParameterNotArrayScore;
+ assertEquals(expected, retrieved);
+
+ originalInput = new Object[] { "StringA", "StringB" };
+ parameterTypes = new Class<?>[] { String.class, String.class };
+ adaptedInput = new Object[] { "StringA", "StringB" };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrieved = ScoreHelper.grossScore(compares);
+ // 10000 (lastParameterNotArray)
+ // 100000 (lastInputNotArray)
+ expected = lastInputNotArrayNotArrayScore + lastParameterNotArrayScore;
+ assertEquals(expected, retrieved);
+
+ originalInput = new Object[] { "StringA", "StringB" };
+ parameterTypes = new Class<?>[] { Object.class.arrayType() };
+ adaptedInput = new Object[] { new Object[] {"StringA", "StringB"} };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrieved = ScoreHelper.grossScore(compares);
+ // 0 (lastParameterNotArray)
+ // 0 (lastInputNotArray)
+ expected = 0;
+ assertEquals( expected, retrieved);
+
+
+ originalInput = new Object[] { "StringA" };
+ parameterTypes = new Class<?>[] { Object.class.arrayType() };
+ adaptedInput = new Object[] { new Object[] {"StringA"} };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrieved = ScoreHelper.grossScore(compares);
+ // 0 (lastParameterNotArray)
+ // 0 (lastInputNotArray)
+ expected = 0;
+ assertEquals( expected, retrieved);
+
+ originalInput = new Object[] { "StringA" };
+ parameterTypes = new Class<?>[] { List.class };
+ adaptedInput = new Object[] { List.of("StringA") };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ int retrievedToCompare = ScoreHelper.grossScore(compares);
+ // 10000 (lastParameterNotArray)
+ // 100000 (lastInputNotArray)
+ expected = lastInputNotArrayNotArrayScore + lastParameterNotArrayScore;
+ assertEquals( expected, retrievedToCompare);
+ assertThat(retrievedToCompare).isGreaterThan(retrieved);
+
+
+ originalInput = new Object[] { null };
+ parameterTypes = new Class<?>[] { Object.class.arrayType() };
+ adaptedInput = new Object[] { null };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrieved = ScoreHelper.grossScore(compares);
+ // 0 (lastParameterNotArray)
+ // 100000 (lastInputNotArray)
+ expected = lastInputNotArrayNotArrayScore;
+ assertEquals( expected, retrieved);
+
+ originalInput = new Object[] { null };
+ parameterTypes = new Class<?>[] { List.class };
+ adaptedInput = new Object[] { null };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrievedToCompare = ScoreHelper.grossScore(compares);
+ // 10000 (lastParameterNotArray)
+ // 100000 (lastInputNotArray)
+ expected = lastInputNotArrayNotArrayScore + lastParameterNotArrayScore;
+ assertEquals( expected, retrievedToCompare);
+ assertThat(retrievedToCompare).isGreaterThan(retrieved);
+
+ Object actualValue = Arrays.asList(2, 4, 7, 5);
+ originalInput = new Object[] {new NamedParameter("list", actualValue)};
+ parameterTypes = new Class<?>[] { List.class };
+ adaptedInput = new Object[] { actualValue };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrieved = ScoreHelper.grossScore(compares);
+ // 10000 (lastParameterNotArray)
+ // 100000 (lastInputNotArray)
+ expected = lastInputNotArrayNotArrayScore + lastParameterNotArrayScore;
+ assertEquals( expected, retrieved);
+
+ parameterTypes = new Class<?>[] { Object.class };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrievedToCompare = ScoreHelper.grossScore(compares);
+ // 10000 (lastParameterNotArray)
+ // 100000 (lastInputNotArray)
+ expected = lastInputNotArrayNotArrayScore + lastParameterNotArrayScore;
+ assertEquals( expected, retrievedToCompare);
+ }
+
+ @Test
+ void fineScore() {
+ Object[] originalInput = new Object[] { "String" };
+ Class<?>[] parameterTypes = new Class<?>[] { String.class };
+ Object[] adaptedInput = new Object[] { "String" };
+ ScoreHelper.Compares compares = new
ScoreHelper.Compares(originalInput, adaptedInput, parameterTypes);
+ int retrieved = ScoreHelper.fineScore(compares);
+ // 0 (nullCounts)
+ // 0 (coercedToVarargs)
+ // 1000 (typeIdentityOfParameters)
+ // 1000 (numberOfParameters)
+ int expected = numberOfParametersScore + 1000;
+ assertEquals(expected, retrieved);
+
+ originalInput = new Object[] { "String", "34" };
+ parameterTypes = new Class<?>[] { String.class, Integer.class };
+ adaptedInput = new Object[] { "String", null };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrieved = ScoreHelper.fineScore(compares);
+ // -1 (nullCounts)
+ // 0 (coercedToVarargs)
+ // 500 (typeIdentityOfParameters)
+ // 1000 (numberOfParameters)
+ expected = numberOfParametersScore + 500 -1;
+ assertEquals(expected, retrieved);
+
+ originalInput = new Object[] { "String", "34" };
+ parameterTypes = new Class<?>[] { String.class, Integer.class };
+ adaptedInput = null;
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrieved = ScoreHelper.fineScore(compares);
+ // 0 (nullCounts)
+ // 0 (coercedToVarargs)
+ // 750 (typeIdentityOfParameters)
+ // 1000 (numberOfParameters)
+ expected = numberOfParametersScore + 750;
+ assertEquals(expected, retrieved);
+
+ originalInput = new Object[] { "StringA", "StringB" };
+ parameterTypes = new Class<?>[] { String.class, String.class };
+ adaptedInput = new Object[] { "StringA", "StringB" };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrieved = ScoreHelper.fineScore(compares);
+ // 0 (nullCounts)
+ // 0 (coercedToVarargs)
+ // 1000 (typeIdentityOfParameters)
+ // 1000 (numberOfParameters)
+ expected = numberOfParametersScore + 1000;
+ assertEquals(expected, retrieved);
+
+ originalInput = new Object[] { "StringA", "StringB" };
+ parameterTypes = new Class<?>[] { Object.class.arrayType() };
+ adaptedInput = new Object[] { new Object[] {"StringA", "StringB"} };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrieved = ScoreHelper.fineScore(compares);
+ // 0 (nullCounts)
+ // -10 (coercedToVarargs)
+ // 1000 (typeIdentityOfParameters)
+ // 0 (numberOfParameters)
+ expected = coercedToVarargsScore + 1000;
+ assertEquals( expected, retrieved);
+
+
+ originalInput = new Object[] { "StringA" };
+ parameterTypes = new Class<?>[] { Object.class.arrayType() };
+ adaptedInput = new Object[] { new Object[] {"StringA"} };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrieved = ScoreHelper.fineScore(compares);
+ // 0 (nullCounts)
+ // -10 (coercedToVarargs)
+ // 1000 (typeIdentityOfParameters)
+ // 1000 (numberOfParameters)
+ expected = numberOfParametersScore + coercedToVarargsScore + 1000;
+ assertEquals( expected, retrieved);
+
+ originalInput = new Object[] { "StringA" };
+ parameterTypes = new Class<?>[] { List.class };
+ adaptedInput = new Object[] { List.of("StringA") };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ int retrievedToCompare = ScoreHelper.fineScore(compares);
+ // 0 (nullCounts)
+ // 0 (coercedToVarargs)
+ // 1000 (typeIdentityOfParameters)
+ // 1000 (numberOfParameters)
+ expected = numberOfParametersScore + 1000;
+ assertEquals( expected, retrievedToCompare);
+ assertThat(retrievedToCompare).isGreaterThan(retrieved);
+
+
+ originalInput = new Object[] { null };
+ parameterTypes = new Class<?>[] { Object.class.arrayType() };
+ adaptedInput = new Object[] { null };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrieved = ScoreHelper.fineScore(compares);
+ // -1 (nullCounts)
+ // 0 (coercedToVarargs)
+ // 500 (typeIdentityOfParameters)
+ // 1000 (numberOfParameters)
+ expected = numberOfParametersScore + 500 -1;
+ assertEquals( expected, retrieved);
+
+ originalInput = new Object[] { null };
+ parameterTypes = new Class<?>[] { List.class };
+ adaptedInput = new Object[] { null };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrieved = ScoreHelper.fineScore(compares);
+ // -1 (nullCounts)
+ // 0 (coercedToVarargs)
+ // 500 (typeIdentityOfParameters)
+ // 1000 (numberOfParameters)
+ expected = numberOfParametersScore + 500 -1;
+ assertEquals( expected, retrieved);
+
+ Object actualValue = Arrays.asList(2, 4, 7, 5);
+ originalInput = new Object[] {new NamedParameter("list", actualValue)};
+ parameterTypes = new Class<?>[] { List.class };
+ adaptedInput = new Object[] { actualValue };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrieved = ScoreHelper.fineScore(compares);
+ // 0 (nullCounts)
+ // 0 (coercedToVarargs)
+ // 1000 (typeIdentityOfParameters)
+ // 1000 (numberOfParameters)
+ expected = numberOfParametersScore + 1000;
+ assertEquals( expected, retrieved);
+
+ parameterTypes = new Class<?>[] { Object.class };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrieved = ScoreHelper.fineScore(compares);
+ // 0 (nullCounts)
+ // 0 (coercedToVarargs)
+ // 500 (typeIdentityOfParameters)
+ // 1000 (numberOfParameters)
+ expected = numberOfParametersScore + 500;
+ assertEquals( expected, retrieved);
+ }
+
+ @Test
+ void lastInputNotArray(){
+ Object[] adaptedInput = new Object[] { "String" };
+ ScoreHelper.Compares compares = new ScoreHelper.Compares(null,
adaptedInput, null);
+ int retrieved = ScoreHelper.lastInputNotArray.applyAsInt(compares);
+ assertEquals(lastInputNotArrayNotArrayScore, retrieved);
+
+ adaptedInput = new Object[] { "String", 34, new Object() };
+ compares = new ScoreHelper.Compares(null, adaptedInput, null);
+ retrieved = ScoreHelper.lastInputNotArray.applyAsInt(compares);
+ assertEquals(lastInputNotArrayNotArrayScore, retrieved);
+
+ adaptedInput = new Object[] { null };
+ compares = new ScoreHelper.Compares(null, adaptedInput, null);
+ retrieved = ScoreHelper.lastInputNotArray.applyAsInt(compares);
+ assertEquals(lastInputNotArrayNotArrayScore, retrieved);
+
+ adaptedInput = new Object[] { new Object[]{} };
+ compares = new ScoreHelper.Compares(null, adaptedInput, null);
+ retrieved = ScoreHelper.lastInputNotArray.applyAsInt(compares);
+ assertEquals(0, retrieved);
+
+ adaptedInput = new Object[] { "String", 34, new Object[]{} };
+ compares = new ScoreHelper.Compares(null, adaptedInput, null);
+ retrieved = ScoreHelper.lastInputNotArray.applyAsInt(compares);
+ assertEquals(0, retrieved);
+ }
+
+ @Test
+ void lastParameterNotArray(){
+ Class<?>[] parameterTypes = new Class<?>[] { String.class };
+ ScoreHelper.Compares compares = new ScoreHelper.Compares(null, null,
parameterTypes);
+ int retrieved = ScoreHelper.lastParameterNotArray.applyAsInt(compares);
+ assertEquals(lastParameterNotArrayScore, retrieved);
+
+ parameterTypes = new Class<?>[] { String.class, Object.class };
+ compares = new ScoreHelper.Compares(null, null, parameterTypes);
+ retrieved = ScoreHelper.lastParameterNotArray.applyAsInt(compares);
+ assertEquals(lastParameterNotArrayScore, retrieved);
+
+ parameterTypes = new Class<?>[] { null };
+ compares = new ScoreHelper.Compares(null, null, parameterTypes);
+ retrieved = ScoreHelper.lastParameterNotArray.applyAsInt(compares);
+ assertEquals(lastParameterNotArrayScore, retrieved);
+
+ parameterTypes = new Class<?>[] { String.class, null };
+ compares = new ScoreHelper.Compares(null, null, parameterTypes);
+ retrieved = ScoreHelper.lastParameterNotArray.applyAsInt(compares);
+ assertEquals(lastParameterNotArrayScore, retrieved);
+
+ parameterTypes = new Class<?>[] { Object.class.arrayType(),
String.class };
+ compares = new ScoreHelper.Compares(null, null, parameterTypes);
+ retrieved = ScoreHelper.lastParameterNotArray.applyAsInt(compares);
+ assertEquals(lastParameterNotArrayScore, retrieved);
+
+ parameterTypes = new Class<?>[] { Object.class.arrayType(), null };
+ compares = new ScoreHelper.Compares(null, null, parameterTypes);
+ retrieved = ScoreHelper.lastParameterNotArray.applyAsInt(compares);
+ assertEquals(lastParameterNotArrayScore, retrieved);
+
+ parameterTypes = new Class<?>[] { Object.class.arrayType() };
+ compares = new ScoreHelper.Compares(null, null, parameterTypes);
+ retrieved = ScoreHelper.lastParameterNotArray.applyAsInt(compares);
+ assertEquals(0, retrieved);
+
+ parameterTypes = new Class<?>[] { String.class,
Object.class.arrayType() };
+ compares = new ScoreHelper.Compares(null, null, parameterTypes);
+ retrieved = ScoreHelper.lastParameterNotArray.applyAsInt(compares);
+ assertEquals(0, retrieved);
+
+ parameterTypes = new Class<?>[] { null, Object.class.arrayType() };
+ compares = new ScoreHelper.Compares(null, null, parameterTypes);
+ retrieved = ScoreHelper.lastParameterNotArray.applyAsInt(compares);
+ assertEquals(0, retrieved);
+ }
+
+ @Test
+ void numberOfParameters() {
+ Object[] originalInput = new Object[] { "String" };
+ Class<?>[] parameterTypes = new Class<?>[] { String.class };
+ ScoreHelper.Compares compares = new
ScoreHelper.Compares(originalInput, null, parameterTypes);
+ int retrieved = ScoreHelper.numberOfParameters.applyAsInt(compares);
+ assertEquals(numberOfParametersScore, retrieved);
+
+ originalInput = new Object[] { "String" };
+ parameterTypes = new Class<?>[] { Object.class };
+ compares = new ScoreHelper.Compares(originalInput, null,
parameterTypes);
+ retrieved = ScoreHelper.numberOfParameters.applyAsInt(compares);
+ assertEquals(numberOfParametersScore, retrieved);
+
+ originalInput = new Object[] { new Object[]{ "String", 34 } };
+ parameterTypes = new Class<?>[] { Object.class.arrayType() };
+ compares = new ScoreHelper.Compares(originalInput, null,
parameterTypes);
+ retrieved = ScoreHelper.numberOfParameters.applyAsInt(compares);
+ assertEquals(numberOfParametersScore, retrieved);
+
+ originalInput = new Object[] { "String", 34 };
+ parameterTypes = new Class<?>[] { Object.class };
+ compares = new ScoreHelper.Compares(originalInput, null,
parameterTypes);
+ retrieved = ScoreHelper.numberOfParameters.applyAsInt(compares);
+ assertEquals(0, retrieved);
+
+ originalInput = new Object[] { "String", 34 };
+ parameterTypes = new Class<?>[] { Object.class.arrayType() };
+ compares = new ScoreHelper.Compares(originalInput, null,
parameterTypes);
+ retrieved = ScoreHelper.numberOfParameters.applyAsInt(compares);
+ assertEquals(0, retrieved);
+ }
+
+ @Test
+ void typeIdentityOfParameters() {
+ Object[] originalInput = new Object[] { "String" };
+ Object[] adaptedInput = originalInput;
+ Class<?>[] parameterTypes = new Class<?>[] { String.class };
+ ScoreHelper.Compares compares = new
ScoreHelper.Compares(originalInput, adaptedInput, parameterTypes);
+ int retrieved =
ScoreHelper.typeIdentityOfParameters.applyAsInt(compares);
+ assertEquals(1000, retrieved);
+
+ parameterTypes = new Class<?>[] { Object.class };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrieved = ScoreHelper.typeIdentityOfParameters.applyAsInt(compares);
+ assertEquals(500, retrieved);
+
+ originalInput = new Object[] { "String", 34 };
+ adaptedInput = originalInput;
+ parameterTypes = new Class<?>[] { String.class, Integer.class };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrieved = ScoreHelper.typeIdentityOfParameters.applyAsInt(compares);
+ assertEquals(1000, retrieved);
+
+ parameterTypes = new Class<?>[] { String.class, Object.class };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrieved = ScoreHelper.typeIdentityOfParameters.applyAsInt(compares);
+ assertEquals(750, retrieved);
+
+ originalInput = new Object[] { "String", "34" };
+ adaptedInput = null;
+ parameterTypes = new Class<?>[] { String.class, Integer.class };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrieved = ScoreHelper.typeIdentityOfParameters.applyAsInt(compares);
+ assertEquals(750, retrieved);
+
+ originalInput = new Object[] { "StringA", "StringB", 40 };
+ parameterTypes = new Class<?>[] { String.class, Integer.class,
Integer.class };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrieved = ScoreHelper.typeIdentityOfParameters.applyAsInt(compares);
+ assertEquals(1000, retrieved);
+
+ originalInput = new Object[] { "StringA", "StringB", 40 };
+ parameterTypes = new Class<?>[] { String.class, Integer.class,
String.class };
+ compares = new ScoreHelper.Compares(originalInput, null,
parameterTypes);
+ retrieved = ScoreHelper.typeIdentityOfParameters.applyAsInt(compares);
+ assertEquals(500, retrieved);
+
+ originalInput = new Object[] {
CodegenTestUtil.newEmptyEvaluationContext(), "String" };
+ adaptedInput = originalInput;
+ parameterTypes = new Class<?>[] { EvaluationContext.class,
String.class };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrieved = ScoreHelper.typeIdentityOfParameters.applyAsInt(compares);
+ assertEquals(1000, retrieved);
+
+ originalInput = new Object[] { "String" };
+ adaptedInput = new Object[] {
CodegenTestUtil.newEmptyEvaluationContext(), "String" };
+ parameterTypes = new Class<?>[] { EvaluationContext.class,
String.class };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrieved = ScoreHelper.typeIdentityOfParameters.applyAsInt(compares);
+ assertEquals(1500, retrieved);
+
+ originalInput = new Object[] { "String" };
+ adaptedInput = null;
+ parameterTypes = new Class<?>[] { Integer.class };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrieved = ScoreHelper.typeIdentityOfParameters.applyAsInt(compares);
+ assertEquals(0, retrieved);
+
+ originalInput = new Object[] { "String", 34 };
+ adaptedInput = new Object[] {
CodegenTestUtil.newEmptyEvaluationContext(), "String", 34 };
+ parameterTypes = new Class<?>[] { EvaluationContext.class,
String.class, Integer.class };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrieved = ScoreHelper.typeIdentityOfParameters.applyAsInt(compares);
+ assertEquals(1500, retrieved);
+
+ originalInput = new Object[] { "String", "34" };
+ adaptedInput = originalInput;
+ parameterTypes = new Class<?>[] { EvaluationContext.class,
String.class, Object.class };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrieved = ScoreHelper.typeIdentityOfParameters.applyAsInt(compares);
+ assertEquals(750, retrieved);
+
+ originalInput = new Object[] { "String", "34" };
+ adaptedInput = null;
+ parameterTypes = new Class<?>[] { EvaluationContext.class,
String.class, Integer.class };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrieved = ScoreHelper.typeIdentityOfParameters.applyAsInt(compares);
+ assertEquals(750, retrieved);
+
+ originalInput = new Object[] { "String" };
+ parameterTypes = new Class<?>[] { EvaluationContext.class,
Integer.class };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrieved = ScoreHelper.typeIdentityOfParameters.applyAsInt(compares);
+ assertEquals(0, retrieved);
+
+ originalInput = new Object[] {
CodegenTestUtil.newEmptyEvaluationContext(), "String" };
+ parameterTypes = new Class<?>[] { EvaluationContext.class,
Integer.class };
+ compares = new ScoreHelper.Compares(originalInput, null,
parameterTypes);
+ retrieved = ScoreHelper.typeIdentityOfParameters.applyAsInt(compares);
+ assertEquals(0, retrieved);
+
+ originalInput = new Object[] { null };
+ parameterTypes = new Class<?>[] { Object.class.arrayType() };
+ compares = new ScoreHelper.Compares(originalInput, null,
parameterTypes);
+ retrieved = ScoreHelper.typeIdentityOfParameters.applyAsInt(compares);
+ assertEquals(500, retrieved);
+
+ originalInput = new Object[] { "StringA", "StringB" };
+ parameterTypes = new Class<?>[] { Object.class.arrayType() };
+ adaptedInput = new Object[] { new Object[] {"StringA", "StringB"} };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput,
parameterTypes);
+ retrieved = ScoreHelper.typeIdentityOfParameters.applyAsInt(compares);
+ assertEquals(1000, retrieved);
+ }
+
+ @Test
+ void coercedToVarargs() {
+ Object[] originalInput = new Object[] { "String" };
+ Object[] adaptedInput = new Object[] { "String" };
+ ScoreHelper.Compares compares = new
ScoreHelper.Compares(originalInput, adaptedInput, null);
+ int retrieved = ScoreHelper.coercedToVarargs.applyAsInt(compares);
+ assertEquals(0, retrieved);
+
+ originalInput = new Object[] { "String" };
+ adaptedInput = new Object[] { new Object[] {"String"} };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput, null);
+ retrieved = ScoreHelper.coercedToVarargs.applyAsInt(compares);
+ assertEquals(coercedToVarargsScore, retrieved);
+
+ originalInput = new Object[] { "String", 34 };
+ adaptedInput = new Object[] { new Object[] {"String", 34} };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput, null);
+ retrieved = ScoreHelper.coercedToVarargs.applyAsInt(compares);
+ assertEquals(coercedToVarargsScore, retrieved);
+
+ originalInput = new Object[] {
CodegenTestUtil.newEmptyEvaluationContext(), "String", 34 };
+ adaptedInput = new Object[] { new Object[] {"String", 34} };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput, null);
+ retrieved = ScoreHelper.coercedToVarargs.applyAsInt(compares);
+ assertEquals(coercedToVarargsScore, retrieved);
+
+ originalInput = new Object[] { "String", 34 };
+ adaptedInput = new Object[] {
CodegenTestUtil.newEmptyEvaluationContext(), new Object[] {"String", 34} };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput, null);
+ retrieved = ScoreHelper.coercedToVarargs.applyAsInt(compares);
+ assertEquals(coercedToVarargsScore, retrieved);
+
+ originalInput = new Object[] {
CodegenTestUtil.newEmptyEvaluationContext(), "String", 34 };
+ adaptedInput = new Object[] {
CodegenTestUtil.newEmptyEvaluationContext(), new Object[] {"String", 34} };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput, null);
+ retrieved = ScoreHelper.coercedToVarargs.applyAsInt(compares);
+ assertEquals(coercedToVarargsScore, retrieved);
+
+ originalInput = new Object[] { new Object[] {"String", 34} };
+ adaptedInput = new Object[] { new Object[] {"String", 34} };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput, null);
+ retrieved = ScoreHelper.coercedToVarargs.applyAsInt(compares);
+ assertEquals(0, retrieved);
+
+ originalInput = new Object[] {BigDecimal.valueOf(10), null,
BigDecimal.valueOf(20), BigDecimal.valueOf(40),null };
+ adaptedInput = new Object[] { new Object[] { BigDecimal.valueOf(10),
null, BigDecimal.valueOf(20), BigDecimal.valueOf(40),null } };
+ compares = new ScoreHelper.Compares(originalInput, adaptedInput, null);
+ retrieved = ScoreHelper.coercedToVarargs.applyAsInt(compares);
+ assertEquals(coercedToVarargsScore, retrieved);
+ }
+
+ @Test
+ void nullCounts() {
+ Object[] adaptedInput = new Object[] { "String" };
+ ScoreHelper.Compares compares = new ScoreHelper.Compares(null,
adaptedInput, null);
+ int retrieved = ScoreHelper.nullCounts.applyAsInt(compares);
+ assertEquals(0, retrieved);
+
+ adaptedInput = new Object[] { };
+ compares = new ScoreHelper.Compares(null, adaptedInput, null);
+ retrieved = ScoreHelper.nullCounts.applyAsInt(compares);
+ assertEquals(0, retrieved);
+
+ adaptedInput = new Object[] { "String", null };
+ compares = new ScoreHelper.Compares(null, adaptedInput, null);
+ retrieved = ScoreHelper.nullCounts.applyAsInt(compares);
+ assertEquals(-1, retrieved);
+
+ adaptedInput = new Object[] { null };
+ compares = new ScoreHelper.Compares(null, adaptedInput, null);
+ retrieved = ScoreHelper.nullCounts.applyAsInt(compares);
+ assertEquals(-1, retrieved);
+
+ adaptedInput = new Object[] { null, new Object(), null };
+ compares = new ScoreHelper.Compares(null, adaptedInput, null);
+ retrieved = ScoreHelper.nullCounts.applyAsInt(compares);
+ assertEquals(-2, retrieved);
+ }
+
+ @Test
+ void nullCount() {
+ Random random = new Random();
+ int elements = random.nextInt(10);
+ Object[] params = new Object[elements];
+ int expectedCount = 0;
+ for (int i = 0; i < elements; i++) {
+ if (random.nextBoolean()) {
+ params[i] = null;
+ expectedCount++;
+ } else {
+ params[i] = new Object();
+ }
+ }
+ assertEquals(expectedCount, ScoreHelper.nullCount(params));
+ }
+}
\ No newline at end of file
diff --git
a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/util/CoerceUtilTest.java
b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/util/CoerceUtilTest.java
index 81127b4230..31cef2149d 100644
---
a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/util/CoerceUtilTest.java
+++
b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/util/CoerceUtilTest.java
@@ -24,6 +24,7 @@ import java.time.LocalDate;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Collections;
+import java.util.List;
import java.util.Optional;
import java.util.Set;
@@ -36,6 +37,46 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
class CoerceUtilTest {
+ @Test
+ void coerceParam() {
+ // Coerce List to singleton
+ Class<?> currentIdxActualParameterType = List.class;
+ Class<?> expectedParameterType = Number.class;
+ Object valueObject = 34;
+ Object actualObject = List.of(valueObject);
+ Optional<Object> retrieved =
CoerceUtil.coerceParam(currentIdxActualParameterType, expectedParameterType,
actualObject);
+ assertNotNull(retrieved);
+ assertTrue(retrieved.isPresent());
+ assertEquals(valueObject, retrieved.get());
+
+ // Coerce single element to singleton list
+ currentIdxActualParameterType = Number.class;
+ expectedParameterType = List.class;
+ actualObject = 34;
+ retrieved = CoerceUtil.coerceParam(currentIdxActualParameterType,
expectedParameterType, actualObject);
+ assertNotNull(retrieved);
+ assertTrue(retrieved.isPresent());
+ assertTrue(retrieved.get() instanceof List);
+ List lstRetrieved = (List) retrieved.get();
+ assertEquals(1, lstRetrieved.size());
+ assertEquals(actualObject, lstRetrieved.get(0));
+
+ // Coerce date to date and time
+ actualObject = LocalDate.now();
+ currentIdxActualParameterType = LocalDate.class;
+ expectedParameterType = ZonedDateTime.class;
+ retrieved = CoerceUtil.coerceParam(currentIdxActualParameterType,
expectedParameterType, actualObject);
+ assertNotNull(retrieved);
+ assertTrue(retrieved.isPresent());
+ assertTrue(retrieved.get() instanceof ZonedDateTime);
+ ZonedDateTime zdtRetrieved = (ZonedDateTime) retrieved.get();
+ assertEquals(actualObject, zdtRetrieved.toLocalDate());
+ assertEquals(ZoneOffset.UTC, zdtRetrieved.getOffset());
+ assertEquals(0, zdtRetrieved.getHour());
+ assertEquals(0, zdtRetrieved.getMinute());
+ assertEquals(0, zdtRetrieved.getSecond());
+ }
+
@Test
void coerceParameterDateToDateTimeConverted() {
Object value = LocalDate.now();
diff --git a/kie-dmn/kie-dmn-feel/src/test/resources/logback.xml
b/kie-dmn/kie-dmn-feel/src/test/resources/logback.xml
index 3d747c98aa..752bdbd6df 100644
--- a/kie-dmn/kie-dmn-feel/src/test/resources/logback.xml
+++ b/kie-dmn/kie-dmn-feel/src/test/resources/logback.xml
@@ -29,6 +29,7 @@
</appender>
<logger name="org.kie" level="warn"/>
+ <logger name="org.kie.dmn.feel.runtime.functions" level="info"/> <!-- set to
trace for very detailed logging <info -->
<logger name="org.kie.dmn.feel.codegen" level="info"/> <!-- useful default
when moving generic org.kie to <info -->
<root level="warn">
diff --git
a/kie-dmn/kie-dmn-signavio/src/main/java/org/kie/dmn/signavio/feel/runtime/functions/AvgFunction.java
b/kie-dmn/kie-dmn-signavio/src/main/java/org/kie/dmn/signavio/feel/runtime/functions/AvgFunction.java
index 3504e55339..e32471e43f 100644
---
a/kie-dmn/kie-dmn-signavio/src/main/java/org/kie/dmn/signavio/feel/runtime/functions/AvgFunction.java
+++
b/kie-dmn/kie-dmn-signavio/src/main/java/org/kie/dmn/signavio/feel/runtime/functions/AvgFunction.java
@@ -39,7 +39,7 @@ public class AvgFunction
}
public FEELFnResult<BigDecimal> invoke(@ParameterName("list") Number
single) {
- return BuiltInFunctions.getFunction(MeanFunction.class).invoke(single);
+ return
BuiltInFunctions.getFunction(MeanFunction.class).invoke(List.of(single));
}
public FEELFnResult<BigDecimal> invoke(@ParameterName("n") Object[] list) {
diff --git
a/kie-dmn/kie-dmn-signavio/src/main/java/org/kie/dmn/signavio/feel/runtime/functions/ConcatFunction.java
b/kie-dmn/kie-dmn-signavio/src/main/java/org/kie/dmn/signavio/feel/runtime/functions/ConcatFunction.java
index 602c2b12cc..e0bc747cbf 100644
---
a/kie-dmn/kie-dmn-signavio/src/main/java/org/kie/dmn/signavio/feel/runtime/functions/ConcatFunction.java
+++
b/kie-dmn/kie-dmn-signavio/src/main/java/org/kie/dmn/signavio/feel/runtime/functions/ConcatFunction.java
@@ -20,6 +20,7 @@ package org.kie.dmn.signavio.feel.runtime.functions;
import java.util.Arrays;
import java.util.List;
+import java.util.Objects;
import org.kie.dmn.api.feel.runtime.events.FEELEvent;
import org.kie.dmn.feel.runtime.events.InvalidParametersEvent;
@@ -39,9 +40,9 @@ public class ConcatFunction
if (list == null) {
return FEELFnResult.ofError(new
InvalidParametersEvent(FEELEvent.Severity.ERROR, "list", "cannot be null"));
}
- if (list.contains(null)) {
- return FEELFnResult.ofError(new
InvalidParametersEvent(FEELEvent.Severity.ERROR, "list", "cannot contain null
values"));
- }
+ if (list.stream().anyMatch(Objects::isNull)) {
+ return FEELFnResult.ofError(new
InvalidParametersEvent(FEELEvent.Severity.ERROR, "list", "cannot contain null
values"));
+ }
StringBuilder sb = new StringBuilder();
for (Object element : list) {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]