/*
* This file is part of LibrePlan
*
* Copyright (C) 2009-2010 Fundación para o Fomento da Calidade Industrial e
* Desenvolvemento Tecnolóxico de Galicia
* Copyright (C) 2010-2011 Igalia, S.L.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.zkoss.ganttz.util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.WeakHashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.zkoss.zul.AbstractTreeModel;
import org.zkoss.zul.event.TreeDataEvent;
/**
* @author Óscar González Fernández <ogonzalez@igalia.com>
* @author Vova Perebykivskyi <vova@libreplan-enterprise.com>
* @author Bogdan Bodnarjuk <b.bodnarjuk@libreplan-enterprise.com>
*/
public class MutableTreeModel<T> extends AbstractTreeModel {
private static final Log LOG = LogFactory.getLog(MutableTreeModel.class);
public interface IChildrenExtractor<T> {
List<? extends T> getChildren(T parent);
}
public static class Node<T> {
private T value;
private List<Node<T>> children = new LinkedList<>();
private Node<T> parentNode;
public Node(T value) {
this.value = value;
}
public void addAll(Integer position, List<Node<T>> nodes) {
for (Node<T> n : nodes) {
n.parentNode = this;
}
if ( position == null ) {
children.addAll(nodes);
} else {
children.addAll(position, nodes);
}
}
public int[] down(Node<T> node) {
ListIterator<Node<T>> listIterator = children.listIterator();
while (listIterator.hasNext()) {
Node<T> current = listIterator.next();
if ( current == node && listIterator.hasNext() ) {
int nextIndex = listIterator.nextIndex();
listIterator.remove();
listIterator.next();
listIterator.add(node);
return new int[] { nextIndex - 1, nextIndex };
}
}
return new int[] {};
}
public int[] up(Node<T> node) {
ListIterator<Node<T>> listIterator = children.listIterator(children.size());
while ( listIterator.hasPrevious() ) {
Node<T> current = listIterator.previous();
if ( current == node && listIterator.hasPrevious() ) {
listIterator.remove();
int previousIndex = listIterator.previousIndex();
listIterator.previous();
listIterator.add(current);
return new int[] { previousIndex, previousIndex + 1 };
}
}
return new int[] {};
}
private void until(LinkedList<Integer> result, Node<T> parent) {
if ( !parent.equals(this) ) {
if ( isRoot() ) {
/* Final reached, but parent not found */
result.clear();
} else {
result.add(0, this.parentNode.getIndexOf(this));
this.parentNode.until(result, parent);
}
}
}
private boolean isRoot() {
return parentNode == null;
}
private int getIndexOf(Node<T> child) {
return children.indexOf(child);
}
public LinkedList<Integer> until(Node<T> parent) {
LinkedList<Integer> result = new LinkedList<>();
until(result, parent);
return result;
}
public int remove() {
int positionInParent = parentNode.getIndexOf(this);
parentNode.children.remove(positionInParent);
return positionInParent;
}
public Node<T> getParent() {
return parentNode;
}
}
private final Node<T> root;
private transient Map<T, Node<T>> nodesByDomainObject = new WeakHashMap<>();
private static <T> Node<T> wrapOne(T object) {
return new Node<>(object);
}
private static <T> List<Node<T>> wrap(T... objects) {
return wrap(Arrays.asList(objects));
}
private static <T> List<Node<T>> wrap(Collection<? extends T> objects) {
List<Node<T>> result = new ArrayList<>();
for (T o : objects) {
result.add(wrapOne(o));
}
return result;
}
private Node<T> find(Object domainObject) {
for (Map.Entry<T, Node<T>> item : nodesByDomainObject.entrySet()) {
if ( item.getKey() != null && item.getKey().equals(domainObject) ) {
return item.getValue();
}
}
return nodesByDomainObject.get(domainObject);
}
private static <T> T unwrap(Node<T> node) {
return node == null ? null : node.value;
}
public static <T> MutableTreeModel<T> create(Class<T> type) {
return new MutableTreeModel<>(type, new Node<>(null));
}
public static <T> MutableTreeModel<T> create(Class<T> type, T root) {
return new MutableTreeModel<>(type, wrapOne(root));
}
private MutableTreeModel(Class<T> type, Node<T> root) {
super(root);
if ( type == null ) {
throw new IllegalArgumentException("type cannot be null");
}
nodesByDomainObject.put(unwrap(root), root);
this.root = root;
}
/**
* Is some cases it was returning new int[0], but should return new path instead.
* Reason of that: API changes. Before it was looking for child index manually.
* Now it is not looking at all.
* So I decided to return value by our own
* {@link MutableTreeModel#shouldILookForLastValue(Object, Node), {@link #shouldILookForParentValue(Object, Node)}}
* Not to use {@link AbstractTreeModel#getIndexOfChild(Object parent, Object child)}.
*/
public int[] getPath(Object parent, Object last) {
Node<T> parentNode = find(parent);
Node<T> lastNode = find(last);
if ( shouldILookForParentValue(parent, parentNode) ) {
parentNode = find( ((Node) parent).value );
}
if ( shouldILookForLastValue(last, lastNode) ) {
lastNode = find( ((Node) last).value );
}
if ( parentNode == null || lastNode == null) {
return new int[0];
}
List<Integer> path = lastNode.until(parentNode);
return asIntArray(path);
}
private boolean shouldILookForParentValue(Object parent, Node<T> parentNode) {
return parent != null &&
parentNode == null &&
parent.getClass().toString().contains("Node") &&
((Node) parent).value != null;
}
private boolean shouldILookForLastValue(Object last, Node<T> lastNode) {
return last != null &&
lastNode == null &&
last.getClass().toString().contains("Node") &&
((Node) last).value != null;
}
@Override
public int[] getPath(Object last) {
return getPath(getRoot(), last);
}
public T findObjectAt(int... path) {
T current = getRoot();
for (int i = 0; i < path.length; i++) {
int position = path[i];
if ( position >= getChildCount(current) ) {
throw new IllegalArgumentException(
"Failure acessing the path at: " + stringRepresentationUntil(path, i));
}
current = getChild(current, position);
}
return current;
}
private static String stringRepresentationUntil(int[] path, int endExclusive) {
String valid = Arrays.toString(Arrays.copyOfRange(path, 0, endExclusive));
String invalid = Arrays.toString(Arrays.copyOfRange(path, endExclusive, path.length));
return valid + "^" + invalid;
}
private int[] asIntArray(List<Integer> path) {
int[] result = new int[path.size()];
int i = 0;
for (Integer integer : path) {
result[i++] = integer;
}
return result;
}
@Override
public T getRoot() {
return unwrap(root);
}
@Override
public T getChild(int[] path){
T node = getRoot();
for (int item : path) {
if (item < 0 || item > _childCount(node))
return null;
node = getChild(node, item);
}
return node;
}
private int _childCount(T parent) {
return isLeaf(parent) ? 0 : getChildCount(parent);
}
/**
* Previously index was correct,
* because ZK API was calling {@link AbstractTreeModel#getIndexOfChild(Object, Object)} method.
* Now it is not calling that method and sometimes index could be incorrect.
* So I decided to make --index if it will throw exception.
*/
@Override
public T getChild(Object parent, int index) {
Node<T> node;
if (parent instanceof MutableTreeModel.Node) {
node = find(((Node) parent).value);
} else {
node = find(parent);
}
T nodeToReturn;
try {
nodeToReturn = unwrap(node.children.get(index));
} catch (IndexOutOfBoundsException e) {
if (parent != null) {
nodeToReturn = unwrap(node.parentNode.children.get(index));
} else if (index - 1 >= 0) {
nodeToReturn = unwrap(node.children.get(index - 1));
} else {
throw new IndexOutOfBoundsException("Something wrong with indexes");
}
}
return nodeToReturn;
}
@Override
public int getChildCount(Object parent) {
Node<T> node;
if (parent instanceof MutableTreeModel.Node) {
node = find(((Node) parent).value);
} else {
node = find(parent);
}
return node.children.size();
}
@Override
public boolean isLeaf(Object object) {
Node<T> node = find(object);
return node.children.isEmpty();
}
@SuppressWarnings("unchecked")
public void addToRoot(T child) {
add(root, null, wrap(child));
}
private void add(Node<T> parent, Integer position, List<Node<T>> children) {
add(parent, position, children, noChildrenExtractor());
}
private IChildrenExtractor<T> noChildrenExtractor() {
return parent -> Collections.emptyList();
}
private void add(Node<T> parent, Integer position, List<Node<T>> children, IChildrenExtractor<T> extractor) {
if ( children.isEmpty() ) {
return;
}
int indexFrom = position == null ? parent.children.size() : position;
int indexTo = indexFrom + children.size() - 1;
addWithoutSendingEvents(parent, position, children, extractor);
fireEvent(TreeDataEvent.INTERVAL_ADDED, getPath(parent), indexFrom, indexTo);
}
private void addWithoutSendingEvents(Node<T> parent,
Integer position,
List<Node<T>> children,
IChildrenExtractor<T> extractor) {
parent.addAll(position, children);
addToNodesAndDomainMapping(children);
for (Node<T> each : children) {
T value = each.value;
addWithoutSendingEvents(each, 0, wrap(extractor.getChildren(value)), extractor);
}
}
private void addToNodesAndDomainMapping(Collection<Node<T>> children) {
for (Node<T> child : children) {
nodesByDomainObject.put(unwrap(child), child);
}
}
public void add(T parent, T child) {
ArrayList<T> children = new ArrayList<>();
children.add(child);
add(parent, children);
}
public void sendContentsChangedEventFor(T object) {
Node<T> node = find(object);
T parent = getParent(object);
Node<T> parentNode = find(parent);
int position = parentNode.getIndexOf(node);
fireEvent(TreeDataEvent.CONTENTS_CHANGED,getPath(parent), position, position);
}
public void add(T parent, int position, Collection<? extends T> children) {
add(find(parent), position, wrap(children));
}
public void add(T parent, Collection<? extends T> children) {
Node<T> parentNode = find(parent);
add(parentNode, null, wrap(children));
}
public void add(T parent, int position, Collection<? extends T> children, IChildrenExtractor<T> childrenExtractor) {
add(find(parent), position, wrap(children), childrenExtractor);
}
public void add(T parent, Collection<? extends T> children, IChildrenExtractor<T> childrenExtractor) {
add(find(parent), null, wrap(children), childrenExtractor);
}
public void remove(T node) {
Node<T> found = find(node);
if ( found.isRoot() ) {
throw new IllegalArgumentException(node + " is root. It can't be removed");
}
int positionInParent = found.remove();
nodesByDomainObject.remove(node);
fireEvent(TreeDataEvent.INTERVAL_REMOVED, getPath(found.parentNode), positionInParent, positionInParent);
}
public T getParent(T node) {
Node<T> associatedNode = find(node);
if ( associatedNode.equals(root) ) {
throw new IllegalArgumentException(node + " is root");
}
return unwrap(associatedNode.getParent());
}
public List<T> getParents(T node) {
ArrayList<T> result = new ArrayList<>();
try {
T current = node;
while ( !isRoot(current) ) {
current = getParent(current);
result.add(current);
}
} catch (Exception e) {
LOG.error("Trying to get the parent of a removed node", e);
}
return result;
}
public boolean isRoot(T node) {
return find(node).isRoot();
}
public void replace(T nodeToRemove, T nodeToAdd, IChildrenExtractor<T> childrenExtractor) {
T parent = getParent(nodeToRemove);
Node<T> parentNode = find(parent);
final int insertionPosition = parentNode.getIndexOf(find(nodeToRemove));
remove(nodeToRemove);
if ( childrenExtractor != null ) {
add(parent, insertionPosition, Collections.singletonList(nodeToAdd), childrenExtractor);
} else {
add(parent, insertionPosition, Collections.singletonList(nodeToAdd));
}
}
public void replace(T nodeToRemove, T nodeToAdd) {
replace(nodeToRemove, nodeToAdd, null);
}
public void down(T node) {
T parent = getParent(node);
Node<T> parentNode = find(parent);
int[] changed = parentNode.down(find(node));
if ( changed.length != 0 ) {
fireRecreationOfInterval(parentNode, changed[0], changed[1]);
}
}
public void up(T node) {
T parent = getParent(node);
Node<T> parentNode = find(parent);
int[] changed = parentNode.up(find(node));
if ( changed.length != 0 ) {
fireRecreationOfInterval(parentNode, changed[0], changed[1]);
}
}
private void fireRecreationOfInterval(Node<T> parentNode, int start, int endInclusive) {
fireEvent(TreeDataEvent.INTERVAL_REMOVED,getPath(parentNode.value), start, endInclusive);
fireEvent(TreeDataEvent.INTERVAL_ADDED, getPath(parentNode.value), start, endInclusive);
}
public boolean isEmpty() {
return getChildCount(getRoot()) == 0;
}
public boolean contains(T object) {
return find(object) != null;
}
public boolean contains(T parent, T child) {
Node<T> parentNode = find(parent);
Node<T> childNode = find(child);
return parentNode != null &&
childNode != null &&
childNode.getParent() != null &&
childNode.getParent().equals(parentNode);
}
public List<T> asList() {
List<T> result = new ArrayList<>();
asList(getRoot(), result);
return result;
}
private void asList(T root, List<T> result) {
List<T> list = new ArrayList<>();
for (int i = 0; i < getChildCount(root); i++) {
final T child = getChild(root, i);
list.add(child);
result.add(child);
}
for (T each: list) {
asList(each, result);
}
}
}