Skip to content

Commit 2e9989d

Browse files
author
nicolaiparlog
committed
Implemented a generic 'ListenerHandle'.
1 parent 905c4f8 commit 2e9989d

File tree

2 files changed

+262
-0
lines changed

2 files changed

+262
-0
lines changed
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package org.codefx.libfx.listener;
2+
3+
import java.util.Objects;
4+
import java.util.function.BiConsumer;
5+
6+
/**
7+
* A generic implementation of {@link ListenerHandle} which uses functions specified during construction to
8+
* {@link #attach()} and {@link #detach()} the listener to the observable instance.
9+
*
10+
* @param <O>
11+
* the type of the observable instance (e.g {@link javafx.beans.value.ObservableValue ObservableValue} or
12+
* {@link javafx.collections.ObservableMap ObservableMap}) to which the listener will be added
13+
* @param <L>
14+
* the type of the listener which will be added to the observable
15+
*/
16+
final class GenericListenerHandle<O, L> implements ListenerHandle {
17+
18+
// #region FIELDS
19+
20+
/**
21+
* The observable instance to which the {@link #listener} will be added.
22+
*/
23+
private final O observable;
24+
25+
/**
26+
* The listener which will be added to the {@link #observable}.
27+
*/
28+
private final L listener;
29+
30+
/**
31+
* Called on {@link #attach()}.
32+
*/
33+
private final BiConsumer<? super O, ? super L> add;
34+
35+
/**
36+
* Called on {@link #detach()}.
37+
*/
38+
private final BiConsumer<? super O, ? super L> remove;
39+
40+
/**
41+
* Indicates whether the {@link #listener} is currently added to the {@link #observable}.
42+
*/
43+
private boolean attached;
44+
45+
// #end FIELDS
46+
47+
// #region CONSTRUCITON
48+
49+
/**
50+
* Creates a new listener handle for the specified arguments. The listener is initially detached.
51+
*
52+
* @param observable
53+
* the observable instance to which the {@code listener} will be added
54+
* @param listener
55+
* the listener which will be added to the {@code observable}
56+
* @param add
57+
* called when the {@code listener} must be added to the {@code observable}
58+
* @param remove
59+
* called when the {@code listener} must be removed from the {@code observable}
60+
*/
61+
public GenericListenerHandle(
62+
O observable, L listener, BiConsumer<? super O, ? super L> add, BiConsumer<? super O, ? super L> remove) {
63+
64+
Objects.requireNonNull(observable, "The argument 'observable' must not be null.");
65+
Objects.requireNonNull(listener, "The argument 'listener' must not be null.");
66+
Objects.requireNonNull(add, "The argument 'add' must not be null.");
67+
Objects.requireNonNull(remove, "The argument 'remove' must not be null.");
68+
69+
this.observable = observable;
70+
this.listener = listener;
71+
this.add = add;
72+
this.remove = remove;
73+
}
74+
75+
// #end CONSTRUCITON
76+
77+
// #region IMPLEMENTATION OF 'ListenerHandle'
78+
79+
@Override
80+
public void attach() {
81+
if (attached)
82+
return;
83+
84+
attached = true;
85+
add.accept(observable, listener);
86+
}
87+
88+
@Override
89+
public void detach() {
90+
if (!attached)
91+
return;
92+
93+
attached = false;
94+
remove.accept(observable, listener);
95+
}
96+
97+
// #end IMPLEMENTATION OF 'ListenerHandle'
98+
99+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package org.codefx.libfx.listener;
2+
3+
import static org.mockito.Mockito.mock;
4+
import static org.mockito.Mockito.times;
5+
import static org.mockito.Mockito.verify;
6+
import static org.mockito.Mockito.verifyNoMoreInteractions;
7+
import static org.mockito.Mockito.verifyZeroInteractions;
8+
9+
import java.util.function.BiConsumer;
10+
11+
import org.junit.Before;
12+
import org.junit.Test;
13+
14+
/**
15+
* Tests the class {@link GenericListenerHandle}.
16+
*/
17+
public class GenericListenerHandleTest {
18+
19+
// #region INSTANCES
20+
21+
/**
22+
* The tested handle.
23+
*/
24+
private GenericListenerHandle<Object, Object> handle;
25+
26+
/**
27+
* The observable on which the {@link #handle} operates.
28+
*/
29+
private Object observable;
30+
31+
/**
32+
* The listener on which the {@link #handle} operates.
33+
*/
34+
private Object listener;
35+
36+
/**
37+
* The function which adds the listener to the observable. This is a mock so calls can be verified.
38+
*/
39+
private BiConsumer<Object, Object> add;
40+
41+
/**
42+
* The function which adds the listener to the observable. This is a mock so calls can be verified.
43+
*/
44+
private BiConsumer<Object, Object> remove;
45+
46+
// #end INSTANCES
47+
48+
// #region SETUP
49+
50+
/**
51+
* Creates the tested instances.
52+
*/
53+
@Before
54+
@SuppressWarnings("unchecked")
55+
public void setUp() {
56+
add = mock(BiConsumer.class);
57+
remove = mock(BiConsumer.class);
58+
observable = "observable";
59+
listener = "listner";
60+
61+
handle = new GenericListenerHandle<Object, Object>(observable, listener, add, remove);
62+
}
63+
64+
// #end SETUP
65+
66+
// #region TESTS
67+
68+
/**
69+
* Tests whether the construction of the handle does not cause any calls to {@link #add} and {@link #remove}.
70+
*/
71+
@Test
72+
public void testNoCallsToAddAndRemoveOnConstruction() {
73+
verifyZeroInteractions(add, remove);
74+
}
75+
76+
/**
77+
* Tests whether the a call to {@link ListenerHandle#detach() detach()} after construction does not cause any calls
78+
* to {@link #add} and {@link #remove}.
79+
*/
80+
@Test
81+
public void testDetachAfterConstruction() {
82+
handle.detach();
83+
84+
verifyZeroInteractions(add, remove);
85+
}
86+
87+
/**
88+
* Tests whether the first call to {@link ListenerHandle#attach() attach()} correctly calls {@link #add}.
89+
*/
90+
@Test
91+
public void testAttachAfterConstruction() {
92+
handle.attach();
93+
94+
verify(add, times(1)).accept(observable, listener);
95+
verifyNoMoreInteractions(add);
96+
verifyZeroInteractions(remove);
97+
}
98+
99+
/**
100+
* Tests whether calling {@link ListenerHandle#attach() attach()} multiple times in a row calls {@link #add} only
101+
* once.
102+
*/
103+
@Test
104+
public void testMultipleAttach() {
105+
handle.attach();
106+
handle.attach();
107+
handle.attach();
108+
109+
verify(add, times(1)).accept(observable, listener);
110+
verifyNoMoreInteractions(add);
111+
verifyZeroInteractions(remove);
112+
}
113+
114+
/**
115+
* Tests whether calling {@link ListenerHandle#detach() detach()} correctly calls {@link #remove}.
116+
*/
117+
@Test
118+
public void testDetach() {
119+
handle.attach();
120+
handle.detach();
121+
122+
// the order of those calls is not verified here;
123+
// but if it would not match the intuition (first 'add', then 'remove'), a more specific test above would fail
124+
verify(add, times(1)).accept(observable, listener);
125+
verify(remove, times(1)).accept(observable, listener);
126+
verifyNoMoreInteractions(add, remove);
127+
}
128+
129+
/**
130+
* Tests whether calling {@link ListenerHandle#detach() detach()} multiple times in a row calls {@link #remove} only
131+
* once.
132+
*/
133+
@Test
134+
public void testMultipleDetach() {
135+
handle.attach();
136+
handle.detach();
137+
handle.detach();
138+
handle.detach();
139+
140+
verify(add, times(1)).accept(observable, listener);
141+
verify(remove, times(1)).accept(observable, listener);
142+
verifyZeroInteractions(remove);
143+
}
144+
145+
/**
146+
* Tests whether reattaching calls {@link #add} twice.
147+
*/
148+
@Test
149+
public void testReattach() {
150+
handle.attach();
151+
handle.detach();
152+
handle.attach();
153+
154+
// the order of those calls is not verified here;
155+
// but if it would not match the intuition ('add', 'remove', 'add'), a more specific test above would fail
156+
verify(add, times(2)).accept(observable, listener);
157+
verify(remove, times(1)).accept(observable, listener);
158+
verifyNoMoreInteractions(add, remove);
159+
}
160+
161+
// #end TESTS
162+
163+
}

0 commit comments

Comments
 (0)