Do not instantiate NormalizedNodes for filter 26/75526/3
authorRobert Varga <robert.varga@pantheon.tech>
Mon, 27 Aug 2018 10:43:27 +0000 (12:43 +0200)
committerRobert Varga <nite@hq.sk>
Fri, 31 Aug 2018 09:26:16 +0000 (09:26 +0000)
Using ImmutableNodes.fromInstanceId() does not work for leaf nodes,
as we do not have a value, which violates LeafNode contract -- leading
to an IllegalStateException when a user attempts to read() a single
leaf via a transaction.

As it turns out, this is completely unnecessary, as all we are doing
is iterating through the temporary NormalizedNode and creating
NormalizedNodeStreamWriter events based on that.

This patch replicates ImmutableNodes.fromInstanceId() logic, but
simplifies it for this particular task, short-cutting NormalizedNode
creation and emitting events directly as we are iterating over
the YangInstanceIdentifier.

JIRA: NETCONF-563
Change-Id: I0781e81d812ee979deaafb9dfa811e143866f4af
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
(cherry picked from commit 1c545b9883f64305983a9c0ef73acdaa97866cc7)

netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/NetconfUtil.java
netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/StreamingContext.java [new file with mode: 0644]
netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfMessageTransformUtil.java
netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/schema/mapping/NetconfMessageTransformerTest.java

index 46bb6cbcd323b10401607814b3057eee4af78743..eb3613534a15bd31a411b670ed9a1e063a2ccd83 100644 (file)
@@ -9,6 +9,7 @@ package org.opendaylight.netconf.util;
 
 import com.google.common.base.Preconditions;
 import java.io.IOException;
+import java.util.Iterator;
 import javax.xml.stream.XMLOutputFactory;
 import javax.xml.stream.XMLStreamException;
 import javax.xml.stream.XMLStreamWriter;
@@ -17,6 +18,8 @@ import org.opendaylight.netconf.api.DocumentedException;
 import org.opendaylight.netconf.api.xml.XmlElement;
 import org.opendaylight.netconf.api.xml.XmlNetconfConstants;
 import org.opendaylight.netconf.api.xml.XmlUtil;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
@@ -74,4 +77,25 @@ public final class NetconfUtil {
             }
         }
     }
+
+    public static void writeFilter(final YangInstanceIdentifier query, final DOMResult result,
+            final SchemaPath schemaPath, final SchemaContext context) throws IOException, XMLStreamException {
+        if (query.isEmpty()) {
+            // No query at all
+            return;
+        }
+
+        final XMLStreamWriter xmlWriter = XML_FACTORY.createXMLStreamWriter(result);
+        try {
+            try (NormalizedNodeStreamWriter writer =
+                    XMLStreamNormalizedNodeStreamWriter.create(xmlWriter, context, schemaPath)) {
+                final Iterator<PathArgument> it = query.getPathArguments().iterator();
+                final PathArgument first = it.next();
+                StreamingContext.fromSchemaAndQNameChecked(context, first.getNodeType()).streamToWriter(writer, first,
+                    it);
+            }
+        } finally {
+            xmlWriter.close();
+        }
+    }
 }
