Skip to content

Commit

Permalink
[DROOLS-4698] Managing user defined expressions for Collections (apac…
Browse files Browse the repository at this point in the history
…he#2691)

* DROOLS-4698: Managing expressions for Collections

* DROOLS-4698: Managing expressions for Collections

* DROOLS-4698: Managing expressions for Collections + Tests

* DROOLS-4698: Managing expressions for Collections + Tests

* DROOLS-4698: Managing expressions for Collections + Tests

* DROOLS-4698: Managing expressions for Collections + Tests

* DROOLS-4698: Managing expressions for Collections + Tests

* DROOLS-4698: Managing expressions for Collections + Tests

* DROOLS-4698: Managing expressions for Collections + Tests

* Merging from origin/master

* DROOLS-4698: Changes required during CR

* DROOLS-4698: Changes required during Code Review

* DROOLS-4698: Changes required during Code Review

* DROOLS-4698: Changes required during Code Review

* DROOLS-4698: Changes required during Code Review

* DROOLS-4698: Changes required during Code Review

* DROOLS-4698: Changes required during Code Review

* DROOLS-4698: Changes required during Code Review

* DROOLS-4698: Changes required during Code Review

* DROOLS-4698: Additional tests.

* DROOLS-4698: Increase coverage (#2)

Co-authored-by: Jozef Marko <jomarko@redhat.com>
  • Loading branch information
2 people authored and danielezonca committed Jan 27, 2020
1 parent ff514ea commit 0c0202b
Show file tree
Hide file tree
Showing 11 changed files with 413 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ public class ConstantsHolder {
public static final List<String> SETTINGS = Collections.unmodifiableList(Arrays.asList(DMO_SESSION_NODE, "dmnFilePath", "type", "fileName", "kieSession",
"kieBase", "ruleFlowGroup", "dmnNamespace", "dmnName", "skipFromBuild", "stateless"));

public static final String MALFORMED_RAW_DATA_MESSAGE = "Malformed raw data";
public static final String MALFORMED_MVEL_EXPRESSION = "Malformed MVEL expression";

private ConstantsHolder() {
// Not instantiable
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,18 @@

package org.drools.scenariosimulation.backend.expression;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.drools.scenariosimulation.api.utils.ConstantsHolder;
import org.drools.scenariosimulation.api.utils.ScenarioSimulationSharedUtils;
import org.drools.scenariosimulation.backend.util.JsonUtils;

import static org.drools.scenariosimulation.api.utils.ConstantsHolder.VALUE;

Expand All @@ -44,7 +45,7 @@ public Object evaluateLiteralExpression(String rawExpression, String className,
@Override
public boolean evaluateUnaryExpression(String rawExpression, Object resultValue, Class<?> resultClass) {
if (isStructuredResult(resultClass)) {
return verifyResult(rawExpression, resultValue);
return verifyResult(rawExpression, resultValue, resultClass);
} else {
return internalUnaryEvaluation(rawExpression, resultValue, resultClass, false);
}
Expand Down Expand Up @@ -73,21 +74,23 @@ protected Object convertResult(String rawString, String className, List<String>
return null;
}

ObjectMapper objectMapper = new ObjectMapper();
try {
JsonNode jsonNode = objectMapper.readTree(rawString);
if (jsonNode.isArray()) {
return createAndFillList((ArrayNode) jsonNode, new ArrayList<>(), className, genericClasses);
} else if (jsonNode.isObject()) {
return createAndFillObject((ObjectNode) jsonNode,
createObject(className, genericClasses),
className,
genericClasses);
}
throw new IllegalArgumentException("Malformed raw data");
} catch (IOException e) {
throw new IllegalArgumentException("Malformed raw data", e);
Optional<JsonNode> optionalJsonNode = JsonUtils.convertFromStringToJSONNode(rawString);
JsonNode jsonNode = optionalJsonNode.orElseThrow(() -> new IllegalArgumentException(ConstantsHolder.MALFORMED_RAW_DATA_MESSAGE));

if (jsonNode.isTextual()) {
/* JSON Text: expression manually written by the user to build a list/map */
return internalLiteralEvaluation(jsonNode.asText(), className);
} else if (jsonNode.isArray()) {
/* JSON Array: list of expressions created using List collection editor */
return createAndFillList((ArrayNode) jsonNode, new ArrayList<>(), className, genericClasses);
} else if (jsonNode.isObject()) {
/* JSON Map: map of expressions created using Map collection editor */
return createAndFillObject((ObjectNode) jsonNode,
createObject(className, genericClasses),
className,
genericClasses);
}
throw new IllegalArgumentException(ConstantsHolder.MALFORMED_RAW_DATA_MESSAGE);
}

protected List<Object> createAndFillList(ArrayNode json, List<Object> toReturn, String className, List<String> genericClasses) {
Expand Down Expand Up @@ -140,26 +143,27 @@ protected Object createAndFillObject(ObjectNode json, Object toReturn, String cl
return toReturn;
}

protected boolean verifyResult(String rawExpression, Object resultRaw) {
protected boolean verifyResult(String rawExpression, Object resultRaw, Class<?> resultClass) {
if (rawExpression == null) {
return resultRaw == null;
}
if (resultRaw != null && !(resultRaw instanceof List) && !(resultRaw instanceof Map)) {
throw new IllegalArgumentException("A list or map was expected");
}
ObjectMapper objectMapper = new ObjectMapper();
Optional<JsonNode> optionalJsonNode = JsonUtils.convertFromStringToJSONNode(rawExpression);
JsonNode jsonNode = optionalJsonNode.orElseThrow(() -> new IllegalArgumentException(ConstantsHolder.MALFORMED_RAW_DATA_MESSAGE));

try {
JsonNode jsonNode = objectMapper.readTree(rawExpression);
if (jsonNode.isArray()) {
return verifyList((ArrayNode) jsonNode, (List) resultRaw);
} else if (jsonNode.isObject()) {
return verifyObject((ObjectNode) jsonNode, resultRaw);
}
throw new IllegalArgumentException("Malformed raw data");
} catch (IOException e) {
throw new IllegalArgumentException("Malformed raw data", e);
if (jsonNode.isTextual()) {
/* JSON Text: expression manually written by the user to build a list/map */
return internalUnaryEvaluation(jsonNode.asText(), resultRaw, resultClass, false);
} else if (jsonNode.isArray()) {
/* JSON Array: list of expressions created using List collection editor */
return verifyList((ArrayNode) jsonNode, (List) resultRaw);
} else if (jsonNode.isObject()) {
/* JSON Map: map of expressions created using Map collection editor */
return verifyObject((ObjectNode) jsonNode, resultRaw);
}
throw new IllegalArgumentException(ConstantsHolder.MALFORMED_RAW_DATA_MESSAGE);
}

protected boolean verifyList(ArrayNode json, List resultRaw) {
Expand Down Expand Up @@ -296,15 +300,15 @@ protected String getSimpleTypeNodeTextValue(JsonNode jsonNode) {
return jsonNode.get(VALUE).textValue();
}

abstract protected boolean internalUnaryEvaluation(String rawExpression, Object resultValue, Class<?> resultClass, boolean skipEmptyString);
protected abstract boolean internalUnaryEvaluation(String rawExpression, Object resultValue, Class<?> resultClass, boolean skipEmptyString);

abstract protected Object internalLiteralEvaluation(String raw, String className);
protected abstract Object internalLiteralEvaluation(String raw, String className);

abstract protected Object extractFieldValue(Object result, String fieldName);
protected abstract Object extractFieldValue(Object result, String fieldName);

abstract protected Object createObject(String className, List<String> genericClasses);
protected abstract Object createObject(String className, List<String> genericClasses);

abstract protected void setField(Object toReturn, String fieldName, Object fieldValue);
protected abstract void setField(Object toReturn, String fieldName, Object fieldValue);

/**
* Return a pair with field className as key and list of generics as value
Expand All @@ -314,5 +318,5 @@ protected String getSimpleTypeNodeTextValue(JsonNode jsonNode) {
* @param genericClasses : list of generics related to this field
* @return
*/
abstract protected Map.Entry<String, List<String>> getFieldClassNameAndGenerics(Object element, String fieldName, String className, List<String> genericClasses);
protected abstract Map.Entry<String, List<String>> getFieldClassNameAndGenerics(Object element, String fieldName, String className, List<String> genericClasses);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@

package org.drools.scenariosimulation.backend.expression;

import java.util.Optional;

import com.fasterxml.jackson.databind.JsonNode;
import org.drools.scenariosimulation.api.model.FactMappingValue;
import org.drools.scenariosimulation.api.model.ScenarioSimulationModel.Type;
import org.drools.scenariosimulation.backend.util.JsonUtils;

import static org.drools.scenariosimulation.api.utils.ConstantsHolder.MVEL_ESCAPE_SYMBOL;

Expand Down Expand Up @@ -54,15 +58,34 @@ public ExpressionEvaluator getOrCreate(FactMappingValue factMappingValue) {
return getOrCreateDMNExpressionEvaluator();
}

Object rawValue = factMappingValue.getRawValue();
String rawValue = (String) factMappingValue.getRawValue();

if (rawValue instanceof String && ((String) rawValue).trim().startsWith(MVEL_ESCAPE_SYMBOL)) {
if (isAnMVELExpression(rawValue)) {
return getOrCreateMVELExpressionEvaluator();
} else {
return getOrCreateBaseExpressionEvaluator();
}
}

/**
* A rawValue is an MVEL expression if:
* - NOT COLLECTIONS CASE: It's a <code>String</code> which starts with MVEL_ESCAPE_SYMBOL ('#')
* - COLLECTION CASE: It's a JSON String node, which is used only when an expression is set
* (in other cases it's a JSON Object (Map) or a JSON Array (List)) and it's value starts with MVEL_ESCAPE_SYMBOL ('#')
* @param rawValue
* @return
*/
protected boolean isAnMVELExpression(String rawValue) {
/* NOT COLLECTIONS CASE */
if (rawValue.trim().startsWith(MVEL_ESCAPE_SYMBOL)) {
return true;
}
/* COLLECTION CASE */
Optional<JsonNode> optionalNode = JsonUtils.convertFromStringToJSONNode(rawValue);
return optionalNode.filter(
jsonNode -> jsonNode.isTextual() && jsonNode.asText().trim().startsWith(MVEL_ESCAPE_SYMBOL)).isPresent();
}

private ExpressionEvaluator getOrCreateBaseExpressionEvaluator() {
if (baseExpressionEvaluator == null) {
baseExpressionEvaluator = new BaseExpressionEvaluator(classLoader);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,18 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import com.fasterxml.jackson.databind.JsonNode;
import org.drools.core.util.MVELSafeHelper;
import org.drools.scenariosimulation.backend.util.JsonUtils;
import org.kie.soup.project.datamodel.commons.util.MVELEvaluator;
import org.mvel2.MVEL;
import org.mvel2.ParserConfiguration;
import org.mvel2.ParserContext;

import static org.drools.scenariosimulation.api.utils.ConstantsHolder.ACTUAL_VALUE_IDENTIFIER;
import static org.drools.scenariosimulation.api.utils.ConstantsHolder.MALFORMED_MVEL_EXPRESSION;
import static org.drools.scenariosimulation.api.utils.ConstantsHolder.MVEL_ESCAPE_SYMBOL;
import static org.drools.scenariosimulation.backend.expression.BaseExpressionOperator.compareValues;
import static org.drools.scenariosimulation.backend.util.ScenarioBeanUtil.loadClass;
Expand Down Expand Up @@ -84,10 +88,28 @@ protected Object compileAndExecute(String rawExpression, Map<String, Object> par
return evaluator.executeExpression(compiledExpression, params);
}

/**
* The clean works in the following ways:
* - NOT COLLECTIONS CASE: The given rawExpression without MVEL_ESCAPE_SYMBOL ('#');
* - COLLECTION CASE: Retrieving the value from rawExpression, which is a JSON String node in this case, removing
* the MVEL_ESCAPE_SYMBOL ('#');
* In both cases, the given String must start with MVEL_ESCAPE_SYMBOL.
* All other cases are wrong: a <code>IllegalArgumentException</code> is thrown.
* @param rawExpression
* @return
*/
protected String cleanExpression(String rawExpression) {
if (rawExpression == null || !rawExpression.trim().startsWith(MVEL_ESCAPE_SYMBOL)) {
throw new IllegalArgumentException("Malformed MVEL expression '" + rawExpression + "'");
if (rawExpression != null && rawExpression.trim().startsWith(MVEL_ESCAPE_SYMBOL)) {
return rawExpression.replaceFirst(MVEL_ESCAPE_SYMBOL, "");
}
return rawExpression.replaceFirst(MVEL_ESCAPE_SYMBOL, "");
Optional<JsonNode> optionalJSONNode = JsonUtils.convertFromStringToJSONNode(rawExpression);
if (optionalJSONNode.isPresent()) {
JsonNode jsonNode = optionalJSONNode.get();
if (jsonNode.isTextual() && jsonNode.asText() != null && jsonNode.asText().trim().startsWith(MVEL_ESCAPE_SYMBOL)) {
String expression = jsonNode.asText();
return expression.replaceFirst(MVEL_ESCAPE_SYMBOL, "");
}
}
throw new IllegalArgumentException(MALFORMED_MVEL_EXPRESSION + "'" + rawExpression + "'");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2019 Red Hat, Inc. and/or its affiliates.
*
* 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.drools.scenariosimulation.backend.util;

import java.io.IOException;
import java.util.Optional;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
* Class used to provide JSON common utils
*/
public class JsonUtils {

private JsonUtils() {
// Not instantiable
}

/**
* This method aim is to to evaluate if any possible String is a valid json or not.
* Given a json in String format, it try to convert it in a <code>JsonNode</code>. In case of success, i.e.
* the given string is a valid json, it put the <code>JsonNode</code> in a <code>Optional</code>. An empty
* <code>Optional</code> is passed otherwise.
* @param json
* @return
*/
public static Optional<JsonNode> convertFromStringToJSONNode(String json) {
if (json == null || json.isEmpty()) {
return Optional.empty();
}
try {
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(json);
return Optional.of(jsonNode);
} catch (JsonParseException e) {
return Optional.empty();
} catch (IOException e) {
throw new IllegalArgumentException("Generic error during json parsing: " + json, e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.IntNode;
Expand Down Expand Up @@ -242,4 +245,17 @@ public void isEmptyText() {
assertFalse(expressionEvaluatorLocal.isEmptyText(new TextNode(VALUE)));
assertTrue(expressionEvaluatorLocal.isEmptyText(new ObjectNode(factory)));
}

@Test
public void isStructuredInput() {
assertTrue(expressionEvaluatorLocal.isStructuredInput(List.class.getCanonicalName()));
assertTrue(expressionEvaluatorLocal.isStructuredInput(ArrayList.class.getCanonicalName()));
assertTrue(expressionEvaluatorLocal.isStructuredInput(LinkedList.class.getCanonicalName()));
assertTrue(expressionEvaluatorLocal.isStructuredInput(Map.class.getCanonicalName()));
assertTrue(expressionEvaluatorLocal.isStructuredInput(HashMap.class.getCanonicalName()));
assertTrue(expressionEvaluatorLocal.isStructuredInput(LinkedHashMap.class.getCanonicalName()));
assertFalse(expressionEvaluatorLocal.isStructuredInput(Set.class.getCanonicalName()));
assertFalse(expressionEvaluatorLocal.isStructuredInput(Integer.class.getCanonicalName()));
assertFalse(expressionEvaluatorLocal.isStructuredInput(String.class.getCanonicalName()));
}
}
Loading

0 comments on commit 0c0202b

Please sign in to comment.