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

fix: changing Shortcuts.kts logic to use HashableKeys. #1616

Merged
merged 7 commits into from
Mar 16, 2023

Conversation

czp13
Copy link
Contributor

@czp13 czp13 commented Mar 15, 2023

Description

The testbench tests failed in some cases, as some part of the filter text generation logic was changed, and now considering the browser differences (Firefox and chrome having different implementations for Alt/Option event). To tackle these browser differences I made a fix in the flow server here:

The new logic is considering now if the key is Alt or not, using contains(). But because of this change, some tests were running failed in testbench project.

In this PR:

  • I am wrapping Key-s to HashableKey as we do in real-life scenarios always (check ShortCutRegistration 485 linef or more information).

So now with this fix, when we call the private methods it will be equal to how we do in a real-life apps.

More info:

Type of change

  • Bugfix
  • Feature

Checklist

  • I have read the contribution guide: https://vaadin.com/docs/latest/guide/contributing/overview/
  • I have added a description following the guideline.
  • The issue is created in the corresponding repository and I have referenced it.
  • I have added tests to ensure my change is effective and works as intended.
  • New and existing tests are passing locally with my change.
  • I have performed self-review and corrected misspellings.

Additional for Feature type of change

  • Enhancement / new feature was discussed in a corresponding GitHub issue and Acceptance Criteria were created.

@CLAassistant
Copy link

CLAassistant commented Mar 15, 2023

CLA assistant check
All committers have signed the CLA.

@czp13 czp13 requested review from tepi, Artur- and ZheSun88 March 15, 2023 12:20
@czp13 czp13 added the bug label Mar 15, 2023
@czp13 czp13 changed the title As the ShortcutRegistration will always use HashableKeys, hencewhy... Fixing Shortcuts.kts to use HashableKeys for invoking the generateEventModifierFilter private method (as we do in other ways if we add keys to Shortcuts) Mar 15, 2023
@czp13 czp13 changed the title Fixing Shortcuts.kts to use HashableKeys for invoking the generateEventModifierFilter private method (as we do in other ways if we add keys to Shortcuts) Fixing Shortcuts.kts to use HashableKeys. Mar 15, 2023
@czp13 czp13 changed the title Fixing Shortcuts.kts to use HashableKeys. fix: Changing Shortcuts.kts logic to use HashableKeys. Mar 15, 2023
@czp13 czp13 changed the title fix: Changing Shortcuts.kts logic to use HashableKeys. fix: changing Shortcuts.kts logic to use HashableKeys. Mar 15, 2023
@Suppress("UNCHECKED_CAST")
val data = MockFilterJsonObject(key, modifiers.toSet() as Set<KeyModifier>)
val data = MockFilterJsonObject(key, hashableKeys.toSet() as Set<KeyModifier>)
Copy link
Member

Choose a reason for hiding this comment

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

Isn't this emulating data coming from the browser? Why would that data have hashableKeys?

Copy link
Contributor Author

@czp13 czp13 Mar 15, 2023

Choose a reason for hiding this comment

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

Thanks.

Super TLDR:

  • we still generate the filterText on the server side, which will be passed down to the client, and will be part of the event on frontend/browser when key pushed. If filter text generation is not the same/proper, then the event will be not listened (Shortcutlistener)

Longer:
It is not exactly doing this in my understanding, it is trying to generate or emaluate a filterText using flow-server private methods.

MockFilterJsonObject will go to ->

      // compute the filter
        filter = mgenerateEventKeyFilter.invoke(null, key).toString() + " && " +
                mgenerateEventModifierFilter.invoke(null, modifiers).toString()

which will call:
mgenerateEventModifierFilter = ShortcutRegistration::class.java.getDeclaredMethod("generateEventModifierFilter", Collection::class.java)

So eventually all comes down to generateEventModifierFilter private method, which was never intended to call in the server with just key-s, those keys/keymodifiers will be always wrapped into HashableKey-s.

This is my understanding of this problem very high level:
we generate the filterTexts on the serverSide -> pass it to client -> client send back the filters when it is having a listener to it (flow client code) and matching it (matchesFilter() if I am correct), and then on the serverSide we handle the event... if the filterText generation is not the same, because the inputs are not the same then it will not work we will not match the filter...

