ODTN manual testing tool for OpenConfig configuration

- ONOS-7567

Example:
onos> odtn-manual-test ENABLE_TRANSCEIVER
JSON:
{
  "openconfig-platform:component" : [ {
    "name" : "TRANSCEIVER_1_1_4_1",
    "openconfig-platform-transceiver:transceiver" : {
      "config" : {
        "enabled" : "true"
      }
    },
    "config" : {
      "name" : "TRANSCEIVER_1_1_4_1"
    }
  } ]
}
XML:
<rpc xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
  <edit-config>
    <target>
      <running/>
    </target>
    <config xmlns:xc="urn:ietf:params:xml:ns:netconf:base:1.0">
      <component xmlns="http://openconfig.net/yang/platform" xc:operation="merge">
        <name>TRANSCEIVER_1_1_4_1</name>
        <transceiver xmlns="http://openconfig.net/yang/platform/transceiver">
          <config>
            <enabled>true</enabled>
          </config>
        </transceiver>
        <config>
          <name>TRANSCEIVER_1_1_4_1</name>
        </config>
      </component>
    </config>
  </edit-config>
</rpc>

Change-Id: Ief5f1a1933fb00a2118bd941d8c5f0310ed9c815
diff --git a/apps/odtn/BUCK b/apps/odtn/BUCK
index 60c82cb..eef902a 100644
--- a/apps/odtn/BUCK
+++ b/apps/odtn/BUCK
@@ -22,7 +22,11 @@
     '//models/openconfig:onos-models-openconfig',
     '//apps/yang:onos-apps-yang',
     '//incubator/api:onos-incubator-api',
