Skip to content

Conversation

yasserzamani
Copy link
Member

Moves checking OgnlValueStack.THROW_EXCEPTION_ON_FAILURE outside loop because it shouldn't throw exception on first failure while is trying all root objects.

Returns on first successful call because it's not rational and is confusing user to skip when user method successfully returns null as an actual result.

Fixes WW-4999 via honoring (devMode && logMissingProperties) for OgnlValueStack.THROW_EXCEPTION_ON_FAILURE and REPORT_ERRORS_ON_NO_PROP.

Moves checking OgnlValueStack.THROW_EXCEPTION_ON_FAILURE outside loop because it shouldn't throw exception on first failure while is trying all root objects.

Returns on first successful call because it's not rational and is confusing user to skip when user method successfully returns null as an actual result.

Fixes WW-4999 via honoring (devMode && logMissingProperties) for OgnlValueStack.THROW_EXCEPTION_ON_FAILURE and REPORT_ERRORS_ON_NO_PROP.
@coveralls
Copy link

coveralls commented May 30, 2019

Coverage Status

Coverage increased (+0.003%) to 47.029% when pulling a50af87 on yasserzamani:WW-4999 into b0dd7c1 on apache:struts-2-5-x.

Copy link

@qqu0127 qqu0127 left a comment

Choose a reason for hiding this comment

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

Is there a reason we have to set both devMode and logMissingProperties true in order to enable logging? Could we just use logMissingProperties for clarity?

}

