/*******************************************************************************
* Copyright (c) 2012-2017 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.ide.ui.smartTree;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.gwt.event.shared.EventHandler;
import com.google.gwt.event.shared.GwtEvent;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.event.shared.SimpleEventBus;
import org.eclipse.che.api.promises.client.Function;
import org.eclipse.che.api.promises.client.FunctionException;
import org.eclipse.che.api.promises.client.Operation;
import org.eclipse.che.api.promises.client.OperationException;
import org.eclipse.che.api.promises.client.Promise;
import org.eclipse.che.api.promises.client.PromiseError;
import org.eclipse.che.api.promises.client.js.Promises;
import org.eclipse.che.commons.annotation.Nullable;
import org.eclipse.che.ide.api.data.tree.Node;
import org.eclipse.che.ide.api.data.tree.NodeInterceptor;
import org.eclipse.che.ide.ui.smartTree.event.BeforeLoadEvent;
import org.eclipse.che.ide.ui.smartTree.event.CancellableEvent;
import org.eclipse.che.ide.ui.smartTree.event.LoadEvent;
import org.eclipse.che.ide.ui.smartTree.event.LoadExceptionEvent;
import org.eclipse.che.ide.ui.smartTree.event.LoaderHandler;
import org.eclipse.che.ide.ui.smartTree.event.PostLoadEvent;
import org.eclipse.che.ide.ui.smartTree.handler.GroupingHandlerRegistration;
import org.eclipse.che.ide.util.loging.Log;
import javax.validation.constraints.NotNull;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Class that perform loading node children. May transform nodes if ones passed set of node interceptors.
*
* @author Vlad Zhukovskiy
* @see NodeInterceptor
*/
public class NodeLoader implements LoaderHandler.HasLoaderHandlers {
/**
* Temporary storage for current requested nodes. When children have been loaded requested node removes from temporary set.
*/
Map<Node, Boolean> childRequested = new HashMap<>();
/**
* Last processed node. Maybe used in general purpose.
*/
private Node lastRequest;
/**
* Set of node interceptors. They need to modify children nodes before they will be set into parent node.
*
* @see NodeInterceptor
*/
private Set<NodeInterceptor> nodeInterceptors;
private final Comparator<NodeInterceptor> priorityComparator = new Comparator<NodeInterceptor>() {
@Override
public int compare(NodeInterceptor o1, NodeInterceptor o2) {
return o1.getPriority() - o2.getPriority();
}
};
/**
* When caching is on nodes will be loaded from cache if they exist otherwise nodes will be loaded every time forcibly.
*/
private boolean useCaching = false;
private Tree tree;
private GroupingHandlerRegistration handlerRegistration;
private CTreeNodeLoaderHandler cTreeNodeLoaderHandler = new CTreeNodeLoaderHandler();
/**
* Event handler for the loading events.
*/
private class CTreeNodeLoaderHandler implements LoadEvent.LoadHandler,
LoadExceptionEvent.LoadExceptionHandler,
BeforeLoadEvent.BeforeLoadHandler {
@Override
public void onLoad(final LoadEvent event) {
Node parent = event.getRequestedNode();
tree.getView().onLoadChange(tree.getNodeDescriptor(parent), false);
//remove joint element if non-leaf node doesn't have any children
if (!parent.isLeaf() && event.getReceivedNodes().isEmpty()) {
tree.getView().onJointChange(tree.getNodeDescriptor(parent), Tree.Joint.NONE);
}
NodeDescriptor requested = tree.getNodeDescriptor(parent);
if (requested == null) {
//smth happened, that requested node isn't registered in storage
Log.error(this.getClass(), "Requested node not found.");
return;
}
requested.setLoading(false);
//search node which has been removed from server to remove them from the tree
List<NodeDescriptor> removedNodes = findRemovedNodes(requested, event.getReceivedNodes());
//now search new nodes to add then into the tree
List<Node> newNodes = findNewNodes(requested, event.getReceivedNodes());
if (removedNodes.isEmpty() && newNodes.equals(event.getReceivedNodes())) {
tree.getNodeStorage().replaceChildren(parent, newNodes);
} else {
for (NodeDescriptor removed : removedNodes) {
if (!tree.getNodeStorage().remove(removed.getNode())) {
Log.info(this.getClass(), "Failed to remove node: " + removed.getNode().getName());
}
}
for (Node newNode : newNodes) {
tree.getNodeStorage().add(parent, newNode);
}
}
//Iterate on nested descendants to make additional load request
if (childRequested.remove(parent)) {
for (Node node : tree.getNodeStorage().getChildren(parent)) {
if (tree.isExpanded(node)) {
loadChildren(node, true);
}
}
}
fireEvent(new PostLoadEvent(event.getRequestedNode(), event.getReceivedNodes()));
}
@Override
public void onLoadException(LoadExceptionEvent event) {
final Node node = event.getRequestedNode();
checkNotNull(node, "Null node occurred");
final NodeDescriptor requested = tree.getNodeDescriptor(node);
if (requested == null) {
return;
}
tree.getView().onLoadChange(requested, false);
requested.setLoading(false);
}
@Override
public void onBeforeLoad(BeforeLoadEvent event) {
NodeDescriptor requested = tree.getNodeDescriptor(event.getRequestedNode());
if (requested == null) {
return;
}
requested.setLoading(true);
}
}
private List<Node> findNewNodes(NodeDescriptor parent, final List<Node> loadedChildren) {
final List<NodeDescriptor> existed = parent.getChildren();
if (existed == null || existed.isEmpty()) {
return loadedChildren;
}
Iterable<Node> newItems = Iterables.filter(loadedChildren, new Predicate<Node>() {
@Override
public boolean apply(Node loadedChild) {
for (NodeDescriptor nodeDescriptor : existed) {
if (nodeDescriptor.getNode().equals(loadedChild)) {
return false;
}
}
return true;
}
});
return Lists.newArrayList(newItems);
}
private List<NodeDescriptor> findRemovedNodes(NodeDescriptor parent, final List<Node> loadedChildren) {
List<NodeDescriptor> existed = parent.getChildren();
if (existed == null || existed.isEmpty()) {
return Collections.emptyList();
}
Iterable<NodeDescriptor> removedItems = Iterables.filter(existed, new Predicate<NodeDescriptor>() {
@Override
public boolean apply(NodeDescriptor existedChild) {
boolean found = false;
for (Node loadedChild : loadedChildren) {
if (existedChild.getNode().equals(loadedChild)) {
found = true;
}
}
return !found;
}
});
return Lists.newArrayList(removedItems);
}
private SimpleEventBus eventBus;
/**
* Creates a new tree node value provider instance.
*/
public NodeLoader() {
this(null);
}
/**
* Creates a new tree node value provider instance.
*
* @param nodeInterceptors
* set of {@link NodeInterceptor}
*/
public NodeLoader(@Nullable Set<NodeInterceptor> nodeInterceptors) {
this.nodeInterceptors = new HashSet<>();
if (nodeInterceptors != null) {
this.nodeInterceptors.addAll(nodeInterceptors);
}
}
/**
* Checks whether node has children or not. This method may allow tree to determine
* whether to show expand control near non-leaf node.
*
* @param parent
* node
* @return true if node has children, otherwise false
*/
public boolean mayHaveChildren(@NotNull Node parent) {
return !parent.isLeaf();
}
/**
* Check if node has children.
* This method make a request to server to read children count.
*
* @param node
* node
* @return true if node has children, otherwise false
*/
public Promise<Boolean> hasChildren(@NotNull Node node) {
return node.getChildren(false).thenPromise(new Function<List<Node>, Promise<Boolean>>() {
@Override
public Promise<Boolean> apply(List<Node> children) throws FunctionException {
return Promises.resolve(!children.isEmpty());
}
});
}
/**
* Initiates a load request for the parent's children.
*
* @param parent
* parent node
* @return true if the load was requested, otherwise false
*/
public boolean loadChildren(@NotNull Node parent) {
return loadChildren(parent, false);
}
public boolean loadChildren(Node parent, boolean reloadExpandedChild) {
//we don't need to load children for leaf nodes
if (parent.isLeaf()) {
return false;
}
if (childRequested.containsKey(parent)) {
return false;
}
childRequested.put(parent, reloadExpandedChild);
return doLoad(parent);
}
/**
* Called when children haven't been successfully loaded.
* Also fire {@link org.eclipse.che.ide.ui.smartTree.event.LoadExceptionEvent} event.
*
* @param parent
* parent node, children which haven't been loaded
* @return instance of {@link org.eclipse.che.api.promises.client.Operation} which contains promise with error
*/
@NotNull
private Operation<PromiseError> onLoadFailure(@NotNull final Node parent) {
return new Operation<PromiseError>() {
@Override
public void apply(PromiseError t) throws OperationException {
childRequested.remove(parent);
fireEvent(new LoadExceptionEvent(parent, t.getCause()));
}
};
}
/**
* Called when children have been successfully received.
* Also fire {@link org.eclipse.che.ide.ui.smartTree.event.LoadEvent} event.
*
* @param parent
* parent node, children which have been loaded
*/
private void onLoadSuccess(@NotNull final Node parent, List<Node> children) {
fireEvent(new LoadEvent(parent, children));
}
/**
* Initiates a load request for the parent's children.
* Also fire {@link org.eclipse.che.ide.ui.smartTree.event.BeforeLoadEvent} event.
*
* @param parent
* parent node
* @return true if load was requested, otherwise false
*/
private boolean doLoad(@NotNull final Node parent) {
if (fireEvent(new BeforeLoadEvent(parent))) {
lastRequest = parent;
parent.getChildren(!useCaching)
.then(interceptChildren(parent))
.catchError(onLoadFailure(parent));
return true;
}
return false;
}
/**
* Fires the given event.
*
* @param event
* event to fire
* @return true if the specified event wasn't cancelled, otherwise false
*/
private boolean fireEvent(@NotNull GwtEvent<?> event) {
if (eventBus != null) {
eventBus.fireEvent(event);
}
if (event instanceof CancellableEvent) {
return !((CancellableEvent)event).isCancelled();
}
return true;
}
/**
* Returns the last processed node.
*
* @return last processed node
*/
@Nullable
public Node getLastRequest() {
return lastRequest;
}
/**
* Perform iteration on every node interceptor, passing to ones the list of children
* to filter them before inserting into parent node.
*
* @param parent
* parent node
* @return instance of {@link org.eclipse.che.api.promises.client.Function} with promise that contains list of intercepted children
*/
@NotNull
private Operation<List<Node>> interceptChildren(@NotNull final Node parent) {
return new Operation<List<Node>>() {
@Override
public void apply(List<Node> children) throws OperationException {
if (nodeInterceptors.isEmpty()) {
onLoadSuccess(parent, children);
}
LinkedList<NodeInterceptor> sortedByPriorityQueue = new LinkedList<>(nodeInterceptors);
Collections.sort(sortedByPriorityQueue, priorityComparator);
iterate(sortedByPriorityQueue, parent, children);
}
};
}
private void iterate(final LinkedList<NodeInterceptor> deque, final Node parent, final List<Node> children) {
if (deque.isEmpty()) {
for (Node child : children) {
child.setParent(parent);
}
onLoadSuccess(parent, children);
return;
}
NodeInterceptor interceptor = deque.poll();
interceptor.intercept(parent, children).then(new Operation<List<Node>>() {
@Override
public void apply(List<Node> arg) throws OperationException {
iterate(deque, parent, arg);
}
});
}
/** {@inheritDoc} */
@NotNull
@Override
public HandlerRegistration addBeforeLoadHandler(@NotNull BeforeLoadEvent.BeforeLoadHandler handler) {
return addHandler(BeforeLoadEvent.getType(), handler);
}
/** {@inheritDoc} */
@NotNull
@Override
public HandlerRegistration addLoadExceptionHandler(@NotNull LoadExceptionEvent.LoadExceptionHandler handler) {
return addHandler(LoadExceptionEvent.getType(), handler);
}
@Override
public HandlerRegistration addPostLoadHandler(PostLoadEvent.PostLoadHandler handler) {
return addHandler(PostLoadEvent.getType(), handler);
}
/** {@inheritDoc} */
@Override
public HandlerRegistration addLoaderHandler(LoaderHandler handler) {
GroupingHandlerRegistration group = new GroupingHandlerRegistration();
group.add(addHandler(BeforeLoadEvent.getType(), handler));
group.add(addHandler(LoadEvent.getType(), handler));
group.add(addHandler(LoadExceptionEvent.getType(), handler));
group.add(addHandler(PostLoadEvent.getType(), handler));
return group;
}
/** {@inheritDoc} */
@Override
public HandlerRegistration addLoadHandler(LoadEvent.LoadHandler handler) {
return addHandler(LoadEvent.getType(), handler);
}
@NotNull
protected <H extends EventHandler> HandlerRegistration addHandler(@NotNull GwtEvent.Type<H> type, @NotNull H handler) {
if (eventBus == null) {
eventBus = new SimpleEventBus();
}
return eventBus.addHandler(type, handler);
}
/**
* Indicates that node value provider uses caching. It means that if node already has
* children they will be returned to the tree, otherwise children nodes will be forcibly
* loaded from the server.
*
* @return true if value provider uses caching, otherwise false
*/
public boolean isUseCaching() {
return useCaching;
}
/**
* Set cache using.
*
* @param useCaching
* true if value provider should use caching, otherwise false
*/
public void setUseCaching(boolean useCaching) {
this.useCaching = useCaching;
}
/**
* Return set of node interceptors.
*
* @return node interceptors list
*/
public Set<NodeInterceptor> getNodeInterceptors() {
return nodeInterceptors;
}
/**
* Binds tree to current node loader.
*
* @param tree
* tree instance
*/
public void bindTree(Tree tree) {
if (this.tree != null) {
handlerRegistration.removeHandler();
}
this.tree = tree;
if (tree != null) {
if (handlerRegistration == null) {
handlerRegistration = new GroupingHandlerRegistration();
}
handlerRegistration.add(addBeforeLoadHandler(cTreeNodeLoaderHandler));
handlerRegistration.add(addLoadHandler(cTreeNodeLoaderHandler));
handlerRegistration.add(addLoadExceptionHandler(cTreeNodeLoaderHandler));
}
}
public boolean isBusy() {
return !childRequested.isEmpty();
}
}