// Copyright 2012 Google Inc. All Rights Reserved. // // 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.google.collide.client.util.collections; import com.google.collide.json.shared.JsonArray; import com.google.collide.shared.util.JsonCollections; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import java.util.Iterator; /* * This class uses iterators since they inherently save traversal state and * allow for removing the current item. (Less for work for this class to do.) */ /** * A helper to visit a tree of objects with features like pause/resume. * */ public class ResumableTreeTraverser<E> { /** * Listener that is notified when the traversal finishes. */ public interface FinishedCallback { void onTraversalFinished(); } /** * Provider for the nodes in the tree. */ public interface NodeProvider<E> { /** * Returns an iterator for the children of {@code node}, or null if * {@code node} doesn't have children. */ Iterator<E> getChildrenIterator(E node); } /** * A visitor that is called for each node traversed in the tree. */ public interface Visitor<E> { void visit(E item, VisitorApi visitorApi); } /** * An object that encapsulates various API available to a * {@link ResumableTreeTraverser.Visitor}. */ public static class VisitorApi { private ResumableTreeTraverser<?> treeTraverser; private boolean wasRemovedCalled; private VisitorApi(ResumableTreeTraverser<?> treeTraverser) { this.treeTraverser = treeTraverser; } /** * Pauses the tree traversal. */ public void pause() { treeTraverser.pause(); } /** * Resumes a paused tree traversal. */ public void resume() { treeTraverser.resume(); } /** * Removes the node currently being visited. */ public void remove() { if (!wasRemovedCalled) { treeTraverser.currentNodeIterator.remove(); wasRemovedCalled = true; } } private boolean resetRemoveState() { boolean previousWasRemovedCalled = wasRemovedCalled; wasRemovedCalled = false; return previousWasRemovedCalled; } } /** * If paused during a dispatch, this will be the dispatch state so we can * resume exactly where we left off. */ private static class DispatchPauseState<E> { Iterator<? extends Visitor<E>> dispatchIterator; E dispatchNode; DispatchPauseState(Iterator<? extends Visitor<E>> visitorIterator, E dispatchNode) { this.dispatchIterator = visitorIterator; this.dispatchNode = dispatchNode; } } private final NodeProvider<E> adapter; private final FinishedCallback finishedCallback; private final VisitorApi visitorApi; private final Iterable<? extends Visitor<E>> visitorIterable; private boolean isPaused; private DispatchPauseState<E> dispatchPauseState; private JsonArray<Iterator<E>> nodeIteratorStack = JsonCollections.createArray(); private Iterator<E> currentNodeIterator; private boolean isDispatching; public ResumableTreeTraverser(NodeProvider<E> adapter, Iterator<E> nodes, JsonArray<? extends Visitor<E>> visitors, FinishedCallback finishedCallback) { this.adapter = adapter; this.finishedCallback = finishedCallback; this.visitorIterable = visitors.asIterable(); this.visitorApi = new VisitorApi(this); nodeIteratorStack.add(nodes); isPaused = true; } @VisibleForTesting boolean hasMore() { return nodeIteratorStack.size() > 0; } /** * Initially starts the traversal or resumes a previously paused traversal. If this is called from * a {@link Visitor#visit(Object, VisitorApi)} dispatch, then the traversal will be resumed after * that visit method returns. If this is not called from a visit dispatch, then the traversal will * be resumed immediately synchronously. */ public void resume() { Preconditions.checkArgument(isPaused); isPaused = false; if (!isDispatching) { resumeTraversal(); } } private void resumeTraversal() { if (nodeIteratorStack.size() == 0) { // Nothing to do, exit early return; } while (!isPaused && (nodeIteratorStack.size() > 0 || dispatchPauseState != null)) { currentNodeIterator = nodeIteratorStack.isEmpty() ? null : nodeIteratorStack.peek(); while (!isPaused && ((currentNodeIterator != null && currentNodeIterator.hasNext()) || dispatchPauseState != null)) { E node; Iterator<? extends Visitor<E>> visitorIterator; if (dispatchPauseState == null) { node = currentNodeIterator.next(); visitorIterator = visitorIterable.iterator(); } else { node = dispatchPauseState.dispatchNode; visitorIterator = dispatchPauseState.dispatchIterator; dispatchPauseState = null; } dispatchToVisitors(visitorIterator, node, visitorApi); /* * A visitor could have paused at this point, if you're doing real work * below, make sure to have a !isPaused check */ if (!isPaused) { boolean wasNodeRemoved = visitorApi.resetRemoveState(); if (!wasNodeRemoved) { Iterator<E> childrenIterator = adapter.getChildrenIterator(node); if (childrenIterator != null) { nodeIteratorStack.add(childrenIterator); currentNodeIterator = childrenIterator; } } } } if (!isPaused) { if (!nodeIteratorStack.isEmpty()) { nodeIteratorStack.pop(); if (nodeIteratorStack.size() == 0 && finishedCallback != null) { finishedCallback.onTraversalFinished(); } } } } } public void pause() { Preconditions.checkArgument(!isPaused); isPaused = true; } private void dispatchToVisitors(Iterator<? extends Visitor<E>> visitorIterator, E node, VisitorApi visitorApi) { // !wasRemovedCalled will prevent subsequent visitors from being called while (!isPaused && visitorIterator.hasNext() && !visitorApi.wasRemovedCalled) { isDispatching = true; try { visitorIterator.next().visit(node, visitorApi); } finally { isDispatching = false; } } if (isPaused) { // The visitor paused dispatchPauseState = new DispatchPauseState<E>(visitorIterator, node); } } }