/*
* Copyright 2000-2016 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.v7.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 java.util.logging.Level;
import java.util.logging.Logger;
import com.vaadin.v7.data.Container;
import com.vaadin.v7.data.Item;
/**
* A specialized Container whose contents can be accessed like it was a
* tree-like structure.
*
* @author Vaadin Ltd.
* @since 3.0
*
* @deprecated As of 8.0, no replacement available yet. A new hierarchical data API is planned in an upcoming
* version of Vaadin Framework 8.
*/
@Deprecated
@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;
/**
* Counts how many nested contents change disable calls are in progress.
*
* Pending events are only fired when the counter reaches zero again.
*/
private int contentChangedEventsDisabledCount = 0;
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 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();
}
@Override
public Object addItem() {
disableContentsChangeEvents();
try {
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);
}
}
}
return itemId;
} finally {
enableAndFireContentsChangeEvents();
}
}
@Override
protected void fireItemSetChange(Container.ItemSetChangeEvent event) {
if (contentsChangeEventsOn()) {
super.fireItemSetChange(event);
} else {
contentsChangedEventPending = true;
}
}
private boolean contentsChangeEventsOn() {
return contentChangedEventsDisabledCount == 0;
}
private void disableContentsChangeEvents() {
contentChangedEventsDisabledCount++;
}
private void enableAndFireContentsChangeEvents() {
if (contentChangedEventsDisabledCount <= 0) {
getLogger().log(Level.WARNING,
"Mismatched calls to disable and enable contents change events in HierarchicalContainer");
contentChangedEventsDisabledCount = 0;
} else {
contentChangedEventsDisabledCount--;
}
if (contentChangedEventsDisabledCount == 0) {
if (contentsChangedEventPending) {
fireItemSetChange();
}
contentsChangedEventPending = false;
}
}
@Override
public Item addItem(Object itemId) {
disableContentsChangeEvents();
try {
final Item item = super.addItem(itemId);
if (item == null) {
return null;
}
roots.add(itemId);
if (filteredRoots != null) {
if (passesFilters(itemId)) {
filteredRoots.add(itemId);
}
}
return item;
} finally {
enableAndFireContentsChangeEvents();
}
}
@Override
public boolean removeAllItems() {
disableContentsChangeEvents();
try {
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;
}
}
return success;
} finally {
enableAndFireContentsChangeEvents();
}
}
@Override
public boolean removeItem(Object itemId) {
disableContentsChangeEvents();
try {
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);
}
return success;
} finally {
enableAndFireContentsChangeEvents();
}
}
/**
* 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();
try {
boolean removeItemRecursively = removeItemRecursively(this, itemId);
return removeItemRecursively;
} finally {
enableAndFireContentsChangeEvents();
}
}
/**
* 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;
}
@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);
}
}
@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;
@Override
protected boolean passesFilters(Object itemId) {
if (filterOverride != null) {
return filterOverride.contains(itemId);
} else {
return super.passesFilters(itemId);
}
}
private static final Logger getLogger() {
return Logger.getLogger(HierarchicalContainer.class.getName());
}
}