Updated RESTCONF implementation 75/1875/4
authorTony Tkacik <ttkacik@cisco.com>
Mon, 14 Oct 2013 12:43:05 +0000 (14:43 +0200)
committerGerrit Code Review <gerrit@opendaylight.org>
Mon, 14 Oct 2013 17:21:54 +0000 (17:21 +0000)
  - Restconf implementation now implements new SAL readign contracts
    in form of <String,CompositeNode> where String is Restconf version
    of instance identifier.

    Translation of Restconf InstanceIdentifier into Binding-Independent
    Restconf Instance-Identifier is done by ControllerContext class
    which implements stateful translation of identifier based on schema
    context.

    After translation of instence-identifier most of actions maps
    directly to Binding-Independent APIs.

Change-Id: I248470e282826a3f04f6e99f54aae8fd14ea2eb3
Signed-off-by: Tony Tkacik <ttkacik@cisco.com>
opendaylight/md-sal/sal-binding-broker/pom.xml
opendaylight/md-sal/sal-rest-connector/pom.xml
opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/BrokerFacade.xtend [new file with mode: 0644]
opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/ControllerContext.xtend [new file with mode: 0644]
opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/MediaTypes.java [new file with mode: 0644]
opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfImpl.xtend [new file with mode: 0644]
opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfService.java [new file with mode: 0644]

index 34c5643..2c4f9c6 100644 (file)
@@ -19,7 +19,6 @@
             <plugin>
                 <groupId>org.apache.felix</groupId>
                 <artifactId>maven-bundle-plugin</artifactId>
-                <version>${bundle.plugin.version}</version>
                 <extensions>true</extensions>
                 <configuration>
                     <instructions>
index bee4280..b28e967 100644 (file)
@@ -1,24 +1,64 @@
 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
-    <modelVersion>4.0.0</modelVersion>
-    <parent>
-        <groupId>org.opendaylight.controller</groupId>
-        <artifactId>sal-parent</artifactId>
-        <version>1.0-SNAPSHOT</version>
-    </parent>
-    <artifactId>sal-rest-connector</artifactId>
-    <scm>
-      <connection>scm:git:ssh://git.opendaylight.org:29418/controller.git</connection>
-      <developerConnection>scm:git:ssh://git.opendaylight.org:29418/controller.git</developerConnection>
-      <url>https://wiki.opendaylight.org/view/OpenDaylight_Controller:MD-SAL</url>
-    </scm>
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.opendaylight.controller</groupId>
+    <artifactId>sal-parent</artifactId>
+    <version>1.0-SNAPSHOT</version>
+  </parent>
+  <artifactId>sal-rest-connector</artifactId>
+  <scm>
+    <connection>scm:git:ssh://git.opendaylight.org:29418/controller.git</connection>
+    <developerConnection>scm:git:ssh://git.opendaylight.org:29418/controller.git</developerConnection>
+    <url>https://wiki.opendaylight.org/view/OpenDaylight_Controller:MD-SAL</url>
+  </scm>
 
-   <dependencies>
-        <dependency>
-            <groupId>${project.groupId}</groupId>
-            <artifactId>sal-connector-api</artifactId>
-        </dependency>
-    </dependencies>
+  <dependencies>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>sal-core-api</artifactId>
+      <version>1.0-SNAPSHOT</version>
+    </dependency>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>sal-connector-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.eclipse.xtend</groupId>
+      <artifactId>org.eclipse.xtend.lib</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.jboss.resteasy</groupId>
+      <artifactId>jaxrs-api</artifactId>
+      <version>3.0.4.Final</version>
+      <scope>provided</scope>
+    </dependency>
+  </dependencies>
 
