Bug 6903 - Implement Query parameters - fields 69/46669/44
authorIvan Hrasko <ivan.hrasko@pantheon.tech>
Fri, 7 Oct 2016 11:55:38 +0000 (13:55 +0200)
committerIvan Hrasko <ivan.hrasko@pantheon.tech>
Fri, 4 Nov 2016 11:18:56 +0000 (11:18 +0000)
- parse and verify fields parameter from URI
- nodes written to output are limited in
ParameterAwareNormalizedNodeWriter according to
parsed fields parameter value
- ParameterAwareNormalizedNodeWriter is used as writer
even if no parameters are used
- fields parameter is used together with depth
parameter, when node is selected by fields parameter
then value of depth parameter has no effect on selected
node and its *ancestors*
- contains unit tests for fields parameter parser and
ParameterAwareNormalizedNodeWriter class
- fixed depth for map nodes in way that when map node is
target and depth is 1 no map entries children are written
to output
- added support to read data (GET) from restconf/data path

Change-Id: I3896dba6faa3fdbcb00e07fdfd7b3fdc02ba57d9
Signed-off-by: Ivan Hrasko <ivan.hrasko@pantheon.tech>
20 files changed:
restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/DepthAwareNormalizedNodeWriter.java
restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/NormalizedNodeJsonBodyWriter.java
restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/NormalizedNodeXmlBodyWriter.java
restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/WriterParameters.java
restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/RestconfApplication.java
restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/common/wrapper/services/ServicesWrapperImpl.java
restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/jersey/providers/NormalizedNodeJsonBodyWriter.java [new file with mode: 0644]
restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/jersey/providers/NormalizedNodeXmlBodyWriter.java [new file with mode: 0644]
restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/jersey/providers/ParameterAwareNormalizedNodeWriter.java [new file with mode: 0644]
restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/restful/services/api/RestconfDataService.java
restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/restful/services/impl/RestconfDataServiceImpl.java
restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/restful/utils/ReadDataTransactionUtil.java
restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/restful/utils/RestconfDataServiceConstant.java
restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/utils/parser/ParserFieldsParameter.java [new file with mode: 0644]
restconf/sal-rest-connector/src/test/java/org/opendaylight/restconf/jersey/providers/ParameterAwareNormalizedNodeWriterDepthTest.java [new file with mode: 0644]
restconf/sal-rest-connector/src/test/java/org/opendaylight/restconf/jersey/providers/ParameterAwareNormalizedNodeWriterFieldsTest.java [new file with mode: 0644]
restconf/sal-rest-connector/src/test/java/org/opendaylight/restconf/jersey/providers/ParameterAwareNormalizedNodeWriterParametersTest.java [new file with mode: 0644]
restconf/sal-rest-connector/src/test/java/org/opendaylight/restconf/restful/utils/ReadDataTransactionUtilTest.java
restconf/sal-rest-connector/src/test/java/org/opendaylight/restconf/utils/parser/ParserFieldsParameterTest.java [new file with mode: 0644]
restconf/sal-rest-connector/src/test/resources/jukebox/augmented-jukebox@2016-05-05.yang [new file with mode: 0644]

index 23872ce0c737c1ec0e6440d5e6575d1a2de8a206..240839b61022c8204633a585863a7e74ef988e38 100644 (file)
@@ -8,7 +8,7 @@
 package org.opendaylight.netconf.sal.rest.impl;
 
 import static org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter.UNKNOWN_SIZE;
-import com.google.common.annotations.Beta;
+
 import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Predicate;
@@ -44,8 +44,11 @@ import org.slf4j.LoggerFactory;
  * the opposite of a {@link javax.xml.stream.XMLStreamReader} -- unlike instantiating an iterator over
  * the backing data, this encapsulates a {@link NormalizedNodeStreamWriter} and allows
  * us to write multiple nodes.
+ *
+ * @deprecated This class will be replaced by
+ * {@link org.opendaylight.restconf.jersey.providers.ParameterAwareNormalizedNodeWriter}
  */
-@Beta
+@Deprecated
 public class DepthAwareNormalizedNodeWriter implements RestconfNormalizedNodeWriter {
     private final NormalizedNodeStreamWriter writer;
     protected int currentDepth = 0;
index 96389809db7994c1143a80eb69d9e4048ff3307d..e48afa695df1e6d9ad796c63bdc145415125a747 100644 (file)
@@ -27,8 +27,6 @@ import org.opendaylight.netconf.sal.rest.api.RestconfNormalizedNodeWriter;
 import org.opendaylight.netconf.sal.rest.api.RestconfService;
 import org.opendaylight.netconf.sal.restconf.impl.InstanceIdentifierContext;
 import org.opendaylight.netconf.sal.restconf.impl.NormalizedNodeContext;
-import org.opendaylight.restconf.Draft17;
-import org.opendaylight.restconf.utils.RestconfConstants;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
@@ -45,10 +43,14 @@ import org.opendaylight.yangtools.yang.model.api.SchemaContext;
 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
 
+/**
+ * @deprecated This class will be replaced by
+ * {@link org.opendaylight.restconf.jersey.providers.NormalizedNodeJsonBodyWriter}
+ */
+@Deprecated
 @Provider
 @Produces({ Draft02.MediaTypes.API + RestconfService.JSON, Draft02.MediaTypes.DATA + RestconfService.JSON,
-        Draft02.MediaTypes.OPERATION + RestconfService.JSON, Draft17.MediaTypes.DATA + RestconfConstants.JSON,
-        MediaType.APPLICATION_JSON })
+        Draft02.MediaTypes.OPERATION + RestconfService.JSON, MediaType.APPLICATION_JSON })
 public class NormalizedNodeJsonBodyWriter implements MessageBodyWriter<NormalizedNodeContext> {
 
     private static final int DEFAULT_INDENT_SPACES_NUM = 2;
@@ -78,7 +80,7 @@ public class NormalizedNodeJsonBodyWriter implements MessageBodyWriter<Normalize
         final SchemaPath path = context.getSchemaNode().getPath();
         final JsonWriter jsonWriter = createJsonWriter(entityStream, t.getWriterParameters().isPrettyPrint());
         jsonWriter.beginObject();
-        writeNormalizedNode(jsonWriter,path,context,data, t.getWriterParameters().getDepth());
+        writeNormalizedNode(jsonWriter,path,context,data, Optional.fromNullable(t.getWriterParameters().getDepth()));
         jsonWriter.endObject();
         jsonWriter.flush();
     }
index 0a0435c09000630b8ad05aef4b3154bb404b080a..827a8d7d08f2a58c8dea8a06f527c0e863889dfa 100644 (file)
@@ -13,6 +13,7 @@ import java.io.IOException;
 import java.io.OutputStream;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Type;
+import javanet.staxutils.IndentingXMLStreamWriter;
 import javax.ws.rs.Produces;
 import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.MediaType;
@@ -29,8 +30,6 @@ import org.opendaylight.netconf.sal.rest.api.RestconfNormalizedNodeWriter;
 import org.opendaylight.netconf.sal.rest.api.RestconfService;
 import org.opendaylight.netconf.sal.restconf.impl.InstanceIdentifierContext;
 import org.opendaylight.netconf.sal.restconf.impl.NormalizedNodeContext;
-import org.opendaylight.restconf.Draft17;
-import org.opendaylight.restconf.utils.RestconfConstants;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
@@ -41,12 +40,15 @@ import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
-import javanet.staxutils.IndentingXMLStreamWriter;
 
