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