View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.lang3.event;
18  
19  import static org.apache.commons.lang3.LangAssertions.assertIllegalArgumentException;
20  import static org.junit.jupiter.api.Assertions.assertEquals;
21  import static org.junit.jupiter.api.Assertions.assertFalse;
22  import static org.junit.jupiter.api.Assertions.assertNotNull;
23  import static org.junit.jupiter.api.Assertions.assertThrows;
24  import static org.junit.jupiter.api.Assertions.assertTrue;
25  
26  import java.beans.PropertyChangeEvent;
27  import java.beans.PropertyChangeListener;
28  import java.beans.VetoableChangeListener;
29  import java.lang.reflect.Constructor;
30  import java.lang.reflect.InvocationHandler;
31  import java.lang.reflect.Method;
32  import java.lang.reflect.Modifier;
33  import java.lang.reflect.Proxy;
34  import java.util.Date;
35  import java.util.Map;
36  import java.util.TreeMap;
37  
38  import javax.naming.event.ObjectChangeListener;
39  
40  import org.apache.commons.lang3.AbstractLangTest;
41  import org.junit.jupiter.api.Test;
42  
43  /**
44   */
45  class EventUtilsTest extends AbstractLangTest {
46      public static class EventCounter {
47          private int count;
48  
49          public void eventOccurred() {
50              count++;
51          }
52  
53          public int getCount() {
54              return count;
55          }
56      }
57  
58      public static class EventCounterWithEvent {
59          private int count;
60  
61          public void eventOccurred(final PropertyChangeEvent e) {
62              count++;
63          }
64  
65          public int getCount() {
66              return count;
67          }
68      }
69  
70      private static final class EventCountingInvocationHandler implements InvocationHandler {
71  
72          private final Map<String, Integer> eventCounts = new TreeMap<>();
73  
74          public <L> L createListener(final Class<L> listenerType) {
75              return listenerType.cast(Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[] { listenerType }, this));
76          }
77  
78          public int getEventCount(final String eventName) {
79              final Integer count = eventCounts.get(eventName);
80              return count == null ? 0 : count.intValue();
81          }
82  
83          @Override
84          public Object invoke(final Object proxy, final Method method, final Object[] args) {
85              final Integer count = eventCounts.get(method.getName());
86              if (count == null) {
87                  eventCounts.put(method.getName(), Integer.valueOf(1));
88              } else {
89                  eventCounts.put(method.getName(), Integer.valueOf(count.intValue() + 1));
90              }
91              return null;
92          }
93      }
94  
95      public static class ExceptionEventSource {
96          public void addPropertyChangeListener(final PropertyChangeListener listener) {
97              throw new RuntimeException();
98          }
99      }
100 
101     public static class ListenerCapturingSource {
102         PropertyChangeListener listener;
103 
104         public void addPropertyChangeListener(final PropertyChangeListener listener) {
105             this.listener = listener;
106         }
107     }
108 
109     public interface MultipleEventListener {
110         void event1(PropertyChangeEvent e);
111 
112         void event2(PropertyChangeEvent e);
113     }
114 
115     public static class MultipleEventSource {
116         private final EventListenerSupport<MultipleEventListener> listeners = EventListenerSupport.create(MultipleEventListener.class);
117 
118         public void addMultipleEventListener(final MultipleEventListener listener) {
119             listeners.addListener(listener);
120         }
121     }
122 
123     public static class PropertyChangeSource {
124         private final EventListenerSupport<PropertyChangeListener> listeners = EventListenerSupport.create(PropertyChangeListener.class);
125 
126         private String property;
127 
128         public void addPropertyChangeListener(final PropertyChangeListener listener) {
129             listeners.addListener(listener);
130         }
131 
132         protected void addVetoableChangeListener(final VetoableChangeListener listener) {
133             // Do nothing!
134         }
135 
136         public void removePropertyChangeListener(final PropertyChangeListener listener) {
137             listeners.removeListener(listener);
138         }
139 
140         public void setProperty(final String property) {
141             final String oldValue = this.property;
142             this.property = property;
143             listeners.fire().propertyChange(new PropertyChangeEvent(this, "property", oldValue, property));
144         }
145     }
146 
147     @Test
148     void testAddEventListener() {
149         final PropertyChangeSource src = new PropertyChangeSource();
150         final EventCountingInvocationHandler handler = new EventCountingInvocationHandler();
151         final PropertyChangeListener listener = handler.createListener(PropertyChangeListener.class);
152         assertEquals(0, handler.getEventCount("propertyChange"));
153         EventUtils.addEventListener(src, PropertyChangeListener.class, listener);
154         assertEquals(0, handler.getEventCount("propertyChange"));
155         src.setProperty("newValue");
156         assertEquals(1, handler.getEventCount("propertyChange"));
157     }
158 
159     @Test
160     void testAddEventListenerThrowsException() {
161         final ExceptionEventSource src = new ExceptionEventSource();
162         assertThrows(RuntimeException.class, () -> EventUtils.addEventListener(src, PropertyChangeListener.class, e -> {
163             // Do nothing!
164         }));
165     }
166 
167     @Test
168     void testAddEventListenerWithNoAddMethod() {
169         final PropertyChangeSource src = new PropertyChangeSource();
170         final EventCountingInvocationHandler handler = new EventCountingInvocationHandler();
171         final ObjectChangeListener listener = handler.createListener(ObjectChangeListener.class);
172         final IllegalArgumentException e = assertIllegalArgumentException(() -> EventUtils.addEventListener(src, ObjectChangeListener.class, listener));
173         assertEquals("Unable to add listener for class " + src.getClass().getName() + " and public add" + ObjectChangeListener.class.getSimpleName()
174                 + " method which takes a parameter of type " + ObjectChangeListener.class.getName() + ".", e.getMessage());
175     }
176 
177     @Test
178     void testAddEventListenerWithPrivateAddMethod() {
179         final PropertyChangeSource src = new PropertyChangeSource();
180         final EventCountingInvocationHandler handler = new EventCountingInvocationHandler();
181         final VetoableChangeListener listener = handler.createListener(VetoableChangeListener.class);
182         final IllegalArgumentException e = assertIllegalArgumentException(() -> EventUtils.addEventListener(src, VetoableChangeListener.class, listener));
183         assertEquals("Unable to add listener for class " + src.getClass().getName() + " and public add" + VetoableChangeListener.class.getSimpleName()
184                 + " method which takes a parameter of type " + VetoableChangeListener.class.getName() + ".", e.getMessage());
185     }
186 
187     @Test
188     void testBindEventsToMethod() {
189         final PropertyChangeSource src = new PropertyChangeSource();
190         final EventCounter counter = new EventCounter();
191         EventUtils.bindEventsToMethod(counter, "eventOccurred", src, PropertyChangeListener.class);
192         assertEquals(0, counter.getCount());
193         src.setProperty("newValue");
194         assertEquals(1, counter.getCount());
195     }
196 
197     @Test
198     void testBindEventsToMethodObjectMethodsNotDispatched() {
199         // Empty eventTypes: every listener-interface method is bound, but Object methods must be
200         // handled locally (identity semantics), never dispatched to the bound target method.
201         final ListenerCapturingSource src = new ListenerCapturingSource();
202         final EventCounter counter = new EventCounter();
203         EventUtils.bindEventsToMethod(counter, "eventOccurred", src, PropertyChangeListener.class);
204         final PropertyChangeListener listener = src.listener;
205         assertEquals(listener.hashCode(), listener.hashCode());
206         assertTrue(listener.equals(listener));
207         assertFalse(listener.equals(counter));
208         assertNotNull(listener.toString());
209         assertEquals(0, counter.getCount(), "Object methods must not invoke the bound target method");
210 
211         // Non-empty eventTypes: hashCode/equals/toString must not return null (previously an NPE
212         // from unboxing inside hash-based collections).
213         final ListenerCapturingSource src2 = new ListenerCapturingSource();
214         final EventCounter counter2 = new EventCounter();
215         EventUtils.bindEventsToMethod(counter2, "eventOccurred", src2, PropertyChangeListener.class, "propertyChange");
216         final PropertyChangeListener listener2 = src2.listener;
217         assertEquals(listener2.hashCode(), listener2.hashCode());
218         assertTrue(listener2.equals(listener2));
219         assertNotNull(listener2.toString());
220         assertEquals(0, counter2.getCount());
221     }
222 
223     @Test
224     void testBindEventsToMethodWithEvent() {
225         final PropertyChangeSource src = new PropertyChangeSource();
226         final EventCounterWithEvent counter = new EventCounterWithEvent();
227         EventUtils.bindEventsToMethod(counter, "eventOccurred", src, PropertyChangeListener.class);
228         assertEquals(0, counter.getCount());
229         src.setProperty("newValue");
230         assertEquals(1, counter.getCount());
231     }
232 
233     @Test
234     void testBindFilteredEventsToMethod() {
235         final MultipleEventSource src = new MultipleEventSource();
236         final EventCounter counter = new EventCounter();
237         EventUtils.bindEventsToMethod(counter, "eventOccurred", src, MultipleEventListener.class, "event1");
238         assertEquals(0, counter.getCount());
239         src.listeners.fire().event1(new PropertyChangeEvent(new Date(), "Day", Integer.valueOf(0), Integer.valueOf(1)));
240         assertEquals(1, counter.getCount());
241         src.listeners.fire().event2(new PropertyChangeEvent(new Date(), "Day", Integer.valueOf(1), Integer.valueOf(2)));
242         assertEquals(1, counter.getCount());
243     }
244 
245     @Test
246     void testConstructor() {
247         assertNotNull(new EventUtils());
248         final Constructor<?>[] cons = EventUtils.class.getDeclaredConstructors();
249         assertEquals(1, cons.length);
250         assertTrue(Modifier.isPublic(cons[0].getModifiers()));
251         assertTrue(Modifier.isPublic(EventUtils.class.getModifiers()));
252         assertFalse(Modifier.isFinal(EventUtils.class.getModifiers()));
253     }
254 }