Bug 3999: Create internal service to access restconf 86/27286/1
authorTom Pantelis <tpanteli@brocade.com>
Tue, 22 Sep 2015 14:27:56 +0000 (10:27 -0400)
committerTom Pantelis <tpanteli@brocade.com>
Tue, 22 Sep 2015 14:29:26 +0000 (10:29 -0400)
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: Ia3753846756ace29bc28aa0a1fe9d474a60f80ce
Signed-off-by: Tom Pantelis <tpanteli@brocade.com>
19 files changed:
features/restconf/pom.xml
features/restconf/src/main/resources/features.xml
opendaylight/restconf/sal-rest-connector-config/pom.xml
opendaylight/restconf/sal-rest-connector-config/src/main/resources/initial/10-restconf-service.xml [new file with mode: 0644]
opendaylight/restconf/sal-rest-connector/pom.xml
opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/controller/config/yang/sal/restconf/service/JSONRestconfServiceModule.java [new file with mode: 0644]
opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/controller/config/yang/sal/restconf/service/JSONRestconfServiceModuleFactory.java [new file with mode: 0644]
opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/JsonNormalizedNodeBodyReader.java
opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/api/JSONRestconfService.java [new file with mode: 0644]
opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/JSONRestconfServiceImpl.java [new file with mode: 0644]
opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/QueryParametersParser.java
opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/RestconfDocumentedException.java
opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/RestconfImpl.java
opendaylight/restconf/sal-rest-connector/src/main/yang/sal-restconf-service.yang [new file with mode: 0644]
opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/nn/to/json/test/NnToJsonLeafrefType.java
opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/JSONRestconfServiceImplTest.java [new file with mode: 0644]
opendaylight/restconf/sal-rest-connector/src/test/resources/full-versions/make-toast-rpc-input.json [new file with mode: 0644]
opendaylight/restconf/sal-rest-connector/src/test/resources/full-versions/testCont1Data.json [new file with mode: 0644]
pom.xml

index 4a0b555eb4b054e06835f81c1067c3bf5e818ac3..74f9ffa80a60021d5027cb2fdc4ba4fef6f2ee51 100644 (file)
       <classifier>config</classifier>
     </dependency>
 
+    <dependency>
+      <groupId>org.opendaylight.netconf</groupId>
+      <artifactId>sal-rest-connector-config</artifactId>
+      <version>${restconf.version}</version>
+      <type>xml</type>
+      <classifier>configrestconfservice</classifier>
+    </dependency>
+
         <dependency>
       <groupId>com.fasterxml.jackson.core</groupId>
       <artifactId>jackson-annotations</artifactId>
index 269525826c5aaf80a010d3eef5ed6a6f76d804f0..eaaa2b0d24087daf9cae724e367f461487201890 100644 (file)
@@ -43,6 +43,7 @@
         <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.netconf/sal-rest-connector-config/${project.version}/xml/config</configfile>