The problem is the duplicated logics here and calling private methods, filterText generation in not considering HashableKeys.

I tested this locally :), and the failed test cases were generating events on my server side for example (shift, alt, w).

So for example this is working in a sample app with newest Flow, although the tests are failing for the same case:

        Shortcuts.addShortcutListener(textField, () -> {
            System.out.println("I am clicked from addShortCutListener textField alt + shift + w, MAC/Chrome/Firefox");
        }, Key.KEY_W, KeyModifier.ALT, KeyModifier.SHIFT);

Copy link
Contributor

Choose a reason for hiding this comment

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

I just took a quick look, but if the failure is because of the following check in Flow code

final boolean altAdded = modifiers
        .contains(new HashableKey(KeyModifier.ALT));

If possible, I would change this one, instead of applying changes in testbench, as the generateEventModifierFilter method expects a collection of generic Key.
Can the above statement be rewritten in terms of Key.isModifier() and Key.matches() instead of using an equality check?

Copy link
Contributor Author

@czp13 czp13 Mar 15, 2023

Choose a reason for hiding this comment

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

Yes, Marco, I shared on Slack that the mentioned line changed, so my Monday/yd changes caused the difference between the testbench and flow, how they call this private method, which has triggered the test failures. I can definitely rewrite the flow part to help address the issue.

However, I would emphasize that generateEventModifierFilter() is always called with HashableKeys right now in my understandings:
(and some other code parts making sure it)

    private void addKey(Key key) {
        assert key != null;

        HashableKey hashableKey = new HashableKey(key);

        if (Key.isModifier(key)) {
            if (!modifiers.contains(hashableKey)) {
                modifiers.add(hashableKey);
                prepareForClientResponse();
            }
        } else {
            if (primaryKey == null || !primaryKey.equals(hashableKey)) {
                primaryKey = hashableKey;
                prepareForClientResponse();
            }
        }
    }

Since generateEventModifierFilter() is a private method that is called through other methods and in a state when HashableKeys have already been added to the ShortcutRegistration object, it would make sense to call it properly with HashableKeys, just like in any other use case, it is not an open API it shall not be called with Key objects, and potentially method signature could have been changed as well to be less flexible and refer to this.

So another solution would be to make the flow side (the private method) more rigid (so use HashableKeys in the method contract, and parameters), to catch errors even before runtime, as this is a specific, exact use case for this private method.

I feel like we want to force the Liskov principle to private methods :D.

But as the consensus here as I feel is to make the methods more flexible, with less constraint, I can do that, let me make the PR for it.

Worth mentioning that by doing this we can have more unexpected behavior in runtime as we are more flexible.
But I am doing this just to share my opinion and ideas about the optimal solution.

I need a bit of time again to build the flow artifact, include it in a customer project, and manually as well trying out many combinations to make sure it is working properly (I always do manual tests too).

Copy link
Contributor

Choose a reason for hiding this comment

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

I get your point.
If ShortcutRegistration only uses HashableKeys, it may be worth to change the method (and the Map) to take this type instead of the interface.
The, for this PR I would maybe move the HashableKey handling inside MockFilterJsonObject, since it is needed only there.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought I replied to this, sorry.

Sure we can do this, I already did a commit containing the changes to do the HashAbleKey transformation within the MockFilterJsonObject! 🙇

