Added ability to upload apps as both app.xml or app.zip.
Added a number of app.xml files for built-in apps.
Added ability to install & activate in one command.

Change-Id: I3fa5fa487ef76d9fe3da4d6dce8045d538cba423
diff --git a/apps/config/app.xml b/apps/config/app.xml
new file mode 100644
index 0000000..b561670
--- /dev/null
+++ b/apps/config/app.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2015 Open Networking Laboratory
+  ~
+  ~ 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.
+  -->
+<app name="org.onosproject.app.config" origin="ON.Lab" version="1.1.0"
+        features="onos-app-config">
+    <description>ONOS network configuration application</description>
+</app>
diff --git a/apps/fwd/app.xml b/apps/fwd/app.xml
new file mode 100644
index 0000000..fad1927
--- /dev/null
+++ b/apps/fwd/app.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2015 Open Networking Laboratory
+  ~
+  ~ 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.
+  -->
+<app name="org.onosproject.app.fwd" origin="ON.Lab" version="1.1.0"
+        features="onos-app-fwd">
+    <description>ONOS Reactive forwarding application using flow subsystem</description>
+</app>
diff --git a/apps/ifwd/app.xml b/apps/ifwd/app.xml
new file mode 100644
index 0000000..9403d06
--- /dev/null
+++ b/apps/ifwd/app.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2015 Open Networking Laboratory
+  ~
+  ~ 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.
+  -->
+<app name="org.onosproject.app.ifwd" origin="ON.Lab" version="1.1.0"
+        features="onos-app-ifwd">
+    <description>ONOS Reactive forwarding application using intent subsystem (experimental)</description>
+</app>
diff --git a/apps/metrics/intent/app.xml b/apps/metrics/intent/app.xml
new file mode 100644
index 0000000..4d184f2
--- /dev/null
+++ b/apps/metrics/intent/app.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2015 Open Networking Laboratory
+  ~
+  ~ 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.
+  -->
+<app name="org.onosproject.app.metrics.intent" origin="ON.Lab" version="1.1.0"
+        features="onos-app-metrics-intent">
+    <description>ONOS intent metrics test application</description>
+</app>
diff --git a/apps/metrics/topology/app.xml b/apps/metrics/topology/app.xml
new file mode 100644
index 0000000..2dd9d53
--- /dev/null
+++ b/apps/metrics/topology/app.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2015 Open Networking Laboratory
+  ~
+  ~ 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.
+  -->
+<app name="org.onosproject.app.metrics.topology" origin="ON.Lab" version="1.1.0"
+        features="onos-app-metrics-topology">
+    <description>ONOS topology metrics test application</description>
+</app>
diff --git a/apps/optical/app.xml b/apps/optical/app.xml
new file mode 100644
index 0000000..92877d9
--- /dev/null
+++ b/apps/optical/app.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2015 Open Networking Laboratory
+  ~
+  ~ 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.
+  -->
+<app name="org.onosproject.app.optical" origin="ON.Lab" version="1.1.0"
+        features="onos-app-sdnip">
+    <description>ONOS Packet/Optical use-case application</description>
+</app>
diff --git a/apps/proxyarp/app.xml b/apps/proxyarp/app.xml
new file mode 100644
index 0000000..6dad95d
--- /dev/null
+++ b/apps/proxyarp/app.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2015 Open Networking Laboratory
+  ~
+  ~ 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.
+  -->
+<app name="org.onosproject.app.proxyarp" origin="ON.Lab" version="1.1.0"
+        features="onos-app-proxyarp">
+    <description>ONOS proxy ARP application</description>
+</app>
diff --git a/apps/sdnip/app.xml b/apps/sdnip/app.xml
new file mode 100644
index 0000000..3264d90
--- /dev/null
+++ b/apps/sdnip/app.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2015 Open Networking Laboratory
+  ~
+  ~ 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.
+  -->
+<app name="org.onosproject.app.sdnip" origin="ON.Lab" version="1.1.0"
+        features="onos-app-sdnip">
+    <description>ONOS SDN/IP use-case application</description>
+</app>
diff --git a/apps/tvue/app.xml b/apps/tvue/app.xml
new file mode 100644
index 0000000..1435903
--- /dev/null
+++ b/apps/tvue/app.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2015 Open Networking Laboratory
+  ~
+  ~ 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.
+  -->
+<app name="org.onosproject.app.tvue" origin="ON.Lab" version="1.1.0"
+        features="onos-app-tvue">
+    <description>Early prototype GUI (deprecated)</description>
+</app>
diff --git a/core/api/src/main/java/org/onosproject/app/ApplicationAdminService.java b/core/api/src/main/java/org/onosproject/app/ApplicationAdminService.java
index 18babd5..e0ea6ec 100644
--- a/core/api/src/main/java/org/onosproject/app/ApplicationAdminService.java
+++ b/core/api/src/main/java/org/onosproject/app/ApplicationAdminService.java
@@ -29,7 +29,9 @@
 
     /**
      * Installs the application contained in the specified application archive
-     * input stream.
+     * input stream. This can be either a ZIP stream containing a compressed
+     * application archive or a plain XML stream containing just the
+     * {@code app.xml} application descriptor file.
      *
      * @param appDescStream application descriptor input stream
      * @return installed application descriptor
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 0260863..5a818e4 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
@@ -40,6 +40,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URI;
+import java.nio.charset.Charset;
 import java.nio.file.NoSuchFileException;
 import java.util.List;
 import java.util.Set;
@@ -57,6 +58,13 @@
 public class ApplicationArchive
         extends AbstractStore<ApplicationEvent, ApplicationStoreDelegate> {
 
+    // Magic strings to search for at the beginning of the archive stream
+    private static final String XML_MAGIC = "<?xml ";
+
+    // Magic strings to search for and how deep to search it into the archive stream
+    private static final String APP_MAGIC = "<app ";
+    private static final int APP_MAGIC_DEPTH = 1024;
+
     private static final String NAME = "[@name]";
     private static final String ORIGIN = "[@origin]";
     private static final String VERSION = "[@version]";
@@ -144,13 +152,21 @@
         try (InputStream ais = stream) {
             byte[] cache = toByteArray(ais);
             InputStream bis = new ByteArrayInputStream(cache);
-            ApplicationDescription desc = parseAppDescription(bis);
-            bis.reset();
 
-            expandApplication(bis, desc);
-            bis.reset();
+            boolean plainXml = isPlainXml(cache);
+            ApplicationDescription desc = plainXml ?
+                    parsePlainAppDescription(bis) : parseZippedAppDescription(bis);
 
-            saveApplication(bis, desc);
+            if (plainXml) {
+                expandPlainApplication(cache, desc);
+            } else {
+                bis.reset();
+                expandZippedApplication(bis, desc);
+
+                bis.reset();
+                saveApplication(bis, desc);
+            }
+
             installArtifacts(desc);
             return desc;
         } catch (IOException e) {
@@ -158,28 +174,45 @@
         }
     }
 
+    // Indicates whether the stream encoded in the given bytes is plain XML.
+    private boolean isPlainXml(byte[] bytes) {
+        return substring(bytes, XML_MAGIC.length()).equals(XML_MAGIC) ||
+                substring(bytes, APP_MAGIC_DEPTH).contains(APP_MAGIC);
+    }
+
+    // Returns the substring of maximum possible length from the specified bytes.
+    private String substring(byte[] bytes, int length) {
+        return new String(bytes, 0, Math.min(bytes.length, length), Charset.forName("UTF-8"));
+    }
+
     /**
      * Purges the application archive directory.
      *
      * @param appName application name
      */
     public void purgeApplication(String appName) {
+        File appDir = new File(appsDir, appName);
         try {
-            Tools.removeDirectory(new File(appsDir, appName));
+            Tools.removeDirectory(appDir);
         } catch (IOException e) {
             throw new ApplicationException("Unable to purge application " + appName, e);
         }
+        if (appDir.exists()) {
+            throw new ApplicationException("Unable to purge application " + appName);
+        }
     }
 
     /**
-     * Returns application archive stream for the specified application.
+     * Returns application archive stream for the specified application. This
+     * will be either the application ZIP file or the application XML file.
      *
      * @param appName application name
      * @return application archive stream
      */
     public InputStream getApplicationInputStream(String appName) {
         try {
-            return new FileInputStream(appFile(appName, appName + ".zip"));
+            File appFile = appFile(appName, appName + ".zip");
+            return new FileInputStream(appFile.exists() ? appFile : appFile(appName, APP_XML));
         } catch (FileNotFoundException e) {
             throw new ApplicationException("Application " + appName + " not found");
         }
@@ -187,20 +220,14 @@
 
     // Scans the specified ZIP stream for app.xml entry and parses it producing
     // an application descriptor.
-    private ApplicationDescription parseAppDescription(InputStream stream)
+    private ApplicationDescription parseZippedAppDescription(InputStream stream)
             throws IOException {
         try (ZipInputStream zis = new ZipInputStream(stream)) {
             ZipEntry entry;
             while ((entry = zis.getNextEntry()) != null) {
                 if (entry.getName().equals(APP_XML)) {
                     byte[] data = ByteStreams.toByteArray(zis);
-                    XMLConfiguration cfg = new XMLConfiguration();
-                    try {
-                        cfg.load(new ByteArrayInputStream(data));
-                        return loadAppDescription(cfg);
-                    } catch (ConfigurationException e) {
-                        throw new IOException("Unable to parse " + APP_XML, e);
-                    }
+                    return parsePlainAppDescription(new ByteArrayInputStream(data));
                 }
                 zis.closeEntry();
             }
@@ -208,6 +235,18 @@
         throw new IOException("Unable to locate " + APP_XML);
     }
 
+    // Scans the specified XML stream and parses it producing an application descriptor.
+    private ApplicationDescription parsePlainAppDescription(InputStream stream)
+            throws IOException {
+        XMLConfiguration cfg = new XMLConfiguration();
+        try {
+            cfg.load(stream);
+            return loadAppDescription(cfg);
+        } catch (ConfigurationException e) {
+            throw new IOException("Unable to parse " + APP_XML, e);
+        }
+    }
+
     private ApplicationDescription loadAppDescription(XMLConfiguration cfg) {
         cfg.setAttributeSplittingDisabled(true);
         cfg.setDelimiterParsingDisabled(true);
@@ -225,7 +264,7 @@
     }
 
     // Expands the specified ZIP stream into app-specific directory.
-    private void expandApplication(InputStream stream, ApplicationDescription desc)
+    private void expandZippedApplication(InputStream stream, ApplicationDescription desc)
             throws IOException {
         ZipInputStream zis = new ZipInputStream(stream);
         ZipEntry entry;
@@ -234,7 +273,6 @@
             if (!entry.isDirectory()) {
                 byte[] data = ByteStreams.toByteArray(zis);
                 zis.closeEntry();
-
                 File file = new File(appDir, entry.getName());
                 createParentDirs(file);
                 write(data, file);
@@ -243,6 +281,15 @@
         zis.close();
     }
 
+    // Saves the specified XML stream into app-specific directory.
+    private void expandPlainApplication(byte[] stream, ApplicationDescription desc)
+            throws IOException {
+        File file = appFile(desc.name(), APP_XML);
+        createParentDirs(file);
+        write(stream, file);
+    }
+
+
     // Saves the specified ZIP stream into a file under app-specific directory.
     private void saveApplication(InputStream stream, ApplicationDescription desc)
             throws IOException {
diff --git a/core/common/src/test/java/org/onosproject/common/app/ApplicationArchiveTest.java b/core/common/src/test/java/org/onosproject/common/app/ApplicationArchiveTest.java
index 3d86dda..240ed96 100644
--- a/core/common/src/test/java/org/onosproject/common/app/ApplicationArchiveTest.java
+++ b/core/common/src/test/java/org/onosproject/common/app/ApplicationArchiveTest.java
@@ -30,8 +30,7 @@
 import java.util.Random;
 import java.util.Set;
 
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.*;
 import static org.onosproject.app.DefaultApplicationDescriptionTest.*;
 
 public class ApplicationArchiveTest {
@@ -64,43 +63,69 @@
     }
 
     @Test
-    public void saveApp() throws IOException {
+    public void saveZippedApp() throws IOException {
         InputStream stream = getClass().getResourceAsStream("app.zip");
         ApplicationDescription app = aar.saveApplication(stream);
         validate(app);
     }
 
     @Test
+    public void savePlainApp() throws IOException {
+        InputStream stream = getClass().getResourceAsStream("app.xml");
+        ApplicationDescription app = aar.saveApplication(stream);
+        validate(app);
+    }
+
+    @Test
     public void loadApp() throws IOException {
-        saveApp();
+        saveZippedApp();
         ApplicationDescription app = aar.getApplicationDescription(APP_NAME);
         validate(app);
     }
 
     @Test
     public void getAppNames() throws IOException {
-        saveApp();
+        saveZippedApp();
         Set<String> names = aar.getApplicationNames();
         assertEquals("incorrect names", ImmutableSet.of(APP_NAME), names);
     }
 
     @Test
     public void purgeApp() throws IOException {
-        saveApp();
+        saveZippedApp();
         aar.purgeApplication(APP_NAME);
         assertEquals("incorrect names", ImmutableSet.<String>of(),
                      aar.getApplicationNames());
     }
 
     @Test
-    public void getAppStream() throws IOException {
-        saveApp();
+    public void getAppZipStream() throws IOException {
+        saveZippedApp();
         InputStream stream = aar.getApplicationInputStream(APP_NAME);
         byte[] orig = ByteStreams.toByteArray(getClass().getResourceAsStream("app.zip"));
         byte[] loaded = ByteStreams.toByteArray(stream);
         assertArrayEquals("incorrect stream", orig, loaded);
     }
 
+    @Test
+    public void getAppXmlStream() throws IOException {
+        savePlainApp();
+        InputStream stream = aar.getApplicationInputStream(APP_NAME);
+        byte[] orig = ByteStreams.toByteArray(getClass().getResourceAsStream("app.xml"));
+        byte[] loaded = ByteStreams.toByteArray(stream);
+        assertArrayEquals("incorrect stream", orig, loaded);
+    }
+
+    @Test
+    public void active() throws IOException {
+        savePlainApp();
+        assertFalse("should not be active", aar.isActive(APP_NAME));
+        aar.setActive(APP_NAME);
+        assertTrue("should not be active", aar.isActive(APP_NAME));
+        aar.clearActive(APP_NAME);
+        assertFalse("should not be active", aar.isActive(APP_NAME));
+    }
+
     @Test(expected = ApplicationException.class)
     public void getBadAppDesc() throws IOException {
         aar.getApplicationDescription("org.foo.BAD");
@@ -111,4 +136,14 @@
         aar.getApplicationInputStream("org.foo.BAD");
     }
 
+    @Test(expected = ApplicationException.class)
+    public void setBadActive() throws IOException {
+        aar.setActive("org.foo.BAD");
+    }
+
+    @Test(expected = ApplicationException.class)
+    public void purgeBadApp() throws IOException {
+        aar.purgeApplication("org.foo.BAD");
+    }
+
 }
\ No newline at end of file
diff --git a/core/common/src/test/resources/org/onosproject/common/app/app.xml b/core/common/src/test/resources/org/onosproject/common/app/app.xml
index 3b8bc62..39f328b 100644
--- a/core/common/src/test/resources/org/onosproject/common/app/app.xml
+++ b/core/common/src/test/resources/org/onosproject/common/app/app.xml
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
 <!--
   ~ Copyright 2015 Open Networking Laboratory
   ~
diff --git a/features/features.xml b/features/features.xml
index 9440f52..aed7d7c 100644
--- a/features/features.xml
+++ b/features/features.xml
@@ -16,7 +16,8 @@
   -->
 <features xmlns="http://karaf.apache.org/xmlns/features/v1.2.0"
           name="onos-@FEATURE-VERSION">
-    <repository>mvn:org.onosproject/onos-features/@ONOS-VERSION/xml/features</repository>
+    <repository>mvn:org.onosproject/onos-features/@ONOS-VERSION/xml/features
+    </repository>
 
     <feature name="onos-thirdparty-base" version="@FEATURE-VERSION"
              description="ONOS 3rd party dependencies">
@@ -126,7 +127,7 @@
     </feature>
 
     <feature name="onos-null" version="@FEATURE-VERSION"
-            description="ONOS Null providers">
+             description="ONOS Null providers">
         <feature>onos-api</feature>
 
         <bundle>mvn:org.onosproject/onos-null-provider-device/@ONOS-VERSION</bundle>
@@ -197,12 +198,12 @@
         <feature>onos-api</feature>
         <bundle>mvn:org.onosproject/onos-app-config/@ONOS-VERSION</bundle>
     </feature>
-   
-     <feature name="onos-app-optical" version="@FEATURE-VERSION"
+
+    <feature name="onos-app-optical" version="@FEATURE-VERSION"
              description="ONOS optical network config">
         <feature>onos-api</feature>
         <bundle>mvn:org.onosproject/onos-app-optical/@ONOS-VERSION</bundle>
-     </feature>
+    </feature>
 
     <feature name="onos-app-sdnip" version="@FEATURE-VERSION"
              description="SDN-IP peering application">
@@ -210,8 +211,8 @@
         <feature>onos-app-proxyarp</feature>
         <feature>onos-app-config</feature>
         <bundle>mvn:org.onosproject/onos-app-sdnip/@ONOS-VERSION</bundle>
-	<bundle>mvn:org.onosproject/onos-app-routing-api/@ONOS-VERSION</bundle>
-	<bundle>mvn:org.onosproject/onos-app-routing/@ONOS-VERSION</bundle>
+        <bundle>mvn:org.onosproject/onos-app-routing-api/@ONOS-VERSION</bundle>
+        <bundle>mvn:org.onosproject/onos-app-routing/@ONOS-VERSION</bundle>
     </feature>
 
     <feature name="onos-app-calendar" version="@FEATURE-VERSION"
diff --git a/tools/test/bin/onos-app b/tools/test/bin/onos-app
index d474ef3..31b87d3 100755
--- a/tools/test/bin/onos-app
+++ b/tools/test/bin/onos-app
@@ -14,7 +14,11 @@
 case $cmd in
     list) $curl -X GET $URL;;
     install) $curl -X POST $HDR $URL --data-binary @$app;;
+    install!) $curl -X POST $HDR $URL?activate=true --data-binary @$app;;
     uninstall) $curl -X DELETE $URL/$app;;
     activate) $curl -X POST $URL/$app/active;;
     deactivate) $curl -X DELETE $URL/$app/active;;
