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  
18  package org.apache.commons.lang3.event;
19  
20  import static org.apache.commons.lang3.LangAssertions.assertIllegalArgumentException;
21  import static org.apache.commons.lang3.LangAssertions.assertNullPointerException;
22  import static org.junit.jupiter.api.Assertions.assertEquals;
23  import static org.junit.jupiter.api.Assertions.assertInstanceOf;
24  import static org.junit.jupiter.api.Assertions.assertSame;
25  import static org.junit.jupiter.api.Assertions.assertThrows;
26  
27  import java.beans.PropertyChangeEvent;
28  import java.beans.PropertyVetoException;
29  import java.beans.VetoableChangeListener;
30  import java.io.ByteArrayInputStream;
31  import java.io.ByteArrayOutputStream;
32  import java.io.IOException;
33  import java.io.ObjectInputStream;
34  import java.io.ObjectOutputStream;
35  import java.lang.reflect.InvocationHandler;
36  import java.lang.reflect.InvocationTargetException;
37  import java.lang.reflect.Method;
38  import java.lang.reflect.UndeclaredThrowableException;
39  import java.util.ArrayList;
40  import java.util.Date;
41  import java.util.List;
42  import java.util.concurrent.atomic.AtomicInteger;
43  import java.util.function.Function;
44  
45  import org.apache.commons.lang3.AbstractLangTest;
46  import org.apache.commons.lang3.exception.ExceptionUtils;
47  import org.apache.commons.lang3.function.FailableConsumer;
48  import org.easymock.EasyMock;
49  import org.junit.jupiter.api.Test;
50  
51  /**
52   * Tests {@link EventListenerSupport}.
53   */
54  class EventListenerSupportTest extends AbstractLangTest {
55  
56      private void addDeregisterListener(final EventListenerSupport<VetoableChangeListener> listenerSupport) {
57          listenerSupport.addListener(new VetoableChangeListener() {
58  
59              @Override
60              public void vetoableChange(final PropertyChangeEvent e) {
61                  listenerSupport.removeListener(this);
62              }
63          });
64      }
65  
66      private VetoableChangeListener createListener(final List<VetoableChangeListener> calledListeners) {
67          return new VetoableChangeListener() {
68  
69              @Override
70              public void vetoableChange(final PropertyChangeEvent e) {
71                  calledListeners.add(this);
72              }
73          };
74      }
75  
76      @Test
77      void testAddListenerNoDuplicates() {
78          final EventListenerSupport<VetoableChangeListener> listenerSupport = EventListenerSupport.create(VetoableChangeListener.class);
79          final VetoableChangeListener[] listeners = listenerSupport.getListeners();
80          assertEquals(0, listeners.length);
81          assertEquals(VetoableChangeListener.class, listeners.getClass().getComponentType());
82          final VetoableChangeListener[] empty = listeners;
83          // for fun, show that the same empty instance is used
84          assertSame(empty, listenerSupport.getListeners());
85          final VetoableChangeListener listener1 = EasyMock.createNiceMock(VetoableChangeListener.class);
86          listenerSupport.addListener(listener1);
87          assertEquals(1, listenerSupport.getListeners().length);
88          listenerSupport.addListener(listener1, false);
89          assertEquals(1, listenerSupport.getListeners().length);
90          listenerSupport.removeListener(listener1);
91          assertSame(empty, listenerSupport.getListeners());
92      }
93  
94      @Test
95      void testAddNullListener() {
96          final EventListenerSupport<VetoableChangeListener> listenerSupport = EventListenerSupport.create(VetoableChangeListener.class);
97          assertNullPointerException(() -> listenerSupport.addListener(null));
98      }
99  
100     @Test
101     void testCreateWithNonInterfaceParameter() {
102         assertIllegalArgumentException(() -> EventListenerSupport.create(String.class));
103     }
104 
105     @Test
106     void testCreateWithNullParameter() {
107         assertNullPointerException(() -> EventListenerSupport.create(null));
108     }
109 
110     @Test
111     void testEventDispatchOrder() throws PropertyVetoException {
112         final EventListenerSupport<VetoableChangeListener> listenerSupport = EventListenerSupport.create(VetoableChangeListener.class);
113         final List<VetoableChangeListener> calledListeners = new ArrayList<>();
114         final VetoableChangeListener listener1 = createListener(calledListeners);
115         final VetoableChangeListener listener2 = createListener(calledListeners);
116         listenerSupport.addListener(listener1);
117         listenerSupport.addListener(listener2);
118         listenerSupport.fire().vetoableChange(new PropertyChangeEvent(new Date(), "Day", 4, 5));
119         assertEquals(calledListeners.size(), 2);
120         assertSame(calledListeners.get(0), listener1);
121         assertSame(calledListeners.get(1), listener2);
122     }
123 
124     @Test
125     void testGetListeners() {
126         final EventListenerSupport<VetoableChangeListener> listenerSupport = EventListenerSupport.create(VetoableChangeListener.class);
127         final VetoableChangeListener[] listeners = listenerSupport.getListeners();
128         assertEquals(0, listeners.length);
129         assertEquals(VetoableChangeListener.class, listeners.getClass().getComponentType());
130         final VetoableChangeListener[] empty = listeners;
131         // for fun, show that the same empty instance is used
132         assertSame(empty, listenerSupport.getListeners());
133         final VetoableChangeListener listener1 = EasyMock.createNiceMock(VetoableChangeListener.class);
134         listenerSupport.addListener(listener1);
135         assertEquals(1, listenerSupport.getListeners().length);
136         final VetoableChangeListener listener2 = EasyMock.createNiceMock(VetoableChangeListener.class);
137         listenerSupport.addListener(listener2);
138         assertEquals(2, listenerSupport.getListeners().length);
139         listenerSupport.removeListener(listener1);
140         assertEquals(1, listenerSupport.getListeners().length);
141         listenerSupport.removeListener(listener2);
142         assertSame(empty, listenerSupport.getListeners());
143     }
144 
145     @Test
146     void testRemoveListenerDuringEvent() throws PropertyVetoException {
147         final EventListenerSupport<VetoableChangeListener> listenerSupport = EventListenerSupport.create(VetoableChangeListener.class);
148         for (int i = 0; i < 10; ++i) {
149             addDeregisterListener(listenerSupport);
150         }
151         assertEquals(listenerSupport.getListenerCount(), 10);
152         listenerSupport.fire().vetoableChange(new PropertyChangeEvent(new Date(), "Day", 4, 5));
153         assertEquals(listenerSupport.getListenerCount(), 0);
154     }
155 
156     @Test
157     void testRemoveNullListener() {
158         final EventListenerSupport<VetoableChangeListener> listenerSupport = EventListenerSupport.create(VetoableChangeListener.class);
159         assertNullPointerException(() -> listenerSupport.removeListener(null));
160     }
161 
162     @Test
163     void testSerialization() throws IOException, ClassNotFoundException, PropertyVetoException {
164         final EventListenerSupport<VetoableChangeListener> listenerSupport = EventListenerSupport.create(VetoableChangeListener.class);
165         listenerSupport.addListener(Function.identity()::apply);
166         listenerSupport.addListener(EasyMock.createNiceMock(VetoableChangeListener.class));
167         // serialize:
168         final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
169         try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream)) {
170             objectOutputStream.writeObject(listenerSupport);
171         }
172         // deserialize:
173         @SuppressWarnings("unchecked")
174         final EventListenerSupport<VetoableChangeListener> deserializedListenerSupport = (EventListenerSupport<VetoableChangeListener>) new ObjectInputStream(
175                 new ByteArrayInputStream(outputStream.toByteArray())).readObject();
176         // make sure we get a listener array back, of the correct component type, and that it contains only the serializable mock
177         final VetoableChangeListener[] listeners = deserializedListenerSupport.getListeners();
178         assertEquals(VetoableChangeListener.class, listeners.getClass().getComponentType());
179         assertEquals(1, listeners.length);
180         // now verify that the mock still receives events; we can infer that the proxy was correctly reconstituted
181         final VetoableChangeListener listener = listeners[0];
182         final PropertyChangeEvent evt = new PropertyChangeEvent(new Date(), "Day", 7, 9);
183         listener.vetoableChange(evt);
184         EasyMock.replay(listener);
185         deserializedListenerSupport.fire().vetoableChange(evt);
186         EasyMock.verify(listener);
187         // remove listener and verify we get an empty array of listeners
188         deserializedListenerSupport.removeListener(listener);
189         assertEquals(0, deserializedListenerSupport.getListeners().length);
190     }
191 
192     @Test
193     void testSubclassInvocationHandling() throws PropertyVetoException {
194         final EventListenerSupport<VetoableChangeListener> eventListenerSupport = new EventListenerSupport<VetoableChangeListener>(
195                 VetoableChangeListener.class) {
196 
197             private static final long serialVersionUID = 1L;
198 
199             @Override
200             protected java.lang.reflect.InvocationHandler createInvocationHandler() {
201                 return new ProxyInvocationHandler() {
202 
203                     @Override
204                     public Object invoke(final Object proxy, final Method method, final Object[] args)
205                             throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
206                         return "vetoableChange".equals(method.getName()) && "Hour".equals(((PropertyChangeEvent) args[0]).getPropertyName()) ? null
207                                 : super.invoke(proxy, method, args);
208                     }
209                 };
210             }
211         };
212         final VetoableChangeListener listener = EasyMock.createNiceMock(VetoableChangeListener.class);
213         eventListenerSupport.addListener(listener);
214         final Object source = new Date();
215         final PropertyChangeEvent ignore = new PropertyChangeEvent(source, "Hour", 5, 6);
216         final PropertyChangeEvent respond = new PropertyChangeEvent(source, "Day", 6, 7);
217         listener.vetoableChange(respond);
218         EasyMock.replay(listener);
219         eventListenerSupport.fire().vetoableChange(ignore);
220         eventListenerSupport.fire().vetoableChange(respond);
221         EasyMock.verify(listener);
222     }
223 
224     /**
225      * Tests that throwing an exception from a listener stops calling the remaining listeners.
226      */
227     @Test
228     void testThrowingListener() {
229         final AtomicInteger count = new AtomicInteger();
230         final EventListenerSupport<VetoableChangeListener> listenerSupport = EventListenerSupport.create(VetoableChangeListener.class);
231         final int vetoLimit = 1;
232         final int listenerCount = 10;
233         for (int i = 0; i < listenerCount; ++i) {
234             listenerSupport.addListener(evt -> {
235                 if (count.incrementAndGet() > vetoLimit) {
236                     throw new PropertyVetoException(count.toString(), evt);
237                 }
238             });
239         }
240         assertEquals(listenerCount, listenerSupport.getListenerCount());
241         assertEquals(0, count.get());
242         final Exception e = assertThrows(UndeclaredThrowableException.class,
243                 () -> listenerSupport.fire().vetoableChange(new PropertyChangeEvent(new Date(), "Day", 0, 1)));
244         final Throwable rootCause = ExceptionUtils.getRootCause(e);
245         assertInstanceOf(PropertyVetoException.class, rootCause);
246         assertEquals(vetoLimit + 1, count.get());
247     }
248 
249     /**
250      * Tests that throwing an exception from a listener continues calling the remaining listeners.
251      */
252     @Test
253     void testThrowingListenerContinues() throws PropertyVetoException {
254         final AtomicInteger count = new AtomicInteger();
255         final EventListenerSupport<VetoableChangeListener> listenerSupport = new EventListenerSupport<VetoableChangeListener>(VetoableChangeListener.class) {
256             @Override
257             protected InvocationHandler createInvocationHandler() {
258                 return new ProxyInvocationHandler(FailableConsumer.nop());
259             }
260         };
261         final int vetoLimit = 1;
262         final int listenerCount = 10;
263         for (int i = 0; i < listenerCount; ++i) {
264             listenerSupport.addListener(evt -> {
265                 if (count.incrementAndGet() > vetoLimit) {
266                     throw new PropertyVetoException(count.toString(), evt);
267                 }
268             });
269         }
270         assertEquals(listenerCount, listenerSupport.getListenerCount());
271         assertEquals(0, count.get());
272         listenerSupport.fire().vetoableChange(new PropertyChangeEvent(new Date(), "Day", 0, 1));
273         assertEquals(listenerCount, count.get());
274     }
275 
276 }