-    '//lib:COMPILE'
+    '//lib:org.apache.karaf.shell.console',
+    '//cli:onos-cli',
+    '//lib:JACKSON',
+    '//lib:COMPILE',
+    '//protocols/netconf/api:onos-protocols-netconf-api'
 ]
 
 TEST_DEPS = [
diff --git a/apps/odtn/pom.xml b/apps/odtn/pom.xml
index 8a7dc4e..d4c6842 100644
--- a/apps/odtn/pom.xml
+++ b/apps/odtn/pom.xml
@@ -72,6 +72,18 @@
         </dependency>
 
         <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-cli</artifactId>
+            <version>${onos.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.karaf.shell</groupId>
+            <artifactId>org.apache.karaf.shell.console</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
             <scope>test</scope>
@@ -89,6 +101,13 @@
             <artifactId>org.apache.felix.scr.annotations</artifactId>
             <scope>provided</scope>
         </dependency>
+
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-protocols-netconf-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
     </dependencies>
 
     <build>
diff --git a/apps/odtn/src/main/java/org/onosproject/odtn/cli/impl/ModeCompleter.java b/apps/odtn/src/main/java/org/onosproject/odtn/cli/impl/ModeCompleter.java
new file mode 100644
index 0000000..f00a3f8
--- /dev/null
+++ b/apps/odtn/src/main/java/org/onosproject/odtn/cli/impl/ModeCompleter.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.odtn.cli.impl;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+
+import org.onosproject.cli.AbstractChoicesCompleter;
+import org.onosproject.odtn.cli.impl.OdtnManualTestCommand.Mode;
+
+public class ModeCompleter extends AbstractChoicesCompleter {
+
+    @Override
+    protected List<String> choices() {
+        return Arrays.asList(Mode.values())
+                .stream()
+                .map(Enum::name)
+                .collect(Collectors.toList());
+    }
+}
diff --git a/apps/odtn/src/main/java/org/onosproject/odtn/cli/impl/OdtnManualTestCommand.java b/apps/odtn/src/main/java/org/onosproject/odtn/cli/impl/OdtnManualTestCommand.java
new file mode 100644
index 0000000..4f1ed4a
--- /dev/null
+++ b/apps/odtn/src/main/java/org/onosproject/odtn/cli/impl/OdtnManualTestCommand.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.odtn.cli.impl;
+
+import static org.onosproject.odtn.utils.YangToolUtil.toCharSequence;
+import static org.onosproject.odtn.utils.YangToolUtil.toCompositeData;
+import static org.onosproject.odtn.utils.YangToolUtil.toDocument;
+import static org.onosproject.odtn.utils.YangToolUtil.toJsonCompositeStream;
+import static org.onosproject.odtn.utils.YangToolUtil.toJsonNode;
+import static org.onosproject.odtn.utils.YangToolUtil.toResourceData;
+import static org.onosproject.odtn.utils.YangToolUtil.toXmlCompositeStream;
+import static org.slf4j.LoggerFactory.getLogger;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.regex.Pattern;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.apache.karaf.shell.commands.Option;
+import org.onlab.util.XmlString;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.cli.net.DeviceIdCompleter;
+import org.onosproject.config.DynamicConfigService;
+import org.onosproject.net.DeviceId;
+import org.onosproject.netconf.NetconfController;
+import org.onosproject.netconf.NetconfDevice;
+import org.onosproject.netconf.NetconfException;
+import org.onosproject.odtn.cli.impl.sub.OpticalChannel;
+import org.onosproject.odtn.cli.impl.sub.Transceiver;
+import org.onosproject.yang.model.DataNode;
+import org.onosproject.yang.model.ResourceId;
+import org.slf4j.Logger;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+
+
+@Command(scope = "onos", name = "odtn-manual-test",
+         description = "ODTN manual test command")
+public class OdtnManualTestCommand extends AbstractShellCommand {
+
+    private static final Logger log = getLogger(OdtnManualTestCommand.class);
+
+    public static enum Mode {
+        ENABLE_TRANSCEIVER,
+        DISABLE_TRANSCEIVER,
+        PRECONF_TRANSCEIVER,
+        PRECONF_OPTICAL_CHANNEL,
+    }
+
+    ModeCompleter modeCompleter;
+    @Argument(index = 0, name = "mode", description = "one of Mode see source",
+              required = true)
+    String modeStr = Mode.ENABLE_TRANSCEIVER.name();
+    Mode mode;
+
+    // injecting dependency for OSGi package import generation purpose
+    DeviceIdCompleter uriCompleter;
+    @Option(name = "--deviceId", description = "Device ID URI to send configuration to",
+              required = false)
+    String uri = null;
+
+    // TODO add completer for this?
+    @Option(name = "--component",
+            description = "Component name",
+            required = false, multiValued = false)
+    private String componentName = "TRANSCEIVER_1_1_4_1";
+
+
+    // OSGi Services to be filled in at the beginning.
+    private DynamicConfigService dcs;
+
+
+    void printlog(String format, Object... objs) {
+        print(format.replaceAll(Pattern.quote("{}"), "%s"), objs);
+        log.info(format, objs);
+    }
+
+    @Override
+    protected void execute() {
+        dcs = get(DynamicConfigService.class);
+
+        try {
+            mode = Mode.valueOf(modeStr);
+        } catch (IllegalArgumentException e) {
+            printlog("{} is not a valid Mode, pick one of {}",
+                     modeStr,
+                     Arrays.asList(Mode.values()),
+                     e);
+            return;
+        }
+
+        // effectively configuration context
+        List<DataNode> nodes = new ArrayList<>();
+
+        switch (mode) {
+        case PRECONF_TRANSCEIVER:
+            nodes.addAll(Transceiver.preconf(componentName));
+            break;
+
+        case ENABLE_TRANSCEIVER:
+            nodes.addAll(Transceiver.enable(componentName, true));
+            break;
+
+        case DISABLE_TRANSCEIVER:
+            nodes.addAll(Transceiver.enable(componentName, false));
+            break;
+
+        case PRECONF_OPTICAL_CHANNEL:
+            nodes.addAll(OpticalChannel.preconf(componentName));
+            break;
+
+        default:
+            printlog("Mode {} not supported yet", mode);
+            break;
+        }
+
+        // Do something about it.
+        doTheMagic(nodes);
+    }
+
+    void doTheMagic(List<DataNode> nodes) {
+
+        Document doc;
+        try {
+            doc = DocumentBuilderFactory.newInstance()
+                    .newDocumentBuilder().newDocument();
+        } catch (ParserConfigurationException e) {
+            printlog("Unexpected error", e);
+            throw new RuntimeException(e);
+        }
+
+        // netconf rpc boilerplate part without message-id
+        Element rpc = doc.createElementNS("urn:ietf:params:xml:ns:netconf:base:1.0", "rpc");
+        doc.appendChild(rpc);
+        Element editConfig = doc.createElement("edit-config");
+        rpc.appendChild(editConfig);
+        Element target = doc.createElement("target");
+        editConfig.appendChild(target);
+        target.appendChild(doc.createElement("running"));
+
+        Element config = doc.createElement("config");
+        config.setAttributeNS("http://www.w3.org/2000/xmlns/",
+                            "xmlns:xc",
+                            "urn:ietf:params:xml:ns:netconf:base:1.0");
+        editConfig.appendChild(config);
+
+
+        for (DataNode node : nodes) {
+            // empty resourceId assuming node is root of cfg tree
+            ResourceId resourceId = ResourceId.builder().build();
+            Document ldoc = toDocument(toXmlCompositeStream(toCompositeData(toResourceData(resourceId, node))));
+            Element cfgRoot = ldoc.getDocumentElement();
+
+            // is everything as merge, ok?
+            cfgRoot.setAttribute("xc:operation", "merge");
+
+            // move (or copy) node to another Document
+            config.appendChild(Optional.ofNullable(doc.adoptNode(cfgRoot))
+                             .orElseGet(() -> doc.importNode(cfgRoot, true)));
+
+            // don't have good use for JSON for now
+            JsonNode json = toJsonNode(toJsonCompositeStream(toCompositeData(toResourceData(resourceId, node))));
+            printlog("JSON:\n{}", toCharSequence(json));
+        }
+
+        printlog("XML:\n{}", XmlString.prettifyXml(toCharSequence(doc)));
+
+        // TODO if deviceId is given send it out to the device
+        if (uri != null) {
+            DeviceId deviceId = DeviceId.deviceId(uri);
+            NetconfController ctr = get(NetconfController.class);
+            Optional.ofNullable(ctr.getNetconfDevice(deviceId))
+                .map(NetconfDevice::getSession)
+                .ifPresent(session -> {
+                    try {
+                        session.rpc(toCharSequence(doc, false).toString()).join();
+                    } catch (NetconfException e) {
+                        log.error("Exception thrown", e);
+                    }
+                });
+        }
+    }
+}
diff --git a/apps/odtn/src/main/java/org/onosproject/odtn/cli/impl/package-info.java b/apps/odtn/src/main/java/org/onosproject/odtn/cli/impl/package-info.java
new file mode 100644
index 0000000..9799fab
--- /dev/null
+++ b/apps/odtn/src/main/java/org/onosproject/odtn/cli/impl/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * ODTN CLI commands implementations.
+ */
+package org.onosproject.odtn.cli.impl;
diff --git a/apps/odtn/src/main/java/org/onosproject/odtn/cli/impl/sub/OpticalChannel.java b/apps/odtn/src/main/java/org/onosproject/odtn/cli/impl/sub/OpticalChannel.java
new file mode 100644
index 0000000..9d813e5
--- /dev/null
+++ b/apps/odtn/src/main/java/org/onosproject/odtn/cli/impl/sub/OpticalChannel.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.odtn.cli.impl.sub;
+
+import static org.onosproject.odtn.utils.YangToolUtil.toDataNode;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.List;
+
+import org.onosproject.yang.gen.v1.openconfigplatform.rev20161222.openconfigplatform.platformcomponenttop.DefaultComponents;
+import org.onosproject.yang.gen.v1.openconfigplatform.rev20161222.openconfigplatform.platformcomponenttop.components.Component;
+import org.onosproject.yang.gen.v1.openconfigterminaldevice.rev20170708.openconfigterminaldevice.components.component.DefaultAugmentedOcPlatformComponent;
+import org.onosproject.yang.gen.v1.openconfigterminaldevice.rev20170708.openconfigterminaldevice.terminalopticalchanneltop.DefaultOpticalChannel;
+import org.onosproject.yang.gen.v1.openconfigterminaldevice.rev20170708.openconfigterminaldevice.terminalopticalchanneltop.opticalchannel.Config;
+import org.onosproject.yang.gen.v1.openconfigterminaldevice.rev20170708.openconfigterminaldevice.terminalopticalchanneltop.opticalchannel.DefaultConfig;
+import org.onosproject.yang.gen.v1.openconfigtransporttypes.rev20170816.openconfigtransporttypes.FrequencyType;
+import org.onosproject.yang.model.DataNode;
+
+import com.google.common.annotations.Beta;
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Utility methods dealing with OpenConfig optical-channel.
+ * <p>
+ * Split into classes for the purpose of avoiding "Config" class collisions.
+ */
+@Beta
+public abstract class OpticalChannel {
+
+    public static List<DataNode> preconf(String componentName) {
+
+        DefaultComponents components = new DefaultComponents();
+
+        Component component = PlainPlatform.componentWithName(componentName);
+        components.addToComponent(component);
+
+        // augmented 'component' shim
+        DefaultAugmentedOcPlatformComponent acomponent = new DefaultAugmentedOcPlatformComponent();
+
+        DefaultOpticalChannel channel = new DefaultOpticalChannel();
+
+        Config config = new DefaultConfig();
+        // TODO make these configurable
+        config.frequency(FrequencyType.of(BigInteger.valueOf(191500000)));
+        config.targetOutputPower(BigDecimal.valueOf(0.0));
+
+        channel.config(config);
+        acomponent.opticalChannel(channel);
+        component.addAugmentation(acomponent);
+
+        return ImmutableList.of(toDataNode(components));
+    }
+
+}
diff --git a/apps/odtn/src/main/java/org/onosproject/odtn/cli/impl/sub/PlainPlatform.java b/apps/odtn/src/main/java/org/onosproject/odtn/cli/impl/sub/PlainPlatform.java
new file mode 100644
index 0000000..74beaa6
--- /dev/null
+++ b/apps/odtn/src/main/java/org/onosproject/odtn/cli/impl/sub/PlainPlatform.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.odtn.cli.impl.sub;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import org.onosproject.yang.gen.v1.openconfigplatform.rev20161222.openconfigplatform.platformcomponenttop.components.Component;
+import org.onosproject.yang.gen.v1.openconfigplatform.rev20161222.openconfigplatform.platformcomponenttop.components.DefaultComponent;
+import org.onosproject.yang.gen.v1.openconfigplatform.rev20161222.openconfigplatform.platformcomponenttop.components.component.Config;
+import org.onosproject.yang.gen.v1.openconfigplatform.rev20161222.openconfigplatform.platformcomponenttop.components.component.DefaultConfig;
+
+import com.google.common.annotations.Beta;
+
+/**
+ * Utility methods dealing with OpenConfig component.
+ * <p>
+ * Split into classes for the purpose of avoiding "Config" class collisions.
+ */
+@Beta
+public abstract class PlainPlatform {
+
+    public static Component componentWithName(String componentName) {
+        checkNotNull(componentName, "componentName cannot be null");
+
+        Component component = new DefaultComponent();
+        component.name(componentName);
+        Config config = new DefaultConfig();
+        config.name(componentName);
+        component.config(config);
+        return component;
+    }
+}
diff --git a/apps/odtn/src/main/java/org/onosproject/odtn/cli/impl/sub/Transceiver.java b/apps/odtn/src/main/java/org/onosproject/odtn/cli/impl/sub/Transceiver.java
new file mode 100644
index 0000000..9d55408
--- /dev/null
+++ b/apps/odtn/src/main/java/org/onosproject/odtn/cli/impl/sub/Transceiver.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.odtn.cli.impl.sub;
+
+import static org.onosproject.odtn.utils.YangToolUtil.toDataNode;
+
+import java.util.List;
+
+import org.onosproject.yang.gen.v1.openconfigplatform.rev20161222.openconfigplatform.platformcomponenttop.DefaultComponents;
+import org.onosproject.yang.gen.v1.openconfigplatform.rev20161222.openconfigplatform.platformcomponenttop.components.Component;
+import org.onosproject.yang.gen.v1.openconfigplatformtransceiver.rev20170708.openconfigplatformtransceiver.components.component.DefaultAugmentedOcPlatformComponent;
+import org.onosproject.yang.gen.v1.openconfigplatformtransceiver.rev20170708.openconfigplatformtransceiver.porttransceivertop.DefaultTransceiver;
+import org.onosproject.yang.gen.v1.openconfigplatformtransceiver.rev20170708.openconfigplatformtransceiver.porttransceivertop.transceiver.Config;
+import org.onosproject.yang.gen.v1.openconfigplatformtransceiver.rev20170708.openconfigplatformtransceiver.porttransceivertop.transceiver.DefaultConfig;
+import org.onosproject.yang.gen.v1.openconfigtransporttypes.rev20170816.openconfigtransporttypes.Eth100GbaseLr4;
+import org.onosproject.yang.gen.v1.openconfigtransporttypes.rev20170816.openconfigtransporttypes.Qsfp28;
+import org.onosproject.yang.model.DataNode;
+
+import com.google.common.annotations.Beta;
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Utility methods dealing with OpenConfig transceiver.
+ * <p>
+ * Split into classes for the purpose of avoiding "Config" class collisions.
+ */
+@Beta
+public abstract class Transceiver {
+
+    public static List<DataNode> enable(String componentName, boolean enable) {
+
+        DefaultComponents components = new DefaultComponents();
+
+        Component component = PlainPlatform.componentWithName(componentName);
+        components.addToComponent(component);
+
+        // augmented 'component' shim
+        DefaultAugmentedOcPlatformComponent tcomponent = new DefaultAugmentedOcPlatformComponent();
+
+        DefaultTransceiver transceiver = new DefaultTransceiver();
+
+        Config configt = new DefaultConfig();
+        configt.enabled(enable); // phase 1.0 flag
+        transceiver.config(configt);
+        tcomponent.transceiver(transceiver);
+        component.addAugmentation(tcomponent);
+
+        return ImmutableList.of(toDataNode(components));
+    }
+
+    public static List<DataNode> preconf(String componentName) {
+        DefaultComponents components = new DefaultComponents();
+
+        Component component = PlainPlatform.componentWithName(componentName);
+        components.addToComponent(component);
+
+        // augmented 'component' shim
+        DefaultAugmentedOcPlatformComponent tcomponent = new DefaultAugmentedOcPlatformComponent();
+
+        DefaultTransceiver transceiver = new DefaultTransceiver();
+
+        Config configt = new DefaultConfig();
+        // TODO make these configurable
+        configt.formFactorPreconf(Qsfp28.class);
+        configt.ethernetPmdPreconf(Eth100GbaseLr4.class);
+        transceiver.config(configt);
+        tcomponent.transceiver(transceiver);
+        component.addAugmentation(tcomponent);
+
+        return ImmutableList.of(toDataNode(components));
+    }
+
+}
diff --git a/apps/odtn/src/main/java/org/onosproject/odtn/cli/impl/sub/package-info.java b/apps/odtn/src/main/java/org/onosproject/odtn/cli/impl/sub/package-info.java
new file mode 100644
index 0000000..ca8433c
--- /dev/null
+++ b/apps/odtn/src/main/java/org/onosproject/odtn/cli/impl/sub/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * sub-package for ODTN related CLI.
+ */
+package org.onosproject.odtn.cli.impl.sub;
diff --git a/apps/odtn/src/main/java/org/onosproject/odtn/utils/YangToolUtil.java b/apps/odtn/src/main/java/org/onosproject/odtn/utils/YangToolUtil.java
new file mode 100644
index 0000000..4df3943
--- /dev/null
+++ b/apps/odtn/src/main/java/org/onosproject/odtn/utils/YangToolUtil.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.odtn.utils;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.slf4j.LoggerFactory.getLogger;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.StringWriter;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.onlab.osgi.DefaultServiceDirectory;
+import org.onosproject.yang.model.DataNode;
+import org.onosproject.yang.model.DefaultModelObjectData;
+import org.onosproject.yang.model.DefaultResourceData;
+import org.onosproject.yang.model.InnerNode;
+import org.onosproject.yang.model.ModelConverter;
+import org.onosproject.yang.model.ModelObject;
+import org.onosproject.yang.model.ModelObjectData;
+import org.onosproject.yang.model.ResourceData;
+import org.onosproject.yang.model.ResourceId;
+import org.onosproject.yang.runtime.CompositeData;
+import org.onosproject.yang.runtime.CompositeStream;
+import org.onosproject.yang.runtime.DefaultCompositeData;
+import org.onosproject.yang.runtime.DefaultRuntimeContext;
+import org.onosproject.yang.runtime.RuntimeContext;
+import org.onosproject.yang.runtime.YangRuntimeService;
+import org.slf4j.Logger;
+import org.w3c.dom.Document;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.google.common.annotations.Beta;
+import com.google.common.io.CharStreams;
+
+@Beta
+@Component(immediate = true)
+public class YangToolUtil {
+    private static final Logger log = getLogger(YangToolUtil.class);
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected YangRuntimeService yangRuntimeService;
+    protected static YangRuntimeService yrs;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected ModelConverter modelConverter;
+    protected static ModelConverter converter;
+
+    @Activate
+    protected void activate() {
+        yrs = yangRuntimeService;
+        converter = modelConverter;
+        log.info("Started");
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        log.info("Stopped");
+    }
+
+    protected static void initStaticContext() {
+        if (yrs == null) {
+            yrs = DefaultServiceDirectory.getService(YangRuntimeService.class);
+        }
+        if (converter == null) {
+            converter = DefaultServiceDirectory.getService(ModelConverter.class);
+        }
+    }
+
+    /**
+     * Converts XML Document into CharSequence.
+     *
+     * @param xmlInput to convert
+     * @return CharSequence
+     */
+    public static CharSequence toCharSequence(Document xmlInput) {
+        return toCharSequence(xmlInput, true);
+    }
+
+    /**
+     * Converts XML Document into CharSequence.
+     *
+     * @param xmlInput to convert
+     * @param omitXmlDecl or not
+     * @return CharSequence
+     */
+    public static CharSequence toCharSequence(Document xmlInput, boolean omitXmlDecl) {
+        try {
+            TransformerFactory tf = TransformerFactory.newInstance();
+            Transformer transformer = tf.newTransformer();
+            if (omitXmlDecl) {
+                transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
+            }
+            StringWriter writer = new StringWriter();
+            transformer.transform(new DOMSource(xmlInput), new StreamResult(writer));
+            return writer.getBuffer();
+        } catch (TransformerException e) {
+            log.error("Exception thrown", e);
+            return null;
+        }
+    }
+
+    /**
+     * Converts JsonNode into CharSequence.
+     *
+     * @param jsonInput to convert
+     * @return CharSequence
+     */
+    public static CharSequence toCharSequence(JsonNode jsonInput) {
+        checkNotNull(jsonInput);
+        ObjectMapper mapper = new ObjectMapper();
+        // TODO following pretty printing option should be removed
+        mapper.enable(SerializationFeature.INDENT_OUTPUT);
+
+        try {
+            return mapper.writerWithDefaultPrettyPrinter()
+                        .writeValueAsString(jsonInput);
+        } catch (JsonProcessingException e) {
+            log.error("Exception thrown", e);
+            return null;
+        }
+    }
+
+    /**
+     * Converts UTF-8 CompositeStream into CharSequence.
+     *
+     * @param utf8Input to convert
+     * @return CharSequence
+     */
+    public static CharSequence toCharSequence(CompositeStream utf8Input) {
+        StringBuilder s = new StringBuilder();
+        try {
+            CharStreams.copy(new InputStreamReader(utf8Input.resourceData(), UTF_8), s);
+            return s;
+        } catch (IOException e) {
+            log.error("Exception thrown", e);
+            return null;
+        }
+    }
+
+    /**
+     * Converts JSON CompositeStream into JsonNode.
+     *
+     * @param jsonInput to convert
+     * @return JsonNode
+     */
+    public static JsonNode toJsonNode(CompositeStream jsonInput) {
+        ObjectMapper mapper = new ObjectMapper();
+        try {
+            return mapper.readTree(jsonInput.resourceData());
+        } catch (IOException e) {
+            log.error("Exception thrown", e);
+            return null;
+        }
+    }
+
+    /**
+     * Converts XML CompositeStream into XML Document.
+     *
+     * @param xmlInput to convert
+     * @return Document
+     */
+    public static Document toDocument(CompositeStream xmlInput) {
+        try {
+            return DocumentBuilderFactory.newInstance()
+                    .newDocumentBuilder()
+                .parse(new InputSource(new InputStreamReader(xmlInput.resourceData(), UTF_8)));
+        } catch (ParserConfigurationException | SAXException | IOException e) {
+            log.error("Exception thrown", e);
+            return null;
+        }
+    }
+
+    /**
+     * Converts CompositeData into XML CompositeStream.
+     *
+     * @param input CompositeData to convert
+     * @return XML CompositeStream
+     */
+    public static CompositeStream toXmlCompositeStream(CompositeData input) {
+        initStaticContext();
+        RuntimeContext yrtContext = new DefaultRuntimeContext.Builder()
+                .setDataFormat("xml")
+                // Following does not have any effect?
+                //.addAnnotation(XMLNS_XC_ANNOTATION)
+                .build();
+        CompositeStream xml = yrs.encode(input, yrtContext);
+        return xml;
+    }
+
+    /**
+     * Converts CompositeData into JSON CompositeStream.
+     *
+     * @param input CompositeData to convert
+     * @return JSON CompositeStream
+     */
+    public static CompositeStream toJsonCompositeStream(CompositeData input) {
+        initStaticContext();
+        RuntimeContext yrtContext = new DefaultRuntimeContext.Builder()
+                .setDataFormat("JSON")
+                .build();
+        CompositeStream xml = yrs.encode(input, yrtContext);
+        return xml;
+    }
+
+    /**
+     * Converts ResourceData into CompositeData.
+     *
+     * @param input ResourceData to convert
+     * @return CompositeData
+     */
+    public static CompositeData toCompositeData(ResourceData input) {
+        CompositeData.Builder builder =
+                DefaultCompositeData.builder();
+        builder.resourceData(input);
+        // remove, merge, replace, ...
+        //builder.addAnnotatedNodeInfo(info)
+
+        return builder.build();
+    }
+
+    /**
+     * Converts DataNode into ResourceData.
+     *
+     * @param resourceId pointing to parent of {@code dataNode}, YANG-wise.
+     * @param dataNode to convert, must be InnerNode
+     * @return ResourceData
+     */
+    public static ResourceData toResourceData(ResourceId resourceId, DataNode dataNode) {
+        DefaultResourceData.Builder builder = DefaultResourceData.builder();
+        builder.resourceId(checkNotNull(resourceId));
+        if (dataNode instanceof InnerNode) {
+            ((InnerNode) dataNode).childNodes().values().forEach(builder::addDataNode);
+        } else {
+            log.error("Unexpected DataNode encountered {}", dataNode);
+        }
+        return builder.build();
+    }
+
+
+    /**
+     * Converts ModelObject into a DataNode.
+     *
+     * @param input ModelOject
+     * @return DataNode
+     */
+    public static DataNode toDataNode(ModelObject input) {
+        initStaticContext();
+        ModelObjectData modelData = DefaultModelObjectData.builder()
+                .addModelObject(input)
+                .identifier(null)
+                .build();
+
+        ResourceData rnode = converter.createDataNode(modelData);
+        if (rnode.dataNodes().isEmpty()) {
+            log.error("input did not result in any datanode. {}", input);
+            return null;
+        }
+        return rnode.dataNodes().get(0);
+    }
+}
diff --git a/apps/odtn/src/main/java/org/onosproject/odtn/utils/package-info.java b/apps/odtn/src/main/java/org/onosproject/odtn/utils/package-info.java
new file mode 100644
index 0000000..4f15f89
--- /dev/null
+++ b/apps/odtn/src/main/java/org/onosproject/odtn/utils/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Utilities developed for ODTN.
+ */
+package org.onosproject.odtn.utils;
diff --git a/apps/odtn/src/main/resources/OSGI-INF/blueprint/shell-config.xml b/apps/odtn/src/main/resources/OSGI-INF/blueprint/shell-config.xml
new file mode 100644
index 0000000..6611c01
--- /dev/null
+++ b/apps/odtn/src/main/resources/OSGI-INF/blueprint/shell-config.xml
@@ -0,0 +1,36 @@
+<!--
+  ~ Copyright 2018-present Open Networking Foundation
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0">
+
+    <command-bundle xmlns="http://karaf.apache.org/xmlns/shell/v1.1.0">
+
+        <command>
+            <action class="org.onosproject.odtn.cli.impl.OdtnManualTestCommand"/>
+            <completers>
+                <ref component-id="modeCompleter"/>
+                <null/>
+            </completers>
+            <optional-completers>
+                <entry key="--deviceId" value-ref="deviceIdCompleter"/>
+            </optional-completers>
+        </command>
+
+    </command-bundle>
+
+    <bean id="deviceIdCompleter" class="org.onosproject.cli.net.DeviceIdCompleter"/>
+    <bean id="modeCompleter" class="org.onosproject.odtn.cli.impl.ModeCompleter"/>
+
+</blueprint>
\ No newline at end of file