Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DROOLS-4698] Managing user defined expressions for Collections #2691

Merged
merged 40 commits into from
Jan 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
89ca53f
DROOLS-4698: Managing expressions for Collections
yesamer Dec 11, 2019
0d20a87
DROOLS-4698: Managing expressions for Collections
yesamer Dec 12, 2019
0b32695
DROOLS-4698: Managing expressions for Collections + Tests
yesamer Dec 13, 2019
897685a
DROOLS-4698: Managing expressions for Collections + Tests
yesamer Dec 16, 2019
16e1f4b
Merge remote-tracking branch 'origin/master' into DROOLS-4698
yesamer Dec 16, 2019
f2fca62
DROOLS-4698: Managing expressions for Collections + Tests
yesamer Dec 16, 2019
1200e94
DROOLS-4698: Managing expressions for Collections + Tests
yesamer Dec 16, 2019
325bf32
DROOLS-4698: Managing expressions for Collections + Tests
yesamer Dec 16, 2019
852faf2
DROOLS-4698: Managing expressions for Collections + Tests
yesamer Dec 16, 2019
7af6053
Merge remote-tracking branch 'origin/master' into DROOLS-4698
yesamer Dec 16, 2019
2720596
DROOLS-4698: Managing expressions for Collections + Tests
yesamer Dec 17, 2019
7adebb2
Merge remote-tracking branch 'origin/master' into DROOLS-4698
yesamer Dec 18, 2019
8ffcecd
Merge remote-tracking branch 'origin/master' into DROOLS-4698
yesamer Dec 19, 2019
34a078e
Merge remote-tracking branch 'origin/master' into DROOLS-4698
yesamer Dec 20, 2019
668e74f
Merge remote-tracking branch 'origin/master' into DROOLS-4698
yesamer Jan 2, 2020
88bd104
Merging from origin/master
yesamer Jan 7, 2020
bbd4453
Merging from origin/master
yesamer Jan 7, 2020
df4930e
DROOLS-4698: Changes required during CR
yesamer Jan 8, 2020
799fa6c
DROOLS-4698: Changes required during Code Review
yesamer Jan 8, 2020
424cfe8
DROOLS-4698: Changes required during Code Review
yesamer Jan 9, 2020
0f24b1d
Merge remote-tracking branch 'origin/master' into DROOLS-4698
yesamer Jan 9, 2020
de0e0bd
Merge remote-tracking branch 'origin/master' into DROOLS-4698
yesamer Jan 13, 2020
91dc107
DROOLS-4698: Changes required during Code Review
yesamer Jan 14, 2020
3e581ef
Merge remote-tracking branch 'origin/master' into DROOLS-4698
yesamer Jan 14, 2020
8d94b6b
Merge remote-tracking branch 'origin/master' into DROOLS-4698
yesamer Jan 14, 2020
0facfa2
Merge remote-tracking branch 'origin/master' into DROOLS-4698
yesamer Jan 15, 2020
a326460
DROOLS-4698: Changes required during Code Review
yesamer Jan 15, 2020
a3fa40f
DROOLS-4698: Changes required during Code Review
yesamer Jan 15, 2020
c901f36
Merge remote-tracking branch 'origin/master' into DROOLS-4698
yesamer Jan 15, 2020
cf71bf5
Merge remote-tracking branch 'origin/master' into DROOLS-4698
yesamer Jan 15, 2020
44cf04f
DROOLS-4698: Changes required during Code Review
yesamer Jan 15, 2020
3fe828e
DROOLS-4698: Changes required during Code Review
yesamer Jan 16, 2020
6859b51
Merge remote-tracking branch 'origin/master' into DROOLS-4698
yesamer Jan 16, 2020
8701835
DROOLS-4698: Changes required during Code Review
yesamer Jan 17, 2020
026c7b3
Merge remote-tracking branch 'origin/master' into DROOLS-4698
yesamer Jan 17, 2020
7c4e797
DROOLS-4698: Additional tests.
yesamer Jan 17, 2020
0b58b46
Merge remote-tracking branch 'origin/master' into DROOLS-4698
yesamer Jan 20, 2020
730a2b4
DROOLS-4698: Increase coverage (#2)
Jan 24, 2020
59369bd
Merge remote-tracking branch 'origin/master' into DROOLS-4698
yesamer Jan 24, 2020
f499aed
Merge remote-tracking branch 'origin/master' into DROOLS-4698
yesamer Jan 27, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yesamer
Not related to this PR, but we have to get rid of this Object rawValue (is cast to String everywhere)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gitgabrio @danielezonca already planned a future refactor to set rawValue to String everywhere.


if (rawValue instanceof String && ((String) rawValue).trim().startsWith(MVEL_ESCAPE_SYMBOL)) {
if (isAnMVELExpression(rawValue)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please notice we changed the logic, we are now not checking instanceof in the isAnMVELExpression. We automatically cast to String what theoretically can cause cast exception.

Copy link
Contributor Author

@yesamer yesamer Jan 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jomarko You're right BUT: rawValue is always a String, even if it's declared as Object in FactMappingValue DTO. If is not a String, it's an error, then it's correct to receive an exception. Consider that we planned with @danielezonca a refactor where we'll change that Object type in String --> https://issues.redhat.com/browse/DROOLS-4948

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is standard public static final helper method, so just wondering if the placement is the best choice we have.

Copy link
Contributor Author

@yesamer yesamer Jan 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jomarko It could be moved to a Util class. On the other side, this method is used only in this class and I don't see another place where it could be used. In my opinon, an Util class should be created if it cans hold static methods which can be shared in multiple places. This is not the case. I vote to don't change it, if you have an alternative pls share.

/* 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, "");
yesamer marked this conversation as resolved.
Show resolved Hide resolved
}
}
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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yesamer
Not sure about swallowing this exception that way: if we get here rawValue is expected to be a valid json, but if it throws exception, something bad has happened, IMO.
"Given a json in String format, it try to convert it in a JsonNode. In case of success, i.e.
the given string is a valid json"...: what would mean a "json in String format" but invalid json?

Copy link
Contributor Author

@yesamer yesamer Jan 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gitgabrio This is a good point, I had the thought, but I didn't find a different way to validate a json.
According to Jackson docs:

  • @throws JsonParseException if underlying input contains invalid content
    • of type {@link JsonParser} supports (JSON for default case)
      */
      public JsonNode readTree(String content) throws IOException {
      return _readTreeAndClose(_jsonFactory.createParser(content));
      }

Then, I can improve it managing JsonParseException and IOException in different ways: in the first case, it's a malformed content i.e. not a JSON, in the second case It could be any other IOException, I can re-throw it with an other exception (which one?)
Consider the JsonParseException is a sub class of IOException
WDYT?

Copy link
Contributor

@gitgabrio gitgabrio Jan 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yesamer
If I've understood correctly, ObjectMapper.readTree(String content) may throw one or the other (it declares only the latter, being the ancestor).
So, I agree both of them should be catch and managed in different way.
In either case, a specific exception should be re-thrown: I would create twos, to help calling code differentiate the actual issue.

Copy link
Contributor Author

@yesamer yesamer Jan 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gitgabrio no, a JsonParseException is thrown if the input is not a valid json. In this case, I return an empty Optional which means: the given string can't be parsed as json and then it's not a json. In all other cases, when a generic IOException is thrown, then I re-throw an IllegalArgumentException.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yesamer
I thought that method would be invoked only to parse an (expected) json string. If this is true, then IMO JsonParseException should not be hided behind an Optional.empty(), because it means something unexpected is happening. If - instead - this method is invoked on any possible String to verify if it is a valid json, then the javadoc should somehow clarify that

Copy link
Contributor Author

@yesamer yesamer Jan 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gitgabrio

If - instead - this method is invoked on any possible String to verify if it is a valid json

This is the correct case, I updated the javadoc to clarify it.

} 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()));
}
}