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

Conversation

yesamer
Copy link
Contributor

@yesamer yesamer commented Dec 16, 2019

@dupliaka @jomarko @danielezonca can you please review it and test it?

Collection editor currently use a JSON to send collections (Maps, Lists) data to the backend.
Lists are wrapped on JSONArray and Maps on JSONObject
The idea I had to manage used defined expressions are to wrap them to a simple JSONTextual node (i.e. a JSON which contains only a string). This is useful to easily separate the scope of data assigning it to a very specific data structure.
Then, the changes here consist basically to handle this new case ( JSONTextual ) to the already present cases of JSONArray and JSONObject, for all three Evaluators (MVEL, Base and FEEL).
JSONTextual format is: "expression", it wraps a String between quote " char.

https://issues.redhat.com/browse/DROOLS-4698

Related PR: kiegroup/drools-wb#1276

@yesamer yesamer changed the title [DROOLS-4698] Managing used defined expressions for Collections [DROOLS-4698] Managing usedrdefined expressions for Collections Dec 18, 2019
@yesamer yesamer changed the title [DROOLS-4698] Managing usedrdefined expressions for Collections [DROOLS-4698] Managing user defined expressions for Collections Dec 18, 2019
@yesamer yesamer marked this pull request as ready for review December 20, 2019 08:39
@yesamer yesamer requested a review from dupliaka January 2, 2020 11:48
@jomarko
Copy link
Contributor

jomarko commented Jan 3, 2020

I think review should be postponed until https://github.com/kiegroup/drools/pull/2690 is in, WDYT @yesamer ?

@yesamer
Copy link
Contributor Author

yesamer commented Jan 3, 2020

I think review should be postponed until #2690 is in, WDYT @yesamer ?

@jomarko Yes, fine for me.

@yesamer yesamer requested review from danielezonca, gitgabrio and jomarko and removed request for dupliaka January 7, 2020 16:33
@jomarko
Copy link
Contributor

jomarko commented Jan 8, 2020

jenkins please retest this

@jomarko
Copy link
Contributor

jomarko commented Jan 8, 2020

jenkins execute compile downstream build

Copy link
Contributor

@gitgabrio gitgabrio left a comment

Choose a reason for hiding this comment

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

@yesamer
Just some minor changes, pls

@@ -61,7 +62,7 @@ protected boolean isStructuredResult(Class<?> resultClass) {

/**
* Check if className represents a structured input
* @param className
* @param className Used to determine if a structured input is passed
* @return
*/
protected boolean isStructuredInput(String className) {
Copy link
Contributor

Choose a reason for hiding this comment

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

@yesamer
This method is functionally identical to the previous one, even if written differently: please remove one of them

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 done! Thanks!

@@ -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.

JsonNode jsonNode = objectMapper.readTree(json);
return Optional.of(jsonNode);
} catch (IOException 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.

* @param json
* @return
*/
public static boolean isAJSONTextualNode(String json) {
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'm not sure where it is used, but I'm almost sure you would need to re-convert the string again and retrieve the .isTextual object: would not be better to directly return an Optional"" here, as in convertFromStringToJSONNode?

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 To be honest it's not used, I removed it.

Copy link
Contributor

Choose a reason for hiding this comment

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

@yesamer
great!

@@ -59,7 +62,7 @@ public boolean evaluateUnaryExpression(String rawExpression, Object resultValue,

@Override
public Object evaluateLiteralExpression(String rawExpression, String className, List<String> genericClasses) {
Object expressionResult = compileAndExecute(rawExpression, Collections.emptyMap());
Object expressionResult = compileAndExecute(rawExpression, new HashMap<>());
Copy link
Contributor

Choose a reason for hiding this comment

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

This changed has been moved to this PR so we can remove it from here (with the tests of course)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@danielezonca I just merged master which now contains that PR.

@@ -162,9 +163,9 @@ public void mapOfComplexTypeTest() {
element.setPhones(phones);
toCheck.put("first", element);

assertTrue(expressionEvaluator.verifyResult(mapJsonString, toCheck));
assertTrue(expressionEvaluator.verifyResult(mapJsonString, toCheck, null));
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you please add a test where the last parameter of verifyResult is used and it has to be different from null?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@danielezonca added :)

Copy link
Contributor

@jomarko jomarko left a comment

Choose a reason for hiding this comment

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

Code review

See inline comments

* @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.


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

Comment on lines +214 to 226
@Test(expected = IllegalArgumentException.class)
public void expressionListVerifyResultTest() {
String expressionCollectionJsonString = new TextNode("10").toString();
List<BigDecimal> contextValue = Collections.singletonList(BigDecimal.valueOf(10));
assertTrue(expressionEvaluator.verifyResult(expressionCollectionJsonString, contextValue, List.class));
}

@Test(expected = IllegalArgumentException.class)
public void expressionMapVerifyResultTest() {
String expressionCollectionJsonString = new TextNode("{key_a : 1}").toString();
Map<String, BigDecimal> contextValue = Collections.singletonMap("key_a", BigDecimal.valueOf(1));
assertTrue(expressionEvaluator.verifyResult(expressionCollectionJsonString, contextValue, Map.class));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you please help me understand this, why IllegalArgumentException is expected in both cases? Is it because expressionCollectionJsonString needs to start with '[' and ends with ']' ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jomarko Yes, the expression collection feature doesn't work with BaseExpressionEvaluator, but only with MVELExpressionEvaluator and DMNExpressionEvaluator. Consider that the frontend, with the current status, can never sends an expression defined collection to be evaluated with BaseExpressionEvaluator. If happens, it's not expected.

"test.add(\"Michael\");\n" +
"test;"),
ArrayList.class.getCanonicalName(),
new ArrayList<>()
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we please use Collections.emptyList()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jomarko Done!


assertEquals(expectedMap, evaluator.evaluateLiteralExpression(mvelExpression("[\"Jim\" : \"Person\"]"),
HashMap.class.getCanonicalName(),
new ArrayList<>()
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we please use Collections.emptyList()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jomarko Done!

"test.put(\"Jim\", a);\n" +
"test;"),
HashMap.class.getCanonicalName(),
new ArrayList<>()
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we please use Collections.emptyList()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jomarko Done!

@yesamer yesamer requested a review from jomarko January 21, 2020 08:29
Copy link
Contributor

@jomarko jomarko left a comment

Choose a reason for hiding this comment

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

Approving this part, but before merge I would like to:

Thank you @yesamer

@sonarcloud
Copy link

sonarcloud bot commented Jan 27, 2020

Kudos, SonarCloud Quality Gate passed!

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities (and Security Hotspot 0 Security Hotspots to review)
Code Smell A 0 Code Smells

85.5% 85.5% Coverage
0.0% 0.0% Duplication

@danielezonca danielezonca merged commit 0c0202b into apache:master Jan 27, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants