This is an automated email from the ASF dual-hosted git repository. reiern70 pushed a commit to branch reiern70/WICKET-7080 in repository https://gitbox.apache.org/repos/asf/wicket.git
commit 6fd7ce97b8276bf2522ad550e1240e234886d5c7 Author: reiern70 <[email protected]> AuthorDate: Mon Oct 16 12:19:53 2023 -0500 [WICKET-7080] 1) make default events delivery machinery pluggable 2) allow to disabled sending events 3) define reflection based machinery to deliver events --- .../org/apache/wicket/event/EventAwareObject.java | 34 +++ .../org/apache/wicket/event/IEventAwareObject.java | 23 ++ .../main/java/org/apache/wicket/event/OnEvent.java | 40 +++ .../wicket/event/ReflectionEventDispatcher.java | 104 +++++++ .../apache/wicket/settings/FrameworkSettings.java | 51 +++- .../event/ReflectionEventDispatcherTest.java | 328 +++++++++++++++++++++ 6 files changed, 567 insertions(+), 13 deletions(-) diff --git a/wicket-core/src/main/java/org/apache/wicket/event/EventAwareObject.java b/wicket-core/src/main/java/org/apache/wicket/event/EventAwareObject.java new file mode 100644 index 0000000000..7214312e9d --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/event/EventAwareObject.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.wicket.event; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation used to mark a component, a session or application as aware of annotated events + * + * See {@link ReflectionEventDispatcher} + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE_USE, ElementType.TYPE }) +public @interface EventAwareObject +{ + +} diff --git a/wicket-core/src/main/java/org/apache/wicket/event/IEventAwareObject.java b/wicket-core/src/main/java/org/apache/wicket/event/IEventAwareObject.java new file mode 100644 index 0000000000..710847b13b --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/event/IEventAwareObject.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.wicket.event; + +/** + * To mark an object as able to react to annotated event handler methods + */ +public interface IEventAwareObject { +} diff --git a/wicket-core/src/main/java/org/apache/wicket/event/OnEvent.java b/wicket-core/src/main/java/org/apache/wicket/event/OnEvent.java new file mode 100644 index 0000000000..18ab9b07a4 --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/event/OnEvent.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.wicket.event; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation used to mark a public method (on a component) or on {@link org.apache.wicket.behavior.Behavior} + * as a handler of an Event. + * <p></p> + * See {@link ReflectionEventDispatcher} + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface OnEvent +{ + + /** + * @return The class of the payload + */ + Class<?> value(); + +} diff --git a/wicket-core/src/main/java/org/apache/wicket/event/ReflectionEventDispatcher.java b/wicket-core/src/main/java/org/apache/wicket/event/ReflectionEventDispatcher.java new file mode 100644 index 0000000000..0e060065f7 --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/event/ReflectionEventDispatcher.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.wicket.event; + +import java.lang.reflect.Method; +import org.apache.wicket.Component; +import org.apache.wicket.IEventDispatcher; +import org.apache.wicket.WicketRuntimeException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * IEventDispatcher that uses reflection in order to locate events methods. + */ +public class ReflectionEventDispatcher implements IEventDispatcher { + + private static final Logger LOGGER = LoggerFactory.getLogger(ReflectionEventDispatcher.class); + + /** + * If set to true then only sinks annotated with @{@link EventAwareObject} will be injected. + * Probably, this should be set to true in most applications and enforce the use of either @{@link EventAwareObject} + * annotation or the marker interface @{@link IEventAwareObject} + */ + private final boolean restrictToEventAware; + + public ReflectionEventDispatcher(boolean restrictToEventAware) + { + this.restrictToEventAware = restrictToEventAware; + } + + @Override + public final void dispatchEvent(Object sink, IEvent<?> event, Component component) + { + if (restrictToEventAware) + { + Class<?> clazz = sink.getClass(); + if (!clazz.isAnnotationPresent(EventAwareObject.class) && !(sink instanceof IEventAwareObject)) + { + // object does not receives events + return; + } + } + executeEvent(sink, event); + } + + private void executeEvent(Object sink, IEvent<?> event) + { + Class<?> clazz = sink.getClass(); + // TODO: allow for private protected methods? + for (Method method : clazz.getMethods()) + { + if (isMethodAnEventMethod(method, event)) + { + method.setAccessible(true); + try + { + // we try to inject IEvent + method.invoke(sink, event); + } + catch (Exception e) + { + try + { + // we try to inject payload directly + method.invoke(sink, event.getPayload()); + } + catch (Exception e1) + { + LOGGER.error("Wrong signature of event method: sink {} and method {}", clazz, method); + throw new WicketRuntimeException("Wrong signature of event method: " + method.getName()); + } + } + } + } + } + + protected boolean isMethodAnEventMethod(Method method, IEvent<?> event) + { + Class<?> payloadClass = event.getPayload().getClass(); + // we look for annotation + if (method.isAnnotationPresent(OnEvent.class)) + { + OnEvent onEvent = method.getAnnotation(OnEvent.class); + Class<?> eventClass = onEvent.value(); + // and we check payload if of the right type + return eventClass.isAssignableFrom(payloadClass); + } + return false; + } +} diff --git a/wicket-core/src/main/java/org/apache/wicket/settings/FrameworkSettings.java b/wicket-core/src/main/java/org/apache/wicket/settings/FrameworkSettings.java index d193ca6a1b..a1eb9b3d84 100644 --- a/wicket-core/src/main/java/org/apache/wicket/settings/FrameworkSettings.java +++ b/wicket-core/src/main/java/org/apache/wicket/settings/FrameworkSettings.java @@ -45,8 +45,28 @@ import org.apache.wicket.util.string.Strings; */ public class FrameworkSettings implements IEventDispatcher { + /** + * Does the standard delivery of events. Override and do nothing if you want to disable it. + */ + private static class DefaultEventDispatcher implements IEventDispatcher + { + @Override + public void dispatchEvent(Object sink, IEvent<?> event, Component component) + { + // direct delivery + if (component != null && sink instanceof IComponentAwareEventSink) + { + ((IComponentAwareEventSink)sink).onEvent(component, event); + } + else if (sink instanceof IEventSink) + { + ((IEventSink)sink).onEvent(event); + } + } + } private IDetachListener detachListener; + private IEventDispatcher defaultEventDispatcher = new DefaultEventDispatcher(); private List<IEventDispatcher> eventDispatchers = null; /** @@ -68,7 +88,7 @@ public class FrameworkSettings implements IEventDispatcher * Gets the Wicket version. The Wicket version is in the same format as the version element in * the pom.xml file (project descriptor). The version is generated by maven in the build/release * cycle and put in the /META-INF/MANIFEST.MF file located in the root folder of the Wicket jar. - * + * <p></p> * The version usually follows one of the following formats: * <ul> * <li>major.minor[.bug] for stable versions. 1.1, 1.2, 1.2.1 are examples</li> @@ -112,7 +132,7 @@ public class FrameworkSettings implements IEventDispatcher /** * Registers a new event dispatcher * - * @param dispatcher + * @param dispatcher {@link IEventDispatcher} * @return {@code this} object for chaining */ public FrameworkSettings add(IEventDispatcher dispatcher) @@ -133,22 +153,14 @@ public class FrameworkSettings implements IEventDispatcher * Dispatches event to registered dispatchers * * @see IEventDispatcher#dispatchEvent(Object, IEvent, Component) - * - * @param sink - * @param event - * @param component + * */ @Override public void dispatchEvent(Object sink, IEvent<?> event, Component component) { - // direct delivery - if (component != null && sink instanceof IComponentAwareEventSink) + if (defaultEventDispatcher != null) { - ((IComponentAwareEventSink)sink).onEvent(component, event); - } - else if (sink instanceof IEventSink) - { - ((IEventSink)sink).onEvent(event); + defaultEventDispatcher.dispatchEvent(sink, event, component); } // additional dispatchers delivery @@ -162,6 +174,19 @@ public class FrameworkSettings implements IEventDispatcher } } + /** + * Allows to set the default events dispatcher + * + * @param defaultEventDispatcher + * IEventDispatcher + * @return {@code this} object for chaining + */ + public FrameworkSettings setDefaultEventDispatcher(IEventDispatcher defaultEventDispatcher) + { + this.defaultEventDispatcher = defaultEventDispatcher; + return this; + } + /** * Sets the {@link ISerializer} that will be used to convert objects to/from byte arrays * diff --git a/wicket-core/src/test/java/org/apache/wicket/event/ReflectionEventDispatcherTest.java b/wicket-core/src/test/java/org/apache/wicket/event/ReflectionEventDispatcherTest.java new file mode 100644 index 0000000000..ed6b232176 --- /dev/null +++ b/wicket-core/src/test/java/org/apache/wicket/event/ReflectionEventDispatcherTest.java @@ -0,0 +1,328 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.wicket.event; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import org.apache.wicket.Component; +import org.apache.wicket.MockPageWithOneComponent; +import org.apache.wicket.behavior.Behavior; +import org.apache.wicket.markup.html.WebComponent; +import org.apache.wicket.util.tester.WicketTestCase; +import org.junit.jupiter.api.Test; + +/** + * @author reiern70 + */ +class ReflectionEventDispatcherTest extends WicketTestCase +{ + + private static class TestPayload { + + } + + // we just disable event dispatching at application level + @Test + void testEventsDeliveryDisable() + { + tester.getApplication().getFrameworkSettings().setDefaultEventDispatcher(null); + MockPageWithOneComponent page = new MockPageWithOneComponent(); + TestComponent testComponent = new TestComponent(MockPageWithOneComponent.COMPONENT_ID); + page.add(testComponent); + page.send(page, Broadcast.DEPTH, new TestPayload()); + assertEquals(0, testComponent.invocationTimes); + assertEquals(0, testComponent.getBehaviors(TestBehavior.class).get(0).invocationTimes); + } + + + /** + * Testing ReflectionEventDispatcher event dispatchers in frameworksettings. This dispatcher + * invoke the methods annotated with @OnEvent + * */ + @Test + void dispatchToAnnotatedMethodNoRestrictions() + { + tester.getApplication().getFrameworkSettings().add(new ReflectionEventDispatcher(false)); + MockPageWithOneComponent page = new MockPageWithOneComponent(); + TestComponent testComponent = new TestComponent(MockPageWithOneComponent.COMPONENT_ID); + page.add(testComponent); + page.send(page, Broadcast.DEPTH, new TestPayload()); + assertEquals( 2, testComponent.invocationTimes); + assertEquals( 2, testComponent.getBehaviors(TestBehavior.class).get(0).invocationTimes); + } + + /** + * The same as the above but injecting IEvent. + */ + @Test + void dispatchToAnnotatedMethodNoRestrictionsReceivesIEvent() + { + tester.getApplication().getFrameworkSettings().add(new ReflectionEventDispatcher(false)); + MockPageWithOneComponent page = new MockPageWithOneComponent(); + TestComponentIEvent testComponent = new TestComponentIEvent(MockPageWithOneComponent.COMPONENT_ID); + page.add(testComponent); + page.send(page, Broadcast.DEPTH, new TestPayload()); + assertEquals(2, testComponent.invocationTimes); + assertEquals(2, testComponent.getBehaviors(TestBehaviorIEvent.class).get(0).invocationTimes); + } + + /** + * Testing ReflectionEventDispatcher event dispatchers in frameworksettings. This dispatcher + * invoke the methods annotated with @OnEvent + * */ + @Test + void dispatchToAnnotatedMethodToAnnotatedObjects() + { + tester.getApplication().getFrameworkSettings().add(new ReflectionEventDispatcher(true)); + MockPageWithOneComponent page = new MockPageWithOneComponent(); + TestComponent testComponent = new TestComponent(MockPageWithOneComponent.COMPONENT_ID); + page.add(testComponent); + page.send(page, Broadcast.DEPTH, new TestPayload()); + // as component and behavior are not annotated with @EventAwareObject only default event system will work + assertEquals(1, testComponent.invocationTimes); + assertEquals(1, testComponent.getBehaviors(TestBehavior.class).get(0).invocationTimes); + } + + @Test + void dispatchToAnnotatedMethodToAnnotatedObjectsAnonymous() + { + tester.getApplication().getFrameworkSettings().add(new ReflectionEventDispatcher(false)); + MockPageWithOneComponent page = new MockPageWithOneComponent(); + TestComponent testComponent = new TestComponent(MockPageWithOneComponent.COMPONENT_ID) { + @OnEvent(TestPayload.class) + @SuppressWarnings("unused") + public void testCallbackEvent(IEvent<TestPayload> event) + { + assertNotNull(event); + assertNotNull(event.getPayload()); + invocationTimes++; + } + + @Override + protected void addBehavior() { + add(new TestBehavior(){ + @OnEvent(TestPayload.class) + @SuppressWarnings("unused") + public void testCallbackEvent(IEvent<TestPayload> event) + { + assertNotNull(event); + assertNotNull(event.getPayload()); + invocationTimes++; + } + }); + } + }; + page.add(testComponent); + page.send(page, Broadcast.DEPTH, new TestPayload()); + // as component and behavior are not annotated with @EventAwareObject only default event system will work + assertEquals(3, testComponent.invocationTimes); + assertEquals(3, testComponent.getBehaviors(TestBehavior.class).get(0).invocationTimes); + } + + /** + * The same as the above but injecting IEvent. + */ + @Test + void dispatchToAnnotatedMethodToAnnotatedEventAwareObject() + { + // both events will arrive + tester.getApplication().getFrameworkSettings().add(new ReflectionEventDispatcher(false)); + MockPageWithOneComponent page = new MockPageWithOneComponent(); + TestComponentIEvent testComponent = new TestComponentIEvent(MockPageWithOneComponent.COMPONENT_ID); + page.add(testComponent); + page.send(page, Broadcast.DEPTH, new TestPayload()); + assertEquals(2, testComponent.invocationTimes); + assertEquals(2, testComponent.getBehaviors(TestBehaviorIEvent.class).get(0).invocationTimes); + } + + /** + * The same as the above but injecting IEvent. + */ + @Test + void dispatchToAnnotatedMethodInOnjectImplementingEventAwareObjectTestBehavior() + { + // both events will arrive + tester.getApplication().getFrameworkSettings().add(new ReflectionEventDispatcher(false)); + MockPageWithOneComponent page = new MockPageWithOneComponent(); + EventAwareObjectTestComponent testComponent = new EventAwareObjectTestComponent(MockPageWithOneComponent.COMPONENT_ID); + page.add(testComponent); + page.send(page, Broadcast.DEPTH, new TestPayload()); + assertEquals(2, testComponent.invocationTimes); + assertEquals(2, testComponent.getBehaviors(EventAwareObjectTestBehavior.class).get(0).invocationTimes); + } + + @Test + void replaceDefaultEvents() + { + // both events will arrive + tester.getApplication().getFrameworkSettings().setDefaultEventDispatcher(new ReflectionEventDispatcher(false)); + MockPageWithOneComponent page = new MockPageWithOneComponent(); + TestComponentIEvent testComponent = new TestComponentIEvent(MockPageWithOneComponent.COMPONENT_ID); + page.add(testComponent); + page.send(page, Broadcast.DEPTH, new TestPayload()); + assertEquals(1, testComponent.invocationTimes); + assertEquals(1, testComponent.getBehaviors(TestBehaviorIEvent.class).get(0).invocationTimes); + } + + @Test + void replaceDefaultEventsIEvent() + { + // both events will arrive + tester.getApplication().getFrameworkSettings().setDefaultEventDispatcher(new ReflectionEventDispatcher(false)); + MockPageWithOneComponent page = new MockPageWithOneComponent(); + TestComponent testComponent = new TestComponent(MockPageWithOneComponent.COMPONENT_ID); + page.add(testComponent); + page.send(page, Broadcast.DEPTH, new TestPayload()); + assertEquals( 1, testComponent.invocationTimes); + assertEquals( 1, testComponent.getBehaviors(TestBehavior.class).get(0).invocationTimes); + } + + @Test + void replaceDefaultEventsAnnotatedNoDelivery() + { + tester.getApplication().getFrameworkSettings().setDefaultEventDispatcher(new ReflectionEventDispatcher(true)); + MockPageWithOneComponent page = new MockPageWithOneComponent(); + TestComponent testComponent = new TestComponent(MockPageWithOneComponent.COMPONENT_ID); + page.add(testComponent); + page.send(page, Broadcast.DEPTH, new TestPayload()); + // no events will arrive as component is not annoated + assertEquals(0, testComponent.invocationTimes); + assertEquals( 0, testComponent.getBehaviors(TestBehavior.class).get(0).invocationTimes); + } + + /** */ + public static class TestComponent extends WebComponent + { + private static final long serialVersionUID = 1L; + int invocationTimes = 0; + + TestComponent(String id) + { + super(id); + addBehavior(); + } + + protected void addBehavior() { + add(new TestBehavior()); + } + + /** */ + @OnEvent(TestPayload.class) + @SuppressWarnings("unused") + public void testCallback(TestPayload payload) + { + assertNotNull(payload); + invocationTimes++; + } + + @Override + public void onEvent(IEvent<?> event) { + invocationTimes++; + } + } + + private static class TestBehavior extends Behavior + { + + private static final long serialVersionUID = 1; + + int invocationTimes = 0; + + @Override + public void onEvent(Component component, IEvent<?> event) + { + invocationTimes++; + } + + @OnEvent(TestPayload.class) + @SuppressWarnings("unused") + public void testCallback(TestPayload payload) + { + assertNotNull(payload); + invocationTimes++; + } + } + + public static class EventAwareObjectTestComponent extends TestComponent implements IEventAwareObject { + + EventAwareObjectTestComponent(String id) { + super(id); + } + + @Override + protected void addBehavior() { + add(new EventAwareObjectTestBehavior()); + } + } + + private static class EventAwareObjectTestBehavior extends TestBehavior implements IEventAwareObject{ + + } + + @EventAwareObject + public static class TestComponentIEvent extends WebComponent + { + private static final long serialVersionUID = 1L; + int invocationTimes = 0; + + TestComponentIEvent(String id) + { + super(id); + + add(new TestBehaviorIEvent()); + } + + @Override + public void onEvent(IEvent<?> event) { + invocationTimes++; + } + + /** */ + @OnEvent(TestPayload.class) + @SuppressWarnings("unused") + public void testCallback(IEvent<TestPayload> event) + { + assertNotNull(event); + assertNotNull(event.getPayload()); + invocationTimes++; + } + } + + @EventAwareObject + private static class TestBehaviorIEvent extends Behavior + { + + private static final long serialVersionUID = 1; + + int invocationTimes = 0; + + @Override + public void onEvent(Component component, IEvent<?> event) + { + invocationTimes++; + } + + @OnEvent(TestPayload.class) + @SuppressWarnings("unused") + public void testCallback(IEvent<TestPayload> event) + { + assertNotNull(event); + assertNotNull(event.getPayload()); + invocationTimes++; + } + } +}