+/**
+ * @deprecated This class will be replaced by
+ * {@link org.opendaylight.restconf.jersey.providers.NormalizedNodeXmlBodyWriter}
+ */
+@Deprecated
 @Provider
 @Produces({ Draft02.MediaTypes.API + RestconfService.XML, Draft02.MediaTypes.DATA + RestconfService.XML,
-        Draft02.MediaTypes.OPERATION + RestconfService.XML, Draft17.MediaTypes.DATA + RestconfConstants.XML,
-        MediaType.APPLICATION_XML, MediaType.TEXT_XML })
+        Draft02.MediaTypes.OPERATION + RestconfService.XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML })
 public class NormalizedNodeXmlBodyWriter implements MessageBodyWriter<NormalizedNodeContext> {
 
     private static final XMLOutputFactory XML_FACTORY;
@@ -92,10 +94,7 @@ public class NormalizedNodeXmlBodyWriter implements MessageBodyWriter<Normalized
         final NormalizedNode<?, ?> data = t.getData();
         final SchemaPath schemaPath = pathContext.getSchemaNode().getPath();
 
-
-
-        writeNormalizedNode(xmlWriter, schemaPath, pathContext, data, t.getWriterParameters().getDepth());
-
+        writeNormalizedNode(xmlWriter, schemaPath, pathContext, data, Optional.fromNullable(t.getWriterParameters().getDepth()));
     }
 
     private void writeNormalizedNode(final XMLStreamWriter xmlWriter, final SchemaPath schemaPath, final InstanceIdentifierContext<?>
index 07978083dd5e1ed001e15f7ba1143d67244df3c0..996f6d9d0d5e63bbd88d22d9f498c18e3cae339c 100644 (file)
@@ -8,16 +8,20 @@
 
 package org.opendaylight.netconf.sal.restconf.impl;
 
-import com.google.common.base.Optional;
+import java.util.List;
+import java.util.Set;
+import org.opendaylight.yangtools.yang.common.QName;
 
 public class WriterParameters {
     private final String content;
-    private final Optional<Integer> depth;
+    private final Integer depth;
+    private final List<Set<QName>> fields;
     private final boolean prettyPrint;
 
     private WriterParameters(final WriterParametersBuilder builder) {
         this.content = builder.content;
         this.depth = builder.depth;
+        this.fields = builder.fields;
         this.prettyPrint = builder.prettyPrint;
     }
 
@@ -25,17 +29,22 @@ public class WriterParameters {
         return content;
     }
 
-    public Optional<Integer> getDepth() {
+    public Integer getDepth() {
         return depth;
     }
 
+    public List<Set<QName>> getFields() {
+        return fields;
+    }
+
     public boolean isPrettyPrint() {
         return prettyPrint;
     }
 
     public static class WriterParametersBuilder {
         private String content;
-        private Optional<Integer> depth = Optional.absent();
+        private Integer depth;
+        private List<Set<QName>> fields;
         private boolean prettyPrint;
 
         public WriterParametersBuilder() {}
@@ -46,7 +55,12 @@ public class WriterParameters {
         }
 
         public WriterParametersBuilder setDepth(final int depth) {
-            this.depth = Optional.of(depth);
+            this.depth = depth;
+            return this;
+        }
+
+        public WriterParametersBuilder setFields(final List<Set<QName>> fields) {
+            this.fields = fields;
             return this;
         }
 
index 14b808205594a29aa48fef707d29fffcfa92fdeb..5f47fcbc4609a2d8f12cc1a637eda85632b2c571 100644 (file)
@@ -14,23 +14,24 @@ import javax.ws.rs.core.Application;
 import org.opendaylight.netconf.md.sal.rest.schema.SchemaExportContentYangBodyWriter;
 import org.opendaylight.netconf.md.sal.rest.schema.SchemaExportContentYinBodyWriter;
 import org.opendaylight.netconf.sal.rest.impl.JsonNormalizedNodeBodyReader;
-import org.opendaylight.netconf.sal.rest.impl.NormalizedNodeJsonBodyWriter;
-import org.opendaylight.netconf.sal.rest.impl.NormalizedNodeXmlBodyWriter;
 import org.opendaylight.netconf.sal.rest.impl.PATCHJsonBodyWriter;
 import org.opendaylight.netconf.sal.rest.impl.PATCHXmlBodyWriter;
 import org.opendaylight.netconf.sal.rest.impl.RestconfDocumentedExceptionMapper;
 import org.opendaylight.netconf.sal.rest.impl.XmlNormalizedNodeBodyReader;
 import org.opendaylight.restconf.common.wrapper.services.ServicesWrapperImpl;
 import org.opendaylight.restconf.jersey.providers.JsonToPATCHBodyReader;
+import org.opendaylight.restconf.jersey.providers.NormalizedNodeJsonBodyWriter;
+import org.opendaylight.restconf.jersey.providers.NormalizedNodeXmlBodyWriter;
 import org.opendaylight.restconf.jersey.providers.XmlToPATCHBodyReader;
 
 public class RestconfApplication extends Application {
 
     @Override
     public Set<Class<?>> getClasses() {
-        return ImmutableSet.<Class<?>> builder().add(NormalizedNodeJsonBodyWriter.class)
-                .add(NormalizedNodeXmlBodyWriter.class).add(JsonNormalizedNodeBodyReader.class)
-                .add(XmlNormalizedNodeBodyReader.class).add(SchemaExportContentYinBodyWriter.class)
+        return ImmutableSet.<Class<?>> builder()
+                .add(NormalizedNodeJsonBodyWriter.class).add(NormalizedNodeXmlBodyWriter.class)
+                .add(JsonNormalizedNodeBodyReader.class).add(XmlNormalizedNodeBodyReader.class)
+                .add(SchemaExportContentYinBodyWriter.class)
                 .add(JsonToPATCHBodyReader.class).add(XmlToPATCHBodyReader.class)
                 .add(PATCHJsonBodyWriter.class).add(PATCHXmlBodyWriter.class)
                 .add(SchemaExportContentYangBodyWriter.class).add(RestconfDocumentedExceptionMapper.class)
index ce948af3ea1e92101cf6cd19350221fde8a6ebfd..febb30802bc851141f6e33e7c6bcc1602ddf9902 100644 (file)
@@ -28,10 +28,10 @@ import org.opendaylight.restconf.rest.services.impl.RestconfModulesServiceImpl;
 import org.opendaylight.restconf.rest.services.impl.RestconfOperationsServiceImpl;
 import org.opendaylight.restconf.rest.services.impl.RestconfSchemaServiceImpl;
 import org.opendaylight.restconf.rest.services.impl.RestconfStreamsServiceImpl;
-import org.opendaylight.restconf.restful.services.api.TransactionServicesWrapper;
 import org.opendaylight.restconf.restful.services.api.RestconfDataService;
 import org.opendaylight.restconf.restful.services.api.RestconfInvokeOperationsService;
 import org.opendaylight.restconf.restful.services.api.RestconfStreamsSubscriptionService;
+import org.opendaylight.restconf.restful.services.api.TransactionServicesWrapper;
 import org.opendaylight.restconf.restful.services.impl.RestconfDataServiceImpl;
 import org.opendaylight.restconf.restful.services.impl.RestconfInvokeOperationsServiceImpl;
 import org.opendaylight.restconf.restful.services.impl.RestconfStreamsSubscriptionServiceImpl;
@@ -101,6 +101,11 @@ public class ServicesWrapperImpl implements BaseServicesWrapper, TransactionServ
         return this.delegRestSchService.getSchema(mountAndModuleId);
     }
 
+    @Override
+    public Response readData(final UriInfo uriInfo) {
+        return this.delegRestconfDataService.readData(uriInfo);
+    }
+
     @Override
     public Response readData(final String identifier, final UriInfo uriInfo) {
         return this.delegRestconfDataService.readData(identifier, uriInfo);
diff --git a/restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/jersey/providers/NormalizedNodeJsonBodyWriter.java b/restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/jersey/providers/NormalizedNodeJsonBodyWriter.java
new file mode 100644 (file)
index 0000000..349563a
--- /dev/null
@@ -0,0 +1,186 @@
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. 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.jersey.providers;
+
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Set;
+import javax.ws.rs.Produces;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.ext.MessageBodyWriter;
+import javax.ws.rs.ext.Provider;
+import org.opendaylight.netconf.sal.rest.api.RestconfNormalizedNodeWriter;
+import org.opendaylight.netconf.sal.restconf.impl.InstanceIdentifierContext;
+import org.opendaylight.netconf.sal.restconf.impl.NormalizedNodeContext;
+import org.opendaylight.restconf.Draft17;
+import org.opendaylight.restconf.utils.RestconfConstants;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
+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.NormalizedNode;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactory;
+import org.opendaylight.yangtools.yang.data.codec.gson.JSONNormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.data.codec.gson.JsonWriterFactory;
+import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
+import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
+import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+import org.opendaylight.yangtools.yang.model.api.SchemaNode;
+import org.opendaylight.yangtools.yang.model.api.SchemaPath;
+
+@Provider
+@Produces({ Draft17.MediaTypes.DATA + RestconfConstants.JSON, MediaType.APPLICATION_JSON })
+public class NormalizedNodeJsonBodyWriter implements MessageBodyWriter<NormalizedNodeContext> {
+
+    private static final int DEFAULT_INDENT_SPACES_NUM = 2;
+
+    @Override
+    public boolean isWriteable(final Class<?> type,
+                               final Type genericType,
+                               final Annotation[] annotations,
+                               final MediaType mediaType) {
+        return type.equals(NormalizedNodeContext.class);
+    }
+
+    @Override
+    public long getSize(final NormalizedNodeContext t,
+                        final Class<?> type,
+                        final Type genericType,
+                        final Annotation[] annotations,
+                        final MediaType mediaType) {
+        return -1;
+    }
+
+    @Override
+    public void writeTo(final NormalizedNodeContext t,
+                        final Class<?> type,
+                        final Type genericType,
+                        final Annotation[] annotations,
+                        final MediaType mediaType,
+                        final MultivaluedMap<String, Object> httpHeaders,
+                        final OutputStream entityStream) throws IOException, WebApplicationException {
+        final NormalizedNode<?, ?> data = t.getData();
+        if (data == null) {
+            return;
+        }
+
+        @SuppressWarnings("unchecked")
+        final InstanceIdentifierContext<SchemaNode> context =
+                (InstanceIdentifierContext<SchemaNode>) t.getInstanceIdentifierContext();
+        final SchemaPath path = context.getSchemaNode().getPath();
+        final JsonWriter jsonWriter = createJsonWriter(entityStream,
+                t.getWriterParameters().isPrettyPrint());
+
+        jsonWriter.beginObject();
+        writeNormalizedNode(jsonWriter, path, context, data,
+                t.getWriterParameters().getDepth(), t.getWriterParameters().getFields());
+        jsonWriter.endObject();
+        jsonWriter.flush();
+    }
+
+    private void writeNormalizedNode(final JsonWriter jsonWriter,
+                                     final SchemaPath path,
+                                     final InstanceIdentifierContext<SchemaNode> context,
+                                     final NormalizedNode<?, ?> data,
+                                     final Integer depth,
+                                     final List<Set<QName>> fields) throws IOException {
+        final RestconfNormalizedNodeWriter nnWriter;
+
+        if (context.getSchemaNode() instanceof RpcDefinition) {
+            /*
+             *  RpcDefinition is not supported as initial codec in JSONStreamWriter,
+             *  so we need to emit initial output declaration..
+             */
+            nnWriter = createNormalizedNodeWriter(
+                    context,
+                    ((RpcDefinition) context.getSchemaNode()).getOutput().getPath(),
+                    jsonWriter,
+                    depth,
+                    fields);
+            jsonWriter.name("output");
+            jsonWriter.beginObject();
+            writeChildren(nnWriter, (ContainerNode) data);
+            jsonWriter.endObject();
+        } else {
+            if (SchemaPath.ROOT.equals(path)) {
+                nnWriter = createNormalizedNodeWriter(context, path, jsonWriter, depth, fields);
+            } else {
+                nnWriter = createNormalizedNodeWriter(context, path.getParent(), jsonWriter, depth, fields);
+            }
+
+            if (data instanceof MapEntryNode) {
+                // Restconf allows returning one list item. We need to wrap it
+                // in map node in order to serialize it properly
+                nnWriter.write(
+                        ImmutableNodes.mapNodeBuilder(data.getNodeType()).withChild(((MapEntryNode) data)).build());
+            } else {
+                nnWriter.write(data);
+            }
+        }
+
+        nnWriter.flush();
+    }
+
+    private void writeChildren(final RestconfNormalizedNodeWriter nnWriter,
+                               final ContainerNode data) throws IOException {
+        for (final DataContainerChild<? extends PathArgument, ?> child : data.getValue()) {
+            nnWriter.write(child);
+        }
+    }
+
+    private RestconfNormalizedNodeWriter createNormalizedNodeWriter(final InstanceIdentifierContext<SchemaNode> context,
+                                                                    final SchemaPath path,
+                                                                    final JsonWriter jsonWriter,
+                                                                    final Integer depth,
+                                                                    final List<Set<QName>> fields) {
+
+        final SchemaNode schema = context.getSchemaNode();
+        final JSONCodecFactory codecs = getCodecFactory(context);
+
+        final URI initialNs;
+        if ((schema instanceof DataSchemaNode)
+                && !((DataSchemaNode)schema).isAugmenting()
+                && !(schema instanceof SchemaContext)) {
+            initialNs = schema.getQName().getNamespace();
+        } else if (schema instanceof RpcDefinition) {
+            initialNs = schema.getQName().getNamespace();
+        } else {
+            initialNs = null;
+        }
+        final NormalizedNodeStreamWriter streamWriter = JSONNormalizedNodeStreamWriter.createNestedWriter(
+                codecs, path, initialNs, jsonWriter);
+        return ParameterAwareNormalizedNodeWriter.forStreamWriter(streamWriter, depth, fields);
+    }
+
+    private JsonWriter createJsonWriter(final OutputStream entityStream, final boolean prettyPrint) {
+        if (prettyPrint) {
+            return JsonWriterFactory.createJsonWriter(
+                    new OutputStreamWriter(entityStream, StandardCharsets.UTF_8), DEFAULT_INDENT_SPACES_NUM);
+        } else {
+            return JsonWriterFactory.createJsonWriter(new OutputStreamWriter(entityStream, StandardCharsets.UTF_8));
+        }
+    }
+
+    private JSONCodecFactory getCodecFactory(final InstanceIdentifierContext<?> context) {
+        // TODO: Performance: Cache JSON Codec factory and schema context
+        return JSONCodecFactory.create(context.getSchemaContext());
+    }
+}
diff --git a/restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/jersey/providers/NormalizedNodeXmlBodyWriter.java b/restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/jersey/providers/NormalizedNodeXmlBodyWriter.java
new file mode 100644 (file)
index 0000000..b474f4e
--- /dev/null
@@ -0,0 +1,171 @@
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. 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.jersey.providers;
+
+import com.google.common.base.Throwables;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.util.List;
+import java.util.Set;
+import javanet.staxutils.IndentingXMLStreamWriter;
+import javax.ws.rs.Produces;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.ext.MessageBodyWriter;
+import javax.ws.rs.ext.Provider;
+import javax.xml.XMLConstants;
+import javax.xml.stream.FactoryConfigurationError;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+import org.opendaylight.netconf.sal.rest.api.RestconfNormalizedNodeWriter;
+import org.opendaylight.netconf.sal.restconf.impl.InstanceIdentifierContext;
+import org.opendaylight.netconf.sal.restconf.impl.NormalizedNodeContext;
+import org.opendaylight.restconf.Draft17;
+import org.opendaylight.restconf.utils.RestconfConstants;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
+import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
+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.impl.codec.xml.XMLStreamNormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
+import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
+import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+import org.opendaylight.yangtools.yang.model.api.SchemaPath;
+
+@Provider
+@Produces({ Draft17.MediaTypes.DATA + RestconfConstants.XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML })
+public class NormalizedNodeXmlBodyWriter implements MessageBodyWriter<NormalizedNodeContext> {
+
+    private static final XMLOutputFactory XML_FACTORY;
+
+    static {
+        XML_FACTORY = XMLOutputFactory.newFactory();
+        XML_FACTORY.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);
+    }
+
+    @Override
+    public boolean isWriteable(final Class<?> type,
+                               final Type genericType,
+                               final Annotation[] annotations,
+                               final MediaType mediaType) {
+        return type.equals(NormalizedNodeContext.class);
+    }
+
+    @Override
+    public long getSize(final NormalizedNodeContext t,
+                        final Class<?> type,
+                        final Type genericType,
+                        final Annotation[] annotations,
+                        final MediaType mediaType) {
+        return -1;
+    }
+
+    @Override
+    public void writeTo(final NormalizedNodeContext t,
+                        final Class<?> type,
+                        final Type genericType,
+                        final Annotation[] annotations,
+                        final MediaType mediaType,
+                        final MultivaluedMap<String, Object> httpHeaders,
+                        final OutputStream entityStream) throws IOException, WebApplicationException {
+        final InstanceIdentifierContext<?> pathContext = t.getInstanceIdentifierContext();
+        if (t.getData() == null) {
+            return;
+        }
+
+        XMLStreamWriter xmlWriter;
+        try {
+            xmlWriter = XML_FACTORY.createXMLStreamWriter(entityStream);
+            if (t.getWriterParameters().isPrettyPrint()) {
+                xmlWriter = new IndentingXMLStreamWriter(xmlWriter);
+            }
+        } catch (final XMLStreamException | FactoryConfigurationError e) {
+            throw new IllegalStateException(e);
+        }
+        final NormalizedNode<?, ?> data = t.getData();
+        final SchemaPath schemaPath = pathContext.getSchemaNode().getPath();
+
+        writeNormalizedNode(xmlWriter, schemaPath, pathContext, data, t.getWriterParameters().getDepth(),
+                t.getWriterParameters().getFields());
+    }
+
+    private void writeNormalizedNode(final XMLStreamWriter xmlWriter,
+                                     final SchemaPath path,
+                                     final InstanceIdentifierContext<?> pathContext,
+                                     final NormalizedNode<?, ?> data,
+                                     final Integer depth,
+                                     final List<Set<QName>> fields) throws IOException {
+        final RestconfNormalizedNodeWriter nnWriter;
+        final SchemaContext schemaCtx = pathContext.getSchemaContext();
+
+        if (pathContext.getSchemaNode() instanceof RpcDefinition) {
+            /*
+             *  RpcDefinition is not supported as initial codec in XMLStreamWriter,
+             *  so we need to emit initial output declaration..
+             */
+            nnWriter = createNormalizedNodeWriter(
+                    xmlWriter,
+                    schemaCtx,
+                    ((RpcDefinition) pathContext.getSchemaNode()).getOutput().getPath(),
+                    depth,
+                    fields);
+            writeElements(xmlWriter, nnWriter, (ContainerNode) data);
+        } else {
+            if (SchemaPath.ROOT.equals(path)) {
+                nnWriter = createNormalizedNodeWriter(xmlWriter, schemaCtx, path, depth, fields);
+            } else {
+                nnWriter = createNormalizedNodeWriter(xmlWriter, schemaCtx, path.getParent(), depth, fields);
+            }
+
+            if (data instanceof MapEntryNode) {
+                // Restconf allows returning one list item. We need to wrap it
+                // in map node in order to serialize it properly
+                nnWriter.write(ImmutableNodes.mapNodeBuilder(data.getNodeType()).addChild((MapEntryNode) data).build());
+            } else {
+                nnWriter.write(data);
+            }
+        }
+
+        nnWriter.flush();
+    }
+
+    private RestconfNormalizedNodeWriter createNormalizedNodeWriter(final XMLStreamWriter xmlWriter,
+                                                                    final SchemaContext schemaContext,
+                                                                    final SchemaPath schemaPath,
+                                                                    final Integer depth,
+                                                                    final List<Set<QName>> fields) {
+        final NormalizedNodeStreamWriter xmlStreamWriter = XMLStreamNormalizedNodeStreamWriter
+                .create(xmlWriter, schemaContext, schemaPath);
+        return ParameterAwareNormalizedNodeWriter.forStreamWriter(xmlStreamWriter, depth, fields);
+    }
+
+    private void writeElements(final XMLStreamWriter xmlWriter,
+                               final RestconfNormalizedNodeWriter nnWriter,
+                               final ContainerNode data) throws IOException {
+        try {
+            final QName name = data.getNodeType();
+            xmlWriter.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX,
+                    name.getLocalName(), name.getNamespace().toString());
+            xmlWriter.writeDefaultNamespace(name.getNamespace().toString());
+            for (final NormalizedNode<?,?> child : data.getValue()) {
+                nnWriter.write(child);
+            }
+            nnWriter.flush();
+            xmlWriter.writeEndElement();
+            xmlWriter.flush();
+        } catch (final XMLStreamException e) {
+            Throwables.propagate(e);
+        }
+    }
+}
diff --git a/restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/jersey/providers/ParameterAwareNormalizedNodeWriter.java b/restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/jersey/providers/ParameterAwareNormalizedNodeWriter.java
new file mode 100644 (file)
index 0000000..060cb0d
--- /dev/null
@@ -0,0 +1,398 @@
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. 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.jersey.providers;
+
+import static org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter.UNKNOWN_SIZE;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.opendaylight.netconf.sal.rest.api.RestconfNormalizedNodeWriter;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.schema.AnyXmlNode;
+import org.opendaylight.yangtools.yang.data.api.schema.AugmentationNode;
+import org.opendaylight.yangtools.yang.data.api.schema.ChoiceNode;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafSetEntryNode;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode;
+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.NormalizedNode;
+import org.opendaylight.yangtools.yang.data.api.schema.OrderedLeafSetNode;
+import org.opendaylight.yangtools.yang.data.api.schema.OrderedMapNode;
+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.NormalizedNodeStreamAttributeWriter;
+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
+ * the opposite of a {@link javax.xml.stream.XMLStreamReader} -- unlike instantiating an iterator over
+ * the backing data, this encapsulates a {@link NormalizedNodeStreamWriter} and allows
+ * us to write multiple nodes.
+ */
+@Beta
+public class ParameterAwareNormalizedNodeWriter implements RestconfNormalizedNodeWriter {
+    private static final QName ROOT_DATA_QNAME = QName.create("urn:ietf:params:xml:ns:netconf:base:1.0", "data");
+
+    private final NormalizedNodeStreamWriter writer;
+    private final Integer maxDepth;
+    protected final List<Set<QName>> fields;
+    protected int currentDepth = 0;
+
+    private ParameterAwareNormalizedNodeWriter(final NormalizedNodeStreamWriter writer, final Integer maxDepth,
+                                               final List<Set<QName>> fields) {
+        this.writer = Preconditions.checkNotNull(writer);
+        this.maxDepth = maxDepth;
+        this.fields = fields;
+    }
+
+    protected final NormalizedNodeStreamWriter getWriter() {
+        return writer;
+    }
+
+    /**
+     * Create a new writer backed by a {@link NormalizedNodeStreamWriter}.
+     *
+     * @param writer Back-end writer
+     * @param maxDepth Maximal depth to write
+     * @param fields Selected child nodes to write
+     * @return A new instance.
+     */
+    public static ParameterAwareNormalizedNodeWriter forStreamWriter(
+            final NormalizedNodeStreamWriter writer, final Integer maxDepth, final List<Set<QName>> fields) {
+        return forStreamWriter(writer, true,  maxDepth, fields);
+    }
+
+    /**
+     * Create a new writer backed by a {@link NormalizedNodeStreamWriter}. Unlike the simple
+     * {@link #forStreamWriter(NormalizedNodeStreamWriter, Integer, List)}
+     * method, this allows the caller to switch off RFC6020 XML compliance, providing better
+     * throughput. The reason is that the XML mapping rules in RFC6020 require the encoding
+     * to emit leaf nodes which participate in a list's key first and in the order in which
+     * they are defined in the key. For JSON, this requirement is completely relaxed and leaves
+     * can be ordered in any way we see fit. The former requires a bit of work: first a lookup
+     * for each key and then for each emitted node we need to check whether it was already
+     * emitted.
+     *
+     * @param writer Back-end writer
+     * @param orderKeyLeaves whether the returned instance should be RFC6020 XML compliant.
+     * @param maxDepth Maximal depth to write
+     * @param fields Selected child nodes to write
+     * @return A new instance.
+     */
+    public static ParameterAwareNormalizedNodeWriter forStreamWriter(final NormalizedNodeStreamWriter writer,
+                                                                     final boolean orderKeyLeaves,
+                                                                     final Integer maxDepth,
+                                                                     final List<Set<QName>> fields) {
+        if (orderKeyLeaves) {
+            return new OrderedParameterAwareNormalizedNodeWriter(writer, maxDepth, fields);
+        } else {
+            return new ParameterAwareNormalizedNodeWriter(writer, maxDepth, fields);
+        }
+    }
+
+    /**
+     * Iterate over the provided {@link NormalizedNode} and emit write
+     * events to the encapsulated {@link NormalizedNodeStreamWriter}.
+     *
+     * @param node Node
+     * @return
+     * @throws IOException when thrown from the backing writer.
+     */
+    public final ParameterAwareNormalizedNodeWriter write(final NormalizedNode<?, ?> node) throws IOException {
+        if (wasProcessedAsCompositeNode(node)) {
+            return this;
+        }
+
+        if (wasProcessAsSimpleNode(node)) {
+            return this;
+        }
+
+        throw new IllegalStateException("It wasn't possible to serialize node " + node);
+    }
+
+    @Override
+    public void flush() throws IOException {
+        writer.flush();
+    }
+
+    @Override
+    public void close() throws IOException {
+        writer.flush();
+        writer.close();
+    }
+
+    /**
+     * Emit a best guess of a hint for a particular set of children. It evaluates the
+     * iterable to see if the size can be easily gotten to. If it is, we hint at the
+     * real number of child nodes. Otherwise we emit UNKNOWN_SIZE.
+     *
+     * @param children Child nodes
+     * @return Best estimate of the collection size required to hold all the children.
+     */
+    static final int childSizeHint(final Iterable<?> children) {
+        return (children instanceof Collection) ? ((Collection<?>) children).size() : UNKNOWN_SIZE;
+    }
+
+    private boolean wasProcessAsSimpleNode(final NormalizedNode<?, ?> node) throws IOException {
+        if (node instanceof LeafSetEntryNode) {
+            if (selectedByParameters(node, false)) {
+                final LeafSetEntryNode<?> nodeAsLeafList = (LeafSetEntryNode<?>) node;
+                if (writer instanceof NormalizedNodeStreamAttributeWriter) {
+                    ((NormalizedNodeStreamAttributeWriter) writer).leafSetEntryNode(nodeAsLeafList.getNodeType(),
+                            nodeAsLeafList.getValue(), nodeAsLeafList.getAttributes());
+                } else {
+                    writer.leafSetEntryNode(nodeAsLeafList.getNodeType(), nodeAsLeafList.getValue());
+                }
+            }
+            return true;
+        } else if (node instanceof LeafNode) {
+            final LeafNode<?> nodeAsLeaf = (LeafNode<?>)node;
+            if (writer instanceof NormalizedNodeStreamAttributeWriter) {
+                ((NormalizedNodeStreamAttributeWriter) writer).leafNode(
+                        nodeAsLeaf.getIdentifier(), nodeAsLeaf.getValue(), nodeAsLeaf.getAttributes());
+            } else {
+                writer.leafNode(nodeAsLeaf.getIdentifier(), nodeAsLeaf.getValue());
+            }
+            return true;
+        } else if (node instanceof AnyXmlNode) {
+            final AnyXmlNode anyXmlNode = (AnyXmlNode)node;
+            writer.anyxmlNode(anyXmlNode.getIdentifier(), anyXmlNode.getValue());
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Check if node should be written according to parameters fields and depth.
+     * See <a href="https://tools.ietf.org/html/draft-ietf-netconf-restconf-18#page-49">Restconf draft</a>.
+     * @param node Node to be written
+     * @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) {
+        // nodes to be written are not limited by fields, only by depth
+        if (fields == null) {
+            return (maxDepth == null || currentDepth < maxDepth);
+        }
+
+        // children of mixin nodes are never selected in fields but must be written if they are first in selected target
+        if (mixinParent && currentDepth == 0) {
+            return true;
+        }
+
+        // always write augmentation nodes
+        if (node instanceof AugmentationNode) {
+            return true;
+        }
+
+        // write only selected nodes
+        if (currentDepth > 0 && currentDepth <= fields.size()) {
+            if (fields.get(currentDepth - 1).contains(node.getNodeType())) {
+                return true;
+            } else {
+                return false;
+            }
+        }
+
+        // after this depth only depth parameter is used to determine when to write node
+        return (maxDepth == null || currentDepth < maxDepth);
+    }
+
+    /**
+     * Emit events for all children and then emit an endNode() event.
+     *
+     * @param children Child iterable
+     * @param mixinParent {@code true} if parent is mixin, {@code false} otherwise
+     * @return True
+     * @throws IOException when the writer reports it
+     */
+    protected final boolean writeChildren(final Iterable<? extends NormalizedNode<?, ?>> children,
+                                          final boolean mixinParent) throws IOException {
+        for (final NormalizedNode<?, ?> child : children) {
+            if (selectedByParameters(child, mixinParent)) {
+                write(child);
+            }
+        }
+        writer.endNode();
+        return true;
+    }
+
+    protected boolean writeMapEntryChildren(final MapEntryNode mapEntryNode) throws IOException {
+        if (selectedByParameters(mapEntryNode, false)) {
+            writeChildren(mapEntryNode.getValue(), false);
+        } else if (fields == null && maxDepth != null && currentDepth == maxDepth) {
+            writeOnlyKeys(mapEntryNode.getIdentifier().getKeyValues());
+        }
+        return true;
+    }
+
+    private void writeOnlyKeys(final Map<QName, Object> keyValues) throws IllegalArgumentException, IOException {
+        for (final Map.Entry<QName, Object> entry : keyValues.entrySet()) {
+            writer.leafNode(new NodeIdentifier(entry.getKey()), entry.getValue());
+        }
+        writer.endNode();
+    }
+
+    protected boolean writeMapEntryNode(final MapEntryNode node) throws IOException {
+        if (writer instanceof NormalizedNodeStreamAttributeWriter) {
+            ((NormalizedNodeStreamAttributeWriter) writer)
+                    .startMapEntryNode(node.getIdentifier(), childSizeHint(node.getValue()), node.getAttributes());
+        } else {
+            writer.startMapEntryNode(node.getIdentifier(), childSizeHint(node.getValue()));
+        }
+        currentDepth++;
+        writeMapEntryChildren(node);
+        currentDepth--;
+        return true;
+    }
+
+    private boolean wasProcessedAsCompositeNode(final NormalizedNode<?, ?> node) throws IOException {
+        boolean processedAsCompositeNode = false;
+        if (node instanceof ContainerNode) {
+            final ContainerNode n = (ContainerNode) node;
+            if (!n.getNodeType().equals(ROOT_DATA_QNAME)) {
+                if (writer instanceof NormalizedNodeStreamAttributeWriter) {
+                    ((NormalizedNodeStreamAttributeWriter) writer).startContainerNode(
+                            n.getIdentifier(), childSizeHint(n.getValue()), n.getAttributes());
+                } else {
+                    writer.startContainerNode(n.getIdentifier(), childSizeHint(n.getValue()));
+                }
+                currentDepth++;
+                processedAsCompositeNode = writeChildren(n.getValue(), false);
+                currentDepth--;
+            } else {
+                // write child nodes of data root container
+                for (final NormalizedNode<?, ?> child : n.getValue()) {
+                    currentDepth++;
+                    if (selectedByParameters(child, false)) {
+                        write(child);
+                    }
+                    currentDepth--;
+                    processedAsCompositeNode = true;
+                }
+            }
+        }
+        else if (node instanceof MapEntryNode) {
+            processedAsCompositeNode = writeMapEntryNode((MapEntryNode) node);
+        }
+        else if (node instanceof UnkeyedListEntryNode) {
+            final UnkeyedListEntryNode n = (UnkeyedListEntryNode) node;
+            writer.startUnkeyedListItem(n.getIdentifier(), childSizeHint(n.getValue()));
+            currentDepth++;
+            processedAsCompositeNode = writeChildren(n.getValue(), false);
+            currentDepth--;
+        }
+        else if (node instanceof ChoiceNode) {
+            final ChoiceNode n = (ChoiceNode) node;
+            writer.startChoiceNode(n.getIdentifier(), childSizeHint(n.getValue()));
+            processedAsCompositeNode = writeChildren(n.getValue(), true);
+        }
+        else if (node instanceof AugmentationNode) {
+            final AugmentationNode n = (AugmentationNode) node;
+            writer.startAugmentationNode(n.getIdentifier());
+            processedAsCompositeNode = writeChildren(n.getValue(), true);
+        }
+        else if (node instanceof UnkeyedListNode) {
+            final UnkeyedListNode n = (UnkeyedListNode) node;
+            writer.startUnkeyedList(n.getIdentifier(), childSizeHint(n.getValue()));
+            processedAsCompositeNode = writeChildren(n.getValue(), false);
+        }
+        else if (node instanceof OrderedMapNode) {
+            final OrderedMapNode n = (OrderedMapNode) node;
+            writer.startOrderedMapNode(n.getIdentifier(), childSizeHint(n.getValue()));
+            processedAsCompositeNode = writeChildren(n.getValue(), true);
+        }
+        else if (node instanceof MapNode) {
+            final MapNode n = (MapNode) node;
+            writer.startMapNode(n.getIdentifier(), childSizeHint(n.getValue()));
+            processedAsCompositeNode = writeChildren(n.getValue(), true);
+        }
+        else if (node instanceof LeafSetNode) {
+            final LeafSetNode<?> n = (LeafSetNode<?>) node;
+            if (node instanceof OrderedLeafSetNode) {
+                writer.startOrderedLeafSet(n.getIdentifier(), childSizeHint(n.getValue()));
+            } else {
+                writer.startLeafSet(n.getIdentifier(), childSizeHint(n.getValue()));
+            }
+            currentDepth++;
+            processedAsCompositeNode = writeChildren(n.getValue(), true);
+            currentDepth--;
+        }
+
+        return processedAsCompositeNode;
+    }
+
+    private static final class OrderedParameterAwareNormalizedNodeWriter extends ParameterAwareNormalizedNodeWriter {
+        private static final Logger LOG = LoggerFactory.getLogger(OrderedParameterAwareNormalizedNodeWriter.class);
+
+        OrderedParameterAwareNormalizedNodeWriter(final NormalizedNodeStreamWriter writer, final Integer maxDepth,
+                                                  final List<Set<QName>> fields) {
+            super(writer, maxDepth, fields);
+        }
+
+        @Override
+        protected boolean writeMapEntryNode(final MapEntryNode node) throws IOException {
+            final NormalizedNodeStreamWriter writer = getWriter();
+            if (writer instanceof NormalizedNodeStreamAttributeWriter) {
+                ((NormalizedNodeStreamAttributeWriter) writer).startMapEntryNode(
+                        node.getIdentifier(), childSizeHint(node.getValue()), node.getAttributes());
+            } else {
+                writer.startMapEntryNode(node.getIdentifier(), childSizeHint(node.getValue()));
+            }
+
+            final Set<QName> qnames = node.getIdentifier().getKeyValues().keySet();
+            // Write out all the key children
+            currentDepth++;
+            for (final QName qname : qnames) {
+                final Optional<? extends NormalizedNode<?, ?>> child = node.getChild(new NodeIdentifier(qname));
+                if (child.isPresent()) {
+                    if (selectedByParameters(child.get(), false)) {
+                        write(child.get());
+                    }
+                } else {
+                    LOG.info("No child for key element {} found", qname);
+                }
+            }
+            currentDepth--;
+
+            currentDepth++;
+            // Write all the rest
+            final boolean result = writeChildren(Iterables.filter(node.getValue(), new Predicate<NormalizedNode<?, ?>>() {
+                @Override
+                public boolean apply(final NormalizedNode<?, ?> input) {
+                    if (input instanceof AugmentationNode) {
+                        return true;
+                    }
+                    if (!qnames.contains(input.getNodeType())) {
+                        return true;
+                    }
+
+                    LOG.debug("Skipping key child {}", input);
+                    return false;
+                }
+            }), false);
+            currentDepth--;
+            return result;
+        }
+    }
+}
index 4ada40c5feeba6c6ff6228210ce49f46d3e11edd..8a851b518c2844ff45f45324474186cdab574625 100644 (file)
@@ -47,8 +47,20 @@ public interface RestconfDataService {
     @Path("/data/{identifier:.+}")
     @Produces({ Draft17.MediaTypes.DATA + RestconfConstants.JSON, Draft17.MediaTypes.DATA, MediaType.APPLICATION_JSON,
             MediaType.APPLICATION_XML, MediaType.TEXT_XML })
-    Response readData(@Encoded @PathParam("identifier") String identifier,
-            @Context UriInfo uriInfo);
+    Response readData(@Encoded @PathParam("identifier") String identifier, @Context UriInfo uriInfo);
+
+    /**
+     * Get target data resource from data root.
+     *
+     * @param uriInfo
+     *            - URI info
+     * @return {@link NormalizedNodeContext}
+     */
+    @GET
+    @Path("/data")
+    @Produces({ Draft17.MediaTypes.DATA + RestconfConstants.JSON, Draft17.MediaTypes.DATA, MediaType.APPLICATION_JSON,
+            MediaType.APPLICATION_XML, MediaType.TEXT_XML })
+    Response readData(@Context UriInfo uriInfo);
 
     /**
      * Create or replace the target data resource.
index e75e78371b8db5c5a2dd200fb73d72769ad7bee8..b15a00aa8683d5a5b1c8a57dff0bef4377b45ff0 100644 (file)
@@ -64,16 +64,20 @@ public class RestconfDataServiceImpl implements RestconfDataService {
     }
 
     @Override
-    public Response readData(final String identifier, final UriInfo uriInfo) {
-        Preconditions.checkNotNull(identifier);
+    public Response readData(final UriInfo uriInfo) {
+        return readData(null, uriInfo);
+    }
 
-        final WriterParameters parameters = ReadDataTransactionUtil.parseUriParameters(uriInfo);
+    @Override
+    public Response readData(final String identifier, final UriInfo uriInfo) {
         final SchemaContextRef schemaContextRef = new SchemaContextRef(this.schemaContextHandler.get());
-
         final InstanceIdentifierContext<?> instanceIdentifier = ParserIdentifier.toInstanceIdentifier(
                 identifier, schemaContextRef.get(), Optional.of(this.mountPointServiceHandler.get()));
-        final DOMMountPoint mountPoint = instanceIdentifier.getMountPoint();
 
+        final WriterParameters parameters = ReadDataTransactionUtil.parseUriParameters(
+                instanceIdentifier, uriInfo);
+
+        final DOMMountPoint mountPoint = instanceIdentifier.getMountPoint();
         final DOMTransactionChain transactionChain;
         if (mountPoint == null) {
             transactionChain = this.transactionChainHandler.get();
index cf4ced08b15437d8269f908555438453a8622cec..e18ea0961337881ae5a7b32d913df1b5c7954728 100644 (file)
@@ -21,11 +21,13 @@ import javax.annotation.Nullable;
 import javax.ws.rs.core.UriInfo;
 import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
 import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException;
+import org.opendaylight.netconf.sal.restconf.impl.InstanceIdentifierContext;
 import org.opendaylight.netconf.sal.restconf.impl.RestconfDocumentedException;
 import org.opendaylight.netconf.sal.restconf.impl.RestconfError;
 import org.opendaylight.netconf.sal.restconf.impl.WriterParameters;
 import org.opendaylight.netconf.sal.restconf.impl.WriterParameters.WriterParametersBuilder;
 import org.opendaylight.restconf.restful.transaction.TransactionVarsWrapper;
+import org.opendaylight.restconf.utils.parser.ParserFieldsParameter;
 import org.opendaylight.yangtools.yang.common.QNameModule;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
@@ -64,11 +66,15 @@ public final class ReadDataTransactionUtil {
     /**
      * Parse parameters from URI request and check their types and values.
      *
+     *
+     * @param identifier
+     *            - {@link InstanceIdentifierContext}
      * @param uriInfo
      *            - URI info
      * @return {@link WriterParameters}
      */
-    @Nonnull public static WriterParameters parseUriParameters(@Nullable final UriInfo uriInfo) {
+    public static @Nonnull WriterParameters parseUriParameters(@Nonnull final InstanceIdentifierContext<?> identifier,
+                                                               @Nullable final UriInfo uriInfo) {
         final WriterParametersBuilder builder = new WriterParametersBuilder();
 
         if (uriInfo == null) {
@@ -80,7 +86,8 @@ public final class ReadDataTransactionUtil {
                 RestconfDataServiceConstant.ReadData.READ_TYPE_TX,
                 uriInfo.getQueryParameters().keySet(),
                 RestconfDataServiceConstant.ReadData.CONTENT,
-                RestconfDataServiceConstant.ReadData.DEPTH);
+                RestconfDataServiceConstant.ReadData.DEPTH,
+                RestconfDataServiceConstant.ReadData.FIELDS);
 
         // read parameters from URI or set default values
         final List<String> content = uriInfo.getQueryParameters().getOrDefault(
@@ -89,10 +96,15 @@ public final class ReadDataTransactionUtil {
         final List<String> depth = uriInfo.getQueryParameters().getOrDefault(
                 RestconfDataServiceConstant.ReadData.DEPTH,
                 Collections.singletonList(RestconfDataServiceConstant.ReadData.UNBOUNDED));
+        // fields
+        final List<String> fields = uriInfo.getQueryParameters().getOrDefault(
+                RestconfDataServiceConstant.ReadData.FIELDS,
+                Collections.emptyList());
 
         // parameter can be in URI at most once
         ParametersUtil.checkParameterCount(content, RestconfDataServiceConstant.ReadData.CONTENT);
         ParametersUtil.checkParameterCount(depth, RestconfDataServiceConstant.ReadData.DEPTH);
+        ParametersUtil.checkParameterCount(fields, RestconfDataServiceConstant.ReadData.FIELDS);
 
         // check and set content
         final String contentValue = content.get(0);
@@ -124,6 +136,11 @@ public final class ReadDataTransactionUtil {
             }
         }
 
+        // check and set fields
+        if (!fields.isEmpty()) {
+            builder.setFields(ParserFieldsParameter.parseFieldsParameter(identifier, fields.get(0)));
+        }
+
         return builder.build();
     }
 
index fd43a4a83473b09df6dd9e16d61622c55e693622..e8d1b327910e598a9f25a37d8e0ed850b048a831 100644 (file)
@@ -45,6 +45,7 @@ public final class RestconfDataServiceConstant {
         // URI parameters
         public static final String CONTENT = "content";
         public static final String DEPTH = "depth";
+        public static final String FIELDS = "fields";
 
         // content values
         public static final String CONFIG = "config";
diff --git a/restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/utils/parser/ParserFieldsParameter.java b/restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/utils/parser/ParserFieldsParameter.java
new file mode 100644 (file)
index 0000000..9b0270e
--- /dev/null
@@ -0,0 +1,265 @@
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. 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.utils.parser;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import org.opendaylight.netconf.sal.restconf.impl.InstanceIdentifierContext;
+import org.opendaylight.netconf.sal.restconf.impl.RestconfDocumentedException;
+import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorTag;
+import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorType;
+import org.opendaylight.restconf.utils.parser.builder.ParserBuilderConstants.Deserializer;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.common.QNameModule;
+import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
+import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+
+public class ParserFieldsParameter {
+    private static final char COLON = ':';
+    private static final char SEMICOLON = ';';
+    private static final char SLASH = '/';
+    private static final char STARTING_PARENTHESIS = '(';
+    private static final char CLOSING_PARENTHESIS = ')';
+
+    /**
+     * Parse fields parameter and return complete list of child nodes organized into levels.
+     * @param identifier identifier context created from request URI
+     * @param input input value of fields parameter
+     * @return {@link List}
+     */
+    public static @Nonnull List<Set<QName>> parseFieldsParameter(@Nonnull final InstanceIdentifierContext<?> identifier,
+                                                                 @Nonnull final String input) {
+        final List<Set<QName>> parsed = new ArrayList<>();
+        final SchemaContext context = identifier.getSchemaContext();
+        final QNameModule startQNameModule = identifier.getSchemaNode().getQName().getModule();
+        final DataSchemaContextNode<?> startNode = DataSchemaContextNode.fromDataSchemaNode(
+                (DataSchemaNode) identifier.getSchemaNode());
+
+        if (startNode == null) {
+            throw new RestconfDocumentedException(
+                    "Start node missing in " + input, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
+        }
+
+        parseInput(input, startQNameModule, startNode, parsed, context);
+        return parsed;
+    }
+
+    /**
+     * Parse input value of fields parameter and create list of sets. Each set represents one level of child nodes.
+     * @param input input value of fields parameter
+     * @param startQNameModule starting qname module
+     * @param startNode starting node
+     * @param parsed list of results
+     * @param context schema context
+     */
+    private static void parseInput(@Nonnull final String input,
+                                   @Nonnull final QNameModule startQNameModule,
+                                   @Nonnull final DataSchemaContextNode<?> startNode,
+                                   @Nonnull final List<Set<QName>> parsed,
+                                   @Nonnull final SchemaContext context) {
+        int currentPosition = 0;
+        int startPosition = 0;
+        DataSchemaContextNode<?> currentNode = startNode;
+        QNameModule currentQNameModule = startQNameModule;
+
+        Set<QName> currentLevel = new HashSet<>();
+        parsed.add(currentLevel);
+
+        while (currentPosition < input.length()) {
+            final char currentChar = input.charAt(currentPosition);
+
+            if (Deserializer.IDENTIFIER.matches(currentChar) || currentChar == '/') {
+                if (currentChar == SLASH) {
+                    // add parsed identifier to results for current level
+                    currentNode = addChildToResult(
+                            currentNode,
+                            input.substring(startPosition, currentPosition), currentQNameModule, currentLevel);
+                    // go one level down
+                    currentLevel = new HashSet<>();
+                    parsed.add(currentLevel);
+
+                    currentPosition++;
+                    startPosition = currentPosition;
+                } else {
+                    currentPosition++;
+                }
+
+                continue;
+            }
+
+            switch (currentChar) {
+                case COLON :
+                    // new namespace and revision found
+                    currentQNameModule = context.findModuleByName(
+                            input.substring(startPosition, currentPosition), null).getQNameModule();
+                    currentPosition++;
+                    break;
+                case STARTING_PARENTHESIS:
+                    // add current child to parsed results for current level
+                    final DataSchemaContextNode<?> child = addChildToResult(
+                            currentNode,
+                            input.substring(startPosition, currentPosition), currentQNameModule, currentLevel);
+                    // call with child node as new start node for one level down
+                    int closingParenthesis = currentPosition
+                            + findClosingParenthesis(input.substring(currentPosition + 1));
+                    parseInput(
+                            input.substring(currentPosition + 1, closingParenthesis),
+                            currentQNameModule,
+                            child,
+                            parsed,
+                            context);
+
+                    // closing parenthesis must be at the end of input or separator and one more character is expected
+                    currentPosition = closingParenthesis + 1;
+                    if (currentPosition != input.length()) {
+                        if (currentPosition + 1 < input.length()) {
+                            if (input.charAt(currentPosition) == SEMICOLON) {
+                                currentPosition++;
+                            } else {
+                                throw new RestconfDocumentedException(
+                                        "Missing semicolon character after "
+                                                + child.getIdentifier().getNodeType().getLocalName()
+                                                + " child nodes",
+                                        ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
+                            }
+                        } else {
+                            throw new RestconfDocumentedException(
+                                    "Unexpected character '"
+                                            + input.charAt(currentPosition)
+                                            + "' found in fields parameter value",
+                                    ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
+                        }
+                    }
+
+                    break;
+                case SEMICOLON:
+                    // complete identifier found
+                    addChildToResult(
+                            currentNode,
+                            input.substring(startPosition, currentPosition), currentQNameModule, currentLevel);
+                    currentPosition++;
+                    break;
+                default:
+                    throw new RestconfDocumentedException(
+                            "Unexpected character '" + currentChar + "' found in fields parameter value",
+                            ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
+            }
+
+            startPosition = currentPosition;
+        }
+
+        // parse input to end
+        if (startPosition < input.length()) {
+            addChildToResult(currentNode, input.substring(startPosition), currentQNameModule, currentLevel);
+        }
+    }
+
+    /**
+     * Add parsed child of current node to result for current level.
+     * @param currentNode current node
+     * @param identifier parsed identifier of child node
+     * @param currentQNameModule current namespace and revision in {@link QNameModule}
+     * @param level current nodes level
+     * @return {@link DataSchemaContextNode}
+     */
+    private static @Nonnull DataSchemaContextNode<?> addChildToResult(
+            @Nonnull final DataSchemaContextNode<?> currentNode,
+            @Nonnull final String identifier,
+            @Nonnull final QNameModule currentQNameModule,
+            @Nonnull final Set<QName> level) {
+        final QName childQName = QName.create(
+                currentQNameModule.getNamespace().toString(),
+                identifier,
+                currentQNameModule.getRevision());
+
+        // resolve parent node
+        final DataSchemaContextNode<?> parentNode = resolveMixinNode(
+                currentNode, level, currentNode.getIdentifier().getNodeType());
+        if (parentNode == null) {
+            throw new RestconfDocumentedException(
+                    "Not-mixin node missing in " + currentNode.getIdentifier().getNodeType().getLocalName(),
+                    ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
+        }
+
+        // resolve child node
+        final DataSchemaContextNode<?> childNode = resolveMixinNode(
+                parentNode.getChild(childQName), level, childQName);
+        if (childNode == null) {
+            throw new RestconfDocumentedException(
+                    "Child " + identifier + " node missing in "
+                            + currentNode.getIdentifier().getNodeType().getLocalName(),
+                    ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
+        }
+
+        // add final childNode node to level nodes
+        level.add(childNode.getIdentifier().getNodeType());
+        return childNode;
+    }
+
+    /**
+     * Resolve mixin node by searching for inner nodes until not mixin node or null is found.
+     * All nodes expect of not mixin node are added to current level nodes.
+     * @param node initial mixin or not-mixin node
+     * @param level current nodes level
+     * @param qName qname of initial node
+     * @return {@link DataSchemaContextNode}
+     */
+    private static @Nullable DataSchemaContextNode<?> resolveMixinNode(@Nullable final DataSchemaContextNode<?> node,
+                                                                       @Nonnull final Set<QName> level,
+                                                                       @Nonnull final QName qName) {
+        DataSchemaContextNode<?> currentNode = node;
+        while (currentNode != null && currentNode.isMixin()) {
+            level.add(qName);
+            currentNode = currentNode.getChild(qName);
+        }
+
+        return currentNode;
+    }
+
+    /**
+     * Find position of matching parenthesis increased by one, but at most equals to input size.
+     * @param input input where to find for closing parenthesis
+     * @return int position of closing parenthesis increased by one
+     */
+    private static int findClosingParenthesis(@Nonnull final String input) {
+        int position = 0;
+        int count = 1;
+
+        while (position < input.length()) {
+            final char currentChar = input.charAt(position);
+
+            if (currentChar == STARTING_PARENTHESIS) {
+                count++;
+            }
+
+            if (currentChar == CLOSING_PARENTHESIS) {
+                count--;
+            }
+
+            if (count == 0) {
+                break;
+            }
+
+            position++;
+        }
+
+        // closing parenthesis was not found
+        if (position >= input.length()) {
+            throw new RestconfDocumentedException("Missing closing parenthesis in fields parameter",
+                    ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
+        }
+
+        return ++position;
+    }
+}
diff --git a/restconf/sal-rest-connector/src/test/java/org/opendaylight/restconf/jersey/providers/ParameterAwareNormalizedNodeWriterDepthTest.java b/restconf/sal-rest-connector/src/test/java/org/opendaylight/restconf/jersey/providers/ParameterAwareNormalizedNodeWriterDepthTest.java
new file mode 100644 (file)
index 0000000..f26441f
--- /dev/null
@@ -0,0 +1,343 @@
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. 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.jersey.providers;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.Sets;
+import java.util.Collection;
+import java.util.Collections;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.opendaylight.yangtools.yang.common.QName;
+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.NodeIdentifierWithPredicates;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
+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.LeafSetEntryNode;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode;
+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.stream.NormalizedNodeStreamWriter;
+
+/**
+ * Unit test for {@link ParameterAwareNormalizedNodeWriter} used with depth parameter.
+ */
+public class ParameterAwareNormalizedNodeWriterDepthTest {
+
+    @Mock
+    private NormalizedNodeStreamWriter writer;
+    @Mock
+    private ContainerNode containerNodeData;
+    @Mock
+    private MapNode mapNodeData;
+    @Mock
+    private MapEntryNode mapEntryNodeData;
+    @Mock
+    private LeafSetNode<String> leafSetNodeData;
+    @Mock
+    private LeafSetEntryNode<String> leafSetEntryNodeData;
+    @Mock
+    private LeafNode<String> keyLeafNodeData;
+    @Mock
+    private LeafNode<String> anotherLeafNodeData;
+
+    private NodeIdentifier containerNodeIdentifier;
+    private NodeIdentifier mapNodeIdentifier;
+    private NodeIdentifierWithPredicates mapEntryNodeIdentifier;
+    private NodeIdentifier leafSetNodeIdentifier;
+    private NodeWithValue<?> leafSetEntryNodeIdentifier;
+    private NodeIdentifier keyLeafNodeIdentifier;
+    private NodeIdentifier anotherLeafNodeIdentifier;
+
+    private Collection<DataContainerChild<?, ?>> containerNodeValue;
+    private Collection<MapEntryNode> mapNodeValue;
+    private Collection<DataContainerChild<?, ?>> mapEntryNodeValue;
+    private Collection<LeafSetEntryNode<String>> leafSetNodeValue;
+    private String leafSetEntryNodeValue;
+    private String keyLeafNodeValue;
+    private String anotherLeafNodeValue;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        // identifiers
+        containerNodeIdentifier = NodeIdentifier.create(QName.create("namespace", "container"));
+        Mockito.when(containerNodeData.getIdentifier()).thenReturn(containerNodeIdentifier);
+        Mockito.when(containerNodeData.getNodeType()).thenReturn(containerNodeIdentifier.getNodeType());
+
+        mapNodeIdentifier = NodeIdentifier.create(QName.create("namespace", "list"));
+        Mockito.when(mapNodeData.getIdentifier()).thenReturn(mapNodeIdentifier);
+
+        final QName leafSetEntryNodeQName = QName.create("namespace", "leaf-set-entry");
+        leafSetEntryNodeValue = "leaf-set-value";
+        leafSetEntryNodeIdentifier = new NodeWithValue<>(leafSetEntryNodeQName, leafSetEntryNodeValue);
+        Mockito.when(leafSetEntryNodeData.getIdentifier()).thenReturn(leafSetEntryNodeIdentifier);
+        Mockito.when(leafSetEntryNodeData.getNodeType()).thenReturn(leafSetEntryNodeQName);
+
+        leafSetNodeIdentifier = NodeIdentifier.create(QName.create("namespace", "leaf-set"));
+        Mockito.when(leafSetNodeData.getIdentifier()).thenReturn(leafSetNodeIdentifier);
+
+        final QName mapEntryNodeKey = QName.create("namespace", "key-field");
+        keyLeafNodeIdentifier = NodeIdentifier.create(mapEntryNodeKey);
+        keyLeafNodeValue = "key-value";
+
+        mapEntryNodeIdentifier = new YangInstanceIdentifier.NodeIdentifierWithPredicates(
+                QName.create("namespace", "list-entry"), Collections.singletonMap(mapEntryNodeKey, keyLeafNodeValue));
+        Mockito.when(mapEntryNodeData.getIdentifier()).thenReturn(mapEntryNodeIdentifier);
+        Mockito.when(mapEntryNodeData.getChild(keyLeafNodeIdentifier)).thenReturn(Optional.of(keyLeafNodeData));
+
+        Mockito.when(keyLeafNodeData.getValue()).thenReturn(keyLeafNodeValue);
+        Mockito.when(keyLeafNodeData.getIdentifier()).thenReturn(keyLeafNodeIdentifier);
+
+        anotherLeafNodeIdentifier = NodeIdentifier.create(QName.create("namespace", "another-field"));
+        anotherLeafNodeValue = "another-value";
+
+        Mockito.when(anotherLeafNodeData.getValue()).thenReturn(anotherLeafNodeValue);
+        Mockito.when(anotherLeafNodeData.getIdentifier()).thenReturn(anotherLeafNodeIdentifier);
+
+        // values
+        Mockito.when(leafSetEntryNodeData.getValue()).thenReturn(leafSetEntryNodeValue);
+
+        leafSetNodeValue = Collections.singletonList(leafSetEntryNodeData);
+        Mockito.when(leafSetNodeData.getValue()).thenReturn(leafSetNodeValue);
+
+        containerNodeValue = Collections.singleton(leafSetNodeData);
+        Mockito.when(containerNodeData.getValue()).thenReturn(containerNodeValue);
+
+        mapEntryNodeValue = Sets.newHashSet(keyLeafNodeData, anotherLeafNodeData);
+        Mockito.when(mapEntryNodeData.getValue()).thenReturn(mapEntryNodeValue);
+
+        mapNodeValue = Collections.singleton(mapEntryNodeData);
+        Mockito.when(mapNodeData.getValue()).thenReturn(mapNodeValue);
+    }
+
+    /**
+     * Test write {@link ContainerNode} with children but write data only to depth 1 (children will not be written).
+     * Depth parameter is limited to 1.
+     */
+    @Test
+    public void writeContainerWithoutChildrenDepthTest() throws Exception {
+        final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter
+                .forStreamWriter(writer, 1, null);
+
+        parameterWriter.write(containerNodeData);
+
+        final InOrder inOrder = Mockito.inOrder(writer);
+        inOrder.verify(writer, Mockito.times(1)).startContainerNode(containerNodeIdentifier, containerNodeValue.size());
+        inOrder.verify(writer, Mockito.times(1)).endNode();
+        Mockito.verifyNoMoreInteractions(writer);
+    }
+
+    /**
+     * Test write {@link ContainerNode} with children and write also all its children.
+     * Depth parameter has higher value than maximal children depth.
+     */
+    @Test
+    public void writeContainerWithChildrenDepthTest() throws Exception {
+        final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+                writer, Integer.MAX_VALUE, null);
+
+        parameterWriter.write(containerNodeData);
+
+        final InOrder inOrder = Mockito.inOrder(writer);
+        inOrder.verify(writer, Mockito.times(1)).startContainerNode(containerNodeIdentifier, containerNodeValue.size());
+        inOrder.verify(writer, Mockito.times(1)).startLeafSet(leafSetNodeIdentifier, leafSetNodeValue.size());
+        inOrder.verify(writer, Mockito.times(1)).leafSetEntryNode(
+                leafSetEntryNodeIdentifier.getNodeType(), leafSetEntryNodeValue);
+        inOrder.verify(writer, Mockito.times(2)).endNode();
+        Mockito.verifyNoMoreInteractions(writer);
+    }
+
+    /**
+     * Test write with {@link MapNode} with children but write data only to depth 1 (children will not be written).
+     * Depth parameter limits depth to 1.
+     */
+    @Test
+    public void writeMapNodeWithoutChildrenDepthTest() throws Exception {
+        final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter
+                .forStreamWriter(writer, 1, null);
+
+        parameterWriter.write(mapNodeData);
+
+        final InOrder inOrder = Mockito.inOrder(writer);
+        inOrder.verify(writer, Mockito.times(1)).startMapNode(mapNodeIdentifier, mapNodeValue.size());
+        inOrder.verify(writer, Mockito.times(1)).startMapEntryNode(mapEntryNodeIdentifier, mapEntryNodeValue.size());
+        inOrder.verify(writer, Mockito.times(2)).endNode();
+        Mockito.verifyNoMoreInteractions(writer);
+    }
+
+    /**
+     * Test write {@link MapNode} with children and write also all its children.
+     * Depth parameter has higher value than maximal children depth.
+     *
+     *
+     * FIXME
+     * Although ordered writer is used leaves are not written in expected order.
+     *
+     */
+    @Ignore
+    @Test
+    public void writeMapNodeWithChildrenDepthTest() throws Exception {
+        final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+                writer, Integer.MAX_VALUE, null);
+
+        parameterWriter.write(mapNodeData);
+
+        final InOrder inOrder = Mockito.inOrder(writer);
+        inOrder.verify(writer, Mockito.times(1)).startMapNode(mapNodeIdentifier, mapNodeValue.size());
+        inOrder.verify(writer, Mockito.times(1)).startMapEntryNode(mapEntryNodeIdentifier, mapEntryNodeValue.size());
+        inOrder.verify(writer, Mockito.times(2)).leafNode(keyLeafNodeIdentifier, keyLeafNodeValue);
+        // FIXME this assertion is not working because leaves are not written in expected order
+        inOrder.verify(writer, Mockito.times(1)).leafNode(anotherLeafNodeIdentifier, anotherLeafNodeValue);
+        inOrder.verify(writer, Mockito.times(2)).endNode();
+        Mockito.verifyNoMoreInteractions(writer);
+    }
+
+    /**
+     * Test write with {@link LeafSetNode} with depth 1 (children will not be written).
+     * Depth parameter limits depth to 1.
+     */
+    @Test
+    public void writeLeafSetNodeWithoutChildrenDepthTest() throws Exception {
+        final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+                writer, 1, null);
+
+        parameterWriter.write(leafSetNodeData);
+
+        final InOrder inOrder = Mockito.inOrder(writer);
+        inOrder.verify(writer, Mockito.times(1)).startLeafSet(leafSetNodeIdentifier, leafSetNodeValue.size());
+        inOrder.verify(writer, Mockito.times(1)).endNode();
+        Mockito.verifyNoMoreInteractions(writer);
+    }
+
+    /**
+     * Test write with {@link LeafSetNode} when all its children will be written.
+     * Depth parameter has higher value than maximal children depth.
+     */
+    @Test
+    public void writeLeafSetNodeWithChildrenDepthTest() throws Exception {
+        final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+                writer, Integer.MAX_VALUE, null);
+
+        parameterWriter.write(leafSetNodeData);
+
+        final InOrder inOrder = Mockito.inOrder(writer);
+        inOrder.verify(writer, Mockito.times(1)).startLeafSet(leafSetNodeIdentifier, leafSetNodeValue.size());
+        inOrder.verify(writer, Mockito.times(1)).leafSetEntryNode(
+                leafSetEntryNodeIdentifier.getNodeType(), leafSetEntryNodeValue);
+        inOrder.verify(writer, Mockito.times(1)).endNode();
+        Mockito.verifyNoMoreInteractions(writer);
+    }
+
+    /**
+     * Test write with {@link LeafSetEntryNode}.
+     * Depth parameter has higher value than maximal children depth.
+     */
+    @Test
+    public void writeLeafSetEntryNodeDepthTest() throws Exception {
+        final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+                writer, Integer.MAX_VALUE, null);
+
+        parameterWriter.write(leafSetEntryNodeData);
+
+        final InOrder inOrder = Mockito.inOrder(writer);
+        inOrder.verify(writer, Mockito.times(1)).leafSetEntryNode(
+                leafSetEntryNodeIdentifier.getNodeType(), leafSetEntryNodeValue);
+        Mockito.verifyNoMoreInteractions(writer);
+    }
+
+    /**
+     * Test write with {@link MapEntryNode} unordered to depth 1 to write only keys.
+     * Depth parameter limits depth to 1.
+     */
+    @Test
+    public void writeMapEntryNodeUnorderedOnlyKeysDepthTest() throws Exception {
+        final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+                writer, false, 1, null);
+
+        parameterWriter.write(mapEntryNodeData);
+
+        final InOrder inOrder = Mockito.inOrder(writer);
+        inOrder.verify(writer, Mockito.times(1)).startMapEntryNode(mapEntryNodeIdentifier, mapEntryNodeValue.size());
+        // write only the key
+        inOrder.verify(writer, Mockito.times(1)).leafNode(keyLeafNodeIdentifier, keyLeafNodeValue);
+        inOrder.verify(writer, Mockito.times(1)).endNode();
+        Mockito.verifyNoMoreInteractions(writer);
+    }
+
+    /**
+     * Test write with {@link MapEntryNode} unordered with full depth.
+     * Depth parameter has higher value than maximal children depth.
+     */
+    @Test
+    public void writeMapEntryNodeUnorderedDepthTest() throws Exception {
+        final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+                writer, false, Integer.MAX_VALUE, null);
+
+        parameterWriter.write(mapEntryNodeData);
+
+        // unordered
+        Mockito.verify(writer, Mockito.times(1)).startMapEntryNode(mapEntryNodeIdentifier, mapEntryNodeValue.size());
+        Mockito.verify(writer, Mockito.times(1)).leafNode(keyLeafNodeIdentifier, keyLeafNodeValue);
+        Mockito.verify(writer, Mockito.times(1)).leafNode(anotherLeafNodeIdentifier, anotherLeafNodeValue);
+        Mockito.verify(writer, Mockito.times(1)).endNode();
+        Mockito.verifyNoMoreInteractions(writer);
+    }
+
+    /**
+     * Test write with {@link MapEntryNode} ordered with depth 1 (children will not be written).
+     * Depth parameter limits depth to 1.
+     */
+    @Test
+    public void writeMapEntryNodeOrderedWithoutChildrenTest() throws Exception {
+        final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+                writer, true, 1, null);
+
+        parameterWriter.write(mapEntryNodeData);
+
+        final InOrder inOrder = Mockito.inOrder(writer);
+        inOrder.verify(writer, Mockito.times(1)).startMapEntryNode(mapEntryNodeIdentifier, mapEntryNodeValue.size());
+        inOrder.verify(writer, Mockito.times(1)).endNode();
+        Mockito.verifyNoMoreInteractions(writer);
+    }
+
+    /**
+     * Test write with {@link MapEntryNode} ordered and write also all its children.
+     * Depth parameter has higher value than maximal children depth.
+     *
+     * FIXME
+     * Although ordered writer is used leaves are not written in expected order.
+     *
+     */
+    @Ignore
+    @Test
+    public void writeMapEntryNodeOrderedTest() throws Exception {
+        final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+                writer, true, Integer.MAX_VALUE, null);
+
+        parameterWriter.write(mapEntryNodeData);
+
+        final InOrder inOrder = Mockito.inOrder(writer);
+        inOrder.verify(writer, Mockito.times(1)).startMapEntryNode(mapEntryNodeIdentifier, mapEntryNodeValue.size());
+        inOrder.verify(writer, Mockito.times(2)).leafNode(keyLeafNodeIdentifier, keyLeafNodeValue);
+        // FIXME this assertion is not working because leaves are not written in expected order
+        inOrder.verify(writer, Mockito.times(1)).leafNode(anotherLeafNodeIdentifier, anotherLeafNodeValue);
+        inOrder.verify(writer, Mockito.times(1)).endNode();
+        Mockito.verifyNoMoreInteractions(writer);
+    }
+}
\ No newline at end of file
diff --git a/restconf/sal-rest-connector/src/test/java/org/opendaylight/restconf/jersey/providers/ParameterAwareNormalizedNodeWriterFieldsTest.java b/restconf/sal-rest-connector/src/test/java/org/opendaylight/restconf/jersey/providers/ParameterAwareNormalizedNodeWriterFieldsTest.java
new file mode 100644 (file)
index 0000000..b6c2da7
--- /dev/null
@@ -0,0 +1,210 @@
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. 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.jersey.providers;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.Sets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.opendaylight.yangtools.yang.common.QName;
+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.schema.ContainerNode;
+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.LeafSetEntryNode;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode;
+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.stream.NormalizedNodeStreamWriter;
+
+/**
+ * Unit test for {@link ParameterAwareNormalizedNodeWriter} used with fields parameter.
+ */
+public class ParameterAwareNormalizedNodeWriterFieldsTest {
+
+    @Mock
+    private NormalizedNodeStreamWriter writer;
+    @Mock
+    private ContainerNode containerNodeData;
+    @Mock
+    private MapNode mapNodeData;
+    @Mock
+    private MapEntryNode mapEntryNodeData;
+    @Mock
+    private LeafSetNode<String> leafSetNodeData;
+    @Mock
+    private LeafSetEntryNode<String> leafSetEntryNodeData;
+    @Mock
+    private LeafNode<String> keyLeafNodeData;
+
+    private NodeIdentifier containerNodeIdentifier;
+    private NodeIdentifier mapNodeIdentifier;
+    private NodeIdentifierWithPredicates mapEntryNodeIdentifier;
+    private NodeIdentifier leafSetNodeIdentifier;
+    private NodeWithValue<?> leafSetEntryNodeIdentifier;
+    private NodeIdentifier keyLeafNodeIdentifier;
+
+    private Collection<DataContainerChild<?, ?>> containerNodeValue;
+    private Collection<MapEntryNode> mapNodeValue;
+    private Collection<DataContainerChild<?, ?>> mapEntryNodeValue;
+    private Collection<LeafSetEntryNode<String>> leafSetNodeValue;
+    private String leafSetEntryNodeValue;
+    private String keyLeafNodeValue;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        // identifiers
+        containerNodeIdentifier = NodeIdentifier.create(QName.create("namespace", "container"));
+        Mockito.when(containerNodeData.getIdentifier()).thenReturn(containerNodeIdentifier);
+        Mockito.when(containerNodeData.getNodeType()).thenReturn(containerNodeIdentifier.getNodeType());
+
+        mapNodeIdentifier = NodeIdentifier.create(QName.create("namespace", "list"));
+        Mockito.when(mapNodeData.getIdentifier()).thenReturn(mapNodeIdentifier);
+        Mockito.when(mapNodeData.getNodeType()).thenReturn(mapNodeIdentifier.getNodeType());
+
+        final QName leafSetEntryNodeQName = QName.create("namespace", "leaf-set-entry");
+        leafSetEntryNodeValue = "leaf-set-value";
+        leafSetEntryNodeIdentifier = new NodeWithValue<>(leafSetEntryNodeQName, leafSetEntryNodeValue);
+        Mockito.when(leafSetEntryNodeData.getIdentifier()).thenReturn(leafSetEntryNodeIdentifier);
+        Mockito.when(leafSetEntryNodeData.getNodeType()).thenReturn(leafSetEntryNodeIdentifier.getNodeType());
+        Mockito.when(leafSetEntryNodeData.getNodeType()).thenReturn(leafSetEntryNodeQName);
+
+        leafSetNodeIdentifier = NodeIdentifier.create(QName.create("namespace", "leaf-set"));
+        Mockito.when(leafSetNodeData.getIdentifier()).thenReturn(leafSetNodeIdentifier);
+        Mockito.when(leafSetNodeData.getNodeType()).thenReturn(leafSetNodeIdentifier.getNodeType());
+
+        final QName mapEntryNodeKey = QName.create("namespace", "key-field");
+        keyLeafNodeIdentifier = NodeIdentifier.create(mapEntryNodeKey);
+        keyLeafNodeValue = "key-value";
+
+        mapEntryNodeIdentifier = new NodeIdentifierWithPredicates(
+                QName.create("namespace", "list-entry"), Collections.singletonMap(mapEntryNodeKey, keyLeafNodeValue));
+        Mockito.when(mapEntryNodeData.getIdentifier()).thenReturn(mapEntryNodeIdentifier);
+        Mockito.when(mapEntryNodeData.getNodeType()).thenReturn(mapEntryNodeIdentifier.getNodeType());
+        Mockito.when(mapEntryNodeData.getChild(keyLeafNodeIdentifier)).thenReturn(Optional.of(keyLeafNodeData));
+
+        Mockito.when(keyLeafNodeData.getValue()).thenReturn(keyLeafNodeValue);
+        Mockito.when(keyLeafNodeData.getIdentifier()).thenReturn(keyLeafNodeIdentifier);
+        Mockito.when(keyLeafNodeData.getNodeType()).thenReturn(keyLeafNodeIdentifier.getNodeType());
+
+        // values
+        Mockito.when(leafSetEntryNodeData.getValue()).thenReturn(leafSetEntryNodeValue);
+
+        leafSetNodeValue = Collections.singletonList(leafSetEntryNodeData);
+        Mockito.when(leafSetNodeData.getValue()).thenReturn(leafSetNodeValue);
+
+        containerNodeValue = Collections.singleton(leafSetNodeData);
+        Mockito.when(containerNodeData.getValue()).thenReturn(containerNodeValue);
+
+        mapEntryNodeValue = Sets.newHashSet(keyLeafNodeData);
+        Mockito.when(mapEntryNodeData.getValue()).thenReturn(mapEntryNodeValue);
+
+        mapNodeValue = Collections.singleton(mapEntryNodeData);
+        Mockito.when(mapNodeData.getValue()).thenReturn(mapNodeValue);
+    }
+
+    /**
+     * Test write {@link ContainerNode} when children which will be written are limited.
+     * Fields parameter selects 0/1 of container children to be written.
+     */
+    @Test
+    public void writeContainerWithLimitedFieldsTest() throws Exception {
+        final List<Set<QName>> limitedFields = new ArrayList<>();
+        limitedFields.add(Sets.newHashSet());
+
+        final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+                writer, null, limitedFields);
+
+        parameterWriter.write(containerNodeData);
+
+        final InOrder inOrder = Mockito.inOrder(writer);
+        inOrder.verify(writer, Mockito.times(1)).startContainerNode(containerNodeIdentifier, containerNodeValue.size());
+        inOrder.verify(writer, Mockito.times(1)).endNode();
+        Mockito.verifyNoMoreInteractions(writer);
+    }
+
+    /**
+     * Test write {@link ContainerNode} when all its children are selected to be written.
+     * Fields parameter selects 1/1 of container children to be written.
+     */
+    @Test
+    public void writeContainerAllFieldsTest() throws Exception {
+        final List<Set<QName>> limitedFields = new ArrayList<>();
+        limitedFields.add(Sets.newHashSet(leafSetNodeIdentifier.getNodeType()));
+
+        final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+                writer, null, limitedFields);
+
+        parameterWriter.write(containerNodeData);
+
+        final InOrder inOrder = Mockito.inOrder(writer);
+        inOrder.verify(writer, Mockito.times(1)).startContainerNode(containerNodeIdentifier, containerNodeValue.size());
+        inOrder.verify(writer, Mockito.times(1)).startLeafSet(leafSetNodeIdentifier, leafSetNodeValue.size());
+        inOrder.verify(writer, Mockito.times(1)).leafSetEntryNode(
+                leafSetEntryNodeIdentifier.getNodeType(), leafSetEntryNodeValue);
+        inOrder.verify(writer, Mockito.times(2)).endNode();
+        Mockito.verifyNoMoreInteractions(writer);
+    }
+
+    /**
+     * Test write {@link MapEntryNode} as child of {@link MapNode} when children which will be written are limited.
+     * Fields parameter selects 0/1 of map entry node children to be written.
+     */
+    @Test
+    public void writeMapEntryNodeWithLimitedFieldsTest() throws Exception {
+        final List<Set<QName>> limitedFields = new ArrayList<>();
+        limitedFields.add(Sets.newHashSet());
+
+        final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+                writer, null, limitedFields);
+
+        parameterWriter.write(mapNodeData);
+
+        final InOrder inOrder = Mockito.inOrder(writer);
+        inOrder.verify(writer, Mockito.times(1)).startMapNode(mapNodeIdentifier, mapNodeValue.size());
+        inOrder.verify(writer, Mockito.times(1)).startMapEntryNode(mapEntryNodeIdentifier, mapEntryNodeValue.size());
+        inOrder.verify(writer, Mockito.times(2)).endNode();
+        Mockito.verifyNoMoreInteractions(writer);
+    }
+
+    /**
+     * Test write {@link MapEntryNode} as child of {@link MapNode} when all its children will be written.
+     * Fields parameter selects 1/1 of map entry node children to be written.
+     */
+    @Test
+    public void writeMapNodeAllFieldsTest() throws Exception {
+        final List<Set<QName>> limitedFields = new ArrayList<>();
+        limitedFields.add(Sets.newHashSet(keyLeafNodeData.getNodeType()));
+
+        final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+                writer, null, limitedFields);
+
+        parameterWriter.write(mapNodeData);
+
+        final InOrder inOrder = Mockito.inOrder(writer);
+        inOrder.verify(writer, Mockito.times(1)).startMapNode(mapNodeIdentifier, mapNodeValue.size());
+        inOrder.verify(writer, Mockito.times(1)).startMapEntryNode(mapEntryNodeIdentifier, mapEntryNodeValue.size());
+        inOrder.verify(writer, Mockito.times(1)).leafNode(keyLeafNodeIdentifier, keyLeafNodeValue);
+        inOrder.verify(writer, Mockito.times(2)).endNode();
+        Mockito.verifyNoMoreInteractions(writer);
+    }
+}
\ No newline at end of file
diff --git a/restconf/sal-rest-connector/src/test/java/org/opendaylight/restconf/jersey/providers/ParameterAwareNormalizedNodeWriterParametersTest.java b/restconf/sal-rest-connector/src/test/java/org/opendaylight/restconf/jersey/providers/ParameterAwareNormalizedNodeWriterParametersTest.java
new file mode 100644 (file)
index 0000000..9604fcd
--- /dev/null
@@ -0,0 +1,141 @@
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. 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.jersey.providers;
+
+import com.google.common.collect.Sets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
+import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafSetEntryNode;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
+
+/**
+ * Unit test for {@link ParameterAwareNormalizedNodeWriter} used with all parameters.
+ */
+public class ParameterAwareNormalizedNodeWriterParametersTest {
+
+    @Mock
+    private NormalizedNodeStreamWriter writer;
+    @Mock
+    private ContainerNode containerNodeData;
+    @Mock
+    private LeafSetNode<String> leafSetNodeData;
+    @Mock
+    private LeafSetEntryNode<String> leafSetEntryNodeData;
+    @Mock
+    private ContainerNode rootDataContainerData;
+
+    private NodeIdentifier containerNodeIdentifier;
+    private NodeIdentifier leafSetNodeIdentifier;
+    private NodeWithValue<?> leafSetEntryNodeIdentifier;
+    private NodeIdentifier rootDataContainerIdentifier;
+
+    private Collection<DataContainerChild<?, ?>> containerNodeValue;
+    private Collection<LeafSetEntryNode<String>> leafSetNodeValue;
+    private String leafSetEntryNodeValue;
+    private Collection<DataContainerChild<?, ?>> rootDataContainerValue;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        // identifiers
+        containerNodeIdentifier = NodeIdentifier.create(QName.create("namespace", "container"));
+        Mockito.when(containerNodeData.getIdentifier()).thenReturn(containerNodeIdentifier);
+        Mockito.when(containerNodeData.getNodeType()).thenReturn(containerNodeIdentifier.getNodeType());
+
+        final QName leafSetEntryNodeQName = QName.create("namespace", "leaf-set-entry");
+        leafSetEntryNodeValue = "leaf-set-value";
+        leafSetEntryNodeIdentifier = new NodeWithValue<>(leafSetEntryNodeQName, leafSetEntryNodeValue);
+        Mockito.when(leafSetEntryNodeData.getIdentifier()).thenReturn(leafSetEntryNodeIdentifier);
+        Mockito.when(leafSetEntryNodeData.getNodeType()).thenReturn(leafSetEntryNodeIdentifier.getNodeType());
+        Mockito.when(leafSetEntryNodeData.getNodeType()).thenReturn(leafSetEntryNodeQName);
+
+        leafSetNodeIdentifier = NodeIdentifier.create(QName.create("namespace", "leaf-set"));
+        Mockito.when(leafSetNodeData.getIdentifier()).thenReturn(leafSetNodeIdentifier);
+        Mockito.when(leafSetNodeData.getNodeType()).thenReturn(leafSetNodeIdentifier.getNodeType());
+
+        rootDataContainerIdentifier = NodeIdentifier.create(
+                QName.create("urn:ietf:params:xml:ns:netconf:base:1.0", "data"));
+        Mockito.when(rootDataContainerData.getIdentifier()).thenReturn(rootDataContainerIdentifier);
+        Mockito.when(rootDataContainerData.getNodeType()).thenReturn(rootDataContainerIdentifier.getNodeType());
+
+        // values
+        Mockito.when(leafSetEntryNodeData.getValue()).thenReturn(leafSetEntryNodeValue);
+
+        leafSetNodeValue = Collections.singletonList(leafSetEntryNodeData);
+        Mockito.when(leafSetNodeData.getValue()).thenReturn(leafSetNodeValue);
+
+        containerNodeValue = Collections.singleton(leafSetNodeData);
+        Mockito.when(containerNodeData.getValue()).thenReturn(containerNodeValue);
+
+        rootDataContainerValue = Collections.singleton(leafSetNodeData);
+        Mockito.when(rootDataContainerData.getValue()).thenReturn(rootDataContainerValue);
+    }
+
+    /**
+     * Test write {@link ContainerNode} when all its children are selected to be written by fields parameter.
+     * Depth parameter is also used and limits output to depth 1.
+     * Fields parameter has effect limiting depth parameter in the way that selected nodes and its ancestors are
+     * written regardless of their depth (some of container children have depth > 1).
+     * Fields parameter selects all container children to be written and also all children of those children.
+     */
+    @Test
+    public void writeContainerParameterPrioritiesTest() throws Exception {
+        final List<Set<QName>> limitedFields = new ArrayList<>();
+        limitedFields.add(Sets.newHashSet(leafSetNodeIdentifier.getNodeType()));
+        limitedFields.add(Sets.newHashSet(leafSetEntryNodeIdentifier.getNodeType()));
+
+        final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+                writer, 1, limitedFields);
+
+        parameterWriter.write(containerNodeData);
+
+        final InOrder inOrder = Mockito.inOrder(writer);
+        inOrder.verify(writer, Mockito.times(1)).startContainerNode(containerNodeIdentifier, containerNodeValue.size());
+        inOrder.verify(writer, Mockito.times(1)).startLeafSet(leafSetNodeIdentifier, leafSetNodeValue.size());
+        inOrder.verify(writer, Mockito.times(1)).leafSetEntryNode(
+                leafSetEntryNodeIdentifier.getNodeType(), leafSetEntryNodeValue);
+        inOrder.verify(writer, Mockito.times(2)).endNode();
+        Mockito.verifyNoMoreInteractions(writer);
+    }
+
+    /**
+     * Test write {@link ContainerNode} which represents data at restconf/data root.
+     * No parameters are used.
+     */
+    @Test
+    public void writeRootDataTest() throws Exception {
+        final ParameterAwareNormalizedNodeWriter parameterWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+                writer, null, null);
+
+        parameterWriter.write(rootDataContainerData);
+
+        final InOrder inOrder = Mockito.inOrder(writer);
+        inOrder.verify(writer, Mockito.times(1)).startLeafSet(leafSetNodeIdentifier, leafSetNodeValue.size());
+        inOrder.verify(writer, Mockito.times(1)).leafSetEntryNode(
+                leafSetEntryNodeIdentifier.getNodeType(), leafSetEntryNodeValue);
+        inOrder.verify(writer, Mockito.times(1)).endNode();
+        Mockito.verifyNoMoreInteractions(writer);
+    }
+}
\ No newline at end of file
index acc543b24e7d8559388f93dd44bd70048b2c9e7a..9713d4f929316b91b235985ad56458ad7ed14811 100644 (file)
@@ -9,9 +9,8 @@
 package org.opendaylight.restconf.restful.utils;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.when;