+    *) echo "usage: onos-app {install|install!} <app-file>" >&2
+       echo "       onos-app {activate|deactivate|uninstall} <app-name>" >&2
+       exit 1;;
 esac
diff --git a/utils/misc/src/main/java/org/onlab/util/Tools.java b/utils/misc/src/main/java/org/onlab/util/Tools.java
index 9e690c0..e366172 100644
--- a/utils/misc/src/main/java/org/onlab/util/Tools.java
+++ b/utils/misc/src/main/java/org/onlab/util/Tools.java
@@ -182,7 +182,11 @@
      * @throws java.io.IOException if unable to remove contents
      */
     public static void removeDirectory(String path) throws IOException {
-        walkFileTree(Paths.get(path), new DirectoryDeleter());
+        DirectoryDeleter visitor = new DirectoryDeleter();
+        walkFileTree(Paths.get(path), visitor);
+        if (visitor.exception != null) {
+            throw visitor.exception;
+        }
     }
 
     /**
@@ -194,11 +198,18 @@
      * @throws java.io.IOException if unable to remove contents
      */
     public static void removeDirectory(File dir) throws IOException {
-        walkFileTree(Paths.get(dir.getAbsolutePath()), new DirectoryDeleter());
+        DirectoryDeleter visitor = new DirectoryDeleter();
+        walkFileTree(Paths.get(dir.getAbsolutePath()), visitor);
+        if (visitor.exception != null) {
+            throw visitor.exception;
+        }
     }
 
