public record TreeNode()

in api/applib/src/main/java/org/apache/causeway/applib/graph/tree/TreeNode.java [51:355]


public record TreeNode<T>(
    /**
     * Is required {@code null} iff this is a root node.
     * However, method {@link #rootNode()} never returns {@code null}.
     * @implNote records cannot self reference
     */
    @Nullable TreeNode<T> rootNode,
    /** position within the tree (as path) */
    @NonNull TreePath treePath,
    @NonNull T value,
    @NonNull TreeAdapter<T> treeAdapter,
    /**
     * this tree's shared state object, holding e.g. the collapse/expand state
     */
    @NonNull TreeState treeState)
implements Vertex<T> {

    // -- FACTORIES
    
    /**
     * Creates the root node of a tree structure as inferred from given treeAdapter.
     */
    public static <T> TreeNode<T> root(
            final @NonNull T rootValue,
            final @NonNull TreeAdapter<T> treeAdapter) {
        return TreeNode.root(rootValue, treeAdapter, TreeState.rootCollapsed());
    }

    /**
     * Creates the root node of a tree structure as inferred from given treeAdapter.
     */
    public static <T> TreeNode<T> root(
            final @NonNull T rootValue,
            final @NonNull Class<? extends TreeAdapter<T>> treeAdapterClass,
            final @NonNull FactoryService factoryService) {
        return root(rootValue, factoryService.getOrCreate(treeAdapterClass));
    }

    public static <T> TreeNode<T> root(
            final T rootValue,
            final Class<? extends TreeAdapter<T>> treeAdapterClass,
            final TreeState sharedState,
            final FactoryService factoryService) {
        return root(rootValue, factoryService.getOrCreate(treeAdapterClass));
    }

    public static <T> TreeNode<T> root(
            final T rootValue,
            final TreeAdapter<T> treeAdapter,
            final TreeState sharedState) {
        return new TreeNode<T>(null, TreePath.root(), rootValue, treeAdapter, sharedState);
    }

    // -- CONTRACT

    @Override
    public final String toString() {
        return "TreeNode[%s, value=%s]".formatted(treePath, value);
    }

    @Override
    public final boolean equals(final Object obj) {
        return obj instanceof TreeNode other
            ? Objects.equals(this.treePath, other.treePath)
                    && Objects.equals(this.value, other.value)
            : false;
    }

    @Override
    public final int hashCode() {
        return Objects.hash(treePath);
    }

    // --

    public TreeNode<T> rootNode() {
        return rootNode!=null
            ? rootNode
            : this;
    }

    public T rootValue() {
        return rootNode().value();
    }

    // -- VERTEX - IMPLEMENTATION

    @Override
    public int incomingCount() {
        return isRoot()
                ? 0
                : 1;
    }
    @Override
    public int outgoingCount() {
        return childCount();
    }
    @Override
    public Stream<Edge<T>> streamIncoming() {
        return lookupParent()
            .map(parentNode->new SimpleEdge<T>(parentNode, this))
            .map(Stream::<Edge<T>>of)
            .orElseGet(Stream::empty);
    }
    @Override
    public Stream<Edge<T>> streamOutgoing() {
        return streamChildren()
                .map(to->new SimpleEdge<T>(this, to));
    }

    // -- RESOLUTION

    /**
     * Resolves given path relative to the root of this tree.
     */
    public Optional<TreeNode<T>> resolve(final TreePath absolutePath) {
        /*
         * Optimize if absolutePath starts with this.treePath:
         *
         * If current path is
         *   /p0/p1/p2/p3
         * and we want to resolve
         *   /p0/p1/p2/p3/p4/p5
         * then instead of starting from root, we can start from here, resolving sub-node
         *   /p3/p4/p5
         * observe: the relative path /p3 would point to the sub-node itself
         */
        return absolutePath.startsWith(treePath)
                ? resolveRelative(absolutePath.subPath(treePath.size() - 1))
                : rootNode().resolveRelative(absolutePath);
    }

    /**
     * Resolves given path relative to this node.
     * <p>
     * E.g. starting from root, '/0' will return the root;<br>
     * starting from root, '/0/2' will return the 3rd child of root;<br>
     * starting from sub-node '/0/2', '/2/9' will resolve the 10th child ('/0/2/9') of this sub-node;<br>
     */
    private Optional<TreeNode<T>> resolveRelative(final TreePath relativePath) {
        if(Objects.equals(this.treePath, relativePath)) return Optional.of(this);

        final int childIndex = relativePath.childIndex().orElse(-1);
        if(childIndex<0) return Optional.empty();

        final Optional<TreeNode<T>> childNode = streamChildren().skip(childIndex).findFirst();
        if(!childNode.isPresent()) return Optional.empty();

        return relativePath.size()>2
                ? childNode.get().resolveRelative(relativePath.subPath(1))
                : childNode;
    }

    // -- PARENT

    public Optional<TreeNode<T>> lookupParent() {
        return treePath.parent()
                .flatMap(this::resolve);
    }

    // -- CHILDREN

    public int childCount() {
        return treeAdapter.childCountOf(value);
    }

    public Stream<TreeNode<T>> streamChildren() {
        if(isLeaf()) return Stream.empty();

        return treeAdapter.childrenOf(value)
                .map(IndexedFunction.zeroBased((siblingIndex, childPojo)->
                    toTreeNode(treePath.append(siblingIndex), childPojo)));
    }

    // -- BASIC PREDICATES

    public boolean isRoot() {
        return rootNode == null;
    }

    public boolean isLeaf() {
        return childCount() == 0;
    }

    // -- COLLAPSE/EXPAND

    public boolean isExpanded(final TreePath treePath) {
        final Set<TreePath> expandedPaths = treeState().expandedNodePaths();
        return expandedPaths.contains(treePath);
    }

    /**
     * Adds {@code treePaths} to the set of expanded nodes, as held by this tree's shared state object.
     * @param treePaths
     */
    public TreeNode<T> expand(final TreePath ... treePaths) {
        final Set<TreePath> expandedPaths = treeState().expandedNodePaths();
        _NullSafe.stream(treePaths).forEach(expandedPaths::add);
        return this;
    }

    /**
     * Expands this node and all its parents.
     */
    public TreeNode<T> expand() {
        final Set<TreePath> expandedPaths = treeState().expandedNodePaths();
        streamHierarchyUp()
            .map(TreeNode::treePath)
            .forEach(expandedPaths::add);
        return this;
    }

    /**
     * Removes {@code treePaths} from the set of expanded nodes, as held by this tree's shared state object.
     * @param treePaths
     */
    public TreeNode<T> collapse(final TreePath ... treePaths) {
        final Set<TreePath> expandedPaths = treeState().expandedNodePaths();
        _NullSafe.stream(treePaths).forEach(expandedPaths::remove);
        return this;
    }

    // -- SELECTION

    /**
     * Clears all selection markers.
     * @see #select(TreePath...)
     */
    public TreeNode<T> clearSelection() {
        treeState().selectedNodePaths().clear();
        return this;
    }

    /**
     * Whether node that corresponds to given {@link TreePath} has a selection marker.
     * @see #select(TreePath...)
     */
    public boolean isSelected(final TreePath treePath) {
        final Set<TreePath> selectedPaths = treeState().selectedNodePaths();
        return selectedPaths.contains(treePath);
    }

    /**
     * Select nodes by their corresponding {@link TreePath}, that is, activate their selection marker.
     * <p>
     * With the <i>Wicket Viewer</i> corresponds to expressing CSS class {@code tree-node-selected}
     * on the rendered tree node, which has default bg-color {@code lightgrey}. Color can be customized
     * by setting CSS var {@code--tree-node-selected-bg-color}
     * <pre>
     * .tree-theme-bootstrap .tree-node-selected {
     *     background-color: var(--tree-node-selected-bg-color, lightgrey);
     * }
     * </pre>
     */
    public TreeNode<T> select(final TreePath ... treePaths) {
        final Set<TreePath> selectedPaths = treeState().selectedNodePaths();
        _NullSafe.stream(treePaths).forEach(selectedPaths::add);
        return this;
    }

    // -- PARENT NODE ITERATION

    public Iterator<TreeNode<T>> iteratorHierarchyUp(){
        return new TreeNode_iteratorHierarchyUp<>(this);
    }

    // -- PARENT NODE STREAMING

    public Stream<TreeNode<T>> streamHierarchyUp(){
        return StreamSupport.stream(
                Spliterators.spliteratorUnknownSize(iteratorHierarchyUp(), Spliterator.ORDERED),
                false); // not parallel
    }

    // -- CHILD NODE ITERATION

    public Iterator<TreeNode<T>> iteratorDepthFirst(){
        return new TreeNode_iteratorDepthFirst<>(this);
    }

    public Iterator<TreeNode<T>> iteratorBreadthFirst(){
        return new TreeNode_iteratorBreadthFirst<>(this);
    }

    // -- CHILD NODE STREAMING

    public Stream<TreeNode<T>> streamDepthFirst(){
        return StreamSupport.stream(
                Spliterators.spliteratorUnknownSize(iteratorDepthFirst(), Spliterator.ORDERED),
                false); // not parallel
    }

    public Stream<TreeNode<T>> streamBreadthFirst(){
        return StreamSupport.stream(
                Spliterators.spliteratorUnknownSize(iteratorBreadthFirst(), Spliterator.ORDERED),
                false); // not parallel
    }

    // -- HELPER

    private TreeNode<T> toTreeNode(final TreePath treePath, final T value){
        return new TreeNode<>(rootNode(), treePath, value, treeAdapter, treeState);
    }

}