@@ -41,122 +40,140 @@ import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
 import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
+import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.SchemaContext;
 
 public class ReadDataTransactionUtilTest {
 
-    private static final TestData data = new TestData();
-    private static final YangInstanceIdentifier.NodeIdentifier nodeIdentifier = new YangInstanceIdentifier
+    private static final TestData DATA = new TestData();
+    private static final YangInstanceIdentifier.NodeIdentifier NODE_IDENTIFIER = new YangInstanceIdentifier
             .NodeIdentifier(QName.create("ns", "2016-02-28", "container"));
 
     private TransactionVarsWrapper wrapper;
     @Mock
     private DOMTransactionChain transactionChain;
     @Mock
-    private InstanceIdentifierContext<?> context;
+    private InstanceIdentifierContext<ContainerSchemaNode> context;
     @Mock
     private DOMDataReadOnlyTransaction read;
+    @Mock
+    private SchemaContext schemaContext;
+    @Mock
+    private ContainerSchemaNode containerSchemaNode;
+    @Mock
+    private LeafSchemaNode containerChildNode;
+    private QName containerChildQName;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
 
-        doReturn(read).when(transactionChain).newReadOnlyTransaction();
+        containerChildQName = QName.create("ns", "2016-02-28", "container-child");
+
+        when(transactionChain.newReadOnlyTransaction()).thenReturn(read);
+        when(context.getSchemaContext()).thenReturn(schemaContext);
+        when(context.getSchemaNode()).thenReturn(containerSchemaNode);
+        when(containerSchemaNode.getQName()).thenReturn(NODE_IDENTIFIER.getNodeType());
+        when(containerChildNode.getQName()).thenReturn(containerChildQName);
+        when(containerSchemaNode.getDataChildByName(containerChildQName)).thenReturn(containerChildNode);
+
         wrapper = new TransactionVarsWrapper(this.context, null, this.transactionChain);
     }
 
     @Test
     public void readDataConfigTest() {
-        doReturn(Futures.immediateCheckedFuture(Optional.of(data.data3))).when(read)
-                .read(LogicalDatastoreType.CONFIGURATION, data.path);
-        doReturn(data.path).when(context).getInstanceIdentifier();
+        doReturn(Futures.immediateCheckedFuture(Optional.of(DATA.data3))).when(read)
+                .read(LogicalDatastoreType.CONFIGURATION, DATA.path);
+        doReturn(DATA.path).when(context).getInstanceIdentifier();
         final String valueOfContent = RestconfDataServiceConstant.ReadData.CONFIG;
         final NormalizedNode<?, ?> normalizedNode = ReadDataTransactionUtil.readData(valueOfContent, wrapper);
-        assertEquals(data.data3, normalizedNode);
+        assertEquals(DATA.data3, normalizedNode);
     }
 
     @Test
     public void readAllHavingOnlyConfigTest() {
-        doReturn(Futures.immediateCheckedFuture(Optional.of(data.data3))).when(read)
-                .read(LogicalDatastoreType.CONFIGURATION, data.path);
+        doReturn(Futures.immediateCheckedFuture(Optional.of(DATA.data3))).when(read)
+                .read(LogicalDatastoreType.CONFIGURATION, DATA.path);
         doReturn(Futures.immediateCheckedFuture(Optional.absent())).when(read)
-                .read(LogicalDatastoreType.OPERATIONAL, data.path);
-        doReturn(data.path).when(context).getInstanceIdentifier();
+                .read(LogicalDatastoreType.OPERATIONAL, DATA.path);
+        doReturn(DATA.path).when(context).getInstanceIdentifier();
         final String valueOfContent = RestconfDataServiceConstant.ReadData.ALL;
         final NormalizedNode<?, ?> normalizedNode = ReadDataTransactionUtil.readData(valueOfContent, wrapper);
-        assertEquals(data.data3, normalizedNode);
+        assertEquals(DATA.data3, normalizedNode);
     }
 
     @Test
     public void readAllHavingOnlyNonConfigTest() {
-        doReturn(Futures.immediateCheckedFuture(Optional.of(data.data2))).when(read)
-                .read(LogicalDatastoreType.OPERATIONAL, data.path2);
+        doReturn(Futures.immediateCheckedFuture(Optional.of(DATA.data2))).when(read)
+                .read(LogicalDatastoreType.OPERATIONAL, DATA.path2);
         doReturn(Futures.immediateCheckedFuture(Optional.absent())).when(read)
-                .read(LogicalDatastoreType.CONFIGURATION, data.path2);
-        doReturn(data.path2).when(context).getInstanceIdentifier();
+                .read(LogicalDatastoreType.CONFIGURATION, DATA.path2);
+        doReturn(DATA.path2).when(context).getInstanceIdentifier();
         final String valueOfContent = RestconfDataServiceConstant.ReadData.ALL;
         final NormalizedNode<?, ?> normalizedNode = ReadDataTransactionUtil.readData(valueOfContent, wrapper);
-        assertEquals(data.data2, normalizedNode);
+        assertEquals(DATA.data2, normalizedNode);
     }
 
     @Test
     public void readDataNonConfigTest() {
-        doReturn(Futures.immediateCheckedFuture(Optional.of(data.data2))).when(read)
-                .read(LogicalDatastoreType.OPERATIONAL, data.path2);
-        doReturn(data.path2).when(context).getInstanceIdentifier();
+        doReturn(Futures.immediateCheckedFuture(Optional.of(DATA.data2))).when(read)
+                .read(LogicalDatastoreType.OPERATIONAL, DATA.path2);
+        doReturn(DATA.path2).when(context).getInstanceIdentifier();
         final String valueOfContent = RestconfDataServiceConstant.ReadData.NONCONFIG;
         final NormalizedNode<?, ?> normalizedNode = ReadDataTransactionUtil.readData(valueOfContent, wrapper);
-        assertEquals(data.data2, normalizedNode);
+        assertEquals(DATA.data2, normalizedNode);
     }
 
     @Test
     public void readContainerDataAllTest() {
-        doReturn(Futures.immediateCheckedFuture(Optional.of(data.data3))).when(read)
-                .read(LogicalDatastoreType.CONFIGURATION, data.path);
-        doReturn(Futures.immediateCheckedFuture(Optional.of(data.data4))).when(read)
-                .read(LogicalDatastoreType.OPERATIONAL, data.path);
-        doReturn(data.path).when(context).getInstanceIdentifier();
+        doReturn(Futures.immediateCheckedFuture(Optional.of(DATA.data3))).when(read)
+                .read(LogicalDatastoreType.CONFIGURATION, DATA.path);
+        doReturn(Futures.immediateCheckedFuture(Optional.of(DATA.data4))).when(read)
+                .read(LogicalDatastoreType.OPERATIONAL, DATA.path);
+        doReturn(DATA.path).when(context).getInstanceIdentifier();
         final String valueOfContent = RestconfDataServiceConstant.ReadData.ALL;
         final NormalizedNode<?, ?> normalizedNode = ReadDataTransactionUtil.readData(valueOfContent, wrapper);
         final ContainerNode checkingData = Builders
                 .containerBuilder()
-                .withNodeIdentifier(nodeIdentifier)
-                .withChild(data.contentLeaf)
-                .withChild(data.contentLeaf2)
+                .withNodeIdentifier(NODE_IDENTIFIER)
+                .withChild(DATA.contentLeaf)
+                .withChild(DATA.contentLeaf2)
                 .build();
         assertEquals(checkingData, normalizedNode);
     }
 
     @Test
     public void readContainerDataConfigNoValueOfContentTest() {
-        doReturn(Futures.immediateCheckedFuture(Optional.of(data.data3))).when(read)
-                .read(LogicalDatastoreType.CONFIGURATION, data.path);
-        doReturn(Futures.immediateCheckedFuture(Optional.of(data.data4))).when(read)
-                .read(LogicalDatastoreType.OPERATIONAL, data.path);
-        doReturn(data.path).when(context).getInstanceIdentifier();
+        doReturn(Futures.immediateCheckedFuture(Optional.of(DATA.data3))).when(read)
+                .read(LogicalDatastoreType.CONFIGURATION, DATA.path);
+        doReturn(Futures.immediateCheckedFuture(Optional.of(DATA.data4))).when(read)
+                .read(LogicalDatastoreType.OPERATIONAL, DATA.path);
+        doReturn(DATA.path).when(context).getInstanceIdentifier();
         final NormalizedNode<?, ?> normalizedNode = ReadDataTransactionUtil.readData(
                 RestconfDataServiceConstant.ReadData.ALL, wrapper);
         final ContainerNode checkingData = Builders
                 .containerBuilder()
-                .withNodeIdentifier(nodeIdentifier)
-                .withChild(data.contentLeaf)
-                .withChild(data.contentLeaf2)
+                .withNodeIdentifier(NODE_IDENTIFIER)
+                .withChild(DATA.contentLeaf)
+                .withChild(DATA.contentLeaf2)
                 .build();
         assertEquals(checkingData, normalizedNode);
     }
 
     @Test
     public void readListDataAllTest() {
-        doReturn(Futures.immediateCheckedFuture(Optional.of(data.listData))).when(read)
-                .read(LogicalDatastoreType.OPERATIONAL, data.path3);
-        doReturn(Futures.immediateCheckedFuture(Optional.of(data.listData2))).when(read)
-                .read(LogicalDatastoreType.CONFIGURATION, data.path3);
-        doReturn(data.path3).when(context).getInstanceIdentifier();
+        doReturn(Futures.immediateCheckedFuture(Optional.of(DATA.listData))).when(read)
+                .read(LogicalDatastoreType.OPERATIONAL, DATA.path3);
+        doReturn(Futures.immediateCheckedFuture(Optional.of(DATA.listData2))).when(read)
+                .read(LogicalDatastoreType.CONFIGURATION, DATA.path3);
+        doReturn(DATA.path3).when(context).getInstanceIdentifier();
         final String valueOfContent = RestconfDataServiceConstant.ReadData.ALL;
         final NormalizedNode<?, ?> normalizedNode = ReadDataTransactionUtil.readData(valueOfContent, wrapper);
         final MapNode checkingData = Builders
                 .mapBuilder()
                 .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(QName.create("ns", "2016-02-28", "list")))