-
+    // Auxiliary path visitor for recursive directory structure removal.
     private static class DirectoryDeleter extends SimpleFileVisitor<Path> {
+
+        private IOException exception;
+
         @Override
         public FileVisitResult visitFile(Path file, BasicFileAttributes attributes)
                 throws IOException {
@@ -218,9 +229,8 @@
         @Override
         public FileVisitResult visitFileFailed(Path file, IOException ioe)
                 throws IOException {
-            log.warn("Unable to delete file {}", file);
-            log.warn("Boom", ioe);
-            return FileVisitResult.CONTINUE;
+            this.exception = ioe;
+            return FileVisitResult.TERMINATE;
         }
     }
 
@@ -253,8 +263,8 @@
                                          dst.getAbsolutePath()));
     }
 
-
-    public static class DirectoryCopier extends SimpleFileVisitor<Path> {
+    // Auxiliary path visitor for recursive directory structure copying.
+    private static class DirectoryCopier extends SimpleFileVisitor<Path> {
         private Path src;
         private Path dst;
         private StandardCopyOption copyOption = StandardCopyOption.REPLACE_EXISTING;
diff --git a/web/api/src/main/java/org/onosproject/rest/ApplicationsWebResource.java b/web/api/src/main/java/org/onosproject/rest/ApplicationsWebResource.java
index 9d32090..1b3bd3a 100644
--- a/web/api/src/main/java/org/onosproject/rest/ApplicationsWebResource.java
+++ b/web/api/src/main/java/org/onosproject/rest/ApplicationsWebResource.java
@@ -21,11 +21,13 @@
 
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
 import javax.ws.rs.GET;
 import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import java.io.InputStream;
@@ -55,9 +57,14 @@
     @POST
     @Consumes(MediaType.APPLICATION_OCTET_STREAM)
     @Produces(MediaType.APPLICATION_JSON)
-    public Response installApplication(InputStream stream) {
+    public Response installApplication(@QueryParam("activate")
+                                           @DefaultValue("false") boolean activate,
+                                       InputStream stream) {
         ApplicationAdminService service = get(ApplicationAdminService.class);
         Application app = service.install(stream);
+        if (activate) {
+            service.activate(app.id());
+        }
         return ok(codec(Application.class).encode(app, this)).build();
     }