/* * Copyright (C) 2011 eXo Platform SAS. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.exoplatform.portal.mop.navigation; import java.util.AbstractCollection; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; import org.exoplatform.portal.tree.list.ListTree; /** * The context of a node. */ public final class NodeContext<N> extends ListTree<NodeContext<N>> { /** The owner tree. */ final TreeContext<N> tree; /** The related model node. */ final N node; /** The handle: either the persistent id or a sequence id. */ String handle; /** A data snapshot. */ NodeData data; /** The new name if any. */ String name; /** The new state if any. */ NodeState state; /** Whether or not this node is hidden. */ private boolean hidden; /** The number of hidden children. */ private int hiddenCount; /** The expension value. */ private boolean expanded; NodeContext(NodeModel<N> model, NodeData data) { if (data == null) { throw new NullPointerException(); } // this.handle = data.id; this.name = null; this.tree = new TreeContext<N>(model, this); this.node = tree.model.create(this); this.data = data; this.state = null; this.hidden = false; this.hiddenCount = 0; this.expanded = false; } private NodeContext(TreeContext<N> tree, NodeData data) { if (data == null) { throw new NullPointerException(); } // this.handle = data.id; this.name = null; this.tree = tree; this.node = tree.model.create(this); this.data = data; this.state = null; this.hidden = false; this.hiddenCount = 0; this.expanded = false; } NodeContext(TreeContext<N> tree, String handle, String name, NodeState state, boolean expanded) { if (handle == null) { throw new NullPointerException(); } if (name == null) { throw new NullPointerException(); } if (state == null) { throw new NullPointerException(); } // this.handle = handle; this.name = name; this.tree = tree; this.node = tree.model.create(this); this.data = null; this.state = state; this.hidden = false; this.hiddenCount = 0; this.expanded = expanded; } /** * Returns true if the tree containing this node has pending transient changes. * * @return true if there are uncommited changes */ public boolean hasChanges() { return tree.hasChanges(); } /** * Returns an unmodifiable list of uncommitted changes applied to this node context. * * @return the list of uncommitted changes */ public List<NodeChange<NodeContext<N>>> getChanges() { return Collections.unmodifiableList(tree.getChanges()); } /** * Returns the associated node with this context * * @return the node */ public N getNode() { return node; } /** * Returns the context id or null if the context is not associated with a persistent navigation node. * * @return the id */ public String getId() { return data != null ? data.getId() : null; } /** * Returns the context index among its parent. * * @return the index value */ public int getIndex() { int count = 0; for (NodeContext<N> node = getPrevious(); node != null; node = node.getPrevious()) { count++; } return count; } public boolean isExpanded() { return expanded; } void expand() { if (!expanded) { this.expanded = true; } else { throw new IllegalStateException("Context is already expanded"); } } /** * Returns true if the context is currently hidden. * * @return the hidden value */ public boolean isHidden() { return hidden; } /** * Updates the hiddent value. * * @param hidden the hidden value */ public void setHidden(boolean hidden) { if (this.hidden != hidden) { NodeContext<N> parent = getParent(); if (parent != null) { if (hidden) { parent.hiddenCount++; } else { parent.hiddenCount--; } } this.hidden = hidden; } } public NodeState getState() { if (state != null) { return state; } else { return data.getState(); } } /** * Update the context state * * @param state the new state * @throws NullPointerException if the state is null */ public void setState(NodeState state) throws NullPointerException { if (state == null) { throw new NullPointerException("No null state accepted"); } // tree.addChange(new NodeChange.Updated<NodeContext<N>>(this, state)); } public String getName() { return name != null ? name : data.name; } /** * Rename this context. * * @param name the new name * @throws NullPointerException if the name is null * @throws IllegalStateException if the parent is null * @throws IllegalArgumentException if the parent already have a child with the specified name */ public void setName(String name) throws NullPointerException, IllegalStateException, IllegalArgumentException { NodeContext<N> parent = getParent(); if (parent == null) { throw new IllegalStateException("Cannot rename a node when its parent is not visible"); } else { NodeContext<N> blah = parent.get(name); if (blah != null) { if (blah == this) { // We do nothing } else { throw new IllegalArgumentException("the node " + name + " already exist"); } } else { tree.addChange(new NodeChange.Renamed<NodeContext<N>>(getParent(), this, name)); } } } /** * Applies a filter recursively, the filter will update the hiddent status of the fragment. * * @param filter the filter to apply */ public void filter(NodeFilter filter) { setHidden(!accept(filter)); if (expanded) { for (NodeContext<N> node = getFirst(); node != null; node = node.getNext()) { node.filter(filter); } } } /** * Apply the filter logic on this node only. This method will not modify the state of this node. * * @param filter the filter to apply * @return true if the filter accepts this node */ public boolean accept(NodeFilter filter) { return filter.accept(getDepth(), getId(), getName(), getState()); } /** * Returns the relative depth of this node with respect to the ancestor argument. * * @param ancestor the ancestor * @return the depth * @throws IllegalArgumentException if the ancestor argument is not an ancestor * @throws NullPointerException if the ancestor argument is null */ public int getDepth(NodeContext<N> ancestor) throws IllegalArgumentException, NullPointerException { if (ancestor == null) { throw new NullPointerException(); } int depth = 0; for (NodeContext<N> current = this; current != null; current = current.getParent()) { if (current == ancestor) { return depth; } else { depth++; } } throw new IllegalArgumentException("Context " + ancestor + " is not an ancestor of " + this); } public NodeContext<N> getDescendant(String handle) throws NullPointerException { if (handle == null) { throw new NullPointerException(); } // NodeContext<N> found = null; if (this.handle.equals(handle)) { found = this; } else { if (expanded) { for (NodeContext<N> current = getFirst(); current != null; current = current.getNext()) { found = current.getDescendant(handle); if (found != null) { break; } } } } return found; } public NodeContext<N> get(String name) throws NullPointerException, IllegalStateException { if (name == null) { throw new NullPointerException(); } if (!expanded) { throw new IllegalStateException("No children relationship"); } // for (NodeContext<N> node = getFirst(); node != null; node = node.getNext()) { if (node.getName().equals(name)) { return node; } } // return null; } /** * Add a child node at the specified index with the specified name. If the index argument is null then the node is added at * the last position among the children otherwise the node is added at the specified index. * * @param index the index * @param name the node name * @return the created node * @throws NullPointerException if the model or the name is null * @throws IndexOutOfBoundsException if the index is negative or greater than the children size * @throws IllegalStateException if the children relationship does not exist */ public NodeContext<N> add(Integer index, String name) throws NullPointerException, IndexOutOfBoundsException, IllegalStateException { if (name == null) { throw new NullPointerException("No null name accepted"); } // NodeContext<N> nodeContext = new NodeContext<N>(tree, "" + tree.sequence++, name, NodeState.INITIAL, true); _add(index, nodeContext); return nodeContext; } /** * Move a context as a child context of this context at the specified index. If the index argument is null then the context * is added at the last position among the children otherwise the context is added at the specified index. * * @param index the index * @param context the context to move * @throws NullPointerException if the model or the context is null * @throws IndexOutOfBoundsException if the index is negative or greater than the children size * @throws IllegalStateException if the children relationship does not exist */ public void add(Integer index, NodeContext<N> context) throws NullPointerException, IndexOutOfBoundsException, IllegalStateException { if (context == null) { throw new NullPointerException("No null context argument accepted"); } // _add(index, context); } public NodeContext<N> insertLast(NodeData data) { if (data == null) { throw new NullPointerException("No null data argument accepted"); } // NodeContext<N> context = new NodeContext<N>(tree, data); insertLast(context); return context; } public NodeContext<N> insertAt(Integer index, NodeData data) { if (data == null) { throw new NullPointerException("No null data argument accepted"); } // NodeContext<N> context = new NodeContext<N>(tree, data); insertAt(index, context); return context; } public NodeContext<N> insertAfter(NodeData data) { if (data == null) { throw new NullPointerException("No null data argument accepted"); } // NodeContext<N> context = new NodeContext<N>(tree, data); insertAfter(context); return context; } private void _add(final Integer index, NodeContext<N> child) { NodeContext<N> previousParent = child.getParent(); // NodeContext<N> previous; if (index == null) { NodeContext<N> before = getLast(); while (before != null && before.isHidden()) { before = before.getPrevious(); } if (before == null) { previous = null; } else { previous = before; } } else if (index < 0) { throw new IndexOutOfBoundsException("No negative index accepted"); } else if (index == 0) { previous = null; } else { NodeContext<N> before = getFirst(); if (before == null) { throw new IndexOutOfBoundsException("Index " + index + " is greater than 0"); } for (int count = index; count > 1; count -= before.isHidden() ? 0 : 1) { before = before.getNext(); if (before == null) { throw new IndexOutOfBoundsException("Index " + index + " is greater than the number of children " + (index - count)); } } previous = before; } // if (previousParent != null) { tree.addChange(new NodeChange.Moved<NodeContext<N>>(previousParent, this, previous, child)); } else { // The name should never be null as it's a newly created node tree.addChange(new NodeChange.Created<NodeContext<N>>(this, previous, child, child.name)); } } // Node related methods /** * Returns the total number of nodes. * * @return the total number of nodes */ public int getNodeSize() { if (expanded) { return getSize(); } else { return data.children.length; } } /** * Returns the node count defined by: * <ul> * <li>when the node has a children relationship, the number of non hidden nodes</li> * <li>when the node has not a children relationship, the total number of nodes</li> * </ul> * * @return the node count */ public int getNodeCount() { if (expanded) { return getSize() - hiddenCount; } else { return data.children.length; } } public N getParentNode() { NodeContext<N> parent = getParent(); return parent != null ? parent.node : null; } public N getNode(String name) throws NullPointerException { NodeContext<N> child = get(name); return child != null && !child.hidden ? child.node : null; } public N getNode(int index) { if (index < 0) { throw new IndexOutOfBoundsException("Index " + index + " cannot be negative"); } if (!expanded) { throw new IllegalStateException("No children relationship"); } NodeContext<N> context = getFirst(); while (context != null && (context.hidden || index-- > 0)) { context = context.getNext(); } if (context == null) { throw new IndexOutOfBoundsException("Index " + index + " is out of bounds"); } else { return context.node; } } public N getDescendantNode(String handle) throws NullPointerException { NodeContext<N> descendant = getDescendant(handle); return descendant != null && !descendant.hidden ? descendant.node : null; } public Iterator<N> iterator() { return new Iterator<N>() { NodeContext<N> next = getFirst(); { while (next != null && next.isHidden()) { next = next.getNext(); } } public boolean hasNext() { return next != null; } public N next() { if (next != null) { NodeContext<N> tmp = next; do { next = next.getNext(); } while (next != null && next.isHidden()); return tmp.getNode(); } else { throw new NoSuchElementException(); } } public void remove() { throw new UnsupportedOperationException(); } }; } /** . */ private Collection<N> nodes; public Collection<N> getNodes() { if (expanded) { if (nodes == null) { nodes = new AbstractCollection<N>() { public Iterator<N> iterator() { return NodeContext.this.iterator(); } public int size() { return getNodeCount(); } }; } return nodes; } else { return null; } } /** * Remove a specified context when it is not hidden. * * @param name the name of the context to remove * @return true if the context was removed * @throws NullPointerException if the name argument is null * @throws IllegalArgumentException if the named context does not exist * @throws IllegalStateException if the children relationship does not exist */ public boolean removeNode(String name) throws NullPointerException, IllegalArgumentException, IllegalStateException { NodeContext<N> node = get(name); if (node == null) { throw new IllegalArgumentException("Cannot remove non existent " + name + " child"); } // return node.removeNode(); } /** * Removes this current context when it is not hidden. * * @return if the context was removed * @throws IllegalStateException if the children relationship does not exist */ public boolean removeNode() throws IllegalStateException { if (hidden) { return false; } else { tree.addChange(new NodeChange.Destroyed<NodeContext<N>>(getParent(), this)); // return true; } } // Callbacks protected void beforeRemove(NodeContext<N> context) { if (!expanded) { throw new IllegalStateException(); } } protected void beforeInsert(NodeContext<N> context) { if (!expanded) { throw new IllegalStateException("No children relationship"); } // if (!tree.editMode) { NodeContext<N> existing = get(context.getName()); if (existing != null && existing != context) { throw new IllegalArgumentException("Tree " + context.getName() + " already in the map"); } } } protected void afterInsert(NodeContext<N> context) { super.afterInsert(context); // if (context.hidden) { hiddenCount++; } } protected void afterRemove(NodeContext<N> context) { if (context.hidden) { hiddenCount--; } // super.afterRemove(context); } @Override public String toString() { return toString(1, new StringBuilder()).toString(); } public StringBuilder toString(int depth, StringBuilder sb) { if (sb == null) { throw new NullPointerException(); } if (depth < 0) { throw new IllegalArgumentException("Depth cannot be negative " + depth); } sb.append("NodeContext[id=").append(getId()).append(",name=").append(getName()); if (expanded && depth > 0) { sb.append(",children={"); for (NodeContext<N> current = getFirst(); current != null; current = current.getNext()) { if (current.getPrevious() != null) { sb.append(','); } current.toString(depth - 1, sb); } sb.append("}"); } else { sb.append("]"); } return sb; } }