-                .withChild(data.checkData)
+                .withChild(DATA.checkData)
                 .build();
         assertEquals(checkingData, normalizedNode);
     }
@@ -164,8 +181,8 @@ public class ReadDataTransactionUtilTest {
     @Test
     public void readDataWrongPathOrNoContentTest() {
         doReturn(Futures.immediateCheckedFuture(Optional.absent())).when(read)
-                .read(LogicalDatastoreType.CONFIGURATION, data.path2);
-        doReturn(data.path2).when(context).getInstanceIdentifier();
+                .read(LogicalDatastoreType.CONFIGURATION, DATA.path2);
+        doReturn(DATA.path2).when(context).getInstanceIdentifier();
         final String valueOfContent = RestconfDataServiceConstant.ReadData.CONFIG;
         final NormalizedNode<?, ?> normalizedNode = ReadDataTransactionUtil.readData(valueOfContent, wrapper);
         assertNull(normalizedNode);
@@ -174,7 +191,8 @@ public class ReadDataTransactionUtilTest {
     @Test(expected = RestconfDocumentedException.class)
     public void readDataFailTest() {
         final String valueOfContent = RestconfDataServiceConstant.ReadData.READ_TYPE_TX;
-        final NormalizedNode<?, ?> normalizedNode = ReadDataTransactionUtil.readData(valueOfContent, null);
+        final NormalizedNode<?, ?> normalizedNode = ReadDataTransactionUtil.readData(
+                valueOfContent, wrapper);
         assertNull(normalizedNode);
     }
 
@@ -189,12 +207,14 @@ public class ReadDataTransactionUtilTest {
         // no parameters, default values should be used
         when(uriInfo.getQueryParameters()).thenReturn(parameters);
 
-        final WriterParameters parsedParameters = ReadDataTransactionUtil.parseUriParameters(uriInfo);
+        final WriterParameters parsedParameters = ReadDataTransactionUtil.parseUriParameters(context, uriInfo);
 
         assertEquals("Not correctly parsed URI parameter",
                 RestconfDataServiceConstant.ReadData.ALL, parsedParameters.getContent());
-        assertFalse("Not correctly parsed URI parameter",
-                parsedParameters.getDepth().isPresent());
+        assertNull("Not correctly parsed URI parameter",
+                parsedParameters.getDepth());
+        assertNull("Not correctly parsed URI parameter",
+                parsedParameters.getFields());
     }
 
     /**
@@ -207,20 +227,35 @@ public class ReadDataTransactionUtilTest {
 
         final String content = "config";
         final String depth = "10";
+        final String fields = containerChildQName.getLocalName();
 
         parameters.put("content", Collections.singletonList(content));
         parameters.put("depth", Collections.singletonList(depth));
+        parameters.put("fields", Collections.singletonList(fields));
 
         when(uriInfo.getQueryParameters()).thenReturn(parameters);
 
-        final WriterParameters parsedParameters = ReadDataTransactionUtil.parseUriParameters(uriInfo);
+        final WriterParameters parsedParameters = ReadDataTransactionUtil.parseUriParameters(context, uriInfo);
 
+        // content
         assertEquals("Not correctly parsed URI parameter",
                 content, parsedParameters.getContent());
-        assertTrue("Not correctly parsed URI parameter",
-                parsedParameters.getDepth().isPresent());
+
+        // depth
+        assertNotNull("Not correctly parsed URI parameter",
+                parsedParameters.getDepth());
+        assertEquals("Not correctly parsed URI parameter",
+                depth, parsedParameters.getDepth().toString());
+
+        // fields
+        assertNotNull("Not correctly parsed URI parameter",
+                parsedParameters.getFields());
+        assertEquals("Not correctly parsed URI parameter",
+                1, parsedParameters.getFields().size());
+        assertEquals("Not correctly parsed URI parameter",
+                1, parsedParameters.getFields().get(0).size());
         assertEquals("Not correctly parsed URI parameter",
-                depth, parsedParameters.getDepth().get().toString());
+                containerChildQName, parsedParameters.getFields().get(0).iterator().next());
     }
 
     /**
@@ -235,7 +270,7 @@ public class ReadDataTransactionUtilTest {
         when(uriInfo.getQueryParameters()).thenReturn(parameters);
 
         try {
-            ReadDataTransactionUtil.parseUriParameters(uriInfo);
+            ReadDataTransactionUtil.parseUriParameters(context, uriInfo);
             fail("Test expected to fail due to not allowed parameter value");
         } catch (final RestconfDocumentedException e) {
             // Bad request
@@ -258,7 +293,7 @@ public class ReadDataTransactionUtilTest {
         when(uriInfo.getQueryParameters()).thenReturn(parameters);
 
         try {
-            ReadDataTransactionUtil.parseUriParameters(uriInfo);
+            ReadDataTransactionUtil.parseUriParameters(context, uriInfo);
             fail("Test expected to fail due to not allowed parameter value");
         } catch (final RestconfDocumentedException e) {
             // Bad request
@@ -282,7 +317,7 @@ public class ReadDataTransactionUtilTest {
         when(uriInfo.getQueryParameters()).thenReturn(parameters);
 
         try {
-            ReadDataTransactionUtil.parseUriParameters(uriInfo);
+            ReadDataTransactionUtil.parseUriParameters(context, uriInfo);
             fail("Test expected to fail due to not allowed parameter value");
         } catch (final RestconfDocumentedException e) {
             // Bad request
@@ -306,7 +341,7 @@ public class ReadDataTransactionUtilTest {
         when(uriInfo.getQueryParameters()).thenReturn(parameters);
 
         try {
-            ReadDataTransactionUtil.parseUriParameters(uriInfo);
+            ReadDataTransactionUtil.parseUriParameters(context, uriInfo);
             fail("Test expected to fail due to not allowed parameter value");
         } catch (final RestconfDocumentedException e) {
             // Bad request
diff --git a/restconf/sal-rest-connector/src/test/java/org/opendaylight/restconf/utils/parser/ParserFieldsParameterTest.java b/restconf/sal-rest-connector/src/test/java/org/opendaylight/restconf/utils/parser/ParserFieldsParameterTest.java
new file mode 100644 (file)
index 0000000..4ba3023
--- /dev/null
@@ -0,0 +1,289 @@
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. 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.utils.parser;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.net.URI;
+import java.text.SimpleDateFormat;
+import java.util.List;
+import java.util.Set;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.opendaylight.controller.md.sal.rest.common.TestRestconfUtils;
+import org.opendaylight.netconf.sal.restconf.impl.InstanceIdentifierContext;
+import org.opendaylight.netconf.sal.restconf.impl.RestconfDocumentedException;
+import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorTag;
+import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorType;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.common.QNameModule;
+import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+
+/**
+ * Unit test for {@link ParserFieldsParameter}
+ */
+public class ParserFieldsParameterTest {
+
+    @Mock
+    private InstanceIdentifierContext<ContainerSchemaNode> identifierContext;
+
+    // container jukebox
+    @Mock
+    private ContainerSchemaNode containerJukebox;
+    private QName jukeboxQName;
+
+    // container player
+    @Mock
+    private ContainerSchemaNode containerPlayer;
+    private QName playerQName;
+
+    // container library
+    @Mock
+    private ContainerSchemaNode containerLibrary;
+    private QName libraryQName;
+
+    // container augmented library
+    @Mock
+    private ContainerSchemaNode augmentedContainerLibrary;
+    private QName augmentedLibraryQName;
+
+    // list album
+    @Mock
+    private ListSchemaNode listAlbum;
+    private QName albumQName;
+
+    // leaf name
+    @Mock
+    private LeafSchemaNode leafName;
+    private QName nameQName;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        final SchemaContext schemaContext = TestRestconfUtils.loadSchemaContext("/jukebox");
+
+        final QNameModule qNameModule = QNameModule.create(URI.create("http://example.com/ns/example-jukebox"),
+                new SimpleDateFormat("yyyy-MM-dd").parse("2015-04-04"));
+
+        jukeboxQName = QName.create(qNameModule, "jukebox");
+        playerQName = QName.create(qNameModule, "player");
+        libraryQName = QName.create(qNameModule, "library");
+        augmentedLibraryQName = QName.create(
+                QNameModule.create(
+                        URI.create("http://example.com/ns/augmented-jukebox"),
+                        new SimpleDateFormat("yyyy-MM-dd").parse("2016-05-05")),
+                "augmented-library");
+        albumQName = QName.create(qNameModule, "album");
+        nameQName = QName.create(qNameModule, "name");
+
+        Mockito.when(identifierContext.getSchemaContext()).thenReturn(schemaContext);
+        Mockito.when(containerJukebox.getQName()).thenReturn(jukeboxQName);
+        Mockito.when(identifierContext.getSchemaNode()).thenReturn(containerJukebox);
+
+        Mockito.when(containerLibrary.getQName()).thenReturn(libraryQName);
+        Mockito.when(containerJukebox.getDataChildByName(libraryQName)).thenReturn(containerLibrary);
+
+        Mockito.when(augmentedContainerLibrary.getQName()).thenReturn(augmentedLibraryQName);
+        Mockito.when(containerJukebox.getDataChildByName(augmentedLibraryQName)).thenReturn(augmentedContainerLibrary);
+
+        Mockito.when(containerPlayer.getQName()).thenReturn(playerQName);
+        Mockito.when(containerJukebox.getDataChildByName(playerQName)).thenReturn(containerPlayer);
+
+        Mockito.when(listAlbum.getQName()).thenReturn(albumQName);
+        Mockito.when(containerLibrary.getDataChildByName(albumQName)).thenReturn(listAlbum);
+
+        Mockito.when(leafName.getQName()).thenReturn(nameQName);
+        Mockito.when(listAlbum.getDataChildByName(nameQName)).thenReturn(leafName);
+    }
+
+    /**
+     * Test parse fields parameter containing only one child selected
+     */
+    @Test
+    public void parseFieldsParameterSimplePathTest() {
+        final String input = "library";
+        final List<Set<QName>> parsedFields = ParserFieldsParameter.parseFieldsParameter(identifierContext, input);
+
+        assertNotNull(parsedFields);
+        assertEquals(1, parsedFields.size());
+        assertEquals(1, parsedFields.get(0).size());
+        assertTrue(parsedFields.get(0).contains(libraryQName));
+    }
+
+    /**
+     * Test parse fields parameter containing two child nodes selected
+     */
+    @Test
+    public void parseFieldsParameterDoublePathTest() {
+        final String input = "library;player";
+        final List<Set<QName>> parsedFields = ParserFieldsParameter.parseFieldsParameter(identifierContext, input);
+
+        assertNotNull(parsedFields);
+        assertEquals(1, parsedFields.size());
+        assertEquals(2, parsedFields.get(0).size());
+        assertTrue(parsedFields.get(0).contains(libraryQName));
+        assertTrue(parsedFields.get(0).contains(playerQName));
+    }
+
+    /**
+     * Test parse fields parameter containing sub-children selected delimited by slash
+     */
+    @Test
+    public void parseFieldsParameterSubPathTest() {
+        final String input = "library/album/name";
+        final List<Set<QName>> parsedFields = ParserFieldsParameter.parseFieldsParameter(identifierContext, input);
+
+        assertNotNull(parsedFields);
+        assertEquals(3, parsedFields.size());
+
+        assertEquals(1, parsedFields.get(0).size());
+        assertTrue(parsedFields.get(0).contains(libraryQName));
+
+        assertEquals(1, parsedFields.get(1).size());
+        assertTrue(parsedFields.get(1).contains(albumQName));
+
+        assertEquals(1, parsedFields.get(2).size());
+        assertTrue(parsedFields.get(2).contains(nameQName));
+    }
+
+    /**
+     * Test parse fields parameter containing sub-children selected delimited by parenthesis
+     */
+    @Test
+    public void parseFieldsParameterChildrenPathTest() {
+        final String input = "library(album(name))";
+        final List<Set<QName>> parsedFields = ParserFieldsParameter.parseFieldsParameter(identifierContext, input);
+
+        assertNotNull(parsedFields);
+        assertEquals(3, parsedFields.size());
+
+        assertEquals(1, parsedFields.get(0).size());
+        assertTrue(parsedFields.get(0).contains(libraryQName));
+
+        assertEquals(1, parsedFields.get(1).size());
+        assertTrue(parsedFields.get(1).contains(albumQName));
+
+        assertEquals(1, parsedFields.get(2).size());
+        assertTrue(parsedFields.get(2).contains(nameQName));
+    }
+
+    /**
+     * Test parse fields parameter when augmentation with different namespace is used
+     */
+    @Test
+    public void parseFieldsParameterNamespaceTest() {
+        final String input = "augmented-jukebox:augmented-library";
+        final List<Set<QName>> parsedFields = ParserFieldsParameter.parseFieldsParameter(identifierContext, input);
+
+        assertNotNull(parsedFields);
+        assertEquals(1, parsedFields.size());
+
+        assertEquals(1, parsedFields.get(0).size());
+        assertTrue(parsedFields.get(0).contains(augmentedLibraryQName));
+    }
+
+    /**
+     * Test parse fields parameter containing not expected character
+     */
+    @Test
+    public void parseFieldsParameterNotExpectedCharacterNegativeTest() {
+        final String input = "*";
+
+        try {
+            ParserFieldsParameter.parseFieldsParameter(identifierContext, input);
+            fail("Test should fail due to not expected character used in parameter input value");
+        } catch (final RestconfDocumentedException e) {
+            // Bad request
+            assertEquals("Error type is not correct", ErrorType.PROTOCOL, e.getErrors().get(0).getErrorType());
+            assertEquals("Error tag is not correct", ErrorTag.INVALID_VALUE, e.getErrors().get(0).getErrorTag());
+            assertEquals("Error status code is not correct", 400, e.getErrors().get(0).getErrorTag().getStatusCode());
+        }
+    }
+
+    /**
+     * Test parse fields parameter with missing closing parenthesis
+     */
+    @Test
+    public void parseFieldsParameterMissingParenthesisNegativeTest() {
+        final String input = "library(";
+
+        try {
+            ParserFieldsParameter.parseFieldsParameter(identifierContext, input);
+            fail("Test should fail due to missing closing parenthesis");
+        } catch (final RestconfDocumentedException e) {
+            // Bad request
+            assertEquals("Error type is not correct", ErrorType.PROTOCOL, e.getErrors().get(0).getErrorType());
+            assertEquals("Error tag is not correct", ErrorTag.INVALID_VALUE, e.getErrors().get(0).getErrorTag());
+            assertEquals("Error status code is not correct", 400, e.getErrors().get(0).getErrorTag().getStatusCode());
+        }
+    }
+
+    /**
+     * Test parse fields parameter when not existing child node selected
+     */
+    @Test
+    public void parseFieldsParameterMissingChildNodeNegativeTest() {
+        final String input = "library(not-existing)";
+
+        try {
+            ParserFieldsParameter.parseFieldsParameter(identifierContext, input);
+            fail("Test should fail due to missing child node in parent node");
+        } catch (final RestconfDocumentedException e) {
+            // Bad request
+            assertEquals("Error type is not correct", ErrorType.PROTOCOL, e.getErrors().get(0).getErrorType());
+            assertEquals("Error tag is not correct", ErrorTag.INVALID_VALUE, e.getErrors().get(0).getErrorTag());
+            assertEquals("Error status code is not correct", 400, e.getErrors().get(0).getErrorTag().getStatusCode());
+        }
+    }
+
+    /**
+     * Test parse fields parameter with unexpected character after parenthesis
+     */
+    @Test
+    public void parseFieldsParameterAfterParenthesisNegativeTest() {
+        final String input = "library(album);";
+
+        try {
+            ParserFieldsParameter.parseFieldsParameter(identifierContext, input);
+            fail("Test should fail due to unexpected character after parenthesis");
+        } catch (final RestconfDocumentedException e) {
+            // Bad request
+            assertEquals("Error type is not correct", ErrorType.PROTOCOL, e.getErrors().get(0).getErrorType());
+            assertEquals("Error tag is not correct", ErrorTag.INVALID_VALUE, e.getErrors().get(0).getErrorTag());
+            assertEquals("Error status code is not correct", 400, e.getErrors().get(0).getErrorTag().getStatusCode());
+        }
+    }
+
+    /**
+     * Test parse fields parameter with missing semicolon after parenthesis
+     */
+    @Test
+    public void parseFieldsParameterMissingSemicolonNegativeTest() {
+        final String input = "library(album)player";
+
+        try {
+            ParserFieldsParameter.parseFieldsParameter(identifierContext, input);
+            fail("Test should fail due to missing semicolon after parenthesis");
+        } catch (final RestconfDocumentedException e) {
+            // Bad request
+            assertEquals("Error type is not correct", ErrorType.PROTOCOL, e.getErrors().get(0).getErrorType());
+            assertEquals("Error tag is not correct", ErrorTag.INVALID_VALUE, e.getErrors().get(0).getErrorTag());
+            assertEquals("Error status code is not correct", 400, e.getErrors().get(0).getErrorTag().getStatusCode());
+        }
+    }
+}
\ No newline at end of file
diff --git a/restconf/sal-rest-connector/src/test/resources/jukebox/augmented-jukebox@2016-05-05.yang b/restconf/sal-rest-connector/src/test/resources/jukebox/augmented-jukebox@2016-05-05.yang
new file mode 100644 (file)
index 0000000..abbd5d0
--- /dev/null
@@ -0,0 +1,16 @@
+module augmented-jukebox {
+
+      namespace "http://example.com/ns/augmented-jukebox";
+      prefix "augmented-jbox";
+
+      revision "2016-05-05" {
+        description "Initial version.";
+      }
+
+      import example-jukebox {prefix jbox; revision-date "2015-04-04";}
+
+      augment "/jbox:jukebox" {
+        container augmented-library {
+        }
+     }
+   }
\ No newline at end of file