From 95949da9c67d13f24c157b5b31fc9b56381747a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leif=20=C3=85strand?= Date: Fri, 17 Jan 2025 18:30:12 +0200 Subject: [PATCH 1/3] Add signal commands This is not yet a complete signal implementation but only the low-level core data manipulation logic. --- signals/pom.xml | 2 +- .../src/main/java/com/vaadin/signals/Id.java | 90 + .../java/com/vaadin/signals/ListSignal.java | 49 + .../main/java/com/vaadin/signals/Node.java | 74 + .../com/vaadin/signals/SignalCommand.java | 385 ++++ .../src/main/java/com/vaadin/signals/dummy | 0 .../signals/impl/MutableTreeRevision.java | 842 +++++++++ .../vaadin/signals/impl/OperationResult.java | 155 ++ .../com/vaadin/signals/impl/Snapshot.java | 41 + .../com/vaadin/signals/impl/TreeRevision.java | 190 ++ .../test/java/com/vaadin/signals/IdTest.java | 56 + .../src/test/java/com/vaadin/signals/dummy | 0 .../signals/impl/MutableTreeRevisionTest.java | 1652 +++++++++++++++++ .../signals/impl/OperationResultTest.java | 23 + .../com/vaadin/signals/impl/SnapshotTest.java | 51 + .../vaadin/signals/impl/TreeRevisionTest.java | 204 ++ 16 files changed, 3813 insertions(+), 1 deletion(-) create mode 100644 signals/src/main/java/com/vaadin/signals/Id.java create mode 100644 signals/src/main/java/com/vaadin/signals/ListSignal.java create mode 100644 signals/src/main/java/com/vaadin/signals/Node.java create mode 100644 signals/src/main/java/com/vaadin/signals/SignalCommand.java delete mode 100644 signals/src/main/java/com/vaadin/signals/dummy create mode 100644 signals/src/main/java/com/vaadin/signals/impl/MutableTreeRevision.java create mode 100644 signals/src/main/java/com/vaadin/signals/impl/OperationResult.java create mode 100644 signals/src/main/java/com/vaadin/signals/impl/Snapshot.java create mode 100644 signals/src/main/java/com/vaadin/signals/impl/TreeRevision.java create mode 100644 signals/src/test/java/com/vaadin/signals/IdTest.java delete mode 100644 signals/src/test/java/com/vaadin/signals/dummy create mode 100644 signals/src/test/java/com/vaadin/signals/impl/MutableTreeRevisionTest.java create mode 100644 signals/src/test/java/com/vaadin/signals/impl/OperationResultTest.java create mode 100644 signals/src/test/java/com/vaadin/signals/impl/SnapshotTest.java create mode 100644 signals/src/test/java/com/vaadin/signals/impl/TreeRevisionTest.java diff --git a/signals/pom.xml b/signals/pom.xml index c1a760be4b9..ee9a9ab7f53 100644 --- a/signals/pom.xml +++ b/signals/pom.xml @@ -21,7 +21,7 @@ com.fasterxml.jackson.core - jackson-core + jackson-databind ${jackson.version} diff --git a/signals/src/main/java/com/vaadin/signals/Id.java b/signals/src/main/java/com/vaadin/signals/Id.java new file mode 100644 index 00000000000..060a0bafa73 --- /dev/null +++ b/signals/src/main/java/com/vaadin/signals/Id.java @@ -0,0 +1,90 @@ +package com.vaadin.signals; + +import java.math.BigInteger; +import java.util.Base64; +import java.util.Base64.Encoder; +import java.util.concurrent.ThreadLocalRandom; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Generated identifier for signals and other related resources. + *