@czp13 czp13 force-pushed the fix-shortcuts-to-use-hashkeys-in-reflection branch 3 times, most recently from a5edab3 to c7c80db Compare March 15, 2023 17:27
Comment on lines 87 to 94
public fun getHashableKey(keyModifier: Key): Any? {
val nestedClass = Class.forName(
"com.vaadin.flow.component.ShortcutRegistration\$HashableKey"
)
val ctor = nestedClass.declaredConstructors[0]
ctor.isAccessible = true
return ctor.newInstance(keyModifier)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this method need to be public? Or can we move it into the MockFilterJsonObject class or in the companion object and make it private or internal?
It seems to me it is not used elsewhere

Copy link
Contributor Author

@czp13 czp13 Mar 15, 2023

Choose a reason for hiding this comment

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

Yes, we can do this as well, makes sense as we only use it from that class.
Also I got these errors:
java.lang.ClassCastException: class com.vaadin.flow.component.Key$1 cannot be cast to class com.vaadin.flow.component.KeyModifier (com.vaadin.flow.component.Key$1 and com.vaadin.flow.component.KeyModifier are in unnamed module of loader 'app')

Hencewhy, I changed some casting and the class variable types. Anytime I tried to get back the Key from the Keymodifier set, I got this....

Some kind of anonymous class/Kotlin magic is happening here that Key$1 is generated, but I would spare my nerves and would not go further down the road and I just fix this with the proper typing/class names in the methods... (I just wanted initially to have this strict as well, but we do enough magic here (reflection for private method, inner class constructor, emulating filterText, etc)...

I will commit your suggestion and just leaving as it is this change:

- private class MockFilterJsonObject(val key: Key, val modifiers: Set<KeyModifier>) : JreJsonObject(JreJsonFactory()) {
+ private class MockFilterJsonObject(val key: Key, val modifiers: Set<Key>) : JreJsonObject(JreJsonFactory()) {

This part was needed because of the class cast exception.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The method replacement is Done ✅.
Please approve if you feel, have time and do not have any further requests, comments. :)

@@ -560,8 +560,7 @@ private String getValueProviderString(int row, Grid.Column targetColumn)
ColumnPathRenderer renderer = (ColumnPathRenderer) targetColumn
.getRenderer();

Field f = ColumnPathRenderer.class
.getDeclaredField("provider");
Field f = ColumnPathRenderer.class.getDeclaredField("provider");
Copy link
Contributor

Choose a reason for hiding this comment

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

I would revert this formatting change, so that the pr only focus on the shortcut issue

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just pushed mvn formatter:format. I can revert it.

@@ -85,9 +81,13 @@ private class MockFilterJsonObject(val key: Key, val modifiers: Set<Key>) : JreJ
companion object {
private val mgenerateEventKeyFilter = ShortcutRegistration::class.java.getDeclaredMethod("generateEventKeyFilter", Key::class.java)
private val mgenerateEventModifierFilter = ShortcutRegistration::class.java.getDeclaredMethod("generateEventModifierFilter", Collection::class.java)
private val chashableKey = Class.forName(
"com.vaadin.flow.component.ShortcutRegistration\$HashableKey"
).declaredConstructors[0]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

very last thing is we could have this:

 ShortcutRegistration::class.java
    .classLoader
    .loadClass("com.vaadin.flow.component.ShortcutRegistration\$HashableKey")
    .declaredConstructors[0]

Not sure if it is better or not, maybe a bit more Kotlin syntax.

@czp13 czp13 force-pushed the fix-shortcuts-to-use-hashkeys-in-reflection branch 4 times, most recently from 3c01309 to b5e564d Compare March 16, 2023 11:03
… need to use it in our tests as well to do not have any differences

within our tests and real life application. (now we used simple Key objects, but we need to use HashableKeys)
… MockFilterJsonObject class.

(Based on @mcollovati  review, thank you)
… constructor), and being more strict on that we are using ShortcutRegistration class. (discussed with @mcollovati that this could be a better way)
@czp13 czp13 force-pushed the fix-shortcuts-to-use-hashkeys-in-reflection branch from 7665c8a to 3fbfd8b Compare March 16, 2023 13:59
@czp13 czp13 enabled auto-merge (squash) March 16, 2023 14:03
@czp13 czp13 disabled auto-merge March 16, 2023 14:03
@czp13 czp13 enabled auto-merge (squash) March 16, 2023 14:04
@czp13 czp13 merged commit baf3886 into main Mar 16, 2023
@czp13 czp13 deleted the fix-shortcuts-to-use-hashkeys-in-reflection branch March 16, 2023 14:14
mshabarov pushed a commit that referenced this pull request Mar 17, 2023
* As the ShortcutRegistration will always use HashableKeys, hence why we need to use it in our tests as well to not have any differences
between our tests and real-life application. (now we used simple Key objects, but we need to use HashableKeys)

* Cleaned up the code a bit to use the HashableKey creation only in the MockFilterJsonObject class.
(Based on @mcollovati  review, thank you)

* Put the getHashableKey() method into the class (mock JSON), so where we use it (suggestion by @mcollovati, thanks!).

* One more time, even nicer this way, like this one :) (Thanks again @mcollovati)

* Revert GridTester changes, and fix java doc for the newly added getHashableKey() method, fix imports.

* Using a bit more Kotlinish syntax for reflection (private inner class constructor), and being more strict on that we are using ShortcutRegistration class. (discussed with @mcollovati that this could be a better way)

* Enabling Shortcut related tests (were disabled by another dependency FIX pr to merge it to master)

(cherry picked from commit baf3886)
mshabarov added a commit that referenced this pull request Mar 17, 2023
* As the ShortcutRegistration will always use HashableKeys, hence why we need to use it in our tests as well to not have any differences
between our tests and real-life application. (now we used simple Key objects, but we need to use HashableKeys)

* Cleaned up the code a bit to use the HashableKey creation only in the MockFilterJsonObject class.
(Based on @mcollovati  review, thank you)

* Put the getHashableKey() method into the class (mock JSON), so where we use it (suggestion by @mcollovati, thanks!).

* One more time, even nicer this way, like this one :) (Thanks again @mcollovati)

* Revert GridTester changes, and fix java doc for the newly added getHashableKey() method, fix imports.

* Using a bit more Kotlinish syntax for reflection (private inner class constructor), and being more strict on that we are using ShortcutRegistration class. (discussed with @mcollovati that this could be a better way)

* Enabling Shortcut related tests (were disabled by another dependency FIX pr to merge it to master)

(cherry picked from commit baf3886)

Co-authored-by: czp13 <61667986+czp13@users.noreply.github.com>
mshabarov added a commit that referenced this pull request Mar 17, 2023
* As the ShortcutRegistration will always use HashableKeys, hence why we need to use it in our tests as well to not have any differences
between our tests and real-life application. (now we used simple Key objects, but we need to use HashableKeys)

* Cleaned up the code a bit to use the HashableKey creation only in the MockFilterJsonObject class.
(Based on @mcollovati  review, thank you)

* Put the getHashableKey() method into the class (mock JSON), so where we use it (suggestion by @mcollovati, thanks!).

* One more time, even nicer this way, like this one :) (Thanks again @mcollovati)

