From c916f04e375fe6116fa12cd0af3aaf4de676b8d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Wo=C5=BAniak?= Date: Tue, 1 Jan 2019 18:23:12 +0200 Subject: [PATCH 01/10] Created initial implementation of creating model from OMG DMN 1.1 XML. Resolved #45 --- .gitignore | 3 +- README.md | 12 +- .../evaluator/decision/DecisionEvaluator.java | 12 + .../dmn/engine/model/decision/HitPolicy.java | 5 +- .../engine/reader/DecisionReadException.java | 4 + .../dmn/engine/reader/DecisionReader.java | 11 +- .../dmn/io/xml/XMLDecisionConverter.java | 278 ++++ .../dmn/io/xml/XmlDecisionReader.java | 116 ++ .../dmn/io/xml/model/XMLDecision.java | 54 + .../dmn/io/xml/model/XMLDecisionTable.java | 57 + .../dmn/io/xml/model/XMLDefinitions.java | 40 + .../dmn/io/xml/model/XMLExpression.java | 39 + .../powerflows/dmn/io/xml/model/XMLInput.java | 50 + .../dmn/io/xml/model/XMLInputEntry.java | 40 + .../dmn/io/xml/model/XMLInputValues.java | 38 + .../dmn/io/xml/model/XMLOutput.java | 46 + .../dmn/io/xml/model/XMLOutputEntry.java | 41 + .../dmn/io/xml/model/XMLOutputValues.java | 38 + .../powerflows/dmn/io/xml/model/XMLRule.java | 46 + .../dmn/io/xml/model/package-info.java | 23 + .../dmn/io/yaml/YamlDecisionReader.java | 23 +- ...ineConfigurationReferenceSingleSpec.groovy | 2 +- .../decision/DecisionEvaluatorSpec.groovy | 19 + .../io/xml/XMLDecisionConverterSpec.groovy | 32 + .../dmn/io/xml/XmlDecisionReaderSpec.groovy | 291 ++++ .../dmn/io/yaml/YamlDecisionReaderSpec.groovy | 31 +- .../dmn/io/yaml/YamlDecisionWriterSpec.groovy | 2 +- ...unda-dmn-1.1-example-bad-input-columns.dmn | 64 + ...nda-dmn-1.1-example-bad-output-columns.dmn | 64 + .../camunda-dmn-1.1-example-duplicate-ids.dmn | 97 ++ .../dmn/io/xml/camunda-dmn-1.1-example.dmn | 101 ++ .../dmn/io/xml/omg-dmn-1.1-example.dmn | 1381 +++++++++++++++++ 32 files changed, 3023 insertions(+), 37 deletions(-) create mode 100644 src/main/java/org/powerflows/dmn/io/xml/XMLDecisionConverter.java create mode 100644 src/main/java/org/powerflows/dmn/io/xml/XmlDecisionReader.java create mode 100644 src/main/java/org/powerflows/dmn/io/xml/model/XMLDecision.java create mode 100644 src/main/java/org/powerflows/dmn/io/xml/model/XMLDecisionTable.java create mode 100644 src/main/java/org/powerflows/dmn/io/xml/model/XMLDefinitions.java create mode 100644 src/main/java/org/powerflows/dmn/io/xml/model/XMLExpression.java create mode 100644 src/main/java/org/powerflows/dmn/io/xml/model/XMLInput.java create mode 100644 src/main/java/org/powerflows/dmn/io/xml/model/XMLInputEntry.java create mode 100644 src/main/java/org/powerflows/dmn/io/xml/model/XMLInputValues.java create mode 100644 src/main/java/org/powerflows/dmn/io/xml/model/XMLOutput.java create mode 100644 src/main/java/org/powerflows/dmn/io/xml/model/XMLOutputEntry.java create mode 100644 src/main/java/org/powerflows/dmn/io/xml/model/XMLOutputValues.java create mode 100644 src/main/java/org/powerflows/dmn/io/xml/model/XMLRule.java create mode 100644 src/main/java/org/powerflows/dmn/io/xml/model/package-info.java create mode 100644 src/test/groovy/org/powerflows/dmn/io/xml/XMLDecisionConverterSpec.groovy create mode 100644 src/test/groovy/org/powerflows/dmn/io/xml/XmlDecisionReaderSpec.groovy create mode 100644 src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example-bad-input-columns.dmn create mode 100644 src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example-bad-output-columns.dmn create mode 100644 src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example-duplicate-ids.dmn create mode 100644 src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example.dmn create mode 100644 src/test/resources/org/powerflows/dmn/io/xml/omg-dmn-1.1-example.dmn diff --git a/.gitignore b/.gitignore index 546a1e0..5f4de73 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ # Others target/ .settings/ -bin/ \ No newline at end of file +bin/ +/.attach_pid* diff --git a/README.md b/README.md index 473f3d5..6f7e22a 100644 --- a/README.md +++ b/README.md @@ -345,7 +345,8 @@ Decision decision = Decision.builder() # IO Thanks to IO module there is a possibility to: -* Read decisions from *.yml files; +* Read decisions from _powerflows_ *.yml files; +* Read decisions from DMN 1.1 *.xml files; * Write decisions to *.yml files. ## Reading @@ -354,8 +355,15 @@ First of all input stream is needed. Then, using YamlDecisionReader class a deve ```java InputStream inputStream = this.class.getResourceAsStream("sample-decision.yml"); -Decision result = new YamlDecisionReader().read(inputStream); +Optional result = new YamlDecisionReader().read(inputStream); ``` +Another source of Decision may be OMG defined DMN 1.1 compatible XML file. + +```java +InputStream inputStream = this.class.getResourceAsStream("sample-decision.xml"); +List result = new XMLDecisionReader.readAll(inputStream); +``` +Currently only reading of decision tables from _decision_ tags is supported. ## Writing The IO module can be used to conversion between different formats. For now the only one supported is *.yml. diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/decision/DecisionEvaluator.java b/src/main/java/org/powerflows/dmn/engine/evaluator/decision/DecisionEvaluator.java index c2747cd..76f9702 100644 --- a/src/main/java/org/powerflows/dmn/engine/evaluator/decision/DecisionEvaluator.java +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/decision/DecisionEvaluator.java @@ -32,14 +32,18 @@ import org.powerflows.dmn.engine.model.evaluation.variable.DecisionVariables; import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @Slf4j public class DecisionEvaluator { + private static final Set UNSUPPORTED_HIT_POLICIES = Collections.unmodifiableSet(EnumSet.of(HitPolicy.OUTPUT_ORDER, HitPolicy.RULE_ORDER, HitPolicy.PRIORITY)); private final RuleEvaluator ruleEvaluator; public DecisionEvaluator(RuleEvaluator ruleEvaluator) { @@ -55,6 +59,10 @@ public DecisionResult evaluate(final Decision decision, final DecisionVariables throw new NullPointerException("Decision variables can not be null"); } + if (isUnsupportedSupportedHitPolicy(decision.getHitPolicy())) { + throw new UnsupportedOperationException("HitPolicy " + decision.getHitPolicy() + " is not supported"); + } + log.info("Starting evaluation of decision: {} with decision variables: {}", decision, decisionVariables); validateDecisionVariables(decision.getInputs(), decisionVariables); @@ -97,6 +105,10 @@ public DecisionResult evaluate(final Decision decision, final DecisionVariables return decisionResult; } + private boolean isUnsupportedSupportedHitPolicy(HitPolicy hitPolicy) { + return UNSUPPORTED_HIT_POLICIES.contains(hitPolicy); + } + private void validateDecisionVariables(final List inputs, final DecisionVariables decisionVariables) { final String invalidInputNames = inputs .stream() diff --git a/src/main/java/org/powerflows/dmn/engine/model/decision/HitPolicy.java b/src/main/java/org/powerflows/dmn/engine/model/decision/HitPolicy.java index 0bd7165..f332a0a 100644 --- a/src/main/java/org/powerflows/dmn/engine/model/decision/HitPolicy.java +++ b/src/main/java/org/powerflows/dmn/engine/model/decision/HitPolicy.java @@ -20,5 +20,8 @@ public enum HitPolicy { UNIQUE, FIRST, ANY, - COLLECT + COLLECT, + PRIORITY, + RULE_ORDER, + OUTPUT_ORDER } diff --git a/src/main/java/org/powerflows/dmn/engine/reader/DecisionReadException.java b/src/main/java/org/powerflows/dmn/engine/reader/DecisionReadException.java index 1e49c32..b224923 100644 --- a/src/main/java/org/powerflows/dmn/engine/reader/DecisionReadException.java +++ b/src/main/java/org/powerflows/dmn/engine/reader/DecisionReadException.java @@ -20,4 +20,8 @@ public class DecisionReadException extends RuntimeException { public DecisionReadException(final String message, final Throwable cause) { super(message, cause); } + + public DecisionReadException(final String message) { + super(message); + } } \ No newline at end of file diff --git a/src/main/java/org/powerflows/dmn/engine/reader/DecisionReader.java b/src/main/java/org/powerflows/dmn/engine/reader/DecisionReader.java index f6db717..2adcb83 100644 --- a/src/main/java/org/powerflows/dmn/engine/reader/DecisionReader.java +++ b/src/main/java/org/powerflows/dmn/engine/reader/DecisionReader.java @@ -21,18 +21,13 @@ import java.io.InputStream; import java.util.List; +import java.util.Optional; public interface DecisionReader { - default Decision read(final String decisionId) { - throw new UnsupportedOperationException(); - } + Optional read(InputStream inputStream); - default List readAll(final Iterable decisionIds) { - throw new UnsupportedOperationException(); - } - - Decision read(InputStream inputStream); + Optional read(InputStream inputStream, String decisionId); List readAll(InputStream inputStream); diff --git a/src/main/java/org/powerflows/dmn/io/xml/XMLDecisionConverter.java b/src/main/java/org/powerflows/dmn/io/xml/XMLDecisionConverter.java new file mode 100644 index 0000000..44326432 --- /dev/null +++ b/src/main/java/org/powerflows/dmn/io/xml/XMLDecisionConverter.java @@ -0,0 +1,278 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.io.xml; + +import lombok.extern.slf4j.Slf4j; +import org.powerflows.dmn.engine.model.decision.Decision; +import org.powerflows.dmn.engine.model.decision.HitPolicy; +import org.powerflows.dmn.engine.model.decision.expression.ExpressionType; +import org.powerflows.dmn.engine.model.decision.field.ValueType; +import org.powerflows.dmn.engine.model.decision.rule.Rule; +import org.powerflows.dmn.engine.reader.DecisionReadException; +import org.powerflows.dmn.io.DecisionToExternalModelConverter; +import org.powerflows.dmn.io.xml.model.XMLDecision; +import org.powerflows.dmn.io.xml.model.XMLInput; +import org.powerflows.dmn.io.xml.model.XMLInputEntry; +import org.powerflows.dmn.io.xml.model.XMLOutput; +import org.powerflows.dmn.io.xml.model.XMLOutputEntry; +import org.powerflows.dmn.io.xml.model.XMLRule; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; + +@Slf4j +public class XMLDecisionConverter implements DecisionToExternalModelConverter { + private static final Map OMG_HITPOLICY_MAPPING; + + static { + final Map mapping = new HashMap<>(); + mapping.put("UNIQUE", HitPolicy.UNIQUE); + mapping.put("FIRST", HitPolicy.FIRST); + mapping.put("PRIORITY", HitPolicy.PRIORITY); + mapping.put("ANY", HitPolicy.ANY); + mapping.put("COLLECT", HitPolicy.COLLECT); + mapping.put("RULE ORDER", HitPolicy.RULE_ORDER); + mapping.put("OUTPUT ORDER", HitPolicy.OUTPUT_ORDER); + + OMG_HITPOLICY_MAPPING = Collections.unmodifiableMap(mapping); + } + + private static final Map EXPRESSION_TYPE_MAPPING; + + static { + final Map mapping = new HashMap<>(); + mapping.put("feel", ExpressionType.FEEL); + mapping.put("groovy", ExpressionType.GROOVY); + mapping.put("javascript", ExpressionType.JAVASCRIPT); + mapping.put("juel", ExpressionType.JUEL); + + EXPRESSION_TYPE_MAPPING = Collections.unmodifiableMap(mapping); + } + + private final HitPolicy defaultHitPolicy; + private final ExpressionType defaultExpressionType; + + public XMLDecisionConverter() { + this.defaultHitPolicy = HitPolicy.UNIQUE; + this.defaultExpressionType = ExpressionType.FEEL; + } + + @Override + public XMLDecision to(final Decision decision) { + throw new UnsupportedOperationException("Serialization is not supported"); + } + + @Override + public Decision from(final XMLDecision xmlDecision) { + final Decision.Builder builder = Decision.builder() + .id(xmlDecision.getId()) + .name(xmlDecision.getName()) + .hitPolicy(makeHitPolicy(xmlDecision)); + + processRules(builder, + xmlDecision.getDecisionTable().getInputs(), + xmlDecision.getDecisionTable().getOutputs(), + xmlDecision.getDecisionTable().getRules()); + + return builder.build(); + } + + private Supplier makeSequenceNameSupplier(final String prefix) { + return new Supplier() { + private int count = 0; + + @Override + public String get() { + return prefix + count++; + } + + @Override + public String toString() { + return prefix + count; + } + }; + } + + private void processRules(final Decision.Builder builder, final List inputs, final List outputs, final List rules) { + final Set inputNames = new LinkedHashSet<>(); + final Supplier inputNameSequence = makeSequenceNameSupplier("input_"); + inputs.forEach(input -> { + final String name = selectOrCreateUniqueName(inputNames, input.getId(), null, inputNameSequence); + + builder.withInput(inputBuilder -> + inputBuilder + .description(input.getLabel()) + .name(name) + .withExpression(expressionBuilder -> + expressionBuilder + .value(input.getInputExpression().getText()) + .type(resolveExpressionType(input.getInputExpression().getExpressionLanguage())) + .build()) + .type(resolveType(input.getInputExpression().getTypeRef())) + .build() + ); + }); + + final Set outputNames = new LinkedHashSet<>(); + final Supplier outputNameSequence = makeSequenceNameSupplier("output_"); + + outputs.forEach(output -> { + final String name = selectOrCreateUniqueName(outputNames, output.getId(), output.getName(), outputNameSequence); + + builder.withOutput(outputBuilder -> + outputBuilder + .description(output.getLabel()) + .type(resolveType(output.getTypeRef())) + .name(name) + .build() + ); + }); + + rules.forEach(this.ruleProcessor(new ArrayList<>(inputNames), new ArrayList<>(outputNames), builder)); + } + + private String selectOrCreateUniqueName(final Set names, final String id, final String name, final Supplier nameSequence) { + final String resultName; + + if (name != null) { + if (names.contains(name)) { + log.warn("Names collection {} already contains name {}, using one from sequence {}", names, name, nameSequence); + resultName = getNextUniqueName(nameSequence, names); + } else { + resultName = name.replaceAll("\\s", "_"); + } + } else if (id != null) { + if (names.contains(id)) { + throw new DecisionReadException("Non unique element id: " + id); + } + resultName = id; + } else { + resultName = getNextUniqueName(nameSequence, names); + } + names.add(resultName); + + return resultName; + } + + private String getNextUniqueName(final Supplier nameSequence, final Set names) { + while (true) { + final String name = nameSequence.get(); + if (!names.contains(name)) { + return name; + } + } + } + + private Consumer ruleProcessor(final List inputNames, final List outputNames, final Decision.Builder builder) { + log.debug("Created rule processor for inputs: {}, and outputs: {}", inputNames, outputNames); + + return rule -> { + log.debug("Processing rule: {}" + rule); + + builder.withRule(ruleBuilder -> { + processInputEntries(rule.getInputEntries(), inputNames, rule.getId(), ruleBuilder); + processOutputEntries(rule.getOutputEntries(), outputNames, rule.getId(), ruleBuilder); + + return ruleBuilder + .description(rule.getDescription()) + .build(); + }); + }; + } + + private void processOutputEntries(final List outputEntries, final List outputNames, final String id, final Rule.Builder ruleBuilder) { + if (outputEntries.size() != outputNames.size()) { + throw new DecisionReadException("Invalid number of outputs in rule " + id); + } + + for (int idx = 0; idx < outputEntries.size(); idx++) { + XMLOutputEntry outputEntry = outputEntries.get(idx); + if (isNotBlank(outputEntry.getExpression())) { + final String name = outputNames.get(idx); + ruleBuilder.withOutputEntry(entryBuilder -> entryBuilder + .name(name) + .withExpression(expressionBuilder -> + expressionBuilder + .type(resolveExpressionType(outputEntry.getExpressionLanguage())) + .value(outputEntry.getExpression()) + .build()) + .build()); + } + } + } + + + private void processInputEntries(final List inputEntries, final List inputNames, final String id, final Rule.Builder ruleBuilder) { + if (inputEntries.size() != inputNames.size()) { + throw new DecisionReadException("Invalid number of inputs in rule " + id); + } + for (int idx = 0; idx < inputEntries.size(); idx++) { + XMLInputEntry outputEntry = inputEntries.get(idx); + if (isNotBlank(outputEntry.getExpression())) { + final String name = inputNames.get(idx); + ruleBuilder.withInputEntry(entryBuilder -> entryBuilder + .name(name) + .withExpression(expressionBuilder -> + expressionBuilder + .type(resolveExpressionType(outputEntry.getExpressionLanguage())) + .value(outputEntry.getExpression()) + .build()) + .build()); + } + } + } + + private ExpressionType resolveExpressionType(final String expressionLanguage) { + if (expressionLanguage == null) { + return defaultExpressionType; + } else { + return EXPRESSION_TYPE_MAPPING.getOrDefault(expressionLanguage.toLowerCase(), defaultExpressionType); + } + } + + private boolean isNotBlank(final String text) { + return text != null && !text.trim().isEmpty(); + } + + private ValueType resolveType(final String typeRef) { + if (typeRef == null) { + return null; + } else { + return Arrays.stream(ValueType.values()) + .filter(v -> v.name().equalsIgnoreCase(typeRef)) + .findFirst() + .orElseGet(() -> { + log.debug("Unable to resolve typeRef: {} to Powerflows Type", typeRef); + + return null; + }); + } + } + + private HitPolicy makeHitPolicy(final XMLDecision externalModel) { + final String hitPolicy = externalModel.getDecisionTable().getHitPolicy(); + + return OMG_HITPOLICY_MAPPING.getOrDefault(hitPolicy, defaultHitPolicy); + } +} \ No newline at end of file diff --git a/src/main/java/org/powerflows/dmn/io/xml/XmlDecisionReader.java b/src/main/java/org/powerflows/dmn/io/xml/XmlDecisionReader.java new file mode 100644 index 0000000..aa35a56 --- /dev/null +++ b/src/main/java/org/powerflows/dmn/io/xml/XmlDecisionReader.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.io.xml; + +import lombok.extern.slf4j.Slf4j; +import org.powerflows.dmn.engine.model.decision.Decision; +import org.powerflows.dmn.engine.reader.DecisionReadException; +import org.powerflows.dmn.engine.reader.DecisionReader; +import org.powerflows.dmn.io.xml.model.XMLDecision; +import org.powerflows.dmn.io.xml.model.XMLDefinitions; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.JAXBIntrospector; +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.helpers.DefaultValidationEventHandler; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +@Slf4j +public class XmlDecisionReader implements DecisionReader { + + private final XMLInputFactory factory; + private final JAXBContext decisionContext; + private final XMLDecisionConverter converter = new XMLDecisionConverter(); + private final boolean strict; + + public XmlDecisionReader() { + this(false); + } + + public XmlDecisionReader(final boolean strict) { + this.factory = XMLInputFactory.newInstance(); + this.strict = strict; + + try { + this.decisionContext = JAXBContext.newInstance(XMLDefinitions.class); + } catch (JAXBException e) { + throw new DecisionReadException("Unable to instantiate JAXB context", e); + } + } + + @Override + public Optional read(final InputStream inputStream) { + return readAll(inputStream).stream().findFirst(); + } + + @Override + public Optional read(final InputStream inputStream, final String decisionId) { + return readAll(inputStream).stream().filter(decision -> Objects.equals(decision.getId(), decisionId)).findFirst(); + } + + @Override + public List readAll(final InputStream inputStream) { + try { + final List results = new ArrayList<>(); + doReadDecisionsStream(factory.createXMLStreamReader(inputStream), results::add); + + return convert(results); + } catch (XMLStreamException | JAXBException e) { + throw new DecisionReadException("Unable to read decision", e); + } + } + + private List convert(final List results) { + return results.stream() + .filter(d -> d.getDecisionTable() != null) + .map(converter::from) + .collect(Collectors.toList()); + } + + private Unmarshaller getUnmarshaller() { + try { + final Unmarshaller decisionUnmarshaller = decisionContext.createUnmarshaller(); + if (strict) { + decisionUnmarshaller.setEventHandler(new DefaultValidationEventHandler()); + } + + return decisionUnmarshaller; + } catch (JAXBException e) { + throw new DecisionReadException("Error creating unmarshaller", e); + } + } + + private void doReadDecisionsStream(final XMLStreamReader r, + final Consumer decisionConsumer) throws XMLStreamException, JAXBException { + final Unmarshaller decisionUnmarshaller = getUnmarshaller(); + final XMLDefinitions definitions = (XMLDefinitions) JAXBIntrospector + .getValue(decisionUnmarshaller.unmarshal(r, XMLDefinitions.class)); + log.trace("Read XML document: {}", definitions); + definitions.getDecisions().forEach(decisionConsumer); + + r.close(); + } +} \ No newline at end of file diff --git a/src/main/java/org/powerflows/dmn/io/xml/model/XMLDecision.java b/src/main/java/org/powerflows/dmn/io/xml/model/XMLDecision.java new file mode 100644 index 0000000..6d3cf44 --- /dev/null +++ b/src/main/java/org/powerflows/dmn/io/xml/model/XMLDecision.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.io.xml.model; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAnyAttribute; +import javax.xml.bind.annotation.XmlAnyElement; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.namespace.QName; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlRootElement(name = "decision", namespace = "http://www.omg.org/spec/DMN/20151101/dmn.xsd") +@Data +@NoArgsConstructor +public class XMLDecision { + + @XmlAttribute + private String name; + + @XmlAttribute + private String id; + + @XmlElement + private XMLDecisionTable decisionTable; + + @XmlAnyAttribute + private Map anyAttributes = new HashMap<>(); + + @XmlAnyElement(lax = true) + private List anyElements = new ArrayList<>(); +} diff --git a/src/main/java/org/powerflows/dmn/io/xml/model/XMLDecisionTable.java b/src/main/java/org/powerflows/dmn/io/xml/model/XMLDecisionTable.java new file mode 100644 index 0000000..b625215 --- /dev/null +++ b/src/main/java/org/powerflows/dmn/io/xml/model/XMLDecisionTable.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.io.xml.model; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAnyAttribute; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.namespace.QName; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlRootElement(name = "decisionTable", namespace = "http://www.omg.org/spec/DMN/20151101/dmn.xsd") +@Data +@NoArgsConstructor +public class XMLDecisionTable { + + @XmlAttribute + private String hitPolicy; + + @XmlAttribute + private String id; + + @XmlAnyAttribute + private Map anyAttributes = new HashMap<>(); + + @XmlElement(name = "input") + private List inputs = new ArrayList<>(); + + @XmlElement(name = "output") + private List outputs = new ArrayList<>(); + + @XmlElement(name = "rule") + private List rules = new ArrayList<>(); +} \ No newline at end of file diff --git a/src/main/java/org/powerflows/dmn/io/xml/model/XMLDefinitions.java b/src/main/java/org/powerflows/dmn/io/xml/model/XMLDefinitions.java new file mode 100644 index 0000000..da9e0e1 --- /dev/null +++ b/src/main/java/org/powerflows/dmn/io/xml/model/XMLDefinitions.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.io.xml.model; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAnyElement; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.ArrayList; +import java.util.List; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlRootElement(name = "definitions", namespace = "http://www.omg.org/spec/DMN/20151101/dmn.xsd") +@Data +@NoArgsConstructor +public class XMLDefinitions { + + @XmlElement(name = "decision") + private List decisions = new ArrayList<>(); + + @XmlAnyElement(lax = true) + private List anyElements = new ArrayList<>(); +} diff --git a/src/main/java/org/powerflows/dmn/io/xml/model/XMLExpression.java b/src/main/java/org/powerflows/dmn/io/xml/model/XMLExpression.java new file mode 100644 index 0000000..80f3888 --- /dev/null +++ b/src/main/java/org/powerflows/dmn/io/xml/model/XMLExpression.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.io.xml.model; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; + +@XmlAccessorType(XmlAccessType.FIELD) +@Data +@NoArgsConstructor +public class XMLExpression { + @XmlAttribute + private String id; + + @XmlAttribute + private String typeRef; + + @XmlAttribute + private String expressionLanguage; + + private String text; +} diff --git a/src/main/java/org/powerflows/dmn/io/xml/model/XMLInput.java b/src/main/java/org/powerflows/dmn/io/xml/model/XMLInput.java new file mode 100644 index 0000000..bf9e2c4 --- /dev/null +++ b/src/main/java/org/powerflows/dmn/io/xml/model/XMLInput.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.io.xml.model; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.JAXBElement; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAnyAttribute; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.namespace.QName; +import java.util.Map; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlRootElement(name = "input", namespace = "http://www.omg.org/spec/DMN/20151101/dmn.xsd") +@Data +@NoArgsConstructor +public class XMLInput { + @XmlAttribute + private String id; + + @XmlAttribute + private String label; + + @XmlElement + private XMLInputValues inputValues; + + @XmlElement + private XMLExpression inputExpression; + + @XmlAnyAttribute + private Map anyAttributes; +} diff --git a/src/main/java/org/powerflows/dmn/io/xml/model/XMLInputEntry.java b/src/main/java/org/powerflows/dmn/io/xml/model/XMLInputEntry.java new file mode 100644 index 0000000..6b8d7c9 --- /dev/null +++ b/src/main/java/org/powerflows/dmn/io/xml/model/XMLInputEntry.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.io.xml.model; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlRootElement(name = "input", namespace = "http://www.omg.org/spec/DMN/20151101/dmn.xsd") +@Data +@NoArgsConstructor +public class XMLInputEntry { + @XmlAttribute + private String id; + + @XmlAttribute + private String expressionLanguage; + + @XmlElement(name = "text") + private String expression; +} diff --git a/src/main/java/org/powerflows/dmn/io/xml/model/XMLInputValues.java b/src/main/java/org/powerflows/dmn/io/xml/model/XMLInputValues.java new file mode 100644 index 0000000..6cc7925 --- /dev/null +++ b/src/main/java/org/powerflows/dmn/io/xml/model/XMLInputValues.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.io.xml.model; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlRootElement(name = "inputValues", namespace = "http://www.omg.org/spec/DMN/20151101/dmn.xsd") +@Data +@NoArgsConstructor +public class XMLInputValues { + + @XmlAttribute + private String expressionLanguage; + + @XmlElement(name = "text") + private String values; +} diff --git a/src/main/java/org/powerflows/dmn/io/xml/model/XMLOutput.java b/src/main/java/org/powerflows/dmn/io/xml/model/XMLOutput.java new file mode 100644 index 0000000..99e963e --- /dev/null +++ b/src/main/java/org/powerflows/dmn/io/xml/model/XMLOutput.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.io.xml.model; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlRootElement(name = "output", namespace = "http://www.omg.org/spec/DMN/20151101/dmn.xsd") +@Data +@NoArgsConstructor +public class XMLOutput { + @XmlAttribute + private String id; + + @XmlAttribute + private String label; + + @XmlAttribute + private String typeRef; + + @XmlAttribute + private String name; + + @XmlElement + private XMLOutputValues outputValues; +} diff --git a/src/main/java/org/powerflows/dmn/io/xml/model/XMLOutputEntry.java b/src/main/java/org/powerflows/dmn/io/xml/model/XMLOutputEntry.java new file mode 100644 index 0000000..4878d3d --- /dev/null +++ b/src/main/java/org/powerflows/dmn/io/xml/model/XMLOutputEntry.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.io.xml.model; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlRootElement(name = "input", namespace = "http://www.omg.org/spec/DMN/20151101/dmn.xsd") +@Data +@NoArgsConstructor +public class XMLOutputEntry { + + @XmlAttribute + private String id; + + @XmlAttribute + private String expressionLanguage; + + @XmlElement(name = "text") + private String expression; +} diff --git a/src/main/java/org/powerflows/dmn/io/xml/model/XMLOutputValues.java b/src/main/java/org/powerflows/dmn/io/xml/model/XMLOutputValues.java new file mode 100644 index 0000000..56bb5b8 --- /dev/null +++ b/src/main/java/org/powerflows/dmn/io/xml/model/XMLOutputValues.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.io.xml.model; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlRootElement(name = "inputValues", namespace = "http://www.omg.org/spec/DMN/20151101/dmn.xsd") +@Data +@NoArgsConstructor +public class XMLOutputValues { + + @XmlAttribute + private String expressionLanguage; + + @XmlElement(name = "text") + private String values; +} diff --git a/src/main/java/org/powerflows/dmn/io/xml/model/XMLRule.java b/src/main/java/org/powerflows/dmn/io/xml/model/XMLRule.java new file mode 100644 index 0000000..fd5e38b --- /dev/null +++ b/src/main/java/org/powerflows/dmn/io/xml/model/XMLRule.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.io.xml.model; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.ArrayList; +import java.util.List; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlRootElement(name = "rule", namespace = "http://www.omg.org/spec/DMN/20151101/dmn.xsd") +@Data +@NoArgsConstructor +public class XMLRule { + @XmlAttribute + private String id; + + @XmlElement + private String description; + + @XmlElement(name = "inputEntry") + private List inputEntries = new ArrayList<>(); + + @XmlElement(name = "outputEntry") + private List outputEntries = new ArrayList<>(); + +} diff --git a/src/main/java/org/powerflows/dmn/io/xml/model/package-info.java b/src/main/java/org/powerflows/dmn/io/xml/model/package-info.java new file mode 100644 index 0000000..a1dc278 --- /dev/null +++ b/src/main/java/org/powerflows/dmn/io/xml/model/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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. + */ +@XmlSchema( + namespace = "http://www.omg.org/spec/DMN/20151101/dmn.xsd", + elementFormDefault = XmlNsForm.QUALIFIED) +package org.powerflows.dmn.io.xml.model; + + +import javax.xml.bind.annotation.XmlNsForm; +import javax.xml.bind.annotation.XmlSchema; \ No newline at end of file diff --git a/src/main/java/org/powerflows/dmn/io/yaml/YamlDecisionReader.java b/src/main/java/org/powerflows/dmn/io/yaml/YamlDecisionReader.java index e6df299..ce2bb37 100644 --- a/src/main/java/org/powerflows/dmn/io/yaml/YamlDecisionReader.java +++ b/src/main/java/org/powerflows/dmn/io/yaml/YamlDecisionReader.java @@ -24,6 +24,7 @@ import java.io.InputStream; import java.util.List; +import java.util.Optional; import java.util.stream.StreamSupport; import static java.util.stream.Collectors.toList; @@ -31,6 +32,7 @@ public class YamlDecisionReader implements DecisionReader { + public static final String UNABLE_TO_READ_FROM_STREAM = "Unable to read from stream"; private final Yaml yaml = new Yaml(new CustomConstructor()); private final YamlDecisionConverter converter = new YamlDecisionConverter(); @@ -42,16 +44,29 @@ public List readAll(final InputStream inputStream) { .map(converter::from) .collect(toList()); } catch (final Exception e) { - throw new DecisionReadException("Unable to read from stream", e); + throw new DecisionReadException(UNABLE_TO_READ_FROM_STREAM, e); } } @Override - public Decision read(final InputStream inputStream) { + public Optional read(final InputStream inputStream) { try { - return converter.from(yaml.load(inputStream)); + return Optional.of((YamlDecision) yaml.load(inputStream)).map(converter::from); } catch (final Exception e) { - throw new DecisionReadException("Unable to read from stream", e); + throw new DecisionReadException(UNABLE_TO_READ_FROM_STREAM, e); + } + } + + @Override + public Optional read(final InputStream inputStream, final String decisionId) { + try { + return StreamSupport.stream(yaml.loadAll(inputStream).spliterator(), true) + .map(o -> (YamlDecision) o) + .filter(d -> decisionId.equals(d.getId())) + .map(converter::from) + .findFirst(); + } catch (final Exception e) { + throw new DecisionReadException(UNABLE_TO_READ_FROM_STREAM, e); } } diff --git a/src/test/groovy/org/powerflows/dmn/engine/configuration/DefaultDecisionEngineConfigurationReferenceSingleSpec.groovy b/src/test/groovy/org/powerflows/dmn/engine/configuration/DefaultDecisionEngineConfigurationReferenceSingleSpec.groovy index b04f639..e4697f4 100644 --- a/src/test/groovy/org/powerflows/dmn/engine/configuration/DefaultDecisionEngineConfigurationReferenceSingleSpec.groovy +++ b/src/test/groovy/org/powerflows/dmn/engine/configuration/DefaultDecisionEngineConfigurationReferenceSingleSpec.groovy @@ -45,7 +45,7 @@ class DefaultDecisionEngineConfigurationReferenceSingleSpec extends Specificatio final String decisionFileName = 'reference-single.yml' final InputStream decisionInputStream = this.class.getResourceAsStream(decisionFileName) - decision = decisionReader.read(decisionInputStream) + decision = decisionReader.read(decisionInputStream).get() } void 'should configure default decision engine'() { diff --git a/src/test/groovy/org/powerflows/dmn/engine/evaluator/decision/DecisionEvaluatorSpec.groovy b/src/test/groovy/org/powerflows/dmn/engine/evaluator/decision/DecisionEvaluatorSpec.groovy index a0ac867..b344f07 100644 --- a/src/test/groovy/org/powerflows/dmn/engine/evaluator/decision/DecisionEvaluatorSpec.groovy +++ b/src/test/groovy/org/powerflows/dmn/engine/evaluator/decision/DecisionEvaluatorSpec.groovy @@ -64,6 +64,25 @@ class DecisionEvaluatorSpec extends Specification { exception.getMessage() == 'Decision variables can not be null' } + @Unroll + void 'should throw exception for unsupported hitPolicy #hitPolicy'(final HitPolicy hitPolicy) { + given: + final Decision decision = [hitPolicy: hitPolicy] as Decision + final DecisionVariables decisionContextVariables = [:] + + when: + decisionEvaluator.evaluate(decision, decisionContextVariables) + + then: + final UnsupportedOperationException exception = thrown() + exception != null + exception.getMessage() == "HitPolicy $hitPolicy is not supported" + + where: + hitPolicy << [HitPolicy.PRIORITY, HitPolicy.OUTPUT_ORDER, HitPolicy.RULE_ORDER] + } + + void 'should throw exception when unique result is expected but non-unique evaluated'() { given: final List inputs = []; diff --git a/src/test/groovy/org/powerflows/dmn/io/xml/XMLDecisionConverterSpec.groovy b/src/test/groovy/org/powerflows/dmn/io/xml/XMLDecisionConverterSpec.groovy new file mode 100644 index 0000000..7803a91 --- /dev/null +++ b/src/test/groovy/org/powerflows/dmn/io/xml/XMLDecisionConverterSpec.groovy @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.io.xml + +import org.powerflows.dmn.engine.model.decision.Decision +import spock.lang.Specification + +class XMLDecisionConverterSpec extends Specification { + void 'should fail on conversion to XML model'() { + given: + final XMLDecisionConverter converter = new XMLDecisionConverter() + + when: + converter.to(Decision.builder().build()) + + then: + thrown(UnsupportedOperationException) + } +} diff --git a/src/test/groovy/org/powerflows/dmn/io/xml/XmlDecisionReaderSpec.groovy b/src/test/groovy/org/powerflows/dmn/io/xml/XmlDecisionReaderSpec.groovy new file mode 100644 index 0000000..ae0e158 --- /dev/null +++ b/src/test/groovy/org/powerflows/dmn/io/xml/XmlDecisionReaderSpec.groovy @@ -0,0 +1,291 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.io.xml + +import org.powerflows.dmn.engine.model.decision.Decision +import org.powerflows.dmn.engine.model.decision.HitPolicy +import org.powerflows.dmn.engine.model.decision.expression.ExpressionType +import org.powerflows.dmn.engine.model.decision.field.ValueType +import org.powerflows.dmn.engine.reader.DecisionReadException +import spock.lang.Shared +import spock.lang.Specification + +class XmlDecisionReaderSpec extends Specification { + final static String omgExampleXml = 'omg-dmn-1.1-example.dmn' + final static String camundaExampleXml = 'camunda-dmn-1.1-example.dmn' + final static String camundaExampleXmlBadInputColumns = 'camunda-dmn-1.1-example-bad-input-columns.dmn' + final static String camundaExampleXmlBadOutputColumns = 'camunda-dmn-1.1-example-bad-output-columns.dmn' + final static String camundaExampleXmlDuplicateIds = 'camunda-dmn-1.1-example-duplicate-ids.dmn' + + @Shared + private XmlDecisionReader reader + + void setupSpec() { + reader = new XmlDecisionReader() + } + + void 'should fail on invalid XML'() { + given: + final String xml = ''' + + + + + + > 5 + + + + + ''' + + when: + reader.readAll(new ByteArrayInputStream(xml.getBytes())) + + then: + thrown(DecisionReadException) + } + + void 'should load minimal markup with empty data'() { + given: + final String xml = ''' + + + + + + ''' + + when: + final List result = reader.readAll(new ByteArrayInputStream(xml.getBytes())) + + then: + result?.size() == 1 + + and: + with(result[0]) { + getInputs().size() == 0 + getOutputs().size() == 0 + getRules().size() == 0 + getName() == 'Some Table Name' + getId() == 'some_table_id_1' + getHitPolicy() == HitPolicy.UNIQUE + } + } + + void 'should load minimal markup with empty data using strict mode and fail on unexpected element'() { + given: + final XmlDecisionReader decisionReader = new XmlDecisionReader(true) + final String xml = ''' + + + + + + + + ''' + + when: + decisionReader.readAll(new ByteArrayInputStream(xml.getBytes())) + + then: + thrown(DecisionReadException) + } + + void 'should read all decisions from OMG example stream'() { + given: + final InputStream inputStream = this.class.getResourceAsStream(omgExampleXml) + + when: + final List result = reader.readAll(inputStream) + + then: + result?.size() == 1 + } + + void 'should read single decision from Camunda example stream'() { + given: + final InputStream inputStream = this.class.getResourceAsStream(camundaExampleXml) + + when: + final Optional result = reader.read(inputStream) + + then: + result.isPresent() + result.ifPresent({ d -> assertFirstCamundaExampleContents(d) }) + } + + void 'should read single decision by id from Camunda example stream'() { + given: + final InputStream inputStream = this.class.getResourceAsStream(camundaExampleXml) + final String decisionId = 'Decision_0replv7' + + when: + final Optional result = reader.read(inputStream, decisionId) + + then: + result.isPresent() + result.ifPresent({ d -> assertSecondCamundaExampleContents(d) }) + } + + void 'should read all decisions from Camunda example stream'() { + given: + final InputStream inputStream = this.class.getResourceAsStream(camundaExampleXml) + + when: + final List result = reader.readAll(inputStream) + + then: + result?.size() == 2 + assertFirstCamundaExampleContents(result[0]) + } + + void 'should fail reading xml with wrong number of input entries vs inputs'() { + given: + final InputStream inputStream = this.class.getResourceAsStream(camundaExampleXmlBadInputColumns) + + when: + reader.readAll(inputStream) + + then: + thrown(DecisionReadException) + } + + void 'should fail reading xml with wrong number of output entries vs outputs'() { + given: + final InputStream inputStream = this.class.getResourceAsStream(camundaExampleXmlBadOutputColumns) + + when: + reader.readAll(inputStream) + + then: + thrown(DecisionReadException) + } + + void 'should fail reading xml with duplicate element ids'() { + given: + final InputStream inputStream = this.class.getResourceAsStream(camundaExampleXmlDuplicateIds) + + when: + reader.readAll(inputStream) + + then: + thrown(DecisionReadException) + } + + private void assertSecondCamundaExampleContents(final Decision result) { + with(result) { + getId() == 'Decision_0replv7' + getName() == 'Another Table' + getHitPolicy() == HitPolicy.UNIQUE + getInputs().size() == 1 + getOutputs().size() == 2 + getRules().size() == 1 + } + } + + private void assertFirstCamundaExampleContents(final Decision result) { + with(result) { + getId() == 'some_table_id_1' + getName() == 'Some Table Name' + getHitPolicy() == HitPolicy.FIRST + getInputs().size() == 3 + getOutputs().size() == 2 + getRules().size() == 2 + } + + with(result.getInputs()[0]) { + getName() == 'input_1' + getDescription() == 'Some Input 1 Description' + getType() == ValueType.INTEGER + getExpression().getType() == ExpressionType.FEEL + getExpression().getValue() == '> 5' + } + + with(result.getInputs()[1]) { + getName() == 'InputClause_03bkdz8' + getType() == ValueType.STRING + getExpression().getType() == ExpressionType.FEEL + getExpression().getValue() == '' //How do we handle lists of possible values? + } + + with(result.getInputs()[2]) { + getName() == 'InputClause_1beg16w' + getType() == ValueType.BOOLEAN + getExpression().getType() == ExpressionType.GROOVY + getExpression().getValue() == 'someInteger > 10' + } + + with(result.getOutputs()[0]) { + getName() == 'output_0' + getType() == ValueType.BOOLEAN + + } + + with(result.getOutputs()[1]) { + getName() == 'output_1' + getType() == ValueType.STRING + } + + with(result.getRules()[0]) { + getInputEntries().size() == 2 + getOutputEntries().size() == 1 + } + + with(result.getRules()[0].getInputEntries()[0]) { + getName() == 'input_1' + getExpression().getType() == ExpressionType.GROOVY + getExpression().getValue() == '> 20' + } + + with(result.getRules()[0].getInputEntries()[1]) { + getName() == 'InputClause_03bkdz8' + getExpression().getType() == ExpressionType.FEEL + getExpression().getValue() == 'not("blue", "purple")' + } + + with(result.getRules()[0].getOutputEntries()[0]) { + getName() == 'output_0' + getExpression().getType() == ExpressionType.GROOVY + getExpression().getValue() == 'someVariable1 || someVariable2' + } + + with(result.getRules()[1]) { + getInputEntries().size() == 2 + getOutputEntries().size() == 1 + } + + with(result.getRules()[1].getInputEntries()[0]) { + getName() == 'input_1' + getExpression().getType() == ExpressionType.FEEL + getExpression().getValue() == '5' + } + + with(result.getRules()[1].getInputEntries()[1]) { + getName() == 'InputClause_1beg16w' + getExpression().getType() == ExpressionType.FEEL + getExpression().getValue() == 'true' + } + + with(result.getRules()[1].getOutputEntries()[0]) { + getName() == 'output_1' + getExpression().getType() == ExpressionType.FEEL + getExpression().getValue() == '"The output"' + } + } +} \ No newline at end of file diff --git a/src/test/groovy/org/powerflows/dmn/io/yaml/YamlDecisionReaderSpec.groovy b/src/test/groovy/org/powerflows/dmn/io/yaml/YamlDecisionReaderSpec.groovy index 01ab831..51500fd 100644 --- a/src/test/groovy/org/powerflows/dmn/io/yaml/YamlDecisionReaderSpec.groovy +++ b/src/test/groovy/org/powerflows/dmn/io/yaml/YamlDecisionReaderSpec.groovy @@ -27,6 +27,7 @@ class YamlDecisionReaderSpec extends Specification { final String singleDecision = 'test-single-decision.yml' final String multipleDecisions = 'test-multiple-decisions.yml' final String decisionNamePrefix = 'sample_decision_' + final String decisionId = 'sample_decision_2' void 'should wrap exceptions in framework ones when reading single decision'() { given: @@ -39,38 +40,30 @@ class YamlDecisionReaderSpec extends Specification { thrown(DecisionReadException) } - void 'should not support read by id yet'() { - given: - final String id = 'id' - - when: - new YamlDecisionReader().read(id) - - then: - thrown(UnsupportedOperationException) - } - - void 'should not support readAll by ids yet'() { + void 'should read decision using InputStream'() { given: - final String id = 'id' + final InputStream inputStream = this.class.getResourceAsStream(singleDecision) when: - new YamlDecisionReader().readAll([id]) + final Optional result = new YamlDecisionReader().read(inputStream) then: - thrown(UnsupportedOperationException) + result != null + result.isPresent() + assertDecision(result.get(), decisionNamePrefix + 1) } - void 'should read decision using InputStream'() { + void 'should read decision by id using InputStream'() { given: - final InputStream inputStream = this.class.getResourceAsStream(singleDecision) + final InputStream inputStream = this.class.getResourceAsStream(multipleDecisions) when: - final Decision result = new YamlDecisionReader().read(inputStream) + final Optional result = new YamlDecisionReader().read(inputStream, decisionId) then: result != null - assertDecision(result, decisionNamePrefix + 1) + result.isPresent() + assertDecision(result.get(), decisionNamePrefix + 2) } void 'should wrap exceptions in framework ones when reading multiple decisions'() { diff --git a/src/test/groovy/org/powerflows/dmn/io/yaml/YamlDecisionWriterSpec.groovy b/src/test/groovy/org/powerflows/dmn/io/yaml/YamlDecisionWriterSpec.groovy index d3fbe10..bbf6a8c 100644 --- a/src/test/groovy/org/powerflows/dmn/io/yaml/YamlDecisionWriterSpec.groovy +++ b/src/test/groovy/org/powerflows/dmn/io/yaml/YamlDecisionWriterSpec.groovy @@ -140,7 +140,7 @@ class YamlDecisionWriterSpec extends Specification { when: writer.write(decision, outputStream) final ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray()) - final Decision result = reader.read(inputStream) + final Decision result = reader.read(inputStream).get() then: result.id == decision.id diff --git a/src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example-bad-input-columns.dmn b/src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example-bad-input-columns.dmn new file mode 100644 index 0000000..0789726 --- /dev/null +++ b/src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example-bad-input-columns.dmn @@ -0,0 +1,64 @@ + + + + + + + + + + + + + "one","two","blue","purple","red" + + + + + someInteger > 10 + + + + + + "test","jest" + + + + Some Rule 1 Description + + > 20 + + + not("blue", "purple") + + + + + + someVariable1 || someVariable2 + + + + + + + + 5 + + + + + + true + + + + + + "The output" + + + + + diff --git a/src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example-bad-output-columns.dmn b/src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example-bad-output-columns.dmn new file mode 100644 index 0000000..7b89104 --- /dev/null +++ b/src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example-bad-output-columns.dmn @@ -0,0 +1,64 @@ + + + + + + + + + + > 5 + + + + + + + + "one","two","blue","purple","red" + + + + + someInteger > 10 + + + + + Some Rule 1 Description + + > 20 + + + not("blue", "purple") + + + + + + someVariable1 || someVariable2 + + + + + + + + 5 + + + + + + true + + + + + + "The output" + + + + + diff --git a/src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example-duplicate-ids.dmn b/src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example-duplicate-ids.dmn new file mode 100644 index 0000000..e5388f9 --- /dev/null +++ b/src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example-duplicate-ids.dmn @@ -0,0 +1,97 @@ + + + + + + + + + + > 5 + + + + + + + + "one","two","blue","purple","red" + + + + + someInteger > 10 + + + + + + "test","jest" + + + + Some Rule 1 Description + + > 20 + + + not("blue", "purple") + + + + + + someVariable1 || someVariable2 + + + + + + + + 5 + + + + + + true + + + + + + "The output" + + + + + + + + + + + + + + + + + + + + + + + + + "test" + + + "success" + + + + + diff --git a/src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example.dmn b/src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example.dmn new file mode 100644 index 0000000..f79780a --- /dev/null +++ b/src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example.dmn @@ -0,0 +1,101 @@ + + + + + + + + + + > 5 + + + + + + + + "one","two","blue","purple","red" + + + + + someInteger > 10 + + + + + + "test","jest" + + + + Some Rule 1 Description + + > 20 + + + not("blue", "purple") + + + + + + someVariable1 || someVariable2 + + + + + + + + 5 + + + + + + true + + + + + + "The output" + + + + + + + + + + + + + + + + + + + + + + + + + + "test" + + + "success" + + + "another" + + + + + diff --git a/src/test/resources/org/powerflows/dmn/io/xml/omg-dmn-1.1-example.dmn b/src/test/resources/org/powerflows/dmn/io/xml/omg-dmn-1.1-example.dmn new file mode 100644 index 0000000..04c3fa7 --- /dev/null +++ b/src/test/resources/org/powerflows/dmn/io/xml/omg-dmn-1.1-example.dmn @@ -0,0 +1,1381 @@ + + + Implements model from chapter 11 of DMN 1.1 spec + + + ns2:number + + + ns2:string + + "S","M" + + + + ns2:string + + + ns2:boolean + + + + ns2:number + + + ns2:number + + + ns2:number + + + + + + ns2:boolean + + + ns2:number + + + + + ns2:string + + "STANDARD LOAN","SPECIAL LOAN" + + + + ns2:number + + + ns2:number + + + ns2:number + + + + ns2:string + + "ELIGIBLE","INELIGIBLE" + + + + ns2:string + + "FULL","MINI","NONE" + + + + ns2:string + + "BUREAU","DECLINE","THROUGH" + + + + ns2:string + + "ACCEPT","DECLINE","REFER" + + + + ns2:string + + + ns2:number + + + + + + + + + + + + + + + + + + + + + + + + + Pre-Bureau Risk Category + + + + + + "HIGH","MEDIUM" + + + "FULL" + + + + + "LOW" + + + "MINI" + + + + + "VERY LOW","DECLINE" + + + "NONE" + + + + + + + + + + + + + + + + Existing Customer + + + + + Application Risk Score + + + + + + false + + + <100 + + + "HIGH" + + + + + false + + + [100-120) + + + "MEDIUM" + + + + + false + + + [120-130] + + + "LOW" + + + + + false + + + >130 + + + "VERY LOW" + + + + + true + + + <80 + + + "DECLINE" + + + + + true + + + [80..90) + + + "HIGH" + + + + + true + + + [90..110] + + + "MEDIUM" + + + + + true + + + >110 + + + "LOW" + + + + + + + + + + + + + + Age + + + [18..120] + + + + + Marital Status + + + "S","M" + + + + + Employment Status + + + "UNEMPLOYED","EMPLOYED","SELF-EMPLOYED","STUDENT" + + + + + + [18..21] + + + - + + + - + + + 32 + + + + + [22..25] + + + - + + + - + + + 35 + + + + + [26..35] + + + - + + + - + + + 40 + + + + + [36..49] + + + - + + + - + + + 43 + + + + + >=50 + + + - + + + - + + + 48 + + + + + - + + + "S" + + + - + + + 25 + + + + + - + + + "M" + + + - + + + 45 + + + + + - + + + - + + + "UNEMPLOYED" + + + 15 + + + + + - + + + - + + + "STUDENT" + + + 18 + + + + + - + + + - + + + "EMPLOYED" + + + 45 + + + + + - + + + - + + + "SELF-EMPLOYED" + + + 36 + + + + + + + + + + + + + + + Risk Category + + + "DECLINE","HIGH","MEDIUM","LOW","VERY LOW" + + + + + + "HIGH","DECLINE" + + + 0.6 + + + + + "MEDIUM" + + + 0.7 + + + + + "LOW","VERY LOW" + + + 0.8 + + + + + + + + + + + + + + + + + + + + Monthly Income – (Monthly Repayments + Monthly Expenses) + + + + + + + Credit contingency factor table + + + + + Risk Category + + + + + + + + if Disposable Income * Credit Contingency Factor > Required Monthly Installment then true +else false + + + + + Affordability + + + + + + + + + + + + + + + + + + + + Pre-Bureau Risk Category + + + + + Pre-Bureau Affordability + + + + + Age + + + + + "INELIGIBLE","ELIGIBLE" + + + + + "DECLINE" + + + - + + + - + + + "INELIGIBLE" + + + + + - + + + false + + + - + + + "INELIGIBLE" + + + + + - + + + - + + + <18 + + + "INELIGIBLE" + + + + + - + + + - + + + - + + + "ELIGIBLE" + + + + + + + + + + + + + + + + + + Post-Bureau Risk Category + + + + + Post-Bureau Affordability + + + + + Bankrupt + + + + + Credit Score + + + "null",[0..999] + + + + + "DECLINE","REFER","ACCEPT" + + + + + - + + + false + + + - + + + - + + + DECLINE + + + + + - + + + - + + + true + + + - + + + DECLINE + + + + + "HIGH" + + + - + + + - + + + - + + + REFER + + + + + - + + + - + + + - + + + <580 + + + "REFER" + + + + + - + + + - + + + - + + + - + + + "ACCEPT" + + + + + + + + + + + + + + + + if Product Type = "STANDARD LOAN" then 20.00 +else if Product Type = "SPECIAL LOAN" then 25.00 + else null + + + + + + PMT(Rate, Term, Amount) + + + + + Monthly Repayment + Monthly Fee + + + + + + + + + + + + + + Existing Customer + + + + + Application Risk Score + + + + + Credit Score + + + + + + false + + + <120 + + + <590 + + + "HIGH" + + + + + false + + + <120 + + + [590..610] + + + "MEDIUM" + + + + + false + + + <120 + + + >610 + + + LOW + + + + + false + + + [120..130] + + + <600 + + + "HIGH" + + + + + false + + + [120..130] + + + [600..625] + + + "MEDIUM" + + + + + false + + + [120..130] + + + >625 + + + "LOW" + + + + + false + + + >130 + + + - + + + "VERY LOW" + + + + + true + + + <=100 + + + <580 + + + "HIGH" + + + + + true + + + <=100 + + + [580.600] + + + "MEDIUM" + + + + + true + + + <=100 + + + >600 + + + "LOW" + + + + + true + + + >100 + + + <590 + + + "HIGH" + + + + + true + + + >100 + + + [590..615] + + + "MEDIUM" + + + + + true + + + >100 + + + >615 + + + "LOW" + + + + + + + + + + + + + + + + + + + Bureau call type table + + + + + Pre-Bureau Risk Category + + + + + + The Strategy decision logic (Figure 70) defines a complete, unique-hit decision table deriving Strategy from Eligibility and Bureau Call Type. + Is credit bureau call required? + Yes: Bureau; No: Decline or Through + + + + + + + + + + eligibility + + + + + bureauCallType + + + + + "DECLINE","BUREAU","THROUGH" + + + + + "INELIGIBLE" + + + - + + + "DECLINE" + + + + + "ELIGIBLE" + + + "FULL" + + + "BUREAU" + + + + + "ELIGIBLE" + + + "NONE" + + + "THROUGH" + + + + + + + + + + + + + + + + + + + + Eligibility rules + + + + + Applicant data.Age + + + + + + Pre-bureau risk category + + + + + + Pre-bureau affordability + + + + + + + + + + + + + + + + + Pre-bureau risk category table + + + + + Applicant data . ExistingCustomer + + + + + + Application risk score + + + + + + + + + + + Application risk score model + + + + + Applicant data . Age + + + + + + Applicant data . MaritalStatus + + + + + + Applicant data . EmploymentStatus + + + + + + + + + + + + + + + + + + + + Affordability calculation + + + + + Applicant data . Monthly . Income + + + + + + Applicant data . Monthly . Repayments + + + + + + Applicant data . Monthly . Expenses + + + + + + Pre-bureau risk category + + + + + + Required monthly installment + + + + + + + + + + + Affordability calculation + + + + + Applicant data . Monthly . Income + + + + + + Applicant data . Monthly . Repayments + + + + + + Applicant data . Monthly . Expenses + + + + + + Post-bureau risk category + + + + + + Required monthly installment + + + + + + + + + + + + + + Installment calculation + + + + + Requested product . ProductType + + + + + + Requested product . Rate + + + + + + Requested product . Term + + + + + + Requested product . Amount + + + + + + + + + + + + + + + + + + + + Post-bureau risk category table + + + + + Applicant data . ExistingCustomer + + + + + + Bureau data . CreditScore + + + + + + Application risk score + + + + + + + + + + + + + + + + + + + + Routing rules + + + + + Bureau data . Bankrupt + + + + + + Bureau data . CreditScore + + + + + + Post-bureau risk category + + + + + + Post-bureau affordability + + + + + + + + + + + + + + + + + + + From 4ceee35fef65e0f7cb5dff7cffc21cd0f29c77e0 Mon Sep 17 00:00:00 2001 From: Mariusz Kumor Date: Fri, 4 Jan 2019 09:22:55 +0100 Subject: [PATCH 02/10] Added a validator for created Decision model. Resolved #22 --- .../engine/model/builder/AbstractBuilder.java | 20 +- .../dmn/engine/model/decision/Decision.java | 67 ++++++ .../engine/model/decision/field/Input.java | 3 + .../engine/model/decision/field/Output.java | 8 + .../dmn/engine/model/decision/rule/Rule.java | 3 + .../model/decision/rule/entry/InputEntry.java | 2 + .../decision/rule/entry/OutputEntry.java | 11 + .../dmn/io/xml/XMLDecisionConverter.java | 4 +- .../dmn/io/yaml/model/YamlDecision.java | 4 +- .../engine/model/decision/DecisionSpec.groovy | 208 +++++++++++++++++- .../builder/AbstractBuilderSpec.groovy | 66 ++++++ .../model/decision/builder/TestBuilder.groovy | 56 +++++ .../io/xml/XMLDecisionConverterSpec.groovy | 2 +- .../dmn/io/xml/XmlDecisionReaderSpec.groovy | 17 +- .../dmn/io/xml/camunda-dmn-1.1-example.dmn | 2 +- 15 files changed, 444 insertions(+), 29 deletions(-) create mode 100644 src/test/groovy/org/powerflows/dmn/engine/model/decision/builder/AbstractBuilderSpec.groovy create mode 100644 src/test/groovy/org/powerflows/dmn/engine/model/decision/builder/TestBuilder.groovy diff --git a/src/main/java/org/powerflows/dmn/engine/model/builder/AbstractBuilder.java b/src/main/java/org/powerflows/dmn/engine/model/builder/AbstractBuilder.java index 0e8b536..9d90fc7 100644 --- a/src/main/java/org/powerflows/dmn/engine/model/builder/AbstractBuilder.java +++ b/src/main/java/org/powerflows/dmn/engine/model/builder/AbstractBuilder.java @@ -16,6 +16,12 @@ package org.powerflows.dmn.engine.model.builder; +import org.powerflows.dmn.engine.model.decision.DecisionBuildException; + +import java.io.Serializable; +import java.util.List; +import java.util.Objects; + public abstract class AbstractBuilder implements Buildable { protected T product; @@ -33,10 +39,22 @@ protected T assembleProduct() { public final T build() { final T temp = assembleProduct(); if (temp == null) { - throw new IllegalStateException("Only single build() call is allowed."); + throw new IllegalStateException("Only single build() call is allowed"); } this.product = null; return temp; } + + protected void validateIsNonNull(final Serializable value, final String message) { + if (Objects.isNull(value)) { + throw new DecisionBuildException(message); + } + } + + protected void validateIsNonEmpty(final List value, final String message) { + if (value.isEmpty()) { + throw new DecisionBuildException(message); + } + } } \ No newline at end of file diff --git a/src/main/java/org/powerflows/dmn/engine/model/decision/Decision.java b/src/main/java/org/powerflows/dmn/engine/model/decision/Decision.java index 9a77732..cff09d8 100644 --- a/src/main/java/org/powerflows/dmn/engine/model/decision/Decision.java +++ b/src/main/java/org/powerflows/dmn/engine/model/decision/Decision.java @@ -23,12 +23,16 @@ import org.powerflows.dmn.engine.model.decision.field.Input; import org.powerflows.dmn.engine.model.decision.field.Output; import org.powerflows.dmn.engine.model.decision.rule.Rule; +import org.powerflows.dmn.engine.model.decision.rule.entry.InputEntry; +import org.powerflows.dmn.engine.model.decision.rule.entry.OutputEntry; import java.io.Serializable; import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; +import java.util.stream.Collectors; import static java.util.Collections.unmodifiableList; import static org.powerflows.dmn.engine.model.decision.DecisionUtil.assignDefaults; @@ -129,6 +133,41 @@ public B evaluationMode(EvaluationMode evaluationMode) { @Override protected Decision assembleProduct() { + validateIsNonEmpty(product.inputs, "At least one input is required"); + validateIsNonEmpty(product.outputs, "At least one output is required"); + validateIsNonEmpty(product.rules, "At least one rule is required"); + + final List inputNames = product.inputs.stream() + .map(Input::getName) + .collect(Collectors.toList()); + + final List outputNames = product.outputs.stream() + .map(Output::getName) + .collect(Collectors.toList()); + + validateIsNonDuplicated(inputNames, "Inputs must have unique names. Duplicated names: "); + validateIsNonDuplicated(outputNames, "Outputs must have unique names. Duplicated names: "); + + product.rules.forEach(rule -> { + final List inputEntryNames = rule.getInputEntries().stream() + .map(InputEntry::getName) + .collect(Collectors.toList()); + validateIsNonDuplicated(inputEntryNames, "Input entries must have unique names. Duplicated names: "); + validateAreEntriesMatch(inputNames, inputEntryNames, "Input entries refer to non existing inputs: "); + + final List outputEntryNames = rule.getOutputEntries().stream() + .map(OutputEntry::getName) + .collect(Collectors.toList()); + validateIsNonDuplicated(outputEntryNames, "Output entries must have unique names. Duplicated names: "); + validateAreEntriesMatch(outputNames, outputEntryNames, "Output entries refer to non existing outputs: "); + }); + + validateIsNonNull(product.id, "Id is required"); + validateIsNonNull(product.name, "Name is required"); + validateIsNonNull(product.hitPolicy, "Hit policy is required"); + validateIsNonNull(product.expressionType, "Expression type is required"); + validateIsNonNull(product.evaluationMode, "Evaluation mode is required"); + assignDefaults(this.product.inputs, this.product.rules, this.product.expressionType, this.product.evaluationMode); this.product.inputs = unmodifiableList(this.product.inputs); @@ -137,6 +176,34 @@ protected Decision assembleProduct() { return this.product; } + + private void validateAreEntriesMatch(final List fields, final List entries, final String message) { + final List missing = entries.stream() + .filter(s -> !fields.contains(s)) + .collect(Collectors.toList()); + + if (!missing.isEmpty()) { + throw new DecisionBuildException(message + missing); + } + } + + private void validateIsNonDuplicated(final List names, final String message) { + final Set duplicates = names.stream() + .collect( + Collectors.collectingAndThen( + Collectors.groupingBy( + Function.identity(), Collectors.counting() + ), map -> { + map.values().removeIf(count -> count == 1); + + return map.keySet(); + } + ) + ); + if (!duplicates.isEmpty()) { + throw new DecisionBuildException(message + duplicates); + } + } } public static final class Builder extends DecisionBuilder { diff --git a/src/main/java/org/powerflows/dmn/engine/model/decision/field/Input.java b/src/main/java/org/powerflows/dmn/engine/model/decision/field/Input.java index 1b5bfd5..924a227 100644 --- a/src/main/java/org/powerflows/dmn/engine/model/decision/field/Input.java +++ b/src/main/java/org/powerflows/dmn/engine/model/decision/field/Input.java @@ -106,6 +106,9 @@ protected Input assembleProduct() { product.expression = Expression.builder().build(); } + validateIsNonNull(product.name, "Name is required"); + validateIsNonNull(product.type, "Type is required"); + return product; } } diff --git a/src/main/java/org/powerflows/dmn/engine/model/decision/field/Output.java b/src/main/java/org/powerflows/dmn/engine/model/decision/field/Output.java index d788cb7..1b6ca51 100644 --- a/src/main/java/org/powerflows/dmn/engine/model/decision/field/Output.java +++ b/src/main/java/org/powerflows/dmn/engine/model/decision/field/Output.java @@ -79,6 +79,14 @@ public B type(ValueType type) { return (B) this; } + + @Override + protected Output assembleProduct() { + validateIsNonNull(product.name, "Name is required"); + validateIsNonNull(product.type, "Type is required"); + + return product; + } } public static final class Builder extends OutputBuilder { diff --git a/src/main/java/org/powerflows/dmn/engine/model/decision/rule/Rule.java b/src/main/java/org/powerflows/dmn/engine/model/decision/rule/Rule.java index c42d83d..b66ab6a 100644 --- a/src/main/java/org/powerflows/dmn/engine/model/decision/rule/Rule.java +++ b/src/main/java/org/powerflows/dmn/engine/model/decision/rule/Rule.java @@ -78,6 +78,9 @@ public B description(String description) { @Override protected Rule assembleProduct() { + validateIsNonEmpty(product.inputEntries, "At least one input entry is required"); + validateIsNonEmpty(product.outputEntries, "At least one output entry is required"); + this.product.inputEntries = unmodifiableList(this.product.inputEntries); this.product.outputEntries = unmodifiableList(this.product.outputEntries); diff --git a/src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/InputEntry.java b/src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/InputEntry.java index 51c40e9..ed9cc7b 100644 --- a/src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/InputEntry.java +++ b/src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/InputEntry.java @@ -94,6 +94,8 @@ protected InputEntry assembleProduct() { product.expression = Expression.builder().build(); } + validateIsNonNull(product.name, "Name is required"); + return product; } } diff --git a/src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/OutputEntry.java b/src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/OutputEntry.java index 2c0f935..4146afe 100644 --- a/src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/OutputEntry.java +++ b/src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/OutputEntry.java @@ -75,6 +75,17 @@ public B withLiteralValue(final Serializable literalValue) { return (B) this; } + + @Override + protected OutputEntry assembleProduct() { + if (product.expression == null) { + product.expression = Expression.builder().build(); + } + + validateIsNonNull(product.name, "Name is required"); + + return product; + } } public static final class Builder extends OutputEntryBuilder { diff --git a/src/main/java/org/powerflows/dmn/io/xml/XMLDecisionConverter.java b/src/main/java/org/powerflows/dmn/io/xml/XMLDecisionConverter.java index 44326432..8a4438f 100644 --- a/src/main/java/org/powerflows/dmn/io/xml/XMLDecisionConverter.java +++ b/src/main/java/org/powerflows/dmn/io/xml/XMLDecisionConverter.java @@ -257,13 +257,13 @@ private boolean isNotBlank(final String text) { private ValueType resolveType(final String typeRef) { if (typeRef == null) { - return null; + return ValueType.STRING; } else { return Arrays.stream(ValueType.values()) .filter(v -> v.name().equalsIgnoreCase(typeRef)) .findFirst() .orElseGet(() -> { - log.debug("Unable to resolve typeRef: {} to Powerflows Type", typeRef); + log.debug("Unable to resolve typeRef: {} to PowerFlows Type", typeRef); return null; }); diff --git a/src/main/java/org/powerflows/dmn/io/yaml/model/YamlDecision.java b/src/main/java/org/powerflows/dmn/io/yaml/model/YamlDecision.java index 88c9ada..f6d66a9 100644 --- a/src/main/java/org/powerflows/dmn/io/yaml/model/YamlDecision.java +++ b/src/main/java/org/powerflows/dmn/io/yaml/model/YamlDecision.java @@ -29,8 +29,8 @@ public final class YamlDecision { private String id; private String name; - private HitPolicy hitPolicy; - private ExpressionType expressionType; + private HitPolicy hitPolicy = HitPolicy.UNIQUE; + private ExpressionType expressionType = ExpressionType.LITERAL; private EvaluationMode evaluationMode = EvaluationMode.BOOLEAN; private YamlFields fields; private List rules; diff --git a/src/test/groovy/org/powerflows/dmn/engine/model/decision/DecisionSpec.groovy b/src/test/groovy/org/powerflows/dmn/engine/model/decision/DecisionSpec.groovy index 26dc78e..2cb777f 100644 --- a/src/test/groovy/org/powerflows/dmn/engine/model/decision/DecisionSpec.groovy +++ b/src/test/groovy/org/powerflows/dmn/engine/model/decision/DecisionSpec.groovy @@ -41,30 +41,29 @@ class DecisionSpec extends Specification { final String someInput2Name = 'Some Input 2 Name' final String someInput2Description = 'Some Input 2 Description' final ValueType someOutput1Type = ValueType.BOOLEAN - final ValueType someInput3Type = ValueType.STRING - final String someInput3Name = 'Some Input 3 Name' - final String someOutput1Name = 'Some Input 1 Name' + final String someOutput1Name = 'Some Output 1 Name' final String someOutput1Description = 'Some Output 1 Description' final ValueType someOutput2Type = ValueType.STRING - final String someOutput2Name = 'Some Input 2 Name' + final String someOutput2Name = 'Some Output 2 Name' final String someOutput2Description = 'Some Output 2 Description' final String someRule1Description = 'Some Rule 1 Description' final String someRule1InputEntry1Name = someInput1Name + final String someRule1InputEntry1NonMatchName = someInput1Name + 'NonMatch' final ExpressionType someRule1InputEntry1ExpressionType = ExpressionType.GROOVY final String someRule1InputEntry1ExpressionValue = '> 20' final String someRule1InputEntry2Name = someInput2Name final ExpressionType someRule1InputEntry2ExpressionType = ExpressionType.FEEL final String someRule1InputEntry2ExpressionValue = 'not("blue", "purple")' - final String someRule1OutputEntry1Name = 'Some Rule 1 Output Entry 1 Name' + final String someRule1OutputEntry1Name = someOutput1Name final ExpressionType someRule1OutputEntry1ExpressionType = ExpressionType.GROOVY final String someRule1OutputEntry1ExpressionValue = 'someVariable1 || someVariable2' final String someRule2Description = 'Some Rule 2 Description' final String someRule2InputEntry1Name = someInput1Name final String someRule2InputEntry2Name = someInput2Name - final String someRule2OutputEntry1Name = 'Some Rule 2 Output Entry 1 Name' - final String someRule2OutputEntry2Name = 'Some Rule 2 Output Entry 2 Name' + final String someRule2OutputEntry1Name = someOutput1Name + final String someRule2OutputEntry2Name = someOutput2Name final Integer someRule2OutputEntry1ExpressionValue = 1 - final Integer someRule2OutputEntry2ExpressionValue = 2 + final Integer someRule2OutputEntry2ExpressionValue = null void 'should build decision with fluent API'() { given: @@ -365,7 +364,6 @@ class DecisionSpec extends Specification { { outputEntryBuilder -> outputEntryBuilder .name(someRule2OutputEntry2Name) - .withLiteralValue(someRule2OutputEntry2ExpressionValue) .build() }) .build() @@ -485,4 +483,196 @@ class DecisionSpec extends Specification { getValue() == someRule2OutputEntry2ExpressionValue } } + + void 'should throw exception when duplicates occur'() { + given: + //nothing + + when: + Decision.builder() + .id(someTableId) + .name(someTableName) + .hitPolicy(someHitPolicy) + .expressionType(someExpressionType) + .withInput( + { inputsBuilder -> + inputsBuilder.name(someInput1Name) + .description(someInput1Description) + .type(someInput1Type) + .withExpression( + { expressionBuilder -> + expressionBuilder + .type(someInput1Expression1Type) + .value(someInput1Expression1Value) + .build() + }) + .build() + }) + .withInput( + { inputsBuilder -> + inputsBuilder + .name(someInput1Name) + .description(someInput2Description) + .type(someInput2Type) + .build() + }) + .withOutput( + { outputsBuilder -> + outputsBuilder + .name(someOutput1Name) + .description(someOutput1Description) + .type(someOutput1Type) + .build() + }) + .withRule( + { rulesBuilder -> + rulesBuilder + .withInputEntry( + { inputEntryBuilder -> + inputEntryBuilder.name(someRule1InputEntry2Name) + .withExpression( + { expressionBuilder -> + expressionBuilder + .type(someRule1InputEntry2ExpressionType) + .value(someRule1InputEntry2ExpressionValue) + .build() + }) + .build() + }) + .withOutputEntry( + { outputEntryBuilder -> + outputEntryBuilder + .name(someRule1OutputEntry1Name) + .withExpression( + { expressionBuilder -> + expressionBuilder + .type(someRule1OutputEntry1ExpressionType) + .value(someRule1OutputEntry1ExpressionValue) + .build() + }) + .build() + }) + .build() + }) + .withRule( + { rulesBuilder -> + rulesBuilder + .withInputEntry( + { inputEntryBuilder -> + inputEntryBuilder + .name(someRule2InputEntry2Name) + .build() + }) + .withOutputEntry( + { outputEntryBuilder -> + outputEntryBuilder + .name(someRule2OutputEntry1Name) + .withLiteralValue(someRule2OutputEntry1ExpressionValue) + .build() + }) + .build() + }) + .build() + + then: + final DecisionBuildException exception = thrown() + exception != null + exception.getMessage() == "Inputs must have unique names. Duplicated names: [$someInput1Name]" + } + + void 'should throw exception when non match entries'() { + given: + //nothing + + when: + Decision.builder() + .id(someTableId) + .name(someTableName) + .hitPolicy(someHitPolicy) + .expressionType(someExpressionType) + .withInput( + { inputsBuilder -> + inputsBuilder.name(someInput1Name) + .description(someInput1Description) + .type(someInput1Type) + .withExpression( + { expressionBuilder -> + expressionBuilder + .type(someInput1Expression1Type) + .value(someInput1Expression1Value) + .build() + }) + .build() + }) + .withInput( + { inputsBuilder -> + inputsBuilder + .name(someInput2Name) + .description(someInput2Description) + .type(someInput2Type) + .build() + }) + .withOutput( + { outputsBuilder -> + outputsBuilder + .name(someOutput1Name) + .description(someOutput1Description) + .type(someOutput1Type) + .build() + }) + .withRule( + { rulesBuilder -> + rulesBuilder + .withInputEntry( + { inputEntryBuilder -> + inputEntryBuilder.name(someRule1InputEntry1NonMatchName) + .withExpression( + { expressionBuilder -> + expressionBuilder + .type(someRule1InputEntry2ExpressionType) + .value(someRule1InputEntry2ExpressionValue) + .build() + }) + .build() + }) + .withOutputEntry( + { outputEntryBuilder -> + outputEntryBuilder + .name(someRule1OutputEntry1Name) + .withExpression( + { expressionBuilder -> + expressionBuilder + .type(someRule1OutputEntry1ExpressionType) + .value(someRule1OutputEntry1ExpressionValue) + .build() + }) + .build() + }) + .build() + }) + .withRule( + { rulesBuilder -> + rulesBuilder + .withInputEntry( + { inputEntryBuilder -> + inputEntryBuilder + .name(someRule2InputEntry2Name) + .build() + }) + .withOutputEntry( + { outputEntryBuilder -> + outputEntryBuilder + .name(someRule2OutputEntry1Name) + .withLiteralValue(someRule2OutputEntry1ExpressionValue) + .build() + }) + .build() + }) + .build() + + then: + final DecisionBuildException exception = thrown() + exception != null + exception.getMessage() == "Input entries refer to non existing inputs: [$someRule1InputEntry1NonMatchName]" + } } diff --git a/src/test/groovy/org/powerflows/dmn/engine/model/decision/builder/AbstractBuilderSpec.groovy b/src/test/groovy/org/powerflows/dmn/engine/model/decision/builder/AbstractBuilderSpec.groovy new file mode 100644 index 0000000..e79b881 --- /dev/null +++ b/src/test/groovy/org/powerflows/dmn/engine/model/decision/builder/AbstractBuilderSpec.groovy @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.engine.model.decision.builder + +import org.powerflows.dmn.engine.model.decision.DecisionBuildException +import org.powerflows.dmn.engine.model.decision.EvaluationMode +import spock.lang.Specification + +class AbstractBuilderSpec extends Specification { + + + void 'should throw exception when multiple build() called'() { + given: + final TestBuilder testBuilder = new TestBuilder(); + + when: + testBuilder.build() + testBuilder.build() + + then: + final IllegalStateException exception = thrown() + exception != null + exception.getMessage() == 'Only single build() call is allowed' + } + + void 'should throw exception when validated value is null'() { + given: + final TestBuilder testBuilder = new TestBuilder(null as EvaluationMode); + + when: + testBuilder.build() + + then: + final DecisionBuildException exception = thrown() + exception != null + exception.getMessage() == 'Evaluation mode is required' + } + + void 'should throw exception when validated value is empty'() { + given: + final TestBuilder testBuilder = new TestBuilder([]); + + when: + testBuilder.build() + + then: + final DecisionBuildException exception = thrown() + exception != null + exception.getMessage() == 'At least one input is required' + } + +} diff --git a/src/test/groovy/org/powerflows/dmn/engine/model/decision/builder/TestBuilder.groovy b/src/test/groovy/org/powerflows/dmn/engine/model/decision/builder/TestBuilder.groovy new file mode 100644 index 0000000..949f86a --- /dev/null +++ b/src/test/groovy/org/powerflows/dmn/engine/model/decision/builder/TestBuilder.groovy @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.engine.model.decision.builder + +import org.powerflows.dmn.engine.model.builder.AbstractBuilder +import org.powerflows.dmn.engine.model.decision.Decision +import org.powerflows.dmn.engine.model.decision.EvaluationMode +import org.powerflows.dmn.engine.model.decision.field.Input + +class TestBuilder extends AbstractBuilder { + + private EvaluationMode evaluationMode + private List inputs + + TestBuilder() { + this.evaluationMode = EvaluationMode.BOOLEAN + this.inputs = [[] as Input] + } + + TestBuilder(final EvaluationMode evaluationMode) { + this.evaluationMode = evaluationMode + this.inputs = new ArrayList<>() + } + + TestBuilder(final List inputs) { + this.evaluationMode = EvaluationMode.BOOLEAN + this.inputs = inputs + } + + @Override + protected void initProduct() { + this.product = [] as Decision; + } + + @Override + protected Decision assembleProduct() { + validateIsNonNull(evaluationMode, 'Evaluation mode is required'); + validateIsNonEmpty(inputs, 'At least one input is required'); + + return this.product; + } +} diff --git a/src/test/groovy/org/powerflows/dmn/io/xml/XMLDecisionConverterSpec.groovy b/src/test/groovy/org/powerflows/dmn/io/xml/XMLDecisionConverterSpec.groovy index 7803a91..e5dfa33 100644 --- a/src/test/groovy/org/powerflows/dmn/io/xml/XMLDecisionConverterSpec.groovy +++ b/src/test/groovy/org/powerflows/dmn/io/xml/XMLDecisionConverterSpec.groovy @@ -24,7 +24,7 @@ class XMLDecisionConverterSpec extends Specification { final XMLDecisionConverter converter = new XMLDecisionConverter() when: - converter.to(Decision.builder().build()) + converter.to([] as Decision) then: thrown(UnsupportedOperationException) diff --git a/src/test/groovy/org/powerflows/dmn/io/xml/XmlDecisionReaderSpec.groovy b/src/test/groovy/org/powerflows/dmn/io/xml/XmlDecisionReaderSpec.groovy index ae0e158..6123b2a 100644 --- a/src/test/groovy/org/powerflows/dmn/io/xml/XmlDecisionReaderSpec.groovy +++ b/src/test/groovy/org/powerflows/dmn/io/xml/XmlDecisionReaderSpec.groovy @@ -17,6 +17,7 @@ package org.powerflows.dmn.io.xml import org.powerflows.dmn.engine.model.decision.Decision +import org.powerflows.dmn.engine.model.decision.DecisionBuildException import org.powerflows.dmn.engine.model.decision.HitPolicy import org.powerflows.dmn.engine.model.decision.expression.ExpressionType import org.powerflows.dmn.engine.model.decision.field.ValueType @@ -60,7 +61,7 @@ class XmlDecisionReaderSpec extends Specification { thrown(DecisionReadException) } - void 'should load minimal markup with empty data'() { + void 'should fail on minimal markup with empty data'() { given: final String xml = ''' @@ -71,20 +72,10 @@ class XmlDecisionReaderSpec extends Specification { ''' when: - final List result = reader.readAll(new ByteArrayInputStream(xml.getBytes())) + reader.readAll(new ByteArrayInputStream(xml.getBytes())) then: - result?.size() == 1 - - and: - with(result[0]) { - getInputs().size() == 0 - getOutputs().size() == 0 - getRules().size() == 0 - getName() == 'Some Table Name' - getId() == 'some_table_id_1' - getHitPolicy() == HitPolicy.UNIQUE - } + thrown(DecisionBuildException) } void 'should load minimal markup with empty data using strict mode and fail on unexpected element'() { diff --git a/src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example.dmn b/src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example.dmn index f79780a..7f4edd8 100644 --- a/src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example.dmn +++ b/src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example.dmn @@ -79,7 +79,7 @@ - + From 71a9eb44265745103b053be419966453931b1cf2 Mon Sep 17 00:00:00 2001 From: Mariusz Kumor Date: Tue, 8 Jan 2019 18:14:32 +0100 Subject: [PATCH 03/10] Added a support for a default alias of the entry name in the input scope. Resolved #130 --- .../evaluator/entry/InputEntryEvaluator.java | 2 +- .../evaluator/entry/OutputEntryEvaluator.java | 4 +- .../ExpressionEvaluationProvider.java | 4 +- .../FeelExpressionEvaluationProvider.java | 6 +-- .../JuelExpressionEvaluationProvider.java | 6 +-- .../LiteralExpressionEvaluationProvider.java | 8 ++-- .../ScriptExpressionEvaluationProvider.java | 18 +++++++-- .../bindings/ContextVariablesBindings.java | 16 +++++++- .../model/decision/rule/entry/Entry.java | 40 +++++++++++++++++++ .../model/decision/rule/entry/InputEntry.java | 17 ++------ .../decision/rule/entry/OutputEntry.java | 18 ++------- ...ovyExpressionEvaluationProviderSpec.groovy | 23 ++++++++++- ...iptExpressionEvaluationProviderSpec.groovy | 24 ++++++++++- ...ralExpressionEvaluationProviderSpec.groovy | 4 +- .../model/decision/DecisionUtilSpec.groovy | 2 +- .../engine/configuration/reference-single.yml | 2 +- 16 files changed, 141 insertions(+), 53 deletions(-) create mode 100644 src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/Entry.java diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/entry/InputEntryEvaluator.java b/src/main/java/org/powerflows/dmn/engine/evaluator/entry/InputEntryEvaluator.java index 701c01a..cbca67f 100644 --- a/src/main/java/org/powerflows/dmn/engine/evaluator/entry/InputEntryEvaluator.java +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/entry/InputEntryEvaluator.java @@ -66,7 +66,7 @@ public boolean evaluate(final InputEntry inputEntry, final Object inputValue = evaluationContext.get(inputEntry.getName()); final SpecifiedTypeValue typedInputValue = typeConverter.convert(inputValue); - final Object inputEntryValue = inputEntryExpressionEvaluator.evaluateEntry(inputEntry.getExpression(), evaluationContext); + final Object inputEntryValue = inputEntryExpressionEvaluator.evaluateEntry(inputEntry, evaluationContext); final SpecifiedTypeValue typedInputEntryValue; if (isBoolean(inputEntryValue)) { diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/entry/OutputEntryEvaluator.java b/src/main/java/org/powerflows/dmn/engine/evaluator/entry/OutputEntryEvaluator.java index 1773e86..03cd063 100644 --- a/src/main/java/org/powerflows/dmn/engine/evaluator/entry/OutputEntryEvaluator.java +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/entry/OutputEntryEvaluator.java @@ -19,8 +19,8 @@ import lombok.extern.slf4j.Slf4j; import org.powerflows.dmn.engine.evaluator.context.EvaluationContext; -import org.powerflows.dmn.engine.evaluator.expression.provider.ExpressionEvaluationProviderFactory; import org.powerflows.dmn.engine.evaluator.expression.provider.ExpressionEvaluationProvider; +import org.powerflows.dmn.engine.evaluator.expression.provider.ExpressionEvaluationProviderFactory; import org.powerflows.dmn.engine.evaluator.type.converter.TypeConverter; import org.powerflows.dmn.engine.evaluator.type.converter.TypeConverterFactory; import org.powerflows.dmn.engine.model.decision.field.Output; @@ -45,7 +45,7 @@ public EntryResult evaluate(final OutputEntry outputEntry, final Output output, final ExpressionEvaluationProvider expressionEvaluator = expressionEvaluationProviderFactory.getInstance(outputEntry.getExpression().getType()); final TypeConverter typeConverter = typeConverterFactory.getInstance(output.getType()); - final Serializable outputEntryValue = expressionEvaluator.evaluateEntry(outputEntry.getExpression(), evaluationContext); + final Serializable outputEntryValue = expressionEvaluator.evaluateEntry(outputEntry, evaluationContext); //Needed for the output entry value validation. //Correct build means the output entry value has a type compatible with the output definition. diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/ExpressionEvaluationProvider.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/ExpressionEvaluationProvider.java index 8308e54..e3ddadc 100644 --- a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/ExpressionEvaluationProvider.java +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/ExpressionEvaluationProvider.java @@ -17,8 +17,8 @@ package org.powerflows.dmn.engine.evaluator.expression.provider; import org.powerflows.dmn.engine.evaluator.context.EvaluationContext; -import org.powerflows.dmn.engine.model.decision.expression.Expression; import org.powerflows.dmn.engine.model.decision.field.Input; +import org.powerflows.dmn.engine.model.decision.rule.entry.Entry; import java.io.Serializable; @@ -26,5 +26,5 @@ public interface ExpressionEvaluationProvider { Serializable evaluateInput(Input input, EvaluationContext evaluationContext); - Serializable evaluateEntry(Expression entryExpression, EvaluationContext evaluationContext); + Serializable evaluateEntry(Entry entry, EvaluationContext evaluationContext); } diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/FeelExpressionEvaluationProvider.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/FeelExpressionEvaluationProvider.java index d0a2373..9b18e23 100644 --- a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/FeelExpressionEvaluationProvider.java +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/FeelExpressionEvaluationProvider.java @@ -17,20 +17,20 @@ package org.powerflows.dmn.engine.evaluator.expression.provider; import org.powerflows.dmn.engine.evaluator.context.EvaluationContext; -import org.powerflows.dmn.engine.model.decision.expression.Expression; import org.powerflows.dmn.engine.model.decision.field.Input; +import org.powerflows.dmn.engine.model.decision.rule.entry.Entry; import java.io.Serializable; class FeelExpressionEvaluationProvider implements ExpressionEvaluationProvider { @Override - public Serializable evaluateEntry(final Expression expression, final EvaluationContext evaluationContext) { + public Serializable evaluateInput(final Input input, final EvaluationContext evaluationContext) { throw new UnsupportedOperationException(); } @Override - public Serializable evaluateInput(final Input input, final EvaluationContext evaluationContext) { + public Serializable evaluateEntry(final Entry entry, final EvaluationContext evaluationContext) { throw new UnsupportedOperationException(); } } diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/JuelExpressionEvaluationProvider.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/JuelExpressionEvaluationProvider.java index e46437c..a237d92 100644 --- a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/JuelExpressionEvaluationProvider.java +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/JuelExpressionEvaluationProvider.java @@ -17,20 +17,20 @@ package org.powerflows.dmn.engine.evaluator.expression.provider; import org.powerflows.dmn.engine.evaluator.context.EvaluationContext; -import org.powerflows.dmn.engine.model.decision.expression.Expression; import org.powerflows.dmn.engine.model.decision.field.Input; +import org.powerflows.dmn.engine.model.decision.rule.entry.Entry; import java.io.Serializable; class JuelExpressionEvaluationProvider implements ExpressionEvaluationProvider { @Override - public Serializable evaluateEntry(final Expression expression, final EvaluationContext evaluationContext) { + public Serializable evaluateInput(final Input input, final EvaluationContext evaluationContext) { throw new UnsupportedOperationException(); } @Override - public Serializable evaluateInput(final Input input, final EvaluationContext evaluationContext) { + public Serializable evaluateEntry(final Entry entry, final EvaluationContext evaluationContext) { throw new UnsupportedOperationException(); } } diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/LiteralExpressionEvaluationProvider.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/LiteralExpressionEvaluationProvider.java index ec59a76..4dc8008 100644 --- a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/LiteralExpressionEvaluationProvider.java +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/LiteralExpressionEvaluationProvider.java @@ -18,8 +18,8 @@ import lombok.extern.slf4j.Slf4j; import org.powerflows.dmn.engine.evaluator.context.EvaluationContext; -import org.powerflows.dmn.engine.model.decision.expression.Expression; import org.powerflows.dmn.engine.model.decision.field.Input; +import org.powerflows.dmn.engine.model.decision.rule.entry.Entry; import java.io.Serializable; @@ -43,10 +43,10 @@ public Serializable evaluateInput(final Input input, final EvaluationContext eva } @Override - public Serializable evaluateEntry(final Expression entryExpression, final EvaluationContext evaluationContext) { - log.debug("Starting evaluation of entry with expression: {} and evaluation context: {}", entryExpression, evaluationContext); + public Serializable evaluateEntry(final Entry entry, final EvaluationContext evaluationContext) { + log.debug("Starting evaluation of entry {} with evaluation context: {}", entry, evaluationContext); - final Serializable result = entryExpression.getValue(); + final Serializable result = entry.getExpression().getValue(); log.debug("Evaluated entry result: {}", result); diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/ScriptExpressionEvaluationProvider.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/ScriptExpressionEvaluationProvider.java index 55d9774..3e49757 100644 --- a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/ScriptExpressionEvaluationProvider.java +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/ScriptExpressionEvaluationProvider.java @@ -23,6 +23,7 @@ import org.powerflows.dmn.engine.evaluator.expression.script.bindings.ContextVariablesBindings; import org.powerflows.dmn.engine.model.decision.expression.Expression; import org.powerflows.dmn.engine.model.decision.field.Input; +import org.powerflows.dmn.engine.model.decision.rule.entry.Entry; import javax.script.Bindings; import javax.script.ScriptEngine; @@ -51,20 +52,31 @@ public Serializable evaluateInput(final Input input, final EvaluationContext eva } @Override - public Serializable evaluateEntry(final Expression entryExpression, final EvaluationContext evaluationContext) { - log.debug("Starting evaluation of entry with expression: {} and evaluation context: {}", entryExpression, evaluationContext); + public Serializable evaluateEntry(final Entry entry, final EvaluationContext evaluationContext) { + log.debug("Starting evaluation of entry {} with evaluation context: {}", entry, evaluationContext); - final Serializable result = evaluate(entryExpression, evaluationContext); + final Serializable result = evaluate(entry, evaluationContext); log.debug("Evaluated entry result: {}", result); return result; } + private Serializable evaluate(final Entry entry, final EvaluationContext evaluationContext) { + final ScriptEngine scriptEngine = scriptEngineProvider.getScriptEngine(entry.getExpression().getType()); + final Bindings bindings = ContextVariablesBindings.create(scriptEngine.createBindings(), evaluationContext, entry.getName()); + + return evaluate(entry.getExpression(), scriptEngine, bindings); + } + private Serializable evaluate(final Expression expression, final EvaluationContext evaluationContext) { final ScriptEngine scriptEngine = scriptEngineProvider.getScriptEngine(expression.getType()); final Bindings bindings = ContextVariablesBindings.create(scriptEngine.createBindings(), evaluationContext); + return evaluate(expression, scriptEngine, bindings); + } + + private Serializable evaluate(final Expression expression, final ScriptEngine scriptEngine, final Bindings bindings) { final Serializable result; try { diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/script/bindings/ContextVariablesBindings.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/script/bindings/ContextVariablesBindings.java index 2c50095..76f83fb 100644 --- a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/script/bindings/ContextVariablesBindings.java +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/script/bindings/ContextVariablesBindings.java @@ -26,6 +26,7 @@ public class ContextVariablesBindings implements Bindings { private final Bindings bindings; + private static final String DEFAULT_INPUT_NAME_ALIAS = "cellInput"; private ContextVariablesBindings(final Bindings bindings, final EvaluationContext evaluationContext) { this.bindings = bindings; @@ -36,10 +37,23 @@ private ContextVariablesBindings(final Bindings bindings, final EvaluationContex .forEach(variableName -> this.bindings.put(variableName, evaluationContext.get(variableName))); } - public static ContextVariablesBindings create(final Bindings bindings, final EvaluationContext evaluationContext) { + private ContextVariablesBindings(final Bindings bindings, final EvaluationContext evaluationContext, final String inputName) { + this(bindings, evaluationContext); + + this.bindings.put(DEFAULT_INPUT_NAME_ALIAS, evaluationContext.get(inputName)); + } + + public static ContextVariablesBindings create(final Bindings bindings, + final EvaluationContext evaluationContext) { return new ContextVariablesBindings(bindings, evaluationContext); } + public static ContextVariablesBindings create(final Bindings bindings, + final EvaluationContext evaluationContext, + final String inputName) { + return new ContextVariablesBindings(bindings, evaluationContext, inputName); + } + @Override public Object put(final String name, final Object value) { return bindings.put(name, value); diff --git a/src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/Entry.java b/src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/Entry.java new file mode 100644 index 0000000..3489597 --- /dev/null +++ b/src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/Entry.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.engine.model.decision.rule.entry; + +import lombok.ToString; +import org.powerflows.dmn.engine.model.decision.expression.Expression; + +import java.io.Serializable; + +@ToString +public class Entry implements Serializable { + + protected String name; + protected Expression expression; + + Entry() { + } + + public String getName() { + return name; + } + + public Expression getExpression() { + return expression; + } +} diff --git a/src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/InputEntry.java b/src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/InputEntry.java index ed9cc7b..6d32fd4 100644 --- a/src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/InputEntry.java +++ b/src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/InputEntry.java @@ -27,25 +27,16 @@ import java.util.function.Consumer; import java.util.function.Function; -@EqualsAndHashCode -@ToString -public class InputEntry implements Serializable { +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class InputEntry extends Entry { private static final long serialVersionUID = 1; - private String name; - private Expression expression; private EvaluationMode evaluationMode; private InputEntry() { - } - - public String getName() { - return name; - } - - public Expression getExpression() { - return expression; + super(); } public EvaluationMode getEvaluationMode() { diff --git a/src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/OutputEntry.java b/src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/OutputEntry.java index 4146afe..e8352c0 100644 --- a/src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/OutputEntry.java +++ b/src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/OutputEntry.java @@ -26,24 +26,14 @@ import java.util.function.Consumer; import java.util.function.Function; -@EqualsAndHashCode -@ToString -public class OutputEntry implements Serializable { +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class OutputEntry extends Entry { private static final long serialVersionUID = 1; - private String name; - private Expression expression; - private OutputEntry() { - } - - public String getName() { - return name; - } - - public Expression getExpression() { - return expression; + super(); } public static

