/* 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(); } }