From 46a64ef6f08fe4e1be66630552bf21d4352b6806 Mon Sep 17 00:00:00 2001 From: Sergey Malenkov Date: Fri, 23 Jun 2017 21:10:06 +0300 Subject: [PATCH] rewrite inner structures and add logging --- .../com/intellij/ui/tree/AsyncTreeModel.java | 637 ++++++++++++++---- 1 file changed, 499 insertions(+), 138 deletions(-) diff --git a/platform/platform-impl/src/com/intellij/ui/tree/AsyncTreeModel.java b/platform/platform-impl/src/com/intellij/ui/tree/AsyncTreeModel.java index a93e523067c4..9f8ee7d1f3b9 100644 --- a/platform/platform-impl/src/com/intellij/ui/tree/AsyncTreeModel.java +++ b/platform/platform-impl/src/com/intellij/ui/tree/AsyncTreeModel.java @@ -18,12 +18,11 @@ package com.intellij.ui.tree; import com.intellij.openapi.Disposable; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Disposer; -import com.intellij.openapi.util.Pair; import com.intellij.ui.LoadingNode; -import com.intellij.ui.tree.MapBasedTree.Entry; import com.intellij.util.concurrency.Command; import com.intellij.util.concurrency.Invoker; import com.intellij.util.concurrency.InvokerSupplier; +import com.intellij.util.containers.SmartHashSet; import com.intellij.util.ui.tree.AbstractTreeModel; import com.intellij.util.ui.tree.TreeModelAdapter; import org.jetbrains.annotations.NotNull; @@ -36,12 +35,19 @@ import javax.swing.event.TreeModelListener; import javax.swing.tree.TreeModel; import javax.swing.tree.TreePath; import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map.Entry; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.function.ToIntFunction; +import java.util.stream.Stream; import static java.util.Collections.emptyList; -import static java.util.Collections.unmodifiableList; +import static java.util.Collections.singletonList; import static org.jetbrains.concurrency.Promises.rejectedPromise; /** @@ -49,9 +55,8 @@ import static org.jetbrains.concurrency.Promises.rejectedPromise; */ public final class AsyncTreeModel extends AbstractTreeModel implements Disposable, Identifiable, Searchable, Navigatable { private static final Logger LOG = Logger.getInstance(AsyncTreeModel.class); - private final AtomicReference>> rootLoader = new AtomicReference<>(); private final Command.Processor processor; - private final MapBasedTree tree = new MapBasedTree<>(true, object -> object); + private final Tree tree = new Tree(); private final TreeModel model; private final boolean showLoadingNode; private final TreeModelListener listener = new TreeModelAdapter() { @@ -63,14 +68,18 @@ public final class AsyncTreeModel extends AbstractTreeModel implements Disposabl return; } Object object = path.getLastPathComponent(); + if (object == null) { + LOG.warn("unsupported path: " + path); + return; + } if (path.getParentPath() == null && type == EventType.StructureChanged) { // set a new root object according to the specification processor.process(new CmdGetRoot("Update root", object)); return; } processor.foreground.invokeLaterIfNeeded(() -> { - Entry entry = tree.findEntry(object); - if (entry == null || entry.isLoadingRequired()) { + Node node = tree.map.get(object); + if (node == null || node.isLoadingRequired()) { LOG.debug("ignore updating of nonexistent node: ", object); } else if (type == EventType.NodesChanged) { @@ -78,13 +87,13 @@ public final class AsyncTreeModel extends AbstractTreeModel implements Disposabl AsyncTreeModel.this.treeNodesChanged(event.getTreePath(), event.getChildIndices(), event.getChildren()); } else if (type == EventType.NodesInserted) { - processor.process(new CmdGetChildren("Insert children", entry, false)); + processor.process(new CmdGetChildren("Insert children", node, false)); } else if (type == EventType.NodesRemoved) { - processor.process(new CmdGetChildren("Remove children", entry, false)); + processor.process(new CmdGetChildren("Remove children", node, false)); } else { - processor.process(new CmdGetChildren("Update children", entry, true)); + processor.process(new CmdGetChildren("Update children", node, true)); } }); } @@ -141,18 +150,19 @@ public final class AsyncTreeModel extends AbstractTreeModel implements Disposabl @NotNull public Promise resolve(TreePath path) { AsyncPromise async = new AsyncPromise<>(); - processor.foreground.invokeLaterIfNeeded(() -> resolve(async, path, entry -> async.setResult(entry))); + processor.foreground.invokeLaterIfNeeded(() -> resolve(async, path, entry -> async.setResult(path))); return async; } private Promise resolve(Promise promise) { AsyncPromise async = new AsyncPromise<>(); promise.rejected(error -> processor.foreground.invokeLaterIfNeeded(() -> async.setError(error))); - promise.done(result -> processor.foreground.invokeLaterIfNeeded(() -> resolve(async, result, entry -> async.setResult(entry)))); + promise.done(path -> processor.foreground.invokeLaterIfNeeded(() -> resolve(async, path, entry -> async.setResult(path)))); return async; } - private void resolve(AsyncPromise async, TreePath path, Consumer> consumer) { + private void resolve(AsyncPromise async, TreePath path, Consumer consumer) { + LOG.debug("resolve path: ", path); if (path == null) { async.setError("path is null"); return; @@ -162,43 +172,26 @@ public final class AsyncTreeModel extends AbstractTreeModel implements Disposabl async.setError("path is wrong"); return; } - if (!consume(consumer, tree.findEntry(object))) { + if (!resolved(path, consumer)) { TreePath parent = path.getParentPath(); if (parent == null) { - promiseRootEntry().done(entry -> { - if (entry == null) { - async.setError("root is null"); - } - else if (object != entry.getNode()) { - async.setError("root is wrong"); - } - else { - consumer.accept(entry); - } + promiseRootEntry().rejected(async::setError).done(node -> { + if (!resolved(path, consumer)) async.setError("root not found"); }); } else { - resolve(async, parent, entry -> processor.process(new Command>>() { - private CmdGetChildren command = new CmdGetChildren("Sync children", entry, false); - - @Override - public List> get() { - return command.get(); - } - - @Override - public void accept(List> children) { - command.accept(children); - if (!consume(consumer, tree.findEntry(object))) async.setError("path not found"); - } + resolve(async, parent, resolved -> promiseChildren(resolved).rejected(async::setError).done(node -> { + if (!resolved(path, consumer)) async.setError("path not found"); })); } } } - private static boolean consume(Consumer> consumer, Entry entry) { - if (entry == null) return false; - consumer.accept(entry); + private boolean resolved(@NotNull TreePath path, @NotNull Consumer consumer) { + Node node = tree.map.get(path.getLastPathComponent()); + if (node == null || !node.paths.contains(path)) return false; + LOG.debug("path resolved: ", path); + consumer.accept(node); return true; } @@ -206,26 +199,25 @@ public final class AsyncTreeModel extends AbstractTreeModel implements Disposabl public Object getRoot() { if (!isValidThread()) return null; promiseRootEntry(); - Entry entry = tree.getRootEntry(); - return entry == null ? null : entry.getNode(); + Node node = tree.root; + return node == null ? null : node.object; } @Override public Object getChild(Object object, int index) { - Entry entry = getEntry(object, true); - return entry == null ? null : entry.getChild(index); + List children = getEntryChildren(object); + return 0 <= index && index < children.size() ? children.get(index).object : null; } @Override public int getChildCount(Object object) { - Entry entry = getEntry(object, true); - return entry == null ? 0 : entry.getChildCount(); + return getEntryChildren(object).size(); } @Override public boolean isLeaf(Object object) { - Entry entry = getEntry(object, false); - return entry == null || entry.isLeaf(); + Node node = getEntry(object); + return node == null || node.leaf; } @Override @@ -235,8 +227,13 @@ public final class AsyncTreeModel extends AbstractTreeModel implements Disposabl @Override public int getIndexOfChild(Object object, Object child) { - Entry entry = getEntry(object, true); - return entry == null ? -1 : entry.getIndexOf(child); + if (child != null) { + List children = getEntryChildren(object); + for (int i = 0; i < children.size(); i++) { + if (child.equals(children.get(i).object)) return i; + } + } + return -1; } private boolean isValidThread() { @@ -245,149 +242,513 @@ public final class AsyncTreeModel extends AbstractTreeModel implements Disposabl return false; } - private static AsyncPromise create(AtomicReference> reference) { - AsyncPromise newPromise = new AsyncPromise<>(); - AsyncPromise oldPromise = reference.getAndSet(newPromise); - if (oldPromise != null && Promise.State.PENDING == oldPromise.getState()) newPromise.notify(oldPromise); - return newPromise; - } - - private Promise> promiseRootEntry() { - AsyncPromise> promise = rootLoader.get(); - if (promise != null) return promise; - CmdGetRoot command = new CmdGetRoot("Load root", null); - processor.process(command); + @NotNull + private Promise promiseRootEntry() { + CmdGetRoot command = tree.reference.get(); + if (command == null) { + command = new CmdGetRoot("Load root", null); + processor.process(command); + } return command.promise; } - private Entry getEntry(Object object, boolean loadChildren) { - Entry entry = object == null || !isValidThread() ? null : tree.findEntry(object); - if (entry != null && loadChildren && entry.isLoadingRequired()) loadChildren(entry, true); - return entry; + @NotNull + private Promise promiseChildren(@NotNull Node node) { + CmdGetChildren command = node.reference.get(); + if (command == null) { + node.setLoading(!showLoadingNode ? null : new Node(new LoadingNode(), true)); + command = new CmdGetChildren("Load children", node, false); + processor.process(command); + } + return command.promise; } - private void loadChildren(Entry entry, boolean insertLoadingNode) { - String name = insertLoadingNode ? "Load children" : "Reload children"; - if (insertLoadingNode && showLoadingNode) entry.setLoadingChildren(new LoadingNode()); - processor.process(new CmdGetChildren(name, entry, true)); + private Node getEntry(Object object) { + return object == null || !isValidThread() ? null : tree.map.get(object); } - private final class CmdGetRoot implements Obsolescent, Command> { - private final AsyncPromise> promise = create(rootLoader); + @NotNull + private List getEntryChildren(Object object) { + Node node = getEntry(object); + if (node == null) return emptyList(); + promiseChildren(node); + return node.getChildren(); + } + + private TreeModelEvent createEvent(TreePath path, LinkedHashMap map) { + if (map == null || map.isEmpty()) return new TreeModelEvent(this, path, null, null); + int i = 0; + int size = map.size(); + int[] indices = new int[size]; + Object[] children = new Object[size]; + for (Entry entry : map.entrySet()) { + indices[i] = entry.getValue(); + children[i] = entry.getKey(); + i++; + } + return new TreeModelEvent(this, path, indices, children); + } + + private void treeNodesChanged(Node node, LinkedHashMap map) { + if (!listeners.isEmpty()) { + for (TreePath path : node.paths) { + listeners.treeNodesChanged(createEvent(path, map)); + } + } + } + + private void treeNodesInserted(Node node, LinkedHashMap map) { + if (!listeners.isEmpty()) { + for (TreePath path : node.paths) { + listeners.treeNodesInserted(createEvent(path, map)); + } + } + } + + private void treeNodesRemoved(Node node, LinkedHashMap map) { + if (!listeners.isEmpty()) { + for (TreePath path : node.paths) { + listeners.treeNodesRemoved(createEvent(path, map)); + } + } + } + + @NotNull + private static LinkedHashMap getIndices(@NotNull List children, ToIntFunction function) { + LinkedHashMap map = new LinkedHashMap<>(); + for (int i = 0; i < children.size(); i++) { + Node child = children.get(i); + if (map.containsKey(child.object)) { + LOG.warn("ignore duplicated " + (function == null ? "old" : "new") + " child at " + i); + } + else { + map.put(child.object, function == null ? i : function.applyAsInt(child)); + } + } + return map; + } + + private static int getIntersectionCount(LinkedHashMap indices, Iterable objects) { + int count = 0; + int last = -1; + for (Object object : objects) { + Integer index = indices.get(object); + if (index != null && last < index.intValue()) { + last = index; + count++; + } + } + return count; + } + + private static List getIntersection(LinkedHashMap indices, Iterable objects) { + List list = new ArrayList<>(indices.size()); + int last = -1; + for (Object object : objects) { + Integer index = indices.get(object); + if (index != null && last < index.intValue()) { + last = index; + list.add(object); + } + } + return list; + } + + private static List getIntersection(LinkedHashMap removed, LinkedHashMap inserted) { + if (removed.isEmpty() || inserted.isEmpty()) return emptyList(); + int countOne = getIntersectionCount(removed, inserted.keySet()); + int countTwo = getIntersectionCount(inserted, removed.keySet()); + if (countOne > countTwo) return getIntersection(removed, inserted.keySet()); + if (countTwo > 0) return getIntersection(inserted, removed.keySet()); + return emptyList(); + } + + private static T setCommandToProcess(AtomicReference reference, T command) { + T old = reference.getAndSet(command); + if (old == null || Promise.State.PENDING != old.promise.getState()) return null; + if (command != null) command.promise.notify(old.promise); + return old; + } + + private static void resetCommand(AtomicReference reference) { + T old = setCommandToProcess(reference, null); + if (old != null) old.promise.setError("loading cancelled"); + } + + private static void resetCommand(@NotNull Node node, String prefix) { + resetCommand(node.reference); + if (prefix != null) LOG.warn(prefix + node.object); + } + + private static abstract class ObsolescentCommand implements Obsolescent, Command, Function { + final AsyncPromise promise = new AsyncPromise<>(); private final String name; - private final Object root; + private final Object object; - public CmdGetRoot(String name, Object root) { + ObsolescentCommand(String name, Object object) { this.name = name; - this.root = root; + this.object = object; + LOG.debug("create command: ", this); } @Override public String toString() { - return root == null ? name : name + ": " + root; + return object == null ? name : name + ": " + object; + } + + @Override + public Node get() { + LOG.debug("background command: ", this); + return isObsolete() ? null : apply(object); + } + } + + private final class CmdGetRoot extends ObsolescentCommand { + private CmdGetRoot(String name, Object object) { + super(name, object); + setCommandToProcess(tree.reference, this); } @Override public boolean isObsolete() { - return promise != rootLoader.get(); + return this != tree.reference.get(); } @Override - public Pair get() { - if (isObsolete()) return null; - Object object = root != null ? root : model.getRoot(); - if (isObsolete()) return null; - return Pair.create(object, model.isLeaf(object)); + public Node apply(Object object) { + if (object == null) object = model.getRoot(); + if (object == null || isObsolete()) return null; + return new Node(object, model.isLeaf(object)); } @Override - public void accept(Pair root) { - if (isObsolete()) return; - boolean updated = tree.updateRoot(root); - Entry entry = tree.getRootEntry(); - if (updated) treeStructureChanged(entry, null, null); - if (entry != null) loadChildren(entry, entry.isLoadingRequired()); - promise.setResult(entry); + public void accept(Node loaded) { + if (isObsolete()) { + LOG.debug("obsolete command: ", this); + return; + } + LOG.debug("foreground command: ", this); + + Node root = tree.root; + if (root == null && loaded == null) { + LOG.debug("no root"); + promise.setResult(null); + return; + } + + if (root != null && loaded != null && root.object.equals(loaded.object)) { + LOG.debug("same root: ", root.object); + promise.setResult(root); + if (root.isLoadingRequired()) return; + processor.process(new CmdGetChildren("Update root children", root, true)); + return; + } + + if (root != null) { + tree.removeMapping(null, root); + } + if (!tree.map.isEmpty()) { + tree.map.values().forEach(value -> resetCommand(value, "remove staled node: ")); + tree.map.clear(); + } + + tree.root = loaded; + if (loaded != null) { + tree.map.put(loaded.object, loaded); + TreePath path = new TreePath(loaded.object); + loaded.insertPath(path); + treeStructureChanged(path, null, null); + LOG.debug("new root: ", loaded.object); + promise.setResult(loaded); + } + else { + treeStructureChanged(null, null, null); + LOG.debug("root removed"); + promise.setResult(null); + } } } - private final class CmdGetChildren implements Command>> { - private final String name; - private final Entry entry; + private final class CmdGetChildren extends ObsolescentCommand { + private final Node node; private final boolean deep; - public CmdGetChildren(String name, Entry entry, boolean deep) { - this.name = name; - this.entry = entry; - this.deep = deep; + public CmdGetChildren(String name, Node node, boolean deep) { + super(name, node.object); + CmdGetChildren old = setCommandToProcess(node.reference, this); + this.node = node; + this.deep = deep || old != null && old.deep; } @Override - public String toString() { - return name + ": " + entry.getNode(); + public boolean isObsolete() { + if (this == node.reference.get()) return false; + LOG.debug("obsolete command: ", this); + return true; } @Override - public List> get() { - Object object = entry.getNode(); - if (model.isLeaf(object)) return null; + public Node apply(Object object) { + Node loaded = new Node(object, model.isLeaf(object)); + if (loaded.leaf) return loaded; if (model instanceof ChildrenProvider) { //noinspection unchecked ChildrenProvider provider = (ChildrenProvider)model; - ArrayList> children = new ArrayList<>(); - provider.getChildren(object).forEach(child -> add(children, child)); - return unmodifiableList(children); + List children = provider.getChildren(object); + //TODO: allow to cancel this command by returning null + loaded.children = load(children.size(), index -> children.get(index)); } - - int count = model.getChildCount(object); - if (count <= 0) return emptyList(); - - ArrayList> children = new ArrayList<>(count); - for (int i = 0; i < count; i++) add(children, model.getChild(object, i)); - return unmodifiableList(children); + else { + loaded.children = load(model.getChildCount(object), index -> model.getChild(object, index)); + } + return loaded; } - private void add(List> children, Object child) { - if (child != null) children.add(Pair.create(child, model.isLeaf(child))); + private List load(int count, IntFunction function) { + if (count < 0) LOG.warn("illegal child count: " + count); + if (count <= 0) return emptyList(); + + SmartHashSet set = new SmartHashSet<>(count); + List children = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + if (isObsolete()) return null; + Object child = function.apply(i); + if (child == null) { + LOG.warn("ignore null child at " + i); + } + else if (!set.add(child)) { + LOG.warn("ignore duplicated child at " + i); + } + else { + if (isObsolete()) return null; + children.add(new Node(child, model.isLeaf(child))); + } + } + return children; } @Override - public void accept(List> children) { - Object object = entry.getNode(); - if (entry != tree.findEntry(object)) { - LOG.debug("ignore updating of changed node: ", object); + public void accept(Node loaded) { + if (isObsolete()) { + LOG.debug("obsolete command: ", this); return; } - MapBasedTree.UpdateResult update = tree.update(entry, children); + if (loaded == null || loaded.isLoadingRequired()) { + LOG.debug("cancelled command: ", this); + return; + } + if (node != tree.map.get(loaded.object)) { + resetCommand(node, "ignore removed node: "); + return; + } + LOG.debug("foreground command: ", this); - boolean removed = !update.getRemoved().isEmpty(); - boolean inserted = !update.getInserted().isEmpty(); - boolean contained = !update.getContained().isEmpty(); - if (!removed && !inserted && !contained) return; + List oldChildren = node.getChildren(); + List newChildren = loaded.getChildren(); + if (oldChildren.isEmpty() && newChildren.isEmpty()) { + node.setLeaf(loaded.leaf); + treeNodesChanged(node, null); + LOG.debug("no children: ", node.object); + promise.setResult(node); + return; + } - if (!deep || !contained) { - if (!removed && inserted) { - if (listeners.isEmpty()) return; - listeners.treeNodesInserted(update.getEvent(AsyncTreeModel.this, entry, update.getInserted())); - return; + LinkedHashMap removed = getIndices(oldChildren, null); + if (newChildren.isEmpty()) { + for (Node child : oldChildren) tree.removeMapping(node, child); + node.setLeaf(loaded.leaf); + treeNodesRemoved(node, removed); + LOG.debug("children removed: ", node.object); + promise.setResult(node); + return; + } + + // remove duplicated nodes during indices calculation + ArrayList list = new ArrayList<>(newChildren.size()); + SmartHashSet reload = new SmartHashSet<>(); + LinkedHashMap inserted = getIndices(newChildren, child -> { + Node found = tree.map.get(child.object); + if (found == null) { + tree.map.put(child.object, child); + list.add(child); } - if (!inserted && removed) { - if (listeners.isEmpty()) return; - listeners.treeNodesRemoved(update.getEvent(AsyncTreeModel.this, entry, update.getRemoved())); - return; + else { + list.add(found); + if (!found.isLoadingRequired() && !removed.containsKey(found.object)) { + reload.add(found.object); // reload reused children if they are inserted + } + } + return list.size() - 1; + }); + newChildren = list; + + if (oldChildren.isEmpty()) { + for (Node child : newChildren) child.insertPaths(node); + node.setChildren(newChildren); + treeNodesInserted(node, inserted); + LOG.debug("children inserted: ", node.object); + promise.setResult(node); + return; + } + + LinkedHashMap contained = new LinkedHashMap<>(); + for (Object object : getIntersection(removed, inserted)) { + Integer oldIndex = removed.remove(object); + if (oldIndex == null) { + LOG.warn("intersection failed"); + } + Integer newIndex = inserted.remove(object); + if (newIndex == null) { + LOG.warn("intersection failed"); + } + else if (!deep) { + contained.put(object, newIndex); + } + else { + reload.add(object); } } - if (!listeners.isEmpty()) { - if (removed) listeners.treeNodesRemoved(update.getEvent(AsyncTreeModel.this, entry, update.getRemoved())); - if (inserted) listeners.treeNodesInserted(update.getEvent(AsyncTreeModel.this, entry, update.getInserted())); - if (contained) listeners.treeNodesChanged(update.getEvent(AsyncTreeModel.this, entry, update.getContained())); + + for (Node child : oldChildren) { + if (removed.containsKey(child.object) && !inserted.containsKey(child.object)) { + tree.removeMapping(node, child); + } } - for (Entry entry : update.getContained()) { - if (!entry.isLoadingRequired()) { - loadChildren(entry, false); + + for (Node child : newChildren) { + if (!removed.containsKey(child.object) && inserted.containsKey(child.object)) { + child.insertPaths(node); + } + } + + node.setChildren(newChildren); + if (!removed.isEmpty()) treeNodesRemoved(node, removed); + if (!inserted.isEmpty()) treeNodesInserted(node, inserted); + if (!contained.isEmpty()) treeNodesChanged(node, contained); + LOG.debug("children changed: ", node.object); + promise.setResult(node); + + if (!reload.isEmpty()) { + for (Node child : newChildren) { + if (!child.isLoadingRequired() && reload.contains(child.object)) { + processor.process(new CmdGetChildren("Update children recursively", child, true)); + } } } } } + + private static final class Tree { + private final AtomicReference reference = new AtomicReference<>(); + private final HashMap map = new HashMap<>(); + private volatile Node root; + + private void removeMapping(Node parent, Node child) { + if (parent != null && parent.loading == child) { + parent.loading = null; + } + else { + for (Node node : child.getChildren()) { + //TODO: remove only parent paths + removeMapping(child, node); + } + child.removePaths(parent); + if (child.paths.isEmpty()) { + resetCommand(child.reference); + Node node = map.remove(child.object); + if (node != child) { + LOG.warn("invalid node: " + child.object); + if (node != null) map.put(node.object, node); + } + } + } + } + } + + private static final class Node { + private final AtomicReference reference = new AtomicReference<>(); + private final SmartHashSet paths = new SmartHashSet<>(); + private final Object object; + private volatile boolean leaf; + private volatile List children; + private volatile Node loading; + + private Node(@NotNull Object object, boolean leaf) { + this.object = object; + this.leaf = leaf; + } + + private void setLeaf(boolean leaf) { + this.leaf = leaf; + this.children = leaf ? null : emptyList(); + this.loading = null; + } + + private void setChildren(List children) { + this.leaf = children == null; + this.children = children; + this.loading = null; + } + + private void setLoading(Node loading) { + this.leaf = false; + this.children = loading != null ? singletonList(loading) : emptyList(); + this.loading = loading; + } + + private boolean isLoadingRequired() { + return !leaf && children == null; + } + + @NotNull + private List getChildren() { + List list = children; + return list != null ? list : emptyList(); + } + + private void insertPath(TreePath path) { + if (!paths.add(path)) { + LOG.warn("node is already attached to " + path); + } + } + + private void insertPaths(Stream stream) { + stream.forEach(path -> insertPath(path.pathByAddingChild(object))); + } + + private void insertPaths(Node parent) { + if (parent == null) { + insertPath(new TreePath(object)); + } + else if (parent.paths.isEmpty()) { + LOG.warn("insert to invalid parent"); + } + else { + insertPaths(parent.paths.stream()); + } + } + + private void removePath(TreePath path) { + if (!paths.remove(path)) { + LOG.warn("node is not attached to " + path); + } + } + + private void removePaths(Stream stream) { + stream.forEach(path -> removePath(path.pathByAddingChild(object))); + } + + private void removePaths(Node parent) { + if (parent == null) { + removePath(new TreePath(object)); + } + else if (parent.paths.isEmpty()) { + LOG.warn("remove from invalid parent"); + } + else { + removePaths(parent.paths.stream()); + } + } + } }