package de.uni_passau.fim.infosun.prophet.util.qTree; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Queue; /** * A tree where nodes contain data (name and type of the node, HTML content) to be displayed by the * <code>ExperimentViewer</code> or <code>ExperimentEditor</code> and <code>Attribute</code>s. */ public class QTreeNode implements Cloneable { /** * Possible types of <code>QTreeNode</code>s. */ public enum Type { EXPERIMENT, CATEGORY, QUESTION } private Type type; private String name; private String html; private Map<String, Attribute> attributes; private QTreeNode parent; private List<QTreeNode> children; private Map<String, String[]> answers; // use String[] because there may be multiple answers to a question (checkboxes) private long answerTime; /** * Constructs a new <code>QTreeNode</code> with the given parameters. The <code>parent</code> may be * <code>null</code> if this is the root node of the tree. * * @param parent the parent of this node * @param type the <code>Type</code> of the node * @param name the name of the node * * @throws NullPointerException if <code>type</code> or <code>name</code> is <code>null</code> */ public QTreeNode(QTreeNode parent, Type type, String name) { Objects.requireNonNull(type, "type must not be null!"); Objects.requireNonNull(name, "name must not be null!"); this.type = type; this.name = name; this.html = ""; this.attributes = new HashMap<>(); this.parent = parent; this.children = new ArrayList<>(); this.answers = new HashMap<>(); } /** * Gets the answer for the given <code>key</code> or <code>null</code> if no mapping for the key exists. * * @param key the key for the answer * @return the answers or <code>null</code> */ public String[] getAnswers(String key) { return answers.get(key); } /** * Gets the answer map. * * @return the answer map */ public Map<String, String[]> getAnswers() { return answers; } /** * Sets a mapping from the given <code>key</code> to the given <code>answers</code>. * Neither <code>key</code> nor <code>answers</code> may be <code>null</code>. * * @param key the key for the answer * @param answer the answer */ public void setAnswers(String key, String[] answer) { Objects.requireNonNull(key, "key must not be null"); Objects.requireNonNull(answer, "answer must not be null"); answers.put(key, answer); } /** * Gets the answer time for this node. * * @return the answer time */ public long getAnswerTime() { return answerTime; } /** * Sets the answer time for this node. * * @param answerTime the new answer time */ public void setAnswerTime(long answerTime) { this.answerTime = answerTime; } /** * Adds an <code>Attribute</code> to this <code>QTreeNode</code>. If an <code>Attribute</code> with the key of * the given <code>Attribute</code> has previously been added it will be overwritten. * * @param attribute the <code>Attribute</code> to be added */ public void addAttribute(Attribute attribute) { Objects.requireNonNull(attribute, "attribute must not be null!"); attributes.put(attribute.getKey(), attribute); } /** * Gets the <code>Attribute</code> with the given key. If it does not exist it will be created with the empty * <code>String</code> as its content. * * @param key the key of the <code>Attribute</code> * @return the <code>Attribute</code> */ public Attribute getAttribute(String key) { Objects.requireNonNull(key, "key must not be null!"); Attribute attribute = attributes.get(key); if (attribute == null) { attribute = new Attribute(key); addAttribute(attribute); } return attribute; } /** * Gets the <code>Attribute</code>s of this <code>QTreeNode</code>. * * @return the <code>Attribute</code>s */ public Collection<Attribute> getAttributes() { return attributes.values(); } /** * Tests whether an <code>Attribute</code> with the given key exists for this <code>QTreeNode</code>. * * @param key the key of the <code>Attribute</code> * @return true iff an <code>Attribute</code> with the given key exists */ public boolean containsAttribute(String key) { Objects.requireNonNull(key, "key must not be null!"); return attributes.containsKey(key); } /** * Sets the parent of the node to the new value. * * @param parent the new parent of the node */ public void setParent(QTreeNode parent) { this.parent = parent; } /** * Sets the name of the node to the new value. * * @param name the new name of the node * @throws NullPointerException if <code>name</code> is <code>null</code> */ public void setName(String name) { Objects.requireNonNull(name, "name must not be null!"); this.name = name; } /** * Sets the HTML content of the node to the new value. * * @param html the new HTML content of the node * @throws NullPointerException if <code>html</code> is <code>null</code> */ public void setHtml(String html) { Objects.requireNonNull(html, "html must not be null!"); this.html = html; } /** * Removes the given child from this node. * * @param child the child to be removed * @throws NullPointerException if <code>child</code> is <code>null</code> */ public void removeChild(QTreeNode child) { Objects.requireNonNull(child, "child must not be null!"); children.remove(child); } /** * Adds a child to this node. * * @param child the child to be added * @throws NullPointerException if <code>child</code> if <code>null</code> */ public void addChild(QTreeNode child) { Objects.requireNonNull(child, "child must not be null!"); children.add(child); } /** * Adds a child to this node at the given index. * * @param child the child to be added * @param index the desired index * @throws NullPointerException if <code>child</code> is <code>null</code> */ public void addChild(QTreeNode child, int index) { Objects.requireNonNull(child, "child must not be null!"); children.add(index, child); } /** * Returns the sub-tree with this node as root in preorder. * * @return the subtree in preorder */ public List<QTreeNode> preOrder() { List<QTreeNode> preOrderNodes = new ArrayList<>(); preOrderNodes.add(this); children.stream().map(QTreeNode::preOrder).forEach(preOrderNodes::addAll); return preOrderNodes; } /** * Returns the sub-tree with this node as root in breadth first order. * * @return the breadth first order of the tree */ public List<QTreeNode> breadthFirst() { List<QTreeNode> nodes = new ArrayList<>(); Queue<QTreeNode> toVisit = new LinkedList<>(); QTreeNode currentNode; nodes.add(this); toVisit.offer(this); while ((currentNode = toVisit.poll()) != null) { nodes.addAll(currentNode.children); currentNode.children.stream().forEach(toVisit::offer); } return nodes; } /** * Returns the next sibling of this node in the parent's children array. * Returns null if this node has no parent or is the parent's last child. * * @return the next sibling or <code>null</code> */ public QTreeNode getNextSibling() { if (parent == null) { return null; } int nextIndex = parent.children.indexOf(this) + 1; int numChildren = parent.children.size(); if (nextIndex == numChildren) { return null; } return parent.children.get(nextIndex); } /** * Returns the previous sibling of this node in the parent's children array. * Returns null if this node has no parent or is the parent's first child. * * @return the next sibling or <code>null</code> */ public QTreeNode getPreviousSibling() { if (parent == null) { return null; } int previousIndex = parent.children.indexOf(this) - 1; if (previousIndex < 0) { return null; } return parent.children.get(previousIndex); } /** * Gets the <code>Type</code> of the node. * * @return the type */ public Type getType() { return type; } /** * Gets the name of the node. * * @return the name */ public String getName() { return name; } /** * Gets the HTML content stored in the node. * * @return the HTML content */ public String getHtml() { return html; } /** * Gets the children of the node. * * @return the children */ public List<QTreeNode> getChildren() { return children; } /** * Returns whether this node is the last child of its parent. Returns <code>false</code> for a node without parent. * * @return whether this node is the last child of its parent */ public boolean isLastChild() { return getParent() != null && getParent().getIndexOfChild(this) == getParent().getChildCount() - 1; } /** * Gets the parent of this <code>QTreeNode</code> or <code>null</code> if this is the root node. * * @return the parent */ public QTreeNode getParent() { return parent; } /** * Returns the root of the tree this <code>QTreeNode</code> is a part of. * * @return the root node */ public QTreeNode getRoot() { QTreeNode root = this; while (!root.isRoot()) { root = root.getParent(); } return root; } /** * Returns whether this <code>QTreeNode</code> is the root of its tree. * * @return true iff this node is the root of its tree */ public boolean isRoot() { return parent == null; } /** * Gets the child at the given index. * * @param index the index * @return the <code>QTreeNode</code> or <code>null</code> * * @throws IndexOutOfBoundsException if <code>index</code> is out of range {@code (index < 0 || index >= getChildCount())} */ public QTreeNode getChild(int index) { return children.get(index); } /** * Gets the index of the given node or -1 if the node is not a child of this node. * * @param node the node whose index is to be returned * @return the index of the node */ public int getIndexOfChild(QTreeNode node) { return children.indexOf(node); } /** * Gets the number of children this node has. * * @return the number of children */ public int getChildCount() { return children.size(); } /** * Returns whether this node is a leaf (it has no children). * * @return true iff the node is a leaf */ public boolean isLeaf() { return children.isEmpty(); } @Override public String toString() { return name; } @Override public Object clone() throws CloneNotSupportedException { QTreeNode clone = (QTreeNode) super.clone(); Map<String, Attribute> cAttributes = new HashMap<>(); List<QTreeNode> cChildren = new ArrayList<>(); clone.parent = null; // clone the attributes for (Map.Entry<String, Attribute> entry : attributes.entrySet()) { cAttributes.put(entry.getKey(), (Attribute) entry.getValue().clone()); } clone.attributes = cAttributes; // clone the children for (QTreeNode child : children) { QTreeNode cChild = (QTreeNode) child.clone(); cChild.parent = clone; cChildren.add(cChild); } clone.children = cChildren; return clone; } /** * This method will be called (by the JVM) after this object was deserialised. It is implemented because * the XML representation for this object uses implicit collections/maps whose fields will be <code>null</code> * if they were empty at the time of serialisation. These fields will be initialised with empty collections. * * @return always returns <code>this</code> */ private Object readResolve() { if (attributes == null) { attributes = new HashMap<>(); } if (children == null) { children = new ArrayList<>(); } if (answers == null) { answers = new HashMap<>(); } return this; } }