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/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
   ~