diff --git a/netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/StreamingContext.java b/netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/StreamingContext.java
new file mode 100644 (file)
index 0000000..e139360
--- /dev/null
@@ -0,0 +1,464 @@
+/*
+ * Copyright (c) 2018 Pantheon Technologies, 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.netconf.util;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter.UNKNOWN_SIZE;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
+import org.opendaylight.yangtools.concepts.Identifiable;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
+import org.opendaylight.yangtools.yang.model.api.AnyXmlSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.AugmentationSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.AugmentationTarget;
+import org.opendaylight.yangtools.yang.model.api.CaseSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
+import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
+import org.opendaylight.yangtools.yang.model.util.EffectiveAugmentationSchema;
+
+abstract class StreamingContext<T extends PathArgument> implements Identifiable<T> {
+    private final T identifier;
+
+    StreamingContext(final T identifier) {
+        this.identifier = identifier;
+    }
+
+    static StreamingContext<?> fromSchemaAndQNameChecked(final DataNodeContainer schema, final QName child) {
+        final Optional<DataSchemaNode> potential = findChildSchemaNode(schema, child);
+        checkArgument(potential.isPresent(),
+                "Supplied QName %s is not valid according to schema %s, potential children nodes: %s", child, schema,
+                schema.getChildNodes());
+
+        final DataSchemaNode result = potential.get();
+        // We try to look up if this node was added by augmentation
+        if (schema instanceof DataSchemaNode && result.isAugmenting()) {
+            for (final AugmentationSchemaNode aug : ((AugmentationTarget)schema).getAvailableAugmentations()) {
+                final DataSchemaNode found = aug.getDataChildByName(result.getQName());
+                if (found != null) {
+                    return new Augmentation(aug, schema);
+                }
+            }
+        }
+        return fromDataSchemaNode(result);
+    }
+
+    static StreamingContext<?> fromDataSchemaNode(final DataSchemaNode potential) {
+        if (potential instanceof ContainerSchemaNode) {
+            return new Container((ContainerSchemaNode) potential);
+        } else if (potential instanceof ListSchemaNode) {
+            return fromListSchemaNode((ListSchemaNode) potential);
+        } else if (potential instanceof LeafSchemaNode) {
+            return new Leaf((LeafSchemaNode) potential);
+        } else if (potential instanceof ChoiceSchemaNode) {
+            return new Choice((ChoiceSchemaNode) potential);
+        } else if (potential instanceof LeafListSchemaNode) {
+            return fromLeafListSchemaNode((LeafListSchemaNode) potential);
+        } else if (potential instanceof AnyXmlSchemaNode) {
+            return new AnyXml((AnyXmlSchemaNode) potential);
+        }
+        return null;
+    }
+
+    @Override
+    public final T getIdentifier() {
+        return identifier;
+    }
+
+    abstract StreamingContext<?> getChild(PathArgument child);
+
+    abstract void streamToWriter(NormalizedNodeStreamWriter writer, PathArgument first, Iterator<PathArgument> others)
+            throws IOException;
+
+    abstract boolean isMixin();
+
+    private static Optional<DataSchemaNode> findChildSchemaNode(final DataNodeContainer parent, final QName child) {
+        DataSchemaNode potential = parent.getDataChildByName(child);
+        if (potential == null) {
+            potential = findChoice(Iterables.filter(parent.getChildNodes(), ChoiceSchemaNode.class), child);
+        }
+        return Optional.ofNullable(potential);
+    }
+
+    private static ChoiceSchemaNode findChoice(final Iterable<ChoiceSchemaNode> choices, final QName child) {
+        for (final ChoiceSchemaNode choice : choices) {
+            for (final CaseSchemaNode caze : choice.getCases().values()) {
+                if (findChildSchemaNode(caze, child).isPresent()) {
+                    return choice;
+                }
+            }
+        }
+        return null;
+    }
+
+    private static StreamingContext<?> fromListSchemaNode(final ListSchemaNode potential) {
+        final List<QName> keyDefinition = potential.getKeyDefinition();
+        if (keyDefinition == null || keyDefinition.isEmpty()) {
+            return new UnkeyedListMixin(potential);
+        }
+        return potential.isUserOrdered() ? new OrderedMapMixin(potential)
+                : new UnorderedMapMixin(potential);
+    }
+
+    private static StreamingContext<?> fromLeafListSchemaNode(final LeafListSchemaNode potential) {
+        return potential.isUserOrdered() ? new OrderedLeafListMixin(potential)
+                : new UnorderedLeafListMixin(potential);
+    }
+
+    private abstract static class AbstractComposite<T extends PathArgument> extends StreamingContext<T> {
+        AbstractComposite(final T identifier) {
+            super(identifier);
+        }
+
+        @Override
+        final void streamToWriter(final NormalizedNodeStreamWriter writer, final PathArgument first,
+                final Iterator<PathArgument> others) throws IOException {
+            if (!isMixin()) {
+                final QName type = getIdentifier().getNodeType();
+                if (type != null) {
+                    final QName firstType = first.getNodeType();
+                    checkArgument(type.equals(firstType), "Node QName must be %s was %s", type, firstType);
+                }
+            }
+
+            emitElementStart(writer, first);
+            if (others.hasNext()) {
+                final PathArgument childPath = others.next();
+                final StreamingContext<?> childOp = getChildOperation(childPath);
+                childOp.streamToWriter(writer, childPath, others);
+            }
+            writer.endNode();
+        }
+
+        abstract void emitElementStart(NormalizedNodeStreamWriter writer, PathArgument arg) throws IOException;
+
+        @SuppressWarnings("checkstyle:illegalCatch")
+        private StreamingContext<?> getChildOperation(final PathArgument childPath) {
+            final StreamingContext<?> childOp;
+            try {
+                childOp = getChild(childPath);
+            } catch (final RuntimeException e) {
+                throw new IllegalArgumentException(String.format("Failed to process child node %s", childPath), e);
+            }
+            checkArgument(childOp != null, "Node %s is not allowed inside %s", childPath, getIdentifier());
+            return childOp;
+        }
+    }
+
+    private abstract static class AbstractDataContainer<T extends PathArgument> extends AbstractComposite<T> {
+        private final Map<PathArgument, StreamingContext<?>> byArg = new HashMap<>();
+        private final DataNodeContainer schema;
+
+        AbstractDataContainer(final T identifier, final DataNodeContainer schema) {
+            super(identifier);
+            this.schema = schema;
+        }
+
+        @Override
+        final StreamingContext<?> getChild(final PathArgument child) {
+            StreamingContext<?> potential = byArg.get(child);
+            if (potential != null) {
+                return potential;
+            }
+            potential = fromLocalSchema(child);
+            if (potential != null) {
+                byArg.put(potential.getIdentifier(), potential);
+            }
+            return potential;
+        }
+
+        private StreamingContext<?> fromLocalSchema(final PathArgument child) {
+            if (child instanceof AugmentationIdentifier) {
+                return fromSchemaAndQNameChecked(schema, ((AugmentationIdentifier) child).getPossibleChildNames()
+                        .iterator().next());
+            }
+            return fromSchemaAndQNameChecked(schema, child.getNodeType());
+        }
+    }
+
+    private abstract static class AbstractMapMixin extends AbstractComposite<NodeIdentifier> {
+        private final ListEntry innerNode;
+
+        AbstractMapMixin(final ListSchemaNode list) {
+            super(NodeIdentifier.create(list.getQName()));
+            this.innerNode = new ListEntry(new NodeIdentifierWithPredicates(list.getQName(), ImmutableMap.of()), list);
+        }
+
+        @Override
+        final StreamingContext<?> getChild(final PathArgument child) {
+            return child.getNodeType().equals(getIdentifier().getNodeType()) ? innerNode : null;
+        }
+
+        @Override
+        final boolean isMixin() {
+            return true;
+        }
+    }
+
+    private abstract static class AbstractSimple<T extends PathArgument> extends StreamingContext<T> {
+        AbstractSimple(final T identifier) {
+            super(identifier);
+        }
+
+        @Override
+        final StreamingContext<?> getChild(final PathArgument child) {
+            return null;
+        }
+
+        @Override
+        final boolean isMixin() {
+            return false;
+        }
+    }
+
+    private static final class AnyXml extends AbstractSimple<NodeIdentifier> {
+        AnyXml(final AnyXmlSchemaNode schema) {
+            super(NodeIdentifier.create(schema.getQName()));
+        }
+
+        @Override
+        void streamToWriter(final NormalizedNodeStreamWriter writer, final PathArgument first,
+                final Iterator<PathArgument> others) throws IOException {
+            writer.anyxmlNode(getIdentifier(), null);
+        }
+    }
+
+    private static final class Choice extends AbstractComposite<NodeIdentifier> {
+        private final ImmutableMap<PathArgument, StreamingContext<?>> byArg;
+
+        Choice(final ChoiceSchemaNode schema) {
+            super(NodeIdentifier.create(schema.getQName()));
+            final ImmutableMap.Builder<PathArgument, StreamingContext<?>> byArgBuilder = ImmutableMap.builder();
+
+            for (final CaseSchemaNode caze : schema.getCases().values()) {
+                for (final DataSchemaNode cazeChild : caze.getChildNodes()) {
+                    final StreamingContext<?> childOp = fromDataSchemaNode(cazeChild);
+                    byArgBuilder.put(childOp.getIdentifier(), childOp);
+                }
+            }
+            byArg = byArgBuilder.build();
+        }
+
+        @Override
+        StreamingContext<?> getChild(final PathArgument child) {
+            return byArg.get(child);
+        }
+
+        @Override
+        boolean isMixin() {
+            return true;
+        }
+
+        @Override
+        void emitElementStart(final NormalizedNodeStreamWriter writer, final PathArgument arg) throws IOException {
+            writer.startChoiceNode(getIdentifier(), UNKNOWN_SIZE);
+        }
+    }
+
+    private static final class Leaf extends AbstractSimple<NodeIdentifier> {
+        Leaf(final LeafSchemaNode potential) {
+            super(new NodeIdentifier(potential.getQName()));
+        }
+
+        @Override
+        void streamToWriter(final NormalizedNodeStreamWriter writer, final PathArgument first,
+                final Iterator<PathArgument> others) throws IOException {
+            writer.leafNode(getIdentifier(), null);
+        }
+    }
+
+    private static final class LeafListEntry extends AbstractSimple<NodeWithValue<?>> {
+        LeafListEntry(final LeafListSchemaNode potential) {
+            super(new NodeWithValue<>(potential.getQName(), null));
+        }
+
+        @Override
+        void streamToWriter(final NormalizedNodeStreamWriter writer, final PathArgument first,
+                final Iterator<PathArgument> others) throws IOException {
+            checkArgument(first instanceof NodeWithValue);
+            final NodeWithValue<?> identifier = (NodeWithValue<?>) first;
+            writer.leafSetEntryNode(identifier.getNodeType(), identifier.getValue());
+        }
+    }
+
+    private static final class ListEntry extends AbstractDataContainer<NodeIdentifierWithPredicates> {
+        ListEntry(final NodeIdentifierWithPredicates identifier, final ListSchemaNode schema) {
+            super(identifier, schema);
+        }
+
+        @Override
+        boolean isMixin() {
+            return false;
+        }
+
+        @Override
+        void emitElementStart(final NormalizedNodeStreamWriter writer, final PathArgument arg) throws IOException {
+            final NodeIdentifierWithPredicates identifier = (NodeIdentifierWithPredicates) arg;
+            writer.startMapEntryNode(identifier, UNKNOWN_SIZE);
+
+            for (Entry<QName, Object> entry : identifier.getKeyValues().entrySet()) {
+                writer.leafNode(new NodeIdentifier(entry.getKey()), entry.getValue());
+            }
+        }
+    }
+
+    private static final class UnkeyedListItem extends AbstractDataContainer<NodeIdentifier> {
+        UnkeyedListItem(final ListSchemaNode schema) {
+            super(NodeIdentifier.create(schema.getQName()), schema);
+        }
+
+        @Override
+        boolean isMixin() {
+            return false;
+        }
+
+        @Override
+        void emitElementStart(final NormalizedNodeStreamWriter writer, final PathArgument arg) throws IOException {
+            writer.startUnkeyedListItem(getIdentifier(), UNKNOWN_SIZE);
+        }
+    }
+
+    private static final class Container extends AbstractDataContainer<NodeIdentifier> {
+        Container(final ContainerSchemaNode schema) {
+            super(NodeIdentifier.create(schema.getQName()), schema);
+        }
+
+        @Override
+        boolean isMixin() {
+            return false;
+        }
+
+        @Override
+        void emitElementStart(final NormalizedNodeStreamWriter writer, final PathArgument arg) throws IOException {
+            writer.startContainerNode(getIdentifier(), UNKNOWN_SIZE);
+        }
+    }
+
+    private abstract static class LeafListMixin extends AbstractComposite<NodeIdentifier> {
+        private final StreamingContext<?> innerOp;
+
+        LeafListMixin(final LeafListSchemaNode potential) {
+            super(NodeIdentifier.create(potential.getQName()));
+            innerOp = new LeafListEntry(potential);
+        }
+
+        @Override
+        final StreamingContext<?> getChild(final PathArgument child) {
+            return child instanceof NodeWithValue ? innerOp : null;
+        }
+
+        @Override
+        final boolean isMixin() {
+            return true;
+        }
+    }
+
+    private static final class OrderedLeafListMixin extends LeafListMixin {
+        OrderedLeafListMixin(final LeafListSchemaNode potential) {
+            super(potential);
+        }
+
+        @Override
+        void emitElementStart(final NormalizedNodeStreamWriter writer, final PathArgument arg) throws IOException {
+            writer.startOrderedLeafSet(getIdentifier(), UNKNOWN_SIZE);
+        }
+    }
+
+    private static class UnorderedLeafListMixin extends LeafListMixin {
+        UnorderedLeafListMixin(final LeafListSchemaNode potential) {
+            super(potential);
+        }
+
+        @Override
+        void emitElementStart(final NormalizedNodeStreamWriter writer, final PathArgument arg) throws IOException {
+            writer.startLeafSet(getIdentifier(), UNKNOWN_SIZE);
+        }
+    }
+
+    private static final class Augmentation extends AbstractDataContainer<AugmentationIdentifier> {
+        Augmentation(final AugmentationSchemaNode augmentation, final DataNodeContainer schema) {
+            super(DataSchemaContextNode.augmentationIdentifierFrom(augmentation),
+                    EffectiveAugmentationSchema.create(augmentation, schema));
+        }
+
+        @Override
+        boolean isMixin() {
+            return true;
+        }
+
+        @Override
+        void emitElementStart(final NormalizedNodeStreamWriter writer, final PathArgument arg) throws IOException {
+            writer.startAugmentationNode(getIdentifier());
+        }
+    }
+
+    private static final class UnorderedMapMixin extends AbstractMapMixin {
+        UnorderedMapMixin(final ListSchemaNode list) {
+            super(list);
+        }
+
+        @Override
+        void emitElementStart(final NormalizedNodeStreamWriter writer, final PathArgument arg) throws IOException {
+            writer.startMapNode(getIdentifier(), UNKNOWN_SIZE);
+        }
+    }
+
+    private static final class OrderedMapMixin extends AbstractMapMixin {
+        OrderedMapMixin(final ListSchemaNode list) {
+            super(list);
+        }
+
+        @Override
+        void emitElementStart(final NormalizedNodeStreamWriter writer, final PathArgument arg) throws IOException {
+            writer.startOrderedMapNode(getIdentifier(), UNKNOWN_SIZE);
+        }
+    }
+
+    private static final class UnkeyedListMixin extends AbstractComposite<NodeIdentifier> {
+        private final UnkeyedListItem innerNode;
+
+        UnkeyedListMixin(final ListSchemaNode list) {
+            super(NodeIdentifier.create(list.getQName()));
+            this.innerNode = new UnkeyedListItem(list);
+        }
+
+        @Override
+        StreamingContext<?> getChild(final PathArgument child) {
+            return child.getNodeType().equals(getIdentifier().getNodeType()) ? innerNode : null;
+        }
+
+        @Override
+        boolean isMixin() {
+            return true;
+        }
+
+        @Override
+        void emitElementStart(final NormalizedNodeStreamWriter writer, final PathArgument arg) throws IOException {
+            writer.startUnkeyedList(getIdentifier(), UNKNOWN_SIZE);
+        }
+    }
+}
index b7f9dfb96476643e1fce4f48d144d5235333a1c2..f7b5e2dc34761b2245ede7d1a0d0b9b83f7aa715 100644 (file)
@@ -184,15 +184,13 @@ public final class NetconfMessageTransformUtil {
                 Builders.anyXmlBuilder().withNodeIdentifier(toId(NETCONF_FILTER_QNAME));
         anyXmlBuilder.withAttributes(Collections.singletonMap(NETCONF_TYPE_QNAME, SUBTREE));
 
-        final NormalizedNode<?, ?> filterContent = ImmutableNodes.fromInstanceId(ctx, identifier);
-
         final Element element = XmlUtil.createElement(BLANK_DOCUMENT, NETCONF_FILTER_QNAME.getLocalName(),
                 Optional.of(NETCONF_FILTER_QNAME.getNamespace().toString()));
         element.setAttributeNS(NETCONF_FILTER_QNAME.getNamespace().toString(), NETCONF_TYPE_QNAME.getLocalName(),
                 "subtree");
 
         try {
-            NetconfUtil.writeNormalizedNode(filterContent, new DOMResult(element), SchemaPath.ROOT, ctx);
+            NetconfUtil.writeFilter(identifier, new DOMResult(element), SchemaPath.ROOT, ctx);
         } catch (IOException | XMLStreamException e) {
             throw new IllegalStateException("Unable to serialize filter element for path " + identifier, e);
         }
index a884a887f209f8f244dd8daad51bd39f691a6f90..da19457f3e734caf2ce7a8f732d27d12c599c10c 100644 (file)
@@ -5,7 +5,6 @@
  * 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.netconf.sal.connect.netconf.schema.mapping;
 
 import static org.junit.Assert.assertEquals;
@@ -31,6 +30,7 @@ import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTr
 import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.toPath;
 
 import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
@@ -213,6 +213,36 @@ public class NetconfMessageTransformerTest {
         assertEquals(schemaNode, schemaParent.getValue().iterator().next());
     }
 
+    @Test
+    public void testGetConfigLeafRequest() throws Exception {
+        final DataContainerChild<?, ?> filter = toFilterStructure(
+                YangInstanceIdentifier.create(toId(NetconfState.QNAME), toId(Schemas.QNAME), toId(Schema.QNAME),
+                    new YangInstanceIdentifier.NodeIdentifierWithPredicates(Schema.QNAME, ImmutableMap.of()),
+                    toId(QName.create(Schemas.QNAME, "version"))), schema);
+
+        final DataContainerChild<?, ?> source = NetconfBaseOps.getSourceNode(NETCONF_RUNNING_QNAME);
+
+        final NetconfMessage netconfMessage = netconfMessageTransformer.toRpcRequest(toPath(NETCONF_GET_CONFIG_QNAME),
+                NetconfMessageTransformUtil.wrap(NETCONF_GET_CONFIG_QNAME, source, filter));
+
+        assertSimilarXml(netconfMessage, "<rpc message-id=\"m-0\" xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n"
+                + "<get-config xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n"
+                + "<filter xmlns:ns0=\"urn:ietf:params:xml:ns:netconf:base:1.0\" ns0:type=\"subtree\">\n"
+                + "<netconf-state xmlns=\"urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring\">\n"
+                + "<schemas>\n"
+                + "<schema>\n"
+                + "<version/>\n"
+                + "</schema>\n"
+                + "</schemas>\n"
+                + "</netconf-state>\n"
+                + "</filter>\n"
+                + "<source>\n"
+                + "<running/>\n"
+                + "</source>\n"
+                + "</get-config>\n"
+                + "</rpc>");
+    }
+
     @Test
     public void testGetConfigRequest() throws Exception {
         final DataContainerChild<?, ?> filter = toFilterStructure(