package com.constellio.app.ui.framework.components.tree;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import com.constellio.app.services.factories.ConstellioFactories;
import com.constellio.app.ui.framework.data.LazyTreeDataProvider;
import com.constellio.app.ui.framework.data.ObjectsResponse;
import com.constellio.model.services.factories.ModelLayerFactory;
import com.vaadin.data.Item;
import com.vaadin.data.Property;
import com.vaadin.event.ItemClickEvent;
import com.vaadin.event.ItemClickEvent.ItemClickListener;
import com.vaadin.server.Extension;
import com.vaadin.server.Resource;
import com.vaadin.ui.AbstractSelect.ItemCaptionMode;
import com.vaadin.ui.AbstractSelect.ItemDescriptionGenerator;
import com.vaadin.ui.Component;
import com.vaadin.ui.CustomField;
import com.vaadin.ui.Tree;
import com.vaadin.ui.Tree.CollapseEvent;
import com.vaadin.ui.Tree.CollapseListener;
import com.vaadin.ui.Tree.ExpandEvent;
import com.vaadin.ui.Tree.ExpandListener;
import com.vaadin.ui.Tree.ItemStyleGenerator;
public class LazyTree<T extends Serializable> extends CustomField<T> {
private static final String LOADER_NEXT_LOADED_INDEX_ID = "loaderNextLoadedIndex";
private static final String LOADER_PARENT_ITEM_ID = "loaderParentItemId";
private LazyTreeDataProvider<T> dataProvider;
private int bufferSize;
private Tree adaptee;
private List<ItemClickListener> itemClickListeners = new ArrayList<ItemClickListener>();
public LazyTree(LazyTreeDataProvider<T> treeDataProvider) {
this(treeDataProvider, getBufferSizeFromConfig());
}
private static int getBufferSizeFromConfig() {
ConstellioFactories constellioFactories = ConstellioFactories.getInstance();
ModelLayerFactory modelLayerFactory = constellioFactories.getModelLayerFactory();
return modelLayerFactory.getSystemConfigs().getLazyTreeBufferSize();
}
public LazyTree(LazyTreeDataProvider<T> treeDataProvider, int bufferSize) {
super();
this.dataProvider = treeDataProvider;
this.bufferSize = bufferSize;
adaptee = new Tree() {
@SuppressWarnings({ "rawtypes", "unchecked" })
@Override
public Collection getContainerPropertyIds() {
Collection propertyIds = LazyTree.this.getContainerPropertyIds();
if (propertyIds == null) {
propertyIds = super.getContainerPropertyIds();
}
propertyIds.add(LOADER_NEXT_LOADED_INDEX_ID);
propertyIds.add(LOADER_PARENT_ITEM_ID);
return propertyIds;
}
@Override
public Property<?> getContainerProperty(Object itemId, Object propertyId) {
Property<?> property;
if (isLoader(itemId)) {
property = super.getContainerProperty(itemId, propertyId);
} else {
property = LazyTree.this.getContainerProperty(itemId, propertyId);
if (property == null) {
property = super.getContainerProperty(itemId, propertyId);
}
}
return property;
}
@SuppressWarnings("unchecked")
@Override
public String getItemCaption(Object itemId) {
String itemCaption;
if (isLoader(itemId)) {
itemCaption = getLoaderItemCaption(itemId);
} else {
itemCaption = LazyTree.this.getItemCaption((T) itemId);
if (itemCaption == null) {
itemCaption = super.getItemCaption(itemId);
}
}
return itemCaption;
}
@SuppressWarnings("unchecked")
@Override
public Resource getItemIcon(Object itemId) {
Resource itemIcon;
if (isLoader(itemId)) {
itemIcon = null;
} else {
itemIcon = LazyTree.this.getItemIcon((T) itemId);
if (itemIcon == null) {
itemIcon = super.getItemIcon(itemId);
}
}
return itemIcon;
}
};
adaptee.setItemDescriptionGenerator(new ItemDescriptionGenerator() {
@SuppressWarnings("unchecked")
@Override
public String generateDescription(Component source, Object itemId, Object propertyId) {
String itemDescription;
if (isLoader(itemId)) {
itemDescription = getLoaderItemDescription(itemId);
} else {
itemDescription = dataProvider.getDescription((T) itemId);
}
return itemDescription;
}
});
adaptee.addContainerProperty(LOADER_NEXT_LOADED_INDEX_ID, Integer.class, null);
adaptee.addContainerProperty(LOADER_PARENT_ITEM_ID, LazyTree.this.getType(), null);
}
@Override
protected Component initContent() {
ObjectsResponse<T> rootObjectsResponse = dataProvider.getRootObjects(0, bufferSize);
final int rootObjectsCount = rootObjectsResponse.getCount();
for (T rootObject : rootObjectsResponse.getObjects()) {
addItem(rootObject, null);
}
if (rootObjectsCount > rootObjectsResponse.getObjects().size()) {
addLoaderItem(null, rootObjectsResponse.getObjects().size());
}
adaptee.addItemClickListener(new ItemClickListener() {
@SuppressWarnings("unchecked")
@Override
public void itemClick(ItemClickEvent event) {
Object itemId = event.getItemId();
if (isLoader(itemId)) {
// Clicking on a loader, we will replace it with the actual children
T parent = getParentForLoader(itemId);
int nextLoadedIndex = getNextLoadedIndexForLoader(itemId);
if (parent != null) {
ObjectsResponse<T> response = dataProvider.getChildren(parent, nextLoadedIndex, bufferSize);
int childrenCount = response.getCount();
replaceLoaderWithChildren(itemId, parent, response.getObjects());
if (childrenCount > nextLoadedIndex + response.getObjects().size()) {
addLoaderItem(parent, nextLoadedIndex + response.getObjects().size());
}
} else {
// Root object
ObjectsResponse<T> rootObjects = dataProvider.getRootObjects(nextLoadedIndex, bufferSize);
adaptee.removeItem(itemId);
for (T rootObject : rootObjects.getObjects()) {
addItem(rootObject, null);
}
if (rootObjects.getCount() > nextLoadedIndex + rootObjects.getObjects().size()) {
addLoaderItem(null, nextLoadedIndex + rootObjects.getObjects().size());
}
}
} else {
setValue((T) itemId);
// Forward to the adaptee
if (isExpanded(itemId)) {
adaptee.collapseItem(itemId);
} else {
adaptee.expandItem(itemId);
}
for (ItemClickListener itemClickListener : itemClickListeners) {
itemClickListener.itemClick(event);
}
}
}
});
adaptee.addExpandListener(new ExpandListener() {
@SuppressWarnings("unchecked")
@Override
public void nodeExpand(ExpandEvent event) {
T itemId = (T) event.getItemId();
adjustLoaderAfterExpand(itemId);
}
});
adaptee.addCollapseListener(new CollapseListener() {
@SuppressWarnings("unchecked")
@Override
public void nodeCollapse(CollapseEvent event) {
T itemId = (T) event.getItemId();
adjustLoaderAfterCollapse(itemId);
}
});
return adaptee;
}
private void adjustLoaderAfterExpand(T itemId) {
Object loaderId = getLoaderId(itemId);
if (loaderId != null) {
// Since the loader is present, it means that the children have not been removed
ObjectsResponse<T> childrenResponse = dataProvider.getChildren(itemId, 0, bufferSize);
replaceLoaderWithChildren(loaderId, itemId, childrenResponse.getObjects());
if (childrenResponse.getCount() > childrenResponse.getObjects().size()) {
addLoaderItem(itemId, childrenResponse.getObjects().size());
}
}
Resource icon = getItemIcon(itemId);
setItemIcon(itemId, icon, "");
}
private void adjustLoaderAfterCollapse(T itemId) {
Resource icon = getItemIcon(itemId);
setItemIcon(itemId, icon, "");
}
public final LazyTreeDataProvider<T> getDataProvider() {
return dataProvider;
}
private void addItem(T child, T parent) {
addItem(child, parent, true);
}
private void addItem(T child, T parent, boolean addLoaderItemIfPossible) {
adaptee.addItem(child);
if (parent != null) {
adaptee.setParent(child, parent);
}
if (!dataProvider.isLeaf(child) && dataProvider.hasChildren(child)) {
if (addLoaderItemIfPossible) {
addLoaderItem(child, 0);
}
} else {
adaptee.setChildrenAllowed(child, false);
}
}
@SuppressWarnings("unchecked")
private void addLoaderItem(T parent, int nextLoadedIndex) {
Object loaderItemId = adaptee.addItem();
Item loadingItem = adaptee.getItem(loaderItemId);
loadingItem.getItemProperty(LOADER_NEXT_LOADED_INDEX_ID).setValue(nextLoadedIndex);
loadingItem.getItemProperty(LOADER_PARENT_ITEM_ID).setValue(parent);
if (parent != null) {
adaptee.setParent(loaderItemId, parent);
}
adaptee.setChildrenAllowed(loaderItemId, false);
}
@SuppressWarnings("unchecked")
private T getParentForLoader(Object loaderItemId) {
Item loaderItem = adaptee.getItem(loaderItemId);
return (T) loaderItem.getItemProperty(LOADER_PARENT_ITEM_ID).getValue();
}
@SuppressWarnings("unchecked")
private Integer getNextLoadedIndexForLoader(Object loaderItemId) {
Item loaderItem = adaptee.getItem(loaderItemId);
Property<Integer> nextLoadedIndexProperty = loaderItem.getItemProperty(LOADER_NEXT_LOADED_INDEX_ID);
return nextLoadedIndexProperty != null ? nextLoadedIndexProperty.getValue() : null;
}
@SuppressWarnings("unchecked")
private boolean isLoader(Object itemId) {
Item item = adaptee.getItem(itemId);
Property<Integer> nextLoadedIndexProperty = item.getItemProperty(LOADER_NEXT_LOADED_INDEX_ID);
return nextLoadedIndexProperty != null && nextLoadedIndexProperty.getValue() != null;
}
private Object getLoaderId(T parent) {
Object initialLoaderId;
Collection<?> children = adaptee.getChildren(parent);
if (children != null && children.size() == 1) {
Object childId = children.iterator().next();
if (isLoader(childId)) {
initialLoaderId = childId;
} else {
initialLoaderId = null;
}
} else {
initialLoaderId = null;
}
return initialLoaderId;
}
private void replaceLoaderWithChildren(Object loaderId, T parent, List<T> children) {
adaptee.removeItem(loaderId);
for (T child : children) {
addItem(child, parent);
}
}
private String getLoaderItemCaption(Object itemId) {
String loaderItemCaption;
Item loaderItem = adaptee.getItem(itemId);
int lowerLimit = (int) loaderItem.getItemProperty(LOADER_NEXT_LOADED_INDEX_ID).getValue();
int upperLimit;
int sameLevelNodeCount;
T parent = getParentForLoader(itemId);
if (parent != null) {
sameLevelNodeCount = dataProvider.getEstimatedChildrenNodesCount(parent);
} else {
sameLevelNodeCount = dataProvider.getEstimatedRootNodesCount();
}
if ((lowerLimit + bufferSize) >= sameLevelNodeCount) {
upperLimit = sameLevelNodeCount;
} else {
upperLimit = lowerLimit + bufferSize;
}
loaderItemCaption = getLoaderItemCaption(lowerLimit + 1, upperLimit);
return loaderItemCaption;
}
protected String getLoaderItemCaption(int lowerLimit, int upperLimit) {
String caption;
if (lowerLimit < upperLimit) {
caption = "[" + (lowerLimit) + "-" + upperLimit + "]...";
} else {
caption = "[" + lowerLimit + "]...";
}
return caption;
}
protected String getLoaderItemDescription(Object itemId) {
return null;
}
public String getItemCaption(T object) {
return null;
}
public void addAttachListener(AttachListener listener) {
adaptee.addAttachListener(listener);
}
public void addDetachListener(DetachListener listener) {
adaptee.addDetachListener(listener);
}
public void addExpandListener(ExpandListener listener) {
adaptee.addExpandListener(listener);
}
public void addCollapseListener(CollapseListener listener) {
adaptee.addCollapseListener(listener);
}
public void addItemClickListener(ItemClickListener listener) {
itemClickListeners.add(listener);
}
public void removeAttachListener(AttachListener listener) {
adaptee.removeAttachListener(listener);
}
public void removeDetachListener(DetachListener listener) {
adaptee.removeDetachListener(listener);
}
public void removeExtension(Extension extension) {
adaptee.removeExtension(extension);
}
public boolean addContainerProperty(Object propertyId, Class<?> type, Object defaultValue)
throws UnsupportedOperationException {
return adaptee.addContainerProperty(propertyId, type, defaultValue);
}
public boolean removeContainerProperty(Object propertyId)
throws UnsupportedOperationException {
return adaptee.removeContainerProperty(propertyId);
}
public void removeExpandListener(ExpandListener listener) {
adaptee.removeExpandListener(listener);
}
public void removeCollapseListener(CollapseListener listener) {
adaptee.removeCollapseListener(listener);
}
public void removeItemClickListener(ItemClickListener listener) {
itemClickListeners.remove(listener);
}
public void setItemIcon(Object itemId, Resource icon, String altText) {
adaptee.setItemIcon(itemId, icon, altText);
}
public void setItemIconAlternateText(Object itemId, String altText) {
adaptee.setItemIconAlternateText(itemId, altText);
}
public void setItemCaption(Object itemId, String caption) {
adaptee.setItemCaption(itemId, caption);
}
public void setItemCaptionMode(ItemCaptionMode mode) {
adaptee.setItemCaptionMode(mode);
}
public void setItemStyleGenerator(ItemStyleGenerator itemStyleGenerator) {
adaptee.setItemStyleGenerator(itemStyleGenerator);
}
public void setItemCaptionPropertyId(Object propertyId) {
adaptee.setItemCaptionPropertyId(propertyId);
}
public void setItemIconPropertyId(Object propertyId)
throws IllegalArgumentException {
adaptee.setItemIconPropertyId(propertyId);
}
public void setItemDescriptionGenerator(ItemDescriptionGenerator generator) {
adaptee.setItemDescriptionGenerator(generator);
}
public final String getItemIconAlternateText(Object itemId) {
return adaptee.getItemIconAlternateText(itemId);
}
public Resource getItemIcon(Object itemId) {
return null;
}
public final ItemCaptionMode getItemCaptionMode() {
return adaptee.getItemCaptionMode();
}
public final Object getItemCaptionPropertyId() {
return adaptee.getItemCaptionPropertyId();
}
public final Object getItemIconPropertyId() {
return adaptee.getItemIconPropertyId();
}
public final ItemDescriptionGenerator getItemDescriptionGenerator() {
return adaptee.getItemDescriptionGenerator();
}
public Collection<?> getContainerPropertyIds() {
return null;
}
public Property<?> getContainerProperty(Object itemId, Object propertyId) {
return null;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Override
public Class getType() {
return Object.class;
}
public final Tree getNestedTree() {
return adaptee;
}
public boolean isExpanded(Object itemId) {
return adaptee.isExpanded(itemId);
}
/**
* Will load the nodes corresponding to the item ids passed. Each item id corresponds to a parent
* except for the last one, which is the last node, which can be a leaf.
*
* The item ids are ordered from the root level to the leaf level. Omitting any level will prevent
* the tree from loading the desired node.
*
* Note: The lazy loading of a level will stop after 10 attempts. This is necessary to prevent
* performance issues.
*
* @param itemIds The item ids to load, from the root level to the leaf level.
* @return
*/
public boolean loadAndExpand(List<T> itemIds) {
boolean match = true;
T currentParentId = null;
for (T itemId : itemIds) {
Item itemFound = loadUntil(itemId, currentParentId);
if (itemFound == null) {
match = false;
break;
}
currentParentId = itemId;
}
return match;
}
public Item loadUntil(T itemId, T parentId) {
// Ensures that initContent() has been called
getContent();
Item match = adaptee.getItem(itemId);
if (match == null) {
loop1 : for (int i = 0; match == null && i < 10; i++) {
int start;
ObjectsResponse<T> extraObjectsResponse;
Object lastLoaderItemId = null;
if (parentId == null) {
Collection<?> rootItemIds = adaptee.rootItemIds();
start = rootItemIds.size();
if (start > 0) {
List<?> rootItemIdsList = new ArrayList<Object>(rootItemIds);
Object lastItemId = rootItemIdsList.get(start - 1);
if (isLoader(lastItemId)) {
lastLoaderItemId = lastItemId;
start--;
}
}
extraObjectsResponse = dataProvider.getRootObjects(start, bufferSize);
} else {
Item existingParentItem = adaptee.getItem(parentId);
if (existingParentItem != null) {
Collection<?> children = adaptee.getChildren(parentId);
if (children != null) {
start = children.size();
if (start == 1) {
Object firstItemId = children.iterator().next();
if (isLoader(firstItemId)) {
adaptee.removeItem(firstItemId);
}
}
if (start > 0) {
List<?> childrenList = new ArrayList<Object>(children);
Object lastItemId = childrenList.get(start - 1);
if (isLoader(lastItemId)) {
lastLoaderItemId = lastItemId;
start--;
}
}
} else {
start = 0;
}
extraObjectsResponse = dataProvider.getChildren(parentId, start, bufferSize);
} else {
break loop1;
}
}
int extraObjectsCount = extraObjectsResponse.getCount();
List<T> extraObjects = extraObjectsResponse.getObjects();
boolean isMoreItems = (start + extraObjects.size()) < extraObjectsCount;
if (!extraObjects.isEmpty()) {
if (lastLoaderItemId != null) {
adaptee.removeItem(lastLoaderItemId);
}
for (T extraObject : extraObjects) {
boolean matchingItemId = itemId.equals(extraObject);
addItem(extraObject, parentId);
if (matchingItemId) {
match = adaptee.getItem(extraObject);
}
}
if (!isMoreItems) {
break loop1;
} else {
int nextLoadedIndex = start + extraObjects.size();
addLoaderItem(parentId, nextLoadedIndex);
}
} else {
break loop1;
}
}
}
if (match != null) {
adaptee.expandItem(itemId);
adjustLoaderAfterExpand(itemId);
}
return match;
}
}