diff options
Diffstat (limited to 'server/src/com/vaadin/data/util/HierarchicalContainer.java')
-rw-r--r-- | server/src/com/vaadin/data/util/HierarchicalContainer.java | 824 |
1 files changed, 824 insertions, 0 deletions
diff --git a/server/src/com/vaadin/data/util/HierarchicalContainer.java b/server/src/com/vaadin/data/util/HierarchicalContainer.java new file mode 100644 index 0000000000..8338e3c190 --- /dev/null +++ b/server/src/com/vaadin/data/util/HierarchicalContainer.java @@ -0,0 +1,824 @@ +/* + * Copyright 2011 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.data.util; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.Set; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; + +/** + * A specialized Container whose contents can be accessed like it was a + * tree-like structure. + * + * @author Vaadin Ltd. + * @since 3.0 + */ +@SuppressWarnings("serial") +public class HierarchicalContainer extends IndexedContainer implements + Container.Hierarchical { + + /** + * Set of IDs of those contained Items that can't have children. + */ + private final HashSet<Object> noChildrenAllowed = new HashSet<Object>(); + + /** + * Mapping from Item ID to parent Item ID. + */ + private final HashMap<Object, Object> parent = new HashMap<Object, Object>(); + + /** + * Mapping from Item ID to parent Item ID for items included in the filtered + * container. + */ + private HashMap<Object, Object> filteredParent = null; + + /** + * Mapping from Item ID to a list of child IDs. + */ + private final HashMap<Object, LinkedList<Object>> children = new HashMap<Object, LinkedList<Object>>(); + + /** + * Mapping from Item ID to a list of child IDs when filtered + */ + private HashMap<Object, LinkedList<Object>> filteredChildren = null; + + /** + * List that contains all root elements of the container. + */ + private final LinkedList<Object> roots = new LinkedList<Object>(); + + /** + * List that contains all filtered root elements of the container. + */ + private LinkedList<Object> filteredRoots = null; + + /** + * Determines how filtering of the container is done. + */ + private boolean includeParentsWhenFiltering = true; + + private boolean contentChangedEventsDisabled = false; + + private boolean contentsChangedEventPending; + + /* + * Can the specified Item have any children? Don't add a JavaDoc comment + * here, we use the default documentation from implemented interface. + */ + @Override + public boolean areChildrenAllowed(Object itemId) { + if (noChildrenAllowed.contains(itemId)) { + return false; + } + return containsId(itemId); + } + + /* + * Gets the IDs of the children of the specified Item. Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public Collection<?> getChildren(Object itemId) { + LinkedList<Object> c; + + if (filteredChildren != null) { + c = filteredChildren.get(itemId); + } else { + c = children.get(itemId); + } + + if (c == null) { + return null; + } + return Collections.unmodifiableCollection(c); + } + + /* + * Gets the ID of the parent of the specified Item. Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public Object getParent(Object itemId) { + if (filteredParent != null) { + return filteredParent.get(itemId); + } + return parent.get(itemId); + } + + /* + * Is the Item corresponding to the given ID a leaf node? Don't add a + * JavaDoc comment here, we use the default documentation from implemented + * interface. + */ + @Override + public boolean hasChildren(Object itemId) { + if (filteredChildren != null) { + return filteredChildren.containsKey(itemId); + } else { + return children.containsKey(itemId); + } + } + + /* + * Is the Item corresponding to the given ID a root node? Don't add a + * JavaDoc comment here, we use the default documentation from implemented + * interface. + */ + @Override + public boolean isRoot(Object itemId) { + // If the container is filtered the itemId must be among filteredRoots + // to be a root. + if (filteredRoots != null) { + if (!filteredRoots.contains(itemId)) { + return false; + } + } else { + // Container is not filtered + if (parent.containsKey(itemId)) { + return false; + } + } + + return containsId(itemId); + } + + /* + * Gets the IDs of the root elements in the container. Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public Collection<?> rootItemIds() { + if (filteredRoots != null) { + return Collections.unmodifiableCollection(filteredRoots); + } else { + return Collections.unmodifiableCollection(roots); + } + } + + /** + * <p> + * Sets the given Item's capability to have children. If the Item identified + * with the itemId already has children and the areChildrenAllowed is false + * this method fails and <code>false</code> is returned; the children must + * be first explicitly removed with + * {@link #setParent(Object itemId, Object newParentId)} or + * {@link com.vaadin.data.Container#removeItem(Object itemId)}. + * </p> + * + * @param itemId + * the ID of the Item in the container whose child capability is + * to be set. + * @param childrenAllowed + * the boolean value specifying if the Item can have children or + * not. + * @return <code>true</code> if the operation succeeded, <code>false</code> + * if not + */ + @Override + public boolean setChildrenAllowed(Object itemId, boolean childrenAllowed) { + + // Checks that the item is in the container + if (!containsId(itemId)) { + return false; + } + + // Updates status + if (childrenAllowed) { + noChildrenAllowed.remove(itemId); + } else { + noChildrenAllowed.add(itemId); + } + + return true; + } + + /** + * <p> + * Sets the parent of an Item. The new parent item must exist and be able to + * have children. (<code>canHaveChildren(newParentId) == true</code>). It is + * also possible to detach a node from the hierarchy (and thus make it root) + * by setting the parent <code>null</code>. + * </p> + * + * @param itemId + * the ID of the item to be set as the child of the Item + * identified with newParentId. + * @param newParentId + * the ID of the Item that's to be the new parent of the Item + * identified with itemId. + * @return <code>true</code> if the operation succeeded, <code>false</code> + * if not + */ + @Override + public boolean setParent(Object itemId, Object newParentId) { + + // Checks that the item is in the container + if (!containsId(itemId)) { + return false; + } + + // Gets the old parent + final Object oldParentId = parent.get(itemId); + + // Checks if no change is necessary + if ((newParentId == null && oldParentId == null) + || ((newParentId != null) && newParentId.equals(oldParentId))) { + return true; + } + + // Making root? + if (newParentId == null) { + // The itemId should become a root so we need to + // - Remove it from the old parent's children list + // - Add it as a root + // - Remove it from the item -> parent list (parent is null for + // roots) + + // Removes from old parents children list + final LinkedList<Object> l = children.get(oldParentId); + if (l != null) { + l.remove(itemId); + if (l.isEmpty()) { + children.remove(oldParentId); + } + + } + + // Add to be a root + roots.add(itemId); + + // Updates parent + parent.remove(itemId); + + if (hasFilters()) { + // Refilter the container if setParent is called when filters + // are applied. Changing parent can change what is included in + // the filtered version (if includeParentsWhenFiltering==true). + doFilterContainer(hasFilters()); + } + + fireItemSetChange(); + + return true; + } + + // We get here when the item should not become a root and we need to + // - Verify the new parent exists and can have children + // - Check that the new parent is not a child of the selected itemId + // - Updated the item -> parent mapping to point to the new parent + // - Remove the item from the roots list if it was a root + // - Remove the item from the old parent's children list if it was not a + // root + + // Checks that the new parent exists in container and can have + // children + if (!containsId(newParentId) || noChildrenAllowed.contains(newParentId)) { + return false; + } + + // Checks that setting parent doesn't result to a loop + Object o = newParentId; + while (o != null && !o.equals(itemId)) { + o = parent.get(o); + } + if (o != null) { + return false; + } + + // Updates parent + parent.put(itemId, newParentId); + LinkedList<Object> pcl = children.get(newParentId); + if (pcl == null) { + // Create an empty list for holding children if one were not + // previously created + pcl = new LinkedList<Object>(); + children.put(newParentId, pcl); + } + pcl.add(itemId); + + // Removes from old parent or root + if (oldParentId == null) { + roots.remove(itemId); + } else { + final LinkedList<Object> l = children.get(oldParentId); + if (l != null) { + l.remove(itemId); + if (l.isEmpty()) { + children.remove(oldParentId); + } + } + } + + if (hasFilters()) { + // Refilter the container if setParent is called when filters + // are applied. Changing parent can change what is included in + // the filtered version (if includeParentsWhenFiltering==true). + doFilterContainer(hasFilters()); + } + + fireItemSetChange(); + + return true; + } + + private boolean hasFilters() { + return (filteredRoots != null); + } + + /** + * Moves a node (an Item) in the container immediately after a sibling node. + * The two nodes must have the same parent in the container. + * + * @param itemId + * the identifier of the moved node (Item) + * @param siblingId + * the identifier of the reference node (Item), after which the + * other node will be located + */ + public void moveAfterSibling(Object itemId, Object siblingId) { + Object parent2 = getParent(itemId); + LinkedList<Object> childrenList; + if (parent2 == null) { + childrenList = roots; + } else { + childrenList = children.get(parent2); + } + if (siblingId == null) { + childrenList.remove(itemId); + childrenList.addFirst(itemId); + + } else { + int oldIndex = childrenList.indexOf(itemId); + int indexOfSibling = childrenList.indexOf(siblingId); + if (indexOfSibling != -1 && oldIndex != -1) { + int newIndex; + if (oldIndex > indexOfSibling) { + newIndex = indexOfSibling + 1; + } else { + newIndex = indexOfSibling; + } + childrenList.remove(oldIndex); + childrenList.add(newIndex, itemId); + } else { + throw new IllegalArgumentException( + "Given identifiers no not have the same parent."); + } + } + fireItemSetChange(); + + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.IndexedContainer#addItem() + */ + @Override + public Object addItem() { + disableContentsChangeEvents(); + final Object itemId = super.addItem(); + if (itemId == null) { + return null; + } + + if (!roots.contains(itemId)) { + roots.add(itemId); + if (filteredRoots != null) { + if (passesFilters(itemId)) { + filteredRoots.add(itemId); + } + } + } + enableAndFireContentsChangeEvents(); + return itemId; + } + + @Override + protected void fireItemSetChange( + com.vaadin.data.Container.ItemSetChangeEvent event) { + if (contentsChangeEventsOn()) { + super.fireItemSetChange(event); + } else { + contentsChangedEventPending = true; + } + } + + private boolean contentsChangeEventsOn() { + return !contentChangedEventsDisabled; + } + + private void disableContentsChangeEvents() { + contentChangedEventsDisabled = true; + } + + private void enableAndFireContentsChangeEvents() { + contentChangedEventsDisabled = false; + if (contentsChangedEventPending) { + fireItemSetChange(); + } + contentsChangedEventPending = false; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.IndexedContainer#addItem(java.lang.Object) + */ + @Override + public Item addItem(Object itemId) { + disableContentsChangeEvents(); + final Item item = super.addItem(itemId); + if (item == null) { + return null; + } + + roots.add(itemId); + + if (filteredRoots != null) { + if (passesFilters(itemId)) { + filteredRoots.add(itemId); + } + } + enableAndFireContentsChangeEvents(); + return item; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.IndexedContainer#removeAllItems() + */ + @Override + public boolean removeAllItems() { + disableContentsChangeEvents(); + final boolean success = super.removeAllItems(); + + if (success) { + roots.clear(); + parent.clear(); + children.clear(); + noChildrenAllowed.clear(); + if (filteredRoots != null) { + filteredRoots = null; + } + if (filteredChildren != null) { + filteredChildren = null; + } + if (filteredParent != null) { + filteredParent = null; + } + } + enableAndFireContentsChangeEvents(); + return success; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.IndexedContainer#removeItem(java.lang.Object ) + */ + @Override + public boolean removeItem(Object itemId) { + disableContentsChangeEvents(); + final boolean success = super.removeItem(itemId); + + if (success) { + // Remove from roots if this was a root + if (roots.remove(itemId)) { + + // If filtering is enabled we might need to remove it from the + // filtered list also + if (filteredRoots != null) { + filteredRoots.remove(itemId); + } + } + + // Clear the children list. Old children will now become root nodes + LinkedList<Object> childNodeIds = children.remove(itemId); + if (childNodeIds != null) { + if (filteredChildren != null) { + filteredChildren.remove(itemId); + } + for (Object childId : childNodeIds) { + setParent(childId, null); + } + } + + // Parent of the item that we are removing will contain the item id + // in its children list + final Object parentItemId = parent.get(itemId); + if (parentItemId != null) { + final LinkedList<Object> c = children.get(parentItemId); + if (c != null) { + c.remove(itemId); + + if (c.isEmpty()) { + children.remove(parentItemId); + } + + // Found in the children list so might also be in the + // filteredChildren list + if (filteredChildren != null) { + LinkedList<Object> f = filteredChildren + .get(parentItemId); + if (f != null) { + f.remove(itemId); + if (f.isEmpty()) { + filteredChildren.remove(parentItemId); + } + } + } + } + } + parent.remove(itemId); + if (filteredParent != null) { + // Item id no longer has a parent as the item id is not in the + // container. + filteredParent.remove(itemId); + } + noChildrenAllowed.remove(itemId); + } + + enableAndFireContentsChangeEvents(); + + return success; + } + + /** + * Removes the Item identified by given itemId and all its children. + * + * @see #removeItem(Object) + * @param itemId + * the identifier of the Item to be removed + * @return true if the operation succeeded + */ + public boolean removeItemRecursively(Object itemId) { + disableContentsChangeEvents(); + boolean removeItemRecursively = removeItemRecursively(this, itemId); + enableAndFireContentsChangeEvents(); + return removeItemRecursively; + } + + /** + * Removes the Item identified by given itemId and all its children from the + * given Container. + * + * @param container + * the container where the item is to be removed + * @param itemId + * the identifier of the Item to be removed + * @return true if the operation succeeded + */ + public static boolean removeItemRecursively( + Container.Hierarchical container, Object itemId) { + boolean success = true; + Collection<?> children2 = container.getChildren(itemId); + if (children2 != null) { + Object[] array = children2.toArray(); + for (int i = 0; i < array.length; i++) { + boolean removeItemRecursively = removeItemRecursively( + container, array[i]); + if (!removeItemRecursively) { + success = false; + } + } + } + // remove the root of subtree if children where succesfully removed + if (success) { + success = container.removeItem(itemId); + } + return success; + + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.IndexedContainer#doSort() + */ + @Override + protected void doSort() { + super.doSort(); + + Collections.sort(roots, getItemSorter()); + for (LinkedList<Object> childList : children.values()) { + Collections.sort(childList, getItemSorter()); + } + } + + /** + * Used to control how filtering works. @see + * {@link #setIncludeParentsWhenFiltering(boolean)} for more information. + * + * @return true if all parents for items that match the filter are included + * when filtering, false if only the matching items are included + */ + public boolean isIncludeParentsWhenFiltering() { + return includeParentsWhenFiltering; + } + + /** + * Controls how the filtering of the container works. Set this to true to + * make filtering include parents for all matched items in addition to the + * items themselves. Setting this to false causes the filtering to only + * include the matching items and make items with excluded parents into root + * items. + * + * @param includeParentsWhenFiltering + * true to include all parents for items that match the filter, + * false to only include the matching items + */ + public void setIncludeParentsWhenFiltering( + boolean includeParentsWhenFiltering) { + this.includeParentsWhenFiltering = includeParentsWhenFiltering; + if (filteredRoots != null) { + // Currently filtered so needs to be re-filtered + doFilterContainer(true); + } + } + + /* + * Overridden to provide filtering for root & children items. + * + * (non-Javadoc) + * + * @see com.vaadin.data.util.IndexedContainer#updateContainerFiltering() + */ + @Override + protected boolean doFilterContainer(boolean hasFilters) { + if (!hasFilters) { + // All filters removed + filteredRoots = null; + filteredChildren = null; + filteredParent = null; + + return super.doFilterContainer(hasFilters); + } + + // Reset data structures + filteredRoots = new LinkedList<Object>(); + filteredChildren = new HashMap<Object, LinkedList<Object>>(); + filteredParent = new HashMap<Object, Object>(); + + if (includeParentsWhenFiltering) { + // Filter so that parents for items that match the filter are also + // included + HashSet<Object> includedItems = new HashSet<Object>(); + for (Object rootId : roots) { + if (filterIncludingParents(rootId, includedItems)) { + filteredRoots.add(rootId); + addFilteredChildrenRecursively(rootId, includedItems); + } + } + // includedItemIds now contains all the item ids that should be + // included. Filter IndexedContainer based on this + filterOverride = includedItems; + super.doFilterContainer(hasFilters); + filterOverride = null; + + return true; + } else { + // Filter by including all items that pass the filter and make items + // with no parent new root items + + // Filter IndexedContainer first so getItemIds return the items that + // match + super.doFilterContainer(hasFilters); + + LinkedHashSet<Object> filteredItemIds = new LinkedHashSet<Object>( + getItemIds()); + + for (Object itemId : filteredItemIds) { + Object itemParent = parent.get(itemId); + if (itemParent == null || !filteredItemIds.contains(itemParent)) { + // Parent is not included or this was a root, in both cases + // this should be a filtered root + filteredRoots.add(itemId); + } else { + // Parent is included. Add this to the children list (create + // it first if necessary) + addFilteredChild(itemParent, itemId); + } + } + + return true; + } + } + + /** + * Adds the given childItemId as a filteredChildren for the parentItemId and + * sets it filteredParent. + * + * @param parentItemId + * @param childItemId + */ + private void addFilteredChild(Object parentItemId, Object childItemId) { + LinkedList<Object> parentToChildrenList = filteredChildren + .get(parentItemId); + if (parentToChildrenList == null) { + parentToChildrenList = new LinkedList<Object>(); + filteredChildren.put(parentItemId, parentToChildrenList); + } + filteredParent.put(childItemId, parentItemId); + parentToChildrenList.add(childItemId); + + } + + /** + * Recursively adds all items in the includedItems list to the + * filteredChildren map in the same order as they are in the children map. + * Starts from parentItemId and recurses down as long as child items that + * should be included are found. + * + * @param parentItemId + * The item id to start recurse from. Not added to a + * filteredChildren list + * @param includedItems + * Set containing the item ids for the items that should be + * included in the filteredChildren map + */ + private void addFilteredChildrenRecursively(Object parentItemId, + HashSet<Object> includedItems) { + LinkedList<Object> childList = children.get(parentItemId); + if (childList == null) { + return; + } + + for (Object childItemId : childList) { + if (includedItems.contains(childItemId)) { + addFilteredChild(parentItemId, childItemId); + addFilteredChildrenRecursively(childItemId, includedItems); + } + } + } + + /** + * Scans the itemId and all its children for which items should be included + * when filtering. All items which passes the filters are included. + * Additionally all items that have a child node that should be included are + * also themselves included. + * + * @param itemId + * @param includedItems + * @return true if the itemId should be included in the filtered container. + */ + private boolean filterIncludingParents(Object itemId, + HashSet<Object> includedItems) { + boolean toBeIncluded = passesFilters(itemId); + + LinkedList<Object> childList = children.get(itemId); + if (childList != null) { + for (Object childItemId : children.get(itemId)) { + toBeIncluded |= filterIncludingParents(childItemId, + includedItems); + } + } + + if (toBeIncluded) { + includedItems.add(itemId); + } + return toBeIncluded; + } + + private Set<Object> filterOverride = null; + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.util.IndexedContainer#passesFilters(java.lang.Object) + */ + @Override + protected boolean passesFilters(Object itemId) { + if (filterOverride != null) { + return filterOverride.contains(itemId); + } else { + return super.passesFilters(itemId); + } + } +} |