Skip to content

Commit 981274c

Browse files
author
nicolaiparlog
committed
Implemented a builder for 'GenericListenerHandle'.
1 parent 2e9989d commit 981274c

File tree

2 files changed

+378
-0
lines changed

2 files changed

+378
-0
lines changed
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package org.codefx.libfx.listener;
2+
3+
import java.util.Objects;
4+
import java.util.Optional;
5+
import java.util.function.BiConsumer;
6+
7+
/**
8+
* A builder for a {@link ListenerHandle}.
9+
* <p>
10+
* The created handle manages whether the listener is currently attached. The functions specified to
11+
* {@link #onAttach(BiConsumer)} and {@link #onDetach(BiConsumer)} are only called when necessary. This is the case
12+
* <ul>
13+
* <li>if {@link ListenerHandle#attach() attach} is called when the listener is not currently added to the observable
14+
* <li>if {@link ListenerHandle#detach() detach} is called when the listener is currently added to the observable
15+
* </ul>
16+
* This implies that they can be stateless functions which simply add and remove the listener. The functions are called
17+
* with the observable and listener specified during construction.
18+
* <p>
19+
* <h2>Example</h2> A typical use looks like this:
20+
*
21+
* <pre>
22+
* Property<String> textProperty;
23+
* ChangeListener<String> textListener;
24+
*
25+
* ListenerHandle textListenerHandle = ListenerHandleBuilder
26+
* .from(textProperty, textListener)
27+
* .onAttach((property, listener) -> property.addListener(listener))
28+
* .onDetach((property, listener) -> property.removeListener(listener))
29+
* .build();
30+
* </pre>
31+
*
32+
* @param <O>
33+
* the type of the observable instance (e.g {@link javafx.beans.value.ObservableValue ObservableValue} or
34+
* {@link javafx.collections.ObservableMap ObservableMap}) to which the listener will be added
35+
* @param <L>
36+
* the type of the listener which will be added to the observable
37+
*/
38+
public final class ListenerHandleBuilder<O, L> {
39+
40+
// #region FIELDS
41+
42+
/**
43+
* The observable instance to which the {@link #listener} will be added.
44+
*/
45+
private final O observable;
46+
47+
/**
48+
* The listener which will be added to the {@link #observable}.
49+
*/
50+
private final L listener;
51+
52+
/**
53+
* Called on {@link ListenerHandle#attach()}.
54+
*/
55+
private Optional<BiConsumer<? super O, ? super L>> add;
56+
57+
/**
58+
* Called on {@link ListenerHandle#detach()}.
59+
*/
60+
private Optional<BiConsumer<? super O, ? super L>> remove;
61+
62+
// #end FIELDS
63+
64+
// #region CONSTRUCTION
65+
66+
/**
67+
* Creates a builder for a generic {@link ListenerHandle}.
68+
*
69+
* @param observable
70+
* the observable instance to which the {@code listener} will be added
71+
* @param listener
72+
* the listener which will be added to the {@code observable}
73+
*/
74+
private ListenerHandleBuilder(O observable, L listener) {
75+
Objects.requireNonNull(observable, "The argument 'observable' must not be null.");
76+
Objects.requireNonNull(listener, "The argument 'listener' must not be null.");
77+
78+
this.observable = observable;
79+
this.listener = listener;
80+
81+
add = Optional.empty();
82+
remove = Optional.empty();
83+
}
84+
85+
/**
86+
* Creates a builder for a generic {@link ListenerHandle}.
87+
*
88+
* @param <O>
89+
* the type of the observable instance (e.g {@link javafx.beans.value.ObservableValue ObservableValue} or
90+
* {@link javafx.collections.ObservableMap ObservableMap}) to which the listener will be added
91+
* @param <L>
92+
* the type of the listener which will be added to the observable
93+
* @param observable
94+
* the observable instance to which the {@code listener} will be added
95+
* @param listener
96+
* the listener which will be added to the {@code observable}
97+
* @return a {@link ListenerHandleBuilder} for a {@link ListenerHandle}.
98+
*/
99+
public static <O, L> ListenerHandleBuilder<O, L> from(O observable, L listener) {
100+
return new ListenerHandleBuilder<>(observable, listener);
101+
}
102+
103+
// #end CONSTRUCTION
104+
105+
// #region SET AND BUILD
106+
107+
/**
108+
* Sets the function which is executed when the built {@link ListenerHandle} must add the listener because
109+
* {@link ListenerHandle#attach() attach} was called.
110+
* <p>
111+
* Because the built handle manages whether the listener is currently attached, the function is only called when
112+
* necessary, i.e. when {@code attach} is called when the listener is currently not added to the observable.
113+
*
114+
* @param add
115+
* the {@link BiConsumer} called on {@code attach}; the arguments for the function are the observable and
116+
* listener specified during this builder's construction
117+
* @return this builder for fluent calls
118+
*/
119+
public ListenerHandleBuilder<O, L> onAttach(BiConsumer<? super O, ? super L> add) {
120+
Objects.requireNonNull(add, "The argument 'add' must not be null.");
121+
122+
this.add = Optional.of(add);
123+
return this;
124+
}
125+
126+
/**
127+
* Sets the function which is executed when the built {@link ListenerHandle} must remove the listener because
128+
* {@link ListenerHandle#attach() detach} was called.
129+
* <p>
130+
* Because the built handle manages whether the listener is currently attached, the function is only called when
131+
* necessary, i.e. when {@code detach} is called when the listener is currently added to the observable.
132+
*
133+
* @param remove
134+
* the {@link BiConsumer} called on {@code detach}; the arguments for the function are the observable and
135+
* listener specified during this builder's construction
136+
* @return this builder for fluent calls
137+
*/
138+
public ListenerHandleBuilder<O, L> onDetach(BiConsumer<? super O, ? super L> remove) {
139+
Objects.requireNonNull(remove, "The argument 'remove' must not be null.");
140+
141+
this.remove = Optional.of(remove);
142+
return this;
143+
}
144+
145+
/**
146+
* Creates a new listener handle. This will only succeed if {@link #onAttach(BiConsumer)} and
147+
* {@link #onDetach(BiConsumer)} have been called.
148+
*
149+
* @return a new {@link ListenerHandle}; initially detached
150+
* @throws IllegalStateException
151+
* if {@link #onAttach(BiConsumer)} or {@link #onDetach(BiConsumer)} have not been called
152+
*/
153+
public ListenerHandle build() throws IllegalStateException {
154+
verifyAddAndRemovePresent();
155+
return new GenericListenerHandle<O, L>(observable, listener, add.get(), remove.get());
156+
}
157+
158+
/**
159+
* Verifies that {@link #add} and {@link #remove} are present.
160+
*
161+
* @throws IllegalStateException
162+
* if {@link #add} or {@link #remove} is empty.
163+
*/
164+
private void verifyAddAndRemovePresent() throws IllegalStateException {
165+
boolean onAttachNotCalled = !add.isPresent();
166+
boolean onDetachNotCalled = !remove.isPresent();
167+
boolean canBuild = !onAttachNotCalled && !onDetachNotCalled;
168+
169+
if (canBuild)
170+
return;
171+
else
172+
throwExceptionForMissingCall(onAttachNotCalled, onDetachNotCalled);
173+
}
174+
175+
/**
176+
* Throws an {@link IllegalStateException} for a missing call.
177+
*
178+
* @param onAttachNotCalled
179+
* indicates whether {@link #onAttach(BiConsumer)} has been called
180+
* @param onDetachNotCalled
181+
* indicates whether {@link #onDetach(BiConsumer)} has been called
182+
* @throws IllegalStateException
183+
* if at least one of the specified booleans is true
184+
*/
185+
private static void throwExceptionForMissingCall(boolean onAttachNotCalled, boolean onDetachNotCalled)
186+
throws IllegalStateException {
187+
188+
if (onAttachNotCalled && onDetachNotCalled)
189+
throw new IllegalStateException(
190+
"A listener handle can not be build until 'onAttach' and 'onDetach' have been called.");
191+
192+
if (onAttachNotCalled)
193+
throw new IllegalStateException("A listener handle can not be build until 'onAttach' has been called.");
194+
195+
if (onDetachNotCalled)
196+
throw new IllegalStateException("A listener handle can not be build until 'onDetach' has been called.");
197+
}
198+
199+
// #end SET AND BUILD
200+
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package org.codefx.libfx.listener;
2+
3+
import static org.junit.Assert.assertNotNull;
4+
import static org.mockito.Mockito.mock;
5+
import static org.mockito.Mockito.times;
6+
import static org.mockito.Mockito.verify;
7+
import static org.mockito.Mockito.verifyNoMoreInteractions;
8+
9+
import java.util.function.BiConsumer;
10+
11+
import org.junit.Test;
12+
13+
/**
14+
* Tests the class {@link ListenerHandleBuilder}.
15+
*/
16+
public class ListenerHandleBuilderTest {
17+
18+
/**
19+
* A not null {@link Object} which can be used to represent an observable or a listener.
20+
*/
21+
private static final Object NOT_NULL = new Object();
22+
23+
/**
24+
* A not null {@link BiConsumer} which can be used to represent an add or a remove function.
25+
*/
26+
private static final BiConsumer<Object, Object> NOT_NULL_CONSUMER = (o, l) -> { /* do nothing */};
27+
28+
// #region TESTS
29+
30+
// construction
31+
32+
/**
33+
* Tests whether the factory method can not be called with a null observable.
34+
*/
35+
@Test(expected = NullPointerException.class)
36+
public void testConstructorWithNullObservable() {
37+
ListenerHandleBuilder.from(null, NOT_NULL);
38+
}
39+
40+
/**
41+
* Tests whether the factory method can not be called with a null listener.
42+
*/
43+
@Test(expected = NullPointerException.class)
44+
public void testConstructorWithNullListener() {
45+
ListenerHandleBuilder.from(NOT_NULL, null);
46+
}
47+
48+
/**
49+
* Tests whether the factory method returns a non-null builder.
50+
*/
51+
@Test
52+
public void testSuccessfulConstruction() {
53+
ListenerHandleBuilder<?, ?> builder = ListenerHandleBuilder.from(NOT_NULL, NOT_NULL);
54+
assertNotNull(builder);
55+
}
56+
57+
// setting values
58+
59+
/**
60+
* Tests whether the builder does not accepts a null add function.
61+
*/
62+
@Test(expected = NullPointerException.class)
63+
public void testSetNullAdd() {
64+
ListenerHandleBuilder<?, ?> builder = ListenerHandleBuilder.from(NOT_NULL, NOT_NULL);
65+
builder.onAttach(null);
66+
}
67+
68+
/**
69+
* Tests whether the builder does not accepts a null remove function.
70+
*/
71+
@Test(expected = NullPointerException.class)
72+
public void testSetNullRemove() {
73+
ListenerHandleBuilder<?, ?> builder = ListenerHandleBuilder.from(NOT_NULL, NOT_NULL);
74+
builder.onDetach(null);
75+
}
76+
77+
// build
78+
79+
/**
80+
* Tests whether {@link ListenerHandleBuilder#build() build} can not be called when neither
81+
* {@link ListenerHandleBuilder#onAttach(BiConsumer) onAttach} nor
82+
* {@link ListenerHandleBuilder#onDetach(BiConsumer) onDetach} were called.
83+
*/
84+
@Test(expected = IllegalStateException.class)
85+
public void testNotCallingOnAttachAndOnDetachBeforeBuild() {
86+
ListenerHandleBuilder
87+
.from(NOT_NULL, NOT_NULL)
88+
.build();
89+
}
90+
91+
/**
92+
* Tests whether {@link ListenerHandleBuilder#build() build} can not be called when
93+
* {@link ListenerHandleBuilder#onAttach(BiConsumer) onAttach} was not called.
94+
*/
95+
@Test(expected = IllegalStateException.class)
96+
public void testNotCallingOnAttachBeforeBuild() {
97+
ListenerHandleBuilder
98+
.from(NOT_NULL, NOT_NULL)
99+
.onDetach(NOT_NULL_CONSUMER)
100+
.build();
101+
}
102+
103+
/**
104+
* Tests whether {@link ListenerHandleBuilder#build() build} can not be called when
105+
* {@link ListenerHandleBuilder#onDetach(BiConsumer) onDetach} was not called.
106+
*/
107+
@Test(expected = IllegalStateException.class)
108+
public void testNotCallingOnDetachBeforeBuild() {
109+
ListenerHandleBuilder
110+
.from(NOT_NULL, NOT_NULL)
111+
.onAttach(NOT_NULL_CONSUMER)
112+
.build();
113+
}
114+
115+
/**
116+
* Tests whether the built {@link ListenerHandle} is not null.
117+
*/
118+
@Test
119+
public void testSuccessfulBuild() {
120+
ListenerHandle handle = ListenerHandleBuilder
121+
.from(NOT_NULL, NOT_NULL)
122+
.onAttach(NOT_NULL_CONSUMER)
123+
.onDetach(NOT_NULL_CONSUMER)
124+
.build();
125+
126+
assertNotNull(handle);
127+
}
128+
129+
// correct arguments for 'add' and 'remove' functions
130+
131+
/**
132+
* Tests whether the add function is called with the correct arguments.
133+
*/
134+
@Test
135+
public void testAddCalledWithCorrectArguments() {
136+
// setup
137+
@SuppressWarnings("unchecked")
138+
BiConsumer<Object, Object> add = mock(BiConsumer.class);
139+
ListenerHandle handle = ListenerHandleBuilder
140+
.from(NOT_NULL, NOT_NULL)
141+
.onAttach(add)
142+
.onDetach(NOT_NULL_CONSUMER)
143+
.build();
144+
145+
// trigger a call to 'add'
146+
handle.attach();
147+
148+
// verify
149+
verify(add, times(1)).accept(NOT_NULL, NOT_NULL);
150+
verifyNoMoreInteractions(add);
151+
}
152+
153+
/**
154+
* Tests whether the remove function is called with the correct arguments.
155+
*/
156+
@Test
157+
public void testRemoveCalledWithCorrectArguments() {
158+
// setup
159+
@SuppressWarnings("unchecked")
160+
BiConsumer<Object, Object> remove = mock(BiConsumer.class);
161+
ListenerHandle handle = ListenerHandleBuilder
162+
.from(NOT_NULL, NOT_NULL)
163+
.onAttach(NOT_NULL_CONSUMER)
164+
.onDetach(remove)
165+
.build();
166+
167+
// trigger a call to 'remove'
168+
handle.attach();
169+
handle.detach();
170+
171+
// verify
172+
verify(remove, times(1)).accept(NOT_NULL, NOT_NULL);
173+
verifyNoMoreInteractions(remove);
174+
}
175+
176+
// #end TESTS
177+
178+
}

0 commit comments

Comments
 (0)