--- /dev/null
+/*
+ * Copyright (c) 2024 PANTHEON.tech, s.r.o. and others. 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
+ */
+package org.opendaylight.restconf.server.spi;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Iterators;
+import com.google.common.collect.Sets;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
+import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
+
+/**
+ * Default, standards-compliant mode. {@code leaf} children referenced in predicates are first, followed by others
+ * in iteration order.
+ */
+@NonNullByDefault
+final class DefaultMapBodyOrder extends MapBodyOrder {
+ static final DefaultMapBodyOrder INSTANCE = new DefaultMapBodyOrder();
+
+ private DefaultMapBodyOrder() {
+ // Hidden on purpose
+ }
+
+ @Override
+ Iterable<DataContainerChild> orderBody(final MapEntryNode entry) throws IOException {
+ // First things first: we will need to size our two collections...
+ final var keys = entry.name().keySet();
+ final var keySize = keys.size();
+ final var entrySize = entry.size();
+ final var otherSize = entrySize - keySize;
+
+ // ... and that allows us to establish some invariants and optimize based on them
+ if (otherSize > 0) {
+ return orderBody(entry, keys, keySize, otherSize);
+ } else if (otherSize == 0) {
+ return keySize == 1 ? orderKey(entry, keys.iterator().next()) : orderKeys(entry, keys, keySize);
+ } else {
+ throw new IOException(entry.name() + " requires " + keySize + ", have only " + entrySize);
+ }
+ }
+
+ private static Iterable<DataContainerChild> orderBody(final MapEntryNode entry, final Set<QName> qnames,
+ final int keySize, final int otherSize) throws IOException {
+ final var keys = new ArrayList<LeafNode<?>>(keySize);
+ final var others = new ArrayList<DataContainerChild>(otherSize);
+
+ // Single-pass over children, classifying them into two parts.
+ for (var child : entry.body()) {
+ if (child instanceof LeafNode<?> leaf && qnames.contains(qnameOf(leaf))) {
+ keys.add(leaf);
+ } else {
+ others.add(child);
+ }
+ }
+
+ // Check we have all the keys
+ if (keys.size() != keySize) {
+ throw new IOException("Missing leaf nodes for "
+ + Sets.difference(qnames, keys.stream().map(DefaultMapBodyOrder::qnameOf).collect(Collectors.toSet()))
+ + " in " + entry);
+ }
+
+ // Make sure key iteration order matches qnames, if not go through a sort
+ if (!Iterators.elementsEqual(qnames.iterator(),
+ Iterators.transform(keys.iterator(), DefaultMapBodyOrder::qnameOf))) {
+ sortKeys(keys, qnames);
+ }
+
+ return Iterables.concat(keys, others);
+ }
+
+ private static Iterable<DataContainerChild> orderKeys(final MapEntryNode entry, final Set<QName> qnames,
+ final int keySize) throws IOException {
+ // Every child is supposed to be a leaf, addressable via NodeIdentifier, just look each one up and be done with
+ // it.
+ final var keys = new ArrayList<DataContainerChild>(keySize);
+ for (var qname : qnames) {
+ keys.add(requireKeyLeaf(entry, qname));
+ }
+ return keys;
+ }
+
+ private static Collection<DataContainerChild> orderKey(final MapEntryNode entry, final QName key)
+ throws IOException {
+ requireKeyLeaf(entry, key);
+ return entry.body();
+ }
+
+ private static LeafNode<?> requireKeyLeaf(final MapEntryNode entry, final QName key) throws IOException {
+ final var child = entry.childByArg(new NodeIdentifier(key));
+ if (child instanceof LeafNode<?> leaf) {
+ return leaf;
+ } else if (child == null) {
+ throw new IOException("No leaf for " + key + " in " + entry.prettyTree());
+ } else {
+ throw new IOException("Child " + child + " is not a leaf");
+ }
+ }
+
+ private static void sortKeys(final ArrayList<LeafNode<?>> keys, final Set<QName> qnames) {
+ throw new UnsupportedOperationException();
+ }
+
+ private static QName qnameOf(final NormalizedNode node) {
+ return node.name().getNodeType();
+ }
+}
*/
package org.opendaylight.restconf.server.spi;
-import com.google.common.collect.Iterables;
import java.io.IOException;
import java.util.List;
-import java.util.Map.Entry;
import java.util.Set;
import javax.xml.transform.dom.DOMSource;
import org.opendaylight.restconf.api.query.DepthParam;
import org.opendaylight.yangtools.yang.common.QName;
-import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
import org.opendaylight.yangtools.yang.data.api.schema.AnydataNode;
import org.opendaylight.yangtools.yang.data.api.schema.AnyxmlNode;
import org.opendaylight.yangtools.yang.data.api.schema.ChoiceNode;
import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListEntryNode;
import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListNode;
import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
* This is an experimental iterator over a {@link NormalizedNode}. This is essentially
* us to write multiple nodes.
*/
// FIXME: this is a copy&paste from yangtools' NormalizedNodeWriter then adapted for filtering
-sealed class DefaultNormalizedNodeWriter extends NormalizedNodeWriter {
+final class DefaultNormalizedNodeWriter extends NormalizedNodeWriter {
private static final QName ROOT_DATA_QNAME = QName.create("urn:ietf:params:xml:ns:netconf:base:1.0", "data");
+ private final MapBodyOrder mapBodyOrder;
private final Integer maxDepth;
private final List<Set<QName>> fields;
- int currentDepth = 0;
+ private int currentDepth = 0;
- DefaultNormalizedNodeWriter(final NormalizedNodeStreamWriter writer, final DepthParam depth,
- final List<Set<QName>> fields) {
+ DefaultNormalizedNodeWriter(final NormalizedNodeStreamWriter writer, final boolean iterationOrder,
+ final DepthParam depth, final List<Set<QName>> fields) {
super(writer);
+ mapBodyOrder = iterationOrder ? IterationMapBodyOrder.INSTANCE : DefaultMapBodyOrder.INSTANCE;
maxDepth = depth == null ? null : depth.value();
this.fields = fields;
}
@Override
protected void writeMapEntry(final MapEntryNode node) throws IOException {
writer.startMapEntryNode(node.name(), node.size());
- currentDepth++;
- writeMapEntryChildren(node);
- currentDepth--;
+ if (selectedByParameters(node, false)) {
+ currentDepth++;
+ writeChildren(mapBodyOrder.orderBody(node), false);
+ currentDepth--;
+ } else {
+ writer.endNode();
+ }
}
@Override
* @param mixinParent {@code true} if parent is mixin, {@code false} otherwise
* @return {@code true} if node will be written, {@code false} otherwise
*/
- protected boolean selectedByParameters(final NormalizedNode node, final boolean mixinParent) {
+ private boolean selectedByParameters(final NormalizedNode node, final boolean mixinParent) {
// nodes to be written are not limited by fields, only by depth
if (fields == null) {
return maxDepth == null || currentDepth < maxDepth;
* @param mixinParent {@code true} if parent is mixin, {@code false} otherwise
* @throws IOException when the writer reports it
*/
- protected final void writeChildren(final Iterable<? extends NormalizedNode> children, final boolean mixinParent)
+ private void writeChildren(final Iterable<? extends NormalizedNode> children, final boolean mixinParent)
throws IOException {
for (var child : children) {
if (selectedByParameters(child, mixinParent)) {
}
writer.endNode();
}
-
- private void writeMapEntryChildren(final MapEntryNode mapEntryNode) throws IOException {
- if (selectedByParameters(mapEntryNode, false)) {
- writeChildren(mapEntryNode.body(), false);
- } else if (fields == null && maxDepth != null && currentDepth == maxDepth) {
- writeOnlyKeys(mapEntryNode.name().entrySet());
- }
- }
-
- private void writeOnlyKeys(final Set<Entry<QName, Object>> entries) throws IOException {
- for (var entry : entries) {
- writer.startLeafNode(new NodeIdentifier(entry.getKey()));
- writer.scalarValue(entry.getValue());
- writer.endNode();
- }
- writer.endNode();
- }
-
- static final class OrderedRestconfNormalizedNodeWriter extends DefaultNormalizedNodeWriter {
- private static final Logger LOG = LoggerFactory.getLogger(OrderedRestconfNormalizedNodeWriter.class);
-
- OrderedRestconfNormalizedNodeWriter(final NormalizedNodeStreamWriter writer, final DepthParam depth,
- final List<Set<QName>> fields) {
- super(writer, depth, fields);
- }
-
- @Override
- protected void writeMapEntry(final MapEntryNode node) throws IOException {
- writer.startMapEntryNode(node.name(), node.size());
-
- final var qnames = node.name().keySet();
- // Write out all the key children
- currentDepth++;
- for (var qname : qnames) {
- final var child = node.childByArg(new NodeIdentifier(qname));
- if (child != null) {
- if (selectedByParameters(child, false)) {
- write(child);
- }
- } else {
- LOG.info("No child for key element {} found", qname);
- }
- }
-
- // Write all the rest
- writeChildren(Iterables.filter(node.body(), input -> {
- if (!qnames.contains(input.name().getNodeType())) {
- return true;
- }
-
- LOG.debug("Skipping key child {}", input);
- return false;
- }), false);
- currentDepth--;
- }
- }
}
--- /dev/null
+/*
+ * Copyright (c) 2024 PANTHEON.tech, s.r.o. and others. 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
+ */
+package org.opendaylight.restconf.server.spi;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
+import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
+
+@NonNullByDefault
+final class IterationMapBodyOrder extends MapBodyOrder {
+ static final IterationMapBodyOrder INSTANCE = new IterationMapBodyOrder();
+
+ private IterationMapBodyOrder() {
+ // Hidden on purpose
+ }
+
+ @Override
+ Iterable<DataContainerChild> orderBody(final MapEntryNode entry) {
+ return entry.body();
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (c) 2024 PANTHEON.tech, s.r.o. and others. 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
+ */
+package org.opendaylight.restconf.server.spi;
+
+import java.io.IOException;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
+import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
+import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
+
+/**
+ * The order in which {@link MapEntryNode}s of a particular {@link MapNode} should be emitted in. This is a sealed
+ * internal implementation detail.
+ *
+ * @apiNote This contract exposes two singleton implementations, which we could model via an enum. We went the way
+ * of a sealed abstract for two reasons:
+ * - we do not want to expose orderChildren
+ * - we actually do not want to load IterationMapBodyOrder unless requested, so that the JVM does not have
+ * to worry about it unless requested
+ * -
+ */
+@NonNullByDefault
+abstract sealed class MapBodyOrder permits DefaultMapBodyOrder, IterationMapBodyOrder {
+ /**
+ * Order the body of specified node.
+ *
+ * @param entry map entry
+ * @return The body in the order it should be written out
+ */
+ abstract Iterable<DataContainerChild> orderBody(MapEntryNode entry) throws IOException;
+}
\ No newline at end of file
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.opendaylight.restconf.api.query.DepthParam;
-import org.opendaylight.restconf.server.spi.DefaultNormalizedNodeWriter.OrderedRestconfNormalizedNodeWriter;
import org.opendaylight.yangtools.yang.common.QName;
import org.opendaylight.yangtools.yang.data.api.schema.AnydataNode;
import org.opendaylight.yangtools.yang.data.api.schema.AnyxmlNode;
*/
public static final NormalizedNodeWriter forStreamWriter(final NormalizedNodeStreamWriter writer,
final boolean orderKeyLeaves, final @Nullable DepthParam depth, final @Nullable List<Set<QName>> fields) {
- return orderKeyLeaves ? new OrderedRestconfNormalizedNodeWriter(writer, depth, fields)
- : new DefaultNormalizedNodeWriter(writer, depth, fields);
+ return new DefaultNormalizedNodeWriter(writer, !orderKeyLeaves, depth, fields);
}
@Override
final var inOrder = inOrder(writer);
inOrder.verify(writer).startMapEntryNode(mapEntryNodeIdentifier, 2);
- // write only the key
- inOrder.verify(writer).startLeafNode(KEY_FIELD_NID);
- inOrder.verify(writer).scalarValue(keyLeafNodeValue);
- inOrder.verify(writer, times(2)).endNode();
+ inOrder.verify(writer).endNode();
}
/**