From 1566e87a79a2a0eb6180e2f8fb8b0ad72d50b691 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jul 2024 08:58:39 +0200 Subject: [PATCH 1/8] Bump braces from 3.0.2 to 3.0.3 in /drools-docs (#5998) Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3. - [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3) --- updated-dependencies: - dependency-name: braces dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- drools-docs/package-lock.json | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/drools-docs/package-lock.json b/drools-docs/package-lock.json index f8010c88263..b6c639a9ef6 100644 --- a/drools-docs/package-lock.json +++ b/drools-docs/package-lock.json @@ -940,12 +940,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -1424,9 +1424,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -3757,12 +3757,12 @@ } }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "buffer-crc32": { @@ -4156,9 +4156,9 @@ } }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "requires": { "to-regex-range": "^5.0.1" From d887d5ce00d77430732f8fe0788b7ba68a523a27 Mon Sep 17 00:00:00 2001 From: Toshiya Kobayashi Date: Wed, 10 Jul 2024 10:10:35 +0900 Subject: [PATCH 2/8] [incubator-kie-drools-6007] Executable model doesn't report an error when duplicated (#6013) * removing kie-ci from dependency, because it causes a test failure in KieBaseIncludeTest * Use canonicalKieModule.getKiePackages() rather than getKieBase() * null check for kiePackage * move populateIncludedRuleNameMap out of packages loop * removed unused FileManager * performance improvement. Use getModelForKBase instead of getKiePackages * Fit into build phases * clean up --- .../kie/builder/impl/AbstractKieProject.java | 8 +- .../kie/builder/impl/BuildContext.java | 13 ++ .../execmodel/CanonicalModelBuildContext.java | 14 ++ .../codegen/execmodel/ModelBuilderImpl.java | 3 +- .../processors/ModelMainCompilationPhase.java | 18 +- .../processors/ModelRuleValidator.java | 64 ++++++ .../PopulateIncludedRuleNameMapPhase.java | 63 ++++++ .../modelcompiler/CanonicalKieModule.java | 7 +- .../integrationtests/KieBaseIncludesTest.java | 196 ++++++++++++++++++ 9 files changed, 380 insertions(+), 6 deletions(-) create mode 100644 drools-model/drools-model-codegen/src/main/java/org/drools/model/codegen/execmodel/processors/ModelRuleValidator.java create mode 100644 drools-model/drools-model-codegen/src/main/java/org/drools/model/codegen/execmodel/processors/PopulateIncludedRuleNameMapPhase.java diff --git a/drools-compiler/src/main/java/org/drools/compiler/kie/builder/impl/AbstractKieProject.java b/drools-compiler/src/main/java/org/drools/compiler/kie/builder/impl/AbstractKieProject.java index 7d8c399bea8..7775abacc43 100644 --- a/drools-compiler/src/main/java/org/drools/compiler/kie/builder/impl/AbstractKieProject.java +++ b/drools-compiler/src/main/java/org/drools/compiler/kie/builder/impl/AbstractKieProject.java @@ -225,6 +225,8 @@ public KnowledgeBuilder buildKnowledgePackages( KieBaseModelImpl kBaseModel, Bui Set assets = new LinkedHashSet<>(); + InternalKieModule kModule = getKieModuleForKBase(kBaseModel.getName()); + boolean allIncludesAreValid = true; for (String include : getTransitiveIncludes(kBaseModel)) { if ( StringUtils.isEmpty( include )) { @@ -240,6 +242,11 @@ public KnowledgeBuilder buildKnowledgePackages( KieBaseModelImpl kBaseModel, Bui } if (compileIncludedKieBases()) { addFiles( buildFilter, assets, getKieBaseModel( include ), includeModule, useFolders ); + } else { + if (kModule != includeModule) { + // includeModule is not part of the current kModule + buildContext.addIncludeModule(getKieBaseModel(include), includeModule); + } } } @@ -247,7 +254,6 @@ public KnowledgeBuilder buildKnowledgePackages( KieBaseModelImpl kBaseModel, Bui return null; } - InternalKieModule kModule = getKieModuleForKBase(kBaseModel.getName()); addFiles( buildFilter, assets, kBaseModel, kModule, useFolders ); KnowledgeBuilder kbuilder; diff --git a/drools-compiler/src/main/java/org/drools/compiler/kie/builder/impl/BuildContext.java b/drools-compiler/src/main/java/org/drools/compiler/kie/builder/impl/BuildContext.java index f9b564d7745..2d731956aba 100644 --- a/drools-compiler/src/main/java/org/drools/compiler/kie/builder/impl/BuildContext.java +++ b/drools-compiler/src/main/java/org/drools/compiler/kie/builder/impl/BuildContext.java @@ -18,6 +18,11 @@ */ package org.drools.compiler.kie.builder.impl; +import java.util.Collections; +import java.util.Map; + +import org.kie.api.builder.model.KieBaseModel; + public class BuildContext { private final ResultsImpl messages; @@ -36,4 +41,12 @@ public ResultsImpl getMessages() { public boolean registerResourceToBuild(String kBaseName, String resource) { return true; } + + public void addIncludeModule(KieBaseModel kieBaseModel, InternalKieModule includeModule) { + // no op + } + + public Map getIncludeModules() { + return Collections.emptyMap(); + } } diff --git a/drools-model/drools-model-codegen/src/main/java/org/drools/model/codegen/execmodel/CanonicalModelBuildContext.java b/drools-model/drools-model-codegen/src/main/java/org/drools/model/codegen/execmodel/CanonicalModelBuildContext.java index ddf51f164ad..3b019df9670 100644 --- a/drools-model/drools-model-codegen/src/main/java/org/drools/model/codegen/execmodel/CanonicalModelBuildContext.java +++ b/drools-model/drools-model-codegen/src/main/java/org/drools/model/codegen/execmodel/CanonicalModelBuildContext.java @@ -26,7 +26,9 @@ import java.util.Set; import org.drools.compiler.kie.builder.impl.BuildContext; +import org.drools.compiler.kie.builder.impl.InternalKieModule; import org.drools.compiler.kie.builder.impl.ResultsImpl; +import org.kie.api.builder.model.KieBaseModel; public class CanonicalModelBuildContext extends BuildContext { @@ -36,6 +38,8 @@ public class CanonicalModelBuildContext extends BuildContext { private final Collection allGeneratedPojos = new HashSet<>(); private final Map> allCompiledClasses = new HashMap<>(); + private final Map includeModules = new HashMap<>(); + public CanonicalModelBuildContext() { } public CanonicalModelBuildContext(ResultsImpl messages) { @@ -84,4 +88,14 @@ private String resource2Package(String resource) { int pathEndPos = resource.lastIndexOf('/'); return pathEndPos <= 0 ? "" :resource.substring(0, pathEndPos).replace('/', '.'); } + + @Override + public void addIncludeModule(KieBaseModel kieBaseModel, InternalKieModule includeModule) { + includeModules.put(kieBaseModel, includeModule); + } + + @Override + public Map getIncludeModules() { + return includeModules; + } } diff --git a/drools-model/drools-model-codegen/src/main/java/org/drools/model/codegen/execmodel/ModelBuilderImpl.java b/drools-model/drools-model-codegen/src/main/java/org/drools/model/codegen/execmodel/ModelBuilderImpl.java index 6c9df9af3fd..a9622724173 100644 --- a/drools-model/drools-model-codegen/src/main/java/org/drools/model/codegen/execmodel/ModelBuilderImpl.java +++ b/drools-model/drools-model-codegen/src/main/java/org/drools/model/codegen/execmodel/ModelBuilderImpl.java @@ -108,7 +108,8 @@ protected void doSecondBuildStep(Collection compositePack this.getGlobalVariableContext(), this.sourcesGenerator, this.packageSources, - oneClassPerRule)); + oneClassPerRule, + this.getBuildContext())); for (CompilationPhase phase : phases) { phase.process(); diff --git a/drools-model/drools-model-codegen/src/main/java/org/drools/model/codegen/execmodel/processors/ModelMainCompilationPhase.java b/drools-model/drools-model-codegen/src/main/java/org/drools/model/codegen/execmodel/processors/ModelMainCompilationPhase.java index c66d6e2fc0c..8485df83da1 100644 --- a/drools-model/drools-model-codegen/src/main/java/org/drools/model/codegen/execmodel/processors/ModelMainCompilationPhase.java +++ b/drools-model/drools-model-codegen/src/main/java/org/drools/model/codegen/execmodel/processors/ModelMainCompilationPhase.java @@ -20,7 +20,10 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.function.Function; import org.drools.compiler.builder.PackageRegistryManager; @@ -33,9 +36,9 @@ import org.drools.compiler.builder.impl.processors.FunctionCompilationPhase; import org.drools.compiler.builder.impl.processors.GlobalCompilationPhase; import org.drools.compiler.builder.impl.processors.IteratingPhase; -import org.drools.compiler.builder.impl.processors.RuleValidator; import org.drools.compiler.builder.impl.processors.SinglePackagePhaseFactory; import org.drools.compiler.builder.impl.processors.WindowDeclarationCompilationPhase; +import org.drools.compiler.kie.builder.impl.BuildContext; import org.drools.compiler.lang.descr.CompositePackageDescr; import org.drools.kiesession.rulebase.InternalKnowledgeBase; import org.drools.model.codegen.execmodel.PackageModel; @@ -61,6 +64,8 @@ public class ModelMainCompilationPhase implements CompilationPhase { private final PackageSourceManager packageSourceManager; private final boolean oneClassPerRule; + private final BuildContext buildContext; + public ModelMainCompilationPhase( PackageModelManager packageModels, PackageRegistryManager pkgRegistryManager, @@ -69,7 +74,11 @@ public ModelMainCompilationPhase( boolean hasMvel, InternalKnowledgeBase kBase, TypeDeclarationContext typeDeclarationContext, - GlobalVariableContext globalVariableContext, Function sourceGenerator, PackageSourceManager packageSourceManager, boolean oneClassPerRule) { + GlobalVariableContext globalVariableContext, + Function sourceGenerator, + PackageSourceManager packageSourceManager, + boolean oneClassPerRule, + BuildContext buildContext) { this.packageModels = packageModels; this.pkgRegistryManager = pkgRegistryManager; this.packages = packages; @@ -81,6 +90,7 @@ public ModelMainCompilationPhase( this.sourceGenerator = sourceGenerator; this.packageSourceManager = packageSourceManager; this.oneClassPerRule = oneClassPerRule; + this.buildContext = buildContext; } @Override @@ -94,7 +104,9 @@ public void process() { phases.add(iteratingPhase((reg, acc) -> GlobalCompilationPhase.of(reg, acc, kBase, globalVariableContext, acc.getFilter()))); phases.add(new DeclaredTypeDeregistrationPhase(packages, pkgRegistryManager)); - phases.add(iteratingPhase((reg, acc) -> new RuleValidator(reg, acc, configuration))); // validateUniqueRuleNames + Map> includedRuleNameMap = new HashMap<>(); + phases.add(new PopulateIncludedRuleNameMapPhase(buildContext.getIncludeModules(), includedRuleNameMap)); + phases.add(iteratingPhase((reg, acc) -> new ModelRuleValidator(reg, acc, configuration, includedRuleNameMap))); // validateUniqueRuleNames phases.add(iteratingPhase((reg, acc) -> new ModelGeneratorPhase(reg, acc, packageModels.getPackageModel(acc, reg, acc.getName()), typeDeclarationContext))); // validateUniqueRuleNames phases.add(iteratingPhase((reg, acc) -> new SourceCodeGenerationPhase<>( packageModels.getPackageModel(acc, reg, acc.getName()), packageSourceManager, sourceGenerator, oneClassPerRule))); // validateUniqueRuleNames diff --git a/drools-model/drools-model-codegen/src/main/java/org/drools/model/codegen/execmodel/processors/ModelRuleValidator.java b/drools-model/drools-model-codegen/src/main/java/org/drools/model/codegen/execmodel/processors/ModelRuleValidator.java new file mode 100644 index 00000000000..b4bc7a9ce75 --- /dev/null +++ b/drools-model/drools-model-codegen/src/main/java/org/drools/model/codegen/execmodel/processors/ModelRuleValidator.java @@ -0,0 +1,64 @@ +/** + * 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.drools.model.codegen.execmodel.processors; + +import java.util.Map; +import java.util.Set; + +import org.drools.compiler.builder.impl.processors.RuleValidator; +import org.drools.compiler.compiler.PackageRegistry; +import org.drools.drl.ast.descr.PackageDescr; +import org.drools.drl.ast.descr.RuleDescr; +import org.drools.drl.parser.ParserError; +import org.kie.internal.builder.KnowledgeBuilderConfiguration; + +public class ModelRuleValidator extends RuleValidator { + + // extra rule names which need to be checked for duplicates. + // Non-executable model doesn't need this because included kbase assets are added to the packageDescr + // Executable model requires this because included kbase assets are modeled as separated kjar, so not added to the packageDescr + private final Map> includedRuleNameMap; + + public ModelRuleValidator(PackageRegistry pkgRegistry, PackageDescr packageDescr, KnowledgeBuilderConfiguration configuration, Map> includedRuleNameMap) { + super(pkgRegistry, packageDescr, configuration); + this.includedRuleNameMap = includedRuleNameMap; + } + + @Override + public void process() { + super.process(); + + // Check with included rule names, because exec-model doesn't add assets of included kbase to the packageDescr + String packageName = packageDescr.getNamespace(); + if (includedRuleNameMap.containsKey(packageName)) { + Set ruleNames = includedRuleNameMap.get(packageName); + for (final RuleDescr rule : packageDescr.getRules()) { + final String name = rule.getUnitQualifiedName(); + if (ruleNames.contains(name)) { + this.results.add(new ParserError(rule.getResource(), + "Duplicate rule name: " + name, + rule.getLine(), + rule.getColumn(), + packageName)); + } + } + } + } +} diff --git a/drools-model/drools-model-codegen/src/main/java/org/drools/model/codegen/execmodel/processors/PopulateIncludedRuleNameMapPhase.java b/drools-model/drools-model-codegen/src/main/java/org/drools/model/codegen/execmodel/processors/PopulateIncludedRuleNameMapPhase.java new file mode 100644 index 00000000000..ea66ed29ec1 --- /dev/null +++ b/drools-model/drools-model-codegen/src/main/java/org/drools/model/codegen/execmodel/processors/PopulateIncludedRuleNameMapPhase.java @@ -0,0 +1,63 @@ +/** + * 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.drools.model.codegen.execmodel.processors; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.drools.compiler.builder.impl.processors.CompilationPhase; +import org.drools.compiler.kie.builder.impl.InternalKieModule; +import org.drools.compiler.kproject.models.KieBaseModelImpl; +import org.drools.model.Model; +import org.drools.modelcompiler.CanonicalKieModule; +import org.kie.api.builder.model.KieBaseModel; +import org.kie.internal.builder.KnowledgeBuilderResult; + +public class PopulateIncludedRuleNameMapPhase implements CompilationPhase { + + private final Map includeModules; + private final Map> includedRuleNameMap; + + public PopulateIncludedRuleNameMapPhase(Map includeModules, Map> includedRuleNameMap) { + this.includeModules = includeModules; + this.includedRuleNameMap = includedRuleNameMap; + } + + @Override + public void process() { + for (Map.Entry entry : includeModules.entrySet()) { + KieBaseModel kieBaseModel = entry.getKey(); + InternalKieModule includeModule = entry.getValue(); + if ((includeModule instanceof CanonicalKieModule canonicalKieModule) && canonicalKieModule.hasModelFile()) { + Collection includeModels = canonicalKieModule.getModelForKBase((KieBaseModelImpl)kieBaseModel); + for (Model includeModel : includeModels) { + includeModel.getRules().forEach(rule -> includedRuleNameMap.computeIfAbsent(includeModel.getPackageName(), k -> new HashSet<>()).add(rule.getName())); + } + } + } + } + + @Override + public Collection getResults() { + return Collections.emptyList(); + } +} diff --git a/drools-model/drools-model-compiler/src/main/java/org/drools/modelcompiler/CanonicalKieModule.java b/drools-model/drools-model-compiler/src/main/java/org/drools/modelcompiler/CanonicalKieModule.java index f5c4ddb66f6..f64be9e4257 100644 --- a/drools-model/drools-model-compiler/src/main/java/org/drools/modelcompiler/CanonicalKieModule.java +++ b/drools-model/drools-model-compiler/src/main/java/org/drools/modelcompiler/CanonicalKieModule.java @@ -466,7 +466,7 @@ private Collection getRuleClassNames() { return ruleClassesNames; } - private Collection getModelForKBase(KieBaseModelImpl kBaseModel) { + public Collection getModelForKBase(KieBaseModelImpl kBaseModel) { Map modelsMap = getModels(); if (kBaseModel.getPackages().isEmpty()) { return modelsMap.values(); @@ -484,6 +484,11 @@ private Collection getModelForKBase(KieBaseModelImpl kBaseModel) { return models; } + // This method indicates if the kjar was already compiled with the executable model + public boolean hasModelFile() { + return resourceFileExists(getModelFileWithGAV(internalKieModule.getReleaseId())); + } + private Collection findRuleClassesNames() { ReleaseId releaseId = internalKieModule.getReleaseId(); String modelFiles = readExistingResourceWithName(getModelFileWithGAV(releaseId)); diff --git a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/KieBaseIncludesTest.java b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/KieBaseIncludesTest.java index e9b358f02da..f68ac7bcfa3 100644 --- a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/KieBaseIncludesTest.java +++ b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/KieBaseIncludesTest.java @@ -18,7 +18,9 @@ */ package org.drools.mvel.integrationtests; +import java.io.IOException; import java.util.Collection; +import java.util.List; import org.drools.testcoverage.common.util.KieBaseTestConfiguration; import org.drools.testcoverage.common.util.KieUtil; @@ -28,11 +30,14 @@ import org.junit.runners.Parameterized; import org.kie.api.KieBase; import org.kie.api.KieServices; +import org.kie.api.builder.KieBuilder; import org.kie.api.builder.KieFileSystem; +import org.kie.api.builder.Message; import org.kie.api.builder.ReleaseId; import org.kie.api.definition.KiePackage; import org.kie.api.definition.rule.Rule; import org.kie.api.runtime.KieContainer; +import org.kie.api.runtime.KieSession; import static org.assertj.core.api.Assertions.assertThat; @@ -243,4 +248,195 @@ private static long getNumberOfRules(KieBase kieBase) { } return nrOfRules; } + + /** + * Test the inclusion of a KieBase defined in one KJAR into the KieBase of another KJAR. + *

+ * The 2 KieBases use the duplicate rule names, so an error should be reported + */ + @Test + public void kieBaseIncludesCrossKJarDuplicateRuleNames_shouldReportError() throws IOException { + + String pomContentMain = "\n" + + "4.0.0\n" + + "org.kie\n" + + "rules-main\n" + + "1.0.0\n" + + "jar\n" + + "\n" + + "\n" + + "org.kie\n" + + "rules-sub\n" + + "1.0.0\n" + + "\n" + + "\n" + + "\n"; + + String kmoduleContentMain = "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + ""; + + String drlMain = "package rules\n" + + "\n" + + "rule \"RuleA\"\n" + + "when\n" + + "then\n" + + "System.out.println(\"Rule in KieBaseMain\");\n" + + "end"; + + String kmoduleContentSub = "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + ""; + + String drlSub = "package rules\n" + + "\n" + + "rule \"RuleA\"\n" + + "when\n" + + "then\n" + + "System.out.println(\"Rule in KieBaseSub\");\n" + + "end"; + + KieServices ks = KieServices.Factory.get(); + ReleaseId releaseIdSub = ks.newReleaseId("org.kie", "rules-sub", "1.0.0"); + + //First deploy the second KJAR on which the first one depends. + KieFileSystem kfsSub = ks.newKieFileSystem() + .generateAndWritePomXML(releaseIdSub) + .write("src/main/resources/rules/rules.drl", drlSub) + .writeKModuleXML(kmoduleContentSub); + + KieUtil.getKieBuilderFromKieFileSystem(kieBaseTestConfiguration, kfsSub, true); + + KieFileSystem kfsMain = ks.newKieFileSystem() + .writePomXML(pomContentMain) + .write("src/main/resources/rules/rules.drl", drlMain) + .writeKModuleXML(kmoduleContentMain); + + KieBuilder kieBuilderMain = KieUtil.getKieBuilderFromKieFileSystem(kieBaseTestConfiguration, kfsMain, false); + List messages = kieBuilderMain.getResults().getMessages(Message.Level.ERROR); + + assertThat(messages).as("Duplication error should be reported") + .extracting(Message::getText).anyMatch(text -> text.contains("Duplicate rule name")); + } + + /** + * One KieBase that includes another KieBase from the same KJAR. Not duplicate names. + */ + @Test + public void kieBaseIncludesSameKJar() { + + String pomContent = "\n" + + "4.0.0\n" + + "org.kie\n" + + "rules-main-sub\n" + + "1.0.0\n" + + "jar\n" + + "\n"; + + String kmoduleContent = "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + ""; + + String drlMain = "package rules.main\n" + + "\n" + + "rule \"RuleA\"\n" + + "when\n" + + " $s : String()\n" + + "then\n" + + " System.out.println(\"Rule in KieBaseMain\");\n" + + "end"; + + String drlSub = "package rules.sub\n" + + "\n" + + "rule \"RuleB\"\n" + + "when\n" + + " $s : String()\n" + + "then\n" + + " System.out.println(\"Rule in KieBaseSub\");\n" + + "end"; + + KieServices ks = KieServices.Factory.get(); + + KieFileSystem kfsMain = ks.newKieFileSystem() + .writePomXML(pomContent) + .write("src/main/resources/rules/main/ruleMain.drl", drlMain) + .write("src/main/resources/rules/sub/ruleSub.drl", drlSub) + .writeKModuleXML(kmoduleContent); + + KieUtil.getKieBuilderFromKieFileSystem(kieBaseTestConfiguration, kfsMain, true); + ReleaseId releaseId = ks.newReleaseId("org.kie", "rules-main-sub", "1.0.0"); + KieContainer kieContainer = ks.newKieContainer(releaseId); + KieSession kieSession = kieContainer.newKieSession("ksessionMain"); + kieSession.insert("test"); + int fired = kieSession.fireAllRules(); + assertThat(fired).as("fire rules in main and sub").isEqualTo(2); + kieSession.dispose(); + } + + /** + * One KieBase that includes another KieBase from the same KJAR. Duplicate rule names. + */ + @Test + public void kieBaseIncludesSameKJarDuplicateRuleNames_shouldReportError() { + + String pomContent = "\n" + + "4.0.0\n" + + "org.kie\n" + + "rules-main-sub\n" + + "1.0.0\n" + + "jar\n" + + "\n"; + + String kmoduleContent = "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + ""; + + String drlMain = "package rules\n" + + "\n" + + "rule \"RuleA\"\n" + + "when\n" + + " $s : String()\n" + + "then\n" + + " System.out.println(\"Rule in KieBaseMain\");\n" + + "end"; + + String drlSub = "package rules\n" + // same package, same rule name + "\n" + + "rule \"RuleA\"\n" + + "when\n" + + " $s : String()\n" + + "then\n" + + " System.out.println(\"Rule in KieBaseSub\");\n" + + "end"; + + KieServices ks = KieServices.Factory.get(); + + KieFileSystem kfsMain = ks.newKieFileSystem() + .writePomXML(pomContent) + .write("src/main/resources/rules/main/ruleMain.drl", drlMain) + .write("src/main/resources/rules/sub/ruleSub.drl", drlSub) + .writeKModuleXML(kmoduleContent); + + KieBuilder kieBuilderMain = KieUtil.getKieBuilderFromKieFileSystem(kieBaseTestConfiguration, kfsMain, false); + List messages = kieBuilderMain.getResults().getMessages(Message.Level.ERROR); + + assertThat(messages).as("Duplication error should be reported") + .extracting(Message::getText).anyMatch(text -> text.contains("Duplicate rule name")); + } } From a41a70ad024d5bfa47e242f9f1b2064dab51d2a2 Mon Sep 17 00:00:00 2001 From: Yeser Amer Date: Wed, 10 Jul 2024 17:19:29 +0200 Subject: [PATCH 3/8] [incubator-kie-issues#1150] Improve Import Resolver error messages to be more user friendly (#6014) * Improved Error Messages + Logs * dependabot.yml fixed * dependabot.yml fixed * Change Request * Minor change * Tests fixed --- .../core/compiler/ImportDMNResolverUtil.java | 80 +++++++++++++------ .../compiler/ImportDMNResolverUtilTest.java | 6 ++ 2 files changed, 61 insertions(+), 25 deletions(-) diff --git a/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/compiler/ImportDMNResolverUtil.java b/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/compiler/ImportDMNResolverUtil.java index 4a15ef4d199..be11db65f25 100644 --- a/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/compiler/ImportDMNResolverUtil.java +++ b/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/compiler/ImportDMNResolverUtil.java @@ -26,58 +26,88 @@ import javax.xml.namespace.QName; import org.kie.dmn.feel.util.Either; +import org.kie.dmn.model.api.Definitions; import org.kie.dmn.model.api.Import; import org.kie.dmn.model.api.NamespaceConsts; import org.kie.dmn.model.v1_1.TImport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class ImportDMNResolverUtil { + private static final Logger LOGGER = LoggerFactory.getLogger(ImportDMNResolverUtil.class); + private ImportDMNResolverUtil() { // No constructor for util class. } - public static Either resolveImportDMN(Import _import, Collection all, Function idExtractor) { - final String iNamespace = _import.getNamespace(); - final String iName = _import.getName(); - final String iModelName = _import.getAdditionalAttributes().get(TImport.MODELNAME_QNAME); - List allInNS = all.stream() - .filter(m -> idExtractor.apply(m).getNamespaceURI().equals(iNamespace)) - .collect(Collectors.toList()); - if (allInNS.size() == 1) { - T located = allInNS.get(0); + public static Either resolveImportDMN(Import importElement, Collection dmns, Function idExtractor) { + final String importerDMNNamespace = ((Definitions) importElement.getParent()).getNamespace(); + final String importerDMNName = ((Definitions) importElement.getParent()).getName(); + final String importNamespace = importElement.getNamespace(); + final String importName = importElement.getName(); + final String importLocationURI = importElement.getLocationURI(); // This is optional + final String importModelName = importElement.getAdditionalAttributes().get(TImport.MODELNAME_QNAME); + + LOGGER.debug("Resolving an Import in DMN Model with name={} and namespace={}. " + + "Importing a DMN model with namespace={} name={} locationURI={}, modelName={}", + importerDMNNamespace, importerDMNName, importNamespace, importName, importLocationURI, importModelName); + + List matchingDMNList = dmns.stream() + .filter(m -> idExtractor.apply(m).getNamespaceURI().equals(importNamespace)) + .collect(Collectors.toList()); + if (matchingDMNList.size() == 1) { + T located = matchingDMNList.get(0); // Check if the located DMN Model in the NS, correspond for the import `drools:modelName`. - if (iModelName == null || idExtractor.apply(located).getLocalPart().equals(iModelName)) { + if (importModelName == null || idExtractor.apply(located).getLocalPart().equals(importModelName)) { + LOGGER.debug("DMN Model with name={} and namespace={} successfully imported a DMN " + + "with namespace={} name={} locationURI={}, modelName={}", + importerDMNNamespace, importerDMNName, importNamespace, importName, importLocationURI, importModelName); return Either.ofRight(located); } else { - return Either.ofLeft(String.format("While importing DMN for namespace: %s, name: %s, modelName: %s, located within namespace only %s but does not match for the actual name", - iNamespace, iName, iModelName, - idExtractor.apply(located))); + LOGGER.error("DMN Model with name={} and namespace={} can't import a DMN with namespace={}, name={}, modelName={}, " + + "located within namespace only {} but does not match for the actual modelName", + importerDMNNamespace, importerDMNName, importNamespace, importName, importModelName, idExtractor.apply(located)); + return Either.ofLeft(String.format( + "DMN Model with name=%s and namespace=%s can't import a DMN with namespace=%s, name=%s, modelName=%s, " + + "located within namespace only %s but does not match for the actual modelName", + importerDMNNamespace, importerDMNName, importNamespace, importName, importModelName, idExtractor.apply(located))); } } else { - List usingNSandName = allInNS.stream() - .filter(m -> idExtractor.apply(m).getLocalPart().equals(iModelName)) - .collect(Collectors.toList()); + List usingNSandName = matchingDMNList.stream() + .filter(dmn -> idExtractor.apply(dmn).getLocalPart().equals(importModelName)) + .toList(); if (usingNSandName.size() == 1) { + LOGGER.debug("DMN Model with name={} and namespace={} successfully imported a DMN " + + "with namespace={} name={} locationURI={}, modelName={}", + importerDMNNamespace, importerDMNName, importNamespace, importName, importLocationURI, importModelName); return Either.ofRight(usingNSandName.get(0)); - } else if (usingNSandName.size() == 0) { - return Either.ofLeft(String.format("Could not locate required dependency while importing DMN for namespace: %s, name: %s, modelName: %s.", - iNamespace, iName, iModelName)); + } else if (usingNSandName.isEmpty()) { + LOGGER.error("DMN Model with name={} and namespace={} failed to import a DMN with namespace={} name={} locationURI={}, modelName={}.", + importerDMNNamespace, importerDMNName, importNamespace, importName, importLocationURI, importModelName); + return Either.ofLeft(String.format( + "DMN Model with name=%s and namespace=%s failed to import a DMN with namespace=%s name=%s locationURI=%s, modelName=%s. ", + importerDMNNamespace, importerDMNName, importNamespace, importName, importLocationURI, importModelName)); } else { - return Either.ofLeft(String.format("While importing DMN for namespace: %s, name: %s, modelName: %s, could not locate required dependency within: %s.", - iNamespace, iName, iModelName, - allInNS.stream().map(idExtractor).collect(Collectors.toList()))); + LOGGER.error("DMN Model with name={} and namespace={} detected a collision ({} elements) trying to import a DMN with namespace={} name={} locationURI={}, modelName={}", + importerDMNNamespace, importerDMNName, usingNSandName.size(), importNamespace, importName, importLocationURI, importModelName); + return Either.ofLeft(String.format( + "DMN Model with name=%s and namespace=%s detected a collision trying to import a DMN with %s namespace, " + + "%s name and modelName %s. There are %s DMN files with the same namespace in your project. " + + "Please change the DMN namespaces and make them unique to fix this issue.", + importerDMNNamespace, importerDMNName, importNamespace, importName, importModelName, usingNSandName.size())); } } } - public static enum ImportType { + public enum ImportType { UNKNOWN, DMN, PMML; } - public static ImportType whichImportType(Import _import) { - switch (_import.getImportType()) { + public static ImportType whichImportType(Import importElement) { + switch (importElement.getImportType()) { case org.kie.dmn.model.v1_1.KieDMNModelInstrumentedBase.URI_DMN: case "http://www.omg.org/spec/DMN1-2Alpha/20160929/MODEL": case org.kie.dmn.model.v1_2.KieDMNModelInstrumentedBase.URI_DMN: diff --git a/kie-dmn/kie-dmn-core/src/test/java/org/kie/dmn/core/compiler/ImportDMNResolverUtilTest.java b/kie-dmn/kie-dmn-core/src/test/java/org/kie/dmn/core/compiler/ImportDMNResolverUtilTest.java index 87156dc70c5..6368dee3cc9 100644 --- a/kie-dmn/kie-dmn-core/src/test/java/org/kie/dmn/core/compiler/ImportDMNResolverUtilTest.java +++ b/kie-dmn/kie-dmn-core/src/test/java/org/kie/dmn/core/compiler/ImportDMNResolverUtilTest.java @@ -28,10 +28,12 @@ import org.junit.jupiter.api.Test; import org.kie.dmn.feel.util.Either; +import org.kie.dmn.model.api.Definitions; import org.kie.dmn.model.api.Import; import org.kie.dmn.model.v1_1.TImport; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; class ImportDMNResolverUtilTest { @@ -174,6 +176,10 @@ private Import makeImport(final String namespace, final String name, final Strin addAttributes.put(TImport.MODELNAME_QNAME, modelName); } i.setAdditionalAttributes(addAttributes); + final Definitions definitions = mock(Definitions.class); + definitions.setNamespace("ParentDMNNamespace"); + definitions.setName("ParentDMN"); + i.setParent(definitions); return i; } From 7bf4aab148e4fb3255b0bfe551437fc083c5e8bc Mon Sep 17 00:00:00 2001 From: Gabriele Cardosi Date: Fri, 12 Jul 2024 10:09:26 +0200 Subject: [PATCH 4/8] [incubator-kie-issues#1382] Fix identifier retrieval for quoted strings (#6021) Co-authored-by: Gabriele-Cardosi --- .../java/org/drools/mvel/MVELConstraint.java | 2 +- .../java/org/drools/util/StringUtils.java | 22 +++- .../java/org/drools/util/StringUtilsTest.java | 105 ++++++++++++++++++ 3 files changed, 126 insertions(+), 3 deletions(-) diff --git a/drools-mvel/src/main/java/org/drools/mvel/MVELConstraint.java b/drools-mvel/src/main/java/org/drools/mvel/MVELConstraint.java index 515d3f13021..932c3bcde9d 100644 --- a/drools-mvel/src/main/java/org/drools/mvel/MVELConstraint.java +++ b/drools-mvel/src/main/java/org/drools/mvel/MVELConstraint.java @@ -553,7 +553,7 @@ private List getPropertyNamesFromSimpleExpression(String expression) { private int nextPropertyName(String expression, List names, int cursor) { StringBuilder propertyNameBuilder = new StringBuilder(); cursor = extractFirstIdentifier(expression, propertyNameBuilder, cursor); - if (propertyNameBuilder.length() == 0) { + if (propertyNameBuilder.isEmpty()) { return cursor; } diff --git a/drools-util/src/main/java/org/drools/util/StringUtils.java b/drools-util/src/main/java/org/drools/util/StringUtils.java index f94ce49b83a..e73555c7bda 100644 --- a/drools-util/src/main/java/org/drools/util/StringUtils.java +++ b/drools-util/src/main/java/org/drools/util/StringUtils.java @@ -964,8 +964,23 @@ public static String extractFirstIdentifier(String string, int start) { return builder.toString(); } + /** + * Method that tries to extract identifiers from a Srting. + * First, it tries to identify "quoted" part, that should be ignored. + * Then, it tries to extract a String that is valid as java identifier. + * If an identifier is found, returns the last index of the identifier itself, otherwise the length of the string itself + * + * {@link Character#isJavaIdentifierStart} + * {@link Character#isJavaIdentifierPart} + * @param string + * @param builder + * @param start + * @return + */ public static int extractFirstIdentifier(String string, StringBuilder builder, int start) { boolean isQuoted = false; + boolean isDoubleQuoted = false; + boolean isSingleQuoted = false; boolean started = false; int i = start; for (; i < string.length(); i++) { @@ -973,13 +988,16 @@ public static int extractFirstIdentifier(String string, StringBuilder builder, i if (!isQuoted && Character.isJavaIdentifierStart(ch)) { builder.append(ch); started = true; - } else if (ch == '"' || ch == '\'') { - isQuoted = !isQuoted; + } else if (ch == '"') { + isDoubleQuoted = !isQuoted && !isDoubleQuoted; + } else if (ch == '\'') { + isSingleQuoted = !isQuoted && !isSingleQuoted; } else if (started && Character.isJavaIdentifierPart(ch)) { builder.append(ch); } else if (started) { break; } + isQuoted = isDoubleQuoted || isSingleQuoted; } return i; } diff --git a/drools-util/src/test/java/org/drools/util/StringUtilsTest.java b/drools-util/src/test/java/org/drools/util/StringUtilsTest.java index 2f41589a179..7fca6c14fe5 100644 --- a/drools-util/src/test/java/org/drools/util/StringUtilsTest.java +++ b/drools-util/src/test/java/org/drools/util/StringUtilsTest.java @@ -239,6 +239,111 @@ public void getPkgUUIDFromGAV() { assertThat(retrieved).isEqualTo(expected); } + @Test + public void testExtractFirstIdentifierWithStringBuilder() { + // Not-quoted string, interpreted as identifier + String string = "IDENTIFIER"; + String expected = string; + StringBuilder builder = new StringBuilder(); + int start = 0; + int retrieved = StringUtils.extractFirstIdentifier(string, builder, start); + assertThat(retrieved).isEqualTo(string.length()); // retrieved size is equals to the length of given string + assertThat(builder.toString()).isEqualTo(expected); + + // Quoted string, not interpreted as identifier + string = "\"IDENTIFIER\""; + expected = ""; + builder = new StringBuilder(); + retrieved = StringUtils.extractFirstIdentifier(string, builder, start); + assertThat(retrieved).isEqualTo(string.length()); + assertThat(builder.toString()).isEqualTo(expected); + + // Only the not-quoted string, and its size, is returned + string = "IDENTIFIER \""; + expected = "IDENTIFIER"; + builder = new StringBuilder(); + retrieved = StringUtils.extractFirstIdentifier(string, builder, start); + assertThat(retrieved).isEqualTo(expected.length()); // it returns the index where the identifier ends + assertThat(builder.toString()).isEqualTo(expected); + + string = "IDENTIFIER \"the_identifier"; + expected = "IDENTIFIER"; + builder = new StringBuilder(); + retrieved = StringUtils.extractFirstIdentifier(string, builder, start); + assertThat(retrieved).isEqualTo(expected.length()); // it returns the index where the identifier ends + assertThat(builder.toString()).isEqualTo(expected); + + string = "\"the_identifier\" IDENTIFIER"; + expected = "IDENTIFIER"; + builder = new StringBuilder(); + retrieved = StringUtils.extractFirstIdentifier(string, builder, start); + assertThat(retrieved).isEqualTo(string.length()); // it returns the index where the identifier ends + assertThat(builder.toString()).isEqualTo(expected); + + // Quoted string, not interpreted as identifier, starting at arbitrary position + string = "THIS IS BEFORE \"IDENTIFIER\""; + expected = ""; + builder = new StringBuilder(); + start = 14; + retrieved = StringUtils.extractFirstIdentifier(string, builder, start); + assertThat(retrieved).isEqualTo(string.length()); + assertThat(builder.toString()).isEqualTo(expected); + + // Only the not-quoted string, and its size, is returned, starting at arbitrary position + string = "THIS IS BEFORE IDENTIFIER \""; + expected = "IDENTIFIER"; + builder = new StringBuilder(); + retrieved = StringUtils.extractFirstIdentifier(string, builder, start); + assertThat(retrieved).isEqualTo(25); // it returns the index where the identifier ends + assertThat(builder.toString()).isEqualTo(expected); + + string = "IDENTIFIER \"the_identifier"; + expected = ""; + builder = new StringBuilder(); + start = 10; + retrieved = StringUtils.extractFirstIdentifier(string, builder, start); + assertThat(retrieved).isEqualTo(string.length()); // it returns the index where the identifier ends + assertThat(builder.toString()).isEqualTo(expected); + + string = "IDENTIFIER \"the_identifier"; + expected = ""; + builder = new StringBuilder(); + start = 10; + retrieved = StringUtils.extractFirstIdentifier(string, builder, start); + assertThat(retrieved).isEqualTo(string.length()); // it returns the index where the identifier ends + assertThat(builder.toString()).isEqualTo(expected); + + string = "\"not an ' identifier\""; + expected = ""; + builder = new StringBuilder(); + start = 0; + retrieved = StringUtils.extractFirstIdentifier(string, builder, start); + assertThat(retrieved).isEqualTo(string.length()); // it returns the whole string length + assertThat(builder.toString()).isEqualTo(expected); + + string = "'not an \" identifier'"; + expected = ""; + builder = new StringBuilder(); + retrieved = StringUtils.extractFirstIdentifier(string, builder, start); + assertThat(retrieved).isEqualTo(string.length()); // it returns the whole string length + assertThat(builder.toString()).isEqualTo(expected); + + string = "'not an \" identifier\"'"; + expected = ""; + builder = new StringBuilder(); + retrieved = StringUtils.extractFirstIdentifier(string, builder, start); + assertThat(retrieved).isEqualTo(string.length()); // it returns the whole string length + assertThat(builder.toString()).isEqualTo(expected); + + string = "\"an \" IDENTIFIER"; + expected = "IDENTIFIER"; + builder = new StringBuilder(); + retrieved = StringUtils.extractFirstIdentifier(string, builder, start); + assertThat(retrieved).isEqualTo(string.length()); // it returns the index where the identifier ends + assertThat(builder.toString()).isEqualTo(expected); + + } + @Test public void testSplitStatements() { String text = From d1064421b5dc7bc145673e3a14f1d8ba25aededf Mon Sep 17 00:00:00 2001 From: Yeser Amer Date: Fri, 12 Jul 2024 14:11:47 +0200 Subject: [PATCH 5/8] Bumps org.xmlunit:xmlunit-core from 2.8.2 to 2.10.0. (#6018) --- build-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-parent/pom.xml b/build-parent/pom.xml index adc4fa06550..d4f77ee8527 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -138,7 +138,7 @@ 2.2.19 2.1.19 1.0.55 - 2.9.1 + 2.10.0 2.2.0 1.5.0 From 99cfd1665c866d7c78dc223ea9f335379fb12181 Mon Sep 17 00:00:00 2001 From: Gabriele Cardosi Date: Wed, 17 Jul 2024 09:51:22 +0200 Subject: [PATCH 6/8] [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 --- .../feel/runtime/functions/AllFunction.java | 1 - .../runtime/functions/BaseFEELFunction.java | 389 ++++------- .../functions/BaseFEELFunctionHelper.java | 318 +++++++++ .../feel/runtime/functions/ScoreHelper.java | 237 +++++++ .../org/kie/dmn/feel/util/CoerceUtil.java | 18 + .../functions/BaseFEELFunctionHelperTest.java | 378 +++++++++++ .../functions/BaseFEELFunctionTest.java | 328 ++++++++++ .../runtime/functions/ScorerHelperTest.java | 610 ++++++++++++++++++ .../org/kie/dmn/feel/util/CoerceUtilTest.java | 41 ++ .../src/test/resources/logback.xml | 1 + .../feel/runtime/functions/AvgFunction.java | 2 +- .../runtime/functions/ConcatFunction.java | 7 +- 12 files changed, 2056 insertions(+), 274 deletions(-) create mode 100644 kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/BaseFEELFunctionHelper.java create mode 100644 kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/ScoreHelper.java create mode 100644 kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/BaseFEELFunctionHelperTest.java create mode 100644 kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/BaseFEELFunctionTest.java create mode 100644 kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/ScorerHelperTest.java 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 fb6bc4cdda4..1ae19f9cebf 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 FEELFnResult invoke(@ParameterName( "b" ) Object[] list) { // 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 7dcb0a5b26a..33922d34504 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.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 String getName() { public void setName(String name) { this.name = name; - ((FunctionSymbol) this.symbol).setId( name ); + ((FunctionSymbol) this.symbol).setId(name); } @Override @@ -82,59 +75,71 @@ public Object invokeReflectively(EvaluationContext ctx, Object[] params) { // use reflection to call the appropriate invoke method try { boolean isNamedParams = params.length > 0 && params[0] instanceof NamedParameter; - if ( !isCustomFunction() ) { - List 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 either = (Either) 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 either = (Either) 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> 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 Object invokeReflectively(EvaluationContext ctx, Object[] params) { * @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 source, Supplier> parameterNamesSupplier, - Supplier> 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 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 true if the parameter refers to value to be retrieved inside + * ctx; false 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 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 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)[] 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> 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 left CandidateMethod if its fineScore is greater than the right one, + * otherwise returns the right CandidateMethod + * @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 available) { - Annotation[][] pas = m.getParameterAnnotations(); - List 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 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 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 source, + Supplier> parameterNamesSupplier, + Supplier> 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 00000000000..327f3fb98cb --- /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 CandidateMethod, + * could match the given parameterTypes, eventually coerced. + * In case of match with coercion, a new Object[] with coerced values is returned. + * @param parameterTypes + * @param actualParams + * @return an Object[], with values eventually coerced, or null 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 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 context reference inside the given parameters, if the given + * Method signature include it. + * Depending on the isNamedParams, the reference could be the given EvaluationContext + * itself, or a NamedParameter + * @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 NamedParameter[] + * It returns null if the actual parameters does not match with the Method ones + * @param m + * @param params + * @return an Object[] with mapped values, or null if the mapping has not been possible + * for all params + */ + static Object[] calculateActualParams(Method m, NamedParameter[] params) { + logger.trace("calculateActualParams {} {}", m, params); + List 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 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 actualParams or variableParams with values extracted + * from NamedParameter + * @param np + * @param names + * @param actualParams + * @param isVariableParameters + * @param variableParamPrefix + * @param variableParams + * @return true if a mapping has been found, false otherwise + */ + static boolean calculateActualParam(NamedParameter np, List names, Object[] actualParams, + boolean isVariableParameters, String variableParamPrefix, + List 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 variableParams with values extracted from NamedParameter + * @param np + * @param variableParamPrefix + * @param variableParams + * @return true if a mapping has been found, false otherwise + */ + static boolean calculateActualParamVariableParameters(NamedParameter np, String variableParamPrefix, + List 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 Method, + * from the ones annotated with ParameterName + * @param m + * @return + */ + static List getParametersNames(Method m) { + logger.trace("getParametersNames {}", m); + Annotation[][] pas = m.getParameterAnnotations(); + List 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 CustomFunction. + * It refactors the input parameters to match the order defined in the CustomFunction, + * returning the actual value of the given params + * @param params + * @param pnames the parameters defined in the CustomFunction + * @return + */ + static Object[] rearrangeParameters(NamedParameter[] params, List 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 NumberEvalHelper.coerceNumber 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 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 00000000000..1b8b0ac566d --- /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 Method score based on the given inputs. + * It compares the original input with the "adapted" one to match a given Method. + * For each condition, a value is provided. The top score is obtained when all the conditions are met. + *

+ * 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> GROSS_SCORER_LIST; + private static final List> FINE_SCORER_LIST; + + static int lastInputNotArrayNotArrayScore = 100000; + static int lastParameterNotArrayScore = 10000; + static int numberOfParametersScore = 1000; + static int coercedToVarargsScore = -10; + + static final ToIntFunction numberOfParameters = compares -> { + int toReturn = compares.originalInput.length == compares.parameterTypes.length ? numberOfParametersScore : 0; + logger.trace("numberOfParameters {} -> {}", compares, toReturn); + return toReturn; + }; + + static final ToIntFunction 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 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 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 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 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 421f7282491..fa8779609f7 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 @@ 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 static Optional coerceParams(Class currentIdxActualParameter static Optional 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, 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 @@ static Optional coerceParam(Class currentIdxActualParameterType, Clas } } } + /* to singleton list: + When the type of the expression is T and the target type is List 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 00000000000..39106d29d10 --- /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 names = Collections.singletonList("n"); + Object[] actualParams = new Object[1]; + boolean isVariableParameters = false; + String variableParamPrefix = null; + List 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 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 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 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 originalResult = List.of(3, "4", 56); + Object result = originalResult.toArray(); + Object retrieved = BaseFEELFunctionHelper.normalizeResult(result); + assertNotNull(retrieved); + assertInstanceOf(List.class, retrieved); + List retrievedList = (List) 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 00000000000..dde34f0d62b --- /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 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("", + 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 00000000000..396e0a03560 --- /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 81127b4230e..31cef2149d5 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.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 @@ 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 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 3d747c98aa0..752bdbd6df8 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 @@ + 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 3504e553390..e32471e43f7 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 FEELFnResult invoke(@ParameterName( "list" ) List list) { } public FEELFnResult invoke(@ParameterName("list") Number single) { - return BuiltInFunctions.getFunction(MeanFunction.class).invoke(single); + return BuiltInFunctions.getFunction(MeanFunction.class).invoke(List.of(single)); } public FEELFnResult 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 602c2b12ccd..e0bc747cbfa 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 @@ 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 FEELFnResult invoke(@ParameterName("values") List list) { 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) { From c9ec2eb659c957427b826d18daa03cc24e2afbbb Mon Sep 17 00:00:00 2001 From: Yeser Amer Date: Wed, 17 Jul 2024 10:50:12 +0200 Subject: [PATCH 7/8] [incubator-kie-issues#1150] Improve Import Resolver error messages to be more user friendly - Part II (#6025) --- .../core/compiler/ImportDMNResolverUtil.java | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/compiler/ImportDMNResolverUtil.java b/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/compiler/ImportDMNResolverUtil.java index be11db65f25..91b86b40ac4 100644 --- a/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/compiler/ImportDMNResolverUtil.java +++ b/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/compiler/ImportDMNResolverUtil.java @@ -21,7 +21,6 @@ import java.util.Collection; import java.util.List; import java.util.function.Function; -import java.util.stream.Collectors; import javax.xml.namespace.QName; @@ -51,27 +50,27 @@ public static Either resolveImportDMN(Import importElement, Colle LOGGER.debug("Resolving an Import in DMN Model with name={} and namespace={}. " + "Importing a DMN model with namespace={} name={} locationURI={}, modelName={}", - importerDMNNamespace, importerDMNName, importNamespace, importName, importLocationURI, importModelName); + importerDMNName, importerDMNNamespace, importNamespace, importName, importLocationURI, importModelName); List matchingDMNList = dmns.stream() .filter(m -> idExtractor.apply(m).getNamespaceURI().equals(importNamespace)) - .collect(Collectors.toList()); + .toList(); if (matchingDMNList.size() == 1) { T located = matchingDMNList.get(0); // Check if the located DMN Model in the NS, correspond for the import `drools:modelName`. if (importModelName == null || idExtractor.apply(located).getLocalPart().equals(importModelName)) { LOGGER.debug("DMN Model with name={} and namespace={} successfully imported a DMN " + "with namespace={} name={} locationURI={}, modelName={}", - importerDMNNamespace, importerDMNName, importNamespace, importName, importLocationURI, importModelName); + importerDMNName, importerDMNNamespace, importNamespace, importName, importLocationURI, importModelName); return Either.ofRight(located); } else { LOGGER.error("DMN Model with name={} and namespace={} can't import a DMN with namespace={}, name={}, modelName={}, " + "located within namespace only {} but does not match for the actual modelName", - importerDMNNamespace, importerDMNName, importNamespace, importName, importModelName, idExtractor.apply(located)); + importerDMNName, importerDMNNamespace, importNamespace, importName, importModelName, idExtractor.apply(located)); return Either.ofLeft(String.format( "DMN Model with name=%s and namespace=%s can't import a DMN with namespace=%s, name=%s, modelName=%s, " + "located within namespace only %s but does not match for the actual modelName", - importerDMNNamespace, importerDMNName, importNamespace, importName, importModelName, idExtractor.apply(located))); + importerDMNName, importerDMNNamespace, importNamespace, importName, importModelName, idExtractor.apply(located))); } } else { List usingNSandName = matchingDMNList.stream() @@ -80,22 +79,22 @@ public static Either resolveImportDMN(Import importElement, Colle if (usingNSandName.size() == 1) { LOGGER.debug("DMN Model with name={} and namespace={} successfully imported a DMN " + "with namespace={} name={} locationURI={}, modelName={}", - importerDMNNamespace, importerDMNName, importNamespace, importName, importLocationURI, importModelName); + importerDMNName, importerDMNNamespace, importNamespace, importName, importLocationURI, importModelName); return Either.ofRight(usingNSandName.get(0)); } else if (usingNSandName.isEmpty()) { LOGGER.error("DMN Model with name={} and namespace={} failed to import a DMN with namespace={} name={} locationURI={}, modelName={}.", - importerDMNNamespace, importerDMNName, importNamespace, importName, importLocationURI, importModelName); + importerDMNName, importerDMNNamespace, importNamespace, importName, importLocationURI, importModelName); return Either.ofLeft(String.format( "DMN Model with name=%s and namespace=%s failed to import a DMN with namespace=%s name=%s locationURI=%s, modelName=%s. ", - importerDMNNamespace, importerDMNName, importNamespace, importName, importLocationURI, importModelName)); + importerDMNName, importerDMNNamespace, importNamespace, importName, importLocationURI, importModelName)); } else { LOGGER.error("DMN Model with name={} and namespace={} detected a collision ({} elements) trying to import a DMN with namespace={} name={} locationURI={}, modelName={}", - importerDMNNamespace, importerDMNName, usingNSandName.size(), importNamespace, importName, importLocationURI, importModelName); + importerDMNName, importerDMNNamespace, usingNSandName.size(), importNamespace, importName, importLocationURI, importModelName); return Either.ofLeft(String.format( "DMN Model with name=%s and namespace=%s detected a collision trying to import a DMN with %s namespace, " + "%s name and modelName %s. There are %s DMN files with the same namespace in your project. " + "Please change the DMN namespaces and make them unique to fix this issue.", - importerDMNNamespace, importerDMNName, importNamespace, importName, importModelName, usingNSandName.size())); + importerDMNName, importerDMNNamespace, importNamespace, importName, importModelName, usingNSandName.size())); } } } From 2a41c619977f1d3682c846a16fde9fcd1df6433d Mon Sep 17 00:00:00 2001 From: Yeser Amer Date: Wed, 17 Jul 2024 14:19:53 +0200 Subject: [PATCH 8/8] [kie-issues#1330] FEEL functions that expect List parameters, should coerce single item to a List (#5997) * Fix + Tests * Fix + Tests * Fix + Tests * Tests fixed * Fix + Tests * Fix + Tests * Fix * Revert * Fix * Fix * Improving scoring * BaseFEELFunction updated * BaseFEELFunction updated * Revert context function * context function * Revert some changes + Tests * Change --- .../feel/runtime/functions/AllFunction.java | 4 -- .../feel/runtime/functions/AnyFunction.java | 4 -- .../feel/runtime/functions/MeanFunction.java | 21 +------ .../runtime/functions/ProductFunction.java | 17 ------ .../runtime/functions/StddevFunction.java | 12 ---- .../feel/runtime/functions/SumFunction.java | 17 ------ .../runtime/FEEL12ExtendedFunctionsTest.java | 5 +- .../dmn/feel/runtime/FEELFunctionsTest.java | 31 ++++++++++ .../runtime/KieFEELExtendedFunctionsTest.java | 15 +++-- .../runtime/functions/AllFunctionTest.java | 15 ----- .../runtime/functions/AnyFunctionTest.java | 15 ----- .../runtime/functions/MeanFunctionTest.java | 29 +++------- .../functions/StringJoinFunctionTest.java | 56 +++++++++++++++++++ .../runtime/functions/SumFunctionTest.java | 20 +------ 14 files changed, 111 insertions(+), 150 deletions(-) create mode 100644 kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/StringJoinFunctionTest.java 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 1ae19f9cebf..fc640d5e5a0 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 @@ -58,10 +58,6 @@ public FEELFnResult invoke(@ParameterName( "list" ) List list) { } } - public FEELFnResult invoke(@ParameterName( "list" ) Boolean single) { - return FEELFnResult.ofResult( single ); - } - public FEELFnResult invoke(@ParameterName( "b" ) Object[] list) { if ( list == null ) { // Arrays.asList does not accept null as parameter diff --git a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/AnyFunction.java b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/AnyFunction.java index 17e8d330634..9243758d730 100644 --- a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/AnyFunction.java +++ b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/AnyFunction.java @@ -58,10 +58,6 @@ public FEELFnResult invoke(@ParameterName( "list" ) List list) { } } - public FEELFnResult invoke(@ParameterName( "list" ) Boolean single) { - return FEELFnResult.ofResult( single ); - } - public FEELFnResult invoke(@ParameterName( "b" ) Object[] list) { if ( list == null ) { // Arrays.asList does not accept null as parameter diff --git a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/MeanFunction.java b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/MeanFunction.java index 0d940380ba7..cdb78dd9246 100644 --- a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/MeanFunction.java +++ b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/MeanFunction.java @@ -47,9 +47,8 @@ public FEELFnResult invoke(@ParameterName( "list" ) List list) { FEELFnResult s = sum.invoke( list ); - Function> ifLeft = (event) -> { - return FEELFnResult.ofError(new InvalidParametersEvent(Severity.ERROR, "list", "unable to sum the elements which is required to calculate the mean")); - }; + Function> ifLeft = event -> + FEELFnResult.ofError(new InvalidParametersEvent(Severity.ERROR, "list", "unable to sum the elements which is required to calculate the mean")); Function> ifRight = (sum) -> { try { @@ -62,22 +61,6 @@ public FEELFnResult invoke(@ParameterName( "list" ) List list) { return s.cata(ifLeft, ifRight); } - public FEELFnResult invoke(@ParameterName( "list" ) Number single) { - if ( single == null ) { - return FEELFnResult.ofError(new InvalidParametersEvent(Severity.ERROR, "single", "the single value list cannot be null")); - } - - if( single instanceof BigDecimal ) { - return FEELFnResult.ofResult((BigDecimal) single ); - } - BigDecimal result = NumberEvalHelper.getBigDecimalOrNull(single ); - if ( result != null ) { - return FEELFnResult.ofResult( result ); - } else { - return FEELFnResult.ofError(new InvalidParametersEvent(Severity.ERROR, "list", "single element in list is not a number")); - } - } - public FEELFnResult invoke(@ParameterName( "n" ) Object[] list) { if ( list == null ) { // Arrays.asList does not accept null as parameter diff --git a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/ProductFunction.java b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/ProductFunction.java index 11462667c87..42922bb82f6 100644 --- a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/ProductFunction.java +++ b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/ProductFunction.java @@ -54,23 +54,6 @@ public FEELFnResult invoke(@ParameterName("list") List list) { return FEELFnResult.ofResult( product ); } - public FEELFnResult invoke(@ParameterName("list") Number single) { - if ( single == null ) { - // Arrays.asList does not accept null as parameter - return FEELFnResult.ofError(new InvalidParametersEvent(Severity.ERROR, "list", "the single value list cannot be null")); - } - - if( single instanceof BigDecimal ) { - return FEELFnResult.ofResult((BigDecimal) single ); - } - BigDecimal result = NumberEvalHelper.getBigDecimalOrNull( single ); - if ( result != null ) { - return FEELFnResult.ofResult( result ); - } else { - return FEELFnResult.ofError(new InvalidParametersEvent(Severity.ERROR, "list", "single element in list not a number")); - } - } - public FEELFnResult invoke(@ParameterName("n") Object[] list) { if ( list == null ) { // Arrays.asList does not accept null as parameter diff --git a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/StddevFunction.java b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/StddevFunction.java index de7451bb13f..8fae1477ceb 100644 --- a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/StddevFunction.java +++ b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/StddevFunction.java @@ -69,18 +69,6 @@ public FEELFnResult invoke(@ParameterName("list") List list) { return FEELFnResult.ofResult( SqrtFunction.sqrt( mean ) ); } - public FEELFnResult invoke(@ParameterName("list") Object sole) { - if ( sole == null ) { - // Arrays.asList does not accept null as parameter - return FEELFnResult.ofError(new InvalidParametersEvent(FEELEvent.Severity.ERROR, "list", "the single value list cannot be null")); - } else if (NumberEvalHelper.getBigDecimalOrNull(sole) == null) { - return FEELFnResult.ofError(new InvalidParametersEvent(FEELEvent.Severity.ERROR, "list", - "the value can not be converted to a number")); - } - return FEELFnResult.ofError(new InvalidParametersEvent(FEELEvent.Severity.ERROR, "list", - "sample standard deviation of a single sample is undefined")); - } - public FEELFnResult invoke(@ParameterName("n") Object[] list) { if ( list == null ) { // Arrays.asList does not accept null as parameter diff --git a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/SumFunction.java b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/SumFunction.java index 86d3a0310c2..8cb0086c21d 100644 --- a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/SumFunction.java +++ b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/SumFunction.java @@ -60,23 +60,6 @@ public FEELFnResult invoke(@ParameterName("list") List list) { return FEELFnResult.ofResult( sum ); } - public FEELFnResult invoke(@ParameterName("list") Number single) { - if ( single == null ) { - // Arrays.asList does not accept null as parameter - return FEELFnResult.ofError(new InvalidParametersEvent(Severity.ERROR, "list", "the single value list cannot be null")); - } - - if( single instanceof BigDecimal ) { - return FEELFnResult.ofResult((BigDecimal) single ); - } - BigDecimal result = NumberEvalHelper.getBigDecimalOrNull( single ); - if ( result != null ) { - return FEELFnResult.ofResult( result ); - } else { - return FEELFnResult.ofError(new InvalidParametersEvent(Severity.ERROR, "list", "single element in list not a number")); - } - } - public FEELFnResult invoke(@ParameterName("n") Object[] list) { if ( list == null ) { // Arrays.asList does not accept null as parameter diff --git a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/FEEL12ExtendedFunctionsTest.java b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/FEEL12ExtendedFunctionsTest.java index 6eb25154891..7f2c9b67f16 100644 --- a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/FEEL12ExtendedFunctionsTest.java +++ b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/FEEL12ExtendedFunctionsTest.java @@ -59,9 +59,10 @@ private static Collection data() { { "stddev( [ 47 ] )", null, FEELEvent.Severity.ERROR }, { "stddev( 47 )", null, FEELEvent.Severity.ERROR }, { "stddev( [ ] )", null, FEELEvent.Severity.ERROR }, - {"mode( 6, 3, 9, 6, 6 )", List.of(BigDecimal.valueOf(6)), null }, + { "mode( 6 )", List.of(BigDecimal.valueOf(6)), null }, + { "mode( 6, 3, 9, 6, 6 )", List.of(BigDecimal.valueOf(6)), null }, { "mode( [6, 1, 9, 6, 1] )", Arrays.asList(BigDecimal.valueOf( 1 ), BigDecimal.valueOf( 6 ) ), null }, - {"mode( [ ] )", List.of(), null }, + { "mode( [ ] )", List.of(), null }, }; return addAdditionalParameters(cases, false); } diff --git a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/FEELFunctionsTest.java b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/FEELFunctionsTest.java index 4ec562e84f9..3707929e3bd 100644 --- a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/FEELFunctionsTest.java +++ b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/FEELFunctionsTest.java @@ -91,21 +91,37 @@ private static Collection data() { { "replace(\"0123456789\",\"(\\d{3})(\\d{3})(\\d{4})\",\"($1) $2-$3\")", "(012) 345-6789" , null}, { "list contains([1, 2, 3], 2)", Boolean.TRUE , null}, { "list contains([1, 2, 3], 5)", Boolean.FALSE , null}, + { "list contains(1, 1)", Boolean.TRUE , null}, + { "list contains(2, 1)", Boolean.FALSE , null}, + { "count( 1 )", BigDecimal.valueOf( 1 ) , null}, { "count([1, 2, 3])", BigDecimal.valueOf( 3 ) , null}, { "count( 1, 2, 3 )", BigDecimal.valueOf( 3 ) , null}, + { "min( \"a\" )", "a" , null}, { "min( \"a\", \"b\", \"c\" )", "a" , null}, { "min([ \"a\", \"b\", \"c\" ])", "a" , null}, + { "max( 1 )", BigDecimal.valueOf( 1 ) , null}, { "max( 1, 2, 3 )", BigDecimal.valueOf( 3 ) , null}, { "max([ 1, 2, 3 ])", BigDecimal.valueOf( 3 ) , null}, { "max(duration(\"PT1H6M\"), duration(\"PT1H5M\"))", Duration.parse("PT1H6M"), null}, { "max(duration(\"P6Y\"), duration(\"P5Y\"))", ComparablePeriod.parse("P6Y"), null}, + { "sum( 1 )", BigDecimal.valueOf( 1 ) , null}, + { "sum( [null] )", null , FEELEvent.Severity.ERROR}, + { "sum( null )", null , FEELEvent.Severity.ERROR}, { "sum( 1, 2, 3 )", BigDecimal.valueOf( 6 ) , null}, { "sum([ 1, 2, 3 ])", BigDecimal.valueOf( 6 ) , null}, { "sum([])", null, null}, + { "product( [2, 3, 4] )", BigDecimal.valueOf( 24 ) , null}, { "product( 2, 3, 4 )", BigDecimal.valueOf( 24 ) , null}, + { "product( 1 )", BigDecimal.valueOf( 1 ) , null}, + { "product( [null] )", null , FEELEvent.Severity.ERROR}, + { "product( null )", null , FEELEvent.Severity.ERROR}, { "product([ 2, 3, 4 ])", BigDecimal.valueOf( 24 ) , null}, { "product([])", null, FEELEvent.Severity.ERROR}, + { "mean( [1, 2, 3] )", BigDecimal.valueOf( 2 ) , null}, { "mean( 1, 2, 3 )", BigDecimal.valueOf( 2 ) , null}, + { "mean( 2 )", BigDecimal.valueOf( 2 ) , null}, + { "mean( [null] )", null , FEELEvent.Severity.ERROR}, + { "mean( null )", null , FEELEvent.Severity.ERROR}, { "mean([ 1, 2, 3 ])", BigDecimal.valueOf( 2 ) , null}, { "sublist( [1, 2, 3, 4, 5 ], 3, 2 )", Arrays.asList( BigDecimal.valueOf( 3 ), BigDecimal.valueOf( 4 ) ), null}, { "sublist( [1, 2, 3, 4, 5 ], -2, 1 )", Collections.singletonList(BigDecimal.valueOf(4)), null}, @@ -114,16 +130,21 @@ private static Collection data() { { "sublist( [1, 2, 3, 4, 5 ], -6, 3 )", null , FEELEvent.Severity.ERROR}, { "sublist( [1, 2, 3, 4, 5 ], -5, 3 )", Arrays.asList( BigDecimal.valueOf( 1 ), BigDecimal.valueOf( 2 ), BigDecimal.valueOf( 3 ) ) , null}, { "sublist( [1, 2, 3, 4, 5 ], 1, 3 )", Arrays.asList( BigDecimal.valueOf( 1 ), BigDecimal.valueOf( 2 ), BigDecimal.valueOf( 3 ) ) , null}, + { "sublist( 1, 1, 1 )", Arrays.asList( BigDecimal.valueOf( 1 ) ) , null}, + { "sublist( 1, 1 )", Arrays.asList( BigDecimal.valueOf( 1 ) ) , null}, { "append( [1, 2], 3, 4 )", Arrays.asList( BigDecimal.valueOf( 1 ), BigDecimal.valueOf( 2 ), BigDecimal.valueOf( 3 ), BigDecimal.valueOf( 4 ) ) , null}, { "append( [], 3, 4 )", Arrays.asList( BigDecimal.valueOf( 3 ), BigDecimal.valueOf( 4 ) ) , null}, + { "append( 1, 3, 4 )", Arrays.asList( BigDecimal.valueOf( 1 ), BigDecimal.valueOf( 3 ), BigDecimal.valueOf( 4 ) ) , null}, { "append( [1, 2] )", Arrays.asList( BigDecimal.valueOf( 1 ), BigDecimal.valueOf( 2 ) ) , null}, { "append( [1, 2], null, 4 )", Arrays.asList( BigDecimal.valueOf( 1 ), BigDecimal.valueOf( 2 ), null, BigDecimal.valueOf( 4 ) ) , null}, { "append( null, 1, 2 )", null , FEELEvent.Severity.ERROR}, { "append( 0, 1, 2 )", Arrays.asList( BigDecimal.valueOf( 0 ), BigDecimal.valueOf( 1 ), BigDecimal.valueOf( 2 ) ), null}, { "concatenate( [1, 2], [3] )", Arrays.asList( BigDecimal.valueOf( 1 ), BigDecimal.valueOf( 2 ), BigDecimal.valueOf( 3 ) ) , null}, + { "concatenate( 1, [3] )", Arrays.asList( BigDecimal.valueOf( 1 ), BigDecimal.valueOf( 3 ) ) , null}, { "concatenate( [1, 2], 3, [4] )", Arrays.asList( BigDecimal.valueOf( 1 ), BigDecimal.valueOf( 2 ), BigDecimal.valueOf( 3 ), BigDecimal.valueOf( 4 ) ) , null}, { "concatenate( [1, 2], null )", null , FEELEvent.Severity.ERROR}, { "insert before( [1, 2, 3], 1, 4 )", Arrays.asList( BigDecimal.valueOf( 4 ), BigDecimal.valueOf( 1 ), BigDecimal.valueOf( 2 ), BigDecimal.valueOf( 3 ) ) , null}, + { "insert before( 1, 1, 4 )", Arrays.asList( BigDecimal.valueOf( 4 ), BigDecimal.valueOf( 1 ) ) , null}, { "insert before( [1, 2, 3], 3, 4 )", Arrays.asList( BigDecimal.valueOf( 1 ), BigDecimal.valueOf( 2 ), BigDecimal.valueOf( 4 ), BigDecimal.valueOf( 3 ) ) , null}, { "insert before( [1, 2, 3], 3, null )", Arrays.asList( BigDecimal.valueOf( 1 ), BigDecimal.valueOf( 2 ), null, BigDecimal.valueOf( 3 ) ) , null}, { "insert before( null, 3, 4 )", null , FEELEvent.Severity.ERROR}, @@ -133,6 +154,7 @@ private static Collection data() { { "insert before( [1, 2, 3], 0, 4 )", null , FEELEvent.Severity.ERROR}, { "insert before( [1, 2, 3], -4, 4 )", null , FEELEvent.Severity.ERROR}, { "remove( [1, 2, 3], 1 )", Arrays.asList( BigDecimal.valueOf( 2 ), BigDecimal.valueOf( 3 ) ) , null}, + { "remove( 1, 1 )", Arrays.asList() , null}, { "remove( [1, 2, 3], 3 )", Arrays.asList( BigDecimal.valueOf( 1 ), BigDecimal.valueOf( 2 ) ) , null}, { "remove( [1, 2, 3], -1 )", Arrays.asList( BigDecimal.valueOf( 1 ), BigDecimal.valueOf( 2 ) ) , null}, { "remove( [1, 2, 3], -3 )", Arrays.asList( BigDecimal.valueOf( 2 ), BigDecimal.valueOf( 3 ) ) , null}, @@ -140,14 +162,20 @@ private static Collection data() { { "remove( [1, 2, 3], -4 )", null , FEELEvent.Severity.ERROR}, { "remove( [1, 2, 3], 0 )", null , FEELEvent.Severity.ERROR}, { "reverse( [1, 2, 3] )", Arrays.asList( BigDecimal.valueOf( 3 ), BigDecimal.valueOf( 2 ), BigDecimal.valueOf( 1 ) ) , null}, + { "reverse( 1 )", Arrays.asList( BigDecimal.valueOf( 1 ) ) , null}, { "reverse( null )", null , FEELEvent.Severity.ERROR}, + { "index of( 1, 1 )", Arrays.asList( BigDecimal.valueOf( 1 ) ) , null}, { "index of( [1, 2, 3, 2], 2 )", Arrays.asList( BigDecimal.valueOf( 2 ), BigDecimal.valueOf( 4 ) ) , null}, { "index of( [1, 2, null, null], null )", Arrays.asList( BigDecimal.valueOf( 3 ), BigDecimal.valueOf( 4 ) ) , null}, { "index of( [1, 2, null, null], 1 )", Collections.singletonList(BigDecimal.valueOf(1)), null}, { "index of( null, 1 )", null , FEELEvent.Severity.ERROR}, + { "union( 1, [2, 3], 2, 4 )", Arrays.asList( BigDecimal.valueOf( 1 ), BigDecimal.valueOf( 2 ), BigDecimal.valueOf( 3 ), BigDecimal.valueOf( 4 ) ) , null}, + { "union( 1 )", Arrays.asList( BigDecimal.valueOf( 1 ) ) , null}, { "union( [1, 2, 1], [2, 3], 2, 4 )", Arrays.asList( BigDecimal.valueOf( 1 ), BigDecimal.valueOf( 2 ), BigDecimal.valueOf( 3 ), BigDecimal.valueOf( 4 ) ) , null}, { "union( [1, 2, null], 4 )", Arrays.asList( BigDecimal.valueOf( 1 ), BigDecimal.valueOf( 2 ), null, BigDecimal.valueOf( 4 ) ) , null}, { "union( null, 4 )", Arrays.asList( null, BigDecimal.valueOf(4) ), null}, + { "flatten( [[1,2],[[3]], 4] )", Arrays.asList( BigDecimal.valueOf( 1 ), BigDecimal.valueOf( 2 ), BigDecimal.valueOf( 3 ), BigDecimal.valueOf( 4 ) ), null}, + { "flatten( 1 )", Arrays.asList( BigDecimal.valueOf( 1 ) ), null}, { "distinct values( [1, 2, 3, 2, 4] )", Arrays.asList( BigDecimal.valueOf( 1 ), BigDecimal.valueOf( 2 ), BigDecimal.valueOf( 3 ), BigDecimal.valueOf( 4 ) ) , null}, { "distinct values( [1, 2, null, 2, 4] )", Arrays.asList( BigDecimal.valueOf( 1 ), BigDecimal.valueOf( 2 ), null, BigDecimal.valueOf( 4 ) ) , null}, { "distinct values( 1 )", Collections.singletonList(BigDecimal.valueOf(1)), null}, @@ -240,6 +268,7 @@ private static Collection data() { { "week of year( date(2005, 1, 1) )", BigDecimal.valueOf( 53 ), null}, { "median( 8, 2, 5, 3, 4 )", new BigDecimal("4") , null}, { "median( [6, 1, 2, 3] )", new BigDecimal("2.5") , null}, + { "median( 1 )", new BigDecimal("1") , null}, { "median( [ ] ) ", null, null}, // DMN spec, Table 69: Semantics of list functions { "0-max( 1, 2, 3 )", BigDecimal.valueOf( -3 ) , null}, @@ -264,6 +293,8 @@ private static Collection data() { { "if list contains ([2.2, 3, 4], 3.000) then \"OK\" else \"NOT_OK\"", "OK" , null}, {"list replace ( null, 3, 6)", null , FEELEvent.Severity.ERROR}, {"list replace ( [2, 4, 7, 8], null, 6)", null , FEELEvent.Severity.ERROR}, + {"list replace ( [1], 1, 7)", Arrays.asList(BigDecimal.valueOf(7)), null}, + {"list replace ( 1, 1, 7)", Arrays.asList(BigDecimal.valueOf(7)), null}, {"list replace ( [2, 4, 7, 8], 3, 6)", Arrays.asList(BigDecimal.valueOf(2), BigDecimal.valueOf(4), BigDecimal.valueOf(6), BigDecimal.valueOf(8)), null}, {"list replace ( [2, 4, 7, 8], -3, 6)", Arrays.asList(BigDecimal.valueOf(2), BigDecimal.valueOf(6), BigDecimal.valueOf(7), BigDecimal.valueOf(8)), null}, {"list replace ( [2, 4, 7, 8], function(item, newItem) item + newItem, 6)", null , FEELEvent.Severity.ERROR}, diff --git a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/KieFEELExtendedFunctionsTest.java b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/KieFEELExtendedFunctionsTest.java index 8e5b0ed40bd..e3d508d1652 100644 --- a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/KieFEELExtendedFunctionsTest.java +++ b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/KieFEELExtendedFunctionsTest.java @@ -50,10 +50,12 @@ private static Collection data() { { "string join([\"a\",\"b\",\"c\"], \"\")", "abc", null}, { "string join([\"a\",\"b\",\"c\"], null)", "abc", null}, { "string join([\"a\"], \"X\")", "a", null}, + { "string join(\"a\", \"X\")", "a", null}, { "string join([\"a\",null,\"c\"], \"X\")", "aXc", null}, { "string join([], \"X\")", "", null}, { "string join([\"a\",\"b\",\"c\"])", "abc", null}, { "string join([\"a\",null,\"c\"])", "ac", null}, + { "string join(\"a\")", "a", null}, { "string join([])", "", null}, { "string join([\"a\",123,\"c\"], null)", null, FEELEvent.Severity.ERROR}, { "string join(null, null)", null, FEELEvent.Severity.ERROR}, @@ -123,11 +125,16 @@ private static Collection data() { { "context put({name: \"John Doe\", address: { street: \"St.\", country:\"US\"}}, [\"address\", \"country\"], \"IT\")", mapOf(entry("name", "John Doe"),entry("address",mapOf(entry("street","St."), entry("country","IT")))), null }, { "context put({name: \"John Doe\", age: 0}, \"age\", 47)", mapOf(entry("name", "John Doe"),entry("age", new BigDecimal(47))), null }, { "context put({name: \"John Doe\", age: 0, z:999}, \"age\", 47)", mapOf(entry("name", "John Doe"),entry("age", new BigDecimal(47)),entry("z", new BigDecimal(999))), null }, - { "context merge([{name: \"John Doe\"}, {age: 47}])", mapOf(entry("name", "John Doe"),entry("age", new BigDecimal(47))), null }, + { "context merge([{name: \"John Doe\"}, {age: 47}])", mapOf(entry("name", "John Doe") ,entry("age", new BigDecimal(47))), null }, + { "context merge({name: \"John Doe\"})", mapOf(entry("name", "John Doe")), null }, { "context merge([{name: \"John Doe\", age: 0}, {age: 47}])", mapOf(entry("name", "John Doe"),entry("age", new BigDecimal(47))), null }, - { "context([{key: \"name\", value: \"John Doe\"},{\"key\":\"age\", \"value\":47}])", mapOf(entry("name", "John Doe"),entry("age", new BigDecimal(47))), null }, - { "context([{key: \"name\", value: \"John Doe\"},{\"key\":\"age\", \"value\":47, \"something\":\"else\"}])", mapOf(entry("name", "John Doe"),entry("age", new BigDecimal(47))), null }, - { "context([{key: \"name\", value: \"John Doe\"},{\"key\":\"age\"}])", null, FEELEvent.Severity.ERROR }, + { "context([{key: null, value: \"John Doe\"},{\"key\": \"age\", \"value\": 47}])", null, FEELEvent.Severity.ERROR }, + { "context([{key: \"name\", value: \"John Doe\"},{\"value\": 47}])", null, FEELEvent.Severity.ERROR }, + { "context([{key: \"name\", value: \"John Doe\"},{\"key\": \"age\"}])", null, FEELEvent.Severity.ERROR }, + { "context([{key: \"name\", value: \"John Doe\"},{\"key\": \"age\", \"value\": 47}])", mapOf(entry("name", "John Doe"), entry("age", new BigDecimal(47))), null }, + { "context([{key: \"name\", value: \"John Doe\"},{\"key\": \"age\", \"value\": 47, \"something\": \"else\"}])", mapOf(entry("name", "John Doe"), entry("age", new BigDecimal(47))), null }, + { "context([{key: \"name\", value: \"John Doe\"},{\"key\": \"age\", \"value\": 47, \"something\": \"else\"}])", mapOf(entry("name", "John Doe"), entry("age", new BigDecimal(47))), null }, + { "context({key: \"name\", value: \"John Doe\"})", mapOf(entry("name", "John Doe")), null }, { "context([{key: \"name\", value: \"John Doe\"},{key: \"name\", value: \"Doe John\"}])", null, FEELEvent.Severity.ERROR }, { "time(10, 20, 30)", LocalTime.of(10, 20, 30), null }, { "date( 2020, 2, 31 )", null, FEELEvent.Severity.ERROR}, diff --git a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/AllFunctionTest.java b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/AllFunctionTest.java index b4d9eaa8d7c..5438fc32d9f 100644 --- a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/AllFunctionTest.java +++ b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/AllFunctionTest.java @@ -29,21 +29,6 @@ class AllFunctionTest { private static final AllFunction allFunction = AllFunction.INSTANCE; - @Test - void invokeBooleanParamNull() { - FunctionTestUtil.assertResultNull(allFunction.invoke((Boolean) null)); - } - - @Test - void invokeBooleanParamTrue() { - FunctionTestUtil.assertResult(allFunction.invoke(true), true); - } - - @Test - void invokeBooleanParamFalse() { - FunctionTestUtil.assertResult(allFunction.invoke(false), false); - } - @Test void invokeArrayParamNull() { FunctionTestUtil.assertResultError(allFunction.invoke((Object[]) null), InvalidParametersEvent.class); diff --git a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/AnyFunctionTest.java b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/AnyFunctionTest.java index 45aa9d6061a..3dfd4006514 100644 --- a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/AnyFunctionTest.java +++ b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/AnyFunctionTest.java @@ -29,21 +29,6 @@ class AnyFunctionTest { private static final AnyFunction anyFunction = AnyFunction.INSTANCE; - @Test - void invokeBooleanParamNull() { - FunctionTestUtil.assertResultNull(anyFunction.invoke((Boolean) null)); - } - - @Test - void invokeBooleanParamTrue() { - FunctionTestUtil.assertResult(anyFunction.invoke(true), true); - } - - @Test - void invokeBooleanParamFalse() { - FunctionTestUtil.assertResult(anyFunction.invoke(false), false); - } - @Test void invokeArrayParamNull() { FunctionTestUtil.assertResultError(anyFunction.invoke((Object[]) null), InvalidParametersEvent.class); diff --git a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/MeanFunctionTest.java b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/MeanFunctionTest.java index 598e0371607..6513c445af4 100644 --- a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/MeanFunctionTest.java +++ b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/MeanFunctionTest.java @@ -32,44 +32,29 @@ class MeanFunctionTest { @Test void invokeNumberNull() { - FunctionTestUtil.assertResultError(meanFunction.invoke((Number) null), InvalidParametersEvent.class); + FunctionTestUtil.assertResultError(meanFunction.invoke(Arrays.asList(10, null, 30)), InvalidParametersEvent.class); } @Test void invokeNumberBigDecimal() { - FunctionTestUtil.assertResult(meanFunction.invoke(BigDecimal.TEN), BigDecimal.TEN); - } - - @Test - void invokeNumberInteger() { - FunctionTestUtil.assertResult(meanFunction.invoke(10), BigDecimal.TEN); - } - - @Test - void invokeNumberDoubleWithoutDecimalPart() { - FunctionTestUtil.assertResult(meanFunction.invoke(10d), BigDecimal.valueOf(10)); - } - - @Test - void invokeNumberDoubleWithDecimalPart() { - FunctionTestUtil.assertResult(meanFunction.invoke(10.1d), BigDecimal.valueOf(10.1)); + FunctionTestUtil.assertResult(meanFunction.invoke(Arrays.asList(BigDecimal.TEN)), BigDecimal.TEN); } @Test void invokeNumberFloat() { - FunctionTestUtil.assertResult(meanFunction.invoke(10.1f), BigDecimal.valueOf(10.1)); + FunctionTestUtil.assertResult(meanFunction.invoke(Arrays.asList(10.1f)), BigDecimal.valueOf(10.1)); } @Test void invokeUnconvertableNumber() { - FunctionTestUtil.assertResultError(meanFunction.invoke(Double.POSITIVE_INFINITY), InvalidParametersEvent.class); - FunctionTestUtil.assertResultError(meanFunction.invoke(Double.NEGATIVE_INFINITY), InvalidParametersEvent.class); - FunctionTestUtil.assertResultError(meanFunction.invoke(Double.NaN), InvalidParametersEvent.class); + FunctionTestUtil.assertResultError(meanFunction.invoke(Arrays.asList(Double.POSITIVE_INFINITY)), InvalidParametersEvent.class); + FunctionTestUtil.assertResultError(meanFunction.invoke(Arrays.asList(Double.NEGATIVE_INFINITY)), InvalidParametersEvent.class); + FunctionTestUtil.assertResultError(meanFunction.invoke(Arrays.asList(Double.NaN)), InvalidParametersEvent.class); } @Test void invokeListNull() { - FunctionTestUtil.assertResultError(meanFunction.invoke((List) null), InvalidParametersEvent.class); + FunctionTestUtil.assertResultError(meanFunction.invoke((List) null), InvalidParametersEvent.class); } @Test diff --git a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/StringJoinFunctionTest.java b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/StringJoinFunctionTest.java new file mode 100644 index 00000000000..37243c8405f --- /dev/null +++ b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/StringJoinFunctionTest.java @@ -0,0 +1,56 @@ +/** + * 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 org.junit.jupiter.api.Test; +import org.kie.dmn.feel.runtime.events.InvalidParametersEvent; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +class StringJoinFunctionTest { + + private static final StringJoinFunction stringJoinFunction = StringJoinFunction.INSTANCE; + + @Test + void setStringJoinFunctionNullValues() { + FunctionTestUtil.assertResultError(stringJoinFunction.invoke( null), InvalidParametersEvent.class); + FunctionTestUtil.assertResultError(stringJoinFunction.invoke((List) null , null), InvalidParametersEvent.class); + } + + @Test + void stringJoinFunctionEmptyList() { + FunctionTestUtil.assertResult(stringJoinFunction.invoke(Collections.emptyList()), ""); + FunctionTestUtil.assertResult(stringJoinFunction.invoke(Collections.emptyList(), "X"), ""); + } + + @Test + void stringJoinFunction() { + FunctionTestUtil.assertResult(stringJoinFunction.invoke(Arrays.asList("a", "b", "c"), "_and_"), "a_and_b_and_c"); + FunctionTestUtil.assertResult(stringJoinFunction.invoke(Arrays.asList("a", "b", "c"), ""), "abc"); + FunctionTestUtil.assertResult(stringJoinFunction.invoke(Arrays.asList("a", "b", "c"), null), "abc"); + FunctionTestUtil.assertResult(stringJoinFunction.invoke(Arrays.asList("a"), "X"), "a"); + FunctionTestUtil.assertResult(stringJoinFunction.invoke(Arrays.asList("a", null, "c"), "X"), "aXc"); + FunctionTestUtil.assertResult(stringJoinFunction.invoke(Arrays.asList("a", "b", "c")), "abc"); + FunctionTestUtil.assertResult(stringJoinFunction.invoke(Arrays.asList("a", null, "c")), "ac"); + } + +} diff --git a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/SumFunctionTest.java b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/SumFunctionTest.java index 50d15bf8e64..84895deb7f4 100644 --- a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/SumFunctionTest.java +++ b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/SumFunctionTest.java @@ -29,27 +29,9 @@ class SumFunctionTest { private static final SumFunction sumFunction = SumFunction.INSTANCE; - @Test - void invokeNumberParamNull() { - FunctionTestUtil.assertResultError(sumFunction.invoke((Number) null), InvalidParametersEvent.class); - } - - @Test - void invokeNumberParamUnsupportedNumber() { - FunctionTestUtil.assertResultError(sumFunction.invoke(Double.NaN), InvalidParametersEvent.class); - } - - @Test - void invokeNumberParamSupportedNumber() { - FunctionTestUtil.assertResult(sumFunction.invoke(BigDecimal.TEN), BigDecimal.TEN); - FunctionTestUtil.assertResult(sumFunction.invoke(10), BigDecimal.TEN); - FunctionTestUtil.assertResult(sumFunction.invoke(-10), BigDecimal.valueOf(-10)); - FunctionTestUtil.assertResult(sumFunction.invoke(10.12), BigDecimal.valueOf(10.12)); - } - @Test void invokeListParam() { - FunctionTestUtil.assertResultError(sumFunction.invoke((List) null), InvalidParametersEvent.class); + FunctionTestUtil.assertResultError(sumFunction.invoke((List) null), InvalidParametersEvent.class); } @Test