*/
package org.opendaylight.mdsal.dom.spi.query;
-import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Verify.verify;
import static java.util.Objects.requireNonNull;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.MoreObjects.ToStringHelper;
+import com.google.common.collect.AbstractIterator;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayDeque;
-import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
import org.opendaylight.mdsal.dom.api.query.DOMQuery;
import org.opendaylight.mdsal.dom.api.query.DOMQueryPredicate;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
-import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNodes;
@NonNullByDefault
-final class LazyDOMQueryResultIterator implements Iterator<Entry<YangInstanceIdentifier, NormalizedNode<?, ?>>> {
- // Current data item
- private final ArrayDeque<NormalizedNode<?, ?>> currentData = new ArrayDeque<>();
- // Absolute path from root of current data item
- private final ArrayDeque<PathArgument> currentPath;
- // Steps remaining in the select part of the query
- private final ArrayDeque<PathArgument> selectSteps;
- // The query which needs to be executed
- private final DOMQuery query;
+final class LazyDOMQueryResultIterator extends AbstractIterator<Entry<YangInstanceIdentifier, NormalizedNode<?, ?>>> {
+ private static class Frame {
+ final NormalizedNode<?, ?> data;
+ final @Nullable PathArgument select;
- // FIXME: MDSAL-610: this needs to be eliminated
- private final Iterator<Entry<YangInstanceIdentifier, NormalizedNode<?, ?>>> iter;
+ @SuppressFBWarnings(value = "NP_STORE_INTO_NONNULL_FIELD", justification = "Ungrokked @Nullable")
+ Frame(final NormalizedNode<?, ?> data) {
+ this.data = requireNonNull(data);
+ // The only case when this can be null: if this a top-level container, as ensured by the sole caller
+ select = null;
+ }
- LazyDOMQueryResultIterator(final DOMQuery query, final NormalizedNode<?, ?> queryRoot) {
- this.query = requireNonNull(query);
- currentPath = new ArrayDeque<>(query.getRoot().getPathArguments());
- selectSteps = new ArrayDeque<>(query.getSelect().getPathArguments());
- currentData.push(queryRoot);
-
- // FIXME: MDSAL-610: this is a recursive algo, filling provided list. Turn it around into a state mutator.
- final List<Entry<YangInstanceIdentifier, NormalizedNode<?,?>>> result = new ArrayList<>();
- evalPath(result);
- this.iter = result.iterator();
- currentPath.clear();
- selectSteps.clear();
+ Frame(final NormalizedNode<?, ?> data, final PathArgument selectArg) {
+ this.data = requireNonNull(data);
+ this.select = requireNonNull(selectArg);
+ }
+
+ // Bimorphic invocation here, MapFrame checks with its iterator.
+ boolean hasNext() {
+ return false;
+ }
+
+ @Override
+ public final String toString() {
+ return addToStringAttributes(MoreObjects.toStringHelper(this).omitNullValues()).toString();
+ }
+
+ protected ToStringHelper addToStringAttributes(final ToStringHelper helper) {
+ return helper.add("data", data.getIdentifier()).add("select", select);
+ }
}
- @Override
- public boolean hasNext() {
- return iter.hasNext();
+ private static final class MapFrame extends Frame {
+ final Iterator<MapEntryNode> iter;
+
+ MapFrame(final NormalizedNode<?, ?> data, final PathArgument selectArg, final Iterator<MapEntryNode> iter) {
+ super(data, selectArg);
+ this.iter = requireNonNull(iter);
+ }
+
+ @Override
+ boolean hasNext() {
+ return iter.hasNext();
+ }
+
+ @Override
+ protected ToStringHelper addToStringAttributes(final ToStringHelper helper) {
+ return super.addToStringAttributes(helper).add("hasNext", iter.hasNext());
+ }
+ }
+
+ // Steps remaining in the select part of the query. @Nullable helps with null analysis with Deque.poll()
+ private final ArrayDeque<@Nullable PathArgument> remainingSelect = new ArrayDeque<>();
+ // Absolute path from root of current data item
+ private final ArrayDeque<PathArgument> currentPath = new ArrayDeque<>();
+ // Work backlog, in terms of frames that need to be processed
+ private final ArrayDeque<Frame> frames = new ArrayDeque<>();
+ // The predicates which need to be evaluated
+ private final List<? extends DOMQueryPredicate> predicates;
+
+ LazyDOMQueryResultIterator(final DOMQuery query, final NormalizedNode<?, ?> queryRoot) {
+ // Note: DOMQueryEvaluator has taken care of the empty case, this is always non-empty
+ remainingSelect.addAll(query.getSelect().getPathArguments());
+ currentPath.addAll(query.getRoot().getPathArguments());
+ predicates = query.getPredicates();
+ frames.push(new Frame(queryRoot));
}
@Override
- public Entry<YangInstanceIdentifier, NormalizedNode<?, ?>> next() {
- return iter.next();
+ protected Entry<YangInstanceIdentifier, NormalizedNode<?, ?>> computeNext() {
+ final Entry<YangInstanceIdentifier, NormalizedNode<?, ?>> next = findNext();
+ return next != null ? next : endOfData();
}
- private void evalPath(final List<Entry<YangInstanceIdentifier, NormalizedNode<?,?>>> result) {
- final NormalizedNode<?, ?> data = currentData.pop();
- final PathArgument next = selectSteps.poll();
- if (next == null) {
- if (matches(data, query)) {
- result.add(new SimpleImmutableEntry<>(YangInstanceIdentifier.create(currentPath), data));
+ @SuppressFBWarnings(value = "NP_NONNULL_RETURN_VIOLATION", justification = "Ungrokked @Nullable")
+ private @Nullable Entry<YangInstanceIdentifier, NormalizedNode<?, ?>> findNext() {
+ // We always start with non-empty frames, as we signal end of data when we reach the end. We know this never
+ // null, by Eclipse insists. We do not care (that much) and use a poll() here.
+ // TODO: this is a huge method which could be restructured with hard tailcalls, alas we do not have those (yet?)
+ // Any such refactor better have some benchmarks to show non-regression.
+ Frame current = frames.poll();
+ while (current != null) {
+ final PathArgument next = remainingSelect.poll();
+ if (next == null) {
+ // We are matching this frame, and if we got here it must have a stashed iterator, as we deal with
+ // single match entries without using the stack. Look for first matching child and return it.
+ final Iterator<MapEntryNode> iter = ((MapFrame) current).iter;
+ while (iter.hasNext()) {
+ final MapEntryNode child = iter.next();
+ if (matches(child)) {
+ return pushAndReturn(current, child);
+ }
+ }
+
+ // Unwind this frame's state and select the next frame from the stack
+ current = unwind(current.select);
+ continue;
}
- return;
- }
- if (data instanceof MapNode && next instanceof NodeIdentifier) {
- checkArgument(data.getIdentifier().equals(next), "Unexpected step %s", next);
- for (MapEntryNode child : ((MapNode) data).getValue()) {
- evalChild(result, child);
+ // Alright, here we are looking for a child to select. This is where things get dicey, as there is a number
+ // of possibilities:
+
+ // 1. we are iterating a map. We are matching the next child against 'next', which can have a number of
+ // outcomes in and of itself.
+ if (current instanceof MapFrame) {
+ final Iterator<MapEntryNode> iter = ((MapFrame) current).iter;
+ if (remainingSelect.isEmpty()) {
+ // ... so all of 1) and this is the last-step map. In this case we want to find the next matching
+ // child without going to stack. We want to push next back, though, as we either need to resume
+ // from it (arriving back here), or will have dealt with it.
+ while (iter.hasNext()) {
+ final MapEntryNode child = iter.next();
+ if (matches(child)) {
+ remainingSelect.push(next);
+ return pushAndReturn(current, child);
+ }
+ }
+
+ // Unwind frames and retry
+ current = unwind(current, next);
+ continue;
+ }
+
+ // ... so all of 1) but this time this is an intermediate step. If we have a child, we'll push the map
+ // entry and set the child frame as current. Let the loop deal with the rest of the lookup.
+ if (iter.hasNext()) {
+ final MapEntryNode child = iter.next();
+ frames.push(current);
+ currentPath.addLast(child.getIdentifier());
+ current = new Frame(child, next);
+ continue;
+ }
+
+ // ... all of 1) but we do not have any more children to match. Discard this frame and move on.
+ current = unwind(current, next);
+ continue;
}
- } else {
- NormalizedNodes.getDirectChild(data, next).ifPresent(child -> evalChild(result, child));
+
+ // 2. we are at a normal container, where we need to resolve a child. This is also a bit involved, so now:
+ final Optional<NormalizedNode<?, ?>> optChild = NormalizedNodes.getDirectChild(current.data, next);
+ if (optChild.isEmpty()) {
+ // If we did not find the child, as we can have only a single match. Unwind to next possible match.
+ current = unwind(current, next);
+ continue;
+ }
+
+ // If we have a child see if this is the ultimate select step, if so, short circuit stack. We do not record
+ // ourselves.
+ final NormalizedNode<?, ?> child = optChild.orElseThrow();
+ if (remainingSelect.isEmpty()) {
+ // This is the ultimate step in lookup, process it without churning the stack by imposing a dedicated
+ // Frame. In either case we are done with this frame, unwinding it in both cases.
+ if (matches(child)) {
+ final YangInstanceIdentifier childPath = createIdentifier(child);
+ unwind(current, next);
+ return new SimpleImmutableEntry<>(childPath, child);
+ }
+
+ current = unwind(current, next);
+ continue;
+ }
+
+ // Push our state back, it's just a placeholder for 'currentSelect'. Current path points at us and so does
+ // the saved Frame.
+ currentPath.addLast(current.data.getIdentifier());
+ frames.push(current);
+
+ // Now decide what sort of entry to push. For maps we want to start an iterator already, so it gets
+ // picked up as a continuation.
+ current = child instanceof MapNode ? new MapFrame(child, next, ((MapNode) child).getValue().iterator())
+ : new Frame(child, next);
}
- selectSteps.push(next);
+
+ // All done, there be entries no more.
+ // Consistency check and clear leftover state
+ verify(frames.isEmpty());
+ remainingSelect.clear();
+ currentPath.clear();
+ return null;
}
- private void evalChild(final List<Entry<YangInstanceIdentifier, NormalizedNode<?,?>>> result,
- final NormalizedNode<?, ?> child) {
+ // Construct child path. This concatenates currentPath and child's identifier.
+ private YangInstanceIdentifier createIdentifier(final NormalizedNode<?, ?> child) {
currentPath.addLast(child.getIdentifier());
- currentData.push(child);
- evalPath(result);
+ final YangInstanceIdentifier ret = YangInstanceIdentifier.create(currentPath);
currentPath.removeLast();
+ return ret;
+ }
+
+ // Save a frame for further processing return its child as an item.
+ private Entry<YangInstanceIdentifier, NormalizedNode<?, ?>> pushAndReturn(final Frame frame,
+ final MapEntryNode child) {
+ final YangInstanceIdentifier childPath = createIdentifier(child);
+
+ // Push the frame back to work, return the result
+ frames.push(frame);
+ return new SimpleImmutableEntry<>(childPath, child);
+ }
+
+ /**
+ * Unwind the stack, discarding current frame, and possibly some others. The unwind starts with pushing {@code next}
+ * to {@link #remainingSelect}, hence we remember to handle it next time around. It then defers to
+ * {@link #unwind(PathArgument)}.
+ *
+ * @param current Current frame
+ * @param next Next path argument to lookup (after this frame)
+ * @return Next frame to process, null if there is no other work
+ */
+ private @Nullable Frame unwind(final Frame current, final PathArgument next) {
+ remainingSelect.push(next);
+ return unwind(current.select);
+ }
+
+ /**
+ * Unwind the stack, discarding current frame, and possibly some others. Unwind removes contents of
+ * {@link #currentPath}, walking back towards the query root.
+ *
+ * <p>
+ * Since we are unwinding a data item, we pop its path -- hence {@link #currentPath} points to the parent path.
+ * We then examine {@link Frame#select} to see if it's null -- if it is, we have reached the top-most frame and
+ * hence have nothing left to do.
+ *
+ * <p>
+ * Otherwise we remember {@code select} back to {@link #remainingSelect} and pop the next frame to be processed.
+ * If the frame does not have work, as indicated by {@link Frame#hasNext()}, we unwind it as well.
+ *
+ * <p>
+ * We repeat this process until we find a frame with some work or we run out of frames.
+ *
+ * @param current Current frame
+ * @param next Next path argument to lookup (after this frame)
+ * @return Next frame to process, null if there is no other work
+ */
+ @SuppressFBWarnings(value = "NP_NONNULL_RETURN_VIOLATION", justification = "Ungrokked @Nullable")
+ private @Nullable Frame unwind(final @Nullable PathArgument selectArg) {
+ @Nullable PathArgument select = selectArg;
+ while (true) {
+ currentPath.removeLast();
+ if (select == null) {
+ verify(frames.isEmpty());
+ return null;
+ }
+
+ remainingSelect.push(select);
+ // pop() for its state-checking properties. Last frame should have had select == null and we would have
+ // bailed there.
+ final Frame next = frames.pop();
+ if (next.hasNext()) {
+ return next;
+ }
+ select = next.select;
+ }
+ }
+
+ private boolean matches(final NormalizedNode<?, ?> data) {
+ return matches(data, predicates);
}
- static boolean matches(final NormalizedNode<?, ?> data, final DOMQuery query) {
- for (DOMQueryPredicate pred : query.getPredicates()) {
+ static boolean matches(final NormalizedNode<?, ?> data, final List<? extends DOMQueryPredicate> predicates) {
+ for (DOMQueryPredicate pred : predicates) {
// Okay, now we need to deal with predicates, but do it in a smart fashion, so we do not end up iterating
// all over the place. Typically we will be matching just a leaf.
final YangInstanceIdentifier path = pred.getPath();