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 356d4702586 [Incubator kie issues#2188] Fix the compilation failure on
DecisionService–Decision output type mismatch (#6776)
356d4702586 is described below
commit 356d4702586b750089ffdf000fe804fd6816a7e7
Author: AthiraHari77 <[email protected]>
AuthorDate: Fri Jul 10 16:08:16 2026 +0530
[Incubator kie issues#2188] Fix the compilation failure on
DecisionService–Decision output type mismatch (#6776)
* [incubator-kie-issues#2188] add coercion logic for decisionservice
* [incubator-kie-issues#2188] WIP
* [incubator-kie-issues#2188] update changes for date time conversion along
with decision service handling
* [incubator-kie-issues#2188] Fix test failures
* [incubator-kie-issues#2188] Refactor code
* Update tests and code refactoring
* code refactoring
* [incubator-kie-issues#2260] handle null result
* [incubator-kie-issues#2260] wip
* [incubator-kie-issues#2260] fix testcases
* [incubator-kie-issues#2188] refactoring code
* [incubator-kie-issues#2188] refactoring code
---------
Co-authored-by: athira <[email protected]>
---
.../dmn/core/ast/DMNDecisionServiceEvaluator.java | 15 ++-
...DecisionServiceFunctionDefinitionEvaluator.java | 2 +-
.../dmn/core/ast/DMNFunctionWithReturnType.java | 2 +-
.../dmn/core/compiler/DMNTypeRegistryAbstract.java | 5 +-
.../dmn/core/compiler/DecisionServiceCompiler.java | 122 ++++++++++-----------
.../java/org/kie/dmn/core/impl/DMNRuntimeImpl.java | 4 +-
.../org/kie/dmn/core/impl/DMNRuntimeUtils.java | 70 +++++++++++-
.../core/compiler/DecisionServiceCompilerTest.java | 1 -
.../decisionservices/DMNDecisionServicesTest.java | 82 ++++++++++++++
.../org/kie/dmn/core/impl/DMNRuntimeUtilsTest.java | 90 +++++++++++++++
.../DecisionServiceImplicitConversions.dmn | 85 ++++++++++++++
11 files changed, 404 insertions(+), 74 deletions(-)
diff --git
a/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/ast/DMNDecisionServiceEvaluator.java
b/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/ast/DMNDecisionServiceEvaluator.java
index fb5da420e79..b991e3941f0 100644
---
a/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/ast/DMNDecisionServiceEvaluator.java
+++
b/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/ast/DMNDecisionServiceEvaluator.java
@@ -22,7 +22,6 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.stream.Collectors;
import org.kie.dmn.api.core.DMNDecisionResult;
import org.kie.dmn.api.core.DMNMessage;
@@ -30,6 +29,7 @@ import org.kie.dmn.api.core.DMNMessage.Severity;
import org.kie.dmn.api.core.DMNModel;
import org.kie.dmn.api.core.DMNResult;
import org.kie.dmn.api.core.DMNRuntime;
+import org.kie.dmn.api.core.DMNType;
import org.kie.dmn.api.core.ast.DecisionServiceNode;
import org.kie.dmn.api.core.event.DMNRuntimeEventManager;
import org.kie.dmn.core.api.DMNExpressionEvaluator;
@@ -41,6 +41,7 @@ import org.kie.dmn.core.impl.DMNResultImpl;
import org.kie.dmn.core.impl.DMNRuntimeEventManagerUtils;
import org.kie.dmn.core.impl.DMNRuntimeImpl;
import org.kie.dmn.core.impl.DMNRuntimeUtils;
+import org.kie.dmn.core.util.CoerceUtil;
import org.kie.dmn.core.util.Msg;
import org.kie.dmn.core.util.MsgUtil;
import org.slf4j.Logger;
@@ -73,7 +74,14 @@ public class DMNDecisionServiceEvaluator implements
DMNExpressionEvaluator {
for (String id : decisionIDs) {
DMNDecisionResult decisionResultById =
evaluateById.getDecisionResultById(id);
String decisionName = dmnModel.getDecisionById(id).getName();
- ctx.put(decisionName, decisionResultById.getResult());
+
+ DMNType expectedType = dsNode.getResultType();
+ Object originalResult = decisionResultById.getResult();
+ Object coercedResult = CoerceUtil.coerceValue(expectedType,
originalResult);
+ if (coercedResult != originalResult) {
+ ((DMNDecisionResultImpl)
decisionResultById).setResult(coercedResult);
+ }
+ ctx.put(decisionName, coercedResult);
decisionResults.add(decisionResultById);
}
boolean errors = false;
@@ -85,7 +93,8 @@ public class DMNDecisionServiceEvaluator implements
DMNExpressionEvaluator {
}
boolean typeCheck = ((DMNRuntimeImpl)
eventManager.getRuntime()).performRuntimeTypeCheck(result.getModel());
if (typeCheck) {
- Object c = DMNRuntimeUtils.coerceUsingType(decisionIDs.size() == 1
? ctx.values().iterator().next() : ctx,
+ Object finalResult = decisionIDs.size() == 1 ?
ctx.values().iterator().next() : ctx;
+ Object c =
DMNRuntimeUtils.coerceSingletonCollectionItemToValue(finalResult,
dsNode.getResultType(),
typeCheck,
(rx, tx) ->
MsgUtil.reportMessage(LOG,
diff --git
a/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/ast/DMNDecisionServiceFunctionDefinitionEvaluator.java
b/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/ast/DMNDecisionServiceFunctionDefinitionEvaluator.java
index d9053581c92..d0388e7d86c 100644
---
a/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/ast/DMNDecisionServiceFunctionDefinitionEvaluator.java
+++
b/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/ast/DMNDecisionServiceFunctionDefinitionEvaluator.java
@@ -157,7 +157,7 @@ public class DMNDecisionServiceFunctionDefinitionEvaluator
implements DMNExpress
private Object performTypeCheckIfNeeded(Object param, int paramIndex) {
DSFormalParameter dsFormalParameter = parameters.get(paramIndex);
- Object result = DMNRuntimeUtils.coerceUsingType(param,
+ Object result =
DMNRuntimeUtils.coerceSingletonCollectionItemToValue(param,
dsFormalParameter.type,
typeCheck,
(rx, tx) ->
MsgUtil.reportMessage(LOG,
diff --git
a/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/ast/DMNFunctionWithReturnType.java
b/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/ast/DMNFunctionWithReturnType.java
index ebad93794e1..01236dddb53 100644
---
a/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/ast/DMNFunctionWithReturnType.java
+++
b/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/ast/DMNFunctionWithReturnType.java
@@ -59,7 +59,7 @@ public class DMNFunctionWithReturnType extends
BaseFEELFunction {
@Override
public Object invokeReflectively(EvaluationContext ctx, Object[] params) {
Object result = wrapped.invokeReflectively(ctx, params);
- result = DMNRuntimeUtils.coerceUsingType(result,
+ result = DMNRuntimeUtils.coerceSingletonCollectionItemToValue(result,
returnType,
true, // this FN is created
when typeCheck==true, hence here always true.
(r, t) ->
MsgUtil.reportMessage(LOG,
diff --git
a/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/compiler/DMNTypeRegistryAbstract.java
b/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/compiler/DMNTypeRegistryAbstract.java
index 9a4c233e7fd..d0697da3d11 100644
---
a/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/compiler/DMNTypeRegistryAbstract.java
+++
b/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/compiler/DMNTypeRegistryAbstract.java
@@ -72,7 +72,7 @@ public abstract class DMNTypeRegistryAbstract implements
DMNTypeRegistry, FEELTy
feelTypesScope.define(new TypeSymbol(name, type));
}
- static DMNType getFeelPrimitiveType(String name, BuiltInType type, String
feelNamespace, DMNType unknownType) {
+ public static DMNType getFeelPrimitiveType(String name, BuiltInType type,
String feelNamespace, DMNType unknownType) {
DMNType feelPrimitiveType;
if( type == BuiltInType.LIST ) {
feelPrimitiveType = new SimpleTypeImpl(feelNamespace, name, null,
true, null, null, unknownType, type);
@@ -152,5 +152,4 @@ public abstract class DMNTypeRegistryAbstract implements
DMNTypeRegistry, FEELTy
return null;
}
-
-}
+}
\ No newline at end of file
diff --git
a/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/compiler/DecisionServiceCompiler.java
b/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/compiler/DecisionServiceCompiler.java
index 42988f8facb..cc151160f6e 100644
---
a/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/compiler/DecisionServiceCompiler.java
+++
b/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/compiler/DecisionServiceCompiler.java
@@ -40,6 +40,7 @@ import org.kie.dmn.core.ast.DecisionServiceNodeImpl;
import org.kie.dmn.core.ast.InputDataNodeImpl;
import org.kie.dmn.core.ast.ItemDefNodeImpl;
import org.kie.dmn.core.impl.DMNModelImpl;
+import org.kie.dmn.core.impl.DMNRuntimeUtils;
import org.kie.dmn.core.impl.SimpleFnTypeImpl;
import org.kie.dmn.core.util.Msg;
import org.kie.dmn.core.util.MsgUtil;
@@ -149,7 +150,7 @@ public class DecisionServiceCompiler implements
DRGElementCompiler {
ni.setEvaluator(exprEvaluator);
if (ni.getType() != null) {
- checkFnConsistency(model, ni, ni.getType(), outputDecisions);
+ validateDecisionServiceFunctionSignature(model, ni, ni.getType(),
outputDecisions);
}
}
@@ -216,115 +217,114 @@ public class DecisionServiceCompiler implements
DRGElementCompiler {
ni.getName());
}
- private void checkFnConsistency(DMNModelImpl model,
DecisionServiceNodeImpl ni, DMNType type, List<DecisionNode> outputDecisions) {
- SimpleFnTypeImpl fnType = ((SimpleFnTypeImpl) type);
- FunctionItem fi = fnType.getFunctionItem();
- if (fi.getParameters().size() != ni.getInputParameters().size()) {
+ private void validateDecisionServiceFunctionSignature(DMNModelImpl model,
DecisionServiceNodeImpl decisionServiceNode, DMNType
decisionServiceFunctionType, List<DecisionNode> outputDecisions) {
+ SimpleFnTypeImpl functionType = ((SimpleFnTypeImpl)
decisionServiceFunctionType);
+ FunctionItem functionItem = functionType.getFunctionItem();
+ if (functionItem.getParameters().size() !=
decisionServiceNode.getInputParameters().size()) {
MsgUtil.reportMessage(LOG,
DMNMessage.Severity.ERROR,
- ni.getDecisionService(),
+ decisionServiceNode.getDecisionService(),
model,
null,
null,
Msg.PARAMETER_COUNT_MISMATCH_COMPILING,
- ni.getName(),
- fi.getParameters().size(),
- ni.getInputParameters().size());
+ decisionServiceNode.getName(),
+ functionItem.getParameters().size(),
+
decisionServiceNode.getInputParameters().size());
return;
}
- for (int i = 0; i < fi.getParameters().size(); i++) {
- InformationItem fiII = fi.getParameters().get(i);
- String fpName =
ni.getInputParameters().keySet().stream().skip(i).findFirst().orElse(null);
- if (!fiII.getName().equals(fpName)) {
- List<String> fiParamNames =
fi.getParameters().stream().map(InformationItem::getName).collect(Collectors.toList());
- List<String> funcDefParamNames =
ni.getInputParameters().keySet().stream().collect(Collectors.toList());
+ for (int i = 0; i < functionItem.getParameters().size(); i++) {
+ InformationItem functionParameter =
functionItem.getParameters().get(i);
+ String functionParameterName =
decisionServiceNode.getInputParameters().keySet().stream().skip(i).findFirst().orElse(null);
+ if (!functionParameter.getName().equals(functionParameterName)) {
+ List<String> functionItemParamNames =
functionItem.getParameters().stream().map(InformationItem::getName).collect(Collectors.toList());
+ List<String> decisionServiceParamNames =
decisionServiceNode.getInputParameters().keySet().stream().collect(Collectors.toList());
MsgUtil.reportMessage(LOG,
DMNMessage.Severity.ERROR,
- ni.getDecisionService(),
+ decisionServiceNode.getDecisionService(),
model,
null,
null,
Msg.PARAMETER_NAMES_MISMATCH_COMPILING,
- ni.getName(),
- fiParamNames,
- funcDefParamNames);
+ decisionServiceNode.getName(),
+ functionItemParamNames,
+ decisionServiceParamNames);
return;
}
- QName fiQname = fiII.getTypeRef();
- QName fdQname = null;
- DMNNode fpDMNNode = ni.getInputParameters().get(fpName);
- if (fpDMNNode instanceof InputDataNodeImpl) {
- fdQname = ((InputDataNodeImpl)
fpDMNNode).getInputData().getVariable().getTypeRef();
- } else if (fpDMNNode instanceof DecisionNodeImpl) {
- fdQname = ((DecisionNodeImpl)
fpDMNNode).getDecision().getVariable().getTypeRef();
+ QName functionParameterTypeRef = functionParameter.getTypeRef();
+ QName decisionServiceParameterTypeRef = null;
+ DMNNode inputParameterNode =
decisionServiceNode.getInputParameters().get(functionParameterName);
+ if (inputParameterNode instanceof InputDataNodeImpl) {
+ decisionServiceParameterTypeRef = ((InputDataNodeImpl)
inputParameterNode).getInputData().getVariable().getTypeRef();
+ } else if (inputParameterNode instanceof DecisionNodeImpl) {
+ decisionServiceParameterTypeRef = ((DecisionNodeImpl)
inputParameterNode).getDecision().getVariable().getTypeRef();
}
- if (fiQname != null && fdQname != null &&
!fiQname.equals(fdQname)) {
+ if (functionParameterTypeRef != null &&
decisionServiceParameterTypeRef != null &&
!functionParameterTypeRef.equals(decisionServiceParameterTypeRef)) {
MsgUtil.reportMessage(LOG,
DMNMessage.Severity.ERROR,
- ni.getDecisionService(),
+ decisionServiceNode.getDecisionService(),
model,
null,
null,
Msg.PARAMETER_TYPEREF_MISMATCH_COMPILING,
- ni.getName(),
- fiII.getName(),
- fiQname,
- fdQname);
+ decisionServiceNode.getName(),
+ functionParameter.getName(),
+ functionParameterTypeRef,
+ decisionServiceParameterTypeRef);
}
}
- QName fiReturnType = fi.getOutputTypeRef();
- if (ni.getDecisionService().getOutputDecision().size() == 1) {
- QName fdReturnType =
outputDecisions.get(0).getDecision().getVariable().getTypeRef();
- if (fiReturnType != null && fdReturnType != null &&
!fiReturnType.equals(fdReturnType)) {
+ QName functionReturnTypeRef = functionItem.getOutputTypeRef();
+ if
(decisionServiceNode.getDecisionService().getOutputDecision().size() == 1) {
+ QName outputDecisionReturnTypeRef =
outputDecisions.get(0).getDecision().getVariable().getTypeRef();
+ if (functionReturnTypeRef != null && outputDecisionReturnTypeRef
!= null && !functionReturnTypeRef.equals(outputDecisionReturnTypeRef) &&
!DMNRuntimeUtils.isReturnTypeCollectionCompatible(functionReturnTypeRef,
outputDecisionReturnTypeRef, model)) {
MsgUtil.reportMessage(LOG,
DMNMessage.Severity.ERROR,
- ni.getDecisionService(),
+ decisionServiceNode.getDecisionService(),
model,
null,
null,
Msg.RETURNTYPE_TYPEREF_MISMATCH_COMPILING,
- ni.getName(),
- fiReturnType,
- fdReturnType);
+ decisionServiceNode.getName(),
+ functionReturnTypeRef,
+ outputDecisionReturnTypeRef);
}
- } else if (ni.getDecisionService().getOutputDecision().size() > 1) {
- final Function<QName, QName> lookupFn = (in) ->
NamespaceUtil.getNamespaceAndName(ni.getDecisionService(),
model.getImportAliasesForNS(), in, model.getNamespace());
- LinkedHashMap<String, QName> fdComposite = new LinkedHashMap<>();
- for (DecisionNode dn : outputDecisions) {
- fdComposite.put(dn.getName(),
lookupFn.apply(dn.getDecision().getVariable().getTypeRef()));
+ } else if
(decisionServiceNode.getDecisionService().getOutputDecision().size() > 1) {
+ final Function<QName, QName> namespaceQNameLookup = (qname) ->
NamespaceUtil.getNamespaceAndName(decisionServiceNode.getDecisionService(),
model.getImportAliasesForNS(), qname, model.getNamespace());
+ LinkedHashMap<String, QName> decisionServiceCompositeType = new
LinkedHashMap<>();
+ for (DecisionNode decisionNode : outputDecisions) {
+ decisionServiceCompositeType.put(decisionNode.getName(),
namespaceQNameLookup.apply(decisionNode.getDecision().getVariable().getTypeRef()));
}
- final QName lookup = lookupFn.apply(fiReturnType);
- Optional<ItemDefNodeImpl> composite =
model.getItemDefinitions().stream().filter(id ->
id.getModelNamespace().equals(lookup.getNamespaceURI()) &&
id.getName().equals(lookup.getLocalPart())).map(ItemDefNodeImpl.class::cast).findFirst();
- if (composite.isEmpty()) {
+ final QName resolvedFunctionReturnTypeRef =
namespaceQNameLookup.apply(functionReturnTypeRef);
+ Optional<ItemDefNodeImpl> matchedItemDefNode =
model.getItemDefinitions().stream().filter(id ->
id.getModelNamespace().equals(resolvedFunctionReturnTypeRef.getNamespaceURI())
&&
id.getName().equals(resolvedFunctionReturnTypeRef.getLocalPart())).map(ItemDefNodeImpl.class::cast).findFirst();
+ if (matchedItemDefNode.isEmpty()) {
MsgUtil.reportMessage(LOG,
DMNMessage.Severity.ERROR,
- ni.getDecisionService(),
+ decisionServiceNode.getDecisionService(),
model,
null,
null,
Msg.RETURNTYPE_TYPEREF_MISMATCH_COMPILING,
- ni.getName(),
- lookup,
- fdComposite);
+ decisionServiceNode.getName(),
+ resolvedFunctionReturnTypeRef,
+ decisionServiceCompositeType);
return;
}
- LinkedHashMap<String, QName> fiComposite = new LinkedHashMap<>();
- for (ItemDefinition ic :
composite.get().getItemDef().getItemComponent()) {
- fiComposite.put(ic.getName(), lookupFn.apply(ic.getTypeRef()));
+ LinkedHashMap<String, QName> functionCompositeType = new
LinkedHashMap<>();
+ for (ItemDefinition itemDefinition :
matchedItemDefNode.get().getItemDef().getItemComponent()) {
+ functionCompositeType.put(itemDefinition.getName(),
namespaceQNameLookup.apply(itemDefinition.getTypeRef()));
}
- if (!fiComposite.equals(fdComposite)) {
+ if (!functionCompositeType.equals(decisionServiceCompositeType)) {
MsgUtil.reportMessage(LOG,
DMNMessage.Severity.ERROR,
- ni.getDecisionService(),
+ decisionServiceNode.getDecisionService(),
model,
null,
null,
Msg.RETURNTYPE_TYPEREF_MISMATCH_COMPILING,
- ni.getName(),
- fiComposite,
- fdComposite);
+ decisionServiceNode.getName(),
+ functionCompositeType,
+ decisionServiceCompositeType);
}
}
}
-
}
\ No newline at end of file
diff --git
a/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/impl/DMNRuntimeImpl.java
b/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/impl/DMNRuntimeImpl.java
index 3789c799bee..41eb748ff14 100644
---
a/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/impl/DMNRuntimeImpl.java
+++
b/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/impl/DMNRuntimeImpl.java
@@ -66,7 +66,7 @@ import static
org.kie.dmn.api.core.DMNDecisionResult.DecisionEvaluationStatus.EV
import static
org.kie.dmn.api.core.DMNDecisionResult.DecisionEvaluationStatus.FAILED;
import static
org.kie.dmn.api.core.DMNDecisionResult.DecisionEvaluationStatus.SKIPPED;
import static org.kie.dmn.core.compiler.UnnamedImportUtils.isInUnnamedImport;
-import static org.kie.dmn.core.impl.DMNRuntimeUtils.coerceUsingType;
+import static
org.kie.dmn.core.impl.DMNRuntimeUtils.coerceSingletonCollectionItemToValue;
import static org.kie.dmn.core.impl.DMNRuntimeUtils.getDependencyIdentifier;
import static org.kie.dmn.core.impl.DMNRuntimeUtils.getIdentifier;
import static org.kie.dmn.core.impl.DMNRuntimeUtils.getObjectString;
@@ -316,7 +316,7 @@ public class DMNRuntimeImpl
} else if (dep instanceof DecisionNode) {
depType = ((DecisionNode) dep).getResultType();
}
- Object c = coerceUsingType(originalValue,
+ Object c =
coerceSingletonCollectionItemToValue(originalValue,
depType,
typeCheck,
(r, t) ->
MsgUtil.reportMessage(logger,
diff --git
a/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/impl/DMNRuntimeUtils.java
b/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/impl/DMNRuntimeUtils.java
index 1158e1539ad..692abe0d122 100644
---
a/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/impl/DMNRuntimeUtils.java
+++
b/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/impl/DMNRuntimeUtils.java
@@ -18,6 +18,7 @@
*/
package org.kie.dmn.core.impl;
+import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
@@ -26,11 +27,15 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
+import javax.xml.namespace.QName;
import org.kie.dmn.api.core.DMNContext;
import org.kie.dmn.api.core.DMNType;
import org.kie.dmn.api.core.ast.DMNNode;
import org.kie.dmn.api.core.ast.InputDataNode;
+import org.kie.dmn.core.compiler.DMNTypeRegistryAbstract;
import org.kie.dmn.core.util.MsgUtil;
+import org.kie.dmn.feel.lang.types.BuiltInType;
+import org.kie.dmn.typesafe.DMNTypeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -46,7 +51,13 @@ public class DMNRuntimeUtils {
}
- public static Object coerceUsingType(Object value, DMNType type, boolean
typeCheck,
+ /**
+ * When type-checking is enabled, verifies that {@code value} is
assignable to {@code type} and, if the type is
+ * a non-collection type and {@code value} is a single-item Collection,
unwraps the sole element.
+ * When type-checking is disabled, unconditionally attempts the
single-item unwrap.
+ *
+ */
+ public static Object coerceSingletonCollectionItemToValue(Object value,
DMNType type, boolean typeCheck,
BiConsumer<Object, DMNType>
nullCallback) {
if (typeCheck) {
if (type.isAssignableValue(value)) {
@@ -273,4 +284,59 @@ public class DMNRuntimeUtils {
static String getIdentifier(DMNNode node) {
return node.getName() != null ? node.getName() : node.getId();
}
-}
+
+ /**
+ * Determines whether a function return-type reference and an
output-decision return-type reference are
+ * compatible under DMN collection-coercion rules (e.g. a scalar type is
compatible with a single-element
+ * collection of the same base type, and {@code date and time} is
compatible with {@code date}).
+ *
+ * @param functionReturnTypeRef the {@link QName} declared as the
function's output type
+ * @param outputDecisionReturnTypeRef the {@link QName} declared as the
output decision's variable type
+ * @param model the DMN model used to resolve both
type references
+ * @return {@code true} when the two types are collection-coercion
compatible
+ */
+ public static boolean isReturnTypeCollectionCompatible(QName
functionReturnTypeRef, QName outputDecisionReturnTypeRef, DMNModelImpl model) {
+ DMNType decisionServiceOutputType =
resolveDMNType(functionReturnTypeRef, model);
+ DMNType decisionOutputType =
resolveDMNType(outputDecisionReturnTypeRef, model);
+
+ if (decisionServiceOutputType == null || decisionOutputType == null) {
+ return false;
+ }
+ if (!decisionServiceOutputType.isCollection() &&
decisionOutputType.isCollection()) {
+ DMNType outputDecisionElementType =
decisionOutputType.getBaseType();
+ return outputDecisionElementType == null ||
DMNTypeUtils.getFEELBuiltInType(outputDecisionElementType)
+ ==
DMNTypeUtils.getFEELBuiltInType(decisionServiceOutputType);
+ }
+ if (decisionServiceOutputType.isCollection() &&
!decisionOutputType.isCollection()) {
+ DMNType decisionServiceElementType =
decisionServiceOutputType.getBaseType();
+ return decisionServiceElementType != null &&
DMNTypeUtils.getFEELBuiltInType(decisionServiceElementType)
+ == DMNTypeUtils.getFEELBuiltInType(decisionOutputType);
+ }
+ return decisionServiceOutputType instanceof SimpleTypeImpl
decisionServiceSimpleType
+ && decisionServiceSimpleType.getFeelType() ==
BuiltInType.DATE_TIME
+ && decisionOutputType instanceof SimpleTypeImpl
decisionOutputSimpleType
+ && decisionOutputSimpleType.getFeelType() == BuiltInType.DATE;
+ }
+
+ /**
+ * Resolves a {@link DMNType} from a {@link QName} by first consulting the
model's type registry, then falling
+ * back to {@link DMNTypeRegistryAbstract#getFeelPrimitiveType} for
built-in FEEL types.
+ *
+ * @param typeRef the qualified type reference to resolve
+ * @param model the DMN model whose registry is searched first
+ * @return the resolved {@link DMNType}, or {@code null} if neither the
registry nor the built-in types match
+ */
+ public static DMNType resolveDMNType(QName typeRef, DMNModelImpl model) {
+ DMNType type =
model.getTypeRegistry().resolveType(model.getNamespace(),
typeRef.getLocalPart());
+ if (type != null) {
+ return type;
+ }
+ // determineTypeFromName returns UNKNOWN both for a genuine no-match
and for the literal "unknown"/"any"
+ return Arrays.stream(BuiltInType.values())
+ .filter(b ->
Arrays.asList(b.getNames()).contains(typeRef.getLocalPart()))
+ .findFirst()
+ .map(b ->
DMNTypeRegistryAbstract.getFeelPrimitiveType(b.getName(), b,
model.getNamespace(), model.getTypeRegistry().unknown()))
+ .orElse(null);
+ }
+
+}
\ No newline at end of file
diff --git
a/kie-dmn/kie-dmn-core/src/test/java/org/kie/dmn/core/compiler/DecisionServiceCompilerTest.java
b/kie-dmn/kie-dmn-core/src/test/java/org/kie/dmn/core/compiler/DecisionServiceCompilerTest.java
index 8004538c47f..0e1c10fb771 100644
---
a/kie-dmn/kie-dmn-core/src/test/java/org/kie/dmn/core/compiler/DecisionServiceCompilerTest.java
+++
b/kie-dmn/kie-dmn-core/src/test/java/org/kie/dmn/core/compiler/DecisionServiceCompilerTest.java
@@ -31,7 +31,6 @@ import org.kie.dmn.model.api.Definitions;
import org.kie.dmn.model.api.Import;
import org.kie.dmn.model.api.NamedElement;
import org.mockito.MockedStatic;
-import org.mockito.Mockito;
import javax.xml.namespace.QName;
import java.util.List;
diff --git
a/kie-dmn/kie-dmn-core/src/test/java/org/kie/dmn/core/decisionservices/DMNDecisionServicesTest.java
b/kie-dmn/kie-dmn-core/src/test/java/org/kie/dmn/core/decisionservices/DMNDecisionServicesTest.java
index 10fb8af9a3b..49e4960a7ea 100644
---
a/kie-dmn/kie-dmn-core/src/test/java/org/kie/dmn/core/decisionservices/DMNDecisionServicesTest.java
+++
b/kie-dmn/kie-dmn-core/src/test/java/org/kie/dmn/core/decisionservices/DMNDecisionServicesTest.java
@@ -19,6 +19,8 @@
package org.kie.dmn.core.decisionservices;
import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
import java.util.Map;
import org.junit.jupiter.params.ParameterizedTest;
@@ -32,6 +34,7 @@ import org.kie.dmn.core.BaseInterpretedVsCompiledTest;
import org.kie.dmn.core.api.DMNFactory;
import org.kie.dmn.core.compiler.CoerceDecisionServiceSingletonOutputOption;
import org.kie.dmn.core.util.DMNRuntimeUtil;
+import org.kie.dmn.feel.runtime.custom.FormattedZonedDateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -733,4 +736,83 @@ public class DMNDecisionServicesTest extends
BaseInterpretedVsCompiledTest {
assertSupplyingPersonname(runtime, dmnModel);
}
+
+ //
=========================================================================
+ // Integration tests — DecisionServiceImplicitConversions.dmn
(incubator-kie-issues#2188)
+ //
+ // DS1 "To Singleton List DS": body typeRef=date, DS
typeRef=dateList
+ // → evaluator wraps scalar date into a singleton dateList
+ // DS2 "From Singleton List DS": body typeRef=dateList, DS
typeRef=date
+ // → evaluator unwraps singleton dateList to a scalar date
+ // DS3 "From Date to Date and Time DS": body typeRef=date, DS
typeRef=date and time
+ // → evaluator coerces LocalDate to ZonedDateTime at midnight UTC
+ //
=========================================================================
+
+ private static final String NS =
"https://kie.org/dmn/_F9BB5760-8BCA-4216-AAD9-8BD4FB70802D";
+ private static final String NAME = "1157-implicit-conversions-DS";
+ private static final String FILE_NAME =
"valid_models/DMNv1_6/DecisionService/DecisionServiceImplicitConversions.dmn";
+
+ @ParameterizedTest
+ @MethodSource("params")
+ void implicitConversionToSingletonList(boolean useExecModelCompiler) {
+ init(useExecModelCompiler);
+ final DMNRuntime runtime = DMNRuntimeUtil.createRuntime(FILE_NAME,
this.getClass());
+ final DMNModel dmnModel = runtime.getModel(NS, NAME);
+ assertThat(dmnModel).isNotNull();
+
assertThat(dmnModel.hasErrors()).as(DMNRuntimeUtil.formatMessages(dmnModel.getMessages())).isFalse();
+
+ final DMNResult dmnResult = runtime.evaluateDecisionService(dmnModel,
DMNFactory.newContext(), "To Singleton List DS");
+ LOG.debug("{}", dmnResult);
+ dmnResult.getDecisionResults().forEach(x -> LOG.debug("{}", x));
+
assertThat(dmnResult.hasErrors()).as(DMNRuntimeUtil.formatMessages(dmnResult.getMessages())).isFalse();
+
+ // Body 1 returns date(2000,1,2) — a scalar date; the DS wraps it into
a singleton dateList
+ Object result = dmnResult.getContext().get("Body 1");
+ assertThat(result).isInstanceOf(List.class);
+ assertThat((List<Object>) result).containsExactly(LocalDate.of(2000,
1, 2));
+ }
+
+ @ParameterizedTest
+ @MethodSource("params")
+ void implicitConversionFromSingletonListDS(boolean useExecModelCompiler) {
+ init(useExecModelCompiler);
+ final DMNRuntime runtime = DMNRuntimeUtil.createRuntime(FILE_NAME,
this.getClass());
+ final DMNModel dmnModel = runtime.getModel(NS, NAME);
+ assertThat(dmnModel).isNotNull();
+
assertThat(dmnModel.hasErrors()).as(DMNRuntimeUtil.formatMessages(dmnModel.getMessages())).isFalse();
+
+ final DMNResult dmnResult = runtime.evaluateDecisionService(dmnModel,
DMNFactory.newContext(), "From Singleton List DS");
+ LOG.debug("{}", dmnResult);
+ dmnResult.getDecisionResults().forEach(x -> LOG.debug("{}", x));
+
assertThat(dmnResult.hasErrors()).as(DMNRuntimeUtil.formatMessages(dmnResult.getMessages())).isFalse();
+
+ // Body 2 returns [date(2000,1,2)] — a singleton list; the DS unwraps
it to the scalar date
+ assertThat(dmnResult.getContext().get("Body
2")).isEqualTo(LocalDate.of(2000, 1, 2));
+ }
+
+ @ParameterizedTest
+ @MethodSource("params")
+ void implicitConversionfromDateToDateTimeDS(boolean useExecModelCompiler) {
+ init(useExecModelCompiler);
+ final DMNRuntime runtime = DMNRuntimeUtil.createRuntime(FILE_NAME,
this.getClass());
+ final DMNModel dmnModel = runtime.getModel(NS, NAME);
+ assertThat(dmnModel).isNotNull();
+
assertThat(dmnModel.hasErrors()).as(DMNRuntimeUtil.formatMessages(dmnModel.getMessages())).isFalse();
+
+ final DMNResult dmnResult = runtime.evaluateDecisionService(dmnModel,
DMNFactory.newContext(), "From Date to Date and Time DS");
+ LOG.debug("{}", dmnResult);
+ dmnResult.getDecisionResults().forEach(x -> LOG.debug("{}", x));
+
assertThat(dmnResult.hasErrors()).as(DMNRuntimeUtil.formatMessages(dmnResult.getMessages())).isFalse();
+
+ // Body 3 returns date(2000,1,2) — the DS coerces it to a
ZonedDateTime at midnight UTC
+ Object result = dmnResult.getContext().get("Body 3");
+ assertThat(result).isInstanceOf(FormattedZonedDateTime.class);
+ FormattedZonedDateTime zdt = (FormattedZonedDateTime) result;
+ assertThat(zdt.getZonedDateTime().getYear()).isEqualTo(2000);
+ assertThat(zdt.getZonedDateTime().getMonthValue()).isEqualTo(1);
+ assertThat(zdt.getZonedDateTime().getDayOfMonth()).isEqualTo(2);
+ assertThat(zdt.getZonedDateTime().getHour()).isEqualTo(0);
+ assertThat(zdt.getZonedDateTime().getMinute()).isEqualTo(0);
+ assertThat(zdt.getZonedDateTime().getSecond()).isEqualTo(0);
+ }
}
diff --git
a/kie-dmn/kie-dmn-core/src/test/java/org/kie/dmn/core/impl/DMNRuntimeUtilsTest.java
b/kie-dmn/kie-dmn-core/src/test/java/org/kie/dmn/core/impl/DMNRuntimeUtilsTest.java
index fb19c68a80e..295a86d28bb 100644
---
a/kie-dmn/kie-dmn-core/src/test/java/org/kie/dmn/core/impl/DMNRuntimeUtilsTest.java
+++
b/kie-dmn/kie-dmn-core/src/test/java/org/kie/dmn/core/impl/DMNRuntimeUtilsTest.java
@@ -27,12 +27,15 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.IntStream;
+import javax.xml.namespace.QName;
import org.junit.jupiter.api.Test;
import org.kie.dmn.api.core.DMNContext;
import org.kie.dmn.api.core.DMNType;
import org.kie.dmn.api.core.ast.DMNNode;
import org.kie.dmn.api.core.ast.InputDataNode;
import org.kie.dmn.core.ast.InputDataNodeImpl;
+import org.kie.dmn.core.compiler.DMNTypeRegistryV16;
+import org.kie.dmn.feel.lang.types.BuiltInType;
import org.kie.dmn.model.api.Definitions;
import org.kie.dmn.model.api.InputData;
@@ -445,6 +448,93 @@ class DMNRuntimeUtilsTest {
assertThat(DMNRuntimeUtils.getIdentifier(dmnNode)).isEqualTo(name);
}
+ // decisionServiceOutputType=scalar "string",
decisionOutputType=collection "tDateList" (base=date) — bases differ → false
+ @Test
+ void isReturnTypeCollectionCompatible_incompatible() {
+ final String ns = "ns";
+ DMNTypeRegistryV16 registry = new DMNTypeRegistryV16(null);
+ SimpleTypeImpl dateType = new SimpleTypeImpl(ns, "date", null, false,
null, null, null, BuiltInType.DATE);
+ SimpleTypeImpl tDateList = new SimpleTypeImpl(ns, "tDateList", null,
true, null, null, dateType, BuiltInType.DATE);
+ registry.registerType(tDateList);
+
+ DMNModelImpl model = mock(DMNModelImpl.class);
+ when(model.getNamespace()).thenReturn(ns);
+ when(model.getTypeRegistry()).thenReturn(registry);
+
+ assertThat(DMNRuntimeUtils.isReturnTypeCollectionCompatible(
+ new QName(ns, "string"), new QName(ns, "tDateList"),
model)).isFalse();
+ }
+
+ // decisionServiceOutputType=scalar "date", decisionOutputType=collection
"tDateList" (base=date) — same base → true
+ @Test
+ void isReturnTypeCollectionCompatible_scalarFiCollectionFd() {
+ final String ns = "ns";
+ DMNTypeRegistryV16 registry = new DMNTypeRegistryV16(null);
+ SimpleTypeImpl dateType = new SimpleTypeImpl(ns, "date", null, false,
null, null, null, BuiltInType.DATE);
+ SimpleTypeImpl tDateList = new SimpleTypeImpl(ns, "tDateList", null,
true, null, null, dateType, BuiltInType.DATE);
+ registry.registerType(tDateList);
+
+ DMNModelImpl model = mock(DMNModelImpl.class);
+ when(model.getNamespace()).thenReturn(ns);
+ when(model.getTypeRegistry()).thenReturn(registry);
+
+ assertThat(DMNRuntimeUtils.isReturnTypeCollectionCompatible(
+ new QName(ns, "date"), new QName(ns, "tDateList"),
model)).isTrue();
+ }
+
+ // decisionServiceOutputType=collection "tDateList" (base=date),
decisionOutputType=scalar "date" — compatible → true
+ @Test
+ void isReturnTypeCollectionCompatible_collectionFiScalarFd() {
+ final String ns = "ns";
+ DMNTypeRegistryV16 registry = new DMNTypeRegistryV16(null);
+ SimpleTypeImpl dateType = new SimpleTypeImpl(ns, "date", null, false,
null, null, null, BuiltInType.DATE);
+ SimpleTypeImpl tDateList = new SimpleTypeImpl(ns, "tDateList", null,
true, null, null, dateType, BuiltInType.DATE);
+ registry.registerType(tDateList);
+
+ DMNModelImpl model = mock(DMNModelImpl.class);
+ when(model.getNamespace()).thenReturn(ns);
+ when(model.getTypeRegistry()).thenReturn(registry);
+
+ assertThat(DMNRuntimeUtils.isReturnTypeCollectionCompatible(
+ new QName(ns, "tDateList"), new QName(ns, "date"),
model)).isTrue();
+ }
+
+ // decisionServiceOutputType="date and time", decisionOutputType="date" —
date-to-dateTime coercion pattern → true
+ @Test
+ void isReturnTypeCollectionCompatible_dateTimeAndDateCompatible() {
+ DMNTypeRegistryV16 registry = new DMNTypeRegistryV16(null);
+ DMNModelImpl model = mock(DMNModelImpl.class);
+ when(model.getNamespace()).thenReturn("ns");
+ when(model.getTypeRegistry()).thenReturn(registry);
+
+ assertThat(DMNRuntimeUtils.isReturnTypeCollectionCompatible(
+ new QName("", "date and time"), new QName("", "date"),
model)).isTrue();
+ }
+
+ // decisionServiceOutputType="date", decisionOutputType="date and time" —
order matters → false
+ @Test
+ void isReturnTypeCollectionCompatible_dateAndDateTimeIncompatible() {
+ DMNTypeRegistryV16 registry = new DMNTypeRegistryV16(null);
+ DMNModelImpl model = mock(DMNModelImpl.class);
+ when(model.getNamespace()).thenReturn("ns");
+ when(model.getTypeRegistry()).thenReturn(registry);
+
+ assertThat(DMNRuntimeUtils.isReturnTypeCollectionCompatible(
+ new QName("", "date"), new QName("", "date and time"),
model)).isFalse();
+ }
+
+ // either typeRef is unknown — resolveDMNType returns null → false
+ @Test
+ void isReturnTypeCollectionCompatible_unknownTypeRef() {
+ DMNTypeRegistryV16 registry = new DMNTypeRegistryV16(null);
+ DMNModelImpl model = mock(DMNModelImpl.class);
+ when(model.getNamespace()).thenReturn("ns");
+ when(model.getTypeRegistry()).thenReturn(registry);
+
+ assertThat(DMNRuntimeUtils.isReturnTypeCollectionCompatible(
+ new QName("", "not-a-type"), new QName("", "date"),
model)).isFalse();
+ }
+
private static class OverflowingObject {
String string;
diff --git
a/kie-dmn/kie-dmn-test-resources/src/test/resources/valid_models/DMNv1_6/DecisionService/DecisionServiceImplicitConversions.dmn
b/kie-dmn/kie-dmn-test-resources/src/test/resources/valid_models/DMNv1_6/DecisionService/DecisionServiceImplicitConversions.dmn
new file mode 100644
index 00000000000..ba121ff86c8
--- /dev/null
+++
b/kie-dmn/kie-dmn-test-resources/src/test/resources/valid_models/DMNv1_6/DecisionService/DecisionServiceImplicitConversions.dmn
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ 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.
+-->
+<definitions xmlns="https://www.omg.org/spec/DMN/20230324/MODEL/"
+ xmlns:dmndi="https://www.omg.org/spec/DMN/20230324/DMNDI/"
+ xmlns:dc="http://www.omg.org/spec/DMN/20180521/DC/"
+ xmlns:di="http://www.omg.org/spec/DMN/20180521/DI/"
+ xmlns:kie="https://kie.org/dmn/extensions/1.0"
+ expressionLanguage="https://www.omg.org/spec/DMN/20230324/FEEL/"
+
namespace="https://kie.org/dmn/_F9BB5760-8BCA-4216-AAD9-8BD4FB70802D"
+ id="_B6EF67C0-8376-4854-8D71-79484E1DBF39"
+ name="1157-implicit-conversions-DS">
+
+ <!-- ── Custom collection types ────────────────────────────────────────
-->
+ <itemDefinition id="_7546A1B0-00DF-40AB-B2CD-6ED845798BAA" name="dateList"
isCollection="true">
+ <typeRef>date</typeRef>
+ </itemDefinition>
+
+ <!-- ── FunctionItem-based DS types ────────────────────────────────────
-->
+ <!-- DS1: function returning dateList (scalar date output → wrapped to
list) -->
+ <itemDefinition id="_7546A1B0-00DF-40AB-B2CD-6ED845798BAE"
name="functionReturningDateList">
+ <functionItem outputTypeRef="dateList"
id="_DC20B4DF-E952-43B8-8CAB-AE78FD7E5F10" />
+ </itemDefinition>
+ <!-- DS2: function returning date (dateList output → unwrapped to scalar)
-->
+ <itemDefinition id="_7546A1B0-00DF-40AB-B2CD-6ED845798BAF"
name="functionReturningDate">
+ <functionItem outputTypeRef="date"
id="_E1AEDADB-A715-4AF9-B312-A60619877709" />
+ </itemDefinition>
+ <!-- DS3: function returning date and time (date output → coerced to
dateTime) -->
+ <itemDefinition id="_7546A1B0-00DF-40AB-B2CD-6ED845798BAD"
name="functionReturningDateTime">
+ <functionItem outputTypeRef="date and time"
id="_8FBD47B9-9EA7-4A08-9AAA-7858B9D62B55" />
+ </itemDefinition>
+
+ <!-- ── DS1: scalar date → singleton dateList ──────────────────────────
-->
+ <decisionService name="To Singleton List DS"
id="_BF3F2933-A588-41F1-AC98-492A79088A53">
+ <variable name="To Singleton List DS"
id="_4C2AB9A4-BDB1-4BC4-A9B6-040020AA235E" typeRef="functionReturningDateList"
/>
+ <outputDecision href="#_B3384D41-36C1-414E-A48D-FF817F728BB2" />
+ </decisionService>
+ <decision name="Body 1" id="_B3384D41-36C1-414E-A48D-FF817F728BB2">
+ <variable id="_9F749E69-EDEB-4B02-9130-E32E0F1065A1" typeRef="date"
name="Body 1" />
+ <literalExpression id="_56188658-200B-4385-99B0-72CC17A8A6F7"
typeRef="date">
+ <text>date(2000, 1, 2)</text>
+ </literalExpression>
+ </decision>
+
+ <!-- ── DS2: singleton dateList → scalar date ──────────────────────────
-->
+ <decisionService name="From Singleton List DS"
id="_79630040-ED4D-4656-A7E8-700D5DF62B3F">
+ <variable name="From Singleton List DS"
id="_6888721D-A77C-4EEB-BB53-0248D23E1B68" typeRef="functionReturningDate" />
+ <outputDecision href="#_A8E5ACA2-02C8-4424-A826-AC330FF424FF" />
+ </decisionService>
+ <decision name="Body 2" id="_A8E5ACA2-02C8-4424-A826-AC330FF424FF">
+ <variable id="_0B08A715-E336-4F39-8665-159D51BD30A7"
typeRef="dateList" name="Body 2" />
+ <literalExpression id="_0A455E79-9FD3-469E-8917-099508B742DB"
typeRef="dateList">
+ <text>[date(2000, 1, 2)]</text>
+ </literalExpression>
+ </decision>
+
+ <!-- ── DS3: date → date and time ──────────────────────────────────────
-->
+ <decisionService name="From Date to Date and Time DS"
id="_44150402-E698-4F84-B30D-51BB159F9E92">
+ <variable name="From Date to Date and Time DS"
id="_A2C1C994-4A9C-4CF0-9062-4A9469E2779A" typeRef="functionReturningDateTime"
/>
+ <outputDecision href="#_9B10A67E-B994-492A-BB53-089F22A8B9D4" />
+ </decisionService>
+ <decision name="Body 3" id="_9B10A67E-B994-492A-BB53-089F22A8B9D4">
+ <variable id="_EDE24ED4-A39B-4FFF-9C67-11369F5F67D6" typeRef="date"
name="Body 3" />
+ <literalExpression id="_363B48FD-4E56-4176-BA21-0B619143EEE7"
typeRef="date">
+ <text>date(2000, 1, 2)</text>
+ </literalExpression>
+ </decision>
+
+</definitions>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]