FluentBuilder

fluentBuilder(final P parentBuilder, final Consumer outputEntryConsumer) { diff --git a/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/GroovyExpressionEvaluationProviderSpec.groovy b/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/GroovyExpressionEvaluationProviderSpec.groovy index 43bb683..74384e0 100644 --- a/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/GroovyExpressionEvaluationProviderSpec.groovy +++ b/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/GroovyExpressionEvaluationProviderSpec.groovy @@ -23,6 +23,8 @@ import org.powerflows.dmn.engine.evaluator.expression.script.ScriptEngineProvide import org.powerflows.dmn.engine.model.decision.expression.Expression import org.powerflows.dmn.engine.model.decision.expression.ExpressionType import org.powerflows.dmn.engine.model.decision.field.Input +import org.powerflows.dmn.engine.model.decision.rule.entry.InputEntry +import org.powerflows.dmn.engine.model.decision.rule.entry.OutputEntry import org.powerflows.dmn.engine.model.evaluation.variable.DecisionVariables import spock.lang.Specification import spock.lang.Unroll @@ -40,12 +42,13 @@ class GroovyExpressionEvaluationProviderSpec extends Specification { final Object entryExpressionValue, final Object contextVariable, final boolean expectedEntryResult) { given: final Expression entryExpression = [value: entryExpressionValue, type: ExpressionType.GROOVY] + final InputEntry inputEntry = [expression: entryExpression] as InputEntry final DecisionVariables decisionVariables = new DecisionVariables([x: contextVariable, TestInputName: true]) final EvaluationContext evaluationContext = new EvaluationContext(decisionVariables) when: - final boolean inputEntryResult = expressionEvaluationProvider.evaluateEntry(entryExpression, evaluationContext) + final boolean inputEntryResult = expressionEvaluationProvider.evaluateEntry(inputEntry, evaluationContext) then: inputEntryResult == expectedEntryResult @@ -59,6 +62,21 @@ class GroovyExpressionEvaluationProviderSpec extends Specification { '(2 + 5) == x' | 7 || true } + void 'should evaluate entry groovy expression with default alias usage'() { + given: + final Expression expression = [value: "cellInput == 'something'", type: ExpressionType.GROOVY] + final InputEntry inputEntry = [name: 'TestInputName', expression: expression] + + final DecisionVariables decisionVariables = new DecisionVariables(['TestInputName': 'something']) + final EvaluationContext contextVariables = new EvaluationContext(decisionVariables) + + when: + final boolean inputEntryResult = expressionEvaluationProvider.evaluateEntry(inputEntry, contextVariables) + + then: + inputEntryResult + } + @Unroll void 'should evaluate input groovy expression value #inputExpression and variables #contextVariable with #expectedInputResult'( final Object inputExpression, final Object contextVariable, final boolean expectedInputResult) { @@ -88,9 +106,10 @@ class GroovyExpressionEvaluationProviderSpec extends Specification { final Expression entryExpression = [value: outputEntryValue, type: ExpressionType.GROOVY] final DecisionVariables decisionVariables = new DecisionVariables([:]) final EvaluationContext contextVariables = new EvaluationContext(decisionVariables) + final OutputEntry outputEntry = [expression: entryExpression] as OutputEntry when: - expressionEvaluationProvider.evaluateEntry(entryExpression, contextVariables) + expressionEvaluationProvider.evaluateEntry(outputEntry, contextVariables) then: final EvaluationException exception = thrown() diff --git a/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/JavaScriptExpressionEvaluationProviderSpec.groovy b/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/JavaScriptExpressionEvaluationProviderSpec.groovy index 0f07f9a..c3bf299 100644 --- a/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/JavaScriptExpressionEvaluationProviderSpec.groovy +++ b/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/JavaScriptExpressionEvaluationProviderSpec.groovy @@ -23,6 +23,8 @@ import org.powerflows.dmn.engine.evaluator.expression.script.ScriptEngineProvide import org.powerflows.dmn.engine.model.decision.expression.Expression import org.powerflows.dmn.engine.model.decision.expression.ExpressionType import org.powerflows.dmn.engine.model.decision.field.Input +import org.powerflows.dmn.engine.model.decision.rule.entry.InputEntry +import org.powerflows.dmn.engine.model.decision.rule.entry.OutputEntry import org.powerflows.dmn.engine.model.evaluation.variable.DecisionVariables import spock.lang.Specification import spock.lang.Unroll @@ -40,12 +42,13 @@ class JavaScriptExpressionEvaluationProviderSpec extends Specification { final Object entryExpressionValue, final Object contextVariable, final boolean expectedEntryResult) { given: final Expression entryExpression = [value: entryExpressionValue, type: ExpressionType.JAVASCRIPT] + final InputEntry inputEntry = [expression: entryExpression] as InputEntry final DecisionVariables decisionVariables = new DecisionVariables([x: contextVariable, TestInputName: true]) final EvaluationContext evaluationContext = new EvaluationContext(decisionVariables) when: - final boolean inputEntryResult = expressionEvaluationProvider.evaluateEntry(entryExpression, evaluationContext) + final boolean inputEntryResult = expressionEvaluationProvider.evaluateEntry(inputEntry, evaluationContext) then: inputEntryResult == expectedEntryResult @@ -59,6 +62,21 @@ class JavaScriptExpressionEvaluationProviderSpec extends Specification { '(2 + 5) == x' | 7 || true } + void 'should evaluate entry javascript expression with default alias usage'() { + given: + final Expression expression = [value: "cellInput == 'something'", type: ExpressionType.JAVASCRIPT] + final InputEntry inputEntry = [name: 'TestInputName', expression: expression] + + final DecisionVariables decisionVariables = new DecisionVariables(['TestInputName': 'something']) + final EvaluationContext contextVariables = new EvaluationContext(decisionVariables) + + when: + final boolean inputEntryResult = expressionEvaluationProvider.evaluateEntry(inputEntry, contextVariables) + + then: + inputEntryResult + } + @Unroll void 'should evaluate input javascript expression value #inputExpression and variables #contextVariable with #expectedInputResult'( final Object inputExpression, final Object contextVariable, final boolean expectedInputResult) { @@ -86,11 +104,13 @@ class JavaScriptExpressionEvaluationProviderSpec extends Specification { given: final String outputEntryValue = 'x' final Expression entryExpression = [value: outputEntryValue, type: ExpressionType.JAVASCRIPT] + final OutputEntry outputEntry = [expression: entryExpression] as OutputEntry + final DecisionVariables decisionVariables = new DecisionVariables([:]) final EvaluationContext contextVariables = new EvaluationContext(decisionVariables) when: - expressionEvaluationProvider.evaluateEntry(entryExpression, contextVariables) + expressionEvaluationProvider.evaluateEntry(outputEntry, contextVariables) then: final EvaluationException exception = thrown() diff --git a/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/LiteralExpressionEvaluationProviderSpec.groovy b/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/LiteralExpressionEvaluationProviderSpec.groovy index 3311543..f9309fa 100644 --- a/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/LiteralExpressionEvaluationProviderSpec.groovy +++ b/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/LiteralExpressionEvaluationProviderSpec.groovy @@ -20,6 +20,7 @@ import org.powerflows.dmn.engine.evaluator.context.EvaluationContext import org.powerflows.dmn.engine.model.decision.expression.Expression import org.powerflows.dmn.engine.model.decision.expression.ExpressionType import org.powerflows.dmn.engine.model.decision.field.Input +import org.powerflows.dmn.engine.model.decision.rule.entry.InputEntry import org.powerflows.dmn.engine.model.evaluation.variable.DecisionVariables import spock.lang.Specification @@ -33,12 +34,13 @@ class LiteralExpressionEvaluationProviderSpec extends Specification { final Object inputEntryValue = 5 final Object contextVariable = 6 final Expression entryExpression = [value: inputEntryValue, type: ExpressionType.LITERAL] + final InputEntry inputEntry = [expression: entryExpression] as InputEntry final DecisionVariables decisionVariables = new DecisionVariables([TestInputName: contextVariable]) final EvaluationContext evaluationContext = new EvaluationContext(decisionVariables) when: - final boolean inputEntryResult = expressionEvaluationProvider.evaluateEntry(entryExpression, evaluationContext) + final boolean inputEntryResult = expressionEvaluationProvider.evaluateEntry(inputEntry, evaluationContext) then: inputEntryResult diff --git a/src/test/groovy/org/powerflows/dmn/engine/model/decision/DecisionUtilSpec.groovy b/src/test/groovy/org/powerflows/dmn/engine/model/decision/DecisionUtilSpec.groovy index cc217ac..68176e3 100644 --- a/src/test/groovy/org/powerflows/dmn/engine/model/decision/DecisionUtilSpec.groovy +++ b/src/test/groovy/org/powerflows/dmn/engine/model/decision/DecisionUtilSpec.groovy @@ -93,7 +93,7 @@ class DecisionUtilSpec extends Specification { then: final DecisionBuildException exception = thrown() exception != null - exception.getMessage() == 'Can not set value 5 for InputEntry(name=null, expression=null, evaluationMode=null)' + exception.getMessage() == 'Can not set value 5 for InputEntry(super=Entry(name=null, expression=null), evaluationMode=null)' } void 'should throw exception when can not find field by type'() { diff --git a/src/test/resources/org/powerflows/dmn/engine/configuration/reference-single.yml b/src/test/resources/org/powerflows/dmn/engine/configuration/reference-single.yml index 72257ad..ff4c43e 100644 --- a/src/test/resources/org/powerflows/dmn/engine/configuration/reference-single.yml +++ b/src/test/resources/org/powerflows/dmn/engine/configuration/reference-single.yml @@ -95,7 +95,7 @@ rules: in: inputFour: expression-type: GROOVY - expression: "((inputFour instanceof String) ? Date.parse('yyyy-MM-dd', '2018-12-24') : inputFour) < Date.parse('yyyy-MM-dd', '2018-12-14')" + expression: "((cellInput instanceof String) ? Date.parse('yyyy-MM-dd', '2018-12-24') : cellInput) < Date.parse('yyyy-MM-dd', '2018-12-14')" evaluation-mode: BOOLEAN out: outputOne: true From 547775642a5e3b73c75a54835099359267e99c40 Mon Sep 17 00:00:00 2001 From: Mariusz Kumor Date: Thu, 10 Jan 2019 19:38:22 +0100 Subject: [PATCH 04/10] Extended the model to make possible a change of the default alias of input name. Resolved #132 --- .../evaluator/entry/InputEntryEvaluator.java | 2 +- .../evaluator/entry/OutputEntryEvaluator.java | 2 +- .../ExpressionEvaluationProvider.java | 7 +++- .../FeelExpressionEvaluationProvider.java | 10 ++++- .../JuelExpressionEvaluationProvider.java | 10 ++++- .../LiteralExpressionEvaluationProvider.java | 22 +++++++--- .../ScriptExpressionEvaluationProvider.java | 28 +++++++++---- .../bindings/ContextVariablesBindings.java | 12 +++--- .../engine/model/decision/DecisionUtil.java | 27 ++++++++----- .../engine/model/decision/field/Input.java | 12 ++++++ .../model/decision/rule/entry/Entry.java | 40 ------------------- .../model/decision/rule/entry/InputEntry.java | 27 +++++++++++-- .../decision/rule/entry/OutputEntry.java | 18 +++++++-- .../dmn/io/xml/XMLDecisionConverter.java | 29 ++++++++------ .../powerflows/dmn/io/xml/model/XMLInput.java | 3 ++ ...eelExpressionEvaluationProviderSpec.groovy | 15 ++++++- ...ovyExpressionEvaluationProviderSpec.groovy | 39 ++++++++++++++---- ...iptExpressionEvaluationProviderSpec.groovy | 39 ++++++++++++++---- ...uelExpressionEvaluationProviderSpec.groovy | 15 ++++++- ...ralExpressionEvaluationProviderSpec.groovy | 23 ++++++++++- .../engine/model/decision/DecisionSpec.groovy | 19 ++++++++- .../model/decision/DecisionUtilSpec.groovy | 21 ++++++---- 22 files changed, 296 insertions(+), 124 deletions(-) delete mode 100644 src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/Entry.java diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/entry/InputEntryEvaluator.java b/src/main/java/org/powerflows/dmn/engine/evaluator/entry/InputEntryEvaluator.java index cbca67f..e8edd43 100644 --- a/src/main/java/org/powerflows/dmn/engine/evaluator/entry/InputEntryEvaluator.java +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/entry/InputEntryEvaluator.java @@ -66,7 +66,7 @@ public boolean evaluate(final InputEntry inputEntry, final Object inputValue = evaluationContext.get(inputEntry.getName()); final SpecifiedTypeValue typedInputValue = typeConverter.convert(inputValue); - final Object inputEntryValue = inputEntryExpressionEvaluator.evaluateEntry(inputEntry, evaluationContext); + final Object inputEntryValue = inputEntryExpressionEvaluator.evaluateInputEntry(inputEntry, evaluationContext); final SpecifiedTypeValue typedInputEntryValue; if (isBoolean(inputEntryValue)) { diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/entry/OutputEntryEvaluator.java b/src/main/java/org/powerflows/dmn/engine/evaluator/entry/OutputEntryEvaluator.java index 03cd063..9560032 100644 --- a/src/main/java/org/powerflows/dmn/engine/evaluator/entry/OutputEntryEvaluator.java +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/entry/OutputEntryEvaluator.java @@ -45,7 +45,7 @@ public EntryResult evaluate(final OutputEntry outputEntry, final Output output, final ExpressionEvaluationProvider expressionEvaluator = expressionEvaluationProviderFactory.getInstance(outputEntry.getExpression().getType()); final TypeConverter typeConverter = typeConverterFactory.getInstance(output.getType()); - final Serializable outputEntryValue = expressionEvaluator.evaluateEntry(outputEntry, evaluationContext); + final Serializable outputEntryValue = expressionEvaluator.evaluateOutputEntry(outputEntry, evaluationContext); //Needed for the output entry value validation. //Correct build means the output entry value has a type compatible with the output definition. diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/ExpressionEvaluationProvider.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/ExpressionEvaluationProvider.java index e3ddadc..0afee44 100644 --- a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/ExpressionEvaluationProvider.java +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/ExpressionEvaluationProvider.java @@ -18,7 +18,8 @@ import org.powerflows.dmn.engine.evaluator.context.EvaluationContext; import org.powerflows.dmn.engine.model.decision.field.Input; -import org.powerflows.dmn.engine.model.decision.rule.entry.Entry; +import org.powerflows.dmn.engine.model.decision.rule.entry.InputEntry; +import org.powerflows.dmn.engine.model.decision.rule.entry.OutputEntry; import java.io.Serializable; @@ -26,5 +27,7 @@ public interface ExpressionEvaluationProvider { Serializable evaluateInput(Input input, EvaluationContext evaluationContext); - Serializable evaluateEntry(Entry entry, EvaluationContext evaluationContext); + Serializable evaluateInputEntry(InputEntry inputEntry, EvaluationContext evaluationContext); + + Serializable evaluateOutputEntry(OutputEntry outputEntry, EvaluationContext evaluationContext); } diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/FeelExpressionEvaluationProvider.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/FeelExpressionEvaluationProvider.java index 9b18e23..a44efd5 100644 --- a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/FeelExpressionEvaluationProvider.java +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/FeelExpressionEvaluationProvider.java @@ -18,7 +18,8 @@ import org.powerflows.dmn.engine.evaluator.context.EvaluationContext; import org.powerflows.dmn.engine.model.decision.field.Input; -import org.powerflows.dmn.engine.model.decision.rule.entry.Entry; +import org.powerflows.dmn.engine.model.decision.rule.entry.InputEntry; +import org.powerflows.dmn.engine.model.decision.rule.entry.OutputEntry; import java.io.Serializable; @@ -30,7 +31,12 @@ public Serializable evaluateInput(final Input input, final EvaluationContext eva } @Override - public Serializable evaluateEntry(final Entry entry, final EvaluationContext evaluationContext) { + public Serializable evaluateInputEntry(final InputEntry inputEntry, final EvaluationContext evaluationContext) { + throw new UnsupportedOperationException(); + } + + @Override + public Serializable evaluateOutputEntry(final OutputEntry outputEntry, final EvaluationContext evaluationContext) { throw new UnsupportedOperationException(); } } diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/JuelExpressionEvaluationProvider.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/JuelExpressionEvaluationProvider.java index a237d92..6a6b768 100644 --- a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/JuelExpressionEvaluationProvider.java +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/JuelExpressionEvaluationProvider.java @@ -18,7 +18,8 @@ import org.powerflows.dmn.engine.evaluator.context.EvaluationContext; import org.powerflows.dmn.engine.model.decision.field.Input; -import org.powerflows.dmn.engine.model.decision.rule.entry.Entry; +import org.powerflows.dmn.engine.model.decision.rule.entry.InputEntry; +import org.powerflows.dmn.engine.model.decision.rule.entry.OutputEntry; import java.io.Serializable; @@ -30,7 +31,12 @@ public Serializable evaluateInput(final Input input, final EvaluationContext eva } @Override - public Serializable evaluateEntry(final Entry entry, final EvaluationContext evaluationContext) { + public Serializable evaluateInputEntry(final InputEntry inputEntry, final EvaluationContext evaluationContext) { + throw new UnsupportedOperationException(); + } + + @Override + public Serializable evaluateOutputEntry(final OutputEntry outputEntry, final EvaluationContext evaluationContext) { throw new UnsupportedOperationException(); } } diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/LiteralExpressionEvaluationProvider.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/LiteralExpressionEvaluationProvider.java index 4dc8008..da50cc3 100644 --- a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/LiteralExpressionEvaluationProvider.java +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/LiteralExpressionEvaluationProvider.java @@ -19,7 +19,8 @@ import lombok.extern.slf4j.Slf4j; import org.powerflows.dmn.engine.evaluator.context.EvaluationContext; import org.powerflows.dmn.engine.model.decision.field.Input; -import org.powerflows.dmn.engine.model.decision.rule.entry.Entry; +import org.powerflows.dmn.engine.model.decision.rule.entry.InputEntry; +import org.powerflows.dmn.engine.model.decision.rule.entry.OutputEntry; import java.io.Serializable; @@ -43,12 +44,23 @@ public Serializable evaluateInput(final Input input, final EvaluationContext eva } @Override - public Serializable evaluateEntry(final Entry entry, final EvaluationContext evaluationContext) { - log.debug("Starting evaluation of entry {} with evaluation context: {}", entry, evaluationContext); + public Serializable evaluateInputEntry(final InputEntry inputEntry, final EvaluationContext evaluationContext) { + log.debug("Starting evaluation of input entry with evaluation context: {}", inputEntry, evaluationContext); - final Serializable result = entry.getExpression().getValue(); + final Serializable result = inputEntry.getExpression().getValue(); - log.debug("Evaluated entry result: {}", result); + log.debug("Evaluated input entry result: {}", result); + + return result; + } + + @Override + public Serializable evaluateOutputEntry(final OutputEntry outputEntry, final EvaluationContext evaluationContext) { + log.debug("Starting evaluation of output entry with evaluation context: {}", outputEntry, evaluationContext); + + final Serializable result = outputEntry.getExpression().getValue(); + + log.debug("Evaluated output entry result: {}", result); return result; } diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/ScriptExpressionEvaluationProvider.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/ScriptExpressionEvaluationProvider.java index 3e49757..7725e23 100644 --- a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/ScriptExpressionEvaluationProvider.java +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/ScriptExpressionEvaluationProvider.java @@ -23,7 +23,8 @@ import org.powerflows.dmn.engine.evaluator.expression.script.bindings.ContextVariablesBindings; import org.powerflows.dmn.engine.model.decision.expression.Expression; import org.powerflows.dmn.engine.model.decision.field.Input; -import org.powerflows.dmn.engine.model.decision.rule.entry.Entry; +import org.powerflows.dmn.engine.model.decision.rule.entry.InputEntry; +import org.powerflows.dmn.engine.model.decision.rule.entry.OutputEntry; import javax.script.Bindings; import javax.script.ScriptEngine; @@ -52,21 +53,32 @@ public Serializable evaluateInput(final Input input, final EvaluationContext eva } @Override - public Serializable evaluateEntry(final Entry entry, final EvaluationContext evaluationContext) { - log.debug("Starting evaluation of entry {} with evaluation context: {}", entry, evaluationContext); + public Serializable evaluateInputEntry(final InputEntry inputEntry, final EvaluationContext evaluationContext) { + log.debug("Starting evaluation of input entry {} with evaluation context: {}", inputEntry, evaluationContext); - final Serializable result = evaluate(entry, evaluationContext); + final Serializable result = evaluate(inputEntry, evaluationContext); log.debug("Evaluated entry result: {}", result); return result; } - private Serializable evaluate(final Entry entry, final EvaluationContext evaluationContext) { - final ScriptEngine scriptEngine = scriptEngineProvider.getScriptEngine(entry.getExpression().getType()); - final Bindings bindings = ContextVariablesBindings.create(scriptEngine.createBindings(), evaluationContext, entry.getName()); + @Override + public Serializable evaluateOutputEntry(final OutputEntry outputEntry, final EvaluationContext evaluationContext) { + log.debug("Starting evaluation of output entry {} with evaluation context: {}", outputEntry, evaluationContext); + + final Serializable result = evaluate(outputEntry.getExpression(), evaluationContext); + + log.debug("Evaluated entry result: {}", result); + + return result; + } + + private Serializable evaluate(final InputEntry inputEntry, final EvaluationContext evaluationContext) { + final ScriptEngine scriptEngine = scriptEngineProvider.getScriptEngine(inputEntry.getExpression().getType()); + final Bindings bindings = ContextVariablesBindings.create(scriptEngine.createBindings(), evaluationContext, inputEntry); - return evaluate(entry.getExpression(), scriptEngine, bindings); + return evaluate(inputEntry.getExpression(), scriptEngine, bindings); } private Serializable evaluate(final Expression expression, final EvaluationContext evaluationContext) { diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/script/bindings/ContextVariablesBindings.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/script/bindings/ContextVariablesBindings.java index 76f83fb..75d8034 100644 --- a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/script/bindings/ContextVariablesBindings.java +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/script/bindings/ContextVariablesBindings.java @@ -17,6 +17,7 @@ package org.powerflows.dmn.engine.evaluator.expression.script.bindings; import org.powerflows.dmn.engine.evaluator.context.EvaluationContext; +import org.powerflows.dmn.engine.model.decision.rule.entry.InputEntry; import javax.script.Bindings; import java.util.Collection; @@ -26,7 +27,6 @@ public class ContextVariablesBindings implements Bindings { private final Bindings bindings; - private static final String DEFAULT_INPUT_NAME_ALIAS = "cellInput"; private ContextVariablesBindings(final Bindings bindings, final EvaluationContext evaluationContext) { this.bindings = bindings; @@ -37,10 +37,12 @@ private ContextVariablesBindings(final Bindings bindings, final EvaluationContex .forEach(variableName -> this.bindings.put(variableName, evaluationContext.get(variableName))); } - private ContextVariablesBindings(final Bindings bindings, final EvaluationContext evaluationContext, final String inputName) { + private ContextVariablesBindings(final Bindings bindings, + final EvaluationContext evaluationContext, + final InputEntry inputEntry) { this(bindings, evaluationContext); - this.bindings.put(DEFAULT_INPUT_NAME_ALIAS, evaluationContext.get(inputName)); + this.bindings.put(inputEntry.getNameAlias(), evaluationContext.get(inputEntry.getName())); } public static ContextVariablesBindings create(final Bindings bindings, @@ -50,8 +52,8 @@ public static ContextVariablesBindings create(final Bindings bindings, public static ContextVariablesBindings create(final Bindings bindings, final EvaluationContext evaluationContext, - final String inputName) { - return new ContextVariablesBindings(bindings, evaluationContext, inputName); + final InputEntry inputEntry) { + return new ContextVariablesBindings(bindings, evaluationContext, inputEntry); } @Override diff --git a/src/main/java/org/powerflows/dmn/engine/model/decision/DecisionUtil.java b/src/main/java/org/powerflows/dmn/engine/model/decision/DecisionUtil.java index 16b57dd..31d471e 100644 --- a/src/main/java/org/powerflows/dmn/engine/model/decision/DecisionUtil.java +++ b/src/main/java/org/powerflows/dmn/engine/model/decision/DecisionUtil.java @@ -24,7 +24,6 @@ import org.powerflows.dmn.engine.model.decision.rule.entry.OutputEntry; import java.lang.reflect.Field; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -39,7 +38,7 @@ static void assignDefaults(final List inputs, final List rules, final ExpressionType decisionExpressionType, final EvaluationMode decisionEvaluationMode) { - final Field expressionTypeField = findField(Expression.class, ExpressionType.class); + final Field expressionTypeField = findField(Expression.class, "type"); expressionTypeField.setAccessible(true); assignInputsDefaults(inputs, decisionExpressionType, decisionEvaluationMode, expressionTypeField); @@ -51,7 +50,7 @@ private static void assignInputsDefaults(final List inputs, final ExpressionType decisionExpressionType, final EvaluationMode decisionEvaluationMode, final Field expressionTypeField) { - final Field inputEvaluationModeField = findField(Input.class, EvaluationMode.class); + final Field inputEvaluationModeField = findField(Input.class, "evaluationMode"); inputEvaluationModeField.setAccessible(true); @@ -72,9 +71,11 @@ private static void assignEntriesDefaults(final List inputs, final List rules, final ExpressionType decisionExpressionType, final Field expressionTypeField) { - final Field inputEntryEvaluationModeField = findField(InputEntry.class, EvaluationMode.class); + final Field inputEntryEvaluationModeField = findField(InputEntry.class, "evaluationMode"); + final Field inputEntryNameAliasField = findField(InputEntry.class, "nameAlias"); inputEntryEvaluationModeField.setAccessible(true); + inputEntryNameAliasField.setAccessible(true); final Map inputsMap = inputs .stream() @@ -93,6 +94,12 @@ private static void assignEntriesDefaults(final List inputs, inputEntry, inputsMap.get(inputEntry.getName()).getEvaluationMode()); } + + if (inputEntry.getNameAlias() == null) { + setValue(inputEntryNameAliasField, + inputEntry, + inputsMap.get(inputEntry.getName()).getNameAlias()); + } } for (OutputEntry outputEntry : rule.getOutputEntries()) { @@ -105,13 +112,15 @@ private static void assignEntriesDefaults(final List inputs, } inputEntryEvaluationModeField.setAccessible(false); + inputEntryNameAliasField.setAccessible(false); } - private static Field findField(final Class clazz, final Class fieldClass) { - return Arrays.stream(clazz.getDeclaredFields()) - .filter(f -> f.getType() == fieldClass) - .findFirst() - .orElseThrow(() -> new DecisionBuildException("Can not find " + fieldClass + " in " + clazz)); + private static Field findField(final Class clazz, final String fieldName) { + try { + return clazz.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + throw new DecisionBuildException("Can not find " + fieldName + " in " + clazz); + } } private static void setValue(final Field field, final Object object, final Object value) { diff --git a/src/main/java/org/powerflows/dmn/engine/model/decision/field/Input.java b/src/main/java/org/powerflows/dmn/engine/model/decision/field/Input.java index 924a227..b7138c4 100644 --- a/src/main/java/org/powerflows/dmn/engine/model/decision/field/Input.java +++ b/src/main/java/org/powerflows/dmn/engine/model/decision/field/Input.java @@ -33,6 +33,7 @@ public class Input implements Serializable { private static final long serialVersionUID = 1; private String name; + private String nameAlias = "cellInput"; private String description; private ValueType type; private Expression expression; @@ -45,6 +46,10 @@ public String getName() { return name; } + public String getNameAlias() { + return nameAlias; + } + public String getDescription() { return description; } @@ -82,6 +87,12 @@ public B name(String name) { return (B) this; } + public B nameAlias(String nameAlias) { + this.product.nameAlias = nameAlias; + + return (B) this; + } + public B description(String description) { this.product.description = description; @@ -107,6 +118,7 @@ protected Input assembleProduct() { } validateIsNonNull(product.name, "Name is required"); + validateIsNonNull(product.nameAlias, "Name alias is required"); validateIsNonNull(product.type, "Type is required"); return product; diff --git a/src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/Entry.java b/src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/Entry.java deleted file mode 100644 index 3489597..0000000 --- a/src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/Entry.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2018-present PowerFlows.org - all rights reserved. - * - * 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.powerflows.dmn.engine.model.decision.rule.entry; - -import lombok.ToString; -import org.powerflows.dmn.engine.model.decision.expression.Expression; - -import java.io.Serializable; - -@ToString -public class Entry implements Serializable { - - protected String name; - protected Expression expression; - - Entry() { - } - - public String getName() { - return name; - } - - public Expression getExpression() { - return expression; - } -} diff --git a/src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/InputEntry.java b/src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/InputEntry.java index 6d32fd4..f70e33e 100644 --- a/src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/InputEntry.java +++ b/src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/InputEntry.java @@ -27,18 +27,33 @@ import java.util.function.Consumer; import java.util.function.Function; -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) -public class InputEntry extends Entry { +@EqualsAndHashCode +@ToString +public class InputEntry implements Serializable { private static final long serialVersionUID = 1; + private String name; + private String nameAlias; + private Expression expression; private EvaluationMode evaluationMode; private InputEntry() { super(); } + public String getName() { + return name; + } + + public String getNameAlias() { + return nameAlias; + } + + public Expression getExpression() { + return expression; + } + public EvaluationMode getEvaluationMode() { return evaluationMode; } @@ -64,6 +79,12 @@ public B name(String name) { return (B) this; } + public B nameAlias(String nameAlias) { + this.product.nameAlias = nameAlias; + + return (B) this; + } + public B evaluationMode(EvaluationMode evaluationMode) { this.product.evaluationMode = evaluationMode; diff --git a/src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/OutputEntry.java b/src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/OutputEntry.java index e8352c0..4146afe 100644 --- a/src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/OutputEntry.java +++ b/src/main/java/org/powerflows/dmn/engine/model/decision/rule/entry/OutputEntry.java @@ -26,14 +26,24 @@ import java.util.function.Consumer; import java.util.function.Function; -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) -public class OutputEntry extends Entry { +@EqualsAndHashCode +@ToString +public class OutputEntry implements Serializable { private static final long serialVersionUID = 1; + private String name; + private Expression expression; + private OutputEntry() { - super(); + } + + public String getName() { + return name; + } + + public Expression getExpression() { + return expression; } public static

FluentBuilder

fluentBuilder(final P parentBuilder, final Consumer outputEntryConsumer) { diff --git a/src/main/java/org/powerflows/dmn/io/xml/XMLDecisionConverter.java b/src/main/java/org/powerflows/dmn/io/xml/XMLDecisionConverter.java index 8a4438f..8368fb9 100644 --- a/src/main/java/org/powerflows/dmn/io/xml/XMLDecisionConverter.java +++ b/src/main/java/org/powerflows/dmn/io/xml/XMLDecisionConverter.java @@ -117,21 +117,26 @@ public String toString() { private void processRules(final Decision.Builder builder, final List inputs, final List outputs, final List rules) { final Set inputNames = new LinkedHashSet<>(); final Supplier inputNameSequence = makeSequenceNameSupplier("input_"); + inputs.forEach(input -> { final String name = selectOrCreateUniqueName(inputNames, input.getId(), null, inputNameSequence); - builder.withInput(inputBuilder -> - inputBuilder - .description(input.getLabel()) - .name(name) - .withExpression(expressionBuilder -> - expressionBuilder - .value(input.getInputExpression().getText()) - .type(resolveExpressionType(input.getInputExpression().getExpressionLanguage())) - .build()) - .type(resolveType(input.getInputExpression().getTypeRef())) - .build() - ); + builder.withInput(inputBuilder -> { + if (input.getInputVariable() != null) { + inputBuilder.nameAlias(input.getInputVariable()); + } + + return inputBuilder + .description(input.getLabel()) + .name(name) + .withExpression(expressionBuilder -> + expressionBuilder + .value(input.getInputExpression().getText()) + .type(resolveExpressionType(input.getInputExpression().getExpressionLanguage())) + .build()) + .type(resolveType(input.getInputExpression().getTypeRef())) + .build(); + }); }); final Set outputNames = new LinkedHashSet<>(); diff --git a/src/main/java/org/powerflows/dmn/io/xml/model/XMLInput.java b/src/main/java/org/powerflows/dmn/io/xml/model/XMLInput.java index bf9e2c4..d193fbc 100644 --- a/src/main/java/org/powerflows/dmn/io/xml/model/XMLInput.java +++ b/src/main/java/org/powerflows/dmn/io/xml/model/XMLInput.java @@ -39,6 +39,9 @@ public class XMLInput { @XmlAttribute private String label; + @XmlAttribute(namespace = "http://camunda.org/schema/1.0/dmn") + private String inputVariable; + @XmlElement private XMLInputValues inputValues; diff --git a/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/FeelExpressionEvaluationProviderSpec.groovy b/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/FeelExpressionEvaluationProviderSpec.groovy index 877fd1e..3142ec8 100644 --- a/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/FeelExpressionEvaluationProviderSpec.groovy +++ b/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/FeelExpressionEvaluationProviderSpec.groovy @@ -33,11 +33,22 @@ class FeelExpressionEvaluationProviderSpec extends Specification { exception != null } - void 'should throw exception for evaluate entry'() { + void 'should throw exception for evaluate input entry'() { given: when: - expressionEvaluationProvider.evaluateEntry(null, null) + expressionEvaluationProvider.evaluateInputEntry(null, null) + + then: + final UnsupportedOperationException exception = thrown() + exception != null + } + + void 'should throw exception for evaluate output entry'() { + given: + + when: + expressionEvaluationProvider.evaluateOutputEntry(null, null) then: final UnsupportedOperationException exception = thrown() diff --git a/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/GroovyExpressionEvaluationProviderSpec.groovy b/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/GroovyExpressionEvaluationProviderSpec.groovy index 74384e0..5e5ef77 100644 --- a/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/GroovyExpressionEvaluationProviderSpec.groovy +++ b/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/GroovyExpressionEvaluationProviderSpec.groovy @@ -38,17 +38,17 @@ class GroovyExpressionEvaluationProviderSpec extends Specification { new ScriptExpressionEvaluationProvider(scriptEngineProvider) @Unroll - void 'should evaluate entry groovy expression value #entryExpressionValue and variables #contextVariable with #expectedEntryResult'( + void 'should evaluate input entry groovy expression value #entryExpressionValue and variables #contextVariable with #expectedEntryResult'( final Object entryExpressionValue, final Object contextVariable, final boolean expectedEntryResult) { given: final Expression entryExpression = [value: entryExpressionValue, type: ExpressionType.GROOVY] - final InputEntry inputEntry = [expression: entryExpression] as InputEntry + final InputEntry inputEntry = [expression: entryExpression, nameAlias: 'cellInput'] final DecisionVariables decisionVariables = new DecisionVariables([x: contextVariable, TestInputName: true]) final EvaluationContext evaluationContext = new EvaluationContext(decisionVariables) when: - final boolean inputEntryResult = expressionEvaluationProvider.evaluateEntry(inputEntry, evaluationContext) + final boolean inputEntryResult = expressionEvaluationProvider.evaluateInputEntry(inputEntry, evaluationContext) then: inputEntryResult == expectedEntryResult @@ -62,21 +62,46 @@ class GroovyExpressionEvaluationProviderSpec extends Specification { '(2 + 5) == x' | 7 || true } - void 'should evaluate entry groovy expression with default alias usage'() { + void 'should evaluate input entry groovy expression with default alias usage'() { given: final Expression expression = [value: "cellInput == 'something'", type: ExpressionType.GROOVY] - final InputEntry inputEntry = [name: 'TestInputName', expression: expression] + final InputEntry inputEntry = [name: 'TestInputName', expression: expression, nameAlias: 'cellInput'] final DecisionVariables decisionVariables = new DecisionVariables(['TestInputName': 'something']) final EvaluationContext contextVariables = new EvaluationContext(decisionVariables) when: - final boolean inputEntryResult = expressionEvaluationProvider.evaluateEntry(inputEntry, contextVariables) + final boolean inputEntryResult = expressionEvaluationProvider.evaluateInputEntry(inputEntry, contextVariables) then: inputEntryResult } + @Unroll + void 'should evaluate output entry groovy expression value #entryExpressionValue and variables #contextVariable with #expectedEntryResult'( + final Object entryExpressionValue, final Object contextVariable, final boolean expectedEntryResult) { + given: + final Expression entryExpression = [value: entryExpressionValue, type: ExpressionType.GROOVY] + final OutputEntry outputEntry = [expression: entryExpression] + + final DecisionVariables decisionVariables = new DecisionVariables([x: contextVariable, TestInputName: true]) + final EvaluationContext evaluationContext = new EvaluationContext(decisionVariables) + + when: + final boolean outputEntryResult = expressionEvaluationProvider.evaluateOutputEntry(outputEntry, evaluationContext) + + then: + outputEntryResult == expectedEntryResult + 0 * _ + + where: + entryExpressionValue | contextVariable || expectedEntryResult + '2 < x' | 4 || true + '2 == x' | 2 || true + '2 == x' | 3 || false + '(2 + 5) == x' | 7 || true + } + @Unroll void 'should evaluate input groovy expression value #inputExpression and variables #contextVariable with #expectedInputResult'( final Object inputExpression, final Object contextVariable, final boolean expectedInputResult) { @@ -109,7 +134,7 @@ class GroovyExpressionEvaluationProviderSpec extends Specification { final OutputEntry outputEntry = [expression: entryExpression] as OutputEntry when: - expressionEvaluationProvider.evaluateEntry(outputEntry, contextVariables) + expressionEvaluationProvider.evaluateOutputEntry(outputEntry, contextVariables) then: final EvaluationException exception = thrown() diff --git a/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/JavaScriptExpressionEvaluationProviderSpec.groovy b/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/JavaScriptExpressionEvaluationProviderSpec.groovy index c3bf299..4084b1f 100644 --- a/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/JavaScriptExpressionEvaluationProviderSpec.groovy +++ b/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/JavaScriptExpressionEvaluationProviderSpec.groovy @@ -38,17 +38,17 @@ class JavaScriptExpressionEvaluationProviderSpec extends Specification { new ScriptExpressionEvaluationProvider(scriptEngineProvider) @Unroll - void 'should evaluate entry javascript expression value #entryExpressionValue and variables #contextVariable with #expectedEntryResult'( + void 'should evaluate input entry javascript expression value #entryExpressionValue and variables #contextVariable with #expectedEntryResult'( final Object entryExpressionValue, final Object contextVariable, final boolean expectedEntryResult) { given: final Expression entryExpression = [value: entryExpressionValue, type: ExpressionType.JAVASCRIPT] - final InputEntry inputEntry = [expression: entryExpression] as InputEntry + final InputEntry inputEntry = [expression: entryExpression, nameAlias: 'cellInput'] final DecisionVariables decisionVariables = new DecisionVariables([x: contextVariable, TestInputName: true]) final EvaluationContext evaluationContext = new EvaluationContext(decisionVariables) when: - final boolean inputEntryResult = expressionEvaluationProvider.evaluateEntry(inputEntry, evaluationContext) + final boolean inputEntryResult = expressionEvaluationProvider.evaluateInputEntry(inputEntry, evaluationContext) then: inputEntryResult == expectedEntryResult @@ -62,21 +62,46 @@ class JavaScriptExpressionEvaluationProviderSpec extends Specification { '(2 + 5) == x' | 7 || true } - void 'should evaluate entry javascript expression with default alias usage'() { + void 'should evaluate input entry javascript expression with default alias usage'() { given: final Expression expression = [value: "cellInput == 'something'", type: ExpressionType.JAVASCRIPT] - final InputEntry inputEntry = [name: 'TestInputName', expression: expression] + final InputEntry inputEntry = [name: 'TestInputName', expression: expression, nameAlias: 'cellInput'] final DecisionVariables decisionVariables = new DecisionVariables(['TestInputName': 'something']) final EvaluationContext contextVariables = new EvaluationContext(decisionVariables) when: - final boolean inputEntryResult = expressionEvaluationProvider.evaluateEntry(inputEntry, contextVariables) + final boolean inputEntryResult = expressionEvaluationProvider.evaluateInputEntry(inputEntry, contextVariables) then: inputEntryResult } + @Unroll + void 'should evaluate output entry javascript expression value #entryExpressionValue and variables #contextVariable with #expectedEntryResult'( + final Object entryExpressionValue, final Object contextVariable, final boolean expectedEntryResult) { + given: + final Expression entryExpression = [value: entryExpressionValue, type: ExpressionType.JAVASCRIPT] + final OutputEntry outputEntry = [expression: entryExpression] + + final DecisionVariables decisionVariables = new DecisionVariables([x: contextVariable, TestInputName: true]) + final EvaluationContext evaluationContext = new EvaluationContext(decisionVariables) + + when: + final boolean outputEntryResult = expressionEvaluationProvider.evaluateOutputEntry(outputEntry, evaluationContext) + + then: + outputEntryResult == expectedEntryResult + 0 * _ + + where: + entryExpressionValue | contextVariable || expectedEntryResult + '2 < x' | 4 || true + '2 == x' | 2 || true + '2 == x' | 3 || false + '(2 + 5) == x' | 7 || true + } + @Unroll void 'should evaluate input javascript expression value #inputExpression and variables #contextVariable with #expectedInputResult'( final Object inputExpression, final Object contextVariable, final boolean expectedInputResult) { @@ -110,7 +135,7 @@ class JavaScriptExpressionEvaluationProviderSpec extends Specification { final EvaluationContext contextVariables = new EvaluationContext(decisionVariables) when: - expressionEvaluationProvider.evaluateEntry(outputEntry, contextVariables) + expressionEvaluationProvider.evaluateOutputEntry(outputEntry, contextVariables) then: final EvaluationException exception = thrown() diff --git a/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/JuelExpressionEvaluationProviderSpec.groovy b/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/JuelExpressionEvaluationProviderSpec.groovy index 0afbf82..b2dd380 100644 --- a/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/JuelExpressionEvaluationProviderSpec.groovy +++ b/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/JuelExpressionEvaluationProviderSpec.groovy @@ -33,11 +33,22 @@ class JuelExpressionEvaluationProviderSpec extends Specification { exception != null } - void 'should throw exception for evaluate entry'() { + void 'should throw exception for evaluate input entry'() { given: when: - expressionEvaluationProvider.evaluateEntry(null, null) + expressionEvaluationProvider.evaluateInputEntry(null, null) + + then: + final UnsupportedOperationException exception = thrown() + exception != null + } + + void 'should throw exception for evaluate output entry'() { + given: + + when: + expressionEvaluationProvider.evaluateOutputEntry(null, null) then: final UnsupportedOperationException exception = thrown() diff --git a/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/LiteralExpressionEvaluationProviderSpec.groovy b/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/LiteralExpressionEvaluationProviderSpec.groovy index f9309fa..a15d258 100644 --- a/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/LiteralExpressionEvaluationProviderSpec.groovy +++ b/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/LiteralExpressionEvaluationProviderSpec.groovy @@ -21,6 +21,7 @@ import org.powerflows.dmn.engine.model.decision.expression.Expression import org.powerflows.dmn.engine.model.decision.expression.ExpressionType import org.powerflows.dmn.engine.model.decision.field.Input import org.powerflows.dmn.engine.model.decision.rule.entry.InputEntry +import org.powerflows.dmn.engine.model.decision.rule.entry.OutputEntry import org.powerflows.dmn.engine.model.evaluation.variable.DecisionVariables import spock.lang.Specification @@ -29,7 +30,7 @@ class LiteralExpressionEvaluationProviderSpec extends Specification { private final ExpressionEvaluationProvider expressionEvaluationProvider = new LiteralExpressionEvaluationProvider() - void 'should evaluate entry literal expression value'() { + void 'should evaluate input entry literal expression value'() { given: final Object inputEntryValue = 5 final Object contextVariable = 6 @@ -40,7 +41,25 @@ class LiteralExpressionEvaluationProviderSpec extends Specification { final EvaluationContext evaluationContext = new EvaluationContext(decisionVariables) when: - final boolean inputEntryResult = expressionEvaluationProvider.evaluateEntry(inputEntry, evaluationContext) + final boolean inputEntryResult = expressionEvaluationProvider.evaluateInputEntry(inputEntry, evaluationContext) + + then: + inputEntryResult + 0 * _ + } + + void 'should evaluate output entry literal expression value'() { + given: + final Object inputEntryValue = 5 + final Object contextVariable = 6 + final Expression entryExpression = [value: inputEntryValue, type: ExpressionType.LITERAL] + final OutputEntry outputEntry = [expression: entryExpression] + + final DecisionVariables decisionVariables = new DecisionVariables([TestInputName: contextVariable]) + final EvaluationContext evaluationContext = new EvaluationContext(decisionVariables) + + when: + final boolean inputEntryResult = expressionEvaluationProvider.evaluateOutputEntry(outputEntry, evaluationContext) then: inputEntryResult diff --git a/src/test/groovy/org/powerflows/dmn/engine/model/decision/DecisionSpec.groovy b/src/test/groovy/org/powerflows/dmn/engine/model/decision/DecisionSpec.groovy index 2cb777f..9954da4 100644 --- a/src/test/groovy/org/powerflows/dmn/engine/model/decision/DecisionSpec.groovy +++ b/src/test/groovy/org/powerflows/dmn/engine/model/decision/DecisionSpec.groovy @@ -28,12 +28,14 @@ import spock.lang.Specification class DecisionSpec extends Specification { + final String defaultNameAlias = 'cellInput' final String someTableId = 'some_table_id' final String someTableName = 'Some Table Name' final HitPolicy someHitPolicy = HitPolicy.UNIQUE final ExpressionType someExpressionType = ExpressionType.GROOVY final ValueType someInput1Type = ValueType.INTEGER final String someInput1Name = 'Some Input 1 Name' + final String someInput1NameAlias = 'Some Input 1 Name Alias' final String someInput1Description = 'Some Input 1 Description' final ExpressionType someInput1Expression1Type = ExpressionType.LITERAL final int someInput1Expression1Value = 5 @@ -52,6 +54,7 @@ class DecisionSpec extends Specification { final ExpressionType someRule1InputEntry1ExpressionType = ExpressionType.GROOVY final String someRule1InputEntry1ExpressionValue = '> 20' final String someRule1InputEntry2Name = someInput2Name + final String someRule1InputEntry2NameAlias = someInput2Name + 'Alias' final ExpressionType someRule1InputEntry2ExpressionType = ExpressionType.FEEL final String someRule1InputEntry2ExpressionValue = 'not("blue", "purple")' final String someRule1OutputEntry1Name = someOutput1Name @@ -77,6 +80,7 @@ class DecisionSpec extends Specification { .expressionType(someExpressionType) .withInputs() .name(someInput1Name) + .nameAlias(someInput1NameAlias) .description(someInput1Description) .type(someInput1Type) .withExpression() @@ -107,6 +111,7 @@ class DecisionSpec extends Specification { .and() .next() .name(someRule1InputEntry2Name) + .nameAlias(someRule1InputEntry2NameAlias) .withExpression() .type(someRule1InputEntry2ExpressionType) .value(someRule1InputEntry2ExpressionValue) @@ -153,6 +158,7 @@ class DecisionSpec extends Specification { final Input input1 = decision.getInputs().get(0) with(input1) { getName() == someInput1Name + getNameAlias() == someInput1NameAlias getDescription() == someInput1Description getType() == someInput1Type @@ -167,6 +173,7 @@ class DecisionSpec extends Specification { final Input input2 = decision.getInputs().get(1) with(input2) { getName() == someInput2Name + getNameAlias() == defaultNameAlias getDescription() == someInput2Description getType() == someInput2Type } @@ -201,6 +208,7 @@ class DecisionSpec extends Specification { final InputEntry rule1InputEntry1 = rule1.getInputEntries().get(0) rule1InputEntry1.getName() == someRule1InputEntry1Name + rule1InputEntry1.getNameAlias() == input1.getNameAlias() final Expression rule1InputEntry1Expression = rule1InputEntry1.getExpression() with(rule1InputEntry1Expression) { @@ -210,6 +218,7 @@ class DecisionSpec extends Specification { final InputEntry rule1InputEntry2 = rule1.getInputEntries().get(1) rule1InputEntry2.getName() == someRule1InputEntry2Name + rule1InputEntry2.getNameAlias() == someRule1InputEntry2NameAlias final Expression rule1InputEntry2Expression = rule1InputEntry2.getExpression() with(rule1InputEntry2Expression) { @@ -234,10 +243,16 @@ class DecisionSpec extends Specification { } final InputEntry rule2InputEntry1 = rule2.getInputEntries().get(0) - rule2InputEntry1.getName() == someRule2InputEntry1Name + with(rule2InputEntry1){ + getName() == someRule2InputEntry1Name + getNameAlias() == input1.getNameAlias() + } final InputEntry rule2InputEntry2 = rule2.getInputEntries().get(1) - rule2InputEntry2.getName() == someRule2InputEntry2Name + with(rule2InputEntry2){ + getName() == someRule2InputEntry2Name + getNameAlias() == input2.getNameAlias() + } final OutputEntry rule2OutputEntry1 = rule2.getOutputEntries().get(0) rule2OutputEntry1.getName() == someRule2OutputEntry1Name diff --git a/src/test/groovy/org/powerflows/dmn/engine/model/decision/DecisionUtilSpec.groovy b/src/test/groovy/org/powerflows/dmn/engine/model/decision/DecisionUtilSpec.groovy index 68176e3..495988c 100644 --- a/src/test/groovy/org/powerflows/dmn/engine/model/decision/DecisionUtilSpec.groovy +++ b/src/test/groovy/org/powerflows/dmn/engine/model/decision/DecisionUtilSpec.groovy @@ -33,17 +33,20 @@ class DecisionUtilSpec extends Specification { given: final String input1Name = 'x' final String input2Name = 'y' + final String input1NameAlias = 'xAlias' + final String input2NameAlias = 'yAlias' final Expression input1Expression = [type: ExpressionType.FEEL] final Expression input2Expression = [type: null] - final Input input1 = [name: input1Name, type: ValueType.INTEGER, expression: input1Expression, evaluationMode: EvaluationMode.BOOLEAN] - final Input input2 = [name: input2Name, type: ValueType.INTEGER, expression: input2Expression] + final Input input1 = [name: input1Name, type: ValueType.INTEGER, expression: input1Expression, nameAlias: input1NameAlias, evaluationMode: EvaluationMode.BOOLEAN] + final Input input2 = [name: input2Name, type: ValueType.INTEGER, expression: input2Expression, nameAlias: input2NameAlias] final Integer inputEntry1Value = 1 final Integer inputEntry2Value = 2 final Expression expression1 = [value: inputEntry1Value, type: null] final InputEntry inputEntry1 = [name: input1Name, expression: expression1, evaluationMode: EvaluationMode.BOOLEAN] final Expression expression2 = [value: inputEntry2Value, type: ExpressionType.LITERAL] - final InputEntry inputEntry2 = [name: input2Name, expression: expression2] + final String inputEntry2NameAlias = 'yInputEntryAlias' + final InputEntry inputEntry2 = [name: input2Name, expression: expression2, nameAlias: inputEntry2NameAlias] final Boolean outputEntry1Value = true final String outputEntry2Value = 'test' @@ -74,8 +77,10 @@ class DecisionUtilSpec extends Specification { inputEntry1.expression.type == ExpressionType.FEEL inputEntry1.evaluationMode == EvaluationMode.BOOLEAN + inputEntry1.nameAlias == input1NameAlias inputEntry2.expression.type == ExpressionType.LITERAL inputEntry2.evaluationMode == EvaluationMode.INPUT_COMPARISON + inputEntry2.nameAlias == inputEntry2NameAlias outputEntry1.expression.type == ExpressionType.LITERAL outputEntry2.expression.type == ExpressionType.GROOVY @@ -83,7 +88,7 @@ class DecisionUtilSpec extends Specification { void 'should throw exception when can not set field value'() { given: - final Field field = DecisionUtil.findField(InputEntry.class, EvaluationMode.class); + final Field field = DecisionUtil.findField(InputEntry.class, 'evaluationMode'); final InputEntry inputEntry = [] final Integer value = 5 @@ -93,20 +98,20 @@ class DecisionUtilSpec extends Specification { then: final DecisionBuildException exception = thrown() exception != null - exception.getMessage() == 'Can not set value 5 for InputEntry(super=Entry(name=null, expression=null), evaluationMode=null)' + exception.getMessage() == 'Can not set value 5 for InputEntry(name=null, nameAlias=null, expression=null, evaluationMode=null)' } void 'should throw exception when can not find field by type'() { given: final Class clazz = InputEntry.class - final Class fieldClass = HitPolicy.class + final String fieldName = 'hitPolicy' when: - DecisionUtil.findField(clazz, fieldClass); + DecisionUtil.findField(clazz, fieldName); then: final DecisionBuildException exception = thrown() exception != null - exception.getMessage() == 'Can not find class org.powerflows.dmn.engine.model.decision.HitPolicy in class org.powerflows.dmn.engine.model.decision.rule.entry.InputEntry' + exception.getMessage() == 'Can not find hitPolicy in class org.powerflows.dmn.engine.model.decision.rule.entry.InputEntry' } } From 0b038958852ea408dabaed8b7669d2008da53768 Mon Sep 17 00:00:00 2001 From: Mariusz Kumor Date: Mon, 14 Jan 2019 22:46:55 +0100 Subject: [PATCH 05/10] Mapped input label from XML model to name in PowerFlows model. Resolved #136 --- .../dmn/io/xml/XMLDecisionConverter.java | 16 ++++++-- .../dmn/io/xml/XmlDecisionReaderSpec.groovy | 40 +++++++++++++------ .../camunda-dmn-1.1-example-duplicate-ids.dmn | 2 +- ...munda-dmn-1.1-example-duplicate-labels.dmn | 36 +++++++++++++++++ 4 files changed, 77 insertions(+), 17 deletions(-) create mode 100644 src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example-duplicate-labels.dmn diff --git a/src/main/java/org/powerflows/dmn/io/xml/XMLDecisionConverter.java b/src/main/java/org/powerflows/dmn/io/xml/XMLDecisionConverter.java index 8368fb9..2811cbd 100644 --- a/src/main/java/org/powerflows/dmn/io/xml/XMLDecisionConverter.java +++ b/src/main/java/org/powerflows/dmn/io/xml/XMLDecisionConverter.java @@ -119,7 +119,7 @@ private void processRules(final Decision.Builder builder, final List i final Supplier inputNameSequence = makeSequenceNameSupplier("input_"); inputs.forEach(input -> { - final String name = selectOrCreateUniqueName(inputNames, input.getId(), null, inputNameSequence); + final String name = selectOrCreateUniqueName(inputNames, input.getId(), null, input.getLabel(), inputNameSequence); builder.withInput(inputBuilder -> { if (input.getInputVariable() != null) { @@ -143,7 +143,7 @@ private void processRules(final Decision.Builder builder, final List i final Supplier outputNameSequence = makeSequenceNameSupplier("output_"); outputs.forEach(output -> { - final String name = selectOrCreateUniqueName(outputNames, output.getId(), output.getName(), outputNameSequence); + final String name = selectOrCreateUniqueName(outputNames, output.getId(), output.getName(), output.getLabel(), outputNameSequence); builder.withOutput(outputBuilder -> outputBuilder @@ -157,10 +157,18 @@ private void processRules(final Decision.Builder builder, final List i rules.forEach(this.ruleProcessor(new ArrayList<>(inputNames), new ArrayList<>(outputNames), builder)); } - private String selectOrCreateUniqueName(final Set names, final String id, final String name, final Supplier nameSequence) { + private String selectOrCreateUniqueName(final Set names, final String id, final String name, final String label, final Supplier nameSequence) { final String resultName; - if (name != null) { + if (label != null) { + final String sanitizedLabel = label.replaceAll("[^a-zA-Z0-9\\-]", "_"); + if (names.contains(sanitizedLabel)) { + log.warn("Names collection {} already contains name {}, using one from sequence {}", names, sanitizedLabel, nameSequence); + resultName = getNextUniqueName(nameSequence, names); + } else { + resultName = sanitizedLabel; + } + } else if (name != null) { if (names.contains(name)) { log.warn("Names collection {} already contains name {}, using one from sequence {}", names, name, nameSequence); resultName = getNextUniqueName(nameSequence, names); diff --git a/src/test/groovy/org/powerflows/dmn/io/xml/XmlDecisionReaderSpec.groovy b/src/test/groovy/org/powerflows/dmn/io/xml/XmlDecisionReaderSpec.groovy index 6123b2a..071519e 100644 --- a/src/test/groovy/org/powerflows/dmn/io/xml/XmlDecisionReaderSpec.groovy +++ b/src/test/groovy/org/powerflows/dmn/io/xml/XmlDecisionReaderSpec.groovy @@ -26,11 +26,12 @@ import spock.lang.Shared import spock.lang.Specification class XmlDecisionReaderSpec extends Specification { - final static String omgExampleXml = 'omg-dmn-1.1-example.dmn' - final static String camundaExampleXml = 'camunda-dmn-1.1-example.dmn' - final static String camundaExampleXmlBadInputColumns = 'camunda-dmn-1.1-example-bad-input-columns.dmn' - final static String camundaExampleXmlBadOutputColumns = 'camunda-dmn-1.1-example-bad-output-columns.dmn' - final static String camundaExampleXmlDuplicateIds = 'camunda-dmn-1.1-example-duplicate-ids.dmn' + final String omgExampleXml = 'omg-dmn-1.1-example.dmn' + final String camundaExampleXml = 'camunda-dmn-1.1-example.dmn' + final String camundaExampleXmlBadInputColumns = 'camunda-dmn-1.1-example-bad-input-columns.dmn' + final String camundaExampleXmlBadOutputColumns = 'camunda-dmn-1.1-example-bad-output-columns.dmn' + final String camundaExampleXmlDuplicateIds = 'camunda-dmn-1.1-example-duplicate-ids.dmn' + final String camundaExampleXmlDuplicateLabels = 'camunda-dmn-1.1-example-duplicate-labels.dmn' @Shared private XmlDecisionReader reader @@ -121,6 +122,21 @@ class XmlDecisionReaderSpec extends Specification { result.ifPresent({ d -> assertFirstCamundaExampleContents(d) }) } + void 'should generate correctly names if duplicated labels'() { + given: + final InputStream inputStream = this.class.getResourceAsStream(camundaExampleXmlDuplicateLabels) + + when: + final Optional result = reader.read(inputStream) + + then: + result.isPresent() + with(result.get()) { + getInputs()[0].name == 'duplicate' + getInputs()[1].name == 'input_0' + } + } + void 'should read single decision by id from Camunda example stream'() { given: final InputStream inputStream = this.class.getResourceAsStream(camundaExampleXml) @@ -201,7 +217,7 @@ class XmlDecisionReaderSpec extends Specification { } with(result.getInputs()[0]) { - getName() == 'input_1' + getName() == 'Some_Input_1_Description' getDescription() == 'Some Input 1 Description' getType() == ValueType.INTEGER getExpression().getType() == ExpressionType.FEEL @@ -223,13 +239,13 @@ class XmlDecisionReaderSpec extends Specification { } with(result.getOutputs()[0]) { - getName() == 'output_0' + getName() == 'Some_Output_1_Description' getType() == ValueType.BOOLEAN } with(result.getOutputs()[1]) { - getName() == 'output_1' + getName() == 'output_0' getType() == ValueType.STRING } @@ -239,7 +255,7 @@ class XmlDecisionReaderSpec extends Specification { } with(result.getRules()[0].getInputEntries()[0]) { - getName() == 'input_1' + getName() == 'Some_Input_1_Description' getExpression().getType() == ExpressionType.GROOVY getExpression().getValue() == '> 20' } @@ -251,7 +267,7 @@ class XmlDecisionReaderSpec extends Specification { } with(result.getRules()[0].getOutputEntries()[0]) { - getName() == 'output_0' + getName() == 'Some_Output_1_Description' getExpression().getType() == ExpressionType.GROOVY getExpression().getValue() == 'someVariable1 || someVariable2' } @@ -262,7 +278,7 @@ class XmlDecisionReaderSpec extends Specification { } with(result.getRules()[1].getInputEntries()[0]) { - getName() == 'input_1' + getName() == 'Some_Input_1_Description' getExpression().getType() == ExpressionType.FEEL getExpression().getValue() == '5' } @@ -274,7 +290,7 @@ class XmlDecisionReaderSpec extends Specification { } with(result.getRules()[1].getOutputEntries()[0]) { - getName() == 'output_1' + getName() == 'output_0' getExpression().getType() == ExpressionType.FEEL getExpression().getValue() == '"The output"' } diff --git a/src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example-duplicate-ids.dmn b/src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example-duplicate-ids.dmn index e5388f9..b75359d 100644 --- a/src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example-duplicate-ids.dmn +++ b/src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example-duplicate-ids.dmn @@ -5,7 +5,7 @@ - + > 5 diff --git a/src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example-duplicate-labels.dmn b/src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example-duplicate-labels.dmn new file mode 100644 index 0000000..b8a7816 --- /dev/null +++ b/src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example-duplicate-labels.dmn @@ -0,0 +1,36 @@ + + + + + + + + + + > 5 + + + + + + + + "one","two","blue","purple","red" + + + + + Some Rule 1 Description + + > 20 + + + not("blue", "purple") + + + someVariable1 || someVariable2 + + + + + From ffdab0d7cefda297556320161ec0ff119da241ac Mon Sep 17 00:00:00 2001 From: Mariusz Kumor Date: Tue, 15 Jan 2019 19:27:26 +0100 Subject: [PATCH 06/10] Missing name alias after save to YAML representation. Fixed #139 --- .../engine/model/decision/field/Input.java | 3 ++- .../dmn/io/yaml/YamlDecisionConverter.java | 22 +++++++++++++++++-- .../dmn/io/yaml/model/field/YamlInput.java | 1 + .../yaml/model/rule/entry/YamlInputEntry.java | 1 + 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/powerflows/dmn/engine/model/decision/field/Input.java b/src/main/java/org/powerflows/dmn/engine/model/decision/field/Input.java index b7138c4..4d2d346 100644 --- a/src/main/java/org/powerflows/dmn/engine/model/decision/field/Input.java +++ b/src/main/java/org/powerflows/dmn/engine/model/decision/field/Input.java @@ -31,9 +31,10 @@ public class Input implements Serializable { private static final long serialVersionUID = 1; + public static final String DEFAULT_NAME_ALIAS = "cellInput"; private String name; - private String nameAlias = "cellInput"; + private String nameAlias = DEFAULT_NAME_ALIAS; private String description; private ValueType type; private Expression expression; diff --git a/src/main/java/org/powerflows/dmn/io/yaml/YamlDecisionConverter.java b/src/main/java/org/powerflows/dmn/io/yaml/YamlDecisionConverter.java index 2203316..0abbdbb 100644 --- a/src/main/java/org/powerflows/dmn/io/yaml/YamlDecisionConverter.java +++ b/src/main/java/org/powerflows/dmn/io/yaml/YamlDecisionConverter.java @@ -33,6 +33,8 @@ import java.io.Serializable; import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.function.Function; import java.util.stream.Collectors; public class YamlDecisionConverter implements DecisionToExternalModelConverter { @@ -45,9 +47,14 @@ public YamlDecision to(final Decision decision) { yamlDecision.setEvaluationMode(decision.getEvaluationMode()); yamlDecision.setHitPolicy(decision.getHitPolicy()); yamlDecision.setFields(createFields(decision.getInputs(), decision.getOutputs())); + + final Map inputsMap = decision.getInputs() + .stream() + .collect(Collectors.toMap(Input::getName, Function.identity())); + yamlDecision.setRules(decision.getRules() .stream() - .map(this::ruleToYamlRule) + .map(rule -> ruleToYamlRule(rule, inputsMap)) .collect(Collectors.toList())); return yamlDecision; @@ -65,6 +72,11 @@ private YamlFields createFields(final List inputs, final List out yamlInput.setDescription(input.getDescription()); yamlInput.setType(input.getType()); yamlInput.setEvaluationMode(input.getEvaluationMode()); + + if (!Input.DEFAULT_NAME_ALIAS.equals(input.getNameAlias())) { + yamlInput.setNameAlias(input.getNameAlias()); + } + if (input.getExpression() != null && input.getExpression().getValue() != null) { yamlInput.setExpression(input.getExpression().getValue()); yamlInput.setExpressionType( @@ -88,7 +100,7 @@ private YamlFields createFields(final List inputs, final List out return yamlFields; } - private YamlRule ruleToYamlRule(final Rule rule) { + private YamlRule ruleToYamlRule(final Rule rule, final Map inputsMap) { final YamlRule yamlRule = new YamlRule(); yamlRule.setDescription(rule.getDescription()); final LinkedHashMap in = new LinkedHashMap<>(); @@ -102,6 +114,12 @@ private YamlRule ruleToYamlRule(final Rule rule) { yamlInputEntry.setExpression(inputEntry.getExpression().getValue()); yamlInputEntry.setEvaluationMode(inputEntry.getEvaluationMode()); + final Input input = inputsMap.get(inputEntry.getName()); + + if (inputEntry.getNameAlias() != null && !inputEntry.getNameAlias().equals(input.getNameAlias())) { + yamlInputEntry.setNameAlias(inputEntry.getNameAlias()); + } + in.put(inputEntry.getName(), yamlInputEntry); }); diff --git a/src/main/java/org/powerflows/dmn/io/yaml/model/field/YamlInput.java b/src/main/java/org/powerflows/dmn/io/yaml/model/field/YamlInput.java index e631652..4844cca 100644 --- a/src/main/java/org/powerflows/dmn/io/yaml/model/field/YamlInput.java +++ b/src/main/java/org/powerflows/dmn/io/yaml/model/field/YamlInput.java @@ -24,6 +24,7 @@ @Data public final class YamlInput { private String description; + private String nameAlias; private ValueType type; private ExpressionType expressionType = ExpressionType.LITERAL; private Object expression; diff --git a/src/main/java/org/powerflows/dmn/io/yaml/model/rule/entry/YamlInputEntry.java b/src/main/java/org/powerflows/dmn/io/yaml/model/rule/entry/YamlInputEntry.java index 74e835e..a6473b0 100644 --- a/src/main/java/org/powerflows/dmn/io/yaml/model/rule/entry/YamlInputEntry.java +++ b/src/main/java/org/powerflows/dmn/io/yaml/model/rule/entry/YamlInputEntry.java @@ -29,6 +29,7 @@ public YamlInputEntry(final Object value) { public YamlInputEntry() { } + private String nameAlias; private ExpressionType expressionType = ExpressionType.LITERAL; private Object expression; private EvaluationMode evaluationMode; From 45c48188bc3b3db9d06b59d213b8869f8d7df26a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Wo=C5=BAniak?= Date: Wed, 23 Jan 2019 01:21:52 +0100 Subject: [PATCH 07/10] Added support for providing custom functions in Expressions. Resolved #133 --- pom.xml | 13 +-- .../DefaultDecisionEngineConfiguration.java | 32 +++--- .../evaluator/entry/InputEntryEvaluator.java | 6 +- .../evaluator/entry/OutputEntryEvaluator.java | 6 +- ...ltExpressionEvaluationProviderFactory.java | 66 +++++++++++++ .../ExpressionEvaluationConfiguration.java | 41 ++++++++ .../ExpressionEvaluationProviderFactory.java | 29 +----- ...elExpressionEvaluationProviderFactory.java | 35 +++++++ .../GroovyExpressionEvaluationProvider.java | 39 ++++++++ ...vyExpressionEvaluationProviderFactory.java | 35 +++++++ ...avascriptExpressionEvaluationProvider.java | 37 +++++++ ...ptExpressionEvaluationProviderFactory.java | 36 +++++++ ...elExpressionEvaluationProviderFactory.java | 35 +++++++ ...alExpressionEvaluationProviderFactory.java | 35 +++++++ ...ptEngineExpressionEvaluationProvider.java} | 27 +++-- .../binding/AbstractMethodBinding.java | 46 +++++++++ .../binding/BoundMethod.java} | 13 +-- .../ExpressionEvaluationException.java | 22 +++++ .../binding/InstanceMethodBinding.java | 30 ++++++ .../provider/binding/MethodBinding.java | 20 ++++ .../provider/binding/StaticMethodBinding.java | 31 ++++++ .../script/DefaultScriptEngineProvider.java | 42 -------- ...ovider.ExpressionEvaluationProviderFactory | 5 + ...ineConfigurationReferenceSingleSpec.groovy | 13 ++- ...ssionEvaluationProviderFactorySpec.groovy} | 26 ++++- ...ovyExpressionEvaluationProviderSpec.groovy | 89 ++++++++++++++--- ...ptExpressionEvaluationProviderSpec.groovy} | 80 ++++++++++++--- .../provider/binding/MethodBindingSpec.groovy | 99 +++++++++++++++++++ .../DefaultScriptEngineProviderSpec.groovy | 58 ----------- .../ContextVariablesBindingsSpec.groovy | 6 +- .../dmn/engine/configuration/TestMethods.java | 26 +++++ .../provider/sample/MethodSource.java | 37 +++++++ .../engine/configuration/reference-single.yml | 2 +- 33 files changed, 910 insertions(+), 207 deletions(-) create mode 100644 src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/DefaultExpressionEvaluationProviderFactory.java create mode 100644 src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/ExpressionEvaluationConfiguration.java create mode 100644 src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/FeelExpressionEvaluationProviderFactory.java create mode 100644 src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/GroovyExpressionEvaluationProvider.java create mode 100644 src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/GroovyExpressionEvaluationProviderFactory.java create mode 100644 src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/JavascriptExpressionEvaluationProvider.java create mode 100644 src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/JavascriptExpressionEvaluationProviderFactory.java create mode 100644 src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/JuelExpressionEvaluationProviderFactory.java create mode 100644 src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/LiteralExpressionEvaluationProviderFactory.java rename src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/{ScriptExpressionEvaluationProvider.java => ScriptEngineExpressionEvaluationProvider.java} (77%) create mode 100644 src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/binding/AbstractMethodBinding.java rename src/main/java/org/powerflows/dmn/engine/evaluator/expression/{script/ScriptEngineProvider.java => provider/binding/BoundMethod.java} (69%) create mode 100644 src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/binding/ExpressionEvaluationException.java create mode 100644 src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/binding/InstanceMethodBinding.java create mode 100644 src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/binding/MethodBinding.java create mode 100644 src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/binding/StaticMethodBinding.java delete mode 100644 src/main/java/org/powerflows/dmn/engine/evaluator/expression/script/DefaultScriptEngineProvider.java create mode 100644 src/main/resources/META-INF/services/org.powerflows.dmn.engine.evaluator.expression.provider.ExpressionEvaluationProviderFactory rename src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/{ExpressionEvaluationProviderFactorySpec.groovy => DefaultExpressionEvaluationProviderFactorySpec.groovy} (53%) rename src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/{JavaScriptExpressionEvaluationProviderSpec.groovy => JavascriptExpressionEvaluationProviderSpec.groovy} (59%) create mode 100644 src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/binding/MethodBindingSpec.groovy delete mode 100644 src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/script/DefaultScriptEngineProviderSpec.groovy create mode 100644 src/test/java/org/powerflows/dmn/engine/configuration/TestMethods.java create mode 100644 src/test/java/org/powerflows/dmn/engine/evaluator/expression/provider/sample/MethodSource.java diff --git a/pom.xml b/pom.xml index a8063a6..99666e1 100644 --- a/pom.xml +++ b/pom.xml @@ -108,6 +108,13 @@ provided + + + org.codehaus.groovy + groovy-all + ${groovy.version} + + org.spockframework @@ -121,12 +128,6 @@ ${cglib.version} test - - org.codehaus.groovy - groovy-all - ${groovy.version} - test - org.objenesis objenesis diff --git a/src/main/java/org/powerflows/dmn/engine/configuration/DefaultDecisionEngineConfiguration.java b/src/main/java/org/powerflows/dmn/engine/configuration/DefaultDecisionEngineConfiguration.java index 742ef6a..ed58371 100644 --- a/src/main/java/org/powerflows/dmn/engine/configuration/DefaultDecisionEngineConfiguration.java +++ b/src/main/java/org/powerflows/dmn/engine/configuration/DefaultDecisionEngineConfiguration.java @@ -17,34 +17,40 @@ package org.powerflows.dmn.engine.configuration; +import lombok.Setter; +import lombok.experimental.Accessors; import org.powerflows.dmn.engine.DecisionEngine; import org.powerflows.dmn.engine.DefaultDecisionEngine; import org.powerflows.dmn.engine.evaluator.decision.DecisionEvaluator; import org.powerflows.dmn.engine.evaluator.entry.InputEntryEvaluator; import org.powerflows.dmn.engine.evaluator.entry.OutputEntryEvaluator; import org.powerflows.dmn.engine.evaluator.entry.mode.provider.EvaluationModeProviderFactory; -import org.powerflows.dmn.engine.evaluator.expression.provider.ExpressionEvaluationProviderFactory; -import org.powerflows.dmn.engine.evaluator.expression.script.DefaultScriptEngineProvider; -import org.powerflows.dmn.engine.evaluator.expression.script.ScriptEngineProvider; +import org.powerflows.dmn.engine.evaluator.expression.provider.DefaultExpressionEvaluationProviderFactory; +import org.powerflows.dmn.engine.evaluator.expression.provider.ExpressionEvaluationConfiguration; +import org.powerflows.dmn.engine.evaluator.expression.provider.binding.MethodBinding; import org.powerflows.dmn.engine.evaluator.rule.RuleEvaluator; import org.powerflows.dmn.engine.evaluator.type.converter.TypeConverterFactory; -import javax.script.ScriptEngineManager; +import java.util.Collections; +import java.util.List; +@Accessors(chain = true, fluent = true) public class DefaultDecisionEngineConfiguration implements DecisionEngineConfiguration { + @Setter + private List methodBindings = Collections.emptyList(); + private ExpressionEvaluationConfiguration configuration; private DecisionEvaluator decisionEvaluator; private RuleEvaluator ruleEvaluator; private EvaluationModeProviderFactory evaluationModeProviderFactory; private InputEntryEvaluator inputEntryEvaluator; private OutputEntryEvaluator outputEntryEvaluator; - private ScriptEngineProvider scriptEngineProvider; - private ExpressionEvaluationProviderFactory expressionEvaluationProviderFactory; + private DefaultExpressionEvaluationProviderFactory expressionEvaluationProviderFactory; private TypeConverterFactory typeConverterFactory; @Override public DecisionEngine configure() { - initScriptEngineProvider(); + initExpressionEvaluation(); initEvaluationProviderFactory(); initTypeConverterFactory(); initEvaluationModeProviderFactory(); @@ -56,18 +62,20 @@ public DecisionEngine configure() { return new DefaultDecisionEngine(decisionEvaluator); } + private void initExpressionEvaluation() { + configuration = ExpressionEvaluationConfiguration.builder() + .methodBinding(methodBindings) + .build(); + } + private void initEvaluationProviderFactory() { - expressionEvaluationProviderFactory = new ExpressionEvaluationProviderFactory(scriptEngineProvider); + expressionEvaluationProviderFactory = new DefaultExpressionEvaluationProviderFactory(configuration); } private void initTypeConverterFactory() { typeConverterFactory = new TypeConverterFactory(); } - private void initScriptEngineProvider() { - scriptEngineProvider = new DefaultScriptEngineProvider(new ScriptEngineManager()); - } - private void initEvaluationModeProviderFactory() { evaluationModeProviderFactory = new EvaluationModeProviderFactory(); } diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/entry/InputEntryEvaluator.java b/src/main/java/org/powerflows/dmn/engine/evaluator/entry/InputEntryEvaluator.java index e8edd43..d6e0a0a 100644 --- a/src/main/java/org/powerflows/dmn/engine/evaluator/entry/InputEntryEvaluator.java +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/entry/InputEntryEvaluator.java @@ -22,7 +22,7 @@ import org.powerflows.dmn.engine.evaluator.entry.mode.provider.EvaluationModeProvider; import org.powerflows.dmn.engine.evaluator.entry.mode.provider.EvaluationModeProviderFactory; import org.powerflows.dmn.engine.evaluator.expression.provider.ExpressionEvaluationProvider; -import org.powerflows.dmn.engine.evaluator.expression.provider.ExpressionEvaluationProviderFactory; +import org.powerflows.dmn.engine.evaluator.expression.provider.DefaultExpressionEvaluationProviderFactory; import org.powerflows.dmn.engine.evaluator.type.converter.TypeConverter; import org.powerflows.dmn.engine.evaluator.type.converter.TypeConverterFactory; import org.powerflows.dmn.engine.evaluator.type.value.SpecifiedTypeValue; @@ -36,12 +36,12 @@ @Slf4j public class InputEntryEvaluator { - private final ExpressionEvaluationProviderFactory expressionEvaluationProviderFactory; + private final DefaultExpressionEvaluationProviderFactory expressionEvaluationProviderFactory; private final TypeConverterFactory typeConverterFactory; private final EvaluationModeProviderFactory evaluationModeProviderFactory; - public InputEntryEvaluator(final ExpressionEvaluationProviderFactory expressionEvaluationProviderFactory, + public InputEntryEvaluator(final DefaultExpressionEvaluationProviderFactory expressionEvaluationProviderFactory, final TypeConverterFactory typeConverterFactory, final EvaluationModeProviderFactory evaluationModeProviderFactory) { this.expressionEvaluationProviderFactory = expressionEvaluationProviderFactory; diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/entry/OutputEntryEvaluator.java b/src/main/java/org/powerflows/dmn/engine/evaluator/entry/OutputEntryEvaluator.java index 9560032..676959e 100644 --- a/src/main/java/org/powerflows/dmn/engine/evaluator/entry/OutputEntryEvaluator.java +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/entry/OutputEntryEvaluator.java @@ -20,7 +20,7 @@ import lombok.extern.slf4j.Slf4j; import org.powerflows.dmn.engine.evaluator.context.EvaluationContext; import org.powerflows.dmn.engine.evaluator.expression.provider.ExpressionEvaluationProvider; -import org.powerflows.dmn.engine.evaluator.expression.provider.ExpressionEvaluationProviderFactory; +import org.powerflows.dmn.engine.evaluator.expression.provider.DefaultExpressionEvaluationProviderFactory; import org.powerflows.dmn.engine.evaluator.type.converter.TypeConverter; import org.powerflows.dmn.engine.evaluator.type.converter.TypeConverterFactory; import org.powerflows.dmn.engine.model.decision.field.Output; @@ -32,10 +32,10 @@ @Slf4j public class OutputEntryEvaluator { - private final ExpressionEvaluationProviderFactory expressionEvaluationProviderFactory; + private final DefaultExpressionEvaluationProviderFactory expressionEvaluationProviderFactory; private final TypeConverterFactory typeConverterFactory; - public OutputEntryEvaluator(ExpressionEvaluationProviderFactory expressionEvaluationProviderFactory, + public OutputEntryEvaluator(DefaultExpressionEvaluationProviderFactory expressionEvaluationProviderFactory, final TypeConverterFactory typeConverterFactory) { this.expressionEvaluationProviderFactory = expressionEvaluationProviderFactory; this.typeConverterFactory = typeConverterFactory; diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/DefaultExpressionEvaluationProviderFactory.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/DefaultExpressionEvaluationProviderFactory.java new file mode 100644 index 0000000..a7de1bd --- /dev/null +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/DefaultExpressionEvaluationProviderFactory.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.engine.evaluator.expression.provider; + +import lombok.extern.slf4j.Slf4j; +import org.powerflows.dmn.engine.model.decision.expression.ExpressionType; + +import java.util.EnumMap; +import java.util.Optional; +import java.util.ServiceLoader; + +@Slf4j +public class DefaultExpressionEvaluationProviderFactory { + + private static final ServiceLoader serviceLoader = ServiceLoader.load(ExpressionEvaluationProviderFactory.class); + + private final EnumMap factories = new EnumMap<>(ExpressionType.class); + + private final EnumMap providers = new EnumMap<>(ExpressionType.class); + private final ExpressionEvaluationConfiguration configuration; + + + public DefaultExpressionEvaluationProviderFactory() { + this(ExpressionEvaluationConfiguration.simpleConfiguration()); + } + + public DefaultExpressionEvaluationProviderFactory(final ExpressionEvaluationConfiguration configuration) { + this.configuration = configuration; + serviceLoader.forEach(provider -> + provider.supportedExpressionTypes() + .forEach(type -> { + log.debug("Found ExpressionEvaluationProvider for type {} - {}", type, provider); + factories.put(type, provider); + } + ) + ); + } + + public ExpressionEvaluationProvider getInstance(final ExpressionType expressionType) { + final ExpressionEvaluationProvider expressionEvaluationProvider = providers.computeIfAbsent(expressionType, key -> Optional + .ofNullable(factories.get(key)) + .map(factory -> factory.createProvider(configuration)) + .orElse(null) + ); + + if (expressionEvaluationProvider == null) { + throw new IllegalArgumentException("Unknown expression type " + expressionType); + } + + return expressionEvaluationProvider; + } +} diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/ExpressionEvaluationConfiguration.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/ExpressionEvaluationConfiguration.java new file mode 100644 index 0000000..322b71c --- /dev/null +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/ExpressionEvaluationConfiguration.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.engine.evaluator.expression.provider; + +import lombok.Builder; +import lombok.Getter; +import org.powerflows.dmn.engine.evaluator.expression.provider.binding.MethodBinding; + +import javax.script.ScriptEngineManager; +import java.util.Collections; +import java.util.List; + +@Getter +@Builder +public class ExpressionEvaluationConfiguration { + + private final List methodBinding; + private final ScriptEngineManager scriptEngineManager; + + private ExpressionEvaluationConfiguration(final List methodBinding, final ScriptEngineManager scriptEngineManager) { + this.methodBinding = methodBinding == null ? Collections.emptyList() : methodBinding; + this.scriptEngineManager = scriptEngineManager == null ? new ScriptEngineManager() : scriptEngineManager; + } + + public static ExpressionEvaluationConfiguration simpleConfiguration() { + return ExpressionEvaluationConfiguration.builder().build(); + } +} \ No newline at end of file diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/ExpressionEvaluationProviderFactory.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/ExpressionEvaluationProviderFactory.java index 273c95f..25ed39d 100644 --- a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/ExpressionEvaluationProviderFactory.java +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/ExpressionEvaluationProviderFactory.java @@ -13,35 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.powerflows.dmn.engine.evaluator.expression.provider; -import org.powerflows.dmn.engine.evaluator.expression.script.ScriptEngineProvider; import org.powerflows.dmn.engine.model.decision.expression.ExpressionType; -import java.util.EnumMap; - -public class ExpressionEvaluationProviderFactory { - - private final EnumMap factories = new EnumMap<>(ExpressionType.class); - - public ExpressionEvaluationProviderFactory(final ScriptEngineProvider scriptEngineProvider) { - final ExpressionEvaluationProvider scriptExpressionEvaluationProvider = new ScriptExpressionEvaluationProvider(scriptEngineProvider); - - factories.put(ExpressionType.LITERAL, new LiteralExpressionEvaluationProvider()); - factories.put(ExpressionType.FEEL, new FeelExpressionEvaluationProvider()); - factories.put(ExpressionType.JUEL, new JuelExpressionEvaluationProvider()); - factories.put(ExpressionType.GROOVY, scriptExpressionEvaluationProvider); - factories.put(ExpressionType.JAVASCRIPT, scriptExpressionEvaluationProvider); - } - - public ExpressionEvaluationProvider getInstance(final ExpressionType expressionType) { - final ExpressionEvaluationProvider expressionEvaluationProvider = factories.get(expressionType); +import java.util.List; - if (expressionEvaluationProvider == null) { - throw new IllegalArgumentException("Unknown expression type " + expressionType); - } +public interface ExpressionEvaluationProviderFactory { + ExpressionEvaluationProvider createProvider(ExpressionEvaluationConfiguration configuration); - return expressionEvaluationProvider; - } + List supportedExpressionTypes(); } diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/FeelExpressionEvaluationProviderFactory.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/FeelExpressionEvaluationProviderFactory.java new file mode 100644 index 0000000..4917472 --- /dev/null +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/FeelExpressionEvaluationProviderFactory.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.engine.evaluator.expression.provider; + +import org.powerflows.dmn.engine.model.decision.expression.ExpressionType; + +import java.util.Collections; +import java.util.List; + +public class FeelExpressionEvaluationProviderFactory implements ExpressionEvaluationProviderFactory { + private static final List SUPPORTED = Collections.singletonList(ExpressionType.FEEL); + + @Override + public ExpressionEvaluationProvider createProvider(final ExpressionEvaluationConfiguration configuration) { + return new FeelExpressionEvaluationProvider(); + } + + @Override + public List supportedExpressionTypes() { + return SUPPORTED; + } +} diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/GroovyExpressionEvaluationProvider.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/GroovyExpressionEvaluationProvider.java new file mode 100644 index 0000000..40b6590 --- /dev/null +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/GroovyExpressionEvaluationProvider.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.engine.evaluator.expression.provider; + +import lombok.extern.slf4j.Slf4j; +import org.codehaus.groovy.runtime.MethodClosure; +import org.powerflows.dmn.engine.evaluator.expression.provider.binding.MethodBinding; + +@Slf4j +class GroovyExpressionEvaluationProvider extends ScriptEngineExpressionEvaluationProvider { + + public GroovyExpressionEvaluationProvider(final ExpressionEvaluationConfiguration configuration) { + super(configuration); + } + + @Override + protected Object createMethodBinding(final MethodBinding methodBinding) { + return new MethodClosure(methodBinding, "execute"); + } + + @Override + protected String getEngineName() { + return "groovy"; + } +} \ No newline at end of file diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/GroovyExpressionEvaluationProviderFactory.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/GroovyExpressionEvaluationProviderFactory.java new file mode 100644 index 0000000..213d4ae --- /dev/null +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/GroovyExpressionEvaluationProviderFactory.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.engine.evaluator.expression.provider; + +import org.powerflows.dmn.engine.model.decision.expression.ExpressionType; + +import java.util.Collections; +import java.util.List; + +public class GroovyExpressionEvaluationProviderFactory implements ExpressionEvaluationProviderFactory { + private static final List SUPPORTED = Collections.singletonList(ExpressionType.GROOVY); + + @Override + public ExpressionEvaluationProvider createProvider(final ExpressionEvaluationConfiguration configuration) { + return new GroovyExpressionEvaluationProvider(configuration); + } + + @Override + public List supportedExpressionTypes() { + return SUPPORTED; + } +} diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/JavascriptExpressionEvaluationProvider.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/JavascriptExpressionEvaluationProvider.java new file mode 100644 index 0000000..6effdc8 --- /dev/null +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/JavascriptExpressionEvaluationProvider.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.engine.evaluator.expression.provider; + +import org.powerflows.dmn.engine.evaluator.expression.provider.binding.BoundMethod; +import org.powerflows.dmn.engine.evaluator.expression.provider.binding.MethodBinding; + +class JavascriptExpressionEvaluationProvider extends ScriptEngineExpressionEvaluationProvider { + + public JavascriptExpressionEvaluationProvider(final ExpressionEvaluationConfiguration configuration) { + super(configuration); + } + + @Override + protected Object createMethodBinding(final MethodBinding methodBinding) { + return (BoundMethod) methodBinding::execute; + } + + @Override + protected String getEngineName() { + return "javascript"; + } +} \ No newline at end of file diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/JavascriptExpressionEvaluationProviderFactory.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/JavascriptExpressionEvaluationProviderFactory.java new file mode 100644 index 0000000..5605505 --- /dev/null +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/JavascriptExpressionEvaluationProviderFactory.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.engine.evaluator.expression.provider; + +import org.powerflows.dmn.engine.model.decision.expression.ExpressionType; + +import java.util.Collections; +import java.util.List; + +public class JavascriptExpressionEvaluationProviderFactory implements ExpressionEvaluationProviderFactory { + + private static final List SUPPORTED = Collections.singletonList(ExpressionType.JAVASCRIPT); + + @Override + public ExpressionEvaluationProvider createProvider(final ExpressionEvaluationConfiguration configuration) { + return new JavascriptExpressionEvaluationProvider(configuration); + } + + @Override + public List supportedExpressionTypes() { + return SUPPORTED; + } +} diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/JuelExpressionEvaluationProviderFactory.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/JuelExpressionEvaluationProviderFactory.java new file mode 100644 index 0000000..3f037e8 --- /dev/null +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/JuelExpressionEvaluationProviderFactory.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.engine.evaluator.expression.provider; + +import org.powerflows.dmn.engine.model.decision.expression.ExpressionType; + +import java.util.Collections; +import java.util.List; + +public class JuelExpressionEvaluationProviderFactory implements ExpressionEvaluationProviderFactory { + private static final List SUPPORTED = Collections.singletonList(ExpressionType.JUEL); + + @Override + public ExpressionEvaluationProvider createProvider(final ExpressionEvaluationConfiguration configuration) { + return new JuelExpressionEvaluationProvider(); + } + + @Override + public List supportedExpressionTypes() { + return SUPPORTED; + } +} diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/LiteralExpressionEvaluationProviderFactory.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/LiteralExpressionEvaluationProviderFactory.java new file mode 100644 index 0000000..19a944f --- /dev/null +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/LiteralExpressionEvaluationProviderFactory.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.engine.evaluator.expression.provider; + +import org.powerflows.dmn.engine.model.decision.expression.ExpressionType; + +import java.util.Collections; +import java.util.List; + +public class LiteralExpressionEvaluationProviderFactory implements ExpressionEvaluationProviderFactory { + private static final List SUPPORTED = Collections.singletonList(ExpressionType.LITERAL); + + @Override + public ExpressionEvaluationProvider createProvider(final ExpressionEvaluationConfiguration configuration) { + return new LiteralExpressionEvaluationProvider(); + } + + @Override + public List supportedExpressionTypes() { + return SUPPORTED; + } +} diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/ScriptExpressionEvaluationProvider.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/ScriptEngineExpressionEvaluationProvider.java similarity index 77% rename from src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/ScriptExpressionEvaluationProvider.java rename to src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/ScriptEngineExpressionEvaluationProvider.java index 7725e23..85f6638 100644 --- a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/ScriptExpressionEvaluationProvider.java +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/ScriptEngineExpressionEvaluationProvider.java @@ -19,7 +19,7 @@ import lombok.extern.slf4j.Slf4j; import org.powerflows.dmn.engine.evaluator.context.EvaluationContext; import org.powerflows.dmn.engine.evaluator.exception.EvaluationException; -import org.powerflows.dmn.engine.evaluator.expression.script.ScriptEngineProvider; +import org.powerflows.dmn.engine.evaluator.expression.provider.binding.MethodBinding; import org.powerflows.dmn.engine.evaluator.expression.script.bindings.ContextVariablesBindings; import org.powerflows.dmn.engine.model.decision.expression.Expression; import org.powerflows.dmn.engine.model.decision.field.Input; @@ -27,20 +27,33 @@ import org.powerflows.dmn.engine.model.decision.rule.entry.OutputEntry; import javax.script.Bindings; +import javax.script.ScriptContext; import javax.script.ScriptEngine; import javax.script.ScriptException; import java.io.Serializable; - +import java.util.stream.Collectors; @Slf4j -class ScriptExpressionEvaluationProvider implements ExpressionEvaluationProvider { +public abstract class ScriptEngineExpressionEvaluationProvider implements ExpressionEvaluationProvider { + protected final ScriptEngine scriptEngine; - private final ScriptEngineProvider scriptEngineProvider; + public ScriptEngineExpressionEvaluationProvider(final ExpressionEvaluationConfiguration configuration) { + scriptEngine = configuration.getScriptEngineManager().getEngineByName(getEngineName()); + if (scriptEngine == null) { + throw new IllegalStateException("Unsupported script engine: " + getEngineName()); + } - public ScriptExpressionEvaluationProvider(final ScriptEngineProvider scriptEngineProvider) { - this.scriptEngineProvider = scriptEngineProvider; + final Bindings bindings = scriptEngine.createBindings(); + bindings.putAll(configuration.getMethodBinding() + .stream() + .collect(Collectors.toMap(MethodBinding::name, this::createMethodBinding))); + scriptEngine.setBindings(bindings, ScriptContext.GLOBAL_SCOPE); } + protected abstract Object createMethodBinding(MethodBinding methodBinding); + + protected abstract String getEngineName(); + @Override public Serializable evaluateInput(final Input input, final EvaluationContext evaluationContext) { log.debug("Starting evaluation of input: {} with evaluation context: {}", input, evaluationContext); @@ -75,14 +88,12 @@ public Serializable evaluateOutputEntry(final OutputEntry outputEntry, final Eva } private Serializable evaluate(final InputEntry inputEntry, final EvaluationContext evaluationContext) { - final ScriptEngine scriptEngine = scriptEngineProvider.getScriptEngine(inputEntry.getExpression().getType()); final Bindings bindings = ContextVariablesBindings.create(scriptEngine.createBindings(), evaluationContext, inputEntry); return evaluate(inputEntry.getExpression(), scriptEngine, bindings); } private Serializable evaluate(final Expression expression, final EvaluationContext evaluationContext) { - final ScriptEngine scriptEngine = scriptEngineProvider.getScriptEngine(expression.getType()); final Bindings bindings = ContextVariablesBindings.create(scriptEngine.createBindings(), evaluationContext); return evaluate(expression, scriptEngine, bindings); diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/binding/AbstractMethodBinding.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/binding/AbstractMethodBinding.java new file mode 100644 index 0000000..15369eb --- /dev/null +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/binding/AbstractMethodBinding.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.engine.evaluator.expression.provider.binding; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.function.Supplier; + +public abstract class AbstractMethodBinding implements MethodBinding { + private final String name; + private final Supplier instanceSupplier; + private final Method method; + + public AbstractMethodBinding(final String name, final Method method, final Supplier instanceSupplier) { + this.name = name; + this.instanceSupplier = instanceSupplier; + this.method = method; + } + + @Override + public String name() { + return name; + } + + @Override + public Object execute(final Object... args) { + try { + return method.invoke(instanceSupplier.get(), args); + } catch (IllegalAccessException | InvocationTargetException | IllegalArgumentException | NullPointerException e) { + throw new ExpressionEvaluationException(e); + } + } +} diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/script/ScriptEngineProvider.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/binding/BoundMethod.java similarity index 69% rename from src/main/java/org/powerflows/dmn/engine/evaluator/expression/script/ScriptEngineProvider.java rename to src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/binding/BoundMethod.java index 92577fc..3e36de0 100644 --- a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/script/ScriptEngineProvider.java +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/binding/BoundMethod.java @@ -13,14 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package org.powerflows.dmn.engine.evaluator.expression.provider.binding; -package org.powerflows.dmn.engine.evaluator.expression.script; - -import org.powerflows.dmn.engine.model.decision.expression.ExpressionType; - -import javax.script.ScriptEngine; - -public interface ScriptEngineProvider { - - ScriptEngine getScriptEngine(ExpressionType expressionType); +@FunctionalInterface +public interface BoundMethod { + Object execute(Object... args); } diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/binding/ExpressionEvaluationException.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/binding/ExpressionEvaluationException.java new file mode 100644 index 0000000..cc767c7 --- /dev/null +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/binding/ExpressionEvaluationException.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.engine.evaluator.expression.provider.binding; + +public class ExpressionEvaluationException extends RuntimeException { + public ExpressionEvaluationException(final Exception e) { + super(e); + } +} \ No newline at end of file diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/binding/InstanceMethodBinding.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/binding/InstanceMethodBinding.java new file mode 100644 index 0000000..ab27a6a --- /dev/null +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/binding/InstanceMethodBinding.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.engine.evaluator.expression.provider.binding; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.function.Supplier; + +public class InstanceMethodBinding extends AbstractMethodBinding { + + public InstanceMethodBinding(final String name, final Method method, final Supplier instanceSupplier) { + super(name, method, instanceSupplier); + if ((method.getModifiers() & Modifier.STATIC) != 0) { + throw new IllegalArgumentException("Provided method must not be static"); + } + } +} diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/binding/MethodBinding.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/binding/MethodBinding.java new file mode 100644 index 0000000..6dc43bf --- /dev/null +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/binding/MethodBinding.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.engine.evaluator.expression.provider.binding; + +public interface MethodBinding extends BoundMethod { + String name(); +} diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/binding/StaticMethodBinding.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/binding/StaticMethodBinding.java new file mode 100644 index 0000000..ee50a21 --- /dev/null +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/binding/StaticMethodBinding.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.engine.evaluator.expression.provider.binding; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.function.Supplier; + +public class StaticMethodBinding extends AbstractMethodBinding { + private static final Supplier NULL_SUPPLIER = () -> null; + + public StaticMethodBinding(final String name, final Method method) { + super(name, method, NULL_SUPPLIER); + if ((method.getModifiers() & Modifier.STATIC) == 0) { + throw new IllegalArgumentException("Provided method must be static"); + } + } +} diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/script/DefaultScriptEngineProvider.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/script/DefaultScriptEngineProvider.java deleted file mode 100644 index e1e7376..0000000 --- a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/script/DefaultScriptEngineProvider.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2018-present PowerFlows.org - all rights reserved. - * - * 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.powerflows.dmn.engine.evaluator.expression.script; - -import org.powerflows.dmn.engine.model.decision.expression.ExpressionType; - -import javax.script.ScriptEngine; -import javax.script.ScriptEngineManager; - -public class DefaultScriptEngineProvider implements ScriptEngineProvider { - - private final ScriptEngineManager scriptEngineManager; - - public DefaultScriptEngineProvider(final ScriptEngineManager scriptEngineManager) { - this.scriptEngineManager = scriptEngineManager; - } - - @Override - public ScriptEngine getScriptEngine(final ExpressionType expressionType) { - final ScriptEngine scriptEngine = scriptEngineManager.getEngineByName(expressionType.name().toLowerCase()); - - if (scriptEngine == null) { - throw new IllegalArgumentException("Unsupported " + expressionType); - } - - return scriptEngine; - } -} diff --git a/src/main/resources/META-INF/services/org.powerflows.dmn.engine.evaluator.expression.provider.ExpressionEvaluationProviderFactory b/src/main/resources/META-INF/services/org.powerflows.dmn.engine.evaluator.expression.provider.ExpressionEvaluationProviderFactory new file mode 100644 index 0000000..29c9181 --- /dev/null +++ b/src/main/resources/META-INF/services/org.powerflows.dmn.engine.evaluator.expression.provider.ExpressionEvaluationProviderFactory @@ -0,0 +1,5 @@ +org.powerflows.dmn.engine.evaluator.expression.provider.FeelExpressionEvaluationProviderFactory +org.powerflows.dmn.engine.evaluator.expression.provider.GroovyExpressionEvaluationProviderFactory +org.powerflows.dmn.engine.evaluator.expression.provider.JavascriptExpressionEvaluationProviderFactory +org.powerflows.dmn.engine.evaluator.expression.provider.JuelExpressionEvaluationProviderFactory +org.powerflows.dmn.engine.evaluator.expression.provider.LiteralExpressionEvaluationProviderFactory \ No newline at end of file diff --git a/src/test/groovy/org/powerflows/dmn/engine/configuration/DefaultDecisionEngineConfigurationReferenceSingleSpec.groovy b/src/test/groovy/org/powerflows/dmn/engine/configuration/DefaultDecisionEngineConfigurationReferenceSingleSpec.groovy index e4697f4..218e84c 100644 --- a/src/test/groovy/org/powerflows/dmn/engine/configuration/DefaultDecisionEngineConfigurationReferenceSingleSpec.groovy +++ b/src/test/groovy/org/powerflows/dmn/engine/configuration/DefaultDecisionEngineConfigurationReferenceSingleSpec.groovy @@ -17,15 +17,18 @@ package org.powerflows.dmn.engine.configuration import org.powerflows.dmn.engine.DecisionEngine +import org.powerflows.dmn.engine.evaluator.expression.provider.binding.StaticMethodBinding import org.powerflows.dmn.engine.model.decision.Decision -import org.powerflows.dmn.engine.model.evaluation.variable.DecisionVariables import org.powerflows.dmn.engine.model.evaluation.result.DecisionResult import org.powerflows.dmn.engine.model.evaluation.result.EntryResult +import org.powerflows.dmn.engine.model.evaluation.variable.DecisionVariables import org.powerflows.dmn.engine.reader.DecisionReader import org.powerflows.dmn.io.yaml.YamlDecisionReader import spock.lang.Shared import spock.lang.Specification +import java.lang.reflect.Method + class DefaultDecisionEngineConfigurationReferenceSingleSpec extends Specification { @Shared @@ -39,13 +42,13 @@ class DefaultDecisionEngineConfigurationReferenceSingleSpec extends Specificatio void setupSpec() { final DecisionReader decisionReader = new YamlDecisionReader() - decisionEngineConfiguration = new DefaultDecisionEngineConfiguration() - - decisionEngine = decisionEngineConfiguration.configure() - final String decisionFileName = 'reference-single.yml' final InputStream decisionInputStream = this.class.getResourceAsStream(decisionFileName) decision = decisionReader.read(decisionInputStream).get() + + final Method method = TestMethods.class.getMethod('parse', String) + decisionEngineConfiguration = new DefaultDecisionEngineConfiguration().methodBindings([new StaticMethodBinding('parseDate', method)]) + decisionEngine = decisionEngineConfiguration.configure() } void 'should configure default decision engine'() { diff --git a/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/ExpressionEvaluationProviderFactorySpec.groovy b/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/DefaultExpressionEvaluationProviderFactorySpec.groovy similarity index 53% rename from src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/ExpressionEvaluationProviderFactorySpec.groovy rename to src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/DefaultExpressionEvaluationProviderFactorySpec.groovy index 9f37573..2d2069f 100644 --- a/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/ExpressionEvaluationProviderFactorySpec.groovy +++ b/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/DefaultExpressionEvaluationProviderFactorySpec.groovy @@ -18,12 +18,11 @@ package org.powerflows.dmn.engine.evaluator.expression.provider import org.powerflows.dmn.engine.model.decision.expression.ExpressionType import spock.lang.Specification +import spock.lang.Unroll -class ExpressionEvaluationProviderFactorySpec extends Specification { - - private - final ExpressionEvaluationProviderFactory evaluationProviderFactory = new ExpressionEvaluationProviderFactory() +class DefaultExpressionEvaluationProviderFactorySpec extends Specification { + private final DefaultExpressionEvaluationProviderFactory evaluationProviderFactory = new DefaultExpressionEvaluationProviderFactory() void 'should throw exception when unknown expression type'() { given: @@ -37,4 +36,23 @@ class ExpressionEvaluationProviderFactorySpec extends Specification { exception != null exception.getMessage() == 'Unknown expression type null' } + + @Unroll + void 'should provide expression evaluation provider for #type'(ExpressionType type, Class targetClazz) { + when: + ExpressionEvaluationProvider result = evaluationProviderFactory.getInstance(type) + + then: + noExceptionThrown() + result != null + targetClazz.isAssignableFrom(result.getClass()) + + where: + type | targetClazz + ExpressionType.FEEL | FeelExpressionEvaluationProvider + ExpressionType.GROOVY | GroovyExpressionEvaluationProvider + ExpressionType.JAVASCRIPT | JavascriptExpressionEvaluationProvider + ExpressionType.JUEL | JuelExpressionEvaluationProvider + ExpressionType.LITERAL | LiteralExpressionEvaluationProvider + } } diff --git a/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/GroovyExpressionEvaluationProviderSpec.groovy b/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/GroovyExpressionEvaluationProviderSpec.groovy index 5e5ef77..508ae2d 100644 --- a/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/GroovyExpressionEvaluationProviderSpec.groovy +++ b/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/GroovyExpressionEvaluationProviderSpec.groovy @@ -18,8 +18,10 @@ package org.powerflows.dmn.engine.evaluator.expression.provider import org.powerflows.dmn.engine.evaluator.context.EvaluationContext import org.powerflows.dmn.engine.evaluator.exception.EvaluationException -import org.powerflows.dmn.engine.evaluator.expression.script.DefaultScriptEngineProvider -import org.powerflows.dmn.engine.evaluator.expression.script.ScriptEngineProvider +import org.powerflows.dmn.engine.evaluator.expression.provider.binding.InstanceMethodBinding +import org.powerflows.dmn.engine.evaluator.expression.provider.binding.MethodBinding +import org.powerflows.dmn.engine.evaluator.expression.provider.binding.StaticMethodBinding +import org.powerflows.dmn.engine.evaluator.expression.provider.sample.MethodSource import org.powerflows.dmn.engine.model.decision.expression.Expression import org.powerflows.dmn.engine.model.decision.expression.ExpressionType import org.powerflows.dmn.engine.model.decision.field.Input @@ -30,16 +32,19 @@ import spock.lang.Specification import spock.lang.Unroll import javax.script.ScriptEngineManager +import java.lang.reflect.Method class GroovyExpressionEvaluationProviderSpec extends Specification { - private final ScriptEngineProvider scriptEngineProvider = new DefaultScriptEngineProvider(new ScriptEngineManager()) - private final ExpressionEvaluationProvider expressionEvaluationProvider = - new ScriptExpressionEvaluationProvider(scriptEngineProvider) + private ExpressionEvaluationProvider expressionEvaluationProvider + + void setup() { + expressionEvaluationProvider = new GroovyExpressionEvaluationProvider(ExpressionEvaluationConfiguration.simpleConfiguration()) + } @Unroll void 'should evaluate input entry groovy expression value #entryExpressionValue and variables #contextVariable with #expectedEntryResult'( - final Object entryExpressionValue, final Object contextVariable, final boolean expectedEntryResult) { + final Object entryExpressionValue, final Object contextVariable, final Serializable expectedEntryResult) { given: final Expression entryExpression = [value: entryExpressionValue, type: ExpressionType.GROOVY] final InputEntry inputEntry = [expression: entryExpression, nameAlias: 'cellInput'] @@ -48,7 +53,7 @@ class GroovyExpressionEvaluationProviderSpec extends Specification { final EvaluationContext evaluationContext = new EvaluationContext(decisionVariables) when: - final boolean inputEntryResult = expressionEvaluationProvider.evaluateInputEntry(inputEntry, evaluationContext) + final Serializable inputEntryResult = expressionEvaluationProvider.evaluateInputEntry(inputEntry, evaluationContext) then: inputEntryResult == expectedEntryResult @@ -71,7 +76,7 @@ class GroovyExpressionEvaluationProviderSpec extends Specification { final EvaluationContext contextVariables = new EvaluationContext(decisionVariables) when: - final boolean inputEntryResult = expressionEvaluationProvider.evaluateInputEntry(inputEntry, contextVariables) + final Serializable inputEntryResult = expressionEvaluationProvider.evaluateInputEntry(inputEntry, contextVariables) then: inputEntryResult @@ -79,7 +84,7 @@ class GroovyExpressionEvaluationProviderSpec extends Specification { @Unroll void 'should evaluate output entry groovy expression value #entryExpressionValue and variables #contextVariable with #expectedEntryResult'( - final Object entryExpressionValue, final Object contextVariable, final boolean expectedEntryResult) { + final Object entryExpressionValue, final Object contextVariable, final Serializable expectedEntryResult) { given: final Expression entryExpression = [value: entryExpressionValue, type: ExpressionType.GROOVY] final OutputEntry outputEntry = [expression: entryExpression] @@ -88,7 +93,7 @@ class GroovyExpressionEvaluationProviderSpec extends Specification { final EvaluationContext evaluationContext = new EvaluationContext(decisionVariables) when: - final boolean outputEntryResult = expressionEvaluationProvider.evaluateOutputEntry(outputEntry, evaluationContext) + final Serializable outputEntryResult = expressionEvaluationProvider.evaluateOutputEntry(outputEntry, evaluationContext) then: outputEntryResult == expectedEntryResult @@ -104,7 +109,7 @@ class GroovyExpressionEvaluationProviderSpec extends Specification { @Unroll void 'should evaluate input groovy expression value #inputExpression and variables #contextVariable with #expectedInputResult'( - final Object inputExpression, final Object contextVariable, final boolean expectedInputResult) { + final Object inputExpression, final Object contextVariable, final Serializable expectedInputResult) { given: final Expression expression = [value: inputExpression, type: ExpressionType.GROOVY] final Input input = [name: 'TestInputName', expression: expression] @@ -113,7 +118,7 @@ class GroovyExpressionEvaluationProviderSpec extends Specification { final EvaluationContext contextVariables = new EvaluationContext(decisionVariables) when: - final boolean inputEntryResult = expressionEvaluationProvider.evaluateInput(input, contextVariables) + final Serializable inputEntryResult = expressionEvaluationProvider.evaluateInput(input, contextVariables) then: inputEntryResult == expectedInputResult @@ -141,4 +146,64 @@ class GroovyExpressionEvaluationProviderSpec extends Specification { exception != null exception.getMessage() == 'Script evaluation exception' } + + void 'should bind static method and make it available in expression'() { + given: + final Method method = MethodSource.class.getMethod('sampleStaticMethod', String, Integer.TYPE) + final List methodBinding = [new StaticMethodBinding('testMethod', method)] + final ExpressionEvaluationConfiguration configuration = ExpressionEvaluationConfiguration + .builder() + .methodBinding(methodBinding) + .build() + final ExpressionEvaluationProvider expressionEvaluationProvider = new GroovyExpressionEvaluationProvider(configuration) + final Expression expression = [value: 'testMethod(x, 1)', type: ExpressionType.GROOVY] + final Input input = [name: 'TestInputName', expression: expression] + + final DecisionVariables decisionVariables = new DecisionVariables([x: 'text']) + final EvaluationContext contextVariables = new EvaluationContext(decisionVariables) + + when: + final Serializable inputEntryResult = expressionEvaluationProvider.evaluateInput(input, contextVariables) + + then: + inputEntryResult == 'static-' + decisionVariables.get('x') + '-1' + } + + void 'should bind instance method and make it available in expression'() { + given: + final MethodSource theInstance = new MethodSource('someValue') + final Method method = MethodSource.class.getMethod('sampleInstanceMethod', Integer.TYPE, String) + final List methodBinding = [new InstanceMethodBinding('testMethod', method, { + theInstance + })] + final ExpressionEvaluationConfiguration configuration = ExpressionEvaluationConfiguration + .builder() + .methodBinding(methodBinding) + .build() + final ExpressionEvaluationProvider expressionEvaluationProvider = new GroovyExpressionEvaluationProvider(configuration) + final Expression expression = [value: 'testMethod(2, y)', type: ExpressionType.GROOVY] + final Input input = [name: 'TestInputName', expression: expression] + + final DecisionVariables decisionVariables = new DecisionVariables([y: 'text']) + final EvaluationContext contextVariables = new EvaluationContext(decisionVariables) + + when: + final Serializable inputEntryResult = expressionEvaluationProvider.evaluateInput(input, contextVariables) + + then: + inputEntryResult == 'static 2 someValue ' + decisionVariables.get('y') + } + + void 'should throw exception when no script engine is provided'() { + given: + final ScriptEngineManager scriptEngineManager = Mock() + final ExpressionEvaluationConfiguration configuration = ExpressionEvaluationConfiguration.builder().scriptEngineManager(scriptEngineManager).build() + + when: + new GroovyExpressionEvaluationProvider(configuration) + + then: + 1 * scriptEngineManager.getEngineByName('groovy') >> null + thrown(IllegalStateException) + } } diff --git a/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/JavaScriptExpressionEvaluationProviderSpec.groovy b/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/JavascriptExpressionEvaluationProviderSpec.groovy similarity index 59% rename from src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/JavaScriptExpressionEvaluationProviderSpec.groovy rename to src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/JavascriptExpressionEvaluationProviderSpec.groovy index 4084b1f..c2d5ad4 100644 --- a/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/JavaScriptExpressionEvaluationProviderSpec.groovy +++ b/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/JavascriptExpressionEvaluationProviderSpec.groovy @@ -18,8 +18,10 @@ package org.powerflows.dmn.engine.evaluator.expression.provider import org.powerflows.dmn.engine.evaluator.context.EvaluationContext import org.powerflows.dmn.engine.evaluator.exception.EvaluationException -import org.powerflows.dmn.engine.evaluator.expression.script.DefaultScriptEngineProvider -import org.powerflows.dmn.engine.evaluator.expression.script.ScriptEngineProvider +import org.powerflows.dmn.engine.evaluator.expression.provider.binding.InstanceMethodBinding +import org.powerflows.dmn.engine.evaluator.expression.provider.binding.MethodBinding +import org.powerflows.dmn.engine.evaluator.expression.provider.binding.StaticMethodBinding +import org.powerflows.dmn.engine.evaluator.expression.provider.sample.MethodSource import org.powerflows.dmn.engine.model.decision.expression.Expression import org.powerflows.dmn.engine.model.decision.expression.ExpressionType import org.powerflows.dmn.engine.model.decision.field.Input @@ -29,17 +31,20 @@ import org.powerflows.dmn.engine.model.evaluation.variable.DecisionVariables import spock.lang.Specification import spock.lang.Unroll -import javax.script.ScriptEngineManager +import java.lang.reflect.Method -class JavaScriptExpressionEvaluationProviderSpec extends Specification { +class JavascriptExpressionEvaluationProviderSpec extends Specification { - private final ScriptEngineProvider scriptEngineProvider = new DefaultScriptEngineProvider(new ScriptEngineManager()) - private final ExpressionEvaluationProvider expressionEvaluationProvider = - new ScriptExpressionEvaluationProvider(scriptEngineProvider) + private ExpressionEvaluationProvider expressionEvaluationProvider + + void setup() { + def configuration = ExpressionEvaluationConfiguration.simpleConfiguration() + expressionEvaluationProvider = new JavascriptExpressionEvaluationProvider(configuration) + } @Unroll void 'should evaluate input entry javascript expression value #entryExpressionValue and variables #contextVariable with #expectedEntryResult'( - final Object entryExpressionValue, final Object contextVariable, final boolean expectedEntryResult) { + final Object entryExpressionValue, final Object contextVariable, final Serializable expectedEntryResult) { given: final Expression entryExpression = [value: entryExpressionValue, type: ExpressionType.JAVASCRIPT] final InputEntry inputEntry = [expression: entryExpression, nameAlias: 'cellInput'] @@ -48,7 +53,7 @@ class JavaScriptExpressionEvaluationProviderSpec extends Specification { final EvaluationContext evaluationContext = new EvaluationContext(decisionVariables) when: - final boolean inputEntryResult = expressionEvaluationProvider.evaluateInputEntry(inputEntry, evaluationContext) + final Serializable inputEntryResult = expressionEvaluationProvider.evaluateInputEntry(inputEntry, evaluationContext) then: inputEntryResult == expectedEntryResult @@ -71,7 +76,7 @@ class JavaScriptExpressionEvaluationProviderSpec extends Specification { final EvaluationContext contextVariables = new EvaluationContext(decisionVariables) when: - final boolean inputEntryResult = expressionEvaluationProvider.evaluateInputEntry(inputEntry, contextVariables) + final Serializable inputEntryResult = expressionEvaluationProvider.evaluateInputEntry(inputEntry, contextVariables) then: inputEntryResult @@ -79,7 +84,7 @@ class JavaScriptExpressionEvaluationProviderSpec extends Specification { @Unroll void 'should evaluate output entry javascript expression value #entryExpressionValue and variables #contextVariable with #expectedEntryResult'( - final Object entryExpressionValue, final Object contextVariable, final boolean expectedEntryResult) { + final Object entryExpressionValue, final Object contextVariable, final Serializable expectedEntryResult) { given: final Expression entryExpression = [value: entryExpressionValue, type: ExpressionType.JAVASCRIPT] final OutputEntry outputEntry = [expression: entryExpression] @@ -88,7 +93,7 @@ class JavaScriptExpressionEvaluationProviderSpec extends Specification { final EvaluationContext evaluationContext = new EvaluationContext(decisionVariables) when: - final boolean outputEntryResult = expressionEvaluationProvider.evaluateOutputEntry(outputEntry, evaluationContext) + final Serializable outputEntryResult = expressionEvaluationProvider.evaluateOutputEntry(outputEntry, evaluationContext) then: outputEntryResult == expectedEntryResult @@ -104,7 +109,7 @@ class JavaScriptExpressionEvaluationProviderSpec extends Specification { @Unroll void 'should evaluate input javascript expression value #inputExpression and variables #contextVariable with #expectedInputResult'( - final Object inputExpression, final Object contextVariable, final boolean expectedInputResult) { + final Object inputExpression, final Object contextVariable, final Serializable expectedInputResult) { given: final Expression expression = [value: inputExpression, type: ExpressionType.JAVASCRIPT] final Input input = [name: 'TestInputName', expression: expression] @@ -113,7 +118,7 @@ class JavaScriptExpressionEvaluationProviderSpec extends Specification { final EvaluationContext contextVariables = new EvaluationContext(decisionVariables) when: - final boolean inputEntryResult = expressionEvaluationProvider.evaluateInput(input, contextVariables) + final Serializable inputEntryResult = expressionEvaluationProvider.evaluateInput(input, contextVariables) then: inputEntryResult == expectedInputResult @@ -142,4 +147,51 @@ class JavaScriptExpressionEvaluationProviderSpec extends Specification { exception != null exception.getMessage() == 'Script evaluation exception' } + + void 'should bind static method and make it available in expression'() { + given: + final Method method = MethodSource.class.getMethod('sampleStaticMethod', String, Integer.TYPE) + final List methodBinding = [new StaticMethodBinding('testMethod', method)] + final ExpressionEvaluationConfiguration configuration = ExpressionEvaluationConfiguration + .builder() + .methodBinding(methodBinding) + .build() + final ExpressionEvaluationProvider expressionEvaluationProvider = new JavascriptExpressionEvaluationProvider(configuration) + final Expression expression = [value: 'testMethod(x, 1)', type: ExpressionType.JAVASCRIPT] + final Input input = [name: 'TestInputName', expression: expression] + + final DecisionVariables decisionVariables = new DecisionVariables([x: 'text']) + final EvaluationContext contextVariables = new EvaluationContext(decisionVariables) + + when: + final Serializable inputEntryResult = expressionEvaluationProvider.evaluateInput(input, contextVariables) + + then: + inputEntryResult == 'static-' + decisionVariables.get('x') + '-1' + } + + void 'should bind instance method and make it available in expression'() { + given: + final MethodSource theInstance = new MethodSource('someValue') + final Method method = MethodSource.class.getMethod('sampleInstanceMethod', Integer.TYPE, String) + final List methodBinding = [new InstanceMethodBinding('testMethod', method, { + theInstance + })] + final ExpressionEvaluationConfiguration configuration = ExpressionEvaluationConfiguration + .builder() + .methodBinding(methodBinding) + .build() + final ExpressionEvaluationProvider expressionEvaluationProvider = new JavascriptExpressionEvaluationProvider(configuration) + final Expression expression = [value: 'testMethod(2, y)', type: ExpressionType.JAVASCRIPT] + final Input input = [name: 'TestInputName', expression: expression] + + final DecisionVariables decisionVariables = new DecisionVariables([y: 'text']) + final EvaluationContext contextVariables = new EvaluationContext(decisionVariables) + + when: + final Serializable inputEntryResult = expressionEvaluationProvider.evaluateInput(input, contextVariables) + + then: + inputEntryResult == 'static 2 someValue ' + decisionVariables.get('y') + } } diff --git a/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/binding/MethodBindingSpec.groovy b/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/binding/MethodBindingSpec.groovy new file mode 100644 index 0000000..40aa673 --- /dev/null +++ b/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/provider/binding/MethodBindingSpec.groovy @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.engine.evaluator.expression.provider.binding + +import org.powerflows.dmn.engine.evaluator.expression.provider.sample.MethodSource +import spock.lang.Specification + +import java.lang.reflect.Method +import java.util.function.Supplier + +class MethodBindingSpec extends Specification { + void 'StaticMethodBinding should not allow non static methods for configuration'() { + given: + final Method method = MethodSource.class.getMethod('sampleInstanceMethod', Integer.TYPE, String) + + when: + new StaticMethodBinding('test', method) + + then: + thrown(IllegalArgumentException) + } + + void 'InstanceMethodBinding should not allow static methods for configuration'() { + given: + final Method method = MethodSource.class.getMethod('sampleStaticMethod', String, Integer.TYPE) + final Supplier supplier = { -> null } + + when: + new InstanceMethodBinding('test', method, supplier) + + then: + thrown(IllegalArgumentException) + } + + void 'MethodBinding should throw exception on private methods'() { + given: + final Method method = MethodSource.class.getDeclaredMethod('privateMethod', String) + final Supplier supplier = { -> new MethodSource() } + final MethodBinding methodBinding = new InstanceMethodBinding('test', method, supplier) + + when: + methodBinding.execute('test') + + then: + thrown(ExpressionEvaluationException) + } + + void 'MethodBinding should throw exception on invalid method arguments'() { + given: + final Method method = MethodSource.class.getMethod('sampleInstanceMethod', Integer.TYPE, String) + final Supplier supplier = { -> new MethodSource() } + final MethodBinding methodBinding = new InstanceMethodBinding('test', method, supplier) + + when: + methodBinding.execute('test') + + then: + thrown(ExpressionEvaluationException) + } + + void 'MethodBinding should throw exception on invalid instance source'() { + given: + final Method method = MethodSource.class.getMethod('sampleInstanceMethod', Integer.TYPE, String) + final Supplier supplier = { -> 'this is string!' } + final MethodBinding methodBinding = new InstanceMethodBinding('test', method, supplier) + + when: + methodBinding.execute('test') + + then: + thrown(ExpressionEvaluationException) + } + + void 'MethodBinding should throw exception on null instance'() { + given: + final Method method = MethodSource.class.getMethod('sampleInstanceMethod', Integer.TYPE, String) + final Supplier supplier = { -> null } + final MethodBinding methodBinding = new InstanceMethodBinding('test', method, supplier) + + when: + methodBinding.execute('test') + + then: + thrown(ExpressionEvaluationException) + } +} diff --git a/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/script/DefaultScriptEngineProviderSpec.groovy b/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/script/DefaultScriptEngineProviderSpec.groovy deleted file mode 100644 index 12bf3f5..0000000 --- a/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/script/DefaultScriptEngineProviderSpec.groovy +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (c) 2018-present PowerFlows.org - all rights reserved. - * - * 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.powerflows.dmn.engine.evaluator.expression.script - -import org.powerflows.dmn.engine.model.decision.expression.ExpressionType -import spock.lang.Shared -import spock.lang.Specification - -import javax.script.ScriptEngine -import javax.script.ScriptEngineManager - -class DefaultScriptEngineProviderSpec extends Specification { - - @Shared - private ScriptEngineProvider scriptEngineProvider - - void setupSpec() { - scriptEngineProvider = new DefaultScriptEngineProvider(new ScriptEngineManager()); - } - - void 'should get script engine instance'() { - given: - final ExpressionType supportedScriptLanguage = ExpressionType.GROOVY - - when: - final ScriptEngine scriptEngine = scriptEngineProvider.getScriptEngine(supportedScriptLanguage) - - then: - scriptEngine != null - } - - void 'should throw exception for non supported script language'() { - given: - final ExpressionType unsupportedScriptLanguage = ExpressionType.LITERAL - - when: - scriptEngineProvider.getScriptEngine(unsupportedScriptLanguage) - - then: - final IllegalArgumentException exception = thrown() - exception != null - exception.getMessage() == 'Unsupported ' + unsupportedScriptLanguage - } -} diff --git a/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/script/bindings/ContextVariablesBindingsSpec.groovy b/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/script/bindings/ContextVariablesBindingsSpec.groovy index eddacd8..cb2dc4b 100644 --- a/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/script/bindings/ContextVariablesBindingsSpec.groovy +++ b/src/test/groovy/org/powerflows/dmn/engine/evaluator/expression/script/bindings/ContextVariablesBindingsSpec.groovy @@ -17,9 +17,6 @@ package org.powerflows.dmn.engine.evaluator.expression.script.bindings import org.powerflows.dmn.engine.evaluator.context.EvaluationContext -import org.powerflows.dmn.engine.evaluator.expression.script.DefaultScriptEngineProvider -import org.powerflows.dmn.engine.evaluator.expression.script.ScriptEngineProvider -import org.powerflows.dmn.engine.model.decision.expression.ExpressionType import org.powerflows.dmn.engine.model.evaluation.variable.DecisionVariables import spock.lang.Shared import spock.lang.Specification @@ -34,8 +31,7 @@ class ContextVariablesBindingsSpec extends Specification { private ScriptEngine scriptEngine void setupSpec() { - final ScriptEngineProvider scriptEngineProvider = new DefaultScriptEngineProvider(new ScriptEngineManager()); - scriptEngine = scriptEngineProvider.getScriptEngine(ExpressionType.GROOVY); + scriptEngine = new ScriptEngineManager().getEngineByName("groovy") } void 'should create ContextVariablesBindings instance'() { diff --git a/src/test/java/org/powerflows/dmn/engine/configuration/TestMethods.java b/src/test/java/org/powerflows/dmn/engine/configuration/TestMethods.java new file mode 100644 index 0000000..b627297 --- /dev/null +++ b/src/test/java/org/powerflows/dmn/engine/configuration/TestMethods.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.engine.configuration; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Date; + +public class TestMethods { + public static Date parse(final String dateText) { + return Date.from(LocalDate.parse(dateText).atStartOfDay(ZoneId.systemDefault()).toInstant()); + } +} diff --git a/src/test/java/org/powerflows/dmn/engine/evaluator/expression/provider/sample/MethodSource.java b/src/test/java/org/powerflows/dmn/engine/evaluator/expression/provider/sample/MethodSource.java new file mode 100644 index 0000000..c82c37b --- /dev/null +++ b/src/test/java/org/powerflows/dmn/engine/evaluator/expression/provider/sample/MethodSource.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.engine.evaluator.expression.provider.sample; + +public class MethodSource { + + private final String value; + + public MethodSource(final String value) { + this.value = value; + } + + public String sampleInstanceMethod(final int firstParam, final String secondParam) { + return "static " + firstParam + " " + value + " " + secondParam; + } + + public static String sampleStaticMethod(final String firstParam, final int secondParam) { + return "static-" + firstParam + "-" + secondParam; + } + + private String privateMethod(final String onlyParam) { + return "private-" + onlyParam; + } +} \ No newline at end of file diff --git a/src/test/resources/org/powerflows/dmn/engine/configuration/reference-single.yml b/src/test/resources/org/powerflows/dmn/engine/configuration/reference-single.yml index ff4c43e..6096233 100644 --- a/src/test/resources/org/powerflows/dmn/engine/configuration/reference-single.yml +++ b/src/test/resources/org/powerflows/dmn/engine/configuration/reference-single.yml @@ -95,7 +95,7 @@ rules: in: inputFour: expression-type: GROOVY - expression: "((cellInput instanceof String) ? Date.parse('yyyy-MM-dd', '2018-12-24') : cellInput) < Date.parse('yyyy-MM-dd', '2018-12-14')" + expression: "((cellInput instanceof String) ? parseDate('2018-12-24') : cellInput) < parseDate('2018-12-14')" evaluation-mode: BOOLEAN out: outputOne: true From f71e3383fafb09fe278392fd42dc4ae560106a37 Mon Sep 17 00:00:00 2001 From: Mariusz Kumor Date: Tue, 29 Jan 2019 00:30:27 +0100 Subject: [PATCH 08/10] Supported name alias matching with evaluation variable if the variable doesn't match an input name. Resolved #145 --- .../LiteralExpressionEvaluationProvider.java | 12 +- .../dmn/io/yaml/CustomPropertyUtils.java | 1 + .../dmn/io/yaml/YamlDecisionConverter.java | 44 ++++--- ...ionEngineConfigurationNameAliasSpec.groovy | 121 ++++++++++++++++++ ...lias-no-ctx-var-matching-to-input-name.yml | 29 +++++ ...nd-ctx-var-for-second-input-is-present.yml | 31 +++++ ...e-and-with no-ctx-var-for-second-input.yml | 29 +++++ 7 files changed, 248 insertions(+), 19 deletions(-) create mode 100644 src/test/groovy/org/powerflows/dmn/engine/configuration/DefaultDecisionEngineConfigurationNameAliasSpec.groovy create mode 100644 src/test/resources/org/powerflows/dmn/engine/configuration/name-alias-no-ctx-var-matching-to-input-name.yml create mode 100644 src/test/resources/org/powerflows/dmn/engine/configuration/name-alias-second-input-has-same-name-alias-like-first-input-name-and-ctx-var-for-second-input-is-present.yml create mode 100644 src/test/resources/org/powerflows/dmn/engine/configuration/name-alias-second-input-has-same-name-alias-like-first-input-name-and-with no-ctx-var-for-second-input.yml diff --git a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/LiteralExpressionEvaluationProvider.java b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/LiteralExpressionEvaluationProvider.java index da50cc3..b538bea 100644 --- a/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/LiteralExpressionEvaluationProvider.java +++ b/src/main/java/org/powerflows/dmn/engine/evaluator/expression/provider/LiteralExpressionEvaluationProvider.java @@ -32,10 +32,18 @@ class LiteralExpressionEvaluationProvider implements ExpressionEvaluationProvide public Serializable evaluateInput(final Input input, final EvaluationContext evaluationContext) { log.debug("Starting evaluation of input: {} with evaluation context: {}", input, evaluationContext); - final Serializable value = evaluationContext.get(input.getName()); + Serializable value = evaluationContext.get(input.getName()); if (value == null) { - log.warn("Input value is null"); + log.warn("Input value by name {} is null", input.getName()); + + value = evaluationContext.get(input.getNameAlias()); + + if (value == null) { + log.warn("Input value by name alias {} is null", input.getNameAlias()); + } else { + evaluationContext.addVariable(input.getName(), value); + } } log.debug("Evaluated input result: {}", value); diff --git a/src/main/java/org/powerflows/dmn/io/yaml/CustomPropertyUtils.java b/src/main/java/org/powerflows/dmn/io/yaml/CustomPropertyUtils.java index b2bc628..334c1ad 100644 --- a/src/main/java/org/powerflows/dmn/io/yaml/CustomPropertyUtils.java +++ b/src/main/java/org/powerflows/dmn/io/yaml/CustomPropertyUtils.java @@ -37,6 +37,7 @@ public class CustomPropertyUtils extends PropertyUtils { PROPERTY_NAME_MAP.put("hitPolicy", "hit-policy"); PROPERTY_NAME_MAP.put("expressionType", "expression-type"); PROPERTY_NAME_MAP.put("evaluationMode", "evaluation-mode"); + PROPERTY_NAME_MAP.put("nameAlias", "name-alias"); } CustomPropertyUtils() { diff --git a/src/main/java/org/powerflows/dmn/io/yaml/YamlDecisionConverter.java b/src/main/java/org/powerflows/dmn/io/yaml/YamlDecisionConverter.java index 0abbdbb..ab4a0c8 100644 --- a/src/main/java/org/powerflows/dmn/io/yaml/YamlDecisionConverter.java +++ b/src/main/java/org/powerflows/dmn/io/yaml/YamlDecisionConverter.java @@ -144,16 +144,25 @@ public Decision from(final YamlDecision model) { .hitPolicy(model.getHitPolicy()); model.getFields().getIn().forEach((name, input) -> builder - .withInput(inputBuilder -> inputBuilder - .name(name) - .type(input.getType()) - .evaluationMode(input.getEvaluationMode()) - .description(input.getDescription()) - .withExpression(expressionBuilder -> expressionBuilder - .type(input.getExpressionType()) - .value((Serializable) input.getExpression()) - .build()) - .build())); + .withInput(inputBuilder -> { + + if (input.getNameAlias() != null) { + inputBuilder.nameAlias(input.getNameAlias()); + } + + return inputBuilder + .name(name) + .type(input.getType()) + .evaluationMode(input.getEvaluationMode()) + .description(input.getDescription()) + .withExpression(expressionBuilder -> expressionBuilder + .type(input.getExpressionType()) + .value((Serializable) input.getExpression()) + .build()) + .build(); + } + ) + ); model.getFields().getOut().forEach((name, output) -> builder .withOutput(outpBuilder -> outpBuilder @@ -163,22 +172,23 @@ public Decision from(final YamlDecision model) { .build())); model.getRules().forEach(rule -> builder.withRule(ruleBuilder -> { - rule.getIn().forEach((name, input) -> ruleBuilder + rule.getIn().forEach((name, inputEntry) -> ruleBuilder .withInputEntry(inputEntryBuilder -> inputEntryBuilder .name(name) - .evaluationMode(input.getEvaluationMode()) + .nameAlias(inputEntry.getNameAlias()) + .evaluationMode(inputEntry.getEvaluationMode()) .withExpression(expressionBuilder -> expressionBuilder - .type(input.getExpressionType()) - .value((Serializable) input.getExpression()) + .type(inputEntry.getExpressionType()) + .value((Serializable) inputEntry.getExpression()) .build()) .build())); - rule.getOut().forEach((name, output) -> ruleBuilder + rule.getOut().forEach((name, outputEntry) -> ruleBuilder .withOutputEntry(outputEntryBuilder -> outputEntryBuilder .name(name) .withExpression(expressionBuilder -> expressionBuilder - .type(output.getExpressionType()) - .value((Serializable) output.getExpression()) + .type(outputEntry.getExpressionType()) + .value((Serializable) outputEntry.getExpression()) .build()) .build())); diff --git a/src/test/groovy/org/powerflows/dmn/engine/configuration/DefaultDecisionEngineConfigurationNameAliasSpec.groovy b/src/test/groovy/org/powerflows/dmn/engine/configuration/DefaultDecisionEngineConfigurationNameAliasSpec.groovy new file mode 100644 index 0000000..5db2f4c --- /dev/null +++ b/src/test/groovy/org/powerflows/dmn/engine/configuration/DefaultDecisionEngineConfigurationNameAliasSpec.groovy @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.engine.configuration + +import org.powerflows.dmn.engine.DecisionEngine +import org.powerflows.dmn.engine.model.decision.Decision +import org.powerflows.dmn.engine.model.evaluation.result.DecisionResult +import org.powerflows.dmn.engine.model.evaluation.variable.DecisionVariables +import org.powerflows.dmn.engine.reader.DecisionReader +import org.powerflows.dmn.io.yaml.YamlDecisionReader +import spock.lang.Shared +import spock.lang.Specification + +class DefaultDecisionEngineConfigurationNameAliasSpec extends Specification { + + @Shared + private DecisionEngine decisionEngine + + @Shared + private DecisionEngineConfiguration decisionEngineConfiguration + + void setupSpec() { + decisionEngineConfiguration = new DefaultDecisionEngineConfiguration() + decisionEngine = decisionEngineConfiguration.configure() + } + + void 'should evaluate with name alias case when no context variables matching to input name'() { + given: + final DecisionReader decisionReader = new YamlDecisionReader() + final String decisionFileName = 'name-alias-no-ctx-var-matching-to-input-name.yml' + final InputStream decisionInputStream = this.class.getResourceAsStream(decisionFileName) + final Decision decision = decisionReader.read(decisionInputStream).get() + + final Map variables = [:] + variables.put('inputOneAlias', 2) + variables.put('inputTwoAlias', true) + final DecisionVariables decisionVariables = new DecisionVariables(variables) + + when: + final DecisionResult decisionResult = decisionEngine.evaluate(decision, decisionVariables) + + then: + decisionResult != null + + with(decisionResult) { + isSingleEntryResult() + isSingleRuleResult() + !isCollectionRulesResult() + getSingleEntryResult().getName() == 'outputOne' + getSingleEntryResult().getValue() == 'Result 1' + } + } + + void 'should evaluate with name alias when second input has same name alias like first input name and context variable having name of second input is present'() { + given: + final DecisionReader decisionReader = new YamlDecisionReader() + final String decisionFileName = 'name-alias-second-input-has-same-name-alias-like-first-input-name-and-ctx-var-for-second-input-is-present.yml' + final InputStream decisionInputStream = this.class.getResourceAsStream(decisionFileName) + final Decision decision = decisionReader.read(decisionInputStream).get() + + final Map variables = [:] + variables.put('inputOne', 2) + variables.put('inputTwo', 7) + final DecisionVariables decisionVariables = new DecisionVariables(variables) + + when: + final DecisionResult decisionResult = decisionEngine.evaluate(decision, decisionVariables) + + then: + decisionResult != null + + with(decisionResult) { + isSingleEntryResult() + isSingleRuleResult() + !isCollectionRulesResult() + getSingleEntryResult().getName() == 'outputOne' + getSingleEntryResult().getValue() == 'Result 1' + } + } + + void 'should evaluate with name alias when second input has same name alias like first input name and with no context variable having name of second input'() { + given: + final DecisionReader decisionReader = new YamlDecisionReader() + final String decisionFileName = 'name-alias-second-input-has-same-name-alias-like-first-input-name-and-with no-ctx-var-for-second-input.yml' + final InputStream decisionInputStream = this.class.getResourceAsStream(decisionFileName) + final Decision decision = decisionReader.read(decisionInputStream).get() + + final Map variables = [:] + variables.put('inputOne', 2) + final DecisionVariables decisionVariables = new DecisionVariables(variables) + + when: + final DecisionResult decisionResult = decisionEngine.evaluate(decision, decisionVariables) + + then: + decisionResult != null + + with(decisionResult) { + isSingleEntryResult() + isSingleRuleResult() + !isCollectionRulesResult() + getSingleEntryResult().getName() == 'outputOne' + getSingleEntryResult().getValue() == 'Result 1' + } + } + +} diff --git a/src/test/resources/org/powerflows/dmn/engine/configuration/name-alias-no-ctx-var-matching-to-input-name.yml b/src/test/resources/org/powerflows/dmn/engine/configuration/name-alias-no-ctx-var-matching-to-input-name.yml new file mode 100644 index 0000000..8c06325 --- /dev/null +++ b/src/test/resources/org/powerflows/dmn/engine/configuration/name-alias-no-ctx-var-matching-to-input-name.yml @@ -0,0 +1,29 @@ +id: some_table_id_1 +name: Some Table Name +hit-policy: COLLECT +evaluation-mode: INPUT_COMPARISON +expression-type: LITERAL +fields: + in: + inputOne: + name-alias: inputOneAlias + type: INTEGER + inputTwo: + name-alias: inputTwoAlias + type: BOOLEAN + out: + outputOne: + type: STRING +rules: +- description: Some Rule 1 Description + in: + inputOne: 2 + inputTwo: true + out: + outputOne: Result 1 +- description: Some Rule 2 Description + in: + inputOne: 3 + inputTwo: true + out: + outputOne: Result 2 \ No newline at end of file diff --git a/src/test/resources/org/powerflows/dmn/engine/configuration/name-alias-second-input-has-same-name-alias-like-first-input-name-and-ctx-var-for-second-input-is-present.yml b/src/test/resources/org/powerflows/dmn/engine/configuration/name-alias-second-input-has-same-name-alias-like-first-input-name-and-ctx-var-for-second-input-is-present.yml new file mode 100644 index 0000000..2361eee --- /dev/null +++ b/src/test/resources/org/powerflows/dmn/engine/configuration/name-alias-second-input-has-same-name-alias-like-first-input-name-and-ctx-var-for-second-input-is-present.yml @@ -0,0 +1,31 @@ +id: some_table_id_1 +name: Some Table Name +hit-policy: COLLECT +evaluation-mode: INPUT_COMPARISON +expression-type: LITERAL +fields: + in: + inputOne: + name-alias: inputOneAlias + type: INTEGER + inputTwo: + name-alias: inputOne + type: INTEGER + out: + outputOne: + type: STRING +rules: +- description: Some Rule 1 Description + in: + inputOne: 2 + inputTwo: + expression-type: GROOVY + expression: inputOne + out: + outputOne: Result 1 +- description: Some Rule 2 Description + in: + inputOne: 2 + inputTwo: 4 + out: + outputOne: Result 2 \ No newline at end of file diff --git a/src/test/resources/org/powerflows/dmn/engine/configuration/name-alias-second-input-has-same-name-alias-like-first-input-name-and-with no-ctx-var-for-second-input.yml b/src/test/resources/org/powerflows/dmn/engine/configuration/name-alias-second-input-has-same-name-alias-like-first-input-name-and-with no-ctx-var-for-second-input.yml new file mode 100644 index 0000000..b57207b --- /dev/null +++ b/src/test/resources/org/powerflows/dmn/engine/configuration/name-alias-second-input-has-same-name-alias-like-first-input-name-and-with no-ctx-var-for-second-input.yml @@ -0,0 +1,29 @@ +id: some_table_id_1 +name: Some Table Name +hit-policy: COLLECT +evaluation-mode: INPUT_COMPARISON +expression-type: LITERAL +fields: + in: + inputOne: + name-alias: inputOneAlias + type: INTEGER + inputTwo: + name-alias: inputOne + type: INTEGER + out: + outputOne: + type: STRING +rules: +- description: Some Rule 1 Description + in: + inputOne: 2 + inputTwo: 2 + out: + outputOne: Result 1 +- description: Some Rule 2 Description + in: + inputOne: 2 + inputTwo: 4 + out: + outputOne: Result 2 \ No newline at end of file From 011c79bb423a0f5a648c52770c823bc2c6aa6ccd Mon Sep 17 00:00:00 2001 From: Mariusz Kumor Date: Thu, 31 Jan 2019 22:00:19 +0100 Subject: [PATCH 09/10] Thrown exception in XMLDecisionConverter if unable to resolve typeRef. Resolved #135 --- .../dmn/io/xml/XMLDecisionConverter.java | 4 +-- .../dmn/io/xml/XmlDecisionReaderSpec.groovy | 12 +++++++++ ...munda-dmn-1.1-example-unknown-type-ref.dmn | 25 +++++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example-unknown-type-ref.dmn diff --git a/src/main/java/org/powerflows/dmn/io/xml/XMLDecisionConverter.java b/src/main/java/org/powerflows/dmn/io/xml/XMLDecisionConverter.java index 2811cbd..0d63164 100644 --- a/src/main/java/org/powerflows/dmn/io/xml/XMLDecisionConverter.java +++ b/src/main/java/org/powerflows/dmn/io/xml/XMLDecisionConverter.java @@ -276,9 +276,7 @@ private ValueType resolveType(final String typeRef) { .filter(v -> v.name().equalsIgnoreCase(typeRef)) .findFirst() .orElseGet(() -> { - log.debug("Unable to resolve typeRef: {} to PowerFlows Type", typeRef); - - return null; + throw new DecisionReadException("Unable to resolve typeRef " + typeRef + " to PowerFlows Type"); }); } } diff --git a/src/test/groovy/org/powerflows/dmn/io/xml/XmlDecisionReaderSpec.groovy b/src/test/groovy/org/powerflows/dmn/io/xml/XmlDecisionReaderSpec.groovy index 071519e..5aad356 100644 --- a/src/test/groovy/org/powerflows/dmn/io/xml/XmlDecisionReaderSpec.groovy +++ b/src/test/groovy/org/powerflows/dmn/io/xml/XmlDecisionReaderSpec.groovy @@ -32,6 +32,7 @@ class XmlDecisionReaderSpec extends Specification { final String camundaExampleXmlBadOutputColumns = 'camunda-dmn-1.1-example-bad-output-columns.dmn' final String camundaExampleXmlDuplicateIds = 'camunda-dmn-1.1-example-duplicate-ids.dmn' final String camundaExampleXmlDuplicateLabels = 'camunda-dmn-1.1-example-duplicate-labels.dmn' + final String camundaExampleXmlUnknownTypeRef = 'camunda-dmn-1.1-example-unknown-type-ref.dmn' @Shared private XmlDecisionReader reader @@ -195,6 +196,17 @@ class XmlDecisionReaderSpec extends Specification { thrown(DecisionReadException) } + void 'should fail reading xml with unknown type ref'() { + given: + final InputStream inputStream = this.class.getResourceAsStream(camundaExampleXmlUnknownTypeRef) + + when: + reader.readAll(inputStream) + + then: + thrown(DecisionReadException) + } + private void assertSecondCamundaExampleContents(final Decision result) { with(result) { getId() == 'Decision_0replv7' diff --git a/src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example-unknown-type-ref.dmn b/src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example-unknown-type-ref.dmn new file mode 100644 index 0000000..d914deb --- /dev/null +++ b/src/test/resources/org/powerflows/dmn/io/xml/camunda-dmn-1.1-example-unknown-type-ref.dmn @@ -0,0 +1,25 @@ + + + + + + + + + + > 5 + + + + + Some Rule 1 Description + + > 20 + + + someVariable1 || someVariable2 + + + + + From 24e8f1134aff62efe3922b0e945a58306e5b96a8 Mon Sep 17 00:00:00 2001 From: Mariusz Kumor Date: Sat, 26 Jan 2019 18:31:14 +0100 Subject: [PATCH 10/10] Undesirable optionals after save to YAML representation. Resolved #137 --- .../dmn/engine/model/decision/Decision.java | 10 +- .../engine/model/decision/DecisionUtil.java | 2 +- .../model/decision/expression/Expression.java | 9 - .../dmn/io/yaml/CustomRepresenter.java | 21 +- .../dmn/io/yaml/YamlDecisionConverter.java | 97 ++++++-- .../dmn/io/yaml/model/YamlDecision.java | 6 +- .../dmn/io/yaml/model/field/YamlInput.java | 2 +- .../yaml/model/rule/entry/YamlInputEntry.java | 2 +- .../model/rule/entry/YamlOutputEntry.java | 2 +- .../engine/model/decision/DecisionSpec.groovy | 4 +- .../model/decision/DecisionUtilSpec.groovy | 2 +- .../io/yaml/YamlDecisionConverterSpec.groovy | 230 ++++++++++++++++++ .../dmn/io/yaml/YamlDecisionReaderSpec.groovy | 24 +- .../dmn/io/yaml/YamlDecisionWriterSpec.groovy | 17 +- .../dmn/io/yaml/converter-advanced.yml | 35 +++ .../converter-simple-with-expression-type.yml | 23 ++ .../dmn/io/yaml/converter-simple.yml | 17 ++ .../dmn/io/yaml/reference-multiple.yml | 64 +++-- .../dmn/io/yaml/reference-single.yml | 32 ++- 19 files changed, 485 insertions(+), 114 deletions(-) create mode 100644 src/test/groovy/org/powerflows/dmn/io/yaml/YamlDecisionConverterSpec.groovy create mode 100644 src/test/resources/org/powerflows/dmn/io/yaml/converter-advanced.yml create mode 100644 src/test/resources/org/powerflows/dmn/io/yaml/converter-simple-with-expression-type.yml create mode 100644 src/test/resources/org/powerflows/dmn/io/yaml/converter-simple.yml diff --git a/src/main/java/org/powerflows/dmn/engine/model/decision/Decision.java b/src/main/java/org/powerflows/dmn/engine/model/decision/Decision.java index cff09d8..80936da 100644 --- a/src/main/java/org/powerflows/dmn/engine/model/decision/Decision.java +++ b/src/main/java/org/powerflows/dmn/engine/model/decision/Decision.java @@ -43,11 +43,15 @@ public class Decision implements Serializable { private static final long serialVersionUID = 1; + public static final HitPolicy DEFAULT_HIT_POLICY = HitPolicy.UNIQUE; + public static final ExpressionType DEFAULT_EXPRESSION_TYPE = ExpressionType.LITERAL; + public static final EvaluationMode DEFAULT_EVALUATION_MODE = EvaluationMode.BOOLEAN; + private String id; private String name; - private HitPolicy hitPolicy = HitPolicy.UNIQUE; - private ExpressionType expressionType = ExpressionType.LITERAL; - private EvaluationMode evaluationMode = EvaluationMode.BOOLEAN; + private HitPolicy hitPolicy = DEFAULT_HIT_POLICY; + private ExpressionType expressionType = DEFAULT_EXPRESSION_TYPE; + private EvaluationMode evaluationMode = DEFAULT_EVALUATION_MODE; private List inputs = new ArrayList<>(); private List outputs = new ArrayList<>(); private List rules = new ArrayList<>(); diff --git a/src/main/java/org/powerflows/dmn/engine/model/decision/DecisionUtil.java b/src/main/java/org/powerflows/dmn/engine/model/decision/DecisionUtil.java index 31d471e..38d60fc 100644 --- a/src/main/java/org/powerflows/dmn/engine/model/decision/DecisionUtil.java +++ b/src/main/java/org/powerflows/dmn/engine/model/decision/DecisionUtil.java @@ -86,7 +86,7 @@ private static void assignEntriesDefaults(final List inputs, if (inputEntry.getExpression().getType() == null) { setValue(expressionTypeField, inputEntry.getExpression(), - inputsMap.get(inputEntry.getName()).getExpression().getType()); + decisionExpressionType); } if (inputEntry.getEvaluationMode() == null) { diff --git a/src/main/java/org/powerflows/dmn/engine/model/decision/expression/Expression.java b/src/main/java/org/powerflows/dmn/engine/model/decision/expression/Expression.java index ed649ad..f4e86de 100644 --- a/src/main/java/org/powerflows/dmn/engine/model/decision/expression/Expression.java +++ b/src/main/java/org/powerflows/dmn/engine/model/decision/expression/Expression.java @@ -69,15 +69,6 @@ public B type(ExpressionType type) { return (B) this; } - - @Override - protected Expression assembleProduct() { - if (product.type == null) { - product.type = ExpressionType.LITERAL; - } - - return product; - } } public static class Builder extends ExpressionBuilder { diff --git a/src/main/java/org/powerflows/dmn/io/yaml/CustomRepresenter.java b/src/main/java/org/powerflows/dmn/io/yaml/CustomRepresenter.java index 713942d..54a8836 100644 --- a/src/main/java/org/powerflows/dmn/io/yaml/CustomRepresenter.java +++ b/src/main/java/org/powerflows/dmn/io/yaml/CustomRepresenter.java @@ -15,8 +15,6 @@ */ package org.powerflows.dmn.io.yaml; -import org.powerflows.dmn.engine.model.decision.EvaluationMode; -import org.powerflows.dmn.engine.model.decision.expression.ExpressionType; import org.powerflows.dmn.io.yaml.model.YamlDecision; import org.powerflows.dmn.io.yaml.model.rule.entry.YamlInputEntry; import org.powerflows.dmn.io.yaml.model.rule.entry.YamlOutputEntry; @@ -42,8 +40,12 @@ class RepresentYamlInputEntry implements Represent { @Override public Node representData(Object data) { - if (data instanceof YamlInputEntry && ((YamlInputEntry) data).getExpressionType() == ExpressionType.LITERAL && ((YamlInputEntry) data).getEvaluationMode() == EvaluationMode.BOOLEAN) { - return represent(((YamlInputEntry) data).getExpression()); + if (data instanceof YamlInputEntry) { + final YamlInputEntry yie = (YamlInputEntry) data; + + if (yie.getEvaluationMode() == null && yie.getExpressionType() == null && yie.getNameAlias() == null) { + return represent(yie.getExpression()); + } } return representJavaBean(getProperties(data.getClass()), data); @@ -51,11 +53,14 @@ public Node representData(Object data) { } class RepresentYamlOutputEntry implements Represent { - @Override public Node representData(Object data) { - if (data instanceof YamlOutputEntry && ((YamlOutputEntry) data).getExpressionType() == ExpressionType.LITERAL) { - return represent(((YamlOutputEntry) data).getExpression()); + if (data instanceof YamlOutputEntry) { + final YamlOutputEntry yoe = (YamlOutputEntry) data; + + if (yoe.getExpressionType() == null) { + return represent(yoe.getExpression()); + } } return representJavaBean(getProperties(data.getClass()), data); @@ -81,7 +86,7 @@ private boolean isEmpty(final Object propertyValue) { } else if (propertyValue instanceof String) { empty = ((String) propertyValue).isEmpty(); } else { - empty = propertyValue == ExpressionType.LITERAL || propertyValue == EvaluationMode.BOOLEAN; + empty = false; } return empty; diff --git a/src/main/java/org/powerflows/dmn/io/yaml/YamlDecisionConverter.java b/src/main/java/org/powerflows/dmn/io/yaml/YamlDecisionConverter.java index ab4a0c8..fec044f 100644 --- a/src/main/java/org/powerflows/dmn/io/yaml/YamlDecisionConverter.java +++ b/src/main/java/org/powerflows/dmn/io/yaml/YamlDecisionConverter.java @@ -17,10 +17,14 @@ package org.powerflows.dmn.io.yaml; import org.powerflows.dmn.engine.model.decision.Decision; +import org.powerflows.dmn.engine.model.decision.EvaluationMode; +import org.powerflows.dmn.engine.model.decision.HitPolicy; import org.powerflows.dmn.engine.model.decision.expression.ExpressionType; import org.powerflows.dmn.engine.model.decision.field.Input; import org.powerflows.dmn.engine.model.decision.field.Output; import org.powerflows.dmn.engine.model.decision.rule.Rule; +import org.powerflows.dmn.engine.model.decision.rule.entry.InputEntry; +import org.powerflows.dmn.engine.model.decision.rule.entry.OutputEntry; import org.powerflows.dmn.io.DecisionToExternalModelConverter; import org.powerflows.dmn.io.yaml.model.YamlDecision; import org.powerflows.dmn.io.yaml.model.field.YamlFields; @@ -38,15 +42,20 @@ import java.util.stream.Collectors; public class YamlDecisionConverter implements DecisionToExternalModelConverter { + @Override public YamlDecision to(final Decision decision) { + final ExpressionType decisionExpressionType = findDecisionExpressionType(decision); + final EvaluationMode decisionEvaluationMode = Decision.DEFAULT_EVALUATION_MODE.equals(decision.getEvaluationMode()) ? null : decision.getEvaluationMode(); + final HitPolicy decisionHitPolicy = Decision.DEFAULT_HIT_POLICY.equals(decision.getHitPolicy()) ? null : decision.getHitPolicy(); + final YamlDecision yamlDecision = new YamlDecision(); yamlDecision.setId(decision.getId()); yamlDecision.setName(decision.getName()); - yamlDecision.setExpressionType(decision.getExpressionType()); - yamlDecision.setEvaluationMode(decision.getEvaluationMode()); - yamlDecision.setHitPolicy(decision.getHitPolicy()); - yamlDecision.setFields(createFields(decision.getInputs(), decision.getOutputs())); + yamlDecision.setExpressionType(Decision.DEFAULT_EXPRESSION_TYPE.equals(decisionExpressionType) ? null : decisionExpressionType); + yamlDecision.setEvaluationMode(decisionEvaluationMode); + yamlDecision.setHitPolicy(decisionHitPolicy); + yamlDecision.setFields(createFields(decision.getInputs(), decision.getOutputs(), decisionEvaluationMode, decisionExpressionType)); final Map inputsMap = decision.getInputs() .stream() @@ -54,13 +63,35 @@ public YamlDecision to(final Decision decision) { yamlDecision.setRules(decision.getRules() .stream() - .map(rule -> ruleToYamlRule(rule, inputsMap)) + .map(rule -> ruleToYamlRule(rule, inputsMap, decisionExpressionType)) .collect(Collectors.toList())); return yamlDecision; } - private YamlFields createFields(final List inputs, final List outputs) { + private ExpressionType findDecisionExpressionType(final Decision decision) { + final ExpressionType expressionType = decision.getRules().get(0).getInputEntries().get(0).getExpression().getType(); + + for (Rule rule : decision.getRules()) { + for (InputEntry inputEntry : rule.getInputEntries()) { + if (!expressionType.equals(inputEntry.getExpression().getType())) { + return decision.getExpressionType(); + } + } + for (OutputEntry outputEntry : rule.getOutputEntries()) { + if (!expressionType.equals(outputEntry.getExpression().getType())) { + return decision.getExpressionType(); + } + } + } + + return expressionType; + } + + private YamlFields createFields(final List inputs, + final List outputs, + final EvaluationMode decisionEvaluationMode, + final ExpressionType decisionExpressionType) { final YamlFields yamlFields = new YamlFields(); final LinkedHashMap in = new LinkedHashMap<>(); yamlFields.setIn(in); @@ -71,19 +102,18 @@ private YamlFields createFields(final List inputs, final List out final YamlInput yamlInput = new YamlInput(); yamlInput.setDescription(input.getDescription()); yamlInput.setType(input.getType()); - yamlInput.setEvaluationMode(input.getEvaluationMode()); + + if (decisionEvaluationMode != null && !decisionEvaluationMode.equals(input.getEvaluationMode())) { + yamlInput.setEvaluationMode(input.getEvaluationMode()); + } if (!Input.DEFAULT_NAME_ALIAS.equals(input.getNameAlias())) { yamlInput.setNameAlias(input.getNameAlias()); } - if (input.getExpression() != null && input.getExpression().getValue() != null) { - yamlInput.setExpression(input.getExpression().getValue()); - yamlInput.setExpressionType( - input.getExpression().getType() == ExpressionType.LITERAL ? null : input - .getExpression() - .getType()); - + yamlInput.setExpression(input.getExpression().getValue()); + if (!decisionExpressionType.equals(input.getExpression().getType())) { + yamlInput.setExpressionType(input.getExpression().getType()); } in.put(input.getName(), yamlInput); @@ -100,7 +130,7 @@ private YamlFields createFields(final List inputs, final List out return yamlFields; } - private YamlRule ruleToYamlRule(final Rule rule, final Map inputsMap) { + private YamlRule ruleToYamlRule(final Rule rule, final Map inputsMap, final ExpressionType decisionExpressionType) { final YamlRule yamlRule = new YamlRule(); yamlRule.setDescription(rule.getDescription()); final LinkedHashMap in = new LinkedHashMap<>(); @@ -109,14 +139,20 @@ private YamlRule ruleToYamlRule(final Rule rule, final Map inputs yamlRule.setOut(out); rule.getInputEntries().forEach(inputEntry -> { + final Input input = inputsMap.get(inputEntry.getName()); final YamlInputEntry yamlInputEntry = new YamlInputEntry(); - yamlInputEntry.setExpressionType(inputEntry.getExpression().getType()); + yamlInputEntry.setExpression(inputEntry.getExpression().getValue()); - yamlInputEntry.setEvaluationMode(inputEntry.getEvaluationMode()); - final Input input = inputsMap.get(inputEntry.getName()); + if (!inputEntry.getExpression().getType().equals(decisionExpressionType)) { + yamlInputEntry.setExpressionType(inputEntry.getExpression().getType()); + } - if (inputEntry.getNameAlias() != null && !inputEntry.getNameAlias().equals(input.getNameAlias())) { + if (!input.getEvaluationMode().equals(inputEntry.getEvaluationMode())) { + yamlInputEntry.setEvaluationMode(inputEntry.getEvaluationMode()); + } + + if (!inputEntry.getNameAlias().equals(input.getNameAlias())) { yamlInputEntry.setNameAlias(inputEntry.getNameAlias()); } @@ -125,7 +161,10 @@ private YamlRule ruleToYamlRule(final Rule rule, final Map inputs rule.getOutputEntries().forEach(outputEntry -> { final YamlOutputEntry yamlOutputEntry = new YamlOutputEntry(); - yamlOutputEntry.setExpressionType(outputEntry.getExpression().getType()); + if (!outputEntry.getExpression().getType().equals(decisionExpressionType)) { + yamlOutputEntry.setExpressionType(outputEntry.getExpression().getType()); + } + yamlOutputEntry.setExpression(outputEntry.getExpression().getValue()); out.put(outputEntry.getName(), yamlOutputEntry); @@ -138,14 +177,22 @@ private YamlRule ruleToYamlRule(final Rule rule, final Map inputs public Decision from(final YamlDecision model) { final Decision.Builder builder = Decision.builder(); builder.id(model.getId()) - .name(model.getName()) - .expressionType(model.getExpressionType()) - .evaluationMode(model.getEvaluationMode()) - .hitPolicy(model.getHitPolicy()); + .name(model.getName()); + + if (model.getExpressionType() != null) { + builder.expressionType(model.getExpressionType()); + } + + if (model.getEvaluationMode() != null) { + builder.evaluationMode(model.getEvaluationMode()); + } + + if (model.getHitPolicy() != null) { + builder.hitPolicy(model.getHitPolicy()); + } model.getFields().getIn().forEach((name, input) -> builder .withInput(inputBuilder -> { - if (input.getNameAlias() != null) { inputBuilder.nameAlias(input.getNameAlias()); } diff --git a/src/main/java/org/powerflows/dmn/io/yaml/model/YamlDecision.java b/src/main/java/org/powerflows/dmn/io/yaml/model/YamlDecision.java index f6d66a9..472eb42 100644 --- a/src/main/java/org/powerflows/dmn/io/yaml/model/YamlDecision.java +++ b/src/main/java/org/powerflows/dmn/io/yaml/model/YamlDecision.java @@ -29,9 +29,9 @@ public final class YamlDecision { private String id; private String name; - private HitPolicy hitPolicy = HitPolicy.UNIQUE; - private ExpressionType expressionType = ExpressionType.LITERAL; - private EvaluationMode evaluationMode = EvaluationMode.BOOLEAN; + private HitPolicy hitPolicy; + private ExpressionType expressionType; + private EvaluationMode evaluationMode; private YamlFields fields; private List rules; } diff --git a/src/main/java/org/powerflows/dmn/io/yaml/model/field/YamlInput.java b/src/main/java/org/powerflows/dmn/io/yaml/model/field/YamlInput.java index 4844cca..507cdc5 100644 --- a/src/main/java/org/powerflows/dmn/io/yaml/model/field/YamlInput.java +++ b/src/main/java/org/powerflows/dmn/io/yaml/model/field/YamlInput.java @@ -26,7 +26,7 @@ public final class YamlInput { private String description; private String nameAlias; private ValueType type; - private ExpressionType expressionType = ExpressionType.LITERAL; + private ExpressionType expressionType; private Object expression; private EvaluationMode evaluationMode; diff --git a/src/main/java/org/powerflows/dmn/io/yaml/model/rule/entry/YamlInputEntry.java b/src/main/java/org/powerflows/dmn/io/yaml/model/rule/entry/YamlInputEntry.java index a6473b0..00908bb 100644 --- a/src/main/java/org/powerflows/dmn/io/yaml/model/rule/entry/YamlInputEntry.java +++ b/src/main/java/org/powerflows/dmn/io/yaml/model/rule/entry/YamlInputEntry.java @@ -30,7 +30,7 @@ public YamlInputEntry() { } private String nameAlias; - private ExpressionType expressionType = ExpressionType.LITERAL; + private ExpressionType expressionType; private Object expression; private EvaluationMode evaluationMode; } diff --git a/src/main/java/org/powerflows/dmn/io/yaml/model/rule/entry/YamlOutputEntry.java b/src/main/java/org/powerflows/dmn/io/yaml/model/rule/entry/YamlOutputEntry.java index 4a86647..fd826c3 100644 --- a/src/main/java/org/powerflows/dmn/io/yaml/model/rule/entry/YamlOutputEntry.java +++ b/src/main/java/org/powerflows/dmn/io/yaml/model/rule/entry/YamlOutputEntry.java @@ -28,6 +28,6 @@ public YamlOutputEntry(final Object value) { public YamlOutputEntry() { } - private ExpressionType expressionType = ExpressionType.LITERAL; + private ExpressionType expressionType; private Object expression; } diff --git a/src/test/groovy/org/powerflows/dmn/engine/model/decision/DecisionSpec.groovy b/src/test/groovy/org/powerflows/dmn/engine/model/decision/DecisionSpec.groovy index 9954da4..8a22dae 100644 --- a/src/test/groovy/org/powerflows/dmn/engine/model/decision/DecisionSpec.groovy +++ b/src/test/groovy/org/powerflows/dmn/engine/model/decision/DecisionSpec.groovy @@ -180,7 +180,7 @@ class DecisionSpec extends Specification { final Expression input1Expression2 = input2.getExpression() with(input1Expression2) { - getType() == ExpressionType.LITERAL + getType() == someExpressionType getValue() == null } @@ -494,7 +494,7 @@ class DecisionSpec extends Specification { final Expression rule2OutputEntry2Expression = rule2OutputEntry2.getExpression() with(rule2OutputEntry2Expression) { - getType() == ExpressionType.LITERAL + getType() == someExpressionType getValue() == someRule2OutputEntry2ExpressionValue } } diff --git a/src/test/groovy/org/powerflows/dmn/engine/model/decision/DecisionUtilSpec.groovy b/src/test/groovy/org/powerflows/dmn/engine/model/decision/DecisionUtilSpec.groovy index 495988c..60c7a7e 100644 --- a/src/test/groovy/org/powerflows/dmn/engine/model/decision/DecisionUtilSpec.groovy +++ b/src/test/groovy/org/powerflows/dmn/engine/model/decision/DecisionUtilSpec.groovy @@ -75,7 +75,7 @@ class DecisionUtilSpec extends Specification { input2.expression.type == ExpressionType.GROOVY input2.evaluationMode == EvaluationMode.INPUT_COMPARISON - inputEntry1.expression.type == ExpressionType.FEEL + inputEntry1.expression.type == ExpressionType.GROOVY inputEntry1.evaluationMode == EvaluationMode.BOOLEAN inputEntry1.nameAlias == input1NameAlias inputEntry2.expression.type == ExpressionType.LITERAL diff --git a/src/test/groovy/org/powerflows/dmn/io/yaml/YamlDecisionConverterSpec.groovy b/src/test/groovy/org/powerflows/dmn/io/yaml/YamlDecisionConverterSpec.groovy new file mode 100644 index 0000000..2dbcfeb --- /dev/null +++ b/src/test/groovy/org/powerflows/dmn/io/yaml/YamlDecisionConverterSpec.groovy @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2018-present PowerFlows.org - all rights reserved. + * + * 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.powerflows.dmn.io.yaml + +import org.powerflows.dmn.engine.model.decision.Decision +import org.powerflows.dmn.engine.model.decision.EvaluationMode +import org.powerflows.dmn.engine.model.decision.HitPolicy +import org.powerflows.dmn.engine.model.decision.expression.ExpressionType +import org.powerflows.dmn.engine.model.decision.field.ValueType +import org.powerflows.dmn.io.yaml.model.YamlDecision +import spock.lang.Specification + +class YamlDecisionConverterSpec extends Specification { + + private final YamlDecisionConverter converter = new YamlDecisionConverter(); + + void 'should convert simple decision to yaml '() { + given: + final InputStream inputStream = this.class.getResourceAsStream('converter-simple.yml') + final Decision decision = new YamlDecisionReader().read(inputStream).get() + + when: + final YamlDecision yamlDecision = converter.to(decision) + + then: + yamlDecision != null + + with(yamlDecision) { + getId() == 'some_table_id' + getName() == 'Some Table Name' + getHitPolicy() == null + getExpressionType() == null + getEvaluationMode() == null + getFields().getIn().size() == 2 + getFields().getOut().size() == 1 + getRules().size() == 1 + } + + with(yamlDecision.getFields().getIn().get('inputOne')) { + getType() == ValueType.INTEGER + getEvaluationMode() == null + getExpressionType() == null + getExpression() == null + getNameAlias() == null + getDescription() == null + } + + with(yamlDecision.getFields().getIn().get('inputTwo')) { + getType() == ValueType.STRING + getEvaluationMode() == null + getExpressionType() == null + getExpression() == null + getNameAlias() == null + getDescription() == null + } + + with(yamlDecision.getFields().getOut().get('outputOne')) { + getType() == ValueType.BOOLEAN + getDescription() == null + } + + yamlDecision.getRules().get(0).getDescription() == null + + with(yamlDecision.getRules().get(0).getIn().get('inputOne')) { + getEvaluationMode() == null + getExpressionType() == null + getExpression() == '> 20' + getNameAlias() == null + } + + with(yamlDecision.getRules().get(0).getIn().get('inputTwo')) { + getEvaluationMode() == null + getExpressionType() == null + getExpression() == 3 + getNameAlias() == null + } + + with(yamlDecision.getRules().get(0).getOut().get('outputOne')) { + getExpressionType() == null + getExpression() == 'someVariable1 || someVariable2' + } + } + + void 'should convert simple with expression type decision to yaml '() { + given: + final InputStream inputStream = this.class.getResourceAsStream('converter-simple-with-expression-type.yml') + final Decision decision = new YamlDecisionReader().read(inputStream).get() + + when: + final YamlDecision yamlDecision = converter.to(decision) + + then: + yamlDecision != null + + with(yamlDecision) { + getId() == 'some_table_id' + getName() == 'Some Table Name' + getHitPolicy() == null + getExpressionType() == null + getEvaluationMode() == null + getFields().getIn().size() == 2 + getFields().getOut().size() == 1 + getRules().size() == 1 + } + + with(yamlDecision.getFields().getIn().get('inputOne')) { + getType() == ValueType.INTEGER + getEvaluationMode() == null + getExpressionType() == null + getExpression() == null + getNameAlias() == null + getDescription() == null + } + + with(yamlDecision.getFields().getIn().get('inputTwo')) { + getType() == ValueType.STRING + getEvaluationMode() == null + getExpressionType() == null + getExpression() == null + getNameAlias() == null + getDescription() == null + } + + with(yamlDecision.getFields().getOut().get('outputOne')) { + getType() == ValueType.BOOLEAN + getDescription() == null + } + + yamlDecision.getRules().get(0).getDescription() == null + + with(yamlDecision.getRules().get(0).getIn().get('inputOne')) { + getEvaluationMode() == null + getExpressionType() == ExpressionType.JUEL + getExpression() == '> 20' + getNameAlias() == null + } + + with(yamlDecision.getRules().get(0).getIn().get('inputTwo')) { + getEvaluationMode() == null + getExpressionType() == ExpressionType.JUEL + getExpression() == 3 + getNameAlias() == null + } + + with(yamlDecision.getRules().get(0).getOut().get('outputOne')) { + getExpressionType() == ExpressionType.JAVASCRIPT + getExpression() == 'someVariable1 || someVariable2' + } + } + + void 'should convert advanced decision to yaml'() { + given: + final InputStream inputStream = this.class.getResourceAsStream('converter-advanced.yml') + final Decision decision = new YamlDecisionReader().read(inputStream).get() + + when: + final YamlDecision yamlDecision = converter.to(decision) + + then: + yamlDecision != null + + with(yamlDecision) { + getId() == 'some_table_id' + getName() == 'Some Table Name' + getHitPolicy() == HitPolicy.COLLECT + getExpressionType() == ExpressionType.GROOVY + getEvaluationMode() == EvaluationMode.INPUT_COMPARISON + getFields().getIn().size() == 2 + getFields().getOut().size() == 1 + getRules().size() == 1 + } + + with(yamlDecision.getFields().getIn().get('inputOne')) { + getType() == ValueType.INTEGER + getEvaluationMode() == EvaluationMode.BOOLEAN + getExpressionType() == ExpressionType.JAVASCRIPT + getExpression() == null + getNameAlias() == 'inputOneAlias' + getDescription() == null + } + + with(yamlDecision.getFields().getIn().get('inputTwo')) { + getType() == ValueType.STRING + getEvaluationMode() == null + getExpressionType() == ExpressionType.FEEL + getExpression() == null + getNameAlias() == 'inputTwoAlias' + getDescription() == null + } + + with(yamlDecision.getFields().getOut().get('outputOne')) { + getType() == ValueType.BOOLEAN + getDescription() == null + } + + yamlDecision.getRules().get(0).getDescription() == null + + with(yamlDecision.getRules().get(0).getIn().get('inputOne')) { + getEvaluationMode() == EvaluationMode.INPUT_COMPARISON + getExpressionType() == ExpressionType.JUEL + getExpression() == '> 20' + getNameAlias() == 'inputOneAliasOverridden' + } + + with(yamlDecision.getRules().get(0).getIn().get('inputTwo')) { + getEvaluationMode() == EvaluationMode.BOOLEAN + getExpressionType() == ExpressionType.JAVASCRIPT + getExpression() == 3 + getNameAlias() == null + } + + with(yamlDecision.getRules().get(0).getOut().get('outputOne')) { + getExpressionType() == ExpressionType.JUEL + getExpression() == 'someVariable1 || someVariable2' + } + } +} diff --git a/src/test/groovy/org/powerflows/dmn/io/yaml/YamlDecisionReaderSpec.groovy b/src/test/groovy/org/powerflows/dmn/io/yaml/YamlDecisionReaderSpec.groovy index 51500fd..0e20bad 100644 --- a/src/test/groovy/org/powerflows/dmn/io/yaml/YamlDecisionReaderSpec.groovy +++ b/src/test/groovy/org/powerflows/dmn/io/yaml/YamlDecisionReaderSpec.groovy @@ -112,7 +112,7 @@ class YamlDecisionReaderSpec extends Specification { with(decision.inputs[1]) { name == 'colour' expression.value == null - expression.type == ExpressionType.LITERAL + expression.type == ExpressionType.GROOVY description == 'This is something about colour' type == ValueType.STRING } @@ -126,37 +126,37 @@ class YamlDecisionReaderSpec extends Specification { with(decision.rules[0]) { description == '3 allows always' inputEntries[0].name == 'age' - inputEntries[0].expression.type == ExpressionType.LITERAL + inputEntries[0].expression.type == ExpressionType.GROOVY inputEntries[0].expression.value == 3 outputEntries[0].name == 'allow' - outputEntries[0].expression.type == ExpressionType.LITERAL + outputEntries[0].expression.type == ExpressionType.GROOVY outputEntries[0].expression.value == true } with(decision.rules[1]) { description == null inputEntries[0].name == 'age' - inputEntries[0].expression.type == ExpressionType.LITERAL + inputEntries[0].expression.type == ExpressionType.GROOVY inputEntries[0].expression.value == 8 inputEntries[1].name == 'colour' - inputEntries[1].expression.type == ExpressionType.LITERAL + inputEntries[1].expression.type == ExpressionType.GROOVY inputEntries[1].expression.value == 'red' outputEntries[0].name == 'allow' - outputEntries[0].expression.type == ExpressionType.LITERAL + outputEntries[0].expression.type == ExpressionType.GROOVY outputEntries[0].expression.value == true } with(decision.rules[2]) { description == 'Green allows always' inputEntries[0].name == 'colour' - inputEntries[0].expression.type == ExpressionType.LITERAL + inputEntries[0].expression.type == ExpressionType.GROOVY inputEntries[0].expression.value == 'green' outputEntries[0].name == 'allow' - outputEntries[0].expression.type == ExpressionType.LITERAL + outputEntries[0].expression.type == ExpressionType.GROOVY outputEntries[0].expression.value == true } @@ -168,11 +168,11 @@ class YamlDecisionReaderSpec extends Specification { inputEntries[0].expression.value == 'not("blue", "purple")' inputEntries[1].name == 'age' - inputEntries[1].expression.type == ExpressionType.LITERAL + inputEntries[1].expression.type == ExpressionType.GROOVY inputEntries[1].expression.value == 10 outputEntries[0].name == 'allow' - outputEntries[0].expression.type == ExpressionType.LITERAL + outputEntries[0].expression.type == ExpressionType.GROOVY outputEntries[0].expression.value == true } @@ -184,11 +184,11 @@ class YamlDecisionReaderSpec extends Specification { inputEntries[0].expression.value == 'not( "red", "pink" )' inputEntries[1].name == 'age' - inputEntries[1].expression.type == ExpressionType.LITERAL + inputEntries[1].expression.type == ExpressionType.GROOVY inputEntries[1].expression.value == 20 outputEntries[0].name == 'allow' - outputEntries[0].expression.type == ExpressionType.LITERAL + outputEntries[0].expression.type == ExpressionType.GROOVY outputEntries[0].expression.value == true } diff --git a/src/test/groovy/org/powerflows/dmn/io/yaml/YamlDecisionWriterSpec.groovy b/src/test/groovy/org/powerflows/dmn/io/yaml/YamlDecisionWriterSpec.groovy index bbf6a8c..a46e780 100644 --- a/src/test/groovy/org/powerflows/dmn/io/yaml/YamlDecisionWriterSpec.groovy +++ b/src/test/groovy/org/powerflows/dmn/io/yaml/YamlDecisionWriterSpec.groovy @@ -27,7 +27,7 @@ class YamlDecisionWriterSpec extends Specification { final String someTableId1 = 'some_table_id_1' final String someTableId2 = 'some_table_id_2' final String someTableName = 'Some Table Name' - final HitPolicy someHitPolicy = HitPolicy.UNIQUE + final HitPolicy someHitPolicy = HitPolicy.COLLECT final ExpressionType someExpressionType = ExpressionType.GROOVY final EvaluationMode someEvaluationMode1 = EvaluationMode.BOOLEAN final EvaluationMode someEvaluationMode2 = EvaluationMode.INPUT_COMPARISON @@ -149,12 +149,12 @@ class YamlDecisionWriterSpec extends Specification { result.hitPolicy == decision.hitPolicy result.name == decision.name result.inputs[0].name == decision.inputs[0].name - result.inputs[0].evaluationMode == decision.evaluationMode + result.inputs[0].evaluationMode == decision.inputs[0].evaluationMode result.inputs[0].type == decision.inputs[0].type result.inputs[0].description == decision.inputs[0].description result.inputs[0].expression == decision.inputs[0].expression result.inputs[1].name == decision.inputs[1].name - result.inputs[1].evaluationMode == decision.evaluationMode + result.inputs[1].evaluationMode == decision.inputs[1].evaluationMode result.inputs[1].type == decision.inputs[1].type result.inputs[1].description == decision.inputs[1].description result.inputs[1].expression == decision.inputs[1].expression @@ -168,22 +168,23 @@ class YamlDecisionWriterSpec extends Specification { result.rules[0].description == decision.rules[0].description result.rules[0].inputEntries.size() == 2 result.rules[0].inputEntries[0].expression == decision.rules[0].inputEntries[0].expression - result.rules[0].inputEntries[0].evaluationMode == decision.evaluationMode + result.rules[0].inputEntries[0].evaluationMode == decision.rules[0].inputEntries[0].evaluationMode result.rules[0].inputEntries[0].name == decision.rules[0].inputEntries[0].name result.rules[0].inputEntries[1].expression == decision.rules[0].inputEntries[1].expression - result.rules[0].inputEntries[1].evaluationMode == decision.evaluationMode + result.rules[0].inputEntries[1].evaluationMode == decision.rules[0].inputEntries[1].evaluationMode result.rules[0].inputEntries[1].name == decision.rules[0].inputEntries[1].name result.rules[0].outputEntries.size() == 1 - result.rules[0].outputEntries[0].expression == decision.rules[0].outputEntries[0].expression + result.rules[0].outputEntries[0].expression.value == decision.rules[0].outputEntries[0].expression.value + result.rules[0].outputEntries[0].expression.type == decision.rules[0].outputEntries[0].expression.type result.rules[0].outputEntries[0].name == decision.rules[0].outputEntries[0].name result.rules[1].description == decision.rules[1].description result.rules[1].inputEntries.size() == 2 result.rules[1].inputEntries[0].expression == decision.rules[1].inputEntries[0].expression - result.rules[1].inputEntries[0].evaluationMode == decision.evaluationMode + result.rules[1].inputEntries[0].evaluationMode == decision.rules[1].inputEntries[0].evaluationMode result.rules[1].inputEntries[0].name == decision.rules[1].inputEntries[0].name result.rules[1].inputEntries[1].expression == decision.rules[1].inputEntries[1].expression - result.rules[1].inputEntries[1].evaluationMode == decision.evaluationMode + result.rules[1].inputEntries[1].evaluationMode == decision.rules[1].inputEntries[1].evaluationMode result.rules[1].inputEntries[1].name == decision.rules[1].inputEntries[1].name result.rules[1].outputEntries.size() == 2 result.rules[1].outputEntries[0].expression == decision.rules[1].outputEntries[0].expression diff --git a/src/test/resources/org/powerflows/dmn/io/yaml/converter-advanced.yml b/src/test/resources/org/powerflows/dmn/io/yaml/converter-advanced.yml new file mode 100644 index 0000000..845bda9 --- /dev/null +++ b/src/test/resources/org/powerflows/dmn/io/yaml/converter-advanced.yml @@ -0,0 +1,35 @@ +id: some_table_id +name: Some Table Name +hit-policy: COLLECT +expression-type: GROOVY +evaluation-mode: INPUT_COMPARISON +fields: + in: + inputOne: + name-alias: inputOneAlias + type: INTEGER + expression-type: JAVASCRIPT + evaluation-mode: BOOLEAN + inputTwo: + name-alias: inputTwoAlias + type: STRING + expression-type: FEEL + evaluation-mode: INPUT_COMPARISON + out: + outputOne: + type: BOOLEAN +rules: +- in: + inputOne: + name-alias: inputOneAliasOverridden + expression: '> 20' + expression-type: JUEL + evaluation-mode: INPUT_COMPARISON + inputTwo: + expression: 3 + expression-type: JAVASCRIPT + evaluation-mode: BOOLEAN + out: + outputOne: + expression: someVariable1 || someVariable2 + expression-type: JUEL diff --git a/src/test/resources/org/powerflows/dmn/io/yaml/converter-simple-with-expression-type.yml b/src/test/resources/org/powerflows/dmn/io/yaml/converter-simple-with-expression-type.yml new file mode 100644 index 0000000..271e5f5 --- /dev/null +++ b/src/test/resources/org/powerflows/dmn/io/yaml/converter-simple-with-expression-type.yml @@ -0,0 +1,23 @@ +id: some_table_id +name: Some Table Name +fields: + in: + inputOne: + type: INTEGER + inputTwo: + type: STRING + out: + outputOne: + type: BOOLEAN +rules: +- in: + inputOne: + expression: '> 20' + expression-type: JUEL + inputTwo: + expression: 3 + expression-type: JUEL + out: + outputOne: + expression: someVariable1 || someVariable2 + expression-type: JAVASCRIPT diff --git a/src/test/resources/org/powerflows/dmn/io/yaml/converter-simple.yml b/src/test/resources/org/powerflows/dmn/io/yaml/converter-simple.yml new file mode 100644 index 0000000..47e54b4 --- /dev/null +++ b/src/test/resources/org/powerflows/dmn/io/yaml/converter-simple.yml @@ -0,0 +1,17 @@ +id: some_table_id +name: Some Table Name +fields: + in: + inputOne: + type: INTEGER + inputTwo: + type: STRING + out: + outputOne: + type: BOOLEAN +rules: +- in: + inputOne: '> 20' + inputTwo: 3 + out: + outputOne: someVariable1 || someVariable2 \ No newline at end of file diff --git a/src/test/resources/org/powerflows/dmn/io/yaml/reference-multiple.yml b/src/test/resources/org/powerflows/dmn/io/yaml/reference-multiple.yml index ed8b10b..3b9d00c 100644 --- a/src/test/resources/org/powerflows/dmn/io/yaml/reference-multiple.yml +++ b/src/test/resources/org/powerflows/dmn/io/yaml/reference-multiple.yml @@ -1,6 +1,6 @@ id: some_table_id_1 name: Some Table Name -hit-policy: UNIQUE +hit-policy: COLLECT expression-type: GROOVY evaluation-mode: INPUT_COMPARISON fields: @@ -10,8 +10,10 @@ fields: type: INTEGER expression-type: FEEL expression: '> 5' + evaluation-mode: BOOLEAN inputTwo: type: INTEGER + evaluation-mode: BOOLEAN out: outputOne: description: Some Output 1 Description @@ -22,30 +24,34 @@ fields: rules: - description: Some Rule 1 Description in: - inputOne: - expression-type: GROOVY - expression: '> 20' + inputOne: '> 20' inputTwo: expression-type: FEEL expression: not("blue", "purple") out: - outputOne: - expression-type: GROOVY - expression: someVariable1 || someVariable2 + outputOne: someVariable1 || someVariable2 - in: - inputOne: 5 + inputOne: + expression-type: LITERAL + expression: 5 inputTwo: - - one - - two + expression-type: LITERAL + expression: + - one + - two out: - outputOne: true + outputOne: + expression-type: LITERAL + expression: true outputTwo: - - The output value 1 - - The output value 2 + expression-type: LITERAL + expression: + - The output value 1 + - The output value 2 --- id: some_table_id_2 name: Some Table Name -hit-policy: UNIQUE +hit-policy: COLLECT expression-type: GROOVY evaluation-mode: INPUT_COMPARISON fields: @@ -55,8 +61,10 @@ fields: type: INTEGER expression-type: FEEL expression: '> 5' + evaluation-mode: BOOLEAN inputTwo: type: INTEGER + evaluation-mode: BOOLEAN out: outputOne: description: Some Output 1 Description @@ -67,23 +75,27 @@ fields: rules: - description: Some Rule 1 Description in: - inputOne: - expression-type: GROOVY - expression: '> 20' + inputOne: '> 20' inputTwo: expression-type: FEEL expression: not("blue", "purple") out: - outputOne: - expression-type: GROOVY - expression: someVariable1 || someVariable2 + outputOne: someVariable1 || someVariable2 - in: - inputOne: 5 + inputOne: + expression-type: LITERAL + expression: 5 inputTwo: - - one - - two + expression-type: LITERAL + expression: + - one + - two out: - outputOne: true + outputOne: + expression-type: LITERAL + expression: true outputTwo: - - The output value 1 - - The output value 2 + expression-type: LITERAL + expression: + - The output value 1 + - The output value 2 diff --git a/src/test/resources/org/powerflows/dmn/io/yaml/reference-single.yml b/src/test/resources/org/powerflows/dmn/io/yaml/reference-single.yml index ec9376a..9a24f68 100644 --- a/src/test/resources/org/powerflows/dmn/io/yaml/reference-single.yml +++ b/src/test/resources/org/powerflows/dmn/io/yaml/reference-single.yml @@ -1,6 +1,6 @@ id: some_table_id_1 name: Some Table Name -hit-policy: UNIQUE +hit-policy: COLLECT expression-type: GROOVY evaluation-mode: INPUT_COMPARISON fields: @@ -10,8 +10,10 @@ fields: type: INTEGER expression-type: FEEL expression: '> 5' + evaluation-mode: BOOLEAN inputTwo: type: INTEGER + evaluation-mode: BOOLEAN out: outputOne: description: Some Output 1 Description @@ -22,23 +24,27 @@ fields: rules: - description: Some Rule 1 Description in: - inputOne: - expression-type: GROOVY - expression: '> 20' + inputOne: '> 20' inputTwo: expression-type: FEEL expression: not("blue", "purple") out: - outputOne: - expression-type: GROOVY - expression: someVariable1 || someVariable2 + outputOne: someVariable1 || someVariable2 - in: - inputOne: 5 + inputOne: + expression-type: LITERAL + expression: 5 inputTwo: - - one - - two + expression-type: LITERAL + expression: + - one + - two out: - outputOne: true + outputOne: + expression-type: LITERAL + expression: true outputTwo: - - The output value 1 - - The output value 2 + expression-type: LITERAL + expression: + - The output value 1 + - The output value 2