* Revert GridTester changes, and fix java doc for the newly added getHashableKey() method, fix imports.

* Using a bit more Kotlinish syntax for reflection (private inner class constructor), and being more strict on that we are using ShortcutRegistration class. (discussed with @mcollovati that this could be a better way)

* Enabling Shortcut related tests (were disabled by another dependency FIX pr to merge it to master)

(cherry picked from commit baf3886)

Co-authored-by: czp13 <61667986+czp13@users.noreply.github.com>
mshabarov added a commit that referenced this pull request Mar 17, 2023
#1620)

* As the ShortcutRegistration will always use HashableKeys, hence why we need to use it in our tests as well to not have any differences
between our tests and real-life application. (now we used simple Key objects, but we need to use HashableKeys)

* Cleaned up the code a bit to use the HashableKey creation only in the MockFilterJsonObject class.
(Based on @mcollovati  review, thank you)

* Put the getHashableKey() method into the class (mock JSON), so where we use it (suggestion by @mcollovati, thanks!).

* One more time, even nicer this way, like this one :) (Thanks again @mcollovati)

* Revert GridTester changes, and fix java doc for the newly added getHashableKey() method, fix imports.

* Using a bit more Kotlinish syntax for reflection (private inner class constructor), and being more strict on that we are using ShortcutRegistration class. (discussed with @mcollovati that this could be a better way)

* Enabling Shortcut related tests (were disabled by another dependency FIX pr to merge it to master)

(cherry picked from commit baf3886)

Co-authored-by: czp13 <61667986+czp13@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants