jease.cmf.domain.Node.java Source code

Java tutorial

Introduction

Here is the source code for jease.cmf.domain.Node.java

Source

/*
Copyright (C) 2016 maik.jablonski@jease.org
    
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
    
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
    
You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package jease.cmf.domain;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Stream;

import jfix.db4o.Persistent;
import jfix.util.Urls;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;

/**
 * A Node is the fundamental base for building tree-like content repositories. A
 * Node has an id, a reference to its parent node and contains an array of
 * children. The id must be unique between all the children of a node.
 * <p>
 * The Node-Class contains a transient static set of changed nodes which is used
 * to store references to all nodes which were changed during a reorganisation
 * of the tree (e.g. appending a node to another parent). This way the
 * persistence layer can perform updates to database very efficiently, because
 * it needs only to iterate the changed nodes and save them.
 */
public class Node extends Persistent {

    private transient static final Set<Node> changedNodes = new HashSet<>();
    private String id;
    private Node parent;
    private Node[] children = new Node[] {};

    /**
     * Returns the id of the node. If the id is null, an empty string is
     * returned.
     */
    public String getId() {
        return id != null ? id : "";
    }

    /**
     * Sets the id of the node. No checks are performed.
     */
    public void setId(final String id) {
        String oldPath = null;
        if (getParent() != null && StringUtils.isNotBlank(getId())) {
            oldPath = getPath();
        }
        this.id = id;
        if (StringUtils.isNotBlank(oldPath)) {
            onPathChange(oldPath);
        }
    }

    /**
     * Returns the parent of the node.
     */
    public Node getParent() {
        return parent;
    }

    /**
     * Sets given parent for node. Internally the call is forwarded to
     * #appendChild(Node).
     */
    public void setParent(final Node newParent) {
        if (newParent == null) {
            detachParent();
        } else if (parent != newParent) {
            newParent.appendChild(this);
        }
    }

    /**
     * Returns all parents of node ordered from root to parent of node.
     */
    public Node[] getParents() {
        List<Node> parents = new ArrayList<>();
        Node parentNode = getParent();
        while (parentNode != null) {
            parents.add(parentNode);
            parentNode = parentNode.getParent();
        }
        Collections.reverse(parents);
        return parents.toArray(new Node[parents.size()]);
    }

    /**
     * Returns all parents of node which are of given class type ordered from
     * root to parent of node.
     */
    public <E extends Node> E[] getParents(final Class<E> clazz) {
        return (E[]) Stream.of(getParents()).filter($node -> clazz.isAssignableFrom($node.getClass()))
                .toArray($size -> (E[]) Array.newInstance(clazz, $size));
    }

    /**
     * Returns true if node is a descendant of given parents.
     */
    public boolean isDescendant(final Node... possibleParents) {
        for (Node possibleParent : possibleParents) {
            if (this == possibleParent) {
                return true;
            }
            Node parentNode = getParent();
            while (parentNode != null) {
                if (parentNode == possibleParent) {
                    return true;
                }
                parentNode = parentNode.getParent();
            }
        }
        return false;
    }

    /**
     * Returns all descendant nodes by recursively traversing children.
     */
    public Node[] getDescendants() {
        final List<Node> nodes = new ArrayList<>();
        traverse(nodes::add);
        return nodes.toArray(new Node[nodes.size()]);
    }

    /**
     * Returns all descendant nodes of given class type by recursively
     * traversing children.
     */
    public <E extends Node> E[] getDescendants(final Class<E> clazz) {
        return (E[]) Stream.of(getDescendants()).filter($node -> clazz.isAssignableFrom($node.getClass()))
                .toArray($size -> (E[]) Array.newInstance(clazz, $size));
    }

    /**
     * Returns all children of the node.
     */
    public Node[] getChildren() {
        return children;
    }

    /**
     * Returns all children of given class type.
     */
    public <E extends Node> E[] getChildren(final Class<E> clazz) {
        return (E[]) Stream.of(getChildren()).filter(node -> clazz.isAssignableFrom(node.getClass()))
                .toArray(size -> (E[]) Array.newInstance(clazz, size));
    }

    /**
     * Returns a child by given path.
     */
    public Node getChild(String path) {
        if (path == null) {
            return null;
        }
        if (path.equals("")) {
            return this;
        }
        if (!path.contains("/")) {
            for (Node child : getChildren()) {
                if (path.equals(child.getId())) {
                    return child;
                }
            }
            return null;
        }
        Node node = this;
        if (path.startsWith("/")) {
            while (node.getParent() != null) {
                node = node.getParent();
            }
            path = path.replaceFirst(node.getPath(), "");
        }
        for (String id : path.split("/")) {
            if ("".equals(id)) {
                continue;
            }
            if (".".equals(id)) {
                Node parentNode = node.isContainer() ? node : node.getParent();
                node = parentNode != null ? parentNode : node;
                continue;
            }
            if ("..".equals(id)) {
                Node parentNode = node.isContainer() ? node.getParent() : node.getParent().getParent();
                node = parentNode != null ? parentNode : node;
                continue;
            }
            node = node.getChild(id);
            if (node == null) {
                return null;
            }
        }
        return node;
    }

    /**
     * Appends given child to node. This method automatically detaches the given
     * child before the child is attached to the new parent.
     */
    public void appendChild(final Node child) {
        String oldPath = null;
        if (child.getParent() != null && StringUtils.isNotBlank(child.getId())) {
            oldPath = child.getPath();
        }
        child.detachParent();
        child.parent = this;
        children = ArrayUtils.add(children, child);
        markChanged();
        if (StringUtils.isNotBlank(oldPath)) {
            child.onPathChange(oldPath);
        }
    }

