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 3d7bc71d162..c94f9bc879b 100644 --- a/signals/src/main/java/com/vaadin/signals/impl/MutableTreeRevision.java +++ b/signals/src/main/java/com/vaadin/signals/impl/MutableTreeRevision.java @@ -25,12 +25,10 @@ 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 java.util.function.Function; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.DoubleNode; @@ -49,7 +47,6 @@ import com.vaadin.signals.SignalCommand.IncrementCommand; import com.vaadin.signals.SignalCommand.InsertCommand; import com.vaadin.signals.SignalCommand.KeyCondition; -import com.vaadin.signals.SignalCommand.ConditionCommand; import com.vaadin.signals.SignalCommand.LastUpdateCondition; import com.vaadin.signals.SignalCommand.PositionCondition; import com.vaadin.signals.SignalCommand.PutCommand; @@ -70,235 +67,218 @@ */ 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. + * Helper for accessing nodes in a consistent way regardless of whether the + * nodes are loaded directly from the map in a revision or also checked for + * overrides in a result builder. */ - private class TreeManipulator { - private final Map updatedNodes = new HashMap<>(); - private final Set detachedNodes = new HashSet<>(); - private final Map originalInserts = new HashMap<>(); + private static abstract class NodeLookup { + protected abstract Node node(Id nodeId); - private final SignalCommand command; + protected boolean isChildAt(List children, int index, + Id expectedChild) { + assert expectedChild != null; - /** - * 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 CommandResult 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 subCommandResults; - - public TreeManipulator(SignalCommand command) { - this.command = command; + if (index < 0 || index >= children.size()) { + return false; + } + + return isSameNode(children.get(index), expectedChild); } - private void setResult(CommandResult result) { - assert this.result == null; - this.result = result; + protected Data data(Id nodeId) { + Node node = node(resolveAlias(nodeId)); + + return (Data) node; } - private void fail(String reason) { - setResult(CommandResult.fail(reason)); + protected boolean isSameNode(Id a, Id b) { + return Objects.equals(resolveAlias(a), resolveAlias(b)); } - private Id resolveAlias(Id nodeId) { - Node dataOrAlias = updatedNodes.get(nodeId); - if (dataOrAlias == null) { - dataOrAlias = nodes().get(nodeId); - } + protected Id resolveAlias(Id id) { + Node node = node(id); - if (dataOrAlias instanceof Alias alias) { + if (node instanceof Alias alias) { return alias.target(); } else { - return nodeId; + return id; } } - private Optional data(Id nodeId) { + protected ResolvedData resolveData(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)); + Node node = node(id); + if (node == null) { + return null; } else { - return MutableTreeRevision.this.data(id); + return new ResolvedData(id, (Data) node); } } + } - 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 class DirectNodeLookup extends NodeLookup { + @Override + protected Node node(Id nodeId) { + return nodes().get(nodeId); } + } - private void updateData(Id nodeId, UnaryOperator updater) { - useData(nodeId, (node, id) -> { - Data updatedNode = updater.apply(node); - if (updatedNode != node) { - updatedNodes.put(id, updatedNode); - } - }); - } + private record ResolvedData(Id resolvedId, Data data) { + } - private JsonNode value(Id nodeId) { - return data(nodeId).map(Data::value).orElse(null); - } + /** + * Gathers and collects all state related to applying a command that affects + * multiple nodes. To help with commands that consist of multiple steps, any + * node updates are tracked and subsequent node lookup uses the updates + * nodes rather than the originals. + */ + private class ResultBuilder extends NodeLookup { + private final Map updatedNodes = new HashMap<>(); + private final Set detachedNodes = new HashSet<>(); + private final Map originalInserts = new HashMap<>(); - private void setValue(Id nodeId, JsonNode value) { - updateData(nodeId, - node -> new Data(node.parent(), command.commandId(), - node.scopeOwner(), value, node.listChildren(), - node.mapChildren())); - } + private final SignalCommand command; - private Optional> listChildren(Id parentId) { - return data(parentId).map(Data::listChildren); + public ResultBuilder(SignalCommand command) { + this.command = command; } - private boolean isChildAt(Id parentId, int index, Id expectedChild) { - assert expectedChild != null; + private Reject fail(String reason) { + return CommandResult.fail(reason); + } - if (index < 0) { - return false; + @Override + protected Node node(Id nodeId) { + if (detachedNodes.contains(nodeId)) { + return null; } - Id idAtIndex = listChildren(parentId).map(children -> { - if (index >= children.size()) { - return null; - } - return children.get(index); - }).orElse(null); - - return isSameNode(idAtIndex, expectedChild); - } + Node node = updatedNodes.get(nodeId); + if (node != null) { + return node; + } - private Optional mapChild(Id nodeId, String key) { - return data(nodeId).map(Data::mapChildren) - .map(children -> children.get(key)); + return nodes().get(nodeId); } - private boolean isSameNode(Id a, Id b) { - return Objects.equals(resolveAlias(a), resolveAlias(b)); - } + private Reject detach(Id nodeId) { + ResolvedData resolved = resolveData(nodeId); + if (resolved == null) { + return fail("Node not found"); + } - private boolean detach(Id nodeId) { - useData(nodeId, (node, id) -> { - if (id.equals(Id.ZERO)) { - fail("Cannot detach the root"); - return; - } + Data node = resolved.data(); + Id id = resolved.resolvedId(); - Id parentId = node.parent(); - if (parentId == null) { - fail("Node is not attached"); - return; - } + if (id.equals(Id.ZERO)) { + return fail("Cannot detach the root"); + } - Data parentData = data(parentId).get(); + Id parentId = node.parent(); + if (parentId == null) { + return fail("Node is not attached"); + } - String key = parentData.mapChildren().entrySet().stream() - .filter(entry -> entry.getValue().equals(id)).findAny() - .map(Entry::getKey).orElse(null); + Data parentData = data(parentId); - if (key != null) { - updatedNodes.put(parentId, updateMapChildren(parentData, - map -> map.remove(key))); - } else { - updatedNodes.put(parentId, updateListChildren(parentData, - list -> list.remove(id))); - } + String key = parentData.mapChildren().entrySet().stream() + .filter(entry -> entry.getValue().equals(id)).findAny() + .map(Entry::getKey).orElse(null); - detachedNodes.add(id); - }); + if (key != null) { + updateMapChildren(parentId, map -> { + map.remove(key); + return null; + }); + } else { + updateListChildren(parentId, list -> { + list.remove(id); + return null; + }); + } - // Check if any error was reported - return result == null; + detachedNodes.add(id); + return null; } - private Data updateMapChildren(Data node, - Consumer> mapUpdater) { + private Reject updateMapChildren(Id resolvedParentId, + Function, CommandResult.Reject> mapUpdater) { + Data node = data(resolvedParentId); + LinkedHashMap map = new LinkedHashMap<>( node.mapChildren()); - mapUpdater.accept(map); + Reject maybeError = mapUpdater.apply(map); + if (maybeError != null) { + return maybeError; + } - return new Data(node.parent(), command.commandId(), - node.scopeOwner(), node.value(), node.listChildren(), - Collections.unmodifiableMap(map)); + updatedNodes.put(resolvedParentId, new Data(node.parent(), + command.commandId(), node.scopeOwner(), node.value(), + node.listChildren(), Collections.unmodifiableMap(map))); + + return null; } - private Data updateListChildren(Data node, - Consumer> listUpdater) { + private Reject updateListChildren(Id resolvedParentId, + Function, CommandResult.Reject> listUpdater) { + Data node = data(resolvedParentId); + ArrayList list = new ArrayList<>(node.listChildren()); - listUpdater.accept(list); + Reject maybeError = listUpdater.apply(list); + if (maybeError != null) { + return maybeError; + } - return new Data(node.parent(), command.commandId(), - node.scopeOwner(), node.value(), - Collections.unmodifiableList(list), node.mapChildren()); + updatedNodes.put(resolvedParentId, new Data(node.parent(), + command.commandId(), node.scopeOwner(), node.value(), + Collections.unmodifiableList(list), node.mapChildren())); + return null; } - private void attach(Id parentId, Id childId, - BiFunction attacher) { - if (result != null) { - return; - } - + private Reject attach(Id parentId, Id childId, + BiFunction attacher) { Id resolvedParentId = resolveAlias(parentId); Id resolvedChildId = resolveAlias(childId); if (!detachedNodes.contains(resolvedChildId)) { - fail("Node is not detached"); - return; + return fail("Node is not detached"); } Id ancestor = resolvedParentId; while (ancestor != null) { - if (ancestor.equals(childId)) { - fail("Cannot attach to own descendant"); - return; + if (ancestor.equals(resolvedChildId)) { + return fail("Cannot attach to own descendant"); } - ancestor = data(ancestor).map(Data::parent).orElse(null); + ancestor = data(ancestor).parent(); } - useData(parentId, (node, id) -> { - // Mark as detached only after the last error condition check - // done by useData - detachedNodes.remove(resolvedChildId); + detachedNodes.remove(resolvedChildId); - Data updated = attacher.apply(node, resolvedChildId); - if (result == null) { - Data child = data(resolvedChildId).get(); + Reject maybeError = attacher.apply(resolvedParentId, + resolvedChildId); + if (maybeError != null) { + return maybeError; + } - updatedNodes.put(id, updated); - updatedNodes.put(resolvedChildId, - new Data(id, child.lastUpdate(), child.scopeOwner(), - child.value(), child.listChildren(), - child.mapChildren())); - } - }); + Data child = data(resolvedChildId); + updatedNodes.put(resolvedChildId, + new Data(resolvedParentId, child.lastUpdate(), + child.scopeOwner(), child.value(), + child.listChildren(), child.mapChildren())); + + return null; } - private void attachAs(Id parentId, String key, Id childId) { - attach(parentId, childId, (parentNode, resolvedChildId) -> { + private Reject attachAs(Id parentId, String key, Id childId) { + return attach(parentId, childId, (parentNode, resolvedChildId) -> { return updateMapChildren(parentNode, map -> { Id previous = map.putIfAbsent(key, resolvedChildId); if (previous != null) { - fail("Key is in use"); + return fail("Key is in use"); + } else { + return null; } }); }); @@ -348,24 +328,27 @@ private int findInsertIndex(List children, } } - 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; - } + private Reject attachAt(Id parentId, ListPosition position, + Id childId) { + return attach(parentId, childId, + (resolvedParentId, resolvedChildId) -> { + int insertIndex = findInsertIndex( + data(resolvedParentId).listChildren(), + position); + if (insertIndex == -1) { + return fail("Insert position not matched"); + } - return updateListChildren(node, - list -> list.add(insertIndex, resolvedChildId)); - }); + return updateListChildren(resolvedParentId, list -> { + list.add(insertIndex, resolvedChildId); + return null; + }); + }); } - private void createNode(Id nodeId, JsonNode value, Id scopeOwner) { - if (data(nodeId).isPresent()) { - fail("Node already exists"); - return; + private Reject createNode(Id nodeId, JsonNode value, Id scopeOwner) { + if (node(nodeId) != null) { + return fail("Node already exists"); } // Mark as detached to make it eligible for attaching @@ -376,6 +359,7 @@ private void createNode(Id nodeId, JsonNode value, Id scopeOwner) { if (ownerId().equals(scopeOwner)) { originalInserts.put(nodeId, (ScopeOwnerCommand) command); } + return null; } private NodeModification createModification(Id id, Node newNode) { @@ -383,61 +367,7 @@ private NodeModification createModification(Id id, Node newNode) { return new NodeModification(original, newNode); } - private static Map, BiConsumer> handlers = new HashMap<>(); - - private static void addHandler( - Class commandType, BiConsumer handler) { - handlers.put(commandType, handler); - } - - private static void addConditionHandler( - Class commandType, - BiFunction handler) { - addHandler(commandType, (manipulator, command) -> manipulator - .setResult(handler.apply(manipulator, command))); - } - - static { - addConditionHandler(ValueCondition.class, - TreeManipulator::handleValueCondition); - addConditionHandler(PositionCondition.class, - TreeManipulator::handlePositionCondition); - addConditionHandler(KeyCondition.class, - TreeManipulator::handleKeyCondition); - addConditionHandler(LastUpdateCondition.class, - TreeManipulator::handleLastUpdateCondition); - - 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 CommandResult handleCommand(SignalCommand command) { - @SuppressWarnings("unchecked") - BiConsumer handler = (BiConsumer) handlers - .get(command.getClass()); - - handler.accept(this, command); - - if (result != null) { - return result; - } - + private CommandResult build() { Map updates = new HashMap<>(); updatedNodes.forEach((id, newNode) -> { @@ -474,384 +404,538 @@ public CommandResult handleCommand(SignalCommand command) { return new Accept(updates, originalInserts); } + } - private CommandResult handleValueCondition(ValueCondition test) { - JsonNode value = value(test.targetNodeId()); - if (value == null) { - value = NullNode.getInstance(); - } - JsonNode expectedValue = test.expectedValue(); + /** + * 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())); + } - if (expectedValue == null) { - expectedValue = NullNode.getInstance(); - } + /** + * 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<>(); - return CommandResult.conditional(value.equals(expectedValue), - "Unexpected value"); + for (SignalCommand command : commands) { + apply(command, results::put); } - private CommandResult handlePositionCondition(PositionCondition test) { - Id nodeId = test.targetNodeId(); - Id resolvedChild = resolveAlias(test.childId()); + return results; + } - int indexOf = listChildren(nodeId) - .map(list -> list.indexOf(resolvedChild)) - .orElseGet(() -> Integer.valueOf(-1)); + /** + * 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); + } + } - if (indexOf == -1) { - return CommandResult.fail("Not a child"); + /** + * 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 resultCollector + * callback to collect command results, or null to + * ignore results + */ + public void apply(SignalCommand command, + BiConsumer resultCollector) { + // Custom logic for transactions that can produce multiple results + if (command instanceof TransactionCommand transaction) { + Map results = handleTransaction(transaction); + + applyResult(results.get(transaction.commandId())); + + if (resultCollector != null) { + results.forEach(resultCollector); } + } else { + CommandResult result; + if (!nodes().containsKey(command.targetNodeId())) { + result = CommandResult.fail("Node not found"); + } else { + @SuppressWarnings("unchecked") + var handler = (BiFunction) handlers + .get(command.getClass()); - ListPosition position = test.position(); + result = handler.apply(this, command); + } - Id after = position.after(); - if (after != null) { - if (after.equals(Id.EDGE)) { - if (indexOf != 0) { - return CommandResult.fail("Not the first child"); - } - } else { - if (!isChildAt(nodeId, indexOf - 1, after)) { - return CommandResult - .fail("Not after the provided child"); - } - } + applyResult(result); + + if (resultCollector != null) { + resultCollector.accept(command.commandId(), result); } + } - Id before = position.before(); - if (before != null) { - if (before.equals(Id.EDGE)) { - int childCount = listChildren(nodeId).map(List::size) - .orElse(0); - if (indexOf != childCount - 1) { - return CommandResult.fail("Not the last child"); - } + assert assertValidTree(); + } + + private void applyResult(CommandResult result) { + if (result instanceof Accept accept) { + accept.updates().forEach((nodeId, update) -> { + Node newNode = update.newNode(); + + if (newNode == null) { + nodes().remove(nodeId); + originalInserts().remove(nodeId); } else { - if (!isChildAt(nodeId, indexOf + 1, before)) { - return CommandResult - .fail("Not before the provided child"); - } + nodes().put(nodeId, newNode); } - } + }); - return CommandResult.ok(); + originalInserts().putAll(accept.originalInserts()); } + } - private CommandResult handleKeyCondition(KeyCondition keyTest) { - Id nodeId = keyTest.targetNodeId(); - String key = keyTest.key(); - Id expectedChild = keyTest.expectedChild(); + private static Map, BiFunction> handlers = new HashMap<>(); - Id actualChildId = mapChild(nodeId, key).orElse(null); - if (expectedChild == null) { - return CommandResult.conditional(actualChildId != null, - "Key not present"); - } else if (Id.ZERO.equals(expectedChild)) { - return CommandResult.conditional(actualChildId == null, - "A key is present"); - } else { - return CommandResult.conditional( - isSameNode(actualChildId, expectedChild), - "Unexpected child"); - } + private static void addHandler( + Class commandType, + BiFunction handler) { + handlers.put(commandType, handler); + } + + static { + addHandler(ValueCondition.class, + MutableTreeRevision::handleValueCondition); + addHandler(PositionCondition.class, + MutableTreeRevision::handlePositionCondition); + addHandler(KeyCondition.class, MutableTreeRevision::handleKeyCondition); + addHandler(LastUpdateCondition.class, + MutableTreeRevision::handleLastUpdateCondition); + + addHandler(AdoptAsCommand.class, MutableTreeRevision::handleAdoptAs); + addHandler(AdoptAtCommand.class, MutableTreeRevision::handleAdoptAt); + addHandler(IncrementCommand.class, + MutableTreeRevision::handleIncrement); + addHandler(ClearCommand.class, MutableTreeRevision::handleClear); + addHandler(RemoveByKeyCommand.class, + MutableTreeRevision::handleRemoveByKey); + addHandler(PutCommand.class, MutableTreeRevision::handlePut); + addHandler(PutIfAbsentCommand.class, + MutableTreeRevision::handlePutIfAbsent); + addHandler(InsertCommand.class, MutableTreeRevision::handleInsert); + addHandler(SetCommand.class, MutableTreeRevision::handleSet); + addHandler(RemoveCommand.class, MutableTreeRevision::handleRemove); + addHandler(ClearOwnerCommand.class, + MutableTreeRevision::handleClearOwner); + addHandler(SnapshotCommand.class, MutableTreeRevision::handleSnapshot); + } + + private CommandResult handleValueCondition(ValueCondition test) { + JsonNode value = data(test.targetNodeId()).map(Data::value) + .orElse(null); + if (value == null) { + value = NullNode.getInstance(); } - private CommandResult handleLastUpdateCondition( - LastUpdateCondition lastUpdateTest) { - Id lastUpdate = data(lastUpdateTest.targetNodeId()) - .map(Data::lastUpdate).orElse(null); + JsonNode expectedValue = test.expectedValue(); + if (expectedValue == null) { + expectedValue = NullNode.getInstance(); + } - return CommandResult.conditional( - Objects.equals(lastUpdate, - lastUpdateTest.expectedLastUpdate()), - "Unexpected last update"); + return CommandResult.conditional(value.equals(expectedValue), + "Unexpected value"); + } + + private CommandResult handlePositionCondition(PositionCondition test) { + DirectNodeLookup nodeLookup = new DirectNodeLookup(); + + List listChildren = data(test.targetNodeId()).get().listChildren(); + + Id resolvedChild = nodeLookup.resolveAlias(test.childId()); + int indexOf = listChildren.indexOf(resolvedChild); + + if (indexOf == -1) { + return CommandResult.fail("Not a child"); } - private void handleAdoptAs(AdoptAsCommand adoptAs) { - Id nodeId = adoptAs.targetNodeId(); - String key = adoptAs.key(); - Id childId = adoptAs.childId(); + ListPosition position = test.position(); - if (detach(childId)) { - attachAs(nodeId, key, childId); + Id after = position.after(); + if (after != null) { + if (after.equals(Id.EDGE)) { + if (indexOf != 0) { + return CommandResult.fail("Not the first child"); + } + } else { + if (!nodeLookup.isChildAt(listChildren, indexOf - 1, after)) { + return CommandResult.fail("Not after the provided child"); + } } } - private void handleAdoptAt(AdoptAtCommand adoptAt) { - Id nodeId = adoptAt.targetNodeId(); - ListPosition position = adoptAt.position(); - Id childId = adoptAt.childId(); - - if (detach(childId)) { - attachAt(nodeId, position, childId); + Id before = position.before(); + if (before != null) { + if (before.equals(Id.EDGE)) { + int childCount = listChildren.size(); + if (indexOf != childCount - 1) { + return CommandResult.fail("Not the last child"); + } + } else { + if (!nodeLookup.isChildAt(listChildren, indexOf + 1, before)) { + return CommandResult.fail("Not before the provided child"); + } } } - private void handleIncrement(IncrementCommand increment) { - Id nodeId = increment.targetNodeId(); - double delta = increment.delta(); + return CommandResult.ok(); + } - JsonNode oldValue = value(nodeId); + private CommandResult handleKeyCondition(KeyCondition keyTest) { + Id nodeId = keyTest.targetNodeId(); + String key = keyTest.key(); + Id expectedChild = keyTest.expectedChild(); - 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; - } + Id actualChildId = data(nodeId).get().mapChildren().get(key); - setValue(nodeId, new DoubleNode(newValue)); + if (expectedChild == null) { + return CommandResult.conditional(actualChildId != null, + "Key not present"); + } else if (Id.ZERO.equals(expectedChild)) { + return CommandResult.conditional(actualChildId == null, + "A key is present"); + } else { + DirectNodeLookup nodeLookup = new DirectNodeLookup(); + + return CommandResult.conditional( + nodeLookup.isSameNode(actualChildId, expectedChild), + "Unexpected child"); } + } - private void handleClear(ClearCommand clear) { - updateData(clear.targetNodeId(), node -> { - detachedNodes.addAll(node.listChildren()); - detachedNodes.addAll(node.mapChildren().values()); + private CommandResult handleLastUpdateCondition( + LastUpdateCondition lastUpdateTest) { + Id lastUpdate = data(lastUpdateTest.targetNodeId()).get().lastUpdate(); - if (detachedNodes.isEmpty()) { - return node; - } + return CommandResult.conditional( + Objects.equals(lastUpdate, lastUpdateTest.expectedLastUpdate()), + "Unexpected last update"); + } - return new Data(node.parent(), command.commandId(), - node.scopeOwner(), node.value(), List.of(), Map.of()); - }); + private CommandResult handleAdoptAs(AdoptAsCommand adoptAs) { + Id nodeId = adoptAs.targetNodeId(); + Id childId = adoptAs.childId(); + String key = adoptAs.key(); + + var builder = new ResultBuilder(adoptAs); + + Reject maybeError = builder.detach(childId); + if (maybeError != null) { + return maybeError; } - private void handleRemoveByKey(RemoveByKeyCommand removeByKey) { - mapChild(removeByKey.targetNodeId(), removeByKey.key()) - .ifPresentOrElse(this::detach, - () -> fail("Key not present")); + maybeError = builder.attachAs(nodeId, key, childId); + if (maybeError != null) { + return maybeError; } - private void handlePut(PutCommand put) { - Id commandId = put.commandId(); - Id nodeId = put.targetNodeId(); - String key = put.key(); - JsonNode value = put.value(); + return builder.build(); + } - mapChild(nodeId, key).ifPresentOrElse(childId -> { - setValue(childId, value); - }, () -> { - createNode(commandId, value, null); - attachAs(nodeId, key, commandId); - }); + private CommandResult handleAdoptAt(AdoptAtCommand adoptAt) { + Id nodeId = adoptAt.targetNodeId(); + Id childId = adoptAt.childId(); + ListPosition pos = adoptAt.position(); + + var builder = new ResultBuilder(adoptAt); + + Reject maybeError = builder.detach(childId); + if (maybeError != null) { + return maybeError; } - private void handlePutIfAbsent(PutIfAbsentCommand putIfAbsent) { - Id commandId = putIfAbsent.commandId(); - Id nodeId = putIfAbsent.targetNodeId(); - String key = putIfAbsent.key(); + maybeError = builder.attachAt(nodeId, pos, childId); + if (maybeError != null) { + return maybeError; + } - mapChild(nodeId, key).ifPresentOrElse(childId -> { - if (data(commandId).isPresent()) { - fail("Node already exists"); - return; - } + return builder.build(); + } - updatedNodes.put(commandId, new Alias(resolveAlias(childId))); - }, () -> { - createNode(commandId, putIfAbsent.value(), - putIfAbsent.scopeOwner()); - attachAs(nodeId, key, commandId); - }); + private CommandResult handleIncrement(IncrementCommand increment) { + DirectNodeLookup nodeLookup = new DirectNodeLookup(); + + ResolvedData resolved = nodeLookup + .resolveData(increment.targetNodeId()); + + double delta = increment.delta(); + + JsonNode oldValue = data(increment.targetNodeId()).get().value(); + + double newValue; + if (oldValue instanceof NumericNode value) { + newValue = value.doubleValue() + delta; + } else if (oldValue == null || oldValue instanceof NullNode) { + newValue = delta; + } else { + return CommandResult.fail("Value is not numeric"); } - private void handleInsert(InsertCommand insert) { - Id commandId = insert.commandId(); + return createValueChange(increment, resolved, new DoubleNode(newValue)); + } + + private CommandResult handleClear(ClearCommand clear) { + var builder = new ResultBuilder(clear); + + ResolvedData resolved = builder.resolveData(clear.targetNodeId()); - createNode(commandId, insert.value(), insert.scopeOwner()); - attachAt(insert.targetNodeId(), insert.position(), commandId); + Data node = resolved.data; + + builder.detachedNodes.addAll(node.listChildren()); + builder.detachedNodes.addAll(node.mapChildren().values()); + + if (builder.detachedNodes.isEmpty()) { + return CommandResult.ok(); } - private void handleSet(SetCommand set) { - setValue(set.targetNodeId(), set.value()); + Data updatedNode = new Data(node.parent(), clear.commandId(), + node.scopeOwner(), node.value(), List.of(), Map.of()); + + builder.updatedNodes.put(resolved.resolvedId, updatedNode); + + return builder.build(); + } + + private CommandResult handleRemoveByKey(RemoveByKeyCommand removeByKey) { + Id nodeId = removeByKey.targetNodeId(); + String key = removeByKey.key(); + + DirectNodeLookup nodeLookup = new DirectNodeLookup(); + + Id childId = nodeLookup.data(nodeId).mapChildren().get(key); + + if (childId == null) { + return CommandResult.fail("Key not present"); + } else { + var builder = new ResultBuilder(removeByKey); + + Reject result = builder.detach(childId); + assert result == null; + + return builder.build(); } + } + + private CommandResult handlePut(PutCommand put) { + Id commandId = put.commandId(); + Id nodeId = put.targetNodeId(); + String key = put.key(); + JsonNode value = put.value(); + + Id childId = data(nodeId).get().mapChildren().get(key); + if (childId != null) { + DirectNodeLookup nodeLookup = new DirectNodeLookup(); + return createValueChange(put, nodeLookup.resolveData(childId), + value); + } else { + var builder = new ResultBuilder(put); + + Reject maybeError = builder.createNode(commandId, value, null); + if (maybeError != null) { + return maybeError; + } - private void handleRemove(RemoveCommand remove) { - Id nodeId = remove.targetNodeId(); - Id expectedParentId = remove.expectedParentId(); + maybeError = builder.attachAs(nodeId, key, commandId); + if (maybeError != null) { + return maybeError; + } - if (expectedParentId != null) { - Id parentId = data(nodeId).map(Data::parent).orElse(null); + return builder.build(); + } + } - if (!isSameNode(expectedParentId, parentId)) { - fail("Not a child"); - return; - } + private CommandResult handlePutIfAbsent(PutIfAbsentCommand putIfAbsent) { + Id commandId = putIfAbsent.commandId(); + Id nodeId = putIfAbsent.targetNodeId(); + String key = putIfAbsent.key(); + + var builder = new ResultBuilder(putIfAbsent); + + Id childId = builder.data(nodeId).mapChildren().get(key); + if (childId != null) { + if (builder.node(commandId) != null) { + return CommandResult.fail("Node already exists"); + } + + builder.updatedNodes.put(commandId, new Alias(childId)); + } else { + Reject maybeError = builder.createNode(commandId, + putIfAbsent.value(), putIfAbsent.scopeOwner()); + if (maybeError != null) { + return maybeError; } - detach(nodeId); + maybeError = builder.attachAs(nodeId, key, commandId); + if (maybeError != null) { + return maybeError; + } } - private void handleClearOwner(ClearOwnerCommand clearOwner) { - Id ownerId = clearOwner.ownerId(); + return builder.build(); + } - // TODO clear originalInserts that have been removed previously? - nodes().forEach((id, nodeOrAlias) -> { - if (nodeOrAlias instanceof Data node - && ownerId.equals(node.scopeOwner())) { - detach(id); - } - }); + private CommandResult handleInsert(InsertCommand insert) { + Id commandId = insert.commandId(); + + var builder = new ResultBuilder(insert); + + Reject maybeError = builder.createNode(commandId, insert.value(), + insert.scopeOwner()); + if (maybeError != null) { + return maybeError; } - private void handleTransaction(TransactionCommand transaction) { - List commands = transaction.commands(); + maybeError = builder.attachAt(insert.targetNodeId(), insert.position(), + commandId); + if (maybeError != null) { + return maybeError; + } - MutableTreeRevision scratchpad = new MutableTreeRevision( - MutableTreeRevision.this); + return builder.build(); + } - subCommandResults = new HashMap(); + private CommandResult handleSet(SetCommand set) { + DirectNodeLookup nodeLookup = new DirectNodeLookup(); - Reject firstReject = null; - for (SignalCommand command : commands) { - scratchpad.apply(command, subCommandResults::put); + ResolvedData resolved = nodeLookup.resolveData(set.targetNodeId()); - CommandResult childResult = subCommandResults - .get(command.commandId()); - if (childResult instanceof Reject reject) { - firstReject = reject; - break; - } - } + return createValueChange(set, resolved, set.value()); + } - 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) subCommandResults - .get(command.commandId()); - op.updates().forEach((nodeId, modification) -> { - if (updates.containsKey(nodeId)) { - updates.put(nodeId, - new NodeModification( - updates.get(nodeId).oldNode(), - modification.newNode())); - } else { - updates.put(nodeId, modification); - } - }); + private CommandResult handleRemove(RemoveCommand remove) { + Id nodeId = remove.targetNodeId(); - originalInserts.putAll(op.originalInserts()); - } + Id expectedParentId = remove.expectedParentId(); + if (expectedParentId != null) { + DirectNodeLookup nodeLookup = new DirectNodeLookup(); - setResult(new Accept(updates, originalInserts)); - } else { - for (SignalCommand command : commands) { - CommandResult originalResult = subCommandResults - .get(command.commandId()); - if (originalResult == null - || originalResult instanceof Accept) { - subCommandResults.put(command.commandId(), firstReject); - } - } + Id actualParentId = nodeLookup.data(nodeId).parent(); - setResult(firstReject); + if (!nodeLookup.isSameNode(expectedParentId, actualParentId)) { + return CommandResult.fail("Not a child"); } } - 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(); + var builder = new ResultBuilder(remove); - updatedNodes.putAll(snapshot.nodes()); + Reject maybeError = builder.detach(nodeId); + if (maybeError != null) { + return maybeError; } - } - /** - * 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())); + return builder.build(); } - /** - * 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<>(); + private CommandResult handleClearOwner(ClearOwnerCommand clearOwner) { + Id ownerId = clearOwner.ownerId(); - for (SignalCommand command : commands) { - apply(command, results::put); - } + var builder = new ResultBuilder(clearOwner); - return results; + nodes().forEach((id, nodeOrAlias) -> { + if (nodeOrAlias instanceof Data node + && ownerId.equals(node.scopeOwner())) { + Reject result = builder.detach(id); + assert result == null; + } + }); + + return builder.build(); } - /** - * Applies a sequence of commands and ignores the results. - * - * @param commands - * the list of commands to apply, not null - */ - public void apply(List commands) { + private Map handleTransaction( + TransactionCommand transaction) { + List commands = transaction.commands(); + + MutableTreeRevision scratchpad = new MutableTreeRevision(this); + + var results = new HashMap(); + + Reject firstReject = null; for (SignalCommand command : commands) { - apply(command, null); - } - } + scratchpad.apply(command, results::put); - /** - * 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 resultCollector - * callback to collect command results, or null to - * ignore results - */ - public void apply(SignalCommand command, - BiConsumer resultCollector) { - CommandResult result = data(command.targetNodeId()).map(data -> { - TreeManipulator manipulator = new TreeManipulator(command); - var opResult = manipulator.handleCommand(command); - if (manipulator.subCommandResults != null - && resultCollector != null) { - manipulator.subCommandResults.forEach(resultCollector); + CommandResult subResult = results.get(command.commandId()); + if (subResult instanceof Reject reject) { + firstReject = reject; + break; } - return opResult; - }).orElseGet(() -> CommandResult.fail("Node not found")); + } - if (result instanceof Accept accept) { - accept.updates().forEach((nodeId, update) -> { - Node newNode = update.newNode(); + if (firstReject == null) { + Map updates = new HashMap<>(); + Map originalInserts = new HashMap<>(); - if (newNode == null) { - nodes().remove(nodeId); - originalInserts().remove(nodeId); - } else { - nodes().put(nodeId, newNode); + // Iterate the command list to preserve order + for (SignalCommand command : commands) { + Accept op = (Accept) results.get(command.commandId()); + op.updates().forEach((nodeId, modification) -> { + NodeModification previous = updates.get(nodeId); + if (previous != null) { + updates.put(nodeId, new NodeModification( + previous.oldNode(), modification.newNode())); + } else { + updates.put(nodeId, modification); + } + }); + + originalInserts.putAll(op.originalInserts()); + } + + results.put(transaction.commandId(), + new Accept(updates, originalInserts)); + } else { + for (SignalCommand command : commands) { + CommandResult originalResult = results.get(command.commandId()); + if (!(originalResult instanceof Reject)) { + results.put(command.commandId(), firstReject); } - }); + } - originalInserts().putAll(accept.originalInserts()); + results.put(transaction.commandId(), firstReject); } - if (resultCollector != null) { - resultCollector.accept(command.commandId(), result); - } + return results; + } - assert assertValidTree(); + private CommandResult handleSnapshot(SnapshotCommand snapshot) { + ResultBuilder builder = new ResultBuilder(snapshot); + + builder.updatedNodes.putAll(snapshot.nodes()); + + return builder.build(); + } + + private static CommandResult createValueChange(SignalCommand command, + ResolvedData resolved, JsonNode value) { + Data oldNode = resolved.data(); + Data newNode = new Node.Data(oldNode.parent(), command.commandId(), + oldNode.scopeOwner(), value, oldNode.listChildren(), + oldNode.mapChildren()); + + return new Accept(Map.of(resolved.resolvedId(), + new NodeModification(oldNode, newNode)), Map.of()); } }