-    <packaging>bundle</packaging>
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.eclipse.xtend</groupId>
+        <artifactId>xtend-maven-plugin</artifactId>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.felix</groupId>
+        <artifactId>maven-bundle-plugin</artifactId>
+        <extensions>true</extensions>
+        <configuration>
+          <instructions>
+            <Bundle-Name>MD SAL Restconf Connector</Bundle-Name>
+            <Private-Package>
+              org.opendaylight.controller.sal.rest.*,
+              org.opendaylight.controller.sal.restconf.impl,
+              org.eclipse.xtend2.lib,
+              org.eclipse.xtend.lib,
+              org.eclipse.xtext.xbase.*
+            </Private-Package>
+          </instructions>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+  <packaging>bundle</packaging>
 </project>
diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/BrokerFacade.xtend b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/BrokerFacade.xtend
new file mode 100644 (file)
index 0000000..4c6eb3b
--- /dev/null
@@ -0,0 +1,65 @@
+package org.opendaylight.controller.sal.restconf.impl
+
+import org.opendaylight.controller.md.sal.common.api.data.DataReader
+import java.net.URI
+import org.opendaylight.yangtools.yang.data.api.CompositeNode
+import org.opendaylight.controller.sal.core.api.data.DataBrokerService
+import org.opendaylight.controller.sal.core.api.model.SchemaService
+import static com.google.common.base.Preconditions.*;
+import org.opendaylight.controller.sal.core.api.Broker.ConsumerSession
+import org.opendaylight.yangtools.yang.common.RpcResult
+import org.opendaylight.controller.md.sal.common.api.data.DataModificationTransactionFactory
+
+class BrokerFacade implements DataReader<String, CompositeNode> {
+
+    @Property
+    private ConsumerSession context;
+    
+    @Property
+    private DataBrokerService dataService;
+    
+    @Property
+    private SchemaService schemaService;
+
+    @Property
+    private extension ControllerContext schemaContext;
+
+
+    def void init() {
+        checkState(dataService !== null)
+        checkState(schemaService !== null)
+        schemaContext = new ControllerContext();
+        schemaContext.schemas = schemaService.globalContext;
+    }
+
+    override readConfigurationData(String path) {
+        val processedPath = path.removePrefixes();
+        return dataService.readConfigurationData(processedPath.toInstanceIdentifier);
+    }
+
+    override readOperationalData(String path) {
+        val processedPath = path.removePrefixes();
+        return dataService.readOperationalData(processedPath.toInstanceIdentifier);
+    }
+    
+    def RpcResult<CompositeNode> invokeRpc(String type,CompositeNode payload) {
+        val future = context.rpc(type.toRpcQName(),payload);
+        return future.get;
+    }
+    
+    def commitConfigurationDataUpdate(String path, CompositeNode payload) {
+        val transaction = dataService.beginTransaction;
+        transaction.putConfigurationData(path.toInstanceIdentifier,payload);
+        return transaction.commit()
+    }
+    
+    def commitConfigurationDataCreate(String path, CompositeNode payload) {
+        val transaction = dataService.beginTransaction;
+        transaction.putConfigurationData(path.toInstanceIdentifier,payload);
+        return transaction.commit()
+    }
+    
+    private def String removePrefixes(String path) {
+        return path;
+    }
+}
diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/ControllerContext.xtend b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/ControllerContext.xtend
new file mode 100644 (file)
index 0000000..daad4cf
--- /dev/null
@@ -0,0 +1,212 @@
+package org.opendaylight.controller.sal.restconf.impl
+
+import org.opendaylight.yangtools.yang.model.api.SchemaContext
+import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier
+import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier.PathArgument
+import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier.NodeIdentifier
+import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier.NodeIdentifierWithPredicates
+import java.net.URI
+import java.util.Map
+import java.util.HashMap
+import org.opendaylight.yangtools.yang.common.QName
+import org.opendaylight.yangtools.yang.model.api.ChoiceNode
+import org.opendaylight.yangtools.yang.model.api.DataSchemaNode
+import org.opendaylight.yangtools.yang.model.api.ChoiceCaseNode
+import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode
+import org.opendaylight.yangtools.yang.model.api.ListSchemaNode
+
+import java.net.URLEncoder
+import org.opendaylight.yangtools.yang.model.api.DataNodeContainer
+import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier.InstanceIdentifierBuilder
+import java.util.List
+import static com.google.common.base.Preconditions.*;
+import com.google.common.collect.Collections2
+import com.google.common.collect.BiMap
+import com.google.common.collect.HashBiMap
+import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode
+import java.net.URLDecoder
+
+class ControllerContext {
+
+    @Property
+    SchemaContext schemas;
+
+    private val BiMap<URI, String> uriToModuleName = HashBiMap.create();
+    private val Map<String, URI> moduleNameToUri = uriToModuleName.inverse();
+
+    public def InstanceIdentifier toInstanceIdentifier(String restconfInstance) {
+        val ret = InstanceIdentifier.builder();
+        val pathArgs = restconfInstance.split("/");
+        val first = pathArgs.get(0);
+        val startModule = first.toModuleName();
+        val module = schemas.findModuleByNamespace(moduleNameToUri.get(startModule));
+        checkArgument(module.size == 1); // Only one version supported now
+        ret.collectPathArguments(pathArgs, module.iterator.next);
+        return ret.toInstance
+    }
+
+    def String toFullRestconfIdentifier(InstanceIdentifier path) {
+        val elements = path.path;
+        val ret = new StringBuilder();
+        val startQName = elements.get(0).nodeType;
+        val initialModule = schemas.findModuleByNamespaceAndRevision(startQName.namespace, startQName.revision)
+        var node = initialModule as DataSchemaNode;
+        for (element : elements) {
+            node = node.childByQName(element.nodeType);
+            ret.append(element.toRestconfIdentifier(node));
+        }
+        return ret.toString
+    }
+
+    private def dispatch CharSequence toRestconfIdentifier(NodeIdentifier argument, DataSchemaNode node) {
+        '''/«argument.nodeType.toRestconfIdentifier()»'''
+    }
+
+    private def dispatch CharSequence toRestconfIdentifier(NodeIdentifierWithPredicates argument, ListSchemaNode node) {
+        val nodeIdentifier = argument.nodeType.toRestconfIdentifier();
+        val keyValues = argument.keyValues;
+        return '''/«nodeIdentifier»/«FOR key : node.keyDefinition SEPARATOR "/"»«keyValues.get(key).toUriString»«ENDFOR»'''
+    }
+
+    private def dispatch CharSequence toRestconfIdentifier(PathArgument argument, DataSchemaNode node) {
+        throw new IllegalArgumentException("Conversion of generic path argument is not supported");
+    }
+
+    public def CharSequence toRestconfIdentifier(QName qname) {
+        var module = uriToModuleName.get(qname.namespace)
+        if (module == null) {
+            val moduleSchema = schemas.findModuleByNamespaceAndRevision(qname.namespace, qname.revision);
+            if(moduleSchema == null) throw new IllegalArgumentException()
+            uriToModuleName.put(qname.namespace, moduleSchema.name)
+            module = moduleSchema.name;
+        }
+        return '''«module»:«qname.localName»''';
+    }
+
+    private static dispatch def DataSchemaNode childByQName(ChoiceNode container, QName name) {
+        for (caze : container.cases) {
+            val ret = caze.childByQName(name)
+            if (ret !== null) {
+                return ret;
+            }
+        }
+        return null;
+    }
+
+    private static dispatch def DataSchemaNode childByQName(ChoiceCaseNode container, QName name) {
+        val ret = container.getDataChildByName(name);
+        return ret;
+    }
+
+    private static dispatch def DataSchemaNode childByQName(ContainerSchemaNode container, QName name) {
+        return container.dataNodeChildByQName(name);
+    }
+
+    private static dispatch def DataSchemaNode childByQName(ListSchemaNode container, QName name) {
+        return container.dataNodeChildByQName(name);
+    }
+
+    private static dispatch def DataSchemaNode childByQName(DataSchemaNode container, QName name) {
+        return null;
+    }
+
+    private static def DataSchemaNode dataNodeChildByQName(DataNodeContainer container, QName name) {
+        var ret = container.getDataChildByName(name);
+        if (ret === null) {
+
+            // Find in Choice Cases
+            for (node : container.childNodes) {
+                if (node instanceof ChoiceCaseNode) {
+                    val caseNode = (node as ChoiceCaseNode);
+                    ret = caseNode.childByQName(name);
+                    if (ret !== null) {
+                        return ret;
+                    }
+                }
+            }
+        }
+        return ret;
+    }
+
+    private def toUriString(Object object) {
+        if(object == null) return "";
+        return URLEncoder.encode(object.toString)
+    }
+
+    def void collectPathArguments(InstanceIdentifierBuilder builder, List<String> strings, DataNodeContainer parentNode) {
+        checkNotNull(strings)
+        if (strings.length == 0) {
+            return;
+        }
+        val nodeRef = strings.get(0);
+
+        //val moduleName = nodeRef.toModuleName();
+        val nodeName = nodeRef.toNodeName();
+        val naiveTargetNode = parentNode.getDataChildByName(nodeName);
+
+        //var URI namespace;
+        var DataSchemaNode targetNode = naiveTargetNode;
+
+        /*if(moduleName !== null) {
+            namespace = moduleNameToUri.get(moduleName);
+            
+        }*/
+        // Number of consumed elements
+        var consumed = 1;
+        if (targetNode instanceof ListSchemaNode) {
+            val listNode = targetNode as ListSchemaNode;
+            val keysSize = listNode.keyDefinition.size
+            val uriKeyValues = strings.subList(1, keysSize);
+            val keyValues = new HashMap<QName, Object>();
+            var i = 0;
+            for (key : listNode.keyDefinition) {
+                val uriKeyValue = uriKeyValues.get(i);
+                keyValues.addKeyValue(listNode.getDataChildByName(key), uriKeyValue);
+                i = i + 1;
+            }
+            consumed = consumed + i;
+            builder.nodeWithKey(targetNode.QName, keyValues);
+        } else {
+
+            // Only one instance of node is allowed
+            builder.node(targetNode.QName);
+        }
+        if (targetNode instanceof DataNodeContainer) {
+            val remaining = strings.subList(consumed, strings.length - 1);
+            builder.collectPathArguments(remaining, targetNode as DataNodeContainer);
+        }
+    }
+
+    def void addKeyValue(HashMap<QName, Object> map, DataSchemaNode node, String uriValue) {
+        checkNotNull(uriValue);
+        checkArgument(node instanceof LeafSchemaNode);
+        val decoded = URLDecoder.decode(uriValue);
+        map.put(node.QName, decoded);
+
+    }
+
+    def String toModuleName(String str) {
+        if (str.contains(":")) {
+            val args = str.split(":");
+            checkArgument(args.size === 2);
+            return args.get(0);
+        } else {
+            return null;
+        }
+    }
+
+    def String toNodeName(String str) {
+        if (str.contains(":")) {
+            val args = str.split(":");
+            checkArgument(args.size === 2);
+            return args.get(1);
+        } else {
+            return str;
+        }
+    }
+    
+    public def QName toRpcQName(String name) {
+        
+        
+    }
+}
diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/MediaTypes.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/MediaTypes.java
new file mode 100644 (file)
index 0000000..af18828
--- /dev/null
@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) 2013 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.controller.sal.restconf.impl;
+
+public class MediaTypes {
+    public static final String API = "application/vnd.yang.api";
+    public static final String DATASTORE = "application/vnd.yang.datastore";
+    public static final String DATA = "application/vnd.yang.data";
+    public static final String EVENT = "application/vnd.yang.event";
+    public static final String OPERATION = "application/vnd.yang.operation";
+    public static final String PATCH = "application/vnd.yang.patch";
+}
diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfImpl.xtend b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfImpl.xtend
new file mode 100644 (file)
index 0000000..4a23c76
--- /dev/null
@@ -0,0 +1,41 @@
+package org.opendaylight.controller.sal.restconf.impl
+
+import org.opendaylight.yangtools.yang.data.api.CompositeNode
+
+class RestconfImpl implements RestconfService {
+    
+    @Property
+    BrokerFacade broker;
+    
+    override readAllData() {
+        return broker.readOperationalData("");
+    }
+    
+    
+    override getModules() {
+        throw new UnsupportedOperationException("TODO: auto-generated method stub")
+    }
+    
+    override getRoot() {
+        throw new UnsupportedOperationException("TODO: auto-generated method stub")
+        
+    }
+    
+    override readData(String identifier) {
+        return broker.readOperationalData(identifier);
+    }
+    
+    override createConfigurationData(String identifier, CompositeNode payload) {
+        return broker.commitConfigurationDataCreate(identifier,payload);
+    }
+    
+    
+    override updateConfigurationData(String identifier, CompositeNode payload) {
+        return broker.commitConfigurationDataCreate(identifier,payload);
+    }
+    
+    override invokeRpc(String identifier, CompositeNode payload) {
+        return broker.invokeRpc(identifier,payload);
+    }
+    
+}
\ No newline at end of file
diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfService.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfService.java
new file mode 100644 (file)
index 0000000..beea6b8
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2013 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.controller.sal.restconf.impl;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+
+import org.opendaylight.yangtools.yang.common.RpcResult;
+import org.opendaylight.yangtools.yang.data.api.CompositeNode;
+
+import static org.opendaylight.controller.sal.restconf.impl.MediaTypes.*;
+
+/**
+ *   The URI hierarchy for the RESTCONF resources consists of an entry
+ *   point container, 3 top-level resources, and 1 field.  Refer to
+ *  Section 5 for details on each URI.
+ *    <ul>
+ *    <li><b>/restconf</b> - {@link #getRoot()}
+ *     <ul><li><b>/datastore</b> - {@link #readAllData()}
+ *         <ul>
+ *            <li>/(top-level-data-nodes) (config=true or false)
+ *         </ul>
+ *         <li>/modules
+ *          <ul><li>/module
+ *              <li>/name
+ *              <li>/revision
+ *              <li>/namespace
+ *              <li>/feature
+ *             <li>/deviation
+ *          </ul>
+ *          <li>/operations
+ *          <ul>
+ *             <li>/(custom protocol operations)
+ *          </ul>
+ *         <li>/version (field)
+ *     </ul>
+ */
+@Path("restconf")
+public interface RestconfService {
+
+    public static final String XML = "+xml";
+    public static final String JSON = "+json";
+    
+    @GET
+    public Object getRoot();
+    
+    @GET
+    @Path("/datastore")
+    @Produces({API+JSON,API+XML})
+    public Object readAllData();
+    
+    @GET
+    @Path("/datastore/{identifier}")
+    @Produces({API+XML})
+    public Object readData(@PathParam("identifier") String identifier);
+    
+    @PUT
+    @Path("/datastore/{identifier}")
+    @Produces({API+XML})
+    public Object createConfigurationData(@PathParam("identifier") String identifier, CompositeNode payload);
+    
+    @POST
+    @Path("/datastore/{identifier}")
+    @Produces({API+XML})
+    public Object updateConfigurationData(@PathParam("identifier") String identifier, CompositeNode payload);
+    
+    
+    
+    @GET
+    @Path("/modules")
+    @Produces(API+XML)
+    public Object getModules();
+    
+    @POST
+    @Path("/operations/{identifier}")
+    @Produces(API+XML)
+    public RpcResult<CompositeNode> invokeRpc(@PathParam("identifier") String identifier, CompositeNode payload);
+}