    /**
     * Appens given children to node. This method automatically detaches all
     * children before each child is attached to the new parent.
     */
    public void appendChildren(final Node[] newChildren) {
        Set<Node> newChildrenSet = new HashSet<>(Arrays.asList(newChildren));
        Set<Node> currentChildrenSet = new HashSet<>(Arrays.asList(children));
        List<Node> result = new ArrayList<>();
        if (newChildrenSet.containsAll(currentChildrenSet)) {
            result.addAll(Arrays.asList(newChildren));
        } else {
            int newChildIndex = 0;
            for (Node child : children) {
                if (newChildrenSet.contains(child)) {
                    result.add(newChildren[newChildIndex++]);
                } else {
                    result.add(child);
                }
            }
            for (int i = newChildIndex; i < newChildren.length; i++) {
                result.add(newChildren[i]);
            }
        }
        result.forEach(this::appendChild);
    }

    /**
     * Detaches a node from node-tree by detaching parent and all children.
     */
    public void detach() {
        detachChildren();
        detachParent();
    }

    /**
     * Detaches all children from node.
     */
    protected void detachChildren() {
        for (Node child : children) {
            child.detach();
        }
    }

    /**
     * Detaches parent from node.
     */
    protected void detachParent() {
        if (parent != null && ArrayUtils.contains(parent.children, this)) {
            parent.children = ArrayUtils.removeElement(parent.children, this);
            parent.markChanged();
        }
        parent = null;
        markChanged();
    }

    /**
     * Returns type of node as string. Per default the type is the simple class
     * name.
     */
    public String getType() {
        return getClass().getSimpleName();
    }

    /**
     * Returns true if node accepts children.
     */
    public boolean isContainer() {
        return true;
    }

    /**
     * Returns the path for the node. The path is built from the root node to
     * the current node by joining the ids with slashes (/).
     */
    public String getPath() {
        if (getParent() == null) {
            return "/" + getId();
        }
        StringBuilder sb = new StringBuilder();
        for (Node node = this; node != null; node = node.getParent()) {
            if (!"".equals(node.getId())) {
                sb.insert(0, "/" + node.getId());
            }
        }
        return sb.toString();
    }

    /**
     * Returns the estimated size of the node in bytes.
     */
    public long getSize() {
        return getId().length();
    }

    /**
     * Creates a copy of the node. This method should be overriden by derived
     * classes by calling #super.copy() and then copying all class-specific
     * fields to copy.
     * <p>
     * If recursive is true, a recursive copy is performed where children and
     * children of children will be copied as well.
     */
    public Node copy(final boolean recursive) {
        try {
            Node node = getClass().newInstance();
            if (recursive) {
                for (Node child : getChildren()) {
                    node.appendChild(child.copy(recursive));
                }
            }
            node.setId(getId());
            return node;
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage(), e);
        }
    }

    /**
     * Validates if the given child with given id can be appended to node.
     */
    public void validateChild(final Node potentialChild, final String potentialChildId) throws NodeException {
        validateId(potentialChild, potentialChildId);
        validateDuplicate(potentialChild, potentialChildId);
        validateNesting(potentialChild, potentialChildId);
        potentialChild.validateParent(this, potentialChildId);
    }

    /**
     * Valididates if given id for given child is correct.
     */
    protected void validateId(final Node potentialChild, final String potentialChildId) throws NodeException {
        if (potentialChildId != null && !Urls.isValid(potentialChildId)) {
            throw new NodeException.IllegalId();
        }
    }

    /**
     * Validates if given child with given id is unique between children of a
     * node.
     */
    protected void validateDuplicate(final Node potentialChild, final String potentialChildId)
            throws NodeException {
        for (Node actualChild : getChildren()) {
            if (actualChild.getId().equals(potentialChildId) && actualChild != potentialChild) {
                throw new NodeException.IllegalDuplicate();
            }
        }
    }

    /**
     * Validates if given child with given id can be child of node. Use this
     * method in derived implementations to restrict the set of valid children.
     */
    protected void validateNesting(final Node potentialChild, final String potentialChildId) throws NodeException {
        for (Node parentNode = this; parentNode != null; parentNode = parentNode.getParent()) {
            if (potentialChild == parentNode) {
                throw new NodeException.IllegalNesting();
            }
        }
    }

    /**
     * Validates if node with given potential id can be attached to given
     * potential parent. Use this method in derived implementations to restrict
     * the set of valid parents for certain kinds of nodes.
     */
    protected void validateParent(final Node potentialParent, final String potentialId) throws NodeException {
        // No restrictions per default.
    }

    /**
     * Applies given procedure to node and recursively to all children.
     */
    public void traverse(final Consumer<Node> action) {
        action.accept(this);
        for (Node child : getChildren()) {
            child.traverse(action);
        }
    }

    /**
     * Applies given procedure to all changed nodes.
     */
    public void processChangedNodes(final Consumer<Node> action) {
        synchronized (changedNodes) {
            changedNodes.forEach(action::accept);
            changedNodes.clear();
        }
    }

    /**
     * Marks the node as changed. Usually you don't have to call this method
     * directly.
     */
    public void markChanged() {
        synchronized (changedNodes) {
            changedNodes.add(this);
        }
    }

    /**
     * Gets called when the path is changed for current Node.
     */
    protected void onPathChange(String oldPath) {
    }

    public String toString() {
        return getPath();
    }

}