diff --git a/navalplanner-gantt-zk/pom.xml b/navalplanner-gantt-zk/pom.xml
index bd1dd5ae0..55c942b60 100644
--- a/navalplanner-gantt-zk/pom.xml
+++ b/navalplanner-gantt-zk/pom.xml
@@ -40,6 +40,11 @@
org.jgrapht
jgrapht-jdk1.5
+
+
+ junit
+ junit
+
diff --git a/navalplanner-gantt-zk/src/main/java/org/zkoss/ganttz/util/MutableTreeModel.java b/navalplanner-gantt-zk/src/main/java/org/zkoss/ganttz/util/MutableTreeModel.java
new file mode 100644
index 000000000..43b82a8bf
--- /dev/null
+++ b/navalplanner-gantt-zk/src/main/java/org/zkoss/ganttz/util/MutableTreeModel.java
@@ -0,0 +1,147 @@
+package org.zkoss.ganttz.util;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+import org.zkoss.zul.AbstractTreeModel;
+import org.zkoss.zul.event.TreeDataEvent;
+
+/**
+ * @author Óscar González Fernández
+ */
+public class MutableTreeModel extends AbstractTreeModel {
+
+ private static class Node {
+ private T value;
+
+ private List> children = new LinkedList>();
+
+ private Node parentNode;
+
+ private Node(T value) {
+ this.value = value;
+ }
+
+ public void add(Node node) {
+ node.parentNode = this;
+ children.add(node);
+ }
+
+ private void until(LinkedList result, Node parent) {
+ if (parent.equals(this)) {
+ return;
+ } else {
+ result.add(0, this.parentNode.getIndexOf(this));
+ this.parentNode.until(result, parent);
+ }
+
+ }
+
+ private int getIndexOf(Node child) {
+ return children.indexOf(child);
+ }
+
+ public LinkedList until(Node parent) {
+ LinkedList result = new LinkedList();
+ until(result, parent);
+ return result;
+ }
+ }
+
+ private final Class type;
+
+ private final Node root;
+
+ private Map> nodesByDomainObject = new WeakHashMap>();
+
+ private static Node wrap(T object) {
+ return new Node(object);
+ }
+
+ private Node find(Object domainObject) {
+ Node result = nodesByDomainObject.get(domainObject);
+ if (result == null)
+ throw new RuntimeException("not found " + domainObject);
+ return result;
+ }
+
+ private static T unwrap(Node node) {
+ return node == null ? null : node.value;
+ }
+
+ public static MutableTreeModel create(Class type) {
+ return new MutableTreeModel(type, new Node(null));
+ }
+
+ public static MutableTreeModel create(Class type, T root) {
+ return new MutableTreeModel(type, wrap(root));
+ }
+
+ private MutableTreeModel(Class type, Node root) {
+ super(root);
+ if (type == null)
+ throw new IllegalArgumentException("type cannot be null");
+ nodesByDomainObject.put(unwrap(root), root);
+ this.type = type;
+ this.root = root;
+ }
+
+ @Override
+ public int[] getPath(Object parent, Object last) {
+ Node parentNode = find(parent);
+ Node lastNode = find(last);
+ List path = lastNode.until(parentNode);
+ return asIntArray(path);
+ }
+
+ private int[] asIntArray(List 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(Object parent, int index) {
+ Node node = find(parent);
+ return unwrap(node.children.get(index));
+ }
+
+ @Override
+ public int getChildCount(Object parent) {
+ Node node = find(parent);
+ return node.children.size();
+ }
+
+ @Override
+ public boolean isLeaf(Object object) {
+ Node node = find(object);
+ return node.children.isEmpty();
+ }
+
+ public void addToRoot(T child) {
+ add(root, wrap(child));
+ }
+
+ private void add(Node parent, Node child) {
+ parent.add(child);
+ nodesByDomainObject.put(unwrap(child), child);
+ final int position = parent.children.size() - 1;
+ fireEvent(unwrap(parent), position, position,
+ TreeDataEvent.INTERVAL_ADDED);
+ }
+
+ public void add(T parent, T child) {
+ add(find(parent), wrap(child));
+ }
+
+}
diff --git a/navalplanner-gantt-zk/src/test/java/org/zkoss/ganttz/util/MutableTreeModelTest.java b/navalplanner-gantt-zk/src/test/java/org/zkoss/ganttz/util/MutableTreeModelTest.java
new file mode 100644
index 000000000..83e5cf7e1
--- /dev/null
+++ b/navalplanner-gantt-zk/src/test/java/org/zkoss/ganttz/util/MutableTreeModelTest.java
@@ -0,0 +1,119 @@
+package org.zkoss.ganttz.util;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Test;
+import org.zkoss.zul.TreeModel;
+import org.zkoss.zul.event.TreeDataEvent;
+import org.zkoss.zul.event.TreeDataListener;
+
+/**
+ * @author Óscar González Fernández
+ */
+public class MutableTreeModelTest {
+
+ public static class Prueba {
+ }
+
+ @Test
+ public void aMutableTreeModelIsAZkTreeModel() {
+ assertTrue(TreeModel.class.isAssignableFrom(MutableTreeModel.class));
+ }
+
+ @Test
+ public void aMutableTreeModelCanBeCreatedPassingType() {
+ MutableTreeModel model = MutableTreeModel.create(Prueba.class);
+ assertNotNull(model);
+ assertNull(model.getRoot());
+ }
+
+ @Test
+ public void aMutableTreeModelCanBeCreatedPassingTypeAndRootObject() {
+ Prueba root = new Prueba();
+ MutableTreeModel model = MutableTreeModel.create(Prueba.class,
+ root);
+ assertNotNull(model);
+ assertThat(model.getRoot(), equalTo(root));
+ }
+
+ @Test
+ public void childrenCanBeAdded() {
+ Prueba prueba = new Prueba();
+ MutableTreeModel model = MutableTreeModel.create(Prueba.class,
+ prueba);
+ Prueba other = new Prueba();
+ model.add(model.getRoot(), other);
+ Prueba otherChild = new Prueba();
+ model.addToRoot(otherChild);
+ assertThat(model.getChildCount(model.getRoot()), equalTo(2));
+ assertThat(model.getChild(model.getRoot(), 0), equalTo(other));
+ assertThat(model.getChild(model.getRoot(), 1), equalTo(otherChild));
+ }
+
+ @Test
+ public void testLeaf() {
+ Prueba root = new Prueba();
+ MutableTreeModel model = MutableTreeModel.create(Prueba.class,
+ root);
+ Prueba other = new Prueba();
+ model.add(model.getRoot(), other);
+ assertTrue(model.isLeaf(other));
+ assertFalse(model.isLeaf(root));
+ }
+
+ @Test
+ public void childAddedCanBeFoundUsingGetPath() {
+ Prueba root = new Prueba();
+ MutableTreeModel model = MutableTreeModel.create(Prueba.class,
+ root);
+ Prueba child = new Prueba();
+ model.add(root, child);
+ int[] path = model.getPath(model.getRoot(), child);
+ assertThat(path.length, equalTo(1));
+ assertThat(path[0], equalTo(0));
+ }
+
+ @Test
+ public void addingTriggersEvent() {
+ MutableTreeModel model = MutableTreeModel.create(Prueba.class);
+ final ArrayList eventsFired = new ArrayList();
+ model.addTreeDataListener(new TreeDataListener() {
+
+ @Override
+ public void onChange(TreeDataEvent event) {
+ eventsFired.add(event);
+ }
+ });
+ Prueba child1 = new Prueba();
+ Prueba child2 = new Prueba();
+ Prueba granChildren1 = new Prueba();
+ model.add(model.getRoot(), child1);
+ checkIsValid(getLast(eventsFired), model.getRoot(), 0);
+ model.add(model.getRoot(), child2);
+ checkIsValid(getLast(eventsFired), model.getRoot(), 1);
+ model.add(child1, granChildren1);
+ checkIsValid(getLast(eventsFired), child1, 0);
+ assertThat(eventsFired.size(), equalTo(3));
+ }
+
+ private void checkIsValid(TreeDataEvent event, Prueba expectedParent,
+ int expectedPosition) {
+ assertEquals(expectedParent, event.getParent());
+ assertThat(event.getIndexFrom(), equalTo(expectedPosition));
+ assertThat(event.getIndexTo(), equalTo(expectedPosition));
+ assertThat(event.getType(), equalTo(TreeDataEvent.INTERVAL_ADDED));
+ }
+
+ private TreeDataEvent getLast(List list) {
+ return list.get(list.size() - 1);
+ }
+}