/******************************************************************************* * 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(); } }