+ * The id is a random 64-bit number to be more compact than a full 128-bit UUID + * or such. The ids don't need to be globally unique but only unique within a + * smaller context so the risk of collisions is still negligible. The value is + * JSON serialized as a base64-encoded string with a special case, + * "", for the frequently used special 0 id. The ids are comparable + * to facilitate consistent ordering to avoid deadlocks in certain situations. + * + * @param value + * the id value as a 64-bit integer + */ +public record Id(long value) implements Comparable { + /** + * Default or initial id in various contexts. Always used for the root node + * in a signal hierarchy. The zero id is frequently used and has a custom + * compact JSON representation. + */ + public static final Id ZERO = new Id(0); + + /** + * Special id value reserved for internal bookkeeping. + */ + public static final Id MAX = new Id(Long.MAX_VALUE); + + /* + * Padding refers to the trailing = characters that are only necessary when + * base64 values are concatenated together + */ + private static final Encoder base64Encoder = Base64.getEncoder() + .withoutPadding(); + + public static Id random() { + var random = ThreadLocalRandom.current(); + + long value; + do { + value = random.nextLong(); + } while (value == 0 || value == Long.MAX_VALUE); + + return new Id(value); + } + + /** + * Parses the given base64 string as an id. As a special case, the empty + * string is parsed as {@link #ZERO}. + * + * @param base64 + * the base64 string to parse, not null + * @return the parsed id. + */ + @JsonCreator + public static Id parse(String base64) { + if (base64.equals("")) { + return ZERO; + } + byte[] bytes = Base64.getDecoder().decode(base64); + return new Id(new BigInteger(bytes).longValue()); + } + + /** + * Returns this id value as a base64 string. + * + * @return the base64 string representing this id + */ + @JsonValue + public final String asBase64() { + if (value == 0) { + return ""; + } + byte[] bytes = BigInteger.valueOf(value).toByteArray(); + return base64Encoder.encodeToString(bytes); + } + + @Override + public int compareTo(Id other) { + return Long.compare(value, other.value); + } +} \ No newline at end of file diff --git a/signals/src/main/java/com/vaadin/signals/ListSignal.java b/signals/src/main/java/com/vaadin/signals/ListSignal.java new file mode 100644 index 00000000000..cd1e406c1da --- /dev/null +++ b/signals/src/main/java/com/vaadin/signals/ListSignal.java @@ -0,0 +1,49 @@ +package com.vaadin.signals; + +/* + * The rest of this class will be implemented later. + */ +public class ListSignal { + + /** + * A list insertion position before and/or after the referenced entries. If + * both entries are defined, then this position represents an exact match + * that is valid only if the two entries are adjacent. If only one is + * defined, then the position is relative to only that position. A position + * with neither reference is not valid for inserts but it is valid to test a + * parent-child relationship regardless of the child position. + * {@link Id#ZERO} represents the edge of the list, i.e. the first or the + * last position. + * + * @param after + * id of the node to insert immediately after, nor + * null to not define a constraint + * @param before + * id of the node to insert immediately before, nor + * null to not define a constraint + */ + public record ListPosition(Id after, Id before) { + /** + * Gets the insertion position that corresponds to the beginning of the + * list. + * + * @return a list position for the beginning of the list, not + * null + */ + public static ListPosition first() { + // After edge + return new ListPosition(Id.ZERO, null); + } + + /** + * Gets the insertion position that corresponds to the end of the list. + * + * @return a list position for the end of the list, not + * null + */ + public static ListPosition last() { + // Before edge + return new ListPosition(null, Id.ZERO); + } + } +} diff --git a/signals/src/main/java/com/vaadin/signals/Node.java b/signals/src/main/java/com/vaadin/signals/Node.java new file mode 100644 index 00000000000..f7c57247ce3 --- /dev/null +++ b/signals/src/main/java/com/vaadin/signals/Node.java @@ -0,0 +1,74 @@ +package com.vaadin.signals; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.NullNode; + +/** + * A node in a signal tree. Each node represents as signal entry. Nodes are + * immutable and referenced by an {@link Id} rather than directly referencing + * the node instance. The node is either a {@link Data} node carrying actual + * signal data or an {@link Alias} node that allows multiple signal ids to + * reference the same data. + */ +public sealed interface Node { + + /** + * An empty data node without parent, scope owner, value or children and the + * initial last update id. + */ + public static final Data EMPTY = new Data(null, Id.ZERO, null, null, + List.of(), Map.of()); + + /** + * A node alias. An alias node allows multiple signal ids to reference the + * same data. + * + * @param target + * the id of the alias target, not null + */ + public record Alias(Id target) implements Node { + } + + /** + * A data node. The node represents the actual data behind a signal + * instance. + * + * @param parent + * the parent id, or null for the root node + * @param lastUpdate + * a unique id for the update that last updated this data node, + * not null + * @param scopeOwner + * the id of the external owner of this node, or + * null if the node has no owner. Any node with an + * owner is deleted if the owner is disconnected. + * @param value + * the JSON value of this node, or null if there is + * no value + * @param listChildren + * a list of child ids, or the an list if the node has no list + * children + * @param mapChildren + * a sequenced map from key to child id, or an empty map if the + * node has no map children + */ + public record Data(Id parent, Id lastUpdate, Id scopeOwner, JsonNode value, + List listChildren, + Map mapChildren) implements Node { + public Data { + Objects.requireNonNull(lastUpdate); + + /* + * Avoid accidentally making a distinction between the two different + * nulls that will look the same after JSON deserialization + */ + if (value instanceof NullNode) { + value = null; + } + } + } +} \ No newline at end of file diff --git a/signals/src/main/java/com/vaadin/signals/SignalCommand.java b/signals/src/main/java/com/vaadin/signals/SignalCommand.java new file mode 100644 index 00000000000..cfc18ffd912 --- /dev/null +++ b/signals/src/main/java/com/vaadin/signals/SignalCommand.java @@ -0,0 +1,385 @@ +package com.vaadin.signals; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.databind.JsonNode; +import com.vaadin.signals.ListSignal.ListPosition; + +/** + * A command triggered from a signal. + */ +public sealed interface SignalCommand { + /** + * A signal command that sets the value of a signal. + */ + sealed interface ValueCommand extends SignalCommand { + /** + * Gets the JSON node with the value to set. + * + * @return the JSON value, or null + */ + JsonNode value(); + } + + /** + * A signal command that targets a map entry by key. + */ + sealed interface KeyCommand extends SignalCommand { + /** + * Gets the targeted map key. + * + * @return the map key, not null + */ + String key(); + } + + /** + * A signal command that doesn't apply any change but only performs a test + * that will be part of determining whether a transaction passes. + */ + sealed interface TestCommand extends SignalCommand { + } + + /** + * A signal command that creates a new signal node that might have an owner. + * The created node will be automatically removed if the owner is + * disconnected. + */ + sealed interface ScopeOwnerCommand extends SignalCommand { + /** + * The owner id. + * + * @return the owner id, nor null if the created signal has + * no scope owner + */ + Id scopeOwner(); + } + + /** + * A signal command that doesn't target a specific node. + */ + sealed interface GlobalCommand extends SignalCommand { + @Override + default Id nodeId() { + return Id.ZERO; + } + } + + /** + * Tests whether the given node has the expected value, based on JSON + * equality. + * + * @param commandId + * the unique command id used to track the status of this + * command, not null + * @param nodeId + * id of the node to check, not null + * @param expecedValue + * the expected value + */ + public record ValueTest(Id commandId, Id nodeId, + JsonNode expectedValue) implements TestCommand { + } + + /** + * Tests whether the given node has a given child at a given position. + * + * @param commandId + * the unique command id used to track the status of this + * command, not null + * @param nodeId + * id of the parent node to check, not null + * @param childId + * the id of the child to check for, not null + * @param position + * the list position to use for optionally checking whether the + * child has the expected siblings + */ + public record PositionTest(Id commandId, Id nodeId, Id childId, + ListPosition position) implements TestCommand { + } + + /** + * Tests whether the given node has the expected child for a specific map + * key. + * + * @param commandId + * the unique command id used to track the status of this + * command, not null + * @param nodeId + * id of the parent node to check, not null + * @param key + * the key to check, not null + * @param expectedChild + * the child id to test for, or null to check that + * any child is present, or Id.ZERO to test that no + * child is present + */ + public record KeyTest(Id commandId, Id nodeId, String key, + Id expectedChild) implements TestCommand { + } + + /** + * Tests that the given node was last updated by the command with the given + * id. + * + * @param commandId + * the unique command id used to track the status of this + * command, not null + * @param nodeId + * id of the node to check, not null + * @param expectedLastUpdate + * the expected id of the command hat last updated this node, not + * null + */ + public record LastUpdateTest(Id commandId, Id nodeId, + Id expectedLastUpdate) implements TestCommand { + } + + /** + * Adopts the given node as a child with the given key. The child must + * already be attached somewhere in the same tree and it cannot be an + * ancestor of its new parent. There cannot be an existing child with the + * same key. The child is detached from its previous parent. + * + * @param commandId + * the unique command id used to track the status of this + * command, not null + * @param nodeId + * id of the parent node to adopt to, not null + * @param childId + * id of the child node to adopt, not null + * @param key + * key to adopt the node as, not null + */ + public record AdoptAsCommand(Id commandId, Id nodeId, Id childId, + String key) implements KeyCommand { + } + + /** + * Adopts the given node as a child at the given insertion position. The + * child must already be attached somewhere in the same tree and it cannot + * be an ancestor of its new parent. The child is detached from its previous + * parent. + * + * @param commandId + * the unique command id used to track the status of this + * command, not null + * @param nodeId + * id of the parent node to adopt to, not null + * @param childId + * id of the child node to adopt, not null + * @param position + * the list insert position to insert into, not null + */ + public record AdoptAtCommand(Id commandId, Id nodeId, Id childId, + ListPosition position) implements SignalCommand { + } + + /** + * Increments the value of the given node by the given delta. The node must + * have a numerical value. If the node has no value at all, then 0 is used + * as the previous value. A negative delta value leads to decrementing the + * value. + * + * @param commandId + * the unique command id used to track the status of this + * command, not null + * @param nodeId + * id of the node to update, not null + * @param delta + * a double value to increment by + */ + public record IncrementCommand(Id commandId, Id nodeId, + double delta) implements SignalCommand { + } + + /** + * Removes all children from the target node. + * + * @param commandId + * the unique command id used to track the status of this + * command, not null + * @param nodeId + * id of the node to update, not null + */ + public record ClearCommand(Id commandId, + Id nodeId) implements SignalCommand { + } + + /** + * Removes the child with the given key, if present. + * + * @param commandId + * the unique command id used to track the status of this + * command, not null + * @param nodeId + * id of the node to update, not null + * @param key + * the key to remove, not null + */ + public record RemoveByKeyCommand(Id commandId, Id nodeId, + String key) implements KeyCommand { + } + + /** + * Stores the given value in a child node with the given key. If a node + * already exists, then its value is updated. If no node exists, then a new + * node is created. + * + * @param commandId + * the unique command id used to track the status of this + * command, not null + * @param nodeId + * id of the parent node to update, not null + * @param key + * the key to update, not null + * @param value + * the value to set + */ + public record PutCommand(Id commandId, Id nodeId, String key, + JsonNode value) implements ValueCommand, KeyCommand { + } + + /** + * Stores the given value in a child node with the given key if it doesn't + * already exist. If the key exists, then the value is not updated but a new + * alias is created to reference the existing entry. If the key doesn't + * exist, then a new node is created to hold the value. + * + * @param commandId + * the unique command id used to track the status of this + * command, not null. Also used as the node id of + * the newly created node. + * @param nodeId + * id of the parent node to update, not null + * @param key + * the key to update, not null + * @param value + * the value to set if a mapping didn't already exist + */ + public record PutIfAbsentCommand(Id commandId, Id nodeId, Id scopeOwner, + String key, JsonNode value) + implements + ValueCommand, + KeyCommand, + ScopeOwnerCommand { + } + + /** + * Inserts a new node with the given value at the given list insert + * position. + * + * @param commandId + * the unique command id used to track the status of this + * command, not null. Also used as the node id of + * the newly created node. + * @param nodeId + * id of the parent node to update, not null + * @param value + * the value to set if a mapping didn't already exist + * @param position + * the list insert position, not null + */ + record InsertCommand(Id commandId, Id nodeId, Id scopeOwner, JsonNode value, + ListSignal.ListPosition position) + implements + ValueCommand, + ScopeOwnerCommand { + } + + /** + * Sets the value of the given node. + * + * @param commandId + * the unique command id used to track the status of this + * command, not null + * @param nodeId + * id of the node to update, not null + * @param value + * the value to set + */ + record SetCommand(Id commandId, Id nodeId, + JsonNode value) implements ValueCommand { + } + + /** + * Removes the given node from its parent, optionally verifying that the + * parent is as expected. + * + * @param commandId + * the unique command id used to track the status of this + * command, not null + * @param nodeId + * id of the node to remove, not null + * @param expectedParentId + * the expected parent node id, or null to not + * verify the parent + */ + record RemoveCommand(Id commandId, Id nodeId, + Id expectedParentId) implements SignalCommand { + } + + /** + * Removes all nodes that have its scope owner set as the given id. + * + * @param commandId + * the unique command id used to track the status of this + * command, not null + * @param ownerId + * the scope owner id to look for, not null + */ + record ClearOwnerCommand(Id commandId, + Id ownerId) implements GlobalCommand { + } + + /** + * A sequence of commands that should be applied atomically and only if all + * commands are individually accepted. + * + * @param commandId + * the unique command id used to track the status of this + * command, not null + * @param commands + * the list of commands to apply, not null + */ + record TransactionCommand(Id commandId, + List commands) implements GlobalCommand { + } + + /** + * Initializes a tree based on a collection of pre-existing nodes. + * + * @param commandId + * the unique command id used to track the status of this + * command, not null + * @param nodes + * a map from node id to nodes to use, not null + */ + record SnapshotCommand(Id commandId, + Map nodes) implements GlobalCommand { + } + + /** + * Gets the unique command id used to track the status of this command. For + * commands that creates a new node, the command id is also used as the node + * id of the created node. + * + * @return the unique command id used to track the status of this command, + * not null + */ + Id commandId(); + + /** + * Gets the id of the signal node that is targeted by this command. Some + * commands might target multiple nodes e.g. in a parent-child relationship + * and in that case this node is the primary node. Some commands, + * implementing {@link GlobalCommand} do not target any specific node and + * for those commands, {@link Id#ZERO} is used as the node id. + * + * @return id of the primary node targeted by this command, not + * null + */ + Id nodeId(); +} diff --git a/signals/src/main/java/com/vaadin/signals/dummy b/signals/src/main/java/com/vaadin/signals/dummy deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/signals/src/main/java/com/vaadin/signals/impl/MutableTreeRevision.java b/signals/src/main/java/com/vaadin/signals/impl/MutableTreeRevision.java new file mode 100644 index 00000000000..9248fa9dd48 --- /dev/null +++ b/signals/src/main/java/com/vaadin/signals/impl/MutableTreeRevision.java @@ -0,0 +1,842 @@ +package com.vaadin.signals.impl; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.UnaryOperator; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.DoubleNode; +import com.fasterxml.jackson.databind.node.NullNode; +import com.fasterxml.jackson.databind.node.NumericNode; +import com.vaadin.signals.Id; +import com.vaadin.signals.ListSignal.ListPosition; +import com.vaadin.signals.Node; +import com.vaadin.signals.Node.Alias; +import com.vaadin.signals.Node.Data; +import com.vaadin.signals.SignalCommand; +import com.vaadin.signals.SignalCommand.AdoptAsCommand; +import com.vaadin.signals.SignalCommand.AdoptAtCommand; +import com.vaadin.signals.SignalCommand.ClearCommand; +import com.vaadin.signals.SignalCommand.ClearOwnerCommand; +import com.vaadin.signals.SignalCommand.IncrementCommand; +import com.vaadin.signals.SignalCommand.InsertCommand; +import com.vaadin.signals.SignalCommand.KeyTest; +import com.vaadin.signals.SignalCommand.TestCommand; +import com.vaadin.signals.SignalCommand.LastUpdateTest; +import com.vaadin.signals.SignalCommand.PositionTest; +import com.vaadin.signals.SignalCommand.PutCommand; +import com.vaadin.signals.SignalCommand.PutIfAbsentCommand; +import com.vaadin.signals.SignalCommand.RemoveByKeyCommand; +import com.vaadin.signals.SignalCommand.RemoveCommand; +import com.vaadin.signals.SignalCommand.ScopeOwnerCommand; +import com.vaadin.signals.SignalCommand.SetCommand; +import com.vaadin.signals.SignalCommand.SnapshotCommand; +import com.vaadin.signals.SignalCommand.TransactionCommand; +import com.vaadin.signals.SignalCommand.ValueTest; +import com.vaadin.signals.impl.OperationResult.Accept; +import com.vaadin.signals.impl.OperationResult.Reject; +import com.vaadin.signals.impl.OperationResult.TreeModification; + +/** + * A tree revision that can be mutated by applying signal commands. + */ +public class MutableTreeRevision extends TreeRevision { + /** + * Gathers and collects all state related to applying a single command. With + * transactions, previously applied commands might end up rolled back if a + * later command in the transaction is rejected. To deal with this, changes + * are applied by collecting a set of changes so that later commands are + * evaluated against the already collected changes. The same structure also + * helps decompose complex single operations into individually evaluated + * steps. + */ + private class TreeManipulator { + private final Map updatedNodes = new HashMap<>(); + private final Set detachedNodes = new HashSet<>(); + private final Map originalInserts = new HashMap<>(); + + private final SignalCommand command; + + /** + * The operation result is tracked in an instance field to allow helper + * methods to optionally set a result while also returning a regular + * value. + */ + private OperationResult result; + + /** + * Child results are collected for transactions and applied at the end + * since the result of earlier operations might change if a later + * operation is rejected. + */ + private Map childResults; + + public TreeManipulator(SignalCommand command) { + this.command = command; + } + + private void setResult(OperationResult result) { + assert this.result == null; + this.result = result; + } + + private void fail(String reason) { + setResult(OperationResult.fail(reason)); + } + + private Id resolveAlias(Id nodeId) { + Node dataOrAlias = updatedNodes.get(nodeId); + if (dataOrAlias == null) { + dataOrAlias = nodes().get(nodeId); + } + + if (dataOrAlias instanceof Alias alias) { + return alias.target(); + } else { + return nodeId; + } + } + + private Optional data(Id nodeId) { + Id id = resolveAlias(nodeId); + + if (detachedNodes.contains(id)) { + return Optional.empty(); + } else if (updatedNodes.containsKey(id)) { + return Optional.ofNullable((Data) updatedNodes.get(id)); + } else { + return MutableTreeRevision.this.data(id); + } + } + + private void useData(Id nodeId, BiConsumer consumer) { + assert result == null; + + Id id = resolveAlias(nodeId); + data(id).ifPresentOrElse(node -> consumer.accept(node, id), () -> { + fail("Node not found"); + }); + } + + private void updateData(Id nodeId, UnaryOperator updater) { + useData(nodeId, (node, id) -> { + Data updatedNode = updater.apply(node); + if (updatedNode != node) { + updatedNodes.put(id, updatedNode); + } + }); + } + + private JsonNode value(Id nodeId) { + return data(nodeId).map(Data::value).orElse(null); + } + + private void setValue(Id nodeId, JsonNode value) { + updateData(nodeId, + node -> new Data(node.parent(), command.commandId(), + node.scopeOwner(), value, node.listChildren(), + node.mapChildren())); + } + + private Optional> listChildren(Id parentId) { + return data(parentId).map(Data::listChildren); + } + + private boolean isChildAt(Id parentId, int index, Id expectedChild) { + assert expectedChild != null; + + if (index < 0) { + return false; + } + + Id idAtIndex = listChildren(parentId).map(children -> { + if (index >= children.size()) { + return null; + } + return children.get(index); + }).orElse(null); + + return isSameNode(idAtIndex, expectedChild); + } + + private Optional mapChild(Id nodeId, String key) { + return data(nodeId).map(Data::mapChildren) + .map(children -> children.get(key)); + } + + private boolean isSameNode(Id a, Id b) { + return Objects.equals(resolveAlias(a), resolveAlias(b)); + } + + private boolean detach(Id nodeId) { + useData(nodeId, (node, id) -> { + if (id.equals(Id.ZERO)) { + fail("Cannot detach the root"); + return; + } + + Id parentId = node.parent(); + if (parentId == null) { + fail("Node is not attached"); + return; + } + + Data parentData = data(parentId).get(); + + String key = parentData.mapChildren().entrySet().stream() + .filter(entry -> entry.getValue().equals(id)) + .map(Entry::getKey).findAny().orElse(null); + + if (key != null) { + updatedNodes.put(parentId, updateMapChildren(parentData, + map -> map.remove(key))); + } else { + updatedNodes.put(parentId, updateListChildren(parentData, + list -> list.remove(id))); + } + + detachedNodes.add(id); + }); + + // Check if any error was reported + return result == null; + } + + private Data updateMapChildren(Data node, + Consumer> mapUpdater) { + LinkedHashMap map = new LinkedHashMap<>( + node.mapChildren()); + mapUpdater.accept(map); + + return new Data(node.parent(), command.commandId(), + node.scopeOwner(), node.value(), node.listChildren(), + Collections.unmodifiableMap(map)); + } + + private Data updateListChildren(Data node, + Consumer> listUpdater) { + ArrayList list = new ArrayList<>(node.listChildren()); + listUpdater.accept(list); + + return new Data(node.parent(), command.commandId(), + node.scopeOwner(), node.value(), + Collections.unmodifiableList(list), node.mapChildren()); + } + + private void attach(Id parentId, Id childId, + BiFunction attacher) { + if (result != null) { + return; + } + + Id resolvedParentId = resolveAlias(parentId); + Id resolvedChildId = resolveAlias(childId); + + if (!detachedNodes.contains(resolvedChildId)) { + fail("Node is not detached"); + return; + } + + Id ancestor = resolvedParentId; + while (ancestor != null) { + if (ancestor.equals(childId)) { + fail("Cannot attach to own descendant"); + return; + } + + ancestor = data(ancestor).map(Data::parent).orElse(null); + } + + useData(parentId, (node, id) -> { + detachedNodes.remove(resolvedChildId); + + Data updated = attacher.apply(node, resolvedChildId); + if (result == null) { + Data child = data(resolvedChildId).get(); + + updatedNodes.put(id, updated); + updatedNodes.put(resolvedChildId, + new Data(id, child.lastUpdate(), child.scopeOwner(), + child.value(), child.listChildren(), + child.mapChildren())); + } + }); + } + + private void attachAs(Id parentId, String key, Id childId) { + attach(parentId, childId, (parentNode, resolvedChildId) -> { + return updateMapChildren(parentNode, map -> { + Id previous = map.putIfAbsent(key, resolvedChildId); + if (previous != null) { + fail("Key is in use"); + } + }); + }); + } + + private int findInsertIndex(List children, + ListPosition insertPosition) { + Id after = resolveAlias(insertPosition.after()); + Id before = resolveAlias(insertPosition.before()); + + if (after != null) { + int position; + if (after.equals(Id.ZERO)) { + // After edge -> insert first + position = 0; + } else { + int indexOf = children.indexOf(after); + if (indexOf == -1) { + return -1; + } + position = indexOf + 1; + } + + // Validate before constraint if there is one + if (before != null) { + Id atPosition = position < children.size() + ? children.get(position) + : Id.ZERO; + if (!atPosition.equals(before)) { + return -1; + } + } + + return position; + } else { + // Invalid to not define any position + if (before == null) { + return -1; + } + + // Before edge -> insert last + if (before.equals(Id.ZERO)) { + return children.size(); + } + + return children.indexOf(before); + } + } + + private void attachAt(Id parentId, ListPosition position, Id childId) { + attach(parentId, childId, (node, resolvedChildId) -> { + int insertIndex = findInsertIndex(node.listChildren(), + position); + if (insertIndex == -1) { + fail("Insert position not matched"); + return null; + } + + return updateListChildren(node, + list -> list.add(insertIndex, resolvedChildId)); + }); + } + + private void createNode(Id nodeId, JsonNode value, Id scopeOwner) { + if (data(nodeId).isPresent()) { + fail("Node already exists"); + return; + } + + // Mark as detached to make it eligible for attaching + detachedNodes.add(nodeId); + updatedNodes.put(nodeId, new Data(null, command.commandId(), + scopeOwner, value, List.of(), Map.of())); + + if (ownerId().equals(scopeOwner)) { + originalInserts.put(nodeId, (ScopeOwnerCommand) command); + } + } + + private TreeModification createUpdate(Id id, Node newNode) { + Data original = MutableTreeRevision.this.data(id).orElse(null); + return new TreeModification(original, newNode); + } + + private static Map, BiConsumer> handlers = new HashMap<>(); + + private static void addHandler( + Class commandType, BiConsumer handler) { + handlers.put(commandType, handler); + } + + private static void addTestHandler( + Class commandType, + BiFunction handler) { + addHandler(commandType, (manipulator, command) -> manipulator + .setResult(handler.apply(manipulator, command))); + } + + static { + addTestHandler(ValueTest.class, TreeManipulator::handleValueTest); + addTestHandler(PositionTest.class, + TreeManipulator::handlePositionTest); + addTestHandler(KeyTest.class, TreeManipulator::handleKeyTest); + addTestHandler(LastUpdateTest.class, + TreeManipulator::handleLastUpdateTest); + + addHandler(AdoptAsCommand.class, TreeManipulator::handleAdoptAs); + addHandler(AdoptAtCommand.class, TreeManipulator::handleAdoptAt); + addHandler(IncrementCommand.class, + TreeManipulator::handleIncrement); + addHandler(ClearCommand.class, TreeManipulator::handleClear); + addHandler(RemoveByKeyCommand.class, + TreeManipulator::handleRemoveByKey); + addHandler(PutCommand.class, TreeManipulator::handlePut); + addHandler(PutIfAbsentCommand.class, + TreeManipulator::handlePutIfAbsent); + addHandler(InsertCommand.class, TreeManipulator::handleInsert); + addHandler(SetCommand.class, TreeManipulator::handleSet); + addHandler(RemoveCommand.class, TreeManipulator::handleRemove); + addHandler(ClearOwnerCommand.class, + TreeManipulator::handleClearOwner); + addHandler(TransactionCommand.class, + TreeManipulator::handleTransaction); + addHandler(SnapshotCommand.class, TreeManipulator::handleSnapshot); + } + + public OperationResult handleCommand(SignalCommand command) { + @SuppressWarnings("unchecked") + BiConsumer handler = (BiConsumer) handlers + .get(command.getClass()); + + handler.accept(this, command); + + if (result != null) { + return result; + } + + Map updates = new HashMap<>(); + + updatedNodes.forEach((id, newNode) -> { + if (!detachedNodes.contains(id)) { + updates.put(id, createUpdate(id, newNode)); + } + }); + + if (!detachedNodes.isEmpty()) { + Map> reverseAliases = new HashMap<>(); + nodes().forEach((signalId, nodeOrAlias) -> { + if (nodeOrAlias instanceof Alias alias) { + reverseAliases + .computeIfAbsent(alias.target(), + ignore -> new ArrayList<>()) + .add(signalId); + } + }); + + LinkedList toDetach = new LinkedList<>(detachedNodes); + while (!toDetach.isEmpty()) { + Id removed = toDetach.removeLast(); + updates.put(removed, createUpdate(removed, null)); + + reverseAliases.getOrDefault(removed, List.of()) + .forEach(aliasToRemove -> updates.put(aliasToRemove, + createUpdate(aliasToRemove, null))); + + Data node = MutableTreeRevision.this.data(removed).get(); + toDetach.addAll(node.listChildren()); + toDetach.addAll(node.mapChildren().values()); + } + } + + return new Accept(updates, originalInserts); + } + + private OperationResult handleValueTest(ValueTest test) { + JsonNode value = value(test.nodeId()); + if (value == null) { + value = NullNode.getInstance(); + } + JsonNode expectedValue = test.expectedValue(); + + if (expectedValue == null) { + expectedValue = NullNode.getInstance(); + } + + if (!value.equals(expectedValue)) { + return OperationResult.fail("Unexpected value"); + } else { + return OperationResult.ok(); + } + } + + private OperationResult handlePositionTest(PositionTest test) { + Id nodeId = test.nodeId(); + Id resolvedChild = resolveAlias(test.childId()); + + int indexOf = listChildren(nodeId) + .map(list -> list.indexOf(resolvedChild)) + .orElseGet(() -> Integer.valueOf(-1)); + + if (indexOf == -1) { + return OperationResult.fail("Not a child"); + } + + ListPosition position = test.position(); + + Id after = position.after(); + if (after != null) { + if (after.equals(Id.ZERO)) { + if (indexOf != 0) { + return OperationResult.fail("Not the first child"); + } + } else { + if (!isChildAt(nodeId, indexOf - 1, after)) { + return OperationResult + .fail("Not after the provided child"); + } + } + } + + Id before = position.before(); + if (before != null) { + if (before.equals(Id.ZERO)) { + int childCount = listChildren(nodeId).map(List::size) + .orElse(0); + if (indexOf != childCount - 1) { + return OperationResult.fail("Not the last child"); + } + } else { + if (!isChildAt(nodeId, indexOf + 1, before)) { + return OperationResult + .fail("Not before the provided child"); + } + } + } + + return OperationResult.ok(); + } + + private OperationResult handleKeyTest(KeyTest keyTest) { + Id nodeId = keyTest.nodeId(); + String key = keyTest.key(); + Id expectedChild = keyTest.expectedChild(); + + Id actualChildId = mapChild(nodeId, key).orElse(null); + if (expectedChild == null) { + return OperationResult.test(actualChildId != null, + "Key not present"); + } else if (Id.ZERO.equals(expectedChild)) { + return OperationResult.test(actualChildId == null, + "A key is present"); + } else { + return OperationResult.test( + isSameNode(actualChildId, expectedChild), + "Unexpected child"); + } + } + + private OperationResult handleLastUpdateTest( + LastUpdateTest lastUpdateTest) { + Id lastUpdate = data(lastUpdateTest.nodeId()).map(Data::lastUpdate) + .orElse(null); + + return OperationResult.test( + Objects.equals(lastUpdate, + lastUpdateTest.expectedLastUpdate()), + "Unexpected last update"); + } + + private void handleAdoptAs(AdoptAsCommand adoptAs) { + Id nodeId = adoptAs.nodeId(); + String key = adoptAs.key(); + Id childId = adoptAs.childId(); + + if (detach(childId)) { + attachAs(nodeId, key, childId); + } + } + + private void handleAdoptAt(AdoptAtCommand adoptAt) { + Id nodeId = adoptAt.nodeId(); + ListPosition position = adoptAt.position(); + Id childId = adoptAt.childId(); + + if (detach(childId)) { + attachAt(nodeId, position, childId); + } + } + + private void handleIncrement(IncrementCommand increment) { + Id nodeId = increment.nodeId(); + double delta = increment.delta(); + + JsonNode oldValue = value(nodeId); + + double newValue; + if (oldValue instanceof NumericNode value) { + newValue = value.doubleValue() + delta; + } else if (oldValue == null || oldValue instanceof NullNode) { + newValue = delta; + } else { + fail("Value is not numeric"); + return; + } + + setValue(nodeId, new DoubleNode(newValue)); + } + + private void handleClear(ClearCommand clear) { + updateData(clear.nodeId(), node -> { + assert detachedNodes.isEmpty(); + detachedNodes.addAll(node.listChildren()); + detachedNodes.addAll(node.mapChildren().values()); + + if (detachedNodes.isEmpty()) { + return node; + } + + return new Data(node.parent(), command.commandId(), + node.scopeOwner(), node.value(), List.of(), Map.of()); + }); + } + + private void handleRemoveByKey(RemoveByKeyCommand removeByKey) { + mapChild(removeByKey.nodeId(), removeByKey.key()).ifPresentOrElse( + this::detach, () -> fail("Key not present")); + } + + private void handlePut(PutCommand put) { + Id commandId = put.commandId(); + Id nodeId = put.nodeId(); + String key = put.key(); + JsonNode value = put.value(); + + mapChild(nodeId, key).ifPresentOrElse(childId -> { + setValue(childId, value); + }, () -> { + createNode(commandId, value, null); + attachAs(nodeId, key, commandId); + }); + } + + private void handlePutIfAbsent(PutIfAbsentCommand putIfAbsent) { + Id commandId = putIfAbsent.commandId(); + Id nodeId = putIfAbsent.nodeId(); + String key = putIfAbsent.key(); + + mapChild(nodeId, key).ifPresentOrElse(childId -> { + if (data(commandId).isPresent()) { + fail("Node already exists"); + return; + } + + updatedNodes.put(commandId, new Alias(resolveAlias(childId))); + }, () -> { + createNode(commandId, putIfAbsent.value(), + putIfAbsent.scopeOwner()); + attachAs(nodeId, key, commandId); + }); + } + + private void handleInsert(InsertCommand insert) { + Id commandId = insert.commandId(); + + createNode(commandId, insert.value(), insert.scopeOwner()); + attachAt(insert.nodeId(), insert.position(), commandId); + } + + private void handleSet(SetCommand set) { + setValue(set.nodeId(), set.value()); + } + + private void handleRemove(RemoveCommand remove) { + Id nodeId = remove.nodeId(); + Id expectedParentId = remove.expectedParentId(); + + if (expectedParentId != null) { + Id parentId = data(nodeId).map(Data::parent).orElse(null); + + if (!isSameNode(expectedParentId, parentId)) { + fail("Not a child"); + return; + } + } + + detach(nodeId); + } + + private void handleClearOwner(ClearOwnerCommand clearOwner) { + Id ownerId = clearOwner.ownerId(); + + // TODO clear originalInserts that have been removed previously? + nodes().forEach((id, nodeOrAlias) -> { + if (nodeOrAlias instanceof Data node + && ownerId.equals(node.scopeOwner())) { + detach(id); + } + }); + } + + private void handleTransaction(TransactionCommand transaction) { + List commands = transaction.commands(); + + MutableTreeRevision child = new MutableTreeRevision( + MutableTreeRevision.this); + + childResults = new HashMap(); + + Reject firstReject = null; + for (SignalCommand command : commands) { + child.apply(command, childResults::put); + + OperationResult childResult = childResults + .get(command.commandId()); + if (childResult instanceof Reject reject) { + firstReject = reject; + break; + } + } + + if (firstReject == null) { + Map updates = new HashMap<>(); + Map originalInserts = new HashMap<>(); + + // Iterate the command list to preserve order + for (SignalCommand command : commands) { + Accept op = (Accept) childResults.get(command.commandId()); + op.updates().forEach((nodeId, modification) -> { + if (updates.containsKey(nodeId)) { + updates.put(nodeId, + new TreeModification( + updates.get(nodeId).oldNode(), + modification.newNode())); + } else { + updates.put(nodeId, modification); + } + }); + + originalInserts.putAll(op.originalInserts()); + } + + setResult(new Accept(updates, originalInserts)); + } else { + for (SignalCommand command : commands) { + OperationResult originalResult = childResults + .get(command.commandId()); + if (originalResult == null + || originalResult instanceof Accept) { + childResults.put(command.commandId(), firstReject); + } + } + + setResult(firstReject); + } + } + + private void handleSnapshot(SnapshotCommand snapshot) { + /* + * We will have to add support for applying a snapshot to a + * non-empty tree if we implement re-synchronization based on + * snapshots. + */ + assert updatedNodes.isEmpty(); + assert detachedNodes.isEmpty(); + + updatedNodes.putAll(snapshot.nodes()); + } + } + + /** + * Creates a new mutable tree revision as a copy of the provided base + * revision. + * + * @param base + * the base revision to copy, not null + */ + public MutableTreeRevision(TreeRevision base) { + super(base.ownerId(), new HashMap<>(base.nodes()), + new HashMap<>(base.originalInserts())); + } + + /** + * Applies a sequence of commands and collects the results to a map. + * + * @param commands + * the list of commands to apply, not null + * @return a map from command id to operation results, not null + */ + public Map applyAndGetResults( + List commands) { + Map results = new HashMap<>(); + + for (SignalCommand command : commands) { + apply(command, results::put); + } + + return results; + } + + /** + * Applies a sequence of commands and ignores the results. + * + * @param commands + * the list of commands to apply, not null + */ + public void apply(List commands) { + for (SignalCommand command : commands) { + apply(command, null); + } + } + + /** + * Applies a single command and passes the results to the provided handler. + * Note that the handler will be invoked exactly once for most types of + * commands but it will be invoked multiple times for transactions. + * + * @param command + * the command to apply, not null + * @param resultHandler + * the result handler callback, or null to ignore + * results + */ + public void apply(SignalCommand command, + BiConsumer resultHandler) { + OperationResult result; + if (data(command.nodeId()).isPresent()) { + TreeManipulator manipulator = new TreeManipulator(command); + result = manipulator.handleCommand(command); + + if (manipulator.childResults != null && resultHandler != null) { + manipulator.childResults.forEach(resultHandler); + } + } else { + result = OperationResult.fail("Node not found"); + } + + if (result instanceof Accept accept) { + accept.updates().forEach((nodeId, update) -> { + Node newNode = update.newNode(); + + if (newNode == null) { + nodes().remove(nodeId); + originalInserts().remove(nodeId); + } else { + nodes().put(nodeId, newNode); + } + }); + + originalInserts().putAll(accept.originalInserts()); + } + + if (resultHandler != null) { + resultHandler.accept(command.commandId(), result); + } + + assert assertValidTree(); + } +} diff --git a/signals/src/main/java/com/vaadin/signals/impl/OperationResult.java b/signals/src/main/java/com/vaadin/signals/impl/OperationResult.java new file mode 100644 index 00000000000..2afcbb8c3e3 --- /dev/null +++ b/signals/src/main/java/com/vaadin/signals/impl/OperationResult.java @@ -0,0 +1,155 @@ +package com.vaadin.signals.impl; + +import java.util.HashMap; +import java.util.Map; + +import com.vaadin.signals.Id; +import com.vaadin.signals.Node; +import com.vaadin.signals.Node.Data; +import com.vaadin.signals.SignalCommand; + +/** + * The result of applying a signal command as an operation against a tree + * revision. The result is either to accept or reject the operation. + * + * @see SignalCommand + * @see TreeRevision + */ +public sealed interface OperationResult { + /** + * A data node update in an accepted operation result. + * + * @param oldNode + * the old node instance, or null if the operation + * created a new node + * @param newNode + * the new node instance or null if the operation removed the + * node + */ + record TreeModification(Node oldNode, Node newNode) { + } + + /** + * An accepted operation. Contains a collection of node updates that are + * performed as a result of the operation and any applied insert operations + * with a {@link Data#scopeOwner()} that matches the tree to which the + * operation was applied. + *

+ * Note that due to the way aliases are resolved, the node id in the update + * map might not match the node id in the applied signal command. + * + * @param updates + * a map from node ids to modifications to apply, not + * null. The map is empty for test operations that + * do not apply any changes even if the test passes. + * @param originalInserts + * a map from inserted node id to the originating signal command + * for new nodes with a matching scope owner. Not + * null. + */ + record Accept(Map updates, + Map originalInserts) + implements + OperationResult { + @Override + public boolean accepted() { + return true; + } + + /** + * Asserts that this operation contains exactly one modification and + * returns it. + * + * @return the single operation, not null + */ + public TreeModification onlyUpdate() { + assert updates.size() == 1; + return updates.values().iterator().next(); + } + } + + /** + * A rejected operation, together with the reason for the rejection. + * + * @param reason + * a string that describes the rejection reason, not + * null + */ + record Reject(String reason) implements OperationResult { + @Override + public boolean accepted() { + return false; + } + } + + /** + * Tests whether this operation result is accepted or rejected. + * + * @return true if the operation is accepted, + * false if it's rejected + */ + boolean accepted(); + + /** + * Creates a copy of the given map of operation results where all accepted + * results are replaced with the same rejection. + * + * @param results + * the original map from ids to operation results, not + * null + * @param reason + * the rejection reason string, not null + * @return a map with all accepted results replaced with rejections + */ + public static Map rejectAll( + Map results, String reason) { + Map failed = new HashMap<>(); + + results.forEach((key, original) -> { + if (original instanceof Reject failure) { + failed.put(key, failure); + } else { + failed.put(key, fail(reason)); + } + }); + + return failed; + } + + /** + * Creates a simple accepted result without modifications or original + * inserts. + * + * @return the accepted result, not null + */ + public static Accept ok() { + return new Accept(Map.of(), Map.of()); + } + + /** + * Creates a new rejected result with the given reason. + * + * @param reason + * the reason string to use, not null + * @return a new rejected result, not null + */ + public static Reject fail(String reason) { + return new Reject(reason); + } + + /** + * Creates an accepted or rejected result depending on the provided + * condition. + * + * @param condition + * the condition to check + * @param reasonIfFailed + * the reason string to use if rejected, not null + * @return an accepted result if the condition is true, a + * rejected result if the condition is false + */ + public static OperationResult test(boolean condition, + String reasonIfFailed) { + return condition ? ok() : fail(reasonIfFailed); + } +} diff --git a/signals/src/main/java/com/vaadin/signals/impl/Snapshot.java b/signals/src/main/java/com/vaadin/signals/impl/Snapshot.java new file mode 100644 index 00000000000..e31f4aca6aa --- /dev/null +++ b/signals/src/main/java/com/vaadin/signals/impl/Snapshot.java @@ -0,0 +1,41 @@ +package com.vaadin.signals.impl; + +import java.util.Map; + +import com.vaadin.signals.Id; +import com.vaadin.signals.Node; + +/** + * An immutable tree revision. + */ +public class Snapshot extends TreeRevision { + /** + * Creates a new snapshot from a mutable tree revision. + * + * @param base + * the mutable base revision to copy, not null + */ + public Snapshot(MutableTreeRevision base) { + super(base.ownerId(), Map.copyOf(base.nodes()), + Map.copyOf(base.originalInserts())); + } + + /** + * Creates an empty snapshot. The snapshot contains an empty root node with + * {@link Id#ZERO} that is used for tracking signal values and optionally + * also another empty root node with {@link Id#MAX} that is used for + * tracking metadata. + * + * @param ownerId + * the id of the tree owner, not null + * @param includeMax + * flag indicating whether an additional root node should be + * created for tracking metadata + */ + public Snapshot(Id ownerId, boolean includeMax) { + super(ownerId, + includeMax ? Map.of(Id.ZERO, Node.EMPTY, Id.MAX, Node.EMPTY) + : Map.of(Id.ZERO, Node.EMPTY), + Map.of()); + } +} diff --git a/signals/src/main/java/com/vaadin/signals/impl/TreeRevision.java b/signals/src/main/java/com/vaadin/signals/impl/TreeRevision.java new file mode 100644 index 00000000000..9227a615eab --- /dev/null +++ b/signals/src/main/java/com/vaadin/signals/impl/TreeRevision.java @@ -0,0 +1,190 @@ +package com.vaadin.signals.impl; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +import com.vaadin.signals.Id; +import com.vaadin.signals.Node; +import com.vaadin.signals.Node.Alias; +import com.vaadin.signals.Node.Data; +import com.vaadin.signals.SignalCommand; +import com.vaadin.signals.SignalCommand.ScopeOwnerCommand; + +/** + * A revision of a signal tree. The revision keeps track of the nodes that make + * up the tree and any insert commands that are owned by this revision. + * + * @see MutableTreeRevision + */ +public abstract class TreeRevision { + private final Map nodes; + private final Map originalInserts; + + private final Id ownerId; + + /** + * Creates a new revision based on the given owner id, map of signal nodes + * and map of original inserts. + * + * @param ownerId + * the id of the tree owner, not null + * @param nodes + * the map of state nodes in this revision, not null + * @param originalInserts + * the map of insert commands that created any nodes owned by + * this tree, not null + */ + public TreeRevision(Id ownerId, Map nodes, + Map originalInserts) { + this.ownerId = ownerId; + this.nodes = nodes; + this.originalInserts = originalInserts; + + assert assertValidTree(); + } + + /** + * Gets the id of the tree that this revision belongs to. + * + * @see #originalInserts() + * + * @return + */ + public Id ownerId() { + return ownerId; + } + + /** + * Gets the nodes that make up this revision. + * + * @return a map from node id to node, not null + */ + public Map nodes() { + return nodes; + } + + /** + * Gets a map of signal commands for creating any nodes owned by this tree. + * Any signal node with a matching {@link Data#scopeOwner()} is considered + * to be owned by that tree and such nodes should be removed if the tree is + * disconnected. The revision keeps track of the original insert operations + * so that the nodes can be inserted back again in the appropriate way if + * the tree is connected back again. + * + * @return a map from node id to signal command, not null + */ + public Map originalInserts() { + return originalInserts; + } + + /** + * Get the data node for the given node id, if present. If the given id + * corresponds to an alias node, then alias is resolved and the data node + * for the alias target is returned instead. + * + * @param id + * the id for which to get a data node, not null + * @return an optional containing the corresponding data node, or an empty + * optional of there is no node with the given id + */ + public Optional data(Id id) { + Node node = nodes.get(id); + if (node instanceof Data data) { + return Optional.of(data); + } else if (node instanceof Alias alias) { + return data(alias.target()); + } else { + // Guard against an unexpected Node subclass + assert node == null; + + return Optional.empty(); + } + } + + /** + * Asserts that the nodes in this revision are internally consistent. + * + *

    + *
  • All nodes are attached to the root + *
  • All parent-child relationships are consistent in both directions + *
  • No node is attached in multiple places + *
  • All aliases target an existing data node + *
  • All nodes with a matching scope owner has a matching original insert + *
  • All original insert entries correspond to a node with a matching + * scope owner + *
+ * + * This method is intended to be invoked with the assert + * keyword. While the return type is boolean, it will never + * return false but instead throws an assertion error from the + * appropriate check to make it easier to pinpoint the source of any error. + * + * @return true if the tree is valid, otherwise an assertion + * error is thrown + */ + protected boolean assertValidTree() { + + Set visited = new HashSet<>(); + Set checkedScopeOwners = new HashSet<>(); + + LinkedList toCheck = new LinkedList<>(); + assert nodes.containsKey(Id.ZERO); + assert nodes.get(Id.ZERO) instanceof Data root && root.parent() == null; + + toCheck.add(Id.ZERO); + if (nodes.containsKey(Id.MAX)) { + assert nodes.get(Id.MAX) instanceof Data root + && root.parent() == null; + + toCheck.add(Id.MAX); + } + + // Traverse to check parent-child relationships + while (!toCheck.isEmpty()) { + Id id = toCheck.poll(); + if (!visited.add(id)) { + assert false : "Already visited"; + } + Data node = data(id).get(); + + if (ownerId.equals(node.scopeOwner())) { + checkedScopeOwners.add(id); + ScopeOwnerCommand scopeOwnerCommand = originalInserts.get(id); + assert scopeOwnerCommand != null; + assert scopeOwnerCommand.scopeOwner().equals(ownerId); + } + + Stream allChildren = Stream.concat(node.listChildren().stream(), + node.mapChildren().values().stream()); + allChildren.forEach(childId -> { + toCheck.add(childId); + + assert nodes.containsKey(childId); + + Data child = data(childId).get(); + assert child.parent() != null; + assert child.parent().equals(id); + }); + } + + List unattached = nodes.keySet().stream().filter( + id -> nodes.get(id) instanceof Data && !visited.contains(id)) + .toList(); + assert unattached.isEmpty(); + + nodes().values().forEach(node -> { + if (node instanceof Alias alias) { + assert nodes.get(alias.target()) instanceof Data; + } + }); + + assert checkedScopeOwners.equals(originalInserts.keySet()); + + return true; + } +} diff --git a/signals/src/test/java/com/vaadin/signals/IdTest.java b/signals/src/test/java/com/vaadin/signals/IdTest.java new file mode 100644 index 00000000000..a91a64e03d9 --- /dev/null +++ b/signals/src/test/java/com/vaadin/signals/IdTest.java @@ -0,0 +1,56 @@ +package com.vaadin.signals; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class IdTest { + @Test + void randomId_isRandom() { + // Test will fail once every 18 quintillion times or so + assertNotEquals(Id.random(), Id.random()); + } + + @Test + void basicJsonSerialization() throws JsonProcessingException { + ObjectMapper mapper = new ObjectMapper(); + + Id id = new Id(4600806552848092835l); + String jsonString = mapper.writeValueAsString(id); + + assertEquals("\"P9lZLwbMzqM\"", jsonString); + + Id deserialized = mapper.readValue(jsonString, Id.class); + + assertEquals(id, deserialized); + } + + @Test + void zeroId_compactJson() throws JsonProcessingException { + ObjectMapper mapper = new ObjectMapper(); + + String jsonString = mapper.writeValueAsString(Id.ZERO); + + assertEquals("\"\"", jsonString); + + Id deserialized = mapper.readValue(jsonString, Id.class); + + assertEquals(Id.ZERO, deserialized); + } + + @Test + void comparable() { + List ids = List.of(new Id(0), new Id(-42365683), new Id(754)); + + List sorted = ids.stream().sorted().toList(); + + assertEquals(List.of(new Id(-42365683), new Id(0), new Id(754)), + sorted); + } +} diff --git a/signals/src/test/java/com/vaadin/signals/dummy b/signals/src/test/java/com/vaadin/signals/dummy deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/signals/src/test/java/com/vaadin/signals/impl/MutableTreeRevisionTest.java b/signals/src/test/java/com/vaadin/signals/impl/MutableTreeRevisionTest.java new file mode 100644 index 00000000000..634e281f976 --- /dev/null +++ b/signals/src/test/java/com/vaadin/signals/impl/MutableTreeRevisionTest.java @@ -0,0 +1,1652 @@ +package com.vaadin.signals.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.IntStream; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.node.DoubleNode; +import com.fasterxml.jackson.databind.node.NullNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.vaadin.signals.Id; +import com.vaadin.signals.ListSignal; +import com.vaadin.signals.ListSignal.ListPosition; +import com.vaadin.signals.Node; +import com.vaadin.signals.Node.Alias; +import com.vaadin.signals.Node.Data; +import com.vaadin.signals.SignalCommand; +import com.vaadin.signals.impl.OperationResult.Accept; +import com.vaadin.signals.impl.OperationResult.TreeModification; + +public class MutableTreeRevisionTest { + private final MutableTreeRevision revision = new MutableTreeRevision( + new Snapshot(Id.random(), false)); + private final Id commandId = Id.random(); + + @Test + void constuctor_modifyBase_copyNotUpdated() { + MutableTreeRevision copy = new MutableTreeRevision(revision); + + revision.nodes().put(Id.random(), Node.EMPTY); + revision.originalInserts().put(Id.random(), null); + + assertEquals(Map.of(Id.ZERO, Node.EMPTY), copy.nodes()); + assertEquals(Map.of(), copy.originalInserts()); + } + + @Test + void constuctor_modifyCopy_baseNotUpdated() { + MutableTreeRevision copy = new MutableTreeRevision(revision); + + copy.nodes().put(Id.random(), Node.EMPTY); + copy.originalInserts().put(Id.random(), null); + + assertEquals(Map.of(Id.ZERO, Node.EMPTY), revision.nodes()); + assertEquals(Map.of(), revision.originalInserts()); + } + + @Test + void setCommand_existingTarget_valueIsSet() { + OperationResult result = applySingle(new SignalCommand.SetCommand( + commandId, Id.ZERO, new TextNode("value"))); + + // Check result object + Accept accept = assertAccepted(result); + TreeModification modification = accept.onlyUpdate(); + assertEquals(Node.EMPTY, modification.oldNode()); + Data newDataNode = (Data) modification.newNode(); + assertEquals(new TextNode("value"), newDataNode.value()); + + // Check revision state + assertValue(Id.ZERO, "value"); + } + + @Test + void setCommand_missingTarget_rejected() { + OperationResult result = applySingle(new SignalCommand.SetCommand( + commandId, Id.random(), new TextNode("value"))); + + // Check result object + assertFalse(result.accepted()); + + // Check revision state + assertNullValue(Id.ZERO); + } + + @Test + void setCommand_aliasTarget_dataNodeUpdated() { + Id alias = createAlias(Id.ZERO); + + OperationResult result = applySingle(new SignalCommand.SetCommand( + commandId, alias, new TextNode("value"))); + + // Check result object + Accept accept = assertAccepted(result); + assertEquals(Set.of(Id.ZERO), accept.updates().keySet()); + assertFalse(accept.updates().containsKey(alias)); + + // Check revision state + assertValue(Id.ZERO, "value"); + assertValue(alias, "value"); + } + + @Test + void incrementCommand_nullValue_incrementsFromZero() { + OperationResult result = applySingle( + new SignalCommand.IncrementCommand(commandId, Id.ZERO, 3)); + + // Check result object + Data newData = assertSingleDataChange(result); + assertEquals(3, newData.value().asInt()); + + // Check revision state + assertValue(Id.ZERO, 3); + } + + @Test + void incrementCommand_numericValue_incremented() { + applySingle(new SignalCommand.SetCommand(Id.random(), Id.ZERO, + new DoubleNode(2))); + + OperationResult result = applySingle( + new SignalCommand.IncrementCommand(commandId, Id.ZERO, 3)); + + // Check result object + Data newData = assertSingleDataChange(result); + assertEquals(5, newData.value().asInt()); + + // Check revision state + assertValue(Id.ZERO, 5); + } + + @Test + void incrementCommand_textValue_reject() { + applySingle(new SignalCommand.SetCommand(Id.random(), Id.ZERO, + new TextNode("value"))); + + OperationResult result = applySingle( + new SignalCommand.IncrementCommand(commandId, Id.ZERO, 3)); + + // Check result object + assertFalse(result.accepted()); + + // Check revision state + assertValue(Id.ZERO, "value"); + } + + @Test + void incrementCommand_alias_dataUpdated() { + Id alias = createAlias(Id.ZERO); + + OperationResult result = applySingle( + new SignalCommand.IncrementCommand(commandId, alias, 3)); + + // Check result object + Accept accept = assertAccepted(result); + assertEquals(Set.of(Id.ZERO), accept.updates().keySet()); + assertFalse(accept.updates().containsKey(alias)); + + // Check revision state + assertValue(Id.ZERO, 3); + } + + @Test + void insertCommand_emptyNode_onlyChild() { + OperationResult result = applySingle(new SignalCommand.InsertCommand( + commandId, Id.ZERO, null, new TextNode("value"), + ListSignal.ListPosition.first())); + + // Check result object + Accept accept = assertAccepted(result); + assertEquals(2, accept.updates().size()); + TreeModification parentUpdate = accept.updates().get(Id.ZERO); + assertEquals(List.of(commandId), + ((Data) parentUpdate.newNode()).listChildren()); + TreeModification childUpdate = accept.updates().get(commandId); + assertNull(childUpdate.oldNode()); + assertEquals("value", + ((Data) childUpdate.newNode()).value().textValue()); + + // Check revision state + assertListChildren(Id.ZERO, commandId); + assertValue(commandId, "value"); + } + + @Test + void insertCommandFirst_otherEntry_insertedFirst() { + Id other = Id.random(); + applySingle(new SignalCommand.InsertCommand(other, Id.ZERO, null, null, + ListSignal.ListPosition.first())); + + Id inserted = Id.random(); + OperationResult result = applySingle( + new SignalCommand.InsertCommand(inserted, Id.ZERO, null, null, + ListSignal.ListPosition.first())); + + // Check result object + Accept accept = assertAccepted(result); + TreeModification parentUpdate = accept.updates().get(Id.ZERO); + assertEquals(List.of(other), + ((Data) parentUpdate.oldNode()).listChildren()); + assertEquals(List.of(inserted, other), + ((Data) parentUpdate.newNode()).listChildren()); + + // Check revision state + assertListChildren(Id.ZERO, inserted, other); + } + + @Test + void insertCommandLast_otherEntry_insertedLast() { + Id other = Id.random(); + applySingle(new SignalCommand.InsertCommand(other, Id.ZERO, null, null, + ListSignal.ListPosition.first())); + + Id inserted = Id.random(); + OperationResult result = applySingle(new SignalCommand.InsertCommand( + inserted, Id.ZERO, null, null, ListSignal.ListPosition.last())); + + // Check result object + assertTrue(result.accepted()); + + // Check revision state + assertListChildren(Id.ZERO, other, inserted); + } + + @Test + void insertCommand_afterOther_insertedAfter() { + Id other = Id.random(); + applySingle(new SignalCommand.InsertCommand(other, Id.ZERO, null, null, + ListSignal.ListPosition.first())); + + Id inserted = Id.random(); + OperationResult result = applySingle( + new SignalCommand.InsertCommand(inserted, Id.ZERO, null, null, + new ListSignal.ListPosition(other, null))); + + // Check result object + assertTrue(result.accepted()); + + // Check revision state + assertListChildren(Id.ZERO, other, inserted); + } + + @Test + void insertCommand_beforeOther_insertedBefore() { + Id other = Id.random(); + applySingle(new SignalCommand.InsertCommand(other, Id.ZERO, null, null, + ListSignal.ListPosition.first())); + + Id inserted = Id.random(); + OperationResult result = applySingle( + new SignalCommand.InsertCommand(inserted, Id.ZERO, null, null, + new ListSignal.ListPosition(null, other))); + + // Check result object + assertTrue(result.accepted()); + + // Check revision state + assertListChildren(Id.ZERO, inserted, other); + } + + @Test + void insertCommand_beforeMissing_reject() { + Id inserted = Id.random(); + OperationResult result = applySingle( + new SignalCommand.InsertCommand(inserted, Id.ZERO, null, null, + new ListSignal.ListPosition(null, Id.random()))); + + // Check result object + assertFalse(result.accepted()); + + // Check revision state + assertUnchanged(); + } + + @Test + void insertCommand_afterMissing_reject() { + Id inserted = Id.random(); + OperationResult result = applySingle( + new SignalCommand.InsertCommand(inserted, Id.ZERO, null, null, + new ListSignal.ListPosition(Id.random(), null))); + + // Check result object + assertFalse(result.accepted()); + + // Check revision state + assertUnchanged(); + } + + @Test + void insertCommand_betweenAdjacent_insertedBetween() { + Id other1 = Id.random(); + applySingle(new SignalCommand.InsertCommand(other1, Id.ZERO, null, null, + ListSignal.ListPosition.last())); + + Id other2 = Id.random(); + applySingle(new SignalCommand.InsertCommand(other2, Id.ZERO, null, null, + ListSignal.ListPosition.last())); + + Id inserted = Id.random(); + OperationResult result = applySingle( + new SignalCommand.InsertCommand(inserted, Id.ZERO, null, null, + new ListSignal.ListPosition(other1, other2))); + + // Check result object + assertTrue(result.accepted()); + + // Check revision state + assertListChildren(Id.ZERO, other1, inserted, other2); + } + + @Test + void insertCommand_betweenNonAdjacent_reject() { + Id other1 = Id.random(); + applySingle(new SignalCommand.InsertCommand(other1, Id.ZERO, null, null, + ListSignal.ListPosition.last())); + + Id other2 = Id.random(); + applySingle(new SignalCommand.InsertCommand(other2, Id.ZERO, null, null, + ListSignal.ListPosition.last())); + + Id inserted = Id.random(); + OperationResult result = applySingle( + new SignalCommand.InsertCommand(inserted, Id.ZERO, null, null, + new ListSignal.ListPosition(other2, other1))); + // they are technically adjacent, but not in the expected order + + // Check result object + assertFalse(result.accepted()); + + // Check revision state + assertListChildren(Id.ZERO, other1, other2); + } + + @Test + void insertCommand_emptyPosition_reject() { + OperationResult result = applySingle(new SignalCommand.InsertCommand( + commandId, Id.ZERO, null, null, new ListPosition(null, null))); + + // Check result object + assertFalse(result.accepted()); + + // Check revision state + assertUnchanged(); + } + + @Test + void insertCommand_parentAlias_dataUpdated() { + Id alias = createAlias(Id.ZERO); + + OperationResult result = applySingle(new SignalCommand.InsertCommand( + commandId, alias, null, null, ListPosition.first())); + + // Check result object + Accept accept = assertAccepted(result); + assertTrue(accept.updates().containsKey(Id.ZERO)); + assertFalse(accept.updates().containsKey(alias)); + + // Check revision state + assertListChildren(Id.ZERO, commandId); + } + + @Test + void insertCommand_afterAlias_afterAliasTarget() { + Id child = insertChildren(Id.ZERO, 1).get(0); + Id alias = createAlias(child); + + OperationResult result = applySingle(new SignalCommand.InsertCommand( + commandId, Id.ZERO, null, null, new ListPosition(alias, null))); + + // Check result object + assertAccepted(result); + + // Check revision state + assertListChildren(Id.ZERO, child, commandId); + } + + @Test + void insertCommand_beforeAlias_beforeAliasTarget() { + Id child = insertChildren(Id.ZERO, 1).get(0); + Id alias = createAlias(child); + + OperationResult result = applySingle(new SignalCommand.InsertCommand( + commandId, Id.ZERO, null, null, new ListPosition(null, alias))); + + // Check result object + assertAccepted(result); + + // Check revision state + assertListChildren(Id.ZERO, commandId, child); + } + + @Test + void putCommand_emptyTarget_nodeInserted() { + OperationResult result = applySingle(new SignalCommand.PutCommand( + commandId, Id.ZERO, "key", new TextNode("value"))); + + // Check result object + Accept accept = assertAccepted(result); + assertEquals(2, accept.updates().size()); + TreeModification rootModification = accept.updates().get(Id.ZERO); + assertEquals(Map.of("key", commandId), + ((Data) rootModification.newNode()).mapChildren()); + TreeModification childModification = accept.updates().get(commandId); + assertNull(childModification.oldNode()); + assertEquals("value", + ((Data) childModification.newNode()).value().textValue()); + + // Check revision state + assertMapChildren(Id.ZERO, Map.of("key", commandId)); + assertValue(commandId, "value"); + } + + @Test + void putCommand_replaceExisting_nodeUpdated() { + Id child = Id.random(); + applySingle(new SignalCommand.PutCommand(child, Id.ZERO, "key", + new TextNode("1"))); + + OperationResult result = applySingle(new SignalCommand.PutCommand( + commandId, Id.ZERO, "key", new TextNode("2"))); + + // Check result object + Accept accept = assertAccepted(result); + TreeModification childUpdate = accept.onlyUpdate(); + assertEquals("1", ((Data) childUpdate.oldNode()).value().textValue()); + assertEquals("2", ((Data) childUpdate.newNode()).value().textValue()); + + // Check revision state + assertMapChildren(Id.ZERO, Map.of("key", child)); + assertValue(child, "2"); + } + + @Test + void putCommand_aliasTarget_dataNodeUpdated() { + Id alias = createAlias(Id.ZERO); + + OperationResult result = applySingle( + new SignalCommand.PutCommand(commandId, alias, "key", null)); + + // Check result object + Accept accept = assertAccepted(result); + assertTrue(accept.updates().containsKey(Id.ZERO)); + assertFalse(accept.updates().containsKey(alias)); + + // Check revision state + assertMapChildren(Id.ZERO, Map.of("key", commandId)); + } + + @Test + void putIfAbsentCommand_absent_nodeCreated() { + OperationResult result = applySingle( + new SignalCommand.PutIfAbsentCommand(commandId, Id.ZERO, null, + "key", new TextNode("value"))); + // Check result object + Accept accept = assertAccepted(result); + assertEquals(2, accept.updates().size()); + + // Check revision state + assertMapChildren(Id.ZERO, Map.of("key", commandId)); + assertInstanceOf(Node.Data.class, revision.nodes().get(commandId)); + assertValue(commandId, "value"); + } + + @Test + void putIfAbsentCommand_present_aliasCreated() { + Id child = Id.random(); + applySingle(new SignalCommand.PutCommand(child, Id.ZERO, "key", + new TextNode("1"))); + + OperationResult result = applySingle( + new SignalCommand.PutIfAbsentCommand(commandId, Id.ZERO, null, + "key", new TextNode("2"))); + + // Check result object + Accept accept = assertAccepted(result); + assertEquals("Only alias is updated", 1, accept.updates().size()); + TreeModification modification = accept.updates().get(commandId); + assertNull(modification.oldNode()); + assertInstanceOf(Node.Alias.class, modification.newNode()); + assertEquals(child, ((Alias) modification.newNode()).target()); + + // Check revision state + assertMapChildren(Id.ZERO, Map.of("key", child)); + assertValue(child, "1"); + assertValue(commandId, "1"); + assertInstanceOf(Node.Data.class, revision.nodes().get(child)); + assertInstanceOf(Node.Alias.class, revision.nodes().get(commandId)); + } + + @Test + void putIfAbsentCommand_aliasTarget_dataNodeUpdated() { + Id alias = createAlias(Id.ZERO); + + OperationResult result = applySingle( + new SignalCommand.PutIfAbsentCommand(commandId, alias, null, + "key", null)); + + // Check result object + Accept accept = assertAccepted(result); + assertTrue(accept.updates().containsKey(Id.ZERO)); + assertFalse(accept.updates().containsKey(alias)); + + // Check revision state + assertMapChildren(Id.ZERO, Map.of("key", commandId)); + } + + @Test + void adoptAtCommand_childMissing_reject() { + OperationResult result = applySingle(new SignalCommand.AdoptAtCommand( + commandId, Id.ZERO, Id.random(), ListPosition.last())); + + // Check result object + assertFalse(result.accepted()); + + // Check revision state + assertUnchanged(); + } + + @Test + void adoptAtCommand_childAdoptsItsParent_reject() { + Id child = Id.random(); + applySingle(new SignalCommand.InsertCommand(child, Id.ZERO, null, null, + ListSignal.ListPosition.last())); + + OperationResult result = applySingle(new SignalCommand.AdoptAtCommand( + commandId, child, Id.ZERO, ListPosition.last())); + + // Check result object + assertFalse(result.accepted()); + + // Check revision state + assertListChildren(Id.ZERO, child); + } + + @Test + void adoptAtCommand_childAlreadyInParent_orderChanged() { + Id other = Id.random(); + applySingle(new SignalCommand.InsertCommand(other, Id.ZERO, null, null, + ListSignal.ListPosition.last())); + + Id child = Id.random(); + applySingle(new SignalCommand.InsertCommand(child, Id.ZERO, null, null, + ListSignal.ListPosition.last())); + + OperationResult result = applySingle(new SignalCommand.AdoptAtCommand( + commandId, Id.ZERO, child, ListPosition.first())); + + // Check result object + assertTrue(result.accepted()); + + // Check revision state + assertListChildren(Id.ZERO, child, other); + } + + @Test + void adoptAtCommand_childInAnotherParent_adopted() { + Id target = Id.random(); + applySingle(new SignalCommand.InsertCommand(target, Id.ZERO, null, null, + ListSignal.ListPosition.last())); + + Id child = Id.random(); + applySingle(new SignalCommand.InsertCommand(child, Id.ZERO, null, null, + ListSignal.ListPosition.last())); + + OperationResult result = applySingle(new SignalCommand.AdoptAtCommand( + commandId, target, child, ListPosition.first())); + + // Check result object + assertTrue(result.accepted()); + + // Check revision state + assertListChildren(Id.ZERO, target); + assertListChildren(target, child); + } + + @Test + void adoptAtCommand_mapChild_removedFromMap() { + Id child = Id.random(); + applySingle(new SignalCommand.PutCommand(child, Id.ZERO, "key", null)); + + OperationResult result = applySingle(new SignalCommand.AdoptAtCommand( + commandId, Id.ZERO, child, ListPosition.first())); + + // Check result object + assertTrue(result.accepted()); + + // Check revision state + assertListChildren(Id.ZERO, child); + assertMapChildren(Id.ZERO, Map.of()); + } + + @Test + void adoptAtCommand_aliasParent_dataNodeUpdated() { + Id alias = createAlias(Id.ZERO); + + List children = insertChildren(Id.ZERO, 2); + + OperationResult result = applySingle(new SignalCommand.AdoptAtCommand( + commandId, alias, children.get(0), ListPosition.last())); + + // Check result object + Accept accept = assertAccepted(result); + assertTrue(accept.updates().containsKey(Id.ZERO)); + assertFalse(accept.updates().containsKey(alias)); + + // Check revision state + assertListChildren(Id.ZERO, children.get(1), children.get(0)); + } + + @Test + void adoptAtCommand_moveAlias_dataNodeMoved() { + List children = insertChildren(Id.ZERO, 2); + + Id alias = createAlias(children.get(0)); + + OperationResult result = applySingle(new SignalCommand.AdoptAtCommand( + commandId, Id.ZERO, alias, ListPosition.last())); + + // Check result object + Accept accept = assertAccepted(result); + assertTrue(accept.updates().containsKey(children.get(0))); + assertFalse(accept.updates().containsKey(alias)); + + // Check revision state + assertListChildren(Id.ZERO, children.get(1), children.get(0)); + } + + @Test + void adoptAsCommand_childMissing_reject() { + OperationResult result = applySingle(new SignalCommand.AdoptAsCommand( + commandId, Id.ZERO, Id.random(), "key")); + + // Check result object + assertFalse(result.accepted()); + + // Check revision state + assertUnchanged(); + } + + @Test + void adoptAsCommand_childAdoptsItsParent_reject() { + Id child = Id.random(); + applySingle(new SignalCommand.PutCommand(child, Id.ZERO, "key", null)); + + OperationResult result = applySingle(new SignalCommand.AdoptAsCommand( + commandId, child, Id.ZERO, "key")); + + // Check result object + assertFalse(result.accepted()); + + // Check revision state + assertMapChildren(Id.ZERO, Map.of("key", child)); + } + + @Test + void adoptAsCommand_childAlreadyInParent_keyChanged() { + Id child = Id.random(); + applySingle(new SignalCommand.PutCommand(child, Id.ZERO, "key", null)); + + OperationResult result = applySingle(new SignalCommand.AdoptAsCommand( + commandId, Id.ZERO, child, "key2")); + + // Check result object + assertTrue(result.accepted()); + + // Check revision state + assertMapChildren(Id.ZERO, Map.of("key2", child)); + } + + @Test + void adoptAsCommand_childInAnotherParent_adopted() { + Id target = Id.random(); + applySingle(new SignalCommand.PutCommand(target, Id.ZERO, "key", null)); + + Id child = Id.random(); + applySingle(new SignalCommand.PutCommand(child, Id.ZERO, "key2", null)); + + OperationResult result = applySingle(new SignalCommand.AdoptAsCommand( + commandId, target, child, "key")); + + // Check result object + assertTrue(result.accepted()); + + // Check revision state + assertMapChildren(Id.ZERO, Map.of("key", target)); + assertMapChildren(target, Map.of("key", child)); + } + + @Test + void adoptAsCommand_listChild_removedFromList() { + Id child = Id.random(); + applySingle(new SignalCommand.InsertCommand(child, Id.ZERO, null, null, + ListPosition.last())); + + OperationResult result = applySingle(new SignalCommand.AdoptAsCommand( + commandId, Id.ZERO, child, "key")); + + // Check result object + assertTrue(result.accepted()); + + // Check revision state + assertListChildren(Id.ZERO); + assertMapChildren(Id.ZERO, Map.of("key", child)); + } + + @Test + void adoptAsCommand_existingKey_reject() { + Id other = Id.random(); + applySingle(new SignalCommand.PutCommand(other, Id.ZERO, "key", null)); + + Id child = Id.random(); + applySingle(new SignalCommand.PutCommand(child, Id.ZERO, "key2", null)); + + OperationResult result = applySingle(new SignalCommand.AdoptAsCommand( + commandId, Id.ZERO, child, "key")); + + // Check result object + assertFalse(result.accepted()); + + // Check revision state + assertMapChildren(Id.ZERO, Map.of("key", other, "key2", child)); + } + + @Test + void adoptAsCommand_aliasParent_dataNodeUpdated() { + Id child = Id.random(); + applySingle(new SignalCommand.PutCommand(child, Id.ZERO, "key", null)); + + Id alias = createAlias(Id.ZERO); + OperationResult result = applySingle(new SignalCommand.AdoptAsCommand( + commandId, alias, child, "key2")); + + // Check result object + Accept accept = assertAccepted(result); + assertTrue(accept.updates().containsKey(Id.ZERO)); + assertFalse(accept.updates().containsKey(alias)); + + // Check revision state + assertMapChildren(Id.ZERO, Map.of("key2", child)); + } + + @Test + void adoptAsCommand_moveAlias_dataNodeUpdated() { + Id child = Id.random(); + applySingle(new SignalCommand.PutCommand(child, Id.ZERO, "key", null)); + + Id alias = createAlias(child); + OperationResult result = applySingle(new SignalCommand.AdoptAsCommand( + commandId, Id.ZERO, alias, "key2")); + + // Check result object + Accept accept = assertAccepted(result); + assertTrue(accept.updates().containsKey(child)); + assertFalse(accept.updates().containsKey(alias)); + + // Check revision state + assertMapChildren(Id.ZERO, Map.of("key2", child)); + } + + @Test + void removeCommand_rootNode_reject() { + OperationResult result = applySingle( + new SignalCommand.RemoveCommand(commandId, Id.ZERO, null)); + + // Check result object + assertFalse(result.accepted()); + + // Check revision state + assertUnchanged(); + } + + @Test + void removeCommand_childWithGrandchild_recursivelyRemoved() { + Id child = Id.random(); + applySingle(new SignalCommand.InsertCommand(child, Id.ZERO, null, null, + ListPosition.last())); + + Id grandChild = Id.random(); + applySingle(new SignalCommand.InsertCommand(grandChild, child, null, + null, ListPosition.last())); + + OperationResult result = applySingle( + new SignalCommand.RemoveCommand(commandId, child, null)); + + // Check result object + Accept accept = assertAccepted(result); + assertEquals(3, accept.updates().size()); + Data newRootData = (Data) accept.updates().get(Id.ZERO).newNode(); + assertEquals(List.of(), newRootData.listChildren()); + TreeModification childUpdate = accept.updates().get(child); + assertNull(childUpdate.newNode()); + TreeModification grandchildUpdate = accept.updates().get(grandChild); + assertNull(grandchildUpdate.newNode()); + + // Check revision state + assertListChildren(Id.ZERO); + assertEquals(1, revision.nodes().size()); + } + + @Test + void removeCommand_expectedParentNotParent_reject() { + Id expectedParent = Id.random(); + applySingle(new SignalCommand.InsertCommand(expectedParent, Id.ZERO, + null, null, ListPosition.last())); + + Id child = Id.random(); + applySingle(new SignalCommand.InsertCommand(child, Id.ZERO, null, null, + ListPosition.last())); + + OperationResult result = applySingle(new SignalCommand.RemoveCommand( + commandId, child, expectedParent)); + + // Check result object + assertFalse(result.accepted()); + + // Check revision state + assertListChildren(Id.ZERO, expectedParent, child); + } + + @Test + void removeCommand_expectedParentIsParent_childRemoved() { + Id child = Id.random(); + applySingle(new SignalCommand.InsertCommand(child, Id.ZERO, null, null, + ListPosition.last())); + + OperationResult result = applySingle( + new SignalCommand.RemoveCommand(commandId, child, Id.ZERO)); + + // Check result object + assertTrue(result.accepted()); + + // Check revision state + assertListChildren(Id.ZERO); + } + + @Test + void removeCommand_parentAlias_dataNodeUpdated() { + Id child = insertChildren(Id.ZERO, 1).get(0); + + Id alias = createAlias(Id.ZERO); + OperationResult result = applySingle( + new SignalCommand.RemoveCommand(commandId, child, alias)); + + // Check result object + Accept accept = assertAccepted(result); + assertTrue(accept.updates().containsKey(Id.ZERO)); + assertFalse(accept.updates().containsKey(alias)); + + // Check revision state + assertListChildren(Id.ZERO); + } + + @Test + void removeCommand_childAlias_dataNodeUpdated() { + Id child = insertChildren(Id.ZERO, 1).get(0); + + Id alias = createAlias(child); + OperationResult result = applySingle( + new SignalCommand.RemoveCommand(commandId, alias, Id.ZERO)); + + // Check result object + Accept accept = assertAccepted(result); + assertTrue(accept.updates().containsKey(child)); + assertTrue("Alias was also removed", + accept.updates().containsKey(alias)); + + // Check revision state + assertListChildren(Id.ZERO); + assertFalse(revision.nodes().containsKey(alias)); + } + + @Test + void removeByKeyCommand_missingKey_reject() { + OperationResult result = applySingle( + new SignalCommand.RemoveByKeyCommand(commandId, Id.ZERO, + "key")); + + // Check result object + assertFalse(result.accepted()); + + // Check revision state + assertUnchanged(); + } + + @Test + void removeByKeyCommand_existingKey_removed() { + Id child = Id.random(); + applySingle(new SignalCommand.PutCommand(child, Id.ZERO, "key", null)); + + OperationResult result = applySingle( + new SignalCommand.RemoveByKeyCommand(commandId, Id.ZERO, + "key")); + + // Check result object + Accept accept = assertAccepted(result); + assertEquals(2, accept.updates().size()); + TreeModification childUpdate = accept.updates().get(child); + assertNull(childUpdate.newNode()); + TreeModification parentUpdate = accept.updates().get(Id.ZERO); + assertEquals(Map.of(), ((Data) parentUpdate.newNode()).mapChildren()); + + // Check revision state + assertMapChildren(Id.ZERO, Map.of()); + } + + @Test + void removeByKeyCommand_parentAlias_dataNodeUpdated() { + Id child = Id.random(); + applySingle(new SignalCommand.PutCommand(child, Id.ZERO, "key", null)); + + Id alias = createAlias(Id.ZERO); + OperationResult result = applySingle( + new SignalCommand.RemoveByKeyCommand(commandId, alias, "key")); + + // Check result object + Accept accept = assertAccepted(result); + assertTrue(accept.updates().containsKey(Id.ZERO)); + assertFalse(accept.updates().containsKey(alias)); + + // Check revision state + assertMapChildren(Id.ZERO, Map.of()); + } + + @Test + void clearCommand_nodeWithChildren_childrenRemoved() { + Id listChild = Id.random(); + applySingle(new SignalCommand.InsertCommand(listChild, Id.ZERO, null, + null, ListPosition.last())); + + Id mapChild = Id.random(); + applySingle( + new SignalCommand.PutCommand(mapChild, Id.ZERO, "key", null)); + + OperationResult result = applySingle( + new SignalCommand.ClearCommand(commandId, Id.ZERO)); + + // Check result object + Accept accept = assertAccepted(result); + assertEquals(3, accept.updates().size()); + TreeModification listChildUpdate = accept.updates().get(listChild); + assertNull(listChildUpdate.newNode()); + TreeModification mapChildUpdate = accept.updates().get(mapChild); + assertNull(mapChildUpdate.newNode()); + TreeModification parentUpdate = accept.updates().get(Id.ZERO); + assertEquals(Map.of(), ((Data) parentUpdate.newNode()).mapChildren()); + + // Check revision state + assertMapChildren(Id.ZERO, Map.of()); + } + + @Test + void clearCommand_emptyNode_noChange() { + OperationResult result = applySingle( + new SignalCommand.ClearCommand(commandId, Id.ZERO)); + + // Check result object + Accept accept = assertAccepted(result); + assertEquals(Map.of(), accept.updates()); + + // Check revision state + assertUnchanged(); + } + + @Test + void clearCommand_alias_dataNodeUpdated() { + insertChildren(Id.ZERO, 1); + + Id alias = createAlias(Id.ZERO); + OperationResult result = applySingle( + new SignalCommand.ClearCommand(alias, Id.ZERO)); + + Accept accept = assertAccepted(result); + assertTrue(accept.updates().containsKey(Id.ZERO)); + assertFalse(accept.updates().containsKey(alias)); + + // Check revision state + assertListChildren(Id.ZERO); + } + + @Test + void positionTestNoPosition_listChild_accepted() { + Id child = insertChildren(Id.ZERO, 1).get(0); + + OperationResult result = applySingle(new SignalCommand.PositionTest( + commandId, Id.ZERO, child, new ListPosition(null, null))); + + assertTestResult(true, result); + } + + @Test + void positionTestNoPosition_missingChild_rejected() { + OperationResult result = applySingle(new SignalCommand.PositionTest( + commandId, Id.ZERO, Id.random(), new ListPosition(null, null))); + + assertTestResult(false, result); + } + + @Test + void positionTestNoPosition_mapChild_rejected() { + Id child = Id.random(); + applySingle(new SignalCommand.PutCommand(child, Id.ZERO, "key", null)); + + OperationResult result = applySingle(new SignalCommand.PositionTest( + commandId, Id.ZERO, child, new ListPosition(null, null))); + + assertTestResult(false, result); + } + + @Test + void positionTestNoPosition_otherParent_rejected() { + List children = insertChildren(Id.ZERO, 2); + + OperationResult result = applySingle( + new SignalCommand.PositionTest(commandId, children.get(0), + children.get(1), new ListPosition(null, null))); + + assertTestResult(false, result); + } + + @Test + void positionTestFirst_isFirst_accepted() { + List children = insertChildren(Id.ZERO, 2); + + OperationResult result = applySingle(new SignalCommand.PositionTest( + commandId, Id.ZERO, children.get(0), ListPosition.first())); + + assertTestResult(true, result); + } + + @Test + void positionTestFirst_isNotFirst_rejected() { + List children = insertChildren(Id.ZERO, 2); + + OperationResult result = applySingle(new SignalCommand.PositionTest( + commandId, Id.ZERO, children.get(1), ListPosition.first())); + + assertTestResult(false, result); + } + + @Test + void positionTestLast_isNotLast_rejected() { + List children = insertChildren(Id.ZERO, 2); + + OperationResult result = applySingle(new SignalCommand.PositionTest( + commandId, Id.ZERO, children.get(0), ListPosition.last())); + + assertTestResult(false, result); + } + + @Test + void positionTestLast_isLast_accepted() { + List children = insertChildren(Id.ZERO, 2); + + OperationResult result = applySingle(new SignalCommand.PositionTest( + commandId, Id.ZERO, children.get(1), ListPosition.last())); + + assertTestResult(true, result); + } + + @Test + void positionTestAfter_isAfter_accepted() { + List children = insertChildren(Id.ZERO, 2); + + OperationResult result = applySingle(new SignalCommand.PositionTest( + commandId, Id.ZERO, children.get(1), + new ListPosition(children.get(0), null))); + + assertTestResult(true, result); + } + + @Test + void positionTestAfter_itself_rejected() { + List children = insertChildren(Id.ZERO, 2); + + OperationResult result = applySingle(new SignalCommand.PositionTest( + commandId, Id.ZERO, children.get(0), + new ListPosition(children.get(0), null))); + + assertTestResult(false, result); + } + + @Test + void positionTestAfter_isNotAfter_rejected() { + List children = insertChildren(Id.ZERO, 2); + + OperationResult result = applySingle(new SignalCommand.PositionTest( + commandId, Id.ZERO, children.get(0), + new ListPosition(children.get(1), null))); + + assertTestResult(false, result); + } + + @Test + void positionTestBefore_isBefore_accepted() { + List children = insertChildren(Id.ZERO, 2); + + OperationResult result = applySingle(new SignalCommand.PositionTest( + commandId, Id.ZERO, children.get(0), + new ListPosition(null, children.get(1)))); + + assertTestResult(true, result); + } + + @Test + void positionTestBefore_itself_rejected() { + List children = insertChildren(Id.ZERO, 2); + + OperationResult result = applySingle(new SignalCommand.PositionTest( + commandId, Id.ZERO, children.get(0), + new ListPosition(null, children.get(0)))); + + assertTestResult(false, result); + } + + @Test + void positionTestBefore_isNotBefore_rejected() { + List children = insertChildren(Id.ZERO, 2); + + OperationResult result = applySingle(new SignalCommand.PositionTest( + commandId, Id.ZERO, children.get(1), + new ListPosition(null, children.get(0)))); + + assertTestResult(false, result); + } + + @Test + void positionTestBetween_isBetween_accepted() { + List children = insertChildren(Id.ZERO, 3); + + OperationResult result = applySingle(new SignalCommand.PositionTest( + commandId, Id.ZERO, children.get(1), + new ListPosition(children.get(0), children.get(2)))); + + assertTestResult(true, result); + } + + @Test + void positionTestBetween_notBetween_rejected() { + List children = insertChildren(Id.ZERO, 3); + + OperationResult result = applySingle(new SignalCommand.PositionTest( + commandId, Id.ZERO, children.get(1), + new ListPosition(children.get(2), children.get(1)))); + + assertTestResult(false, result); + } + + @Test + void positionTest_parentAlias_checksAliasTarget() { + Id child = insertChildren(Id.ZERO, 1).get(0); + + Id alias = createAlias(Id.ZERO); + + OperationResult result = applySingle(new SignalCommand.PositionTest( + commandId, alias, child, new ListPosition(null, null))); + + assertTestResult(true, result); + } + + @Test + void positionTest_childAlias_checkAliasTarget() { + Id child = insertChildren(Id.ZERO, 1).get(0); + + Id alias = createAlias(child); + + OperationResult result = applySingle(new SignalCommand.PositionTest( + commandId, Id.ZERO, alias, new ListPosition(null, null))); + + assertTestResult(true, result); + } + + @Test + void positionTest_aliasSiblings_checkAliasTargets() { + List children = insertChildren(Id.ZERO, 3); + Id first = createAlias(children.get(0)); + Id last = createAlias(children.get(2)); + + OperationResult result = applySingle( + new SignalCommand.PositionTest(commandId, Id.ZERO, + children.get(1), new ListPosition(first, last))); + + assertTestResult(true, result); + } + + @Test + void valueTest_sameValue_accepted() { + applySingle(new SignalCommand.SetCommand(Id.random(), Id.ZERO, + new TextNode("value"))); + + OperationResult result = applySingle(new SignalCommand.ValueTest( + commandId, Id.ZERO, new TextNode("value"))); + + assertTestResult(true, result); + } + + @Test + void valueTest_alias_dataNodeChecked() { + applySingle(new SignalCommand.SetCommand(Id.random(), Id.ZERO, + new TextNode("value"))); + + Id alias = createAlias(Id.ZERO); + + OperationResult result = applySingle(new SignalCommand.ValueTest( + commandId, alias, new TextNode("value"))); + + assertTestResult(true, result); + } + + @Test + void valueTest_otherValue_rejected() { + applySingle(new SignalCommand.SetCommand(Id.random(), Id.ZERO, + new TextNode("other"))); + + OperationResult result = applySingle(new SignalCommand.ValueTest( + commandId, Id.ZERO, new TextNode("value"))); + + assertTestResult(false, result); + } + + @Test + void valueTestNull_jsonNullValue_accepted() { + applySingle(new SignalCommand.SetCommand(Id.random(), Id.ZERO, + NullNode.getInstance())); + + OperationResult result = applySingle( + new SignalCommand.ValueTest(commandId, Id.ZERO, null)); + + assertTestResult(true, result); + } + + @Test + void valueTestJsonNull_nullValue_accepted() { + applySingle(new SignalCommand.SetCommand(Id.random(), Id.ZERO, null)); + + OperationResult result = applySingle(new SignalCommand.ValueTest( + commandId, Id.ZERO, NullNode.getInstance())); + + assertTestResult(true, result); + } + + @Test + void lastUpdateTest_sameValue_accepted() { + Id update = Id.random(); + applySingle(new SignalCommand.SetCommand(update, Id.ZERO, null)); + + OperationResult result = applySingle( + new SignalCommand.LastUpdateTest(commandId, Id.ZERO, update)); + + assertTestResult(true, result); + } + + @Test + void lastUpdateTest_alias_targetNodeChecked() { + Id update = Id.random(); + applySingle(new SignalCommand.SetCommand(update, Id.ZERO, null)); + + Id alias = createAlias(Id.ZERO); + OperationResult result = applySingle( + new SignalCommand.LastUpdateTest(commandId, alias, update)); + + assertTestResult(true, result); + } + + @Test + void lastUpdateTest_differentValue_rejected() { + Id update = Id.random(); + applySingle(new SignalCommand.SetCommand(update, Id.ZERO, null)); + + OperationResult result = applySingle(new SignalCommand.LastUpdateTest( + commandId, Id.ZERO, Id.random())); + + assertTestResult(false, result); + } + + @Test + void keyTestNoKey_noKey_accepted() { + OperationResult result = applySingle( + new SignalCommand.KeyTest(commandId, Id.ZERO, "key", Id.ZERO)); + + assertTestResult(true, result); + } + + @Test + void keyTestNoKey_keyPresent_rejected() { + applySingle(new SignalCommand.PutCommand(Id.random(), Id.ZERO, "key", + null)); + + OperationResult result = applySingle( + new SignalCommand.KeyTest(commandId, Id.ZERO, "key", Id.ZERO)); + + assertTestResult(false, result); + } + + @Test + void keyTestKeyPresent_keyPresent_accepted() { + applySingle(new SignalCommand.PutCommand(Id.random(), Id.ZERO, "key", + null)); + + OperationResult result = applySingle( + new SignalCommand.KeyTest(commandId, Id.ZERO, "key", null)); + + assertTestResult(true, result); + } + + @Test + void keyTestKeyPresent_noKey_rejected() { + OperationResult result = applySingle( + new SignalCommand.KeyTest(commandId, Id.ZERO, "key", null)); + + assertTestResult(false, result); + } + + @Test + void keyTestSpecificNode_nodePresent_accepted() { + Id child = Id.random(); + applySingle(new SignalCommand.PutCommand(child, Id.ZERO, "key", null)); + + OperationResult result = applySingle( + new SignalCommand.KeyTest(commandId, Id.ZERO, "key", child)); + + assertTestResult(true, result); + } + + @Test + void keyTestSpecificNode_noNode_rejected() { + Id child = Id.random(); + + OperationResult result = applySingle( + new SignalCommand.KeyTest(commandId, Id.ZERO, "key", child)); + + assertTestResult(false, result); + } + + @Test + void keyTestSpecificNode_otherKey_rejected() { + Id child = Id.random(); + applySingle( + new SignalCommand.PutCommand(child, Id.ZERO, "other", null)); + + OperationResult result = applySingle( + new SignalCommand.KeyTest(commandId, Id.ZERO, "key", child)); + + assertTestResult(false, result); + } + + @Test + void keyTestSpecificNode_otherNode_rejected() { + Id child = Id.random(); + applySingle( + new SignalCommand.PutCommand(child, Id.ZERO, "other", null)); + applySingle(new SignalCommand.PutCommand(Id.random(), Id.ZERO, "key", + null)); + + OperationResult result = applySingle( + new SignalCommand.KeyTest(commandId, Id.ZERO, "key", child)); + + assertTestResult(false, result); + } + + @Test + void keyTest_aliasParent_targetChecked() { + Id child = Id.random(); + applySingle(new SignalCommand.PutCommand(child, Id.ZERO, "key", null)); + + Id alias = createAlias(Id.ZERO); + OperationResult result = applySingle( + new SignalCommand.KeyTest(commandId, alias, "key", child)); + + assertTestResult(true, result); + } + + @Test + void keyTest_aliasChild_targetChecked() { + Id child = Id.random(); + applySingle(new SignalCommand.PutCommand(child, Id.ZERO, "key", null)); + + Id alias = createAlias(child); + OperationResult result = applySingle( + new SignalCommand.KeyTest(commandId, Id.ZERO, "key", alias)); + + assertTestResult(true, result); + } + + @Test + void transactionCommand_empty_noChange() { + OperationResult result = applySingle( + new SignalCommand.TransactionCommand(commandId, List.of())); + + // Check result object + Accept accept = assertAccepted(result); + assertEquals(Map.of(), accept.updates()); + + // Check revision state + assertUnchanged(); + } + + @Test + void transactionCommand_allAccepted_changesApplied() { + Id command1 = Id.random(); + Id command2 = Id.random(); + + Map results = revision.applyAndGetResults( + List.of(new SignalCommand.TransactionCommand(commandId, + List.of(new SignalCommand.SetCommand(command1, Id.ZERO, + new TextNode("value")), + new SignalCommand.PutCommand(command2, Id.ZERO, + "key", null))))); + + // Check result objects + Accept transaction = assertAccepted(results.get(commandId)); + assertEquals(2, transaction.updates().size()); + // Verify that modifications from both commands are merged + TreeModification rootModification = transaction.updates().get(Id.ZERO); + assertEquals(Node.EMPTY, rootModification.oldNode()); + assertEquals(revision.nodes().get(Id.ZERO), rootModification.newNode()); + + Accept set = assertAccepted(results.get(command1)); + Data setModificationNode = (Data) set.onlyUpdate().newNode(); + assertEquals("value", setModificationNode.value().textValue()); + assertEquals(Map.of(), setModificationNode.mapChildren()); + + // Check revision state + assertValue(Id.ZERO, "value"); + assertMapChildren(Id.ZERO, Map.of("key", command2)); + } + + @Test + void transactionCommand_lastRejected_allRejected() { + Id command1 = Id.random(); + Id command2 = Id.random(); + + Map results = revision.applyAndGetResults( + List.of(new SignalCommand.TransactionCommand(commandId, + List.of(new SignalCommand.SetCommand(command1, Id.ZERO, + new TextNode("value")), + new SignalCommand.ValueTest(command2, Id.ZERO, + null))))); + + // Check result objects + assertEquals(Set.of(command1, command2, commandId), results.keySet()); + for (OperationResult subResult : results.values()) { + assertFalse(subResult.accepted()); + } + + // Check revision state + assertUnchanged(); + } + + @Test + void transactionCommand_nestedTransactions_allApplied() { + Id set = Id.random(); + Id innerTransaction = Id.random(); + + Map results = revision.applyAndGetResults( + List.of(new SignalCommand.TransactionCommand(commandId, + List.of(new SignalCommand.TransactionCommand( + innerTransaction, + List.of(new SignalCommand.SetCommand(set, + Id.ZERO, new TextNode("value")))))))); + + // Check result objects + assertEquals(3, results.size()); + assertAccepted(results.get(commandId)); + assertAccepted(results.get(innerTransaction)); + assertAccepted(results.get(set)); + + // Check revision state + assertValue(Id.ZERO, "value"); + } + + @Test + void snapshotEvent_withNodes_loaded() { + MutableTreeRevision copy = new MutableTreeRevision(revision); + + Id child = Id.random(); + copy.apply(new SignalCommand.PutCommand(child, Id.ZERO, "key", null), + null); + + OperationResult result = applySingle( + new SignalCommand.SnapshotCommand(commandId, copy.nodes())); + + // Check result objects + Accept accept = assertAccepted(result); + // This is a special case + assertEquals(2, accept.updates().size()); + + // Check revision state + assertMapChildren(Id.ZERO, Map.of("key", child)); + } + + @Test + void clearOwnerEvent_ownedListNode_removed() { + Id node = Id.random(); + applySingle(new SignalCommand.InsertCommand(node, Id.ZERO, + revision.ownerId(), null, ListPosition.last())); + + assertEquals(node, revision.originalInserts().get(node).commandId()); + + Id childOfOwned = Id.random(); + applySingle( + new SignalCommand.PutCommand(childOfOwned, node, "key", null)); + + OperationResult result = applySingle( + new SignalCommand.ClearOwnerCommand(commandId, + revision.ownerId())); + + // Check result object + Accept accept = assertAccepted(result); + assertEquals(3, accept.updates().size()); + + // Check revision state + assertListChildren(Id.ZERO); + assertEquals(Set.of(Id.ZERO), revision.nodes().keySet()); + assertEquals(Map.of(), revision.originalInserts()); + } + + @Test + void clearOwnerEvent_ownedMapNode_removed() { + Id node = Id.random(); + applySingle(new SignalCommand.PutIfAbsentCommand(node, Id.ZERO, + revision.ownerId(), "key", null)); + + assertEquals(node, revision.originalInserts().get(node).commandId()); + + OperationResult result = applySingle( + new SignalCommand.ClearOwnerCommand(commandId, + revision.ownerId())); + + // Check result object + Accept accept = assertAccepted(result); + assertEquals(2, accept.updates().size()); + + // Check revision state + assertMapChildren(Id.ZERO, Map.of()); + assertEquals(Map.of(), revision.originalInserts()); + } + + @Test + void clearOwnerEvent_ownedByOther_retained() { + Id node = Id.random(); + applySingle(new SignalCommand.PutIfAbsentCommand(node, Id.ZERO, + Id.random(), "key", null)); + + assertEquals(Map.of(), revision.originalInserts()); + + OperationResult result = applySingle( + new SignalCommand.ClearOwnerCommand(commandId, + revision.ownerId())); + + // Check result object + Accept accept = assertAccepted(result); + assertEquals(0, accept.updates().size()); + + // Check revision state + assertMapChildren(Id.ZERO, Map.of("key", node)); + } + + @Test + void clearOwnerEvent_ownedByNone_retained() { + Id node = Id.random(); + applySingle(new SignalCommand.PutIfAbsentCommand(node, Id.ZERO, null, + "key", null)); + + assertEquals(Map.of(), revision.originalInserts()); + + OperationResult result = applySingle( + new SignalCommand.ClearOwnerCommand(commandId, + revision.ownerId())); + + // Check result object + Accept accept = assertAccepted(result); + assertEquals(0, accept.updates().size()); + + // Check revision state + assertMapChildren(Id.ZERO, Map.of("key", node)); + } + + private OperationResult applySingle(SignalCommand command) { + Map results = revision + .applyAndGetResults(List.of(command)); + assertEquals(1, results.size()); + + return results.get(command.commandId()); + } + + private void assertNullValue(Id nodeId) { + assertNull(revision.data(nodeId).get().value()); + } + + private void assertValue(Id nodeId, String expectedValue) { + assertEquals(new TextNode(expectedValue), + revision.data(nodeId).get().value()); + } + + private void assertValue(Id nodeId, double expectedValue) { + assertEquals(new DoubleNode(expectedValue), + revision.data(nodeId).get().value()); + } + + private void assertListChildren(Id nodeId, Id... expectedChildren) { + assertEquals(List.of(expectedChildren), + revision.data(nodeId).get().listChildren()); + + for (Id child : expectedChildren) { + assertEquals(nodeId, revision.data(child).get().parent()); + } + } + + private void assertMapChildren(Id nodeId, + Map expectedChildren) { + assertEquals(expectedChildren, + revision.data(nodeId).get().mapChildren()); + + for (Id child : expectedChildren.values()) { + assertEquals(nodeId, revision.data(child).get().parent()); + } + } + + private void assertUnchanged() { + assertEquals(Map.of(Id.ZERO, Node.EMPTY), revision.nodes()); + } + + private static void assertTestResult(boolean expectedResult, + OperationResult result) { + assertEquals(expectedResult, result.accepted()); + if (result.accepted()) { + assertEquals(0, ((Accept) result).updates().size()); + } + } + + private static Accept assertAccepted(OperationResult result) { + assertTrue(result.accepted()); + return (Accept) result; + } + + private static Data assertSingleDataChange(OperationResult result) { + Accept accept = assertAccepted(result); + TreeModification onlyUpdate = accept.onlyUpdate(); + return (Data) onlyUpdate.newNode(); + } + + private List insertChildren(Id parent, int count) { + return IntStream.range(0, count).mapToObj(ignore -> Id.random()) + .peek(id -> applySingle(new SignalCommand.InsertCommand(id, + parent, null, null, ListPosition.last()))) + .toList(); + } + + private Id createAlias(Id target) { + Id alias = Id.random(); + revision.nodes().put(alias, new Node.Alias(target)); + return alias; + } +} diff --git a/signals/src/test/java/com/vaadin/signals/impl/OperationResultTest.java b/signals/src/test/java/com/vaadin/signals/impl/OperationResultTest.java new file mode 100644 index 00000000000..9019d7fb3f8 --- /dev/null +++ b/signals/src/test/java/com/vaadin/signals/impl/OperationResultTest.java @@ -0,0 +1,23 @@ +package com.vaadin.signals.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import com.vaadin.signals.Id; + +public class OperationResultTest { + + @Test + void rejectAll() { + Map in = Map.of(new Id(1), OperationResult.ok(), + new Id(2), OperationResult.fail("Original")); + + Map out = OperationResult.rejectAll(in, "New"); + + assertEquals(Map.of(new Id(1), OperationResult.fail("New"), new Id(2), + OperationResult.fail("Original")), out); + } +} diff --git a/signals/src/test/java/com/vaadin/signals/impl/SnapshotTest.java b/signals/src/test/java/com/vaadin/signals/impl/SnapshotTest.java new file mode 100644 index 00000000000..05c663668d2 --- /dev/null +++ b/signals/src/test/java/com/vaadin/signals/impl/SnapshotTest.java @@ -0,0 +1,51 @@ +package com.vaadin.signals.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Map; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import com.vaadin.signals.Id; +import com.vaadin.signals.Node; + +public class SnapshotTest { + + @Test + void emptyConstructor_withoutMaxNode_hasOnlyZeroNode() { + Id id = Id.random(); + + Snapshot snapshot = new Snapshot(id, false); + + assertEquals(id, snapshot.ownerId()); + + assertEquals(Set.of(Id.ZERO), snapshot.nodes().keySet()); + } + + @Test + void emptyConstructor_withMaxNode_hasZeroAndMaxNodes() { + Id id = Id.random(); + + Snapshot snapshot = new Snapshot(id, true); + + assertEquals(id, snapshot.ownerId()); + + assertEquals(Set.of(Id.ZERO, Id.MAX), snapshot.nodes().keySet()); + } + + @Test + void copyingConstructor_baseUpdated_snapshotFrozen() { + MutableTreeRevision mutable = new MutableTreeRevision( + new Snapshot(Id.random(), false)); + + Snapshot snapshot = new Snapshot(mutable); + + mutable.nodes().put(Id.random(), Node.EMPTY); + mutable.originalInserts().put(Id.random(), null); + + assertEquals(Set.of(Id.ZERO), snapshot.nodes().keySet()); + assertEquals(Map.of(), snapshot.originalInserts()); + } + +} diff --git a/signals/src/test/java/com/vaadin/signals/impl/TreeRevisionTest.java b/signals/src/test/java/com/vaadin/signals/impl/TreeRevisionTest.java new file mode 100644 index 00000000000..e135db4f175 --- /dev/null +++ b/signals/src/test/java/com/vaadin/signals/impl/TreeRevisionTest.java @@ -0,0 +1,204 @@ +package com.vaadin.signals.impl; + +import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import com.vaadin.signals.Id; +import com.vaadin.signals.ListSignal.ListPosition; +import com.vaadin.signals.Node; +import com.vaadin.signals.SignalCommand; + +public class TreeRevisionTest { + private static class MutableTestRevision extends TreeRevision { + public MutableTestRevision() { + super(Id.ZERO, + new HashMap<>( + Map.of(Id.ZERO, + new Node.Data(null, Id.ZERO, null, null, + List.of(), Map.of()))), + new HashMap<>()); + } + } + + @Test + void assertValidTree_emptyTree_passes() { + MutableTestRevision revision = new MutableTestRevision(); + + assertTrue(revision.assertValidTree()); + } + + @Test + void assertValidTree_validTree_passes() { + MutableTestRevision revision = new MutableTestRevision(); + + Id listChildId = Id.random(); + Id mapChildId = Id.random(); + + revision.nodes().put(Id.ZERO, new Node.Data(null, Id.ZERO, null, null, + List.of(listChildId), Map.of("key", mapChildId))); + revision.nodes().put(listChildId, new Node.Data(Id.ZERO, Id.ZERO, + Id.ZERO, null, List.of(), Map.of())); + revision.nodes().put(mapChildId, new Node.Data(Id.ZERO, Id.ZERO, + Id.random(), null, List.of(), Map.of())); + revision.nodes().put(Id.random(), new Node.Alias(listChildId)); + + revision.originalInserts().put(listChildId, + new SignalCommand.InsertCommand(listChildId, Id.ZERO, Id.ZERO, + null, ListPosition.first())); + + assertTrue(revision.assertValidTree()); + } + + @Test + void assertValidTree_rootMissing_fails() { + MutableTestRevision revision = new MutableTestRevision(); + + revision.nodes().remove(Id.ZERO); + + assertThrows(AssertionError.class, revision::assertValidTree); + } + + @Test + void assertValidTree_rootHasParent_fails() { + MutableTestRevision revision = new MutableTestRevision(); + + revision.nodes().put(Id.ZERO, createSimpleNode(Id.random())); + + assertThrows(AssertionError.class, revision::assertValidTree); + } + + @Test + void assertValidTree_detachedNode_fails() { + MutableTestRevision revision = new MutableTestRevision(); + + revision.nodes().put(Id.random(), createSimpleNode(Id.random())); + + assertThrows(AssertionError.class, revision::assertValidTree); + } + + @Test + void assertValidTree_missingListChild_fails() { + MutableTestRevision revision = new MutableTestRevision(); + + revision.nodes().put(Id.ZERO, new Node.Data(null, Id.ZERO, null, null, + List.of(Id.random()), Map.of())); + + assertThrows(AssertionError.class, revision::assertValidTree); + } + + @Test + void assertValidTree_missingMapChild_fails() { + MutableTestRevision revision = new MutableTestRevision(); + + revision.nodes().put(Id.ZERO, new Node.Data(null, Id.ZERO, null, null, + List.of(), Map.of("key", Id.random()))); + + assertThrows(AssertionError.class, revision::assertValidTree); + } + + @Test + void assertValidTree_aliasChild_fails() { + MutableTestRevision revision = new MutableTestRevision(); + + Id childId = Id.random(); + + revision.nodes().put(Id.ZERO, new Node.Data(null, Id.ZERO, null, null, + List.of(childId), Map.of())); + revision.nodes().put(childId, new Node.Alias(Id.ZERO)); + + assertThrows(AssertionError.class, revision::assertValidTree); + + } + + @Test + void assertValidTree_childWithoutParentPointer_fails() { + MutableTestRevision revision = new MutableTestRevision(); + + Id childId = Id.random(); + + revision.nodes().put(Id.ZERO, new Node.Data(null, Id.ZERO, null, null, + List.of(childId), Map.of())); + revision.nodes().put(childId, createSimpleNode(null)); + + assertThrows(AssertionError.class, revision::assertValidTree); + } + + @Test + void assertValidTree_childWithoutMissingParent_fails() { + MutableTestRevision revision = new MutableTestRevision(); + + Id childId = Id.random(); + + revision.nodes().put(Id.ZERO, new Node.Data(null, Id.ZERO, null, null, + List.of(childId), Map.of())); + revision.nodes().put(childId, createSimpleNode(Id.random())); + + assertThrows(AssertionError.class, revision::assertValidTree); + } + + @Test + void assertValidTree_childWithWrongParent_fails() { + MutableTestRevision revision = new MutableTestRevision(); + + Id childId = Id.random(); + Id otherChildId = Id.random(); + + revision.nodes().put(Id.ZERO, new Node.Data(null, Id.ZERO, null, null, + List.of(childId, otherChildId), Map.of())); + revision.nodes().put(childId, createSimpleNode(Id.ZERO)); + revision.nodes().put(otherChildId, createSimpleNode(childId)); + + assertThrows(AssertionError.class, revision::assertValidTree); + } + + @Test + void assertValidTree_aliasTargetMissing_fails() { + MutableTestRevision revision = new MutableTestRevision(); + + revision.nodes().put(Id.random(), new Node.Alias(Id.random())); + + assertThrows(AssertionError.class, revision::assertValidTree); + } + + @Test + void assertValidTree_aliasTargetOtherAlias_fails() { + MutableTestRevision revision = new MutableTestRevision(); + + Id aliasId = Id.random(); + + revision.nodes().put(aliasId, new Node.Alias(Id.ZERO)); + revision.nodes().put(Id.random(), new Node.Alias(aliasId)); + + assertThrows(AssertionError.class, revision::assertValidTree); + } + + @Test + void assertValidTree_missingOriginalInsert_fails() { + MutableTestRevision revision = new MutableTestRevision(); + + revision.nodes().put(Id.ZERO, new Node.Data(null, Id.ZERO, Id.ZERO, + null, List.of(), Map.of())); + + assertThrows(AssertionError.class, revision::assertValidTree); + } + + @Test + void assertValidTree_redundantOriginalInsert_fails() { + MutableTestRevision revision = new MutableTestRevision(); + + revision.originalInserts().put(Id.ZERO, new SignalCommand.InsertCommand( + Id.ZERO, Id.ZERO, Id.ZERO, null, ListPosition.first())); + + assertThrows(AssertionError.class, revision::assertValidTree); + } + + private static Node.Data createSimpleNode(Id parent) { + return new Node.Data(parent, Id.ZERO, null, null, List.of(), Map.of()); + } +} From eea874065fae8a84779bd18097e8269470a5fab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leif=20=C3=85strand?= Date: Fri, 17 Jan 2025 18:56:36 +0200 Subject: [PATCH 2/3] Add some missing Javadocs --- .../src/main/java/com/vaadin/signals/Id.java | 7 +++++ .../java/com/vaadin/signals/ListSignal.java | 2 +- .../main/java/com/vaadin/signals/Node.java | 31 +++++++++++++++++-- .../com/vaadin/signals/SignalCommand.java | 2 +- 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/signals/src/main/java/com/vaadin/signals/Id.java b/signals/src/main/java/com/vaadin/signals/Id.java index 060a0bafa73..44eefe136d2 100644 --- a/signals/src/main/java/com/vaadin/signals/Id.java +++ b/signals/src/main/java/com/vaadin/signals/Id.java @@ -41,6 +41,13 @@ public record Id(long value) implements Comparable { private static final Encoder base64Encoder = Base64.getEncoder() .withoutPadding(); + /** + * Creates a random id. Randomness is only needed to reduce the risk of + * collisions but there's no security impact from being able to guess random + * ids. + * + * @return a random id, not null + */ public static Id random() { var random = ThreadLocalRandom.current(); diff --git a/signals/src/main/java/com/vaadin/signals/ListSignal.java b/signals/src/main/java/com/vaadin/signals/ListSignal.java index cd1e406c1da..d7c34f53f1b 100644 --- a/signals/src/main/java/com/vaadin/signals/ListSignal.java +++ b/signals/src/main/java/com/vaadin/signals/ListSignal.java @@ -1,6 +1,6 @@ package com.vaadin.signals; -/* +/** * The rest of this class will be implemented later. */ public class ListSignal { diff --git a/signals/src/main/java/com/vaadin/signals/Node.java b/signals/src/main/java/com/vaadin/signals/Node.java index f7c57247ce3..8b46ee21e91 100644 --- a/signals/src/main/java/com/vaadin/signals/Node.java +++ b/signals/src/main/java/com/vaadin/signals/Node.java @@ -57,8 +57,35 @@ public record Alias(Id target) implements Node { * node has no map children */ public record Data(Id parent, Id lastUpdate, Id scopeOwner, JsonNode value, - List listChildren, - Map mapChildren) implements Node { + List listChildren, Map mapChildren) + implements Node { + /** + * Creates a new data node. + * + * @param parent + * the parent id, or null for the root node + * @param lastUpdate + * a unique id for the update that last updated this data + * node, not null + * @param scopeOwner + * the id of the external owner of this node, or + * null if the node has no owner. Any node with + * an owner is deleted if the owner is disconnected. + * @param value + * the JSON value of this node, or null if there + * is no value + * @param listChildren + * a list of child ids, or the an list if the node has no + * list children + * @param mapChildren + * a sequenced map from key to child id, or an empty map if + * the node has no map children + */ + /* + * There's no point in copying the record components here since they are + * already documented on the top level, but the Javadoc checker insist + * that this constructor also has full documentation... + */ public Data { Objects.requireNonNull(lastUpdate); diff --git a/signals/src/main/java/com/vaadin/signals/SignalCommand.java b/signals/src/main/java/com/vaadin/signals/SignalCommand.java index cfc18ffd912..922714013db 100644 --- a/signals/src/main/java/com/vaadin/signals/SignalCommand.java +++ b/signals/src/main/java/com/vaadin/signals/SignalCommand.java @@ -75,7 +75,7 @@ default Id nodeId() { * command, not null * @param nodeId * id of the node to check, not null - * @param expecedValue + * @param expectedValue * the expected value */ public record ValueTest(Id commandId, Id nodeId, From 371348d90f65f69ec667041d606c9df9a27803ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leif=20=C3=85strand?= Date: Fri, 17 Jan 2025 18:59:18 +0200 Subject: [PATCH 3/3] Add license headers --- .../src/main/java/com/vaadin/signals/Id.java | 17 ++++++++++++++- .../java/com/vaadin/signals/ListSignal.java | 15 +++++++++++++ .../main/java/com/vaadin/signals/Node.java | 21 ++++++++++++++++--- .../com/vaadin/signals/SignalCommand.java | 15 +++++++++++++ .../signals/impl/MutableTreeRevision.java | 15 +++++++++++++ .../vaadin/signals/impl/OperationResult.java | 15 +++++++++++++ .../com/vaadin/signals/impl/Snapshot.java | 15 +++++++++++++ .../com/vaadin/signals/impl/TreeRevision.java | 15 +++++++++++++ .../test/java/com/vaadin/signals/IdTest.java | 15 +++++++++++++ .../signals/impl/MutableTreeRevisionTest.java | 15 +++++++++++++ .../signals/impl/OperationResultTest.java | 15 +++++++++++++ .../com/vaadin/signals/impl/SnapshotTest.java | 15 +++++++++++++ .../vaadin/signals/impl/TreeRevisionTest.java | 15 +++++++++++++ 13 files changed, 199 insertions(+), 4 deletions(-) diff --git a/signals/src/main/java/com/vaadin/signals/Id.java b/signals/src/main/java/com/vaadin/signals/Id.java index 44eefe136d2..95a48f27d51 100644 --- a/signals/src/main/java/com/vaadin/signals/Id.java +++ b/signals/src/main/java/com/vaadin/signals/Id.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2025 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ package com.vaadin.signals; import java.math.BigInteger; @@ -45,7 +60,7 @@ public record Id(long value) implements Comparable { * Creates a random id. Randomness is only needed to reduce the risk of * collisions but there's no security impact from being able to guess random * ids. - * + * * @return a random id, not null */ public static Id random() { diff --git a/signals/src/main/java/com/vaadin/signals/ListSignal.java b/signals/src/main/java/com/vaadin/signals/ListSignal.java index d7c34f53f1b..27c78eec232 100644 --- a/signals/src/main/java/com/vaadin/signals/ListSignal.java +++ b/signals/src/main/java/com/vaadin/signals/ListSignal.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2025 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ package com.vaadin.signals; /** diff --git a/signals/src/main/java/com/vaadin/signals/Node.java b/signals/src/main/java/com/vaadin/signals/Node.java index 8b46ee21e91..eeea5f08b46 100644 --- a/signals/src/main/java/com/vaadin/signals/Node.java +++ b/signals/src/main/java/com/vaadin/signals/Node.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2025 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ package com.vaadin.signals; import java.util.List; @@ -57,11 +72,11 @@ public record Alias(Id target) implements Node { * node has no map children */ public record Data(Id parent, Id lastUpdate, Id scopeOwner, JsonNode value, - List listChildren, Map mapChildren) - implements Node { + List listChildren, + Map mapChildren) implements Node { /** * Creates a new data node. - * + * * @param parent * the parent id, or null for the root node * @param lastUpdate diff --git a/signals/src/main/java/com/vaadin/signals/SignalCommand.java b/signals/src/main/java/com/vaadin/signals/SignalCommand.java index 922714013db..9e5c28320a7 100644 --- a/signals/src/main/java/com/vaadin/signals/SignalCommand.java +++ b/signals/src/main/java/com/vaadin/signals/SignalCommand.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2025 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ package com.vaadin.signals; import java.util.List; diff --git a/signals/src/main/java/com/vaadin/signals/impl/MutableTreeRevision.java b/signals/src/main/java/com/vaadin/signals/impl/MutableTreeRevision.java index 9248fa9dd48..482d2ebcafc 100644 --- a/signals/src/main/java/com/vaadin/signals/impl/MutableTreeRevision.java +++ b/signals/src/main/java/com/vaadin/signals/impl/MutableTreeRevision.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2025 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ package com.vaadin.signals.impl; import java.util.ArrayList; diff --git a/signals/src/main/java/com/vaadin/signals/impl/OperationResult.java b/signals/src/main/java/com/vaadin/signals/impl/OperationResult.java index 2afcbb8c3e3..f14bcf3b52e 100644 --- a/signals/src/main/java/com/vaadin/signals/impl/OperationResult.java +++ b/signals/src/main/java/com/vaadin/signals/impl/OperationResult.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2025 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ package com.vaadin.signals.impl; import java.util.HashMap; diff --git a/signals/src/main/java/com/vaadin/signals/impl/Snapshot.java b/signals/src/main/java/com/vaadin/signals/impl/Snapshot.java index e31f4aca6aa..1bafccbaf0b 100644 --- a/signals/src/main/java/com/vaadin/signals/impl/Snapshot.java +++ b/signals/src/main/java/com/vaadin/signals/impl/Snapshot.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2025 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ package com.vaadin.signals.impl; import java.util.Map; diff --git a/signals/src/main/java/com/vaadin/signals/impl/TreeRevision.java b/signals/src/main/java/com/vaadin/signals/impl/TreeRevision.java index 9227a615eab..e5ea22b70f2 100644 --- a/signals/src/main/java/com/vaadin/signals/impl/TreeRevision.java +++ b/signals/src/main/java/com/vaadin/signals/impl/TreeRevision.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2025 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ package com.vaadin.signals.impl; import java.util.HashSet; diff --git a/signals/src/test/java/com/vaadin/signals/IdTest.java b/signals/src/test/java/com/vaadin/signals/IdTest.java index a91a64e03d9..f6935630258 100644 --- a/signals/src/test/java/com/vaadin/signals/IdTest.java +++ b/signals/src/test/java/com/vaadin/signals/IdTest.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2025 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ package com.vaadin.signals; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/signals/src/test/java/com/vaadin/signals/impl/MutableTreeRevisionTest.java b/signals/src/test/java/com/vaadin/signals/impl/MutableTreeRevisionTest.java index 634e281f976..506c8c1500c 100644 --- a/signals/src/test/java/com/vaadin/signals/impl/MutableTreeRevisionTest.java +++ b/signals/src/test/java/com/vaadin/signals/impl/MutableTreeRevisionTest.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2025 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ package com.vaadin.signals.impl; import static org.junit.Assert.assertEquals; diff --git a/signals/src/test/java/com/vaadin/signals/impl/OperationResultTest.java b/signals/src/test/java/com/vaadin/signals/impl/OperationResultTest.java index 9019d7fb3f8..8444528b1cf 100644 --- a/signals/src/test/java/com/vaadin/signals/impl/OperationResultTest.java +++ b/signals/src/test/java/com/vaadin/signals/impl/OperationResultTest.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2025 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ package com.vaadin.signals.impl; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/signals/src/test/java/com/vaadin/signals/impl/SnapshotTest.java b/signals/src/test/java/com/vaadin/signals/impl/SnapshotTest.java index 05c663668d2..1671ff5326e 100644 --- a/signals/src/test/java/com/vaadin/signals/impl/SnapshotTest.java +++ b/signals/src/test/java/com/vaadin/signals/impl/SnapshotTest.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2025 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ package com.vaadin.signals.impl; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/signals/src/test/java/com/vaadin/signals/impl/TreeRevisionTest.java b/signals/src/test/java/com/vaadin/signals/impl/TreeRevisionTest.java index e135db4f175..b16f7d6d346 100644 --- a/signals/src/test/java/com/vaadin/signals/impl/TreeRevisionTest.java +++ b/signals/src/test/java/com/vaadin/signals/impl/TreeRevisionTest.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2025 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ package com.vaadin.signals.impl; import static org.junit.Assert.assertTrue;