+        <configfile finalname="${config.configfile.directory}/${config.restconf.service.configfile}">mvn:org.opendaylight.netconf/sal-rest-connector-config/${project.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 2525e9be5d77a415bf8825ee30536c6454d75442..dea6ea1363f1957c7c38a27d3f36d48193063616 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/restconf/sal-rest-connector-config/src/main/resources/initial/10-restconf-service.xml b/opendaylight/restconf/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 cf10586053c36fcc1c7b99677bdd5a4da00c800d..3f60223d56ee1bf4aae60f50f62b8c241aa471af 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/restconf/sal-rest-connector/src/main/java/org/opendaylight/controller/config/yang/sal/restconf/service/JSONRestconfServiceModule.java b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/controller/config/yang/sal/restconf/service/JSONRestconfServiceModule.java
new file mode 100644 (file)
index 0000000..8da5dcb
--- /dev/null
@@ -0,0 +1,23 @@
+package org.opendaylight.controller.config.yang.sal.restconf.service;
+
+import org.opendaylight.netconf.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/restconf/sal-rest-connector/src/main/java/org/opendaylight/controller/config/yang/sal/restconf/service/JSONRestconfServiceModuleFactory.java b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/controller/config/yang/sal/restconf/service/JSONRestconfServiceModuleFactory.java
new file mode 100644 (file)
index 0000000..d822afd
--- /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 Sep 22 07:17:38 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 {
+
+}
index c7ef2084f72fcb8ed7758749d744a8d9385d918b..83d2449a81a5629f8820287c3839fca5f359cf26 100644 (file)
@@ -24,6 +24,7 @@ import javax.ws.rs.ext.MessageBodyReader;
 import javax.ws.rs.ext.Provider;
 import org.opendaylight.netconf.sal.rest.api.Draft02;
 import org.opendaylight.netconf.sal.rest.api.RestconfService;
+import org.opendaylight.netconf.sal.restconf.impl.ControllerContext;
 import org.opendaylight.netconf.sal.restconf.impl.InstanceIdentifierContext;
 import org.opendaylight.netconf.sal.restconf.impl.NormalizedNodeContext;
 import org.opendaylight.netconf.sal.restconf.impl.RestconfDocumentedException;
@@ -67,76 +68,100 @@ public class JsonNormalizedNodeBodyReader extends AbstractIdentifierAwareJaxRsPr
             final MultivaluedMap<String, String> httpHeaders, final InputStream entityStream) throws IOException,
             WebApplicationException {
         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/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/api/JSONRestconfService.java b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/api/JSONRestconfService.java
new file mode 100644 (file)
index 0000000..6a16a5f
--- /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.netconf.sal.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;
+}
diff --git a/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/JSONRestconfServiceImpl.java b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/JSONRestconfServiceImpl.java
new file mode 100644 (file)
index 0000000..cfe12c7
--- /dev/null
@@ -0,0 +1,216 @@
+/*
+ * 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.netconf.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.netconf.sal.rest.impl.JsonNormalizedNodeBodyReader;
+import org.opendaylight.netconf.sal.rest.impl.NormalizedNodeJsonBodyWriter;
+import org.opendaylight.netconf.sal.restconf.api.JSONRestconfService;
+import org.opendaylight.netconf.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(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 05222d8dfdd1e517b63a29686191f98204ad8fb5..082ba18e81e183b4db3d40c80b83fddd2e7a58f2 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 90752f9460e570f876bfd8aa00c7b930131a5d54..c50ea8cc7788a869f8f05b960d6aaf6af366fd3f 100644 (file)
@@ -11,13 +11,10 @@ package org.opendaylight.netconf.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.netconf.sal.restconf.impl.RestconfError.ErrorTag;
 import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorType;
 import org.opendaylight.yangtools.yang.common.RpcError;
@@ -48,6 +45,22 @@ public class RestconfDocumentedException extends WebApplicationException {
         this(message, RestconfError.ErrorType.APPLICATION, RestconfError.ErrorTag.OPERATION_FAILED);
     }
 
+    /**
+     * 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, error type, and error tag.
      *
index db7ff809411b2234bc05118eac00f67dd62f9794..a0f6cb31d4534c2a5ebe44ff8f254da8e761cf85 100644 (file)
@@ -92,7 +92,6 @@ import org.opendaylight.yangtools.yang.model.api.SchemaContext;
 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
 import org.opendaylight.yangtools.yang.model.util.EmptyType;
-import org.opendaylight.yangtools.yang.model.util.SchemaContextUtil;
 import org.opendaylight.yangtools.yang.parser.builder.api.GroupingBuilder;
 import org.opendaylight.yangtools.yang.parser.builder.impl.ContainerSchemaNodeBuilder;
 import org.opendaylight.yangtools.yang.parser.builder.impl.LeafSchemaNodeBuilder;
@@ -630,17 +629,8 @@ public class RestconfImpl implements RestconfService {
 
         final DOMRpcResult result = checkRpcResponse(response);
 
-        DataSchemaNode resultNodeSchema = null;
-        NormalizedNode<?, ?> resultData = null;
-        if (result != null && result.getResult() != null) {
-            resultData = result.getResult();
-            final ContainerSchemaNode rpcDataSchemaNode =
-                    SchemaContextUtil.getRpcDataSchema(schemaContext, rpc.getOutput().getPath());
-            resultNodeSchema = rpcDataSchemaNode.getDataChildByName(result.getResult().getNodeType());
-        }
-
-        return new NormalizedNodeContext(new InstanceIdentifierContext<>(null, resultNodeSchema, mountPoint,
-                schemaContext), resultData, QueryParametersParser.parseWriterParameters(uriInfo));
+        return new NormalizedNodeContext(new InstanceIdentifierContext<>(null, rpc, mountPoint, schemaContext),
+                result.getResult(), QueryParametersParser.parseWriterParameters(uriInfo));
     }
 
     private RpcDefinition findRpc(final SchemaContext schemaContext, final String identifierDecoded) {
@@ -899,6 +889,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/restconf/sal-rest-connector/src/main/yang/sal-restconf-service.yang b/opendaylight/restconf/sal-rest-connector/src/main/yang/sal-restconf-service.yang
new file mode 100644 (file)
index 0000000..e918b3f
--- /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.netconf.sal.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
index bddb8016836c5679aeefce84cc56638f4b1fef78..62d295cf9e4e910ecae4d620c404295ff8bfdafa 100644 (file)
@@ -18,8 +18,8 @@ import javax.ws.rs.core.MediaType;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.opendaylight.controller.md.sal.rest.common.TestRestconfUtils;
-import org.opendaylight.netconf.sal.rest.impl.NormalizedNodeJsonBodyWriter;
 import org.opendaylight.controller.sal.rest.impl.test.providers.AbstractBodyReaderTest;
+import org.opendaylight.netconf.sal.rest.impl.NormalizedNodeJsonBodyWriter;
 import org.opendaylight.netconf.sal.restconf.impl.NormalizedNodeContext;
 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
 
@@ -71,7 +71,7 @@ public class NnToJsonLeafrefType extends AbstractBodyReaderTest {
     public void leafrefFromLeafListToLeafTest() throws Exception {
         final String json = toJson("/nn-to-json/leafref/xml/data_relativ_ref_from_leaflist_to_existing_leaf.xml");
         validateJson(
-                ".*\"cont-augment-module\\p{Blank}*:\\p{Blank}*lflst1\":\\p{Blank}*.*\"346\",*\"347\",*\"345\".*",
+                ".*\"cont-augment-module\\p{Blank}*:\\p{Blank}*lflst1\":\\p{Blank}*.*\"34[5|6|7]\",*\"34[5|6|7]\",*\"34[5|6|7]\".*",
                 json);
     }
 
diff --git a/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/JSONRestconfServiceImplTest.java b/opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/JSONRestconfServiceImplTest.java
new file mode 100644 (file)
index 0000000..41d06df
--- /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.netconf.sal.restconf.impl.BrokerFacade;
+import org.opendaylight.netconf.sal.restconf.impl.ControllerContext;
+import org.opendaylight.netconf.sal.restconf.impl.JSONRestconfServiceImpl;
+import org.opendaylight.netconf.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/restconf/sal-rest-connector/src/test/resources/full-versions/make-toast-rpc-input.json b/opendaylight/restconf/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/restconf/sal-rest-connector/src/test/resources/full-versions/testCont1Data.json b/opendaylight/restconf/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
diff --git a/pom.xml b/pom.xml
index 3f14c6fa98cf2bff511e235bb6959b82a468c0ae..643acd813853b400ea535b6503a4b7fc2edb7fde 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -27,6 +27,7 @@
         <config.netconf.topology.configfile>02-netconf-topology.xml</config.netconf.topology.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>
 
         <aaa.version>0.3.0-SNAPSHOT</aaa.version>