if (context.containsKey(OgnlValueStack.THROW_EXCEPTION_ON_FAILURE)) {
throw new MethodFailedException(target, name, reason);
Copy link

Choose a reason for hiding this comment

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

Seems like previously this MethodFailedException is thrown when the method is found but failed on execution. Now we are mixing up both cases "no such method" and "method failed" by throwing this exception outside the loop. Is this the expected behavior?
Also, shall we consider break the loop at the point we threw MethodFailedException?

Copy link
Member Author

Choose a reason for hiding this comment

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

No, previously it was mixed up both cases also so it is same as previous in this behavior. The only difference for it is: Now it is outside of the loop. Being inside of the loop is a bug. I discovered it in #357 - For example OgnlValueStack.findValue(expr: "someProp", throwExceptionOnFailure: true) always fail when top of stack doesn't have someProp property while it must check also other objects in stack and fail if none of them had this property.

Also, shall we consider break the loop at the point we threw MethodFailedException?

I think no. AFAIK it must try on all of objects in stack.

Copy link

Choose a reason for hiding this comment

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

Looks ok to me, it makes sense to keep the mixed up behavior.
Regarding the exception previously at line 248, doesn't that break the loop already? I don't think that would be an issue even throwExceptionOnFailure is set true. Because that is only thrown when the property is found, but an exception is thrown in application code (that's why reason != null).
Do you propose to search down the stack anyway even we've already "hit"?
The only use case I can think of: we have more than one "hit" on stack, while first one throws runtime exception and second one execute smoothly and return -- yes, in this case we don't want to break the loop.
I'm not super familiar with the coding style and design pattern of ognl, is that really what we want?
Hiding exceptions doesn't look quite right to me...

Copy link
Member Author

Choose a reason for hiding this comment

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

Now, it doesn't break the loop but previously it did, and I thought it was an issue when throwExceptionOnFailure is set true because of casual failed tests #357 - for example it accidentally uncovered that XWorkConverter.getConversionErrorMessage seems fails when user set throwExceptionOnFailure to true, I think. right?

I just found all usages of OgnlValueStack.THROW_EXCEPTION_ON_FAILURE and found all of them outside loop except here. Thanks for heads up for reason != null, it makes sense! But now I'm surprised if reason != null is a good decision to distinguish between property not found and user app exception then why #357 tests fail?! (see prev para) In #357 everything is same as before except throwExceptionOnFailure which is hard-coded to true.

@yasserzamani
Copy link
Member Author

Is there a reason we have to set both devMode and logMissingProperties true in order to enable logging? Could we just use logMissingProperties for clarity?

@apache/struts-committers, I agree with @qqu0127 , any objection?

Copy link
Contributor

@JCgH4164838Gh792C124B5 JCgH4164838Gh792C124B5 left a comment

Choose a reason for hiding this comment

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

My "vote" would be too keep devMode and logMissingProperties "linked" so that logMissingProperties is only effective in devMode (reasoning given within the review).

if (!context.containsKey(OgnlValueStack.THROW_EXCEPTION_ON_FAILURE) && (mc != null) && (reason != null) && (reason.getClass() == NoSuchMethodException.class)) {
if ((mc != null) && (reason != null) && (reason.getClass() == NoSuchMethodException.class)) {
invalidMethods.put(mc, Boolean.TRUE);
} else if (reason != null) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it certain that dropping the "if (reason != null)" check as part of the decision whether to throw the exception or not won't cause an issue ?

It looks like the old code would "skip" throwing an Exception in that case, no matter what THROW_EXCEPTION_ON_FAILURE is set to ?

Copy link
Member Author

Choose a reason for hiding this comment

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

As @qqu0127 mentioned, it seems reason != null tries to distinguish between property not found and user app exception but actually it doesn't work. As far as I could see and debug, OGNL always fill it here

https://github.com/jkuhnert/ognl/blob/b6e58c4db776beb20e20d5792227d1da7b4355e0/src/main/java/ognl/OgnlRuntime.java#L1529

in both NoSuchMethodException (i.e. property not found) and InvocationTargetException (i.e. user app exception).

I realized that this is an old issue, please see this test :

* Fails on 2.5.20 and earlier - tested on 2.5 (5/5/2016) and failed

Copy link

@qqu0127 qqu0127 Jun 1, 2019

Choose a reason for hiding this comment

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

Oh, you are right. reason != null itself is not a complete check. Actually, I fixed this in my workplace integrated with our logging system internally, based on 2.5.17. And it classifies the exceptions correctly. FYI, we modified ValueStack. (we also changed CompoundRootAccessor, but differently, so not sure if it works here). Just for reference, not super important.

  @Override
  public Object handleOgnlException(String expr, boolean throwExceptionOnFailure, OgnlException e) {
    Object ret = findInContext(expr);
    if (ret != null) {
      return ret;
    }
    String errMsg = null;
    if (shouldLogWarning()) {
      if (e.getCause() == null || e.getCause() instanceof NoSuchMethodException) {
        errMsg = String.format("Could not evaluate expression [%s]", expr);
      }
      else {
        // exception in invoked method
        errMsg = String.format("Caught an exception in evaluating expression [%s]", expr);
      }

      logStackTrace(errMsg, e);
    }
    if (throwExceptionOnFailure) {
      throw new XWorkException(e, errMsg);
    }
    return null;
  }

And

@Override
  protected Object handleOtherException(String expr, boolean throwExceptionOnFailure, Exception e) {
    if (shouldLogWarning()) {
      if (e.getCause() == null) {
        logStackTrace(String.format("Could not evaluate expression [%s]", expr), e);
      }
      else {
        logStackTrace(String.format("Caught an exception in evaluating expression [%s]", expr), e);
      }
    }
    if (throwExceptionOnFailure) {
      throw new XWorkException(e);
    }
    return findInContext(expr);
  }

Copy link

Choose a reason for hiding this comment

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

I'm still not sure if we should remove elseif (reason != null). In my view it changes the flow too much (validity and performance), and it's irrelevant to the issue.

Copy link
Member Author

Choose a reason for hiding this comment

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

We should because with that following test fails. I tested and even Struts 2.5 (5/5/2016) with no change also fails on this test:

public void testNotThrowExceptionOnTopMissingProperty() {

but this is and was a wrong behavior.

if ((mc != null) && (reason != null) && (reason.getClass() == NoSuchMethodException.class)) {
invalidMethods.put(mc, Boolean.TRUE);
} else if (reason != null) {
throw new MethodFailedException(o, name, e.getReason());
Copy link
Contributor

Choose a reason for hiding this comment

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

Was the old logic intentionally limiting the loop to 1 iteration (unless the exception reason was null ) ?
Previously it would seem things would only succeed if the method was defined on the object at the top of the stack.

Will "walking the whole valuestack" potentially introduce a different issue (unexpectedly) ?

Copy link
Member Author

Choose a reason for hiding this comment

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

Was the old logic intentionally limiting the loop to 1 iteration (unless the exception reason was null ) ?
Previously it would seem things would only succeed if the method was defined on the object at the top of the stack.

In case of throwExceptionOnFailure=true, Yes, but it is a wrong behavior. Please see my test at

* Fails on 2.5.20 and earlier - tested on 2.5 (5/5/2016) and failed
which fails on Struts 2.5 (5/5/2016) on L#975.

Will "walking the whole valuestack" potentially introduce a different issue (unexpectedly) ?

No, it is already current behavior in case of throwExceptionOnFailure=false.


protected void setupExceptionOnFailure(boolean throwExceptionOnFailure) {
if (throwExceptionOnFailure) {
if (throwExceptionOnFailure || (devMode && logMissingProperties)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is the main reason why devMode and logMissingProperties should continue to be "linked" (at least in 2.5.x). Most users probably only want to check for missing properties in devMode (development) anyway.

If logMissingProperties becomes a "standalone flag", it could turn on THROW_EXCEPTION_ON_FAILURE in a production configuration - and that could have greater repercussions than intended. Maybe it could be decoupled in 2.6 in a "safe way" ?

Copy link
Member Author

Choose a reason for hiding this comment

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

throwExceptionOnFailure || logMissingProperties also works even better. As far as I can see in codes, Struts will check them separately later to check throw any exception and log any message.

return null;
}

Throwable reason = null;
Copy link
Contributor

Choose a reason for hiding this comment

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

(Oops ... forgot to include this comment in the review).

Assuming the loop logic will continue to process more than 1 iteration, I suggest a small optimization:

  • Move Line 227: Class[] argTypes = getArgTypes(objects); -- to Line 219/220 (outside of the loop).
    Should be safe as the argument types don't change across the loop iterations.

Copy link
Member Author

Choose a reason for hiding this comment

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

👍 fixed

@lukaszlenart
Copy link
Member

Would you mind adding a test case where an existing property throws a runtime exception? Such case should not be reported as missing property. Basically something has changed in struts <-> ognl flow and ognl.NoSuchPropertyException isn't thrown/handled any more. I was trying to figure out what was wrong but failed :(

Also reaks loop on user method exceptions - but continue to next objects in stack on NoSuchMethodException.
@yasserzamani
Copy link
Member Author

Hi all,

Thanks all for comments! I tried to fix all of them in a new commit above. The only weakness is when user method body itself throws NoSuchMethodException which cannot be fixed nicely without changing ognl itself.

@lukaszlenart , I realized these issues are old. I checked out Struts 2.5 (5/5/2016) and tested it against two issues. 1. devMode=true&&logMissingProperties=true doesn't work their either. 2. throwExceptionOnFailure=true causes failure on XWorkConverter.getConversionErrorMessage.

@qqu0127 , I think in this new commit I was able to address your concerns. Could you check please? and thanks a tone for heads up!

@JCgH4164838Gh792C124B5 , I'm working on your concerns :)

instead continue to next objects in stack

Also tests not skip returned null values by user method
@yasserzamani
Copy link
Member Author

I tested and finally found it more clear and nice in both test and production codes to decouple logMissingProperties from devMode so did it.

}
}

public void testLogMissingProperties() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Since testNotThrowExceptionOnTopMissingProperty() added @since 2.5.21, do you want the same annotation here ?

Copy link
Member Author

Choose a reason for hiding this comment

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

👍 fixed

}
}

public void testNotLogUserExceptionsAsMissingProperties() {
Copy link
Contributor

Choose a reason for hiding this comment

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

And @since 2.5.21 here ?

Copy link
Member Author

Choose a reason for hiding this comment

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

👍 fixed

logger.removeAppender(testAppender);
}
}

Copy link
Contributor

Choose a reason for hiding this comment

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

@yasserzamani , do you think adding equivalent tests testLogMissingPropertiesFalse() and testNotLogUserExceptionsAsMissingPropertiesFalse() would be a good idea ?

They could set vs.setLogMissingProperties("false"); and then confirm that nothing was logged. Might be a good additional check ?

As it stands the changes LGTM, but I think those two additional tests might be useful to catch a future change that accidentally causes the opposite behaviour (i.e. something gets logged when logMissingProperties is false, but should not).

Copy link
Contributor

Choose a reason for hiding this comment

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

Hello @yasserzamani . Thanks for responding to the feedback and your excellent work on this PR. 👍 .
Thanks to @qqu0127 for opening the Jira issue and reviewing the PR's changes too.

Copy link
Member Author

Choose a reason for hiding this comment

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

👍 fixed

Copy link

@qqu0127 qqu0127 left a comment

Choose a reason for hiding this comment

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

Great work. Thanks!

Copy link
Contributor

@JCgH4164838Gh792C124B5 JCgH4164838Gh792C124B5 left a comment

Choose a reason for hiding this comment

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

Good work. 👍

@lukaszlenart
Copy link
Member

LGTM 👍

@lukaszlenart lukaszlenart merged commit 651eac2 into apache:struts-2-5-x Jun 3, 2019
@lukaszlenart
Copy link
Member

@yasserzamani do you plan cherry-pick those changes into 2.6?

@yasserzamani yasserzamani deleted the WW-4999 branch June 3, 2019 07:29
@yasserzamani
Copy link
Member Author

Thanks for thanks and 👍s :)

@lukaszlenart , cherry picked into 2.6 and travis is green :)

@lukaszlenart
Copy link
Member

💯

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants