Bug 3999: Create internal service to access restconf 56/23856/9
authorTom Pantelis <tpanteli@brocade.com>
Tue, 7 Jul 2015 15:07:31 +0000 (11:07 -0400)
committerGerrit Code Review <gerrit@opendaylight.org>
Mon, 20 Jul 2015 21:26:38 +0000 (21:26 +0000)
There are use cases for invoking restconf from internal code. However
issuing an HTTP request is problematic as one would need to know the
credentials and scheme (http or https).

So I added a JSONRestconfService interface and implementation with CRUD
methods that call the JSON readers/writers and RestconfImpl internally.
The implementation is advertised as an OSGi service via the config
system for consumption by clients.

I only added a service for JSON - an XML service could be added as well
later.

Change-Id: I5d1304c568c9be9c204afea68aadc0306bac50b3
Signed-off-by: Tom Pantelis <tpanteli@brocade.com>
18 files changed:
features/restconf/pom.xml
features/restconf/src/main/resources/features.xml
opendaylight/commons/opendaylight/pom.xml
opendaylight/md-sal/sal-rest-connector-config/pom.xml
opendaylight/md-sal/sal-rest-connector-config/src/main/resources/initial/10-restconf-service.xml [new file with mode: 0644]
opendaylight/md-sal/sal-rest-connector/pom.xml
opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/config/yang/sal/restconf/service/JSONRestconfServiceModule.java [new file with mode: 0644]
opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/config/yang/sal/restconf/service/JSONRestconfServiceModuleFactory.java [new file with mode: 0644]
opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/restconf/api/JSONRestconfService.java [new file with mode: 0644]
opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/JsonNormalizedNodeBodyReader.java
opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/JSONRestconfServiceImpl.java [new file with mode: 0644]
opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/QueryParametersParser.java
opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfDocumentedException.java
opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfImpl.java
opendaylight/md-sal/sal-rest-connector/src/main/yang/sal-restconf-service.yang [new file with mode: 0644]
opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/JSONRestconfServiceImplTest.java [new file with mode: 0644]
opendaylight/md-sal/sal-rest-connector/src/test/resources/full-versions/make-toast-rpc-input.json [new file with mode: 0644]
opendaylight/md-sal/sal-rest-connector/src/test/resources/full-versions/testCont1Data.json [new file with mode: 0644]

index 197e2f70a8674a315356abbd75c06f378f5c655c..8d8f38432f8d175a0c6626208462efe410d49580 100644 (file)
       <type>xml</type>
       <classifier>config</classifier>
     </dependency>
+    <dependency>
+      <groupId>org.opendaylight.controller</groupId>
+      <artifactId>sal-rest-connector-config</artifactId>
+      <version>${mdsal.version}</version>
+      <type>xml</type>
+      <classifier>configrestconfservice</classifier>
+    </dependency>
 
         <dependency>
       <groupId>com.fasterxml.jackson.core</groupId>
index dc2cce2ac22f03190a7ffeccfe84a2a12a3a9d79..f1a10914a809b0ef28b5ff31fb3177baceba2fc5 100644 (file)
         <bundle>mvn:io.netty/netty-handler/${netty.version}</bundle>
         <bundle>mvn:io.netty/netty-transport/${netty.version}</bundle>
         <configfile finalname="${config.configfile.directory}/${config.restconf.configfile}">mvn:org.opendaylight.controller/sal-rest-connector-config/${mdsal.version}/xml/config</configfile>
+        <configfile finalname="${config.configfile.directory}/${config.restconf.service.configfile}">mvn:org.opendaylight.controller/sal-rest-connector-config/${mdsal.version}/xml/configrestconfservice</configfile>
     </feature>
     <feature name ='odl-mdsal-apidocs' version='${project.version}' description="OpenDaylight :: MDSAL :: APIDOCS">
         <feature version='${project.version}'>odl-restconf</feature>
index 8d7e63edd43a7633a81bc52cff53124324e7a166..f04bb2091264497036847d91ee7d2837de89dae0 100644 (file)
@@ -63,6 +63,7 @@
     <config.toaster.configfile>03-toaster-sample.xml</config.toaster.configfile>
     <config.netconf.mdsal.configfile>08-mdsal-netconf.xml</config.netconf.mdsal.configfile>
     <config.restconf.configfile>10-rest-connector.xml</config.restconf.configfile>
+    <config.restconf.service.configfile>10-restconf-service.xml</config.restconf.service.configfile>
     <config.netconf.connector.configfile>99-netconf-connector.xml</config.netconf.connector.configfile>
     <configuration.implementation.version>0.5.1-SNAPSHOT</configuration.implementation.version>
     <configuration.version>0.5.1-SNAPSHOT</configuration.version>
index 3932d3c220fb5a1a716e78737c96147558aff70b..ad8334f02707b375e3ff1bdc9c7230709dbcfb19 100644 (file)
                   <type>xml</type>
                   <classifier>config</classifier>
                 </artifact>
+                <artifact>
+                  <file>${project.build.directory}/classes/initial/10-restconf-service.xml</file>
+                  <type>xml</type>
+                  <classifier>configrestconfservice</classifier>
+                </artifact>
               </artifacts>
             </configuration>
           </execution>
diff --git a/opendaylight/md-sal/sal-rest-connector-config/src/main/resources/initial/10-restconf-service.xml b/opendaylight/md-sal/sal-rest-connector-config/src/main/resources/initial/10-restconf-service.xml
new file mode 100644 (file)
index 0000000..6a0362e
--- /dev/null
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (c) 2015 Brocade Communications 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
+-->
+<snapshot>
+  <configuration>
+    <data xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+      <modules xmlns="urn:opendaylight:params:xml:ns:yang:controller:config">
+        <module>
+          <type xmlns:rest="urn:opendaylight:params:xml:ns:yang:controller:sal:restconf:service">rest:json-restconf-service-impl</type>
+          <name>json-restconf-service-impl</name>
+        </module>
+      </modules>
+
+      <services xmlns="urn:opendaylight:params:xml:ns:yang:controller:config">
+        <service>
+          <type xmlns:rest="urn:opendaylight:params:xml:ns:yang:controller:sal:restconf:service">rest:json-restconf-service</type>
+          <instance>
+            <name>json-restconf-service</name>
+            <provider>
+              /modules/module[type='json-restconf-service-impl'][name='json-restconf-service-impl']
+            </provider>
+          </instance>
+        </service>
+      </services>
+    </data>
+  </configuration>
+  <required-capabilities>
+      <capability>urn:opendaylight:params:xml:ns:yang:controller:sal:restconf:service?module=sal-restconf-service&amp;revision=2015-07-08</capability>
+  </required-capabilities>
+</snapshot>
index 9349e2e7bace77445231f9edbcd035fd91bde0c3..c991b419f7eb6931f2c18cd39503a64895f46c6c 100644 (file)
     </dependency>
 
     <!-- Testing Dependencies -->
+    <dependency>
+      <groupId>commons-io</groupId>
+      <artifactId>commons-io</artifactId>
+      <scope>test</scope>
+    </dependency>
     <dependency>
       <groupId>junit</groupId>
       <artifactId>junit</artifactId>
diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/config/yang/sal/restconf/service/JSONRestconfServiceModule.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/config/yang/sal/restconf/service/JSONRestconfServiceModule.java
new file mode 100644 (file)
index 0000000..42dd8dd
--- /dev/null
@@ -0,0 +1,23 @@
+package org.opendaylight.controller.config.yang.sal.restconf.service;
+
+import org.opendaylight.controller.sal.restconf.impl.JSONRestconfServiceImpl;
+
+public class JSONRestconfServiceModule extends org.opendaylight.controller.config.yang.sal.restconf.service.AbstractJSONRestconfServiceModule {
+    public JSONRestconfServiceModule(org.opendaylight.controller.config.api.ModuleIdentifier identifier, org.opendaylight.controller.config.api.DependencyResolver dependencyResolver) {
+        super(identifier, dependencyResolver);
+    }
+
+    public JSONRestconfServiceModule(org.opendaylight.controller.config.api.ModuleIdentifier identifier, org.opendaylight.controller.config.api.DependencyResolver dependencyResolver, org.opendaylight.controller.config.yang.sal.restconf.service.JSONRestconfServiceModule oldModule, java.lang.AutoCloseable oldInstance) {
+        super(identifier, dependencyResolver, oldModule, oldInstance);
+    }
+
+    @Override
+    public void customValidation() {
+        // add custom validation form module attributes here.
+    }
+
+    @Override
+    public java.lang.AutoCloseable createInstance() {
+        return new JSONRestconfServiceImpl();
+    }
+}
diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/config/yang/sal/restconf/service/JSONRestconfServiceModuleFactory.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/config/yang/sal/restconf/service/JSONRestconfServiceModuleFactory.java
new file mode 100644 (file)
index 0000000..4165882
--- /dev/null
@@ -0,0 +1,13 @@
+/*
+* Generated file
+*
+* Generated from: yang module name: sal-restconf-service yang module local name: json-restconf-service-impl
+* Generated by: org.opendaylight.controller.config.yangjmxgenerator.plugin.JMXGenerator
+* Generated at: Tue Jul 07 18:18:52 EDT 2015
+*
+* Do not modify this file unless it is present under src/main directory
+*/
+package org.opendaylight.controller.config.yang.sal.restconf.service;
+public class JSONRestconfServiceModuleFactory extends org.opendaylight.controller.config.yang.sal.restconf.service.AbstractJSONRestconfServiceModuleFactory {
+
+}
diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/restconf/api/JSONRestconfService.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/restconf/api/JSONRestconfService.java
new file mode 100644 (file)
index 0000000..8216258
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2015 Brocade Communications 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.controller.restconf.api;
+
+import com.google.common.base.Optional;
+import javax.annotation.Nonnull;
+import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
+import org.opendaylight.yangtools.yang.common.OperationFailedException;
+
+/**
+ * @author Thomas Pantelis
+ */
+public interface JSONRestconfService {
+    /**
+     * The data tree root path.
+     */
+    String ROOT_PATH = null;
+
+    /**
+     * Issues a restconf PUT request to the configuration data store.
+     *
+     * @param uriPath the yang instance identifier path, eg "opendaylight-inventory:nodes/node/device-id".
+     *       To specify the root, use {@link ROOT_PATH}.
+     * @param payload the payload data in JSON format.
+     * @throws OperationFailedException if the request fails.
+     */
+    void put(String uriPath, @Nonnull String payload) throws OperationFailedException;
+
+    /**
+     * Issues a restconf POST request to the configuration data store.
+     *
+     * @param uriPath the yang instance identifier path, eg "opendaylight-inventory:nodes/node/device-id".
+     *       To specify the root, use {@link ROOT_PATH}.
+     * @param payload the payload data in JSON format.
+     * @throws OperationFailedException if the request fails.
+     */
+    void post(String uriPath, @Nonnull String payload) throws OperationFailedException;
+
+    /**
+     * Issues a restconf DELETE request to the configuration data store.
+     *
+     * @param uriPath the yang instance identifier path, eg "opendaylight-inventory:nodes/node/device-id".
+     *       To specify the root, use {@link ROOT_PATH}.
+     * @throws OperationFailedException if the request fails.
+     */
+    void delete(String uriPath) throws OperationFailedException;
+
+    /**
+     * Issues a restconf GET request to the given data store.
+     *
+     * @param uriPath the yang instance identifier path, eg "opendaylight-inventory:nodes/node/device-id".
+     *       To specify the root, use {@link ROOT_PATH}.
+     * @param datastoreType the data store type to read from.
+     * @return an Optional containing the data in JSON format if present.
+     * @throws OperationFailedException if the request fails.
+     */
+    Optional<String> get(String uriPath, LogicalDatastoreType datastoreType) throws OperationFailedException;
+
+    /**
+     * Invokes a yang-defined RPC.
+     *
+     * @param uriPath the path representing the RPC to invoke, eg "toaster:make-toast".
+     * @param input the input in JSON format if the RPC takes input.
+     * @return an Optional containing the output in JSON format if the RPC returns output.
+     * @throws OperationFailedException if the request fails.
+     */
+    Optional<String> invokeRpc(@Nonnull String uriPath, Optional<String> input) throws OperationFailedException;
+}
index 42024cab08748cadc9abf338f496e055a616722d..0fbe94d5ab48b8e7dbefe66b0e4876758ac51897 100644 (file)
@@ -24,6 +24,7 @@ import javax.ws.rs.ext.MessageBodyReader;
 import javax.ws.rs.ext.Provider;
 import org.opendaylight.controller.sal.rest.api.Draft02;
 import org.opendaylight.controller.sal.rest.api.RestconfService;
+import org.opendaylight.controller.sal.restconf.impl.ControllerContext;
 import org.opendaylight.controller.sal.restconf.impl.InstanceIdentifierContext;
 import org.opendaylight.controller.sal.restconf.impl.NormalizedNodeContext;
 import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException;
@@ -64,79 +65,103 @@ public class JsonNormalizedNodeBodyReader extends AbstractIdentifierAwareJaxRsPr
     @Override
     public NormalizedNodeContext readFrom(final Class<NormalizedNodeContext> type, final Type genericType,
             final Annotation[] annotations, final MediaType mediaType,
-            final MultivaluedMap<String, String> httpHeaders, final InputStream entityStream) throws IOException,
-            WebApplicationException {
+            final MultivaluedMap<String, String> httpHeaders, final InputStream entityStream)
+                    throws WebApplicationException, IOException {
         try {
-            final InstanceIdentifierContext<?> path = getInstanceIdentifierContext();
-            if (entityStream.available() < 1) {
-                return new NormalizedNodeContext(path, null);
-            }
-            final NormalizedNodeResult resultHolder = new NormalizedNodeResult();
-            final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
-
-            final SchemaNode parentSchema;
-            if(isPost()) {
-                // FIXME: We need dispatch for RPC.
-                parentSchema = path.getSchemaNode();
-            } else if(path.getSchemaNode() instanceof SchemaContext) {
+            return readFrom(getInstanceIdentifierContext(), entityStream, isPost());
+        } catch (final Exception e) {
+            propagateExceptionAs(e);
+            return null; // no-op
+        }
+    }
+
+    private static void propagateExceptionAs(Exception e) throws RestconfDocumentedException {
+        if(e instanceof RestconfDocumentedException) {
+            throw (RestconfDocumentedException)e;
+        }
+
+        if(e instanceof ResultAlreadySetException) {
+            LOG.debug("Error parsing json input:", e);
+
+            throw new RestconfDocumentedException("Error parsing json input: Failed to create new parse result data. " +
+                    "Are you creating multiple resources/subresources in POST request?");
+        }
+
+        LOG.debug("Error parsing json input", e);
+
+        throw new RestconfDocumentedException("Error parsing input: " + e.getMessage(), ErrorType.PROTOCOL,
+                ErrorTag.MALFORMED_MESSAGE, e);
+    }
+
+    public static NormalizedNodeContext readFrom(final String uriPath, final InputStream entityStream,
+            final boolean isPost) throws RestconfDocumentedException {
+
+        try {
+            return readFrom(ControllerContext.getInstance().toInstanceIdentifier(uriPath), entityStream, isPost);
+        } catch (final Exception e) {
+            propagateExceptionAs(e);
+            return null; // no-op
+        }
+    }
+
+    private static NormalizedNodeContext readFrom(final InstanceIdentifierContext<?> path, final InputStream entityStream,
+            final boolean isPost) throws IOException {
+        if (entityStream.available() < 1) {
+            return new NormalizedNodeContext(path, null);
+        }
+        final NormalizedNodeResult resultHolder = new NormalizedNodeResult();
+        final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
+
+        final SchemaNode parentSchema;
+        if(isPost) {
+            // FIXME: We need dispatch for RPC.
+            parentSchema = path.getSchemaNode();
+        } else if(path.getSchemaNode() instanceof SchemaContext) {
+            parentSchema = path.getSchemaContext();
+        } else {
+            if (SchemaPath.ROOT.equals(path.getSchemaNode().getPath().getParent())) {
                 parentSchema = path.getSchemaContext();
             } else {
-                if (SchemaPath.ROOT.equals(path.getSchemaNode().getPath().getParent())) {
-                    parentSchema = path.getSchemaContext();
-                } else {
-                    parentSchema = SchemaContextUtil.findDataSchemaNode(path.getSchemaContext(), path.getSchemaNode().getPath().getParent());
-                }
+                parentSchema = SchemaContextUtil.findDataSchemaNode(path.getSchemaContext(), path.getSchemaNode().getPath().getParent());
             }
+        }
 
-            final JsonParserStream jsonParser = JsonParserStream.create(writer, path.getSchemaContext(), parentSchema);
-            final JsonReader reader = new JsonReader(new InputStreamReader(entityStream));
-            jsonParser.parse(reader);
+        final JsonParserStream jsonParser = JsonParserStream.create(writer, path.getSchemaContext(), parentSchema);
+        final JsonReader reader = new JsonReader(new InputStreamReader(entityStream));
+        jsonParser.parse(reader);
 
-            NormalizedNode<?, ?> result = resultHolder.getResult();
-            final List<YangInstanceIdentifier.PathArgument> iiToDataList = new ArrayList<>();
-            InstanceIdentifierContext<? extends SchemaNode> newIIContext;
+        NormalizedNode<?, ?> result = resultHolder.getResult();
+        final List<YangInstanceIdentifier.PathArgument> iiToDataList = new ArrayList<>();
+        InstanceIdentifierContext<? extends SchemaNode> newIIContext;
 
-            while (result instanceof AugmentationNode || result instanceof ChoiceNode) {
-                final Object childNode = ((DataContainerNode) result).getValue().iterator().next();
-                if (isPost()) {
-                    iiToDataList.add(result.getIdentifier());
-                }
-                result = (NormalizedNode<?, ?>) childNode;
+        while (result instanceof AugmentationNode || result instanceof ChoiceNode) {
+            final Object childNode = ((DataContainerNode) result).getValue().iterator().next();
+            if (isPost) {
+                iiToDataList.add(result.getIdentifier());
             }
+            result = (NormalizedNode<?, ?>) childNode;
+        }
 
-            if (isPost()) {
-                if (result instanceof MapEntryNode) {
-                    iiToDataList.add(new YangInstanceIdentifier.NodeIdentifier(result.getNodeType()));
-                    iiToDataList.add(result.getIdentifier());
-                } else {
-                    iiToDataList.add(result.getIdentifier());
-                }
+        if (isPost) {
+            if (result instanceof MapEntryNode) {
+                iiToDataList.add(new YangInstanceIdentifier.NodeIdentifier(result.getNodeType()));
+                iiToDataList.add(result.getIdentifier());
             } else {
-                if (result instanceof MapNode) {
-                    result = Iterables.getOnlyElement(((MapNode) result).getValue());
-                }
+                iiToDataList.add(result.getIdentifier());
             }
+        } else {
+            if (result instanceof MapNode) {
+                result = Iterables.getOnlyElement(((MapNode) result).getValue());
+            }
+        }
 
-            final YangInstanceIdentifier fullIIToData = YangInstanceIdentifier.create(Iterables.concat(
-                            path.getInstanceIdentifier().getPathArguments(), iiToDataList));
-
-            newIIContext = new InstanceIdentifierContext<>(fullIIToData, path.getSchemaNode(), path.getMountPoint(),
-                    path.getSchemaContext());
-
-            return new NormalizedNodeContext(newIIContext, result);
-        } catch (final RestconfDocumentedException e) {
-            throw e;
-        } catch (final ResultAlreadySetException e) {
-            LOG.debug("Error parsing json input:", e);
+        final YangInstanceIdentifier fullIIToData = YangInstanceIdentifier.create(Iterables.concat(
+                path.getInstanceIdentifier().getPathArguments(), iiToDataList));
 
-            throw new RestconfDocumentedException("Error parsing json input: Failed to create new parse result data. " +
-                    "Are you creating multiple resources/subresources in POST request?");
-        } catch (final Exception e) {
-            LOG.debug("Error parsing json input", e);
+        newIIContext = new InstanceIdentifierContext<>(fullIIToData, path.getSchemaNode(), path.getMountPoint(),
+                path.getSchemaContext());
 
-            throw new RestconfDocumentedException("Error parsing input: " + e.getMessage(), ErrorType.PROTOCOL,
-                    ErrorTag.MALFORMED_MESSAGE);
-        }
+        return new NormalizedNodeContext(newIIContext, result);
     }
 }
 
diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/JSONRestconfServiceImpl.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/JSONRestconfServiceImpl.java
new file mode 100644 (file)
index 0000000..8f1a9f7
--- /dev/null
@@ -0,0 +1,217 @@
+/*
+ * Copyright (c) 2015 Brocade Communications 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.controller.sal.restconf.impl;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.annotation.Annotation;
+import java.util.List;
+import javax.ws.rs.core.MediaType;
+import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
+import org.opendaylight.controller.restconf.api.JSONRestconfService;
+import org.opendaylight.controller.sal.rest.impl.JsonNormalizedNodeBodyReader;
+import org.opendaylight.controller.sal.rest.impl.NormalizedNodeJsonBodyWriter;
+import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorTag;
+import org.opendaylight.yangtools.yang.common.OperationFailedException;
+import org.opendaylight.yangtools.yang.common.RpcError;
+import org.opendaylight.yangtools.yang.common.RpcError.ErrorType;
+import org.opendaylight.yangtools.yang.common.RpcResultBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Implementation of the JSONRestconfService interface.
+ *
+ * @author Thomas Pantelis
+ */
+public class JSONRestconfServiceImpl implements JSONRestconfService, AutoCloseable {
+    private final static Logger LOG = LoggerFactory.getLogger(JSONRestconfServiceImpl.class);
+
+    private static final Annotation[] EMPTY_ANNOTATIONS = new Annotation[0];
+
+    @Override
+    public void put(String uriPath, String payload) throws OperationFailedException {
+        Preconditions.checkNotNull(payload, "payload can't be null");
+
+        LOG.debug("put: uriPath: {}, payload: {}", uriPath, payload);
+
+        InputStream entityStream = new ByteArrayInputStream(payload.getBytes(Charsets.UTF_8));
+        NormalizedNodeContext context = JsonNormalizedNodeBodyReader.readFrom(uriPath, entityStream, false);
+
+        LOG.debug("Parsed YangInstanceIdentifier: {}", context.getInstanceIdentifierContext().getInstanceIdentifier());
+        LOG.debug("Parsed NormalizedNode: {}", context.getData());
+
+        try {
+            RestconfImpl.getInstance().updateConfigurationData(uriPath, context);
+        } catch (Exception e) {
+            propagateExceptionAs(uriPath, e, "PUT");
+        }
+    }
+
+    @Override
+    public void post(String uriPath, String payload) throws OperationFailedException {
+        Preconditions.checkNotNull(payload, "payload can't be null");
+
+        LOG.debug("post: uriPath: {}, payload: {}", uriPath, payload);
+
+        InputStream entityStream = new ByteArrayInputStream(payload.getBytes(Charsets.UTF_8));
+        NormalizedNodeContext context = JsonNormalizedNodeBodyReader.readFrom(uriPath, entityStream, true);
+
+        LOG.debug("Parsed YangInstanceIdentifier: {}", context.getInstanceIdentifierContext().getInstanceIdentifier());
+        LOG.debug("Parsed NormalizedNode: {}", context.getData());
+
+        try {
+            RestconfImpl.getInstance().createConfigurationData(uriPath, context, null);
+        } catch (Exception e) {
+            propagateExceptionAs(uriPath, e, "POST");
+        }
+    }
+
+    @Override
+    public void delete(String uriPath) throws OperationFailedException {
+        LOG.debug("delete: uriPath: {}", uriPath);
+
+        try {
+            RestconfImpl.getInstance().deleteConfigurationData(uriPath);
+        } catch (Exception e) {
+            propagateExceptionAs(uriPath, e, "DELETE");
+        }
+    }
+
+    @Override
+    public Optional<String> get(String uriPath, LogicalDatastoreType datastoreType) throws OperationFailedException {
+        LOG.debug("get: uriPath: {}", uriPath);
+
+        try {
+            NormalizedNodeContext readData;
+            if(datastoreType == LogicalDatastoreType.CONFIGURATION) {
+                readData = RestconfImpl.getInstance().readConfigurationData(uriPath, null);
+            } else {
+                readData = RestconfImpl.getInstance().readOperationalData(uriPath, null);
+            }
+
+            Optional<String> result = Optional.of(toJson(readData));
+
+            LOG.debug("get returning: {}", result.get());
+
+            return result;
+        } catch (Exception e) {
+            if(!isDataMissing(e)) {
+                propagateExceptionAs(uriPath, e, "GET");
+            }
+
+            LOG.debug("Data missing - returning absent");
+            return Optional.absent();
+        }
+    }
+
+    @Override
+    public Optional<String> invokeRpc(String uriPath, Optional<String> input) throws OperationFailedException {
+        Preconditions.checkNotNull(uriPath, "uriPath can't be null");
+
+        String actualInput = input.isPresent() ? input.get() : null;
+
+        LOG.debug("invokeRpc: uriPath: {}, input: {}", uriPath, actualInput);
+
+        String output = null;
+        try {
+            NormalizedNodeContext outputContext;
+            if(actualInput != null) {
+                InputStream entityStream = new ByteArrayInputStream(actualInput.getBytes(Charsets.UTF_8));
+                NormalizedNodeContext inputContext = JsonNormalizedNodeBodyReader.readFrom(uriPath, entityStream, true);
+
+                LOG.debug("Parsed YangInstanceIdentifier: {}", inputContext.getInstanceIdentifierContext()
+                        .getInstanceIdentifier());
+                LOG.debug("Parsed NormalizedNode: {}", inputContext.getData());
+
+                outputContext = RestconfImpl.getInstance().invokeRpc(uriPath, inputContext, null);
+            } else {
+                outputContext = RestconfImpl.getInstance().invokeRpc(uriPath, "", null);
+            }
+
+            if(outputContext.getData() != null) {
+                output = toJson(outputContext);
+            }
+        } catch (Exception e) {
+            propagateExceptionAs(uriPath, e, "RPC");
+        }
+
+        return Optional.fromNullable(output);
+    }
+
+    @Override
+    public void close() {
+    }
+
+    private String toJson(NormalizedNodeContext readData) throws IOException {
+        NormalizedNodeJsonBodyWriter writer = new NormalizedNodeJsonBodyWriter();
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        writer.writeTo(readData, NormalizedNodeContext.class, null, EMPTY_ANNOTATIONS,
+                MediaType.APPLICATION_JSON_TYPE, null, outputStream );
+        return outputStream.toString(Charsets.UTF_8.name());
+    }
+
+    private boolean isDataMissing(Exception e) {
+        boolean dataMissing = false;
+        if(e instanceof RestconfDocumentedException) {
+            RestconfDocumentedException rde = (RestconfDocumentedException)e;
+            if(!rde.getErrors().isEmpty()) {
+                if(rde.getErrors().get(0).getErrorTag() == ErrorTag.DATA_MISSING) {
+                    dataMissing = true;
+                }
+            }
+        }
+
+        return dataMissing;
+    }
+
+    private static void propagateExceptionAs(String uriPath, Exception e, String operation) throws OperationFailedException {
+        LOG.debug("Error for uriPath: {}", uriPath, e);
+
+        if(e instanceof RestconfDocumentedException) {
+            throw new OperationFailedException(String.format("%s failed for URI %s", operation, uriPath), e.getCause(),
+                    toRpcErrors(((RestconfDocumentedException)e).getErrors()));
+        }
+
+        throw new OperationFailedException(String.format("%s failed for URI %s", operation, uriPath), e);
+    }
+
+    private static RpcError[] toRpcErrors(List<RestconfError> from) {
+        RpcError[] to = new RpcError[from.size()];
+        int i = 0;
+        for(RestconfError e: from) {
+            to[i++] = RpcResultBuilder.newError(toRpcErrorType(e.getErrorType()), e.getErrorTag().getTagValue(),
+                    e.getErrorMessage());
+        }
+
+        return to;
+    }
+
+    private static ErrorType toRpcErrorType(
+            org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorType errorType) {
+        switch(errorType) {
+            case TRANSPORT: {
+                return ErrorType.TRANSPORT;
+            }
+            case RPC: {
+                return ErrorType.RPC;
+            }
+            case PROTOCOL: {
+                return ErrorType.PROTOCOL;
+            }
+            default: {
+                return ErrorType.APPLICATION;
+            }
+        }
+    }
+}
index 4fc716e78a7131122287684b6667616015029035..2fba309986ae823e3391c2dbf02f1c006e020ad5 100644 (file)
@@ -16,7 +16,7 @@ public class QueryParametersParser {
         PRETTY_PRINT("prettyPrint"),
         DEPTH("depth");
 
-        private String uriParameterName;
+        private final String uriParameterName;
 
         UriParameters(final String uriParameterName) {
             this.uriParameterName = uriParameterName;
@@ -30,6 +30,10 @@ public class QueryParametersParser {
 
     public static WriterParameters parseWriterParameters(final UriInfo info) {
         WriterParameters.WriterParametersBuilder wpBuilder = new WriterParameters.WriterParametersBuilder();
+        if(info == null) {
+            return wpBuilder.build();
+        }
+
         String param = info.getQueryParameters(false).getFirst(UriParameters.DEPTH.toString());
         if (!Strings.isNullOrEmpty(param) && !"unbounded".equals(param)) {
             try {
index bfa987ab8d4292e08da2cfd75ee5c9b7c0abd898..b013fe1d82c4a8f95165cd5274fe023981c75e7d 100644 (file)
@@ -11,13 +11,10 @@ package org.opendaylight.controller.sal.restconf.impl;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
-
 import java.util.Collection;
 import java.util.List;
-
 import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.Response.Status;
-
 import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorTag;
 import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorType;
 import org.opendaylight.yangtools.yang.common.RpcError;
@@ -62,6 +59,22 @@ public class RestconfDocumentedException extends WebApplicationException {
         this(null, new RestconfError(errorType, errorTag, message));
     }
 
+    /**
+     * Constructs an instance with an error message, error type, error tag and exception cause.
+     *
+     * @param message
+     *            A string which provides a plain text string describing the error.
+     * @param errorType
+     *            The enumerated type indicating the layer where the error occurred.
+     * @param errorTag
+     *            The enumerated tag representing a more specific error cause.
+     * @param cause
+     *            The underlying exception cause.
+     */
+    public RestconfDocumentedException(String message, ErrorType errorType, ErrorTag errorTag, Throwable cause) {
+        this(cause, new RestconfError(errorType, errorTag, message, null, RestconfError.toErrorInfo(cause)));
+    }
+
     /**
      * Constructs an instance with an error message and exception cause.
      * The stack trace of the exception is included in the error info.
index 1a47c1284273268d2cd34bff837bcbb5d1425d4c..87838872fb6d7b51bbbed4341ca9df3e1a9b2222 100644 (file)
@@ -867,7 +867,7 @@ public class RestconfImpl implements RestconfService {
             throw e;
         } catch (final Exception e) {
             final String errMsg = "Error creating data ";
-            LOG.info(errMsg + uriInfo.getPath(), e);
+            LOG.info(errMsg + (uriInfo != null ? uriInfo.getPath() : ""), e);
             throw new RestconfDocumentedException(errMsg, e);
         }
 
@@ -881,6 +881,11 @@ public class RestconfImpl implements RestconfService {
     }
 
     private URI resolveLocation(final UriInfo uriInfo, final String uriBehindBase, final DOMMountPoint mountPoint, final YangInstanceIdentifier normalizedII) {
+        if(uriInfo == null) {
+            // This is null if invoked internally
+            return null;
+        }
+
         final UriBuilder uriBuilder = uriInfo.getBaseUriBuilder();
         uriBuilder.path("config");
         try {
diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/yang/sal-restconf-service.yang b/opendaylight/md-sal/sal-rest-connector/src/main/yang/sal-restconf-service.yang
new file mode 100644 (file)
index 0000000..1f939c8
--- /dev/null
@@ -0,0 +1,30 @@
+module sal-restconf-service {
+    yang-version 1;
+    namespace "urn:opendaylight:params:xml:ns:yang:controller:sal:restconf:service";
+    prefix "sal-restconf-service";
+
+    import config { prefix config; revision-date 2013-04-05; }
+
+    description "Definition for the internal restconf service";
+
+    revision "2015-07-08" {
+        description "Initial revision";
+    }
+
+    identity json-restconf-service {
+        base "config:service-type";
+        config:java-class "org.opendaylight.controller.restconf.api.JSONRestconfService";
+    }
+
+    identity json-restconf-service-impl {
+        base config:module-type;
+        config:provided-service json-restconf-service;
+        config:java-name-prefix JSONRestconfService;
+    }
+
+    augment "/config:modules/config:module/config:configuration" {
+        case json-restconf-service-impl {
+            when "/config:modules/config:module/config:type = 'json-restconf-service-impl'";
+        }
+    }
+}
\ No newline at end of file
diff --git a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/JSONRestconfServiceImplTest.java b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/JSONRestconfServiceImplTest.java
new file mode 100644 (file)
index 0000000..a9955cd
--- /dev/null
@@ -0,0 +1,481 @@
+/*
+ * Copyright (c) 2015 Brocade Communications 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.controller.sal.restconf.impl.test;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.isNull;
+import static org.mockito.Matchers.notNull;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import com.google.common.base.Optional;
+import com.google.common.util.concurrent.Futures;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.io.IOUtils;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
+import org.opendaylight.controller.md.sal.common.api.data.TransactionCommitFailedException;
+import org.opendaylight.controller.md.sal.dom.api.DOMMountPoint;
+import org.opendaylight.controller.md.sal.dom.api.DOMMountPointService;
+import org.opendaylight.controller.md.sal.dom.api.DOMRpcException;
+import org.opendaylight.controller.md.sal.dom.api.DOMRpcImplementationNotAvailableException;
+import org.opendaylight.controller.md.sal.dom.api.DOMRpcResult;
+import org.opendaylight.controller.md.sal.dom.spi.DefaultDOMRpcResult;
+import org.opendaylight.controller.sal.restconf.impl.BrokerFacade;
+import org.opendaylight.controller.sal.restconf.impl.ControllerContext;
+import org.opendaylight.controller.sal.restconf.impl.JSONRestconfServiceImpl;
+import org.opendaylight.controller.sal.restconf.impl.RestconfImpl;
+import org.opendaylight.yangtools.yang.common.OperationFailedException;
+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.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.DataContainerNode;
+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.impl.schema.ImmutableNodes;
+import org.opendaylight.yangtools.yang.data.impl.schema.builder.impl.ImmutableContainerNodeBuilder;
+import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+import org.opendaylight.yangtools.yang.model.api.SchemaPath;
+
+/**
+ * Unit tests for JSONRestconfServiceImpl.
+ *
+ * @author Thomas Pantelis
+ */
+public class JSONRestconfServiceImplTest {
+    static final String IETF_INTERFACES_NS = "urn:ietf:params:xml:ns:yang:ietf-interfaces";
+    static final String IETF_INTERFACES_VERSION = "2013-07-04";
+    static final QName INTERFACES_QNAME = QName.create(IETF_INTERFACES_NS, IETF_INTERFACES_VERSION, "interfaces");
+    static final QName INTERFACE_QNAME = QName.create(IETF_INTERFACES_NS, IETF_INTERFACES_VERSION, "interface");
+    static final QName NAME_QNAME = QName.create(IETF_INTERFACES_NS, IETF_INTERFACES_VERSION, "name");
+    static final QName TYPE_QNAME = QName.create(IETF_INTERFACES_NS, IETF_INTERFACES_VERSION, "type");
+    static final QName ENABLED_QNAME = QName.create(IETF_INTERFACES_NS, IETF_INTERFACES_VERSION, "enabled");
+    static final QName DESC_QNAME = QName.create(IETF_INTERFACES_NS, IETF_INTERFACES_VERSION, "description");
+
+    static final String TEST_MODULE_NS = "test:module";
+    static final String TEST_MODULE_VERSION = "2014-01-09";
+    static final QName TEST_CONT_QNAME = QName.create(TEST_MODULE_NS, TEST_MODULE_VERSION, "cont");
+    static final QName TEST_CONT1_QNAME = QName.create(TEST_MODULE_NS, TEST_MODULE_VERSION, "cont1");
+    static final QName TEST_LF11_QNAME = QName.create(TEST_MODULE_NS, TEST_MODULE_VERSION, "lf11");
+    static final QName TEST_LF12_QNAME = QName.create(TEST_MODULE_NS, TEST_MODULE_VERSION, "lf12");
+
+    static final String TOASTER_MODULE_NS = "http://netconfcentral.org/ns/toaster";
+    static final String TOASTER_MODULE_VERSION = "2009-11-20";
+    static final QName TOASTER_DONENESS_QNAME = QName.create(TOASTER_MODULE_NS, TOASTER_MODULE_VERSION, "toasterDoneness");
+    static final QName TOASTER_TYPE_QNAME = QName.create(TOASTER_MODULE_NS, TOASTER_MODULE_VERSION, "toasterToastType");
+    static final QName WHEAT_BREAD_QNAME = QName.create(TOASTER_MODULE_NS, TOASTER_MODULE_VERSION, "wheat-bread");
+    static final QName MAKE_TOAST_QNAME = QName.create(TOASTER_MODULE_NS, TOASTER_MODULE_VERSION, "make-toast");
+    static final QName CANCEL_TOAST_QNAME = QName.create(TOASTER_MODULE_NS, TOASTER_MODULE_VERSION, "cancel-toast");
+    static final QName TEST_OUTPUT_QNAME = QName.create(TOASTER_MODULE_NS, TOASTER_MODULE_VERSION, "testOutput");
+    static final QName TEXT_OUT_QNAME = QName.create(TOASTER_MODULE_NS, TOASTER_MODULE_VERSION, "textOut");
+
+    private static BrokerFacade brokerFacade;
+
+    private final JSONRestconfServiceImpl service = new JSONRestconfServiceImpl();
+
+    @BeforeClass
+    public static void init() throws IOException {
+        ControllerContext.getInstance().setSchemas(TestUtils.loadSchemaContext("/full-versions/yangs"));
+        brokerFacade = mock(BrokerFacade.class);
+        RestconfImpl.getInstance().setBroker(brokerFacade);
+        RestconfImpl.getInstance().setControllerContext(ControllerContext.getInstance());
+    }
+
+    @Before
+    public void setup() {
+        reset(brokerFacade);
+    }
+
+    private String loadData(String path) throws IOException {
+        InputStream stream = JSONRestconfServiceImplTest.class.getResourceAsStream(path);
+        return IOUtils.toString(stream, "UTF-8");
+    }
+
+    @SuppressWarnings("rawtypes")
+    @Test
+    public void testPut() throws Exception {
+        doReturn(Futures.immediateCheckedFuture(null)).when(brokerFacade).commitConfigurationDataPut(
+                notNull(SchemaContext.class), notNull(YangInstanceIdentifier.class), notNull(NormalizedNode.class));
+
+        String uriPath = "ietf-interfaces:interfaces/interface/eth0";
+        String payload = loadData("/parts/ietf-interfaces_interfaces.json");
+
+        service.put(uriPath, payload);
+
+        ArgumentCaptor<YangInstanceIdentifier> capturedPath = ArgumentCaptor.forClass(YangInstanceIdentifier.class);
+        ArgumentCaptor<NormalizedNode> capturedNode = ArgumentCaptor.forClass(NormalizedNode.class);
+        verify(brokerFacade).commitConfigurationDataPut(notNull(SchemaContext.class), capturedPath.capture(),
+                capturedNode.capture());
+
+        verifyPath(capturedPath.getValue(), INTERFACES_QNAME, INTERFACE_QNAME,
+                new Object[]{INTERFACE_QNAME, NAME_QNAME, "eth0"});
+
+        assertTrue("Expected MapEntryNode. Actual " + capturedNode.getValue().getClass(),
+                capturedNode.getValue() instanceof MapEntryNode);
+        MapEntryNode actualNode = (MapEntryNode) capturedNode.getValue();
+        assertEquals("MapEntryNode node type", INTERFACE_QNAME, actualNode.getNodeType());
+        verifyLeafNode(actualNode, NAME_QNAME, "eth0");
+        verifyLeafNode(actualNode, TYPE_QNAME, "ethernetCsmacd");
+        verifyLeafNode(actualNode, ENABLED_QNAME, Boolean.FALSE);
+        verifyLeafNode(actualNode, DESC_QNAME, "some interface");
+    }
+
+    @SuppressWarnings("rawtypes")
+    @Test
+    public void testPutBehindMountPoint() throws Exception {
+        DOMMountPoint mockMountPoint = setupTestMountPoint();
+
+        doReturn(Futures.immediateCheckedFuture(null)).when(brokerFacade).commitConfigurationDataPut(
+                notNull(DOMMountPoint.class), notNull(YangInstanceIdentifier.class), notNull(NormalizedNode.class));
+
+        String uriPath = "ietf-interfaces:interfaces/yang-ext:mount/test-module:cont/cont1";
+        String payload = loadData("/full-versions/testCont1Data.json");
+
+        service.put(uriPath, payload);
+
+        ArgumentCaptor<YangInstanceIdentifier> capturedPath = ArgumentCaptor.forClass(YangInstanceIdentifier.class);
+        ArgumentCaptor<NormalizedNode> capturedNode = ArgumentCaptor.forClass(NormalizedNode.class);
+        verify(brokerFacade).commitConfigurationDataPut(same(mockMountPoint), capturedPath.capture(),
+                capturedNode.capture());
+
+        verifyPath(capturedPath.getValue(), TEST_CONT_QNAME, TEST_CONT1_QNAME);
+
+        assertTrue("Expected ContainerNode", capturedNode.getValue() instanceof ContainerNode);
+        ContainerNode actualNode = (ContainerNode) capturedNode.getValue();
+        assertEquals("ContainerNode node type", TEST_CONT1_QNAME, actualNode.getNodeType());
+        verifyLeafNode(actualNode, TEST_LF11_QNAME, "lf11 data");
+        verifyLeafNode(actualNode, TEST_LF12_QNAME, "lf12 data");
+    }
+
+    @Test(expected=TransactionCommitFailedException.class)
+    public void testPutFailure() throws Throwable {
+        doReturn(Futures.immediateFailedCheckedFuture(new TransactionCommitFailedException("mock")))
+                .when(brokerFacade).commitConfigurationDataPut(notNull(SchemaContext.class),
+                        notNull(YangInstanceIdentifier.class), notNull(NormalizedNode.class));
+
+        String uriPath = "ietf-interfaces:interfaces/interface/eth0";
+        String payload = loadData("/parts/ietf-interfaces_interfaces.json");
+
+        try {
+            service.put(uriPath, payload);
+        } catch (OperationFailedException e) {
+            assertNotNull(e.getCause());
+            throw e.getCause();
+        }
+    }
+
+    @SuppressWarnings("rawtypes")
+    @Test
+    public void testPost() throws Exception {
+        doReturn(Futures.immediateCheckedFuture(null)).when(brokerFacade).commitConfigurationDataPost(
+                any(SchemaContext.class), any(YangInstanceIdentifier.class), any(NormalizedNode.class));
+
+        String uriPath = null;
+        String payload = loadData("/parts/ietf-interfaces_interfaces_absolute_path.json");
+
+        service.post(uriPath, payload);
+
+        ArgumentCaptor<YangInstanceIdentifier> capturedPath = ArgumentCaptor.forClass(YangInstanceIdentifier.class);
+        ArgumentCaptor<NormalizedNode> capturedNode = ArgumentCaptor.forClass(NormalizedNode.class);
+        verify(brokerFacade).commitConfigurationDataPost(notNull(SchemaContext.class), capturedPath.capture(),
+                capturedNode.capture());
+
+        verifyPath(capturedPath.getValue(), INTERFACES_QNAME);
+
+        assertTrue("Expected ContainerNode", capturedNode.getValue() instanceof ContainerNode);
+        ContainerNode actualNode = (ContainerNode) capturedNode.getValue();
+        assertEquals("ContainerNode node type", INTERFACES_QNAME, actualNode.getNodeType());
+
+        Optional<DataContainerChild<?, ?>> mapChild = actualNode.getChild(new NodeIdentifier(INTERFACE_QNAME));
+        assertEquals(INTERFACE_QNAME.toString() + " present", true, mapChild.isPresent());
+        assertTrue("Expected MapNode. Actual " + mapChild.get().getClass(), mapChild.get() instanceof MapNode);
+        MapNode mapNode = (MapNode)mapChild.get();
+
+        NodeIdentifierWithPredicates entryNodeID = new NodeIdentifierWithPredicates(
+                INTERFACE_QNAME, NAME_QNAME, "eth0");
+        Optional<MapEntryNode> entryChild = mapNode.getChild(entryNodeID);
+        assertEquals(entryNodeID.toString() + " present", true, entryChild.isPresent());
+        MapEntryNode entryNode = entryChild.get();
+        verifyLeafNode(entryNode, NAME_QNAME, "eth0");
+        verifyLeafNode(entryNode, TYPE_QNAME, "ethernetCsmacd");
+        verifyLeafNode(entryNode, ENABLED_QNAME, Boolean.FALSE);
+        verifyLeafNode(entryNode, DESC_QNAME, "some interface");
+    }
+
+    @SuppressWarnings("rawtypes")
+    @Test
+    public void testPostBehindMountPoint() throws Exception {
+        DOMMountPoint mockMountPoint = setupTestMountPoint();
+
+        doReturn(Futures.immediateCheckedFuture(null)).when(brokerFacade).commitConfigurationDataPost(
+                notNull(DOMMountPoint.class), notNull(YangInstanceIdentifier.class), notNull(NormalizedNode.class));
+
+        String uriPath = "ietf-interfaces:interfaces/yang-ext:mount/test-module:cont";
+        String payload = loadData("/full-versions/testCont1Data.json");
+
+        service.post(uriPath, payload);
+
+        ArgumentCaptor<YangInstanceIdentifier> capturedPath = ArgumentCaptor.forClass(YangInstanceIdentifier.class);
+        ArgumentCaptor<NormalizedNode> capturedNode = ArgumentCaptor.forClass(NormalizedNode.class);
+        verify(brokerFacade).commitConfigurationDataPost(same(mockMountPoint), capturedPath.capture(),
+                capturedNode.capture());
+
+        verifyPath(capturedPath.getValue(), TEST_CONT_QNAME, TEST_CONT1_QNAME);
+
+        assertTrue("Expected ContainerNode", capturedNode.getValue() instanceof ContainerNode);
+        ContainerNode actualNode = (ContainerNode) capturedNode.getValue();
+        assertEquals("ContainerNode node type", TEST_CONT1_QNAME, actualNode.getNodeType());
+        verifyLeafNode(actualNode, TEST_LF11_QNAME, "lf11 data");
+        verifyLeafNode(actualNode, TEST_LF12_QNAME, "lf12 data");
+    }
+
+    @Test(expected=TransactionCommitFailedException.class)
+    public void testPostFailure() throws Throwable {
+        doReturn(Futures.immediateFailedCheckedFuture(new TransactionCommitFailedException("mock")))
+                .when(brokerFacade).commitConfigurationDataPost(any(SchemaContext.class),
+                        any(YangInstanceIdentifier.class), any(NormalizedNode.class));
+
+        String uriPath = null;
+        String payload = loadData("/parts/ietf-interfaces_interfaces_absolute_path.json");
+
+        try {
+            service.post(uriPath, payload);
+        } catch (OperationFailedException e) {
+            assertNotNull(e.getCause());
+            throw e.getCause();
+        }
+    }
+
+    @Test
+    public void testDelete() throws Exception {
+        doReturn(Futures.immediateCheckedFuture(null)).when(brokerFacade).commitConfigurationDataDelete(
+                notNull(YangInstanceIdentifier.class));
+
+        String uriPath = "ietf-interfaces:interfaces/interface/eth0";
+
+        service.delete(uriPath);
+
+        ArgumentCaptor<YangInstanceIdentifier> capturedPath = ArgumentCaptor.forClass(YangInstanceIdentifier.class);
+        verify(brokerFacade).commitConfigurationDataDelete(capturedPath.capture());
+
+        verifyPath(capturedPath.getValue(), INTERFACES_QNAME, INTERFACE_QNAME,
+                new Object[]{INTERFACE_QNAME, NAME_QNAME, "eth0"});
+    }
+
+    @Test(expected=OperationFailedException.class)
+    public void testDeleteFailure() throws Exception {
+        String invalidUriPath = "ietf-interfaces:interfaces/invalid";
+
+        service.delete(invalidUriPath);
+    }
+
+    @Test
+    public void testGetConfig() throws Exception {
+        testGet(LogicalDatastoreType.CONFIGURATION);
+    }
+
+    @Test
+    public void testGetOperational() throws Exception {
+        testGet(LogicalDatastoreType.OPERATIONAL);
+    }
+
+    @Test
+    public void testGetWithNoData() throws Exception {
+        doReturn(null).when(brokerFacade).readConfigurationData(notNull(YangInstanceIdentifier.class));
+
+        String uriPath = "ietf-interfaces:interfaces";
+
+        Optional<String> optionalResp = service.get(uriPath, LogicalDatastoreType.CONFIGURATION);
+
+        assertEquals("Response present", false, optionalResp.isPresent());
+    }
+
+    @Test(expected=OperationFailedException.class)
+    public void testGetFailure() throws Exception {
+        String invalidUriPath = "/ietf-interfaces:interfaces/invalid";
+
+        service.get(invalidUriPath, LogicalDatastoreType.CONFIGURATION);
+    }
+
+    @SuppressWarnings("rawtypes")
+    @Test
+    public void testInvokeRpcWithInput() throws Exception {
+        SchemaPath path = SchemaPath.create(true, MAKE_TOAST_QNAME);
+
+        DOMRpcResult expResult = new DefaultDOMRpcResult((NormalizedNode<?, ?>)null);
+        doReturn(Futures.immediateCheckedFuture(expResult)).when(brokerFacade).invokeRpc(eq(path),
+                any(NormalizedNode.class));
+
+        String uriPath = "toaster:make-toast";
+        String input = loadData("/full-versions/make-toast-rpc-input.json");
+
+        Optional<String> output = service.invokeRpc(uriPath, Optional.of(input));
+
+        assertEquals("Output present", false, output.isPresent());
+
+        ArgumentCaptor<NormalizedNode> capturedNode = ArgumentCaptor.forClass(NormalizedNode.class);
+        verify(brokerFacade).invokeRpc(eq(path), capturedNode.capture());
+
+        assertTrue("Expected ContainerNode. Actual " + capturedNode.getValue().getClass(),
+                capturedNode.getValue() instanceof ContainerNode);
+        ContainerNode actualNode = (ContainerNode) capturedNode.getValue();
+        verifyLeafNode(actualNode, TOASTER_DONENESS_QNAME, Long.valueOf(10));
+        verifyLeafNode(actualNode, TOASTER_TYPE_QNAME, WHEAT_BREAD_QNAME);
+    }
+
+    @Test
+    public void testInvokeRpcWithNoInput() throws Exception {
+        SchemaPath path = SchemaPath.create(true, CANCEL_TOAST_QNAME);
+
+        DOMRpcResult expResult = new DefaultDOMRpcResult((NormalizedNode<?, ?>)null);
+        doReturn(Futures.immediateCheckedFuture(expResult)).when(brokerFacade).invokeRpc(any(SchemaPath.class),
+                any(NormalizedNode.class));
+
+        String uriPath = "toaster:cancel-toast";
+
+        Optional<String> output = service.invokeRpc(uriPath, Optional.<String>absent());
+
+        assertEquals("Output present", false, output.isPresent());
+
+        verify(brokerFacade).invokeRpc(eq(path), isNull(NormalizedNode.class));
+    }
+
+    @Test
+    public void testInvokeRpcWithOutput() throws Exception {
+        SchemaPath path = SchemaPath.create(true, TEST_OUTPUT_QNAME);
+
+        NormalizedNode<?, ?> outputNode = ImmutableContainerNodeBuilder.create()
+                .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TEST_OUTPUT_QNAME))
+                .withChild(ImmutableNodes.leafNode(TEXT_OUT_QNAME, "foo")).build();
+        DOMRpcResult expResult = new DefaultDOMRpcResult(outputNode);
+        doReturn(Futures.immediateCheckedFuture(expResult)).when(brokerFacade).invokeRpc(any(SchemaPath.class),
+                any(NormalizedNode.class));
+
+        String uriPath = "toaster:testOutput";
+
+        Optional<String> output = service.invokeRpc(uriPath, Optional.<String>absent());
+
+        assertEquals("Output present", true, output.isPresent());
+        assertNotNull("Returned null response", output.get());
+        assertThat("Missing \"textOut\"", output.get(), containsString("\"textOut\":\"foo\""));
+
+        verify(brokerFacade).invokeRpc(eq(path), isNull(NormalizedNode.class));
+    }
+
+    @Test(expected=OperationFailedException.class)
+    public void testInvokeRpcFailure() throws Exception {
+        DOMRpcException exception = new DOMRpcImplementationNotAvailableException("testExeption");
+        doReturn(Futures.immediateFailedCheckedFuture(exception)).when(brokerFacade).invokeRpc(any(SchemaPath.class),
+                any(NormalizedNode.class));
+
+        String uriPath = "toaster:cancel-toast";
+
+        service.invokeRpc(uriPath, Optional.<String>absent());
+    }
+
+    void testGet(LogicalDatastoreType datastoreType) throws OperationFailedException {
+        MapEntryNode entryNode = ImmutableNodes.mapEntryBuilder(INTERFACE_QNAME, NAME_QNAME, "eth0")
+                .withChild(ImmutableNodes.leafNode(NAME_QNAME, "eth0"))
+                .withChild(ImmutableNodes.leafNode(TYPE_QNAME, "ethernetCsmacd"))
+                .withChild(ImmutableNodes.leafNode(ENABLED_QNAME, Boolean.TRUE))
+                .withChild(ImmutableNodes.leafNode(DESC_QNAME, "eth interface"))
+                .build();
+
+        if(datastoreType == LogicalDatastoreType.CONFIGURATION) {
+            doReturn(entryNode).when(brokerFacade).readConfigurationData(notNull(YangInstanceIdentifier.class));
+        } else {
+            doReturn(entryNode).when(brokerFacade).readOperationalData(notNull(YangInstanceIdentifier.class));
+        }
+
+        String uriPath = "/ietf-interfaces:interfaces/interface/eth0";
+
+        Optional<String> optionalResp = service.get(uriPath, datastoreType);
+        assertEquals("Response present", true, optionalResp.isPresent());
+        String jsonResp = optionalResp.get();
+
+        assertNotNull("Returned null response", jsonResp);
+        assertThat("Missing \"name\"", jsonResp, containsString("\"name\":\"eth0\""));
+        assertThat("Missing \"type\"", jsonResp, containsString("\"type\":\"ethernetCsmacd\""));
+        assertThat("Missing \"enabled\"", jsonResp, containsString("\"enabled\":true"));
+        assertThat("Missing \"description\"", jsonResp, containsString("\"description\":\"eth interface\""));
+
+        ArgumentCaptor<YangInstanceIdentifier> capturedPath = ArgumentCaptor.forClass(YangInstanceIdentifier.class);
+        if (datastoreType == LogicalDatastoreType.CONFIGURATION) {
+            verify(brokerFacade).readConfigurationData(capturedPath.capture());
+        } else {
+            verify(brokerFacade).readOperationalData(capturedPath.capture());
+        }
+
+        verifyPath(capturedPath.getValue(), INTERFACES_QNAME, INTERFACE_QNAME,
+                new Object[]{INTERFACE_QNAME, NAME_QNAME, "eth0"});
+    }
+
+    DOMMountPoint setupTestMountPoint() throws FileNotFoundException {
+        SchemaContext schemaContextTestModule = TestUtils.loadSchemaContext("/full-versions/test-module");
+        DOMMountPoint mockMountPoint = mock(DOMMountPoint.class);
+        doReturn(schemaContextTestModule).when(mockMountPoint).getSchemaContext();
+
+        DOMMountPointService mockMountService = mock(DOMMountPointService.class);
+        doReturn(Optional.of(mockMountPoint)).when(mockMountService).getMountPoint(notNull(YangInstanceIdentifier.class));
+
+        ControllerContext.getInstance().setMountService(mockMountService);
+        return mockMountPoint;
+    }
+
+    void verifyLeafNode(DataContainerNode<?> parent, QName leafType, Object leafValue) {
+        Optional<DataContainerChild<?, ?>> leafChild = parent.getChild(new NodeIdentifier(leafType));
+        assertEquals(leafType.toString() + " present", true, leafChild.isPresent());
+        assertEquals(leafType.toString() + " value", leafValue, leafChild.get().getValue());
+    }
+
+    void verifyPath(YangInstanceIdentifier path, Object... expArgs) {
+        List<PathArgument> pathArgs = path.getPathArguments();
+        assertEquals("Arg count for actual path " + path, expArgs.length, pathArgs.size());
+        int i = 0;
+        for(PathArgument actual: pathArgs) {
+            QName expNodeType;
+            if(expArgs[i] instanceof Object[]) {
+                Object[] listEntry = (Object[]) expArgs[i];
+                expNodeType = (QName) listEntry[0];
+
+                assertTrue(actual instanceof NodeIdentifierWithPredicates);
+                Map<QName, Object> keyValues = ((NodeIdentifierWithPredicates)actual).getKeyValues();
+                assertEquals(String.format("Path arg %d keyValues size", i + 1), 1, keyValues.size());
+                QName expKey = (QName) listEntry[1];
+                assertEquals(String.format("Path arg %d keyValue for %s", i + 1, expKey), listEntry[2],
+                        keyValues.get(expKey));
+            } else {
+                expNodeType = (QName) expArgs[i];
+            }
+
+            assertEquals(String.format("Path arg %d node type", i + 1), expNodeType, actual.getNodeType());
+            i++;
+        }
+
+    }
+}
diff --git a/opendaylight/md-sal/sal-rest-connector/src/test/resources/full-versions/make-toast-rpc-input.json b/opendaylight/md-sal/sal-rest-connector/src/test/resources/full-versions/make-toast-rpc-input.json
new file mode 100644 (file)
index 0000000..1bba146
--- /dev/null
@@ -0,0 +1,7 @@
+{
+    "input" :
+    {
+     "toaster:toasterDoneness" : "10",
+     "toaster:toasterToastType": "wheat-bread" 
+    }
+}
\ No newline at end of file
diff --git a/opendaylight/md-sal/sal-rest-connector/src/test/resources/full-versions/testCont1Data.json b/opendaylight/md-sal/sal-rest-connector/src/test/resources/full-versions/testCont1Data.json
new file mode 100644 (file)
index 0000000..c7554f7
--- /dev/null
@@ -0,0 +1,6 @@
+{
+    "cont1": {
+        "lf11": "lf11 data",
+        "lf12": "lf12 data"
+    }
+}
\ No newline at end of file