From 251e98146d3f1ee567c5fedbf280f431f81d98f5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Jul 2023 00:18:19 +0000 Subject: [PATCH 1/5] chore(deps): bump feel-engine from 1.16.0 to 1.16.1 Bumps [feel-engine](https://github.com/camunda/feel-scala) from 1.16.0 to 1.16.1. - [Release notes](https://github.com/camunda/feel-scala/releases) - [Commits](https://github.com/camunda/feel-scala/compare/1.16.0...1.16.1) --- updated-dependencies: - dependency-name: org.camunda.feel:feel-engine dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d061e8d7..72c72956 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,7 @@ - 1.16.0 + 1.16.1 7.19.0 11 2.13.11 From 632f5c362b70d4fa468f526114a6c7a94aee89e0 Mon Sep 17 00:00:00 2001 From: Tracy Hires Date: Sun, 27 Aug 2023 17:26:16 -0600 Subject: [PATCH 2/5] late-loading of imported models --- .../BusinessKnowledgeEvaluator.scala | 4 +- .../dmn/evaluation/DecisionEvaluator.scala | 72 ++++----- .../org/camunda/dmn/parser/DmnParser.scala | 150 +++++++++--------- .../camunda/dmn/parser/DmnRepository.scala | 15 ++ .../dmn/parser/InMemoryDmnRepository.scala | 15 ++ .../org/camunda/dmn/parser/ParsedDmn.scala | 109 +++++++++---- .../dmn/parser/StatelessDmnRepository.scala | 15 ++ .../scala/org/camunda/dmn/DmnImportTest.scala | 21 ++- 8 files changed, 250 insertions(+), 151 deletions(-) diff --git a/src/main/scala/org/camunda/dmn/evaluation/BusinessKnowledgeEvaluator.scala b/src/main/scala/org/camunda/dmn/evaluation/BusinessKnowledgeEvaluator.scala index 97514ede..a8672728 100644 --- a/src/main/scala/org/camunda/dmn/evaluation/BusinessKnowledgeEvaluator.scala +++ b/src/main/scala/org/camunda/dmn/evaluation/BusinessKnowledgeEvaluator.scala @@ -31,7 +31,7 @@ class BusinessKnowledgeEvaluator( def eval(bkm: ParsedBusinessKnowledgeModel, context: EvalContext): Either[Failure, Val] = { - evalRequiredKnowledge(bkm.requiredBkms, context) + evalRequiredKnowledge(bkm.requiredBkms.map(_.resolve()), context) .flatMap(functions => { val evalContext = @@ -47,7 +47,7 @@ class BusinessKnowledgeEvaluator( bkm: ParsedBusinessKnowledgeModel, context: EvalContext): Either[Failure, (String, ValFunction)] = { - evalRequiredKnowledge(bkm.requiredBkms, context).map(functions => { + evalRequiredKnowledge(bkm.requiredBkms.map(_.resolve()), context).map(functions => { val evalContext = context.copy(variables = context.variables ++ functions, currentElement = bkm) diff --git a/src/main/scala/org/camunda/dmn/evaluation/DecisionEvaluator.scala b/src/main/scala/org/camunda/dmn/evaluation/DecisionEvaluator.scala index fe32cf35..09d2c4f4 100644 --- a/src/main/scala/org/camunda/dmn/evaluation/DecisionEvaluator.scala +++ b/src/main/scala/org/camunda/dmn/evaluation/DecisionEvaluator.scala @@ -18,7 +18,13 @@ package org.camunda.dmn.evaluation import org.camunda.dmn.DmnEngine._ import org.camunda.dmn.FunctionalHelper._ import org.camunda.feel.syntaxtree.{Val, ValContext, ValFunction} -import org.camunda.dmn.parser.{ParsedBusinessKnowledgeModel, ParsedDecision, ParsedDecisionLogic} +import org.camunda.dmn.parser.{ + ParsedBusinessKnowledgeModel, + ParsedBusinessKnowledgeModelReference, + ParsedDecision, + ParsedDecisionReference, + ParsedDecisionLogic, + ParsedDecisionLogicContainerReference} import org.camunda.feel.context.Context.StaticContext class DecisionEvaluator( @@ -42,38 +48,11 @@ class DecisionEvaluator( evalRequiredKnowledge(decision.requiredBkms, context) .flatMap(functions => { - val isImported: ((String, Val)) => Boolean = { - case (name, _) => name.contains(".") - } - - // todo: replace the hack to wrap the imported BKMs and decisions into a context, maybe move to the BKM evaluation logic - val importedFunctions = functions - .filter(isImported) - .map { case (name, function) => - val Array(prefix: String, functionName: String) = name.split('.') - prefix -> ValContext(StaticContext( - variables = Map.empty, - functions = Map(functionName -> List(function)) - )) - } - val embeddedFunctions = functions.filterNot(isImported) - - val importedDecisions = decisionResults - .filter(isImported) - .map { case (name, decisionResult) => - val Array(prefix: String, decisionName: String) = name.split('.') - prefix -> ValContext(StaticContext( - variables = Map(decisionName -> decisionResult), - functions = Map.empty - )) - } - val embeddedDecisions = decisionResults.filterNot(isImported) - val decisionEvaluationContext = context.copy( variables = context.variables - ++ embeddedDecisions ++ importedDecisions - ++ embeddedFunctions ++ importedFunctions, - currentElement = decision) + ++ decisionResults ++ functions, + // ++ embeddedFunctions ++ importedFunctions, + currentElement = decision) eval(decision.logic, decisionEvaluationContext) .flatMap( @@ -87,17 +66,36 @@ class DecisionEvaluator( } private def evalRequiredDecisions( - requiredDecisions: Iterable[ParsedDecision], - context: EvalContext): Either[Failure, List[(String, Val)]] = { + requiredDecisions: Iterable[ParsedDecisionReference], + context: EvalContext): Either[Failure, List[(String, Val)]] = { mapEither(requiredDecisions, - (d: ParsedDecision) => evalDecision(d, context)) + (decisionRef: ParsedDecisionReference) => evalDecision(decisionRef.resolve(), context) + .map(maybeWrapResult(decisionRef, _))) } private def evalRequiredKnowledge( - requiredBkms: Iterable[ParsedBusinessKnowledgeModel], - context: EvalContext): Either[Failure, List[(String, ValFunction)]] = { + requiredBkms: Iterable[ParsedBusinessKnowledgeModelReference], + context: EvalContext): Either[Failure, List[(String, Val)]] = { mapEither(requiredBkms, - (bkm: ParsedBusinessKnowledgeModel) => evalBkm(bkm, context)) + (bkm: ParsedBusinessKnowledgeModelReference) => evalBkm(bkm.resolve(), context) + .map(maybeWrapResult(bkm, _))) } + private def maybeWrapResult( + reference: ParsedDecisionLogicContainerReference[_], result: (String, Val)) = + reference.importedModelName match { + case Some(importName) => + val ctx = result._2 match { + case func: ValFunction => StaticContext( + variables = Map.empty, + functions = Map(result._1 -> List(func)) + ) + case _ => StaticContext( + variables = Map(result._1 -> result._2), + functions = Map.empty + ) + } + importName -> ValContext(ctx) + case _ => result + } } diff --git a/src/main/scala/org/camunda/dmn/parser/DmnParser.scala b/src/main/scala/org/camunda/dmn/parser/DmnParser.scala index b5fa6ac3..34a1967d 100644 --- a/src/main/scala/org/camunda/dmn/parser/DmnParser.scala +++ b/src/main/scala/org/camunda/dmn/parser/DmnParser.scala @@ -15,19 +15,37 @@ */ package org.camunda.dmn.parser -import java.io.InputStream -import org.camunda.dmn.logger import org.camunda.bpm.model.dmn._ import org.camunda.bpm.model.dmn.impl.DmnModelConstants -import org.camunda.bpm.model.dmn.impl.instance.DrgElementImpl -import org.camunda.bpm.model.dmn.instance.{BusinessKnowledgeModel, Column, Context, Decision, DecisionTable, Definitions, DmnElementReference, DrgElement, Expression, FunctionDefinition, InformationItem, InformationRequirement, Invocation, ItemDefinition, KnowledgeRequirement, LiteralExpression, Relation, RequiredDecisionReference, RequiredKnowledgeReference, UnaryTests, List => DmnList} +import org.camunda.bpm.model.dmn.instance.BusinessKnowledgeModel +import org.camunda.bpm.model.dmn.instance.Column +import org.camunda.bpm.model.dmn.instance.Context +import org.camunda.bpm.model.dmn.instance.Decision +import org.camunda.bpm.model.dmn.instance.DecisionTable +import org.camunda.bpm.model.dmn.instance.Definitions +import org.camunda.bpm.model.dmn.instance.DrgElement +import org.camunda.bpm.model.dmn.instance.Expression +import org.camunda.bpm.model.dmn.instance.FunctionDefinition +import org.camunda.bpm.model.dmn.instance.InformationItem +import org.camunda.bpm.model.dmn.instance.InformationRequirement +import org.camunda.bpm.model.dmn.instance.Invocation +import org.camunda.bpm.model.dmn.instance.ItemDefinition +import org.camunda.bpm.model.dmn.instance.KnowledgeRequirement +import org.camunda.bpm.model.dmn.instance.LiteralExpression +import org.camunda.bpm.model.dmn.instance.Relation +import org.camunda.bpm.model.dmn.instance.RequiredDecisionReference +import org.camunda.bpm.model.dmn.instance.RequiredKnowledgeReference +import org.camunda.bpm.model.dmn.instance.UnaryTests +import org.camunda.bpm.model.dmn.instance.{List => DmnList} import org.camunda.bpm.model.xml.instance.ModelElementInstance -import org.camunda.dmn.DmnEngine.{Configuration, Failure} +import org.camunda.dmn.DmnEngine.Configuration +import org.camunda.dmn.DmnEngine.Failure +import org.camunda.dmn.logger import org.camunda.feel -import scala.annotation.tailrec -import scala.collection.JavaConverters._ +import java.io.InputStream import scala.collection.mutable +import scala.jdk.CollectionConverters._ import scala.util.Try object DmnParser { @@ -71,8 +89,8 @@ class DmnParser( val parsedFeelExpressions = mutable.Map[String, ParsedExpression]() val parsedFeelUnaryTest = mutable.Map[String, ParsedExpression]() - val decisions = mutable.Map[String, ParsedDecision]() - val bkms = mutable.Map[String, ParsedBusinessKnowledgeModel]() + val decisions = mutable.Map[String, ParsedDecisionReference]() + val bkms = mutable.Map[String, ParsedBusinessKnowledgeModelReference]() val importedModels = mutable.ListBuffer[ImportedModel]() @@ -102,7 +120,7 @@ class DmnParser( } private def parseModel( - model: DmnModelInstance): Either[Iterable[Failure], ParsedDmn] = { + model: DmnModelInstance): Either[Iterable[Failure], ParsedDmn] = { val ctx = ParsingContext(model) @@ -126,8 +144,8 @@ class DmnParser( val parsedDmn = ParsedDmn( model = model, - decisions = ctx.decisions.values, - bkms = ctx.bkms.values, + decisions = ctx.decisions.values.filter(_.isEmbedded).map(_.resolve()), + bkms = ctx.bkms.values.filter(_.isEmbedded).map(_.resolve()), namespace = definitions.getNamespace) if (ctx.failures.isEmpty) { @@ -227,7 +245,7 @@ class DmnParser( private def parseDecision(decision: Decision)( implicit - ctx: ParsingContext): ParsedDecision = { + ctx: ParsingContext): ParsedDecisionReference = { val requiredDecisions = decision.getInformationRequirements.asScala .flatMap(requirement => @@ -287,92 +305,72 @@ class DmnParser( ) } - private def parseRequiredDecision(informationRequirement: InformationRequirement, reference: ModelReference)(implicit ctx: ParsingContext): ParsedDecision = { + private def parseRequiredDecision(informationRequirement: InformationRequirement, reference: ModelReference)(implicit ctx: ParsingContext): ParsedDecisionReference = { if (reference.isEmbedded) { val requiredDecision = informationRequirement.getRequiredDecision ctx.decisions.getOrElseUpdate(requiredDecision.getId, parseDecision(decision = requiredDecision)) } else { ctx.importedModels .find(importedModel => reference.namespace == importedModel.namespace) - .map(importedModel => createReferenceForImportedDecision(importedModel, reference)) + .map(importedModel => ImportedDecision(dmnRepository, reference.namespace, reference.id, Some(importedModel.name))) .getOrElse { - ctx.failures += Failure(s"No import found for namespace '${reference.namespace}'.") - ImportedDecision(() => - throw new RuntimeException(s"Failed to invoke decision. No import found for namespace '${reference.namespace}'.") - ) + val failure = Failure(s"No import found for namespace '${reference.namespace}'.") + ctx.failures += failure + ParsedDecisionFailure(reference.id, reference.namespace, ExpressionFailure(failure.message)) } } } - private def createReferenceForImportedDecision(importedModel: ImportedModel, reference: ModelReference): ImportedDecision = { - ImportedDecision(() => { - // todo: extract loading, try to move to evaluation phase - dmnRepository.getDecision( - namespace = reference.namespace, - decisionId = reference.id - ) match { - case Right(decision) => EmbeddedDecision( - id = reference.id, - // todo: replace the hack to add the namespace to the name - name = s"${importedModel.name}.${decision.name}", - resultName = s"${importedModel.name}.${decision.resultName}", - logic = decision.logic, - resultType = decision.resultType, - requiredDecisions = decision.requiredDecisions, - requiredBkms = decision.requiredBkms - ) - // todo: don't throw an exception if a decision was not found, but return a failure - case Left(failure) => throw new RuntimeException(failure.message) - } - }) - } - private def getBkmReference(knowledgeRequirement: KnowledgeRequirement): Option[ModelReference] = { Option(knowledgeRequirement.getUniqueChildElementByType(classOf[RequiredKnowledgeReference])) .map(createModelReference) } - private def parseRequiredBkm(knowledgeRequirement: KnowledgeRequirement, reference: ModelReference)(implicit ctx: ParsingContext): ParsedBusinessKnowledgeModel = { + private def parseRequiredBkm(knowledgeRequirement: KnowledgeRequirement, reference: ModelReference)(implicit ctx: ParsingContext): ParsedBusinessKnowledgeModelReference = { if (reference.isEmbedded) { val requiredKnowledge = knowledgeRequirement.getRequiredKnowledge ctx.bkms.getOrElseUpdate(requiredKnowledge.getName, parseBusinessKnowledgeModel(requiredKnowledge)) } else { ctx.importedModels .find(importedModel => reference.namespace == importedModel.namespace) - .map(importedModel => createReferenceForImportedBkm(importedModel, reference)) - .getOrElse { - ctx.failures += Failure(s"No import found for namespace '${reference.namespace}'.") - ImportedBusinessKnowledgeModel(() => - throw new RuntimeException(s"Failed to invoke BKM. No import found for namespace '${reference.namespace}'.") - ) - } + .map(importedModel => ImportedBusinessKnowledgeModel( + dmnRepository, reference.namespace, reference.id, Some(importedModel.name))) + .getOrElse { + val failure = Failure(s"No import found for namespace '${reference.namespace}'.") + ctx.failures += failure + ParsedBusinessKnowledgeModelFailure(reference.id, reference.namespace, ExpressionFailure(failure.message)) + } } } - private def createReferenceForImportedBkm(importedModel: ImportedModel, reference: ModelReference): ImportedBusinessKnowledgeModel = { - ImportedBusinessKnowledgeModel(() => { - // todo: extract loading, try to move to evaluation phase - dmnRepository.getBusinessKnowledgeModel( - namespace = reference.namespace, - bkmId = reference.id - ) match { - case Right(bkm) => EmbeddedBusinessKnowledgeModel( - id = reference.id, - // todo: replace the hack to add the namespace to the name - name = s"${importedModel.name}.${bkm.name}", - logic = bkm.logic, - parameters = bkm.parameters, - requiredBkms = bkm.requiredBkms - ) - // todo: don't throw an exception if a BKM was not found, but return a failure - case Left(failure) => throw new RuntimeException(failure.message) - } - }) + private def createReferenceForImportedBkm(importedModel: ImportedModel, reference: ModelReference)( + implicit + ctx: ParsingContext): ImportedBusinessKnowledgeModel = { + ImportedBusinessKnowledgeModel(dmnRepository, reference.namespace, reference.id, Some(importedModel.name)) + // ImportedBusinessKnowledgeModel(() => { + // // todo: extract loading, try to move to evaluation phase + // dmnRepository.getBusinessKnowledgeModel( + // namespace = reference.namespace, + // bkmId = reference.id + // ) match { + // case Right(bkm) => EmbeddedBusinessKnowledgeModel( + // id = reference.id, + // // todo: replace the hack to add the namespace to the name + // name = s"${importedModel.name}.${bkm.name}", + // logic = bkm.logic, + // parameters = bkm.parameters, + // requiredBkms = bkm.requiredBkms + // ) + // case Left(failure) => + // ctx.failures += Failure(failure.message) + // EmbeddedBusinessKnowledgeModel(reference.id, "", EmptyLogic, Iterable.empty, Iterable.empty) + // } + // }) } private def parseBusinessKnowledgeModel(bkm: BusinessKnowledgeModel)( implicit - ctx: ParsingContext): ParsedBusinessKnowledgeModel = { + ctx: ParsingContext): ParsedBusinessKnowledgeModelReference = { // TODO be aware of loops val knowledgeRequirements = bkm.getKnowledgeRequirement.asScala @@ -580,7 +578,7 @@ class DmnParser( ctx.bkms .get(expression) .map(bkm => { - ParsedInvocation(bindings, bkm) + ParsedInvocation(bindings, bkm.resolve()) }) .getOrElse { ctx.failures += Failure(s"no BKM found with name '$expression'") @@ -636,7 +634,7 @@ class DmnParser( .toRight(Failure(s"The expression '${lt.getId}' must not be empty.")) private def validateExpressionLanguage( - lt: LiteralExpression): Either[Failure, Unit] = { + lt: LiteralExpression): Either[Failure, Unit] = { val language = Option(lt.getExpressionLanguage).map(_.toLowerCase).getOrElse("feel") if (feelNameSpaces.contains(language)) { @@ -683,11 +681,11 @@ class DmnParser( ctx.parsedFeelUnaryTest.getOrElseUpdate( expression, { - if (expression.isEmpty()) { + if (expression.isEmpty) { EmptyExpression } else { - var escapedExpression = + val escapedExpression = escapeNamesInExpression(expression, ctx.namesToEscape) feelUnaryTestsParser(escapedExpression) match { @@ -705,8 +703,8 @@ class DmnParser( } private def escapeNamesInExpression( - expression: String, - namesWithSpaces: Iterable[String]): String = { + expression: String, + namesWithSpaces: Iterable[String]): String = { (expression /: namesWithSpaces)( (e, name) => diff --git a/src/main/scala/org/camunda/dmn/parser/DmnRepository.scala b/src/main/scala/org/camunda/dmn/parser/DmnRepository.scala index 8d026e9b..3d7280ed 100644 --- a/src/main/scala/org/camunda/dmn/parser/DmnRepository.scala +++ b/src/main/scala/org/camunda/dmn/parser/DmnRepository.scala @@ -1,3 +1,18 @@ +/* + * Copyright © 2022 Camunda Services GmbH (info@camunda.com) + * + * Licensed 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.camunda.dmn.parser import org.camunda.dmn.DmnEngine.Failure diff --git a/src/main/scala/org/camunda/dmn/parser/InMemoryDmnRepository.scala b/src/main/scala/org/camunda/dmn/parser/InMemoryDmnRepository.scala index d846c324..1987f1b7 100644 --- a/src/main/scala/org/camunda/dmn/parser/InMemoryDmnRepository.scala +++ b/src/main/scala/org/camunda/dmn/parser/InMemoryDmnRepository.scala @@ -1,3 +1,18 @@ +/* + * Copyright © 2022 Camunda Services GmbH (info@camunda.com) + * + * Licensed 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.camunda.dmn.parser import org.camunda.dmn.DmnEngine.Failure diff --git a/src/main/scala/org/camunda/dmn/parser/ParsedDmn.scala b/src/main/scala/org/camunda/dmn/parser/ParsedDmn.scala index afd2da06..4812d729 100644 --- a/src/main/scala/org/camunda/dmn/parser/ParsedDmn.scala +++ b/src/main/scala/org/camunda/dmn/parser/ParsedDmn.scala @@ -51,55 +51,98 @@ sealed trait ParsedDecisionLogicContainer { val logic: ParsedDecisionLogic } -trait ParsedDecision extends ParsedDecisionLogicContainer { +sealed trait ParsedDecision extends ParsedDecisionLogicContainer{ val resultName: String val resultType: Option[String] - val requiredDecisions: Iterable[ParsedDecision] - val requiredBkms: Iterable[ParsedBusinessKnowledgeModel] + val requiredDecisions: Iterable[ParsedDecisionReference] + val requiredBkms: Iterable[ParsedBusinessKnowledgeModelReference] } -case class EmbeddedDecision(id: String, - name: String, - logic: ParsedDecisionLogic, - resultName: String, - resultType: Option[String], - requiredDecisions: Iterable[ParsedDecision], - requiredBkms: Iterable[ParsedBusinessKnowledgeModel]) - extends ParsedDecision - -case class ImportedDecision(importer: () => ParsedDecision) extends ParsedDecision { - private lazy val model = importer() - override lazy val id: String = model.id - override lazy val name: String = model.name - override lazy val logic: ParsedDecisionLogic = model.logic - override lazy val resultName: String = model.resultName - override lazy val resultType: Option[String] = model.resultType - override lazy val requiredDecisions: Iterable[ParsedDecision] = model.requiredDecisions - override lazy val requiredBkms: Iterable[ParsedBusinessKnowledgeModel] = model.requiredBkms +trait ParsedDecisionLogicContainerReference[T <: ParsedDecisionLogicContainer] { + val importedModelName: Option[String] = None + def resolve(): T + def isEmbedded: Boolean + def isImported: Boolean = !isEmbedded } -trait ParsedBusinessKnowledgeModel extends ParsedDecisionLogicContainer { +trait ParsedDecisionReference extends ParsedDecisionLogicContainerReference[ParsedDecision] { +} + +case class EmbeddedDecision( + id: String, + name: String, + logic: ParsedDecisionLogic, + resultName: String, + resultType: Option[String], + requiredDecisions: Iterable[ParsedDecisionReference], + requiredBkms: Iterable[ParsedBusinessKnowledgeModelReference] +) + extends ParsedDecision with ParsedDecisionReference { + override def resolve(): ParsedDecision = this + + override def isEmbedded: Boolean = true +} + +trait ImportedParsedDecisionLogicFailure[T <: ParsedDecisionLogicContainer] + extends ParsedDecisionLogicContainerReference[T] { + val id: String + val namespace: String + val expressionFailure: ExpressionFailure + override def resolve(): T = throw new RuntimeException(expressionFailure.failure) + + override def isEmbedded: Boolean = false +} + +case class ImportedDecision(repository: DmnRepository, namespace: String, id: String, override val importedModelName: Option[String]) extends ParsedDecisionReference { + override def resolve(): ParsedDecision = repository.getDecision(namespace, id) match { + case Right(found) => found + case Left(failure) => ParsedDecisionFailure(id, namespace, ExpressionFailure(failure.message)).resolve() + } + + override def isEmbedded: Boolean = false + +} + +sealed trait ParsedBusinessKnowledgeModel extends ParsedDecisionLogicContainer { val parameters: Iterable[(String, String)] - val requiredBkms: Iterable[ParsedBusinessKnowledgeModel] + val requiredBkms: Iterable[ParsedBusinessKnowledgeModelReference] } +trait ParsedBusinessKnowledgeModelReference extends ParsedDecisionLogicContainerReference[ParsedBusinessKnowledgeModel] + + case class EmbeddedBusinessKnowledgeModel( id: String, name: String, logic: ParsedDecisionLogic, parameters: Iterable[(String, String)], - requiredBkms: Iterable[ParsedBusinessKnowledgeModel]) - extends ParsedBusinessKnowledgeModel - -case class ImportedBusinessKnowledgeModel(importer: () => ParsedBusinessKnowledgeModel) extends ParsedBusinessKnowledgeModel { - private lazy val model = importer() - override lazy val id: String = model.id - override lazy val name: String = model.name - override lazy val logic: ParsedDecisionLogic = model.logic - override lazy val parameters: Iterable[(String, String)] = model.parameters - override lazy val requiredBkms: Iterable[ParsedBusinessKnowledgeModel] = model.requiredBkms + requiredBkms: Iterable[ParsedBusinessKnowledgeModelReference]) + extends ParsedBusinessKnowledgeModel with ParsedBusinessKnowledgeModelReference { + override def resolve(): ParsedBusinessKnowledgeModel = this + + override def isEmbedded: Boolean = true } +case class ImportedBusinessKnowledgeModel( + repository: DmnRepository, namespace: String, id: String, override val importedModelName: Option[String]) + extends ParsedBusinessKnowledgeModelReference { + override def resolve(): ParsedBusinessKnowledgeModel = repository.getBusinessKnowledgeModel(namespace, id) match { + case Right(found) => found + case Left(failure) => + ParsedBusinessKnowledgeModelFailure(id, namespace, ExpressionFailure(failure.message)).resolve() + } + + override def isEmbedded: Boolean = false +} + +case class ParsedBusinessKnowledgeModelFailure(id: String, namespace: String, expressionFailure: ExpressionFailure) + extends ImportedParsedDecisionLogicFailure[ParsedBusinessKnowledgeModel] + with ParsedBusinessKnowledgeModelReference + +case class ParsedDecisionFailure(id: String, namespace: String, expressionFailure: ExpressionFailure) + extends ImportedParsedDecisionLogicFailure[ParsedDecision] + with ParsedDecisionReference + sealed trait ParsedDecisionLogic case class ParsedInvocation(bindings: Iterable[(String, ParsedExpression)], diff --git a/src/main/scala/org/camunda/dmn/parser/StatelessDmnRepository.scala b/src/main/scala/org/camunda/dmn/parser/StatelessDmnRepository.scala index 5668292e..c366caf0 100644 --- a/src/main/scala/org/camunda/dmn/parser/StatelessDmnRepository.scala +++ b/src/main/scala/org/camunda/dmn/parser/StatelessDmnRepository.scala @@ -1,3 +1,18 @@ +/* + * Copyright © 2022 Camunda Services GmbH (info@camunda.com) + * + * Licensed 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.camunda.dmn.parser import org.camunda.dmn.DmnEngine.Failure diff --git a/src/test/scala/org/camunda/dmn/DmnImportTest.scala b/src/test/scala/org/camunda/dmn/DmnImportTest.scala index cb5860aa..17076f45 100644 --- a/src/test/scala/org/camunda/dmn/DmnImportTest.scala +++ b/src/test/scala/org/camunda/dmn/DmnImportTest.scala @@ -1,3 +1,18 @@ +/* + * Copyright © 2022 Camunda Services GmbH (info@camunda.com) + * + * Licensed 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.camunda.dmn import org.camunda.dmn.parser.InMemoryDmnRepository @@ -11,15 +26,15 @@ class DmnImportTest extends AnyFlatSpec with Matchers with DecisionTest{ dmnRepository = new InMemoryDmnRepository() ) + private val decisionWithBkmImport = parse("/tck/0086-import/0086-import.dmn") + private val decisionWithDecisionImport = parse("/tck/0089-nested-inputdata-imports/0089-nested-inputdata-imports.dmn") + // parse required DMNs parse("/tck/0086-import/Imported_Model.dmn") parse("/tck/0089-nested-inputdata-imports/Say_hello_1ID1D.dmn") parse("/tck/0089-nested-inputdata-imports/Model_B.dmn") parse("/tck/0089-nested-inputdata-imports/Model_B2.dmn") - private val decisionWithBkmImport = parse("/tck/0086-import/0086-import.dmn") - private val decisionWithDecisionImport = parse("/tck/0089-nested-inputdata-imports/0089-nested-inputdata-imports.dmn") - "A decision with an imported BKM" should "invoke the BKM from the imported DMN" in { eval(decisionWithBkmImport, "decision_with_imported_bkm", From 72da365af31c9806d21b90e2eedab8c6a44d4383 Mon Sep 17 00:00:00 2001 From: Tracy Hires Date: Mon, 28 Aug 2023 09:32:35 -0600 Subject: [PATCH 3/5] cleanup code --- api-check-ignore.xml | 38 +++++++++++++++++-- pom.xml | 2 +- .../dmn/evaluation/DecisionEvaluator.scala | 5 +-- .../org/camunda/dmn/parser/DmnParser.scala | 32 ++++++++-------- .../org/camunda/dmn/parser/ParsedDmn.scala | 23 +++++------ 5 files changed, 65 insertions(+), 35 deletions(-) diff --git a/api-check-ignore.xml b/api-check-ignore.xml index f62f6eef..e4c1af26 100644 --- a/api-check-ignore.xml +++ b/api-check-ignore.xml @@ -11,7 +11,6 @@ 7004 *$anonfun* - * * @@ -19,8 +18,6 @@ 7005 *$anonfun* - - * * @@ -43,4 +40,39 @@ 7002 * + + + org/camunda/dmn/DmnEngine + 7004 + DmnEngine(* + + + org/camunda/dmn/parser/DmnParser + 7004 + DmnParser(* + + + org/camunda/dmn/parser/ParsedDmn + 7004 + * + + + org/camunda/dmn/parser/ParsedBusinessKnowledgeModel + 2000 + + + org/camunda/dmn/parser/ParsedBusinessKnowledgeModel + 4001 + ** + + + org/camunda/dmn/parser/ParsedDecision + 2000 + + + org/camunda/dmn/parser/ParsedDecision + 4001 + ** + + diff --git a/pom.xml b/pom.xml index 72c72956..04474e14 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.camunda.bpm.extension.dmn.scala dmn-engine DMN Scala Engine - 1.8.2-SNAPSHOT + 1.9.0-SNAPSHOT org.camunda diff --git a/src/main/scala/org/camunda/dmn/evaluation/DecisionEvaluator.scala b/src/main/scala/org/camunda/dmn/evaluation/DecisionEvaluator.scala index 09d2c4f4..8722affd 100644 --- a/src/main/scala/org/camunda/dmn/evaluation/DecisionEvaluator.scala +++ b/src/main/scala/org/camunda/dmn/evaluation/DecisionEvaluator.scala @@ -51,7 +51,6 @@ class DecisionEvaluator( val decisionEvaluationContext = context.copy( variables = context.variables ++ decisionResults ++ functions, - // ++ embeddedFunctions ++ importedFunctions, currentElement = decision) eval(decision.logic, decisionEvaluationContext) @@ -77,8 +76,8 @@ class DecisionEvaluator( requiredBkms: Iterable[ParsedBusinessKnowledgeModelReference], context: EvalContext): Either[Failure, List[(String, Val)]] = { mapEither(requiredBkms, - (bkm: ParsedBusinessKnowledgeModelReference) => evalBkm(bkm.resolve(), context) - .map(maybeWrapResult(bkm, _))) + (bkmRef: ParsedBusinessKnowledgeModelReference) => evalBkm(bkmRef.resolve(), context) + .map(maybeWrapResult(bkmRef, _))) } private def maybeWrapResult( diff --git a/src/main/scala/org/camunda/dmn/parser/DmnParser.scala b/src/main/scala/org/camunda/dmn/parser/DmnParser.scala index 34a1967d..7de56b22 100644 --- a/src/main/scala/org/camunda/dmn/parser/DmnParser.scala +++ b/src/main/scala/org/camunda/dmn/parser/DmnParser.scala @@ -67,11 +67,11 @@ object DmnParser { } class DmnParser( - configuration: Configuration, - feelParser: String => Either[String, feel.syntaxtree.ParsedExpression], - feelUnaryTestsParser: String => Either[String, - feel.syntaxtree.ParsedExpression], - dmnRepository: DmnRepository) { + configuration: Configuration, + feelParser: String => Either[String, feel.syntaxtree.ParsedExpression], + feelUnaryTestsParser: String => Either[String, + feel.syntaxtree.ParsedExpression], + dmnRepository: DmnRepository) { import DmnParser._ @@ -79,6 +79,7 @@ class DmnParser( case class ModelReference(namespace: String, id: String) { def isEmbedded: Boolean = namespace.isEmpty + def isImported: Boolean = !isEmbedded } @@ -335,11 +336,11 @@ class DmnParser( .find(importedModel => reference.namespace == importedModel.namespace) .map(importedModel => ImportedBusinessKnowledgeModel( dmnRepository, reference.namespace, reference.id, Some(importedModel.name))) - .getOrElse { - val failure = Failure(s"No import found for namespace '${reference.namespace}'.") - ctx.failures += failure - ParsedBusinessKnowledgeModelFailure(reference.id, reference.namespace, ExpressionFailure(failure.message)) - } + .getOrElse { + val failure = Failure(s"No import found for namespace '${reference.namespace}'.") + ctx.failures += failure + ParsedBusinessKnowledgeModelFailure(reference.id, reference.namespace, ExpressionFailure(failure.message)) + } } } @@ -558,18 +559,16 @@ class DmnParser( ctx: ParsingContext): ParsedDecisionLogic = { val bindings = invocation.getBindings.asScala - .map(b => + .flatMap(b => b.getExpression match { case lt: LiteralExpression => Some(b.getParameter.getName -> parseFeelExpression(lt)) case other => { ctx.failures += Failure( s"expected binding with literal expression but found '$other'") - None } }) - .flatten invocation.getExpression match { case lt: LiteralExpression => { @@ -577,8 +576,8 @@ class DmnParser( ctx.bkms .get(expression) - .map(bkm => { - ParsedInvocation(bindings, bkm.resolve()) + .map(bkmRef => { + ParsedInvocation(bindings, bkmRef.resolve()) }) .getOrElse { ctx.failures += Failure(s"no BKM found with name '$expression'") @@ -633,8 +632,7 @@ class DmnParser( .map(_.getTextContent) .toRight(Failure(s"The expression '${lt.getId}' must not be empty.")) - private def validateExpressionLanguage( - lt: LiteralExpression): Either[Failure, Unit] = { + private def validateExpressionLanguage(lt: LiteralExpression): Either[Failure, Unit] = { val language = Option(lt.getExpressionLanguage).map(_.toLowerCase).getOrElse("feel") if (feelNameSpaces.contains(language)) { diff --git a/src/main/scala/org/camunda/dmn/parser/ParsedDmn.scala b/src/main/scala/org/camunda/dmn/parser/ParsedDmn.scala index 4812d729..47556693 100644 --- a/src/main/scala/org/camunda/dmn/parser/ParsedDmn.scala +++ b/src/main/scala/org/camunda/dmn/parser/ParsedDmn.scala @@ -51,7 +51,7 @@ sealed trait ParsedDecisionLogicContainer { val logic: ParsedDecisionLogic } -sealed trait ParsedDecision extends ParsedDecisionLogicContainer{ +trait ParsedDecision extends ParsedDecisionLogicContainer { val resultName: String val resultType: Option[String] val requiredDecisions: Iterable[ParsedDecisionReference] @@ -76,8 +76,7 @@ case class EmbeddedDecision( resultType: Option[String], requiredDecisions: Iterable[ParsedDecisionReference], requiredBkms: Iterable[ParsedBusinessKnowledgeModelReference] -) - extends ParsedDecision with ParsedDecisionReference { +) extends ParsedDecision with ParsedDecisionReference { override def resolve(): ParsedDecision = this override def isEmbedded: Boolean = true @@ -112,20 +111,22 @@ trait ParsedBusinessKnowledgeModelReference extends ParsedDecisionLogicContainer case class EmbeddedBusinessKnowledgeModel( - id: String, - name: String, - logic: ParsedDecisionLogic, - parameters: Iterable[(String, String)], - requiredBkms: Iterable[ParsedBusinessKnowledgeModelReference]) - extends ParsedBusinessKnowledgeModel with ParsedBusinessKnowledgeModelReference { + id: String, + name: String, + logic: ParsedDecisionLogic, + parameters: Iterable[(String, String)], + requiredBkms: Iterable[ParsedBusinessKnowledgeModelReference]) extends +ParsedBusinessKnowledgeModel with ParsedBusinessKnowledgeModelReference { + override def resolve(): ParsedBusinessKnowledgeModel = this override def isEmbedded: Boolean = true } case class ImportedBusinessKnowledgeModel( - repository: DmnRepository, namespace: String, id: String, override val importedModelName: Option[String]) - extends ParsedBusinessKnowledgeModelReference { + repository: DmnRepository, + namespace: String, id: String, + override val importedModelName: Option[String]) extends ParsedBusinessKnowledgeModelReference { override def resolve(): ParsedBusinessKnowledgeModel = repository.getBusinessKnowledgeModel(namespace, id) match { case Right(found) => found case Left(failure) => From b171f490cd17278aeb4db7847f6dc795e629226c Mon Sep 17 00:00:00 2001 From: Tracy Hires Date: Mon, 28 Aug 2023 12:57:39 -0600 Subject: [PATCH 4/5] more code cleanup --- .../org/camunda/dmn/parser/DmnParser.scala | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/src/main/scala/org/camunda/dmn/parser/DmnParser.scala b/src/main/scala/org/camunda/dmn/parser/DmnParser.scala index 7de56b22..3cac8558 100644 --- a/src/main/scala/org/camunda/dmn/parser/DmnParser.scala +++ b/src/main/scala/org/camunda/dmn/parser/DmnParser.scala @@ -344,31 +344,6 @@ class DmnParser( } } - private def createReferenceForImportedBkm(importedModel: ImportedModel, reference: ModelReference)( - implicit - ctx: ParsingContext): ImportedBusinessKnowledgeModel = { - ImportedBusinessKnowledgeModel(dmnRepository, reference.namespace, reference.id, Some(importedModel.name)) - // ImportedBusinessKnowledgeModel(() => { - // // todo: extract loading, try to move to evaluation phase - // dmnRepository.getBusinessKnowledgeModel( - // namespace = reference.namespace, - // bkmId = reference.id - // ) match { - // case Right(bkm) => EmbeddedBusinessKnowledgeModel( - // id = reference.id, - // // todo: replace the hack to add the namespace to the name - // name = s"${importedModel.name}.${bkm.name}", - // logic = bkm.logic, - // parameters = bkm.parameters, - // requiredBkms = bkm.requiredBkms - // ) - // case Left(failure) => - // ctx.failures += Failure(failure.message) - // EmbeddedBusinessKnowledgeModel(reference.id, "", EmptyLogic, Iterable.empty, Iterable.empty) - // } - // }) - } - private def parseBusinessKnowledgeModel(bkm: BusinessKnowledgeModel)( implicit ctx: ParsingContext): ParsedBusinessKnowledgeModelReference = { From 67853b60faa1b33a240b1a63cd273e33b50dab8c Mon Sep 17 00:00:00 2001 From: Philipp Ossler Date: Fri, 6 Oct 2023 10:33:20 +0200 Subject: [PATCH 5/5] refactor: move decision/BKM loading to evaluation Remove the resolve() function of the references. Instead, load imported models during the evaluation. Handle missing models during the evaluation by returning a failure. Refine and simplify the parsed types. --- .../scala/org/camunda/dmn/DmnEngine.scala | 17 +++-- .../BusinessKnowledgeEvaluator.scala | 23 +++++-- .../dmn/evaluation/DecisionEvaluator.scala | 66 ++++++++++--------- .../dmn/evaluation/InvocationEvaluator.scala | 20 ++++-- .../org/camunda/dmn/parser/DmnParser.scala | 18 +++-- .../org/camunda/dmn/parser/ParsedDmn.scala | 59 +++++------------ 6 files changed, 100 insertions(+), 103 deletions(-) diff --git a/src/main/scala/org/camunda/dmn/DmnEngine.scala b/src/main/scala/org/camunda/dmn/DmnEngine.scala index 0d49af3d..e9c47226 100644 --- a/src/main/scala/org/camunda/dmn/DmnEngine.scala +++ b/src/main/scala/org/camunda/dmn/DmnEngine.scala @@ -156,19 +156,20 @@ class DmnEngine(configuration: DmnEngine.Configuration = val parser = new DmnParser( configuration = configuration, feelParser = feelEngine.parseExpression(_).left.map(_.message), - feelUnaryTestsParser = feelEngine.parseUnaryTests(_).left.map(_.message), - dmnRepository = dmnRepository - ) + feelUnaryTestsParser = feelEngine.parseUnaryTests(_).left.map(_.message)) - val decisionEval = new DecisionEvaluator(eval = this.evalExpression, - evalBkm = bkmEval.createFunction) + val decisionEval = new DecisionEvaluator( + eval = this.evalExpression, + evalBkm = bkmEval.createFunction, + repository = dmnRepository + ) val literalExpressionEval = new LiteralExpressionEvaluator(feelEngine) val decisionTableEval = new DecisionTableEvaluator( literalExpressionEval.evalExpression) - val bkmEval = new BusinessKnowledgeEvaluator(this.evalExpression, valueMapper) + val bkmEval = new BusinessKnowledgeEvaluator(this.evalExpression, valueMapper, dmnRepository) val contextEval = new ContextEvaluator(this.evalExpression) @@ -178,7 +179,9 @@ class DmnEngine(configuration: DmnEngine.Configuration = val invocationEval = new InvocationEvaluator( eval = literalExpressionEval.evalExpression, - evalBkm = bkmEval.eval) + evalBkm = bkmEval.eval, + repository = dmnRepository + ) val functionDefinitionEval = new FunctionDefinitionEvaluator( literalExpressionEval.evalExpression) diff --git a/src/main/scala/org/camunda/dmn/evaluation/BusinessKnowledgeEvaluator.scala b/src/main/scala/org/camunda/dmn/evaluation/BusinessKnowledgeEvaluator.scala index a8672728..80353ace 100644 --- a/src/main/scala/org/camunda/dmn/evaluation/BusinessKnowledgeEvaluator.scala +++ b/src/main/scala/org/camunda/dmn/evaluation/BusinessKnowledgeEvaluator.scala @@ -17,21 +17,20 @@ package org.camunda.dmn.evaluation import org.camunda.dmn.DmnEngine._ import org.camunda.dmn.FunctionalHelper._ -import org.camunda.dmn.parser.{ - ParsedBusinessKnowledgeModel, - ParsedDecisionLogic -} +import org.camunda.dmn.parser.{DmnRepository, EmbeddedBusinessKnowledgeModel, ExpressionFailure, ImportedBusinessKnowledgeModel, ParsedBusinessKnowledgeModel, ParsedBusinessKnowledgeModelFailure, ParsedBusinessKnowledgeModelReference, ParsedDecisionLogic} import org.camunda.feel.syntaxtree.{Val, ValError, ValFunction} import org.camunda.feel.valuemapper.ValueMapper class BusinessKnowledgeEvaluator( eval: (ParsedDecisionLogic, EvalContext) => Either[Failure, Val], - valueMapper: ValueMapper) { + valueMapper: ValueMapper, + repository: DmnRepository) { def eval(bkm: ParsedBusinessKnowledgeModel, context: EvalContext): Either[Failure, Val] = { - evalRequiredKnowledge(bkm.requiredBkms.map(_.resolve()), context) + resolveRequiredBkms(bkm) + .flatMap(evalRequiredKnowledge(_, context)) .flatMap(functions => { val evalContext = @@ -43,11 +42,21 @@ class BusinessKnowledgeEvaluator( }) } + private def resolveRequiredBkms(bkm: ParsedBusinessKnowledgeModel): Either[Failure, Iterable[ParsedBusinessKnowledgeModel]] = { + mapEither[ParsedBusinessKnowledgeModelReference, ParsedBusinessKnowledgeModel](bkm.requiredBkms, { + case ImportedBusinessKnowledgeModel(namespace, id, _) => repository.getBusinessKnowledgeModel(namespace = namespace, bkmId = id) + case ParsedBusinessKnowledgeModelFailure(_, _, failureMessage) => Left(Failure(failureMessage)) + case bkm: EmbeddedBusinessKnowledgeModel => Right(bkm) + }) + } + def createFunction( bkm: ParsedBusinessKnowledgeModel, context: EvalContext): Either[Failure, (String, ValFunction)] = { - evalRequiredKnowledge(bkm.requiredBkms.map(_.resolve()), context).map(functions => { + resolveRequiredBkms(bkm) + .flatMap(evalRequiredKnowledge(_, context)) + .map(functions => { val evalContext = context.copy(variables = context.variables ++ functions, currentElement = bkm) diff --git a/src/main/scala/org/camunda/dmn/evaluation/DecisionEvaluator.scala b/src/main/scala/org/camunda/dmn/evaluation/DecisionEvaluator.scala index 8722affd..b47a3870 100644 --- a/src/main/scala/org/camunda/dmn/evaluation/DecisionEvaluator.scala +++ b/src/main/scala/org/camunda/dmn/evaluation/DecisionEvaluator.scala @@ -18,19 +18,14 @@ package org.camunda.dmn.evaluation import org.camunda.dmn.DmnEngine._ import org.camunda.dmn.FunctionalHelper._ import org.camunda.feel.syntaxtree.{Val, ValContext, ValFunction} -import org.camunda.dmn.parser.{ - ParsedBusinessKnowledgeModel, - ParsedBusinessKnowledgeModelReference, - ParsedDecision, - ParsedDecisionReference, - ParsedDecisionLogic, - ParsedDecisionLogicContainerReference} +import org.camunda.dmn.parser.{DmnRepository, EmbeddedBusinessKnowledgeModel, EmbeddedDecision, ImportedBusinessKnowledgeModel, ImportedDecision, ParsedBusinessKnowledgeModel, ParsedBusinessKnowledgeModelFailure, ParsedBusinessKnowledgeModelReference, ParsedDecision, ParsedDecisionFailure, ParsedDecisionLogic, ParsedDecisionReference} import org.camunda.feel.context.Context.StaticContext class DecisionEvaluator( eval: (ParsedDecisionLogic, EvalContext) => Either[Failure, Val], evalBkm: (ParsedBusinessKnowledgeModel, - EvalContext) => Either[Failure, (String, ValFunction)]) { + EvalContext) => Either[Failure, (String, ValFunction)], + repository: DmnRepository) { def eval(decision: ParsedDecision, context: EvalContext): Either[Failure, Val] = { @@ -67,34 +62,43 @@ class DecisionEvaluator( private def evalRequiredDecisions( requiredDecisions: Iterable[ParsedDecisionReference], context: EvalContext): Either[Failure, List[(String, Val)]] = { - mapEither(requiredDecisions, - (decisionRef: ParsedDecisionReference) => evalDecision(decisionRef.resolve(), context) - .map(maybeWrapResult(decisionRef, _))) + mapEither[ParsedDecisionReference, (String, Val)](requiredDecisions, { + case ImportedDecision(namespace, decisionId, importName) => + repository.getDecision(namespace = namespace, decisionId = decisionId) + .flatMap(evalDecision(_, context)) + .map { case (name, result) => + importName -> ValContext(StaticContext( + variables = Map(name -> result), + functions = Map.empty + )) + } + + case ParsedDecisionFailure(_, _, failureMessage) => Left(Failure(failureMessage)) + case decision: EmbeddedDecision => evalDecision(decision, context) + } + ) } private def evalRequiredKnowledge( requiredBkms: Iterable[ParsedBusinessKnowledgeModelReference], context: EvalContext): Either[Failure, List[(String, Val)]] = { - mapEither(requiredBkms, - (bkmRef: ParsedBusinessKnowledgeModelReference) => evalBkm(bkmRef.resolve(), context) - .map(maybeWrapResult(bkmRef, _))) - } + mapEither[ParsedBusinessKnowledgeModelReference, (String, Val)](requiredBkms, { + case ImportedBusinessKnowledgeModel(namespace, id, importName) => + repository.getBusinessKnowledgeModel(namespace = namespace, bkmId = id) + .flatMap(evalBkm(_, context)) + .map { case (name, resultFunction) => + importName -> ValContext( + StaticContext( + variables = Map.empty, + functions = Map(name -> List(resultFunction)) + ) + ) + } - private def maybeWrapResult( - reference: ParsedDecisionLogicContainerReference[_], result: (String, Val)) = - reference.importedModelName match { - case Some(importName) => - val ctx = result._2 match { - case func: ValFunction => StaticContext( - variables = Map.empty, - functions = Map(result._1 -> List(func)) - ) - case _ => StaticContext( - variables = Map(result._1 -> result._2), - functions = Map.empty - ) + case ParsedBusinessKnowledgeModelFailure(_, _, failureMessage) => Left(Failure(failureMessage)) + case bkm: EmbeddedBusinessKnowledgeModel => evalBkm(bkm, context) } - importName -> ValContext(ctx) - case _ => result - } + ) + } + } diff --git a/src/main/scala/org/camunda/dmn/evaluation/InvocationEvaluator.scala b/src/main/scala/org/camunda/dmn/evaluation/InvocationEvaluator.scala index c8f1a298..959107fd 100644 --- a/src/main/scala/org/camunda/dmn/evaluation/InvocationEvaluator.scala +++ b/src/main/scala/org/camunda/dmn/evaluation/InvocationEvaluator.scala @@ -17,29 +17,35 @@ package org.camunda.dmn.evaluation import org.camunda.dmn.DmnEngine._ import org.camunda.dmn.FunctionalHelper._ -import org.camunda.dmn.parser.{ - ParsedBusinessKnowledgeModel, - ParsedExpression, - ParsedInvocation -} +import org.camunda.dmn.parser.{DmnRepository, EmbeddedBusinessKnowledgeModel, ImportedBusinessKnowledgeModel, ParsedBusinessKnowledgeModel, ParsedBusinessKnowledgeModelFailure, ParsedBusinessKnowledgeModelReference, ParsedExpression, ParsedInvocation} import org.camunda.feel.syntaxtree.Val class InvocationEvaluator( eval: (ParsedExpression, EvalContext) => Either[Failure, Val], - evalBkm: (ParsedBusinessKnowledgeModel, EvalContext) => Either[Failure, Val]) { + evalBkm: (ParsedBusinessKnowledgeModel, EvalContext) => Either[Failure, Val], + repository: DmnRepository) { def eval(invocation: ParsedInvocation, context: EvalContext): Either[Failure, Val] = { val result = evalParameters(invocation.bindings, context).flatMap { p => val ctx = context.copy(variables = context.variables ++ p.toMap) - evalBkm(invocation.invocation, ctx) + + resolveBkm(invocation.invocation).flatMap(evalBkm(_, ctx)) } context.audit(invocation, result) result } + private def resolveBkm(bkmRef: ParsedBusinessKnowledgeModelReference): Either[Failure, ParsedBusinessKnowledgeModel] = { + bkmRef match { + case ImportedBusinessKnowledgeModel(namespace, id, _) => repository.getBusinessKnowledgeModel(namespace = namespace, bkmId = id) + case ParsedBusinessKnowledgeModelFailure(_, _, failureMessage) => Left(Failure(failureMessage)) + case bkm: EmbeddedBusinessKnowledgeModel => Right(bkm) + } + } + private def evalParameters( bindings: Iterable[(String, ParsedExpression)], context: EvalContext): Either[Failure, List[(String, Any)]] = { diff --git a/src/main/scala/org/camunda/dmn/parser/DmnParser.scala b/src/main/scala/org/camunda/dmn/parser/DmnParser.scala index 3cac8558..3172f131 100644 --- a/src/main/scala/org/camunda/dmn/parser/DmnParser.scala +++ b/src/main/scala/org/camunda/dmn/parser/DmnParser.scala @@ -70,8 +70,7 @@ class DmnParser( configuration: Configuration, feelParser: String => Either[String, feel.syntaxtree.ParsedExpression], feelUnaryTestsParser: String => Either[String, - feel.syntaxtree.ParsedExpression], - dmnRepository: DmnRepository) { + feel.syntaxtree.ParsedExpression]) { import DmnParser._ @@ -145,8 +144,8 @@ class DmnParser( val parsedDmn = ParsedDmn( model = model, - decisions = ctx.decisions.values.filter(_.isEmbedded).map(_.resolve()), - bkms = ctx.bkms.values.filter(_.isEmbedded).map(_.resolve()), + decisions = ctx.decisions.values.collect{ case decision: ParsedDecision => decision }, + bkms = ctx.bkms.values.collect { case bkm: ParsedBusinessKnowledgeModel => bkm }, namespace = definitions.getNamespace) if (ctx.failures.isEmpty) { @@ -313,11 +312,11 @@ class DmnParser( } else { ctx.importedModels .find(importedModel => reference.namespace == importedModel.namespace) - .map(importedModel => ImportedDecision(dmnRepository, reference.namespace, reference.id, Some(importedModel.name))) + .map(importedModel => ImportedDecision(reference.namespace, reference.id, importedModel.name)) .getOrElse { val failure = Failure(s"No import found for namespace '${reference.namespace}'.") ctx.failures += failure - ParsedDecisionFailure(reference.id, reference.namespace, ExpressionFailure(failure.message)) + ParsedDecisionFailure(reference.id, reference.namespace, failure.message) } } } @@ -334,12 +333,11 @@ class DmnParser( } else { ctx.importedModels .find(importedModel => reference.namespace == importedModel.namespace) - .map(importedModel => ImportedBusinessKnowledgeModel( - dmnRepository, reference.namespace, reference.id, Some(importedModel.name))) + .map(importedModel => ImportedBusinessKnowledgeModel(reference.namespace, reference.id, importedModel.name)) .getOrElse { val failure = Failure(s"No import found for namespace '${reference.namespace}'.") ctx.failures += failure - ParsedBusinessKnowledgeModelFailure(reference.id, reference.namespace, ExpressionFailure(failure.message)) + ParsedBusinessKnowledgeModelFailure(reference.id, reference.namespace, failure.message) } } } @@ -552,7 +550,7 @@ class DmnParser( ctx.bkms .get(expression) .map(bkmRef => { - ParsedInvocation(bindings, bkmRef.resolve()) + ParsedInvocation(bindings, bkmRef) }) .getOrElse { ctx.failures += Failure(s"no BKM found with name '$expression'") diff --git a/src/main/scala/org/camunda/dmn/parser/ParsedDmn.scala b/src/main/scala/org/camunda/dmn/parser/ParsedDmn.scala index 47556693..6650cbeb 100644 --- a/src/main/scala/org/camunda/dmn/parser/ParsedDmn.scala +++ b/src/main/scala/org/camunda/dmn/parser/ParsedDmn.scala @@ -58,14 +58,10 @@ trait ParsedDecision extends ParsedDecisionLogicContainer { val requiredBkms: Iterable[ParsedBusinessKnowledgeModelReference] } -trait ParsedDecisionLogicContainerReference[T <: ParsedDecisionLogicContainer] { - val importedModelName: Option[String] = None - def resolve(): T +trait ParsedDecisionReference { def isEmbedded: Boolean - def isImported: Boolean = !isEmbedded -} -trait ParsedDecisionReference extends ParsedDecisionLogicContainerReference[ParsedDecision] { + def isImported: Boolean = !isEmbedded } case class EmbeddedDecision( @@ -77,26 +73,11 @@ case class EmbeddedDecision( requiredDecisions: Iterable[ParsedDecisionReference], requiredBkms: Iterable[ParsedBusinessKnowledgeModelReference] ) extends ParsedDecision with ParsedDecisionReference { - override def resolve(): ParsedDecision = this override def isEmbedded: Boolean = true } -trait ImportedParsedDecisionLogicFailure[T <: ParsedDecisionLogicContainer] - extends ParsedDecisionLogicContainerReference[T] { - val id: String - val namespace: String - val expressionFailure: ExpressionFailure - override def resolve(): T = throw new RuntimeException(expressionFailure.failure) - - override def isEmbedded: Boolean = false -} - -case class ImportedDecision(repository: DmnRepository, namespace: String, id: String, override val importedModelName: Option[String]) extends ParsedDecisionReference { - override def resolve(): ParsedDecision = repository.getDecision(namespace, id) match { - case Right(found) => found - case Left(failure) => ParsedDecisionFailure(id, namespace, ExpressionFailure(failure.message)).resolve() - } +case class ImportedDecision(namespace: String, id: String, importedModelName: String) extends ParsedDecisionReference { override def isEmbedded: Boolean = false @@ -107,7 +88,11 @@ sealed trait ParsedBusinessKnowledgeModel extends ParsedDecisionLogicContainer { val requiredBkms: Iterable[ParsedBusinessKnowledgeModelReference] } -trait ParsedBusinessKnowledgeModelReference extends ParsedDecisionLogicContainerReference[ParsedBusinessKnowledgeModel] +trait ParsedBusinessKnowledgeModelReference { + def isEmbedded: Boolean + + def isImported: Boolean = !isEmbedded +} case class EmbeddedBusinessKnowledgeModel( @@ -118,36 +103,28 @@ case class EmbeddedBusinessKnowledgeModel( requiredBkms: Iterable[ParsedBusinessKnowledgeModelReference]) extends ParsedBusinessKnowledgeModel with ParsedBusinessKnowledgeModelReference { - override def resolve(): ParsedBusinessKnowledgeModel = this - override def isEmbedded: Boolean = true } -case class ImportedBusinessKnowledgeModel( - repository: DmnRepository, - namespace: String, id: String, - override val importedModelName: Option[String]) extends ParsedBusinessKnowledgeModelReference { - override def resolve(): ParsedBusinessKnowledgeModel = repository.getBusinessKnowledgeModel(namespace, id) match { - case Right(found) => found - case Left(failure) => - ParsedBusinessKnowledgeModelFailure(id, namespace, ExpressionFailure(failure.message)).resolve() - } +case class ImportedBusinessKnowledgeModel(namespace: String, id: String, importedModelName: String) extends ParsedBusinessKnowledgeModelReference { override def isEmbedded: Boolean = false } -case class ParsedBusinessKnowledgeModelFailure(id: String, namespace: String, expressionFailure: ExpressionFailure) - extends ImportedParsedDecisionLogicFailure[ParsedBusinessKnowledgeModel] - with ParsedBusinessKnowledgeModelReference +case class ParsedBusinessKnowledgeModelFailure(id: String, namespace: String, failureMessage: String) + extends ParsedBusinessKnowledgeModelReference { + override def isEmbedded: Boolean = false +} -case class ParsedDecisionFailure(id: String, namespace: String, expressionFailure: ExpressionFailure) - extends ImportedParsedDecisionLogicFailure[ParsedDecision] - with ParsedDecisionReference +case class ParsedDecisionFailure(id: String, namespace: String, failureMessage: String) + extends ParsedDecisionReference { + override def isEmbedded: Boolean = false +} sealed trait ParsedDecisionLogic case class ParsedInvocation(bindings: Iterable[(String, ParsedExpression)], - invocation: ParsedBusinessKnowledgeModel) + invocation: ParsedBusinessKnowledgeModelReference) extends ParsedDecisionLogic case class ParsedContext(entries: Iterable[(String, ParsedDecisionLogic)],