ONOS-6980 Adding support for download of application bits.

Change-Id: I742950690b50038cac0bb2ad2da4eaac5781da85
diff --git a/cli/src/main/java/org/onosproject/cli/app/ApplicationCommand.java b/cli/src/main/java/org/onosproject/cli/app/ApplicationCommand.java
index 5916dbf..4db129d 100644
--- a/cli/src/main/java/org/onosproject/cli/app/ApplicationCommand.java
+++ b/cli/src/main/java/org/onosproject/cli/app/ApplicationCommand.java
@@ -15,6 +15,7 @@
  */
 package org.onosproject.cli.app;
 
+import com.google.common.io.ByteStreams;
 import org.apache.karaf.shell.commands.Argument;
 import org.apache.karaf.shell.commands.Command;
 import org.onosproject.app.ApplicationAdminService;
@@ -38,9 +39,10 @@
     static final String UNINSTALL = "uninstall";
     static final String ACTIVATE = "activate";
     static final String DEACTIVATE = "deactivate";
+    static final String DOWNLOAD = "download";
 
     @Argument(index = 0, name = "command",
-            description = "Command name (install|activate|deactivate|uninstall)",
+            description = "Command name (install|activate|deactivate|uninstall|download)",
             required = true, multiValued = false)
     String command = null;
 
@@ -58,14 +60,20 @@
                 }
             }
 
-        } else {
+        } else if (command.equals(DOWNLOAD)) {
             for (String name : names) {
-                if (!manageApp(service, name)) {
+                if (!downloadApp(service, name)) {
                     return;
                 }
             }
+        } else {
+                for (String name : names) {
+                    if (!manageApp(service, name)) {
+                        return;
+                    }
+                }
+            }
         }
-    }
 
     // Installs the application from input of the specified URL
     private boolean installApp(ApplicationAdminService service, String url) {
@@ -82,6 +90,18 @@
         return true;
     }
 
+    // Downloads the application bits to the standard output.
+    private boolean downloadApp(ApplicationAdminService service, String name) {
+        try {
+            ByteStreams.copy(service.getApplicationArchive(service.getId(name)),
+                             System.out);
+        } catch (IOException e) {
+            error("Unable to download bits for application %s", name);
+            return false;
+        }
+        return true;
+    }
+
     // Manages the specified application.
     private boolean manageApp(ApplicationAdminService service, String name) {
         ApplicationId appId = service.getId(name);
diff --git a/cli/src/main/java/org/onosproject/cli/app/ApplicationCommandCompleter.java b/cli/src/main/java/org/onosproject/cli/app/ApplicationCommandCompleter.java
index eb53ec6..8164b0e 100644
--- a/cli/src/main/java/org/onosproject/cli/app/ApplicationCommandCompleter.java
+++ b/cli/src/main/java/org/onosproject/cli/app/ApplicationCommandCompleter.java
@@ -28,7 +28,7 @@
 public class ApplicationCommandCompleter extends AbstractChoicesCompleter {
     @Override
     public List<String> choices() {
-        return ImmutableList.of(INSTALL, UNINSTALL, ACTIVATE, DEACTIVATE);
+        return ImmutableList.of(INSTALL, UNINSTALL, ACTIVATE, DEACTIVATE, DOWNLOAD);
     }
 
 }
diff --git a/cli/src/main/java/org/onosproject/cli/app/ApplicationNameCompleter.java b/cli/src/main/java/org/onosproject/cli/app/ApplicationNameCompleter.java
index 0efb1dd..1848bf3 100644
--- a/cli/src/main/java/org/onosproject/cli/app/ApplicationNameCompleter.java
+++ b/cli/src/main/java/org/onosproject/cli/app/ApplicationNameCompleter.java
@@ -63,7 +63,7 @@
 //            if (previousApps.contains(app.id().name())) {
 //                continue;
 //            }
-            if ("uninstall".equals(cmd) ||
+            if ("uninstall".equals(cmd) || "download".equals(cmd) ||
                     ("activate".equals(cmd) && state == INSTALLED) ||
                     ("deactivate".equals(cmd) && state == ACTIVE)) {
                 strings.add(app.id().name());
diff --git a/core/api/src/main/java/org/onosproject/app/ApplicationService.java b/core/api/src/main/java/org/onosproject/app/ApplicationService.java
index 683f90a..51d3eb4 100644
--- a/core/api/src/main/java/org/onosproject/app/ApplicationService.java
+++ b/core/api/src/main/java/org/onosproject/app/ApplicationService.java
@@ -20,6 +20,7 @@
 import org.onosproject.event.ListenerService;
 import org.onosproject.security.Permission;
 
+import java.io.InputStream;
 import java.util.Set;
 
 /**
@@ -74,4 +75,14 @@
      * @param hook  pre-deactivation hook
      */
     void registerDeactivateHook(ApplicationId appId, Runnable hook);
+
+    /**
+     * Returns stream that contains the application OAR/JAR file contents.
+     *
+     * @param appId application identifier
+     * @return input stream containing the app OAR/JAR file
+     */
+    default InputStream getApplicationArchive(ApplicationId appId) {
+        return null;
+    }
 }
diff --git a/core/api/src/main/java/org/onosproject/app/ApplicationStore.java b/core/api/src/main/java/org/onosproject/app/ApplicationStore.java
index 3544956..bd5a66d 100644
--- a/core/api/src/main/java/org/onosproject/app/ApplicationStore.java
+++ b/core/api/src/main/java/org/onosproject/app/ApplicationStore.java
@@ -105,4 +105,14 @@
      */
     void setPermissions(ApplicationId appId, Set<Permission> permissions);
 
+    /**
+     * Returns stream that contains the application OAR/JAR file contents.
+     *
+     * @param appId application identifier
+     * @return input stream containing the app OAR/JAR file
+     */
+    default InputStream getApplicationArchive(ApplicationId appId) {
+        return null;
+    }
+
 }
diff --git a/core/common/src/main/java/org/onosproject/common/app/ApplicationArchive.java b/core/common/src/main/java/org/onosproject/common/app/ApplicationArchive.java
index ec38607..92709cb 100644
--- a/core/common/src/main/java/org/onosproject/common/app/ApplicationArchive.java
+++ b/core/common/src/main/java/org/onosproject/common/app/ApplicationArchive.java
@@ -199,13 +199,11 @@
             checkState(!appFile(desc.name(), APP_XML).exists(),
                     "Application %s already installed", desc.name());
 
-            boolean isSelfContainedJar = false;
-
             if (plainXml) {
                 expandPlainApplication(cache, desc);
             } else {
                 bis.reset();
-                isSelfContainedJar = expandZippedApplication(bis, desc);
+                boolean isSelfContainedJar = expandZippedApplication(bis, desc);
 
                 if (isSelfContainedJar) {
                     bis.reset();
@@ -254,7 +252,7 @@
 
     /**
      * Returns application archive stream for the specified application. This
-     * will be either the application ZIP file or the application XML file.
+     * will be either the application OAR file, JAR file or the plain XML file.
      *
      * @param appName application name
      * @return application archive stream
diff --git a/core/net/src/main/java/org/onosproject/app/impl/ApplicationManager.java b/core/net/src/main/java/org/onosproject/app/impl/ApplicationManager.java
index 3c3d60b..d8b09b0 100644
--- a/core/net/src/main/java/org/onosproject/app/impl/ApplicationManager.java
+++ b/core/net/src/main/java/org/onosproject/app/impl/ApplicationManager.java
@@ -179,6 +179,12 @@
         store.setPermissions(appId, permissions);
     }
 
+    @Override
+    public InputStream getApplicationArchive(ApplicationId appId) {
+        checkNotNull(appId, APP_ID_NULL);
+        return store.getApplicationArchive(appId);
+    }
+
     private void updateStoreAndWaitForNotificationHandling(ApplicationId appId,
                                                            Consumer<ApplicationId> storeUpdateTask) {
         CountDownLatch latch = new CountDownLatch(1);
diff --git a/core/store/dist/src/main/java/org/onosproject/store/app/DistributedApplicationStore.java b/core/store/dist/src/main/java/org/onosproject/store/app/DistributedApplicationStore.java
index 21450c0..c64942f 100644
--- a/core/store/dist/src/main/java/org/onosproject/store/app/DistributedApplicationStore.java
+++ b/core/store/dist/src/main/java/org/onosproject/store/app/DistributedApplicationStore.java
@@ -437,6 +437,11 @@
         }
     }
 
+    @Override
+    public InputStream getApplicationArchive(ApplicationId appId) {
+        return getApplicationInputStream(appId.name());
+    }
+
     private class AppActivator implements Consumer<Application> {
         @Override
         public void accept(Application app) {
diff --git a/tools/test/scenarios/yang-live-compile.xml b/tools/test/scenarios/yang-live-compile.xml
index 7ddad20..c87c4fc 100644
--- a/tools/test/scenarios/yang-live-compile.xml
+++ b/tools/test/scenarios/yang-live-compile.xml
@@ -13,7 +13,7 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<scenario name="yang-lve-compile" description="Live YANG compilation">
+<scenario name="yang-live-compile" description="Live YANG compilation">
     <group name="YANG-Live-Compile">
         <step name="Activate-YANG-Runtime" exec="onos ${OC1} app activate org.onosproject.yang"/>
         <step name="Pre-Cleanup-YANG-Model" exec="onos ${OC1} app uninstall l3vpn" env="~"/>
diff --git a/web/api/src/main/java/org/onosproject/rest/resources/ApplicationsWebResource.java b/web/api/src/main/java/org/onosproject/rest/resources/ApplicationsWebResource.java
index 3a4eb5c..4a71ffd 100644
--- a/web/api/src/main/java/org/onosproject/rest/resources/ApplicationsWebResource.java
+++ b/web/api/src/main/java/org/onosproject/rest/resources/ApplicationsWebResource.java
@@ -208,6 +208,23 @@
     }
 
     /**
+     * Get application OAR/JAR file.
+     * Returns the OAR/JAR file used to install the specified application.
+     *
+     * @param name application name
+     * @return 200 OK; 404; 401
+     */
+    @GET
+    @Produces(MediaType.APPLICATION_OCTET_STREAM)
+    @Path("{name}/bits")
+    public Response getAppBits(@PathParam("name") String name) {
+        ApplicationAdminService service = get(ApplicationAdminService.class);
+        ApplicationId appId = nullIsNotFound(service.getId(name), APP_ID_NOT_FOUND);
+        InputStream bits = service.getApplicationArchive(appId);
+        return ok(bits).build();
+    }
+
+    /**
      * Gets applicationId entry by either id or name.
      *
      * @param id   id of application
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/ApplicationResource.java b/web/gui/src/main/java/org/onosproject/ui/impl/ApplicationResource.java
index d245eff..16f288a 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/ApplicationResource.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/ApplicationResource.java
@@ -58,6 +58,26 @@
         return Response.ok().build();
     }
 
+    /**
+     * Get application OAR/JAR file.
+     * Returns the OAR/JAR file used to install the specified application.
+     *
+     * @param name application name
+     * @return 200 OK; 404; 401
+     */
+    @GET
+    @Produces(MediaType.APPLICATION_OCTET_STREAM)
+    @Path("{name}/download")
+    public Response download(@PathParam("name") String name) {
+        ApplicationAdminService service = get(ApplicationAdminService.class);
+        ApplicationId appId = service.getId(name);
+        InputStream bits = service.getApplicationArchive(appId);
+        String fileName = appId.name() + ".oar";
+        return Response.ok(bits)
+                .header("Content-Disposition", "attachment; filename=\"" + fileName + "\"")
+                .build();
+    }
+
     @Path("{name}/icon")
     @GET
     @Produces("image/png")
diff --git a/web/gui/src/main/resources/org/onosproject/ui/lion/core/view/App.properties b/web/gui/src/main/resources/org/onosproject/ui/lion/core/view/App.properties
index 5ce011a..b362edd 100644
--- a/web/gui/src/main/resources/org/onosproject/ui/lion/core/view/App.properties
+++ b/web/gui/src/main/resources/org/onosproject/ui/lion/core/view/App.properties
@@ -26,6 +26,7 @@
 tt_ctl_activate=Activate selected application
 tt_ctl_deactivate=Deactivate selected application
 tt_ctl_uninstall=Uninstall selected application
+tt_ctl_download=Download selected application (.oar file)
 
 # Quick-Help panel
 qh_hint_esc=Deselect application
diff --git a/web/gui/src/main/webapp/app/view/app/app.css b/web/gui/src/main/webapp/app/view/app/app.css
index 9eb8ba8..4d61f81 100644
--- a/web/gui/src/main/webapp/app/view/app/app.css
+++ b/web/gui/src/main/webapp/app/view/app/app.css
@@ -23,7 +23,7 @@
 }
 
 #ov-app div.ctrl-btns {
-    width: 250px;
+    width: 290px;
 }
 
 /* -- Drag-n-Drop oar file upload -- */
diff --git a/web/gui/src/main/webapp/app/view/app/app.html b/web/gui/src/main/webapp/app/view/app/app.html
index 6eaa126..e371248 100644
--- a/web/gui/src/main/webapp/app/view/app/app.html
+++ b/web/gui/src/main/webapp/app/view/app/app.html
@@ -38,6 +38,12 @@
                  tooltip tt-msg="uninstallTip"
                  ng-class="{active: ctrlBtnState.selection}">
             </div>
+            <!-- FIXME: create proper download icon -->
+            <div icon icon-size="42" icon-id="downArrow"
+                 ng-click="downloadApp()"
+                 tooltip tt-msg="downloadTip"
+                 ng-class="{active: ctrlBtnState.selection}">
+            </div>
         </div>
     </div>
 
diff --git a/web/gui/src/main/webapp/app/view/app/app.js b/web/gui/src/main/webapp/app/view/app/app.js
index a01dd2e..5a2272e 100644
--- a/web/gui/src/main/webapp/app/view/app/app.js
+++ b/web/gui/src/main/webapp/app/view/app/app.js
@@ -45,8 +45,9 @@
         detailsResp = 'appDetailsResponse',
         fileUploadUrl = 'applications/upload',
         activateOption = '?activate=true',
-        iconUrlPrefix = 'rs/applications/',
+        appUrlPrefix = 'rs/applications/',
         iconUrlSuffix = '/icon',
+        downloadSuffix = '/download',
         dialogId = 'app-dialog',
         dialogOpts = {
             edge: 'right',
@@ -167,7 +168,7 @@
     }
 
     function addIcon(elem, value) {
-        elem.append('img').attr('src', iconUrlPrefix + value + iconUrlSuffix);
+        elem.append('img').attr('src', appUrlPrefix + value + iconUrlSuffix);
     }
 
     function populateTop(details) {
@@ -250,6 +251,7 @@
         $scope.activateTip = lion('tt_ctl_activate');
         $scope.deactivateTip = lion('tt_ctl_deactivate');
         $scope.uninstallTip = lion('tt_ctl_uninstall');
+        $scope.downloadTip = lion('tt_ctl_download');
 
 
         var handlers = {};
@@ -359,6 +361,12 @@
             }
         };
 
+        $scope.downloadApp = function () {
+            if ($scope.ctrlBtnState.selection) {
+                window.location = appUrlPrefix + $scope.selId + downloadSuffix;
+            }
+        };
+
         $scope.$on('FileChanged', function () {
             var formData = new FormData(),
                 url;