Skip to content

Commit db16e12

Browse files
Legiothtaefimshabarov
authored
feat: Add high-level signal APIs (#21183)
Co-authored-by: Soroosh Taefi <taefi.soroosh@gmail.com> Co-authored-by: Mikhail Shabarov <61410877+mshabarov@users.noreply.github.com>
1 parent 76562fb commit db16e12

31 files changed

+6936
-17
lines changed

signals/src/main/java/com/vaadin/signals/ListSignal.java

Lines changed: 319 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,28 @@
1515
*/
1616
package com.vaadin.signals;
1717

18+
import java.util.List;
19+
import java.util.Objects;
20+
import java.util.function.Function;
21+
import java.util.function.Predicate;
22+
import java.util.stream.Collectors;
23+
24+
import com.vaadin.signals.Node.Data;
25+
import com.vaadin.signals.impl.SignalTree;
26+
import com.vaadin.signals.impl.SynchronousSignalTree;
27+
import com.vaadin.signals.operations.InsertOperation;
28+
import com.vaadin.signals.operations.SignalOperation;
29+
1830
/**
19-
* The rest of this class will be implemented later.
31+
* A signal containing a list of values. Supports atomic updates to the list
32+
* structure. Each value in the list is accessed as a separate
33+
* {@link ValueSignal} instance which enables atomic updates to the value of
34+
* that list entry.
35+
*
36+
* @param <T>
37+
* the element type
2038
*/
21-
public class ListSignal {
39+
public class ListSignal<T> extends Signal<List<ValueSignal<T>>> {
2240

2341
/**
2442
* A list insertion position before and/or after the referenced entries. If
@@ -60,5 +78,304 @@ public static ListPosition last() {
6078
// Before edge
6179
return new ListPosition(null, Id.EDGE);
6280
}
81+
82+
/**
83+
* Gets the insertion position immediately after the given signal.
84+
* Inserting after <code>null</code> is interpreted as inserting after
85+
* the start of the list, i.e. as the first child.
86+
*
87+
* @param after
88+
* the signal to insert after, or <code>null</code> to insert
89+
* first
90+
* @return a list position after the given signal, not <code>null</code>
91+
*/
92+
public static ListPosition after(Signal<?> after) {
93+
return new ListPosition(idOf(after), null);
94+
}
95+
96+
/**
97+
* Gets the insertion position immediately before the given signal.
98+
* Inserting before <code>null</code> signal is interpreted as inserting
99+
* before the end of the list, i.e. as the last child.
100+
*
101+
* @param before
102+
* the signal to insert before, or <code>null</code> to
103+
* insert last
104+
* @return a list position after the given signal, not <code>null</code>
105+
*/
106+
public static ListPosition before(Signal<?> before) {
107+
return new ListPosition(null, idOf(before));
108+
}
109+
110+
/**
111+
* Gets the insertion position between the given signals, assuming those
112+
* signals are currently adjacent. Inserting after <code>null</code> is
113+
* interpreted as inserting after the start of the list, i.e. as the
114+
* first child. Inserting before <code>null</code> signal is interpreted
115+
* as inserting before the end of the list, i.e. as the last child.
116+
*
117+
* @param after
118+
* the signal to insert after, or <code>null</code> to insert
119+
* first
120+
* @param before
121+
* the signal to insert before, or <code>null</code> to
122+
* insert last
123+
* @return a list position between the given signals, not
124+
* <code>null</code>
125+
*/
126+
public static ListPosition between(Signal<?> after, Signal<?> before) {
127+
return new ListPosition(idOf(after), idOf(before));
128+
}
129+
130+
private static Id idOf(Signal<?> signal) {
131+
if (signal == null) {
132+
return Id.EDGE;
133+
} else {
134+
return signal.id();
135+
}
136+
}
137+
}
138+
139+
private final Class<T> elementType;
140+
141+
/**
142+
* Creates a new list signal with the given element type. The signal does
143+
* not support clustering.
144+
*
145+
* @param elementType
146+
* the element type, not <code>null</code>
147+
*/
148+
public ListSignal(Class<T> elementType) {
149+
this(new SynchronousSignalTree(false), Id.ZERO, ANYTHING_GOES,
150+
elementType);
151+
}
152+
153+
/**
154+
* Creates a new list signal instance with the given id and validator for
155+
* the given signal tree with the given element type.
156+
*
157+
* @param tree
158+
* the signal tree that contains the value for this signal, not
159+
* <code>null</code>
160+
* @param id
161+
* the id of the signal node within the signal tree, not
162+
* <code>null</code>
163+
* @param validator
164+
* the validator to check operations submitted to this singal,
165+
* not <code>null</code>
166+
* @param elementType
167+
* the element type, not <code>null</code>
168+
*/
169+
protected ListSignal(SignalTree tree, Id id,
170+
Predicate<SignalCommand> validator, Class<T> elementType) {
171+
super(tree, id, validator);
172+
this.elementType = Objects.requireNonNull(elementType);
173+
}
174+
175+
private ValueSignal<T> child(Id childId) {
176+
return new ValueSignal<T>(tree(), childId, validator(), elementType);
177+
}
178+
179+
@Override
180+
protected List<ValueSignal<T>> extractValue(Data data) {
181+
if (data == null) {
182+
return List.of();
183+
} else {
184+
return children(data, this::child);
185+
}
186+
}
187+
188+
@Override
189+
protected Object usageChangeValue(Data data) {
190+
return data.listChildren();
191+
}
192+
193+
/**
194+
* Inserts a value as the first entry in this list.
195+
*
196+
* @param value
197+
* the value to insert
198+
* @return an operation containing a signal for the inserted entry and the
199+
* eventual result
200+
*/
201+
public InsertOperation<ValueSignal<T>> insertFirst(T value) {
202+
return insertAt(value, ListPosition.first());
203+
}
204+
205+
static <T extends Signal<?>> List<T> children(Data node,
206+
Function<Id, T> factory) {
207+
return node.listChildren().stream().map(factory).toList();
208+
}
209+
210+
/**
211+
* Inserts a value as the last entry in this list.
212+
*
213+
* @param value
214+
* the value to insert
215+
* @return an operation containing a signal for the inserted entry and the
216+
* eventual result
217+
*/
218+
public InsertOperation<ValueSignal<T>> insertLast(T value) {
219+
return insertAt(value, ListPosition.last());
63220
}
221+
222+
/**
223+
* Inserts a value at the given position in this list. The operation fails
224+
* if the position is not valid at the time when the operation is processed.
225+
*
226+
* @param value
227+
* the value to insert
228+
* @param at
229+
* the insert position, not <code>null</code>
230+
* @return an operation containing a signal for the inserted entry and the
231+
* eventual result
232+
*/
233+
public InsertOperation<ValueSignal<T>> insertAt(T value, ListPosition at) {
234+
return submitInsert(
235+
new SignalCommand.InsertCommand(Id.random(), id(), null,
236+
toJson(value), Objects.requireNonNull(at)),
237+
this::child);
238+
}
239+
240+
/**
241+
* Moves the given child signal to the given position in this list. The
242+
* operation fails if the child is not a child or if this list of if
243+
* position is not valid at the time when the operation is processed.
244+
*
245+
* @param child
246+
* the child signal to move, not <code>null</code>
247+
* @param to
248+
* the position to move to, not <code>null</code>
249+
* @return an operation containing the eventual result
250+
*/
251+
public SignalOperation<Void> moveTo(Signal<T> child, ListPosition to) {
252+
var verifyChild = new SignalCommand.PositionCondition(Id.random(), id(),
253+
child.id(), new ListPosition(null, null));
254+
var adopt = new SignalCommand.AdoptAtCommand(Id.random(), id(),
255+
child.id(), Objects.requireNonNull(to));
256+
257+
return submit(new SignalCommand.TransactionCommand(Id.random(),
258+
List.of(verifyChild, adopt)));
259+
}
260+
261+
/**
262+
* Removes the given child from this list. The operation fails if the child
263+
* is not a child of this list at the time when the operation is processed.
264+
*
265+
* @param child
266+
* the child to remove, not <code>null</code>
267+
* @return an operation containing the eventual result
268+
*/
269+
public SignalOperation<Void> remove(ValueSignal<T> child) {
270+
// Override to make public
271+
return super.remove(child);
272+
}
273+
274+
/**
275+
* Removes all children from this list. Note that is this list shares data
276+
* with a {@link NodeSignal} that has map children, then the map children
277+
* will also be removed.
278+
*
279+
* @return an operation containing the eventual result
280+
*/
281+
public SignalOperation<Void> clear() {
282+
// Override to make public
283+
return super.clear();
284+
}
285+
286+
/**
287+
* Checks that the given child is at the given position in this list. This
288+
* operation is only meaningful to use as a condition in a
289+
* {@link #runInTransaction(Runnable) transaction}. The result of the
290+
* returned operation will be resolved as successful if the given child is a
291+
* child of this list and at the given position when the operation is
292+
* processed.
293+
*
294+
* @param child
295+
* the child to test, not <code>null</code>
296+
* @param expectedPosition
297+
* the expected position of the child, not <code>null</code>
298+
* @return an operation containing the eventual result
299+
*/
300+
public SignalOperation<Void> verifyPosition(Signal<?> child,
301+
ListPosition expectedPosition) {
302+
return submit(new SignalCommand.PositionCondition(Id.random(), id(),
303+
child.id(), Objects.requireNonNull(expectedPosition)));
304+
}
305+
306+
/**
307+
* Checks that the given signal is a child in this list. This operation is
308+
* only meaningful to use as a condition in a
309+
* {@link #runInTransaction(Runnable) transaction}. The result of the
310+
* returned operation will be resolved as successful if the given child is a
311+
* child of this list and at the given position when the operation is
312+
* processed.
313+
*
314+
* @param child
315+
* the child to look for test, not <code>null</code>
316+
* @return an operation containing the eventual result
317+
*/
318+
public SignalOperation<Void> verifyChild(Signal<?> child) {
319+
return verifyPosition(child, new ListPosition(null, null));
320+
}
321+
322+
/**
323+
* Wraps this signal with a validator. The validator is used to check all
324+
* value changing commands issued through the new signal instance and all
325+
* child signals. If this signal has a validator, then the new signal will
326+
* use both validators. Note that due to the way validators are retained by
327+
* {@link #asNode()}, there's a possibility that the validator also receives
328+
* commands that cannot be directly issued for a list signal or its
329+
* children.
330+
* <p>
331+
* This signal will keep its current configuration and changes applied
332+
* through this instance will be visible through the wrapped instance.
333+
*
334+
* @param validator
335+
* the validator to use, not <code>null</code>
336+
* @return a new list signal that uses the validator, not <code>null</code>
337+
*/
338+
public ListSignal<T> withValidator(Predicate<SignalCommand> validator) {
339+
return new ListSignal<>(tree(), id(), mergeValidators(validator),
340+
elementType);
341+
}
342+
343+
/**
344+
* Wraps this signal to not accept changes. Child value signals retrieved
345+
* through the wrapped signal will also not accept changes.
346+
* <p>
347+
* This signal will keep its current configuration and changes applied
348+
* through this instance will be visible through the wrapped instance.
349+
*
350+
* @return the new readonly list signal, not <code>null</code>
351+
*/
352+
public ListSignal<T> asReadonly() {
353+
return withValidator(anything -> false);
354+
}
355+
356+
public NodeSignal asNode() {
357+
// Override to make public
358+
return super.asNode();
359+
}
360+
361+
@Override
362+
public boolean equals(Object obj) {
363+
return this == obj || obj instanceof ListSignal<?> other
364+
&& Objects.equals(tree(), other.tree())
365+
&& Objects.equals(id(), other.id())
366+
&& Objects.equals(validator(), other.validator())
367+
&& Objects.equals(elementType, other.elementType);
368+
}
369+
370+
@Override
371+
public int hashCode() {
372+
return Objects.hash(tree(), id(), validator(), elementType);
373+
}
374+
375+
@Override
376+
public String toString() {
377+
return peek().stream().map(ValueSignal::peek).map(Objects::toString)
378+
.collect(Collectors.joining(", ", "ListSignal[", "]"));
379+
}
380+
64381
}

0 commit comments

Comments
 (0)