Implementing ability to process single-bundle ONOS apps whose meta-data
is self-contained in the jar itself.

Change-Id: Ie80ce5e39fd236dbcd0499ac1afec2399c2ea3cc
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 a8b5e0e..3d92eae 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
@@ -94,11 +94,12 @@
     private static final String NET_PERMISSIONS = "security.permissions.net-perm";
     private static final String JAVA_PERMISSIONS = "security.permissions.java-perm";
 
+    private static final String JAR = ".jar";
     private static final String OAR = ".oar";
-    private static final String PNG = "png";
     private static final String APP_XML = "app.xml";
     private static final String APP_PNG = "app.png";
     private static final String M2_PREFIX = "m2";
+    private static final String FEATURES_XML = "features.xml";
 
     private static final String ROOT = "../";
     private static final String M2_ROOT = "system/";
@@ -198,14 +199,21 @@
             checkState(!appFile(desc.name(), APP_XML).exists(),
                     "Application %s already installed", desc.name());
 
+            boolean isSelfContainedJar = false;
+
             if (plainXml) {
                 expandPlainApplication(cache, desc);
             } else {
                 bis.reset();
-                expandZippedApplication(bis, desc);
+                isSelfContainedJar = expandZippedApplication(bis, desc);
+
+                if (isSelfContainedJar) {
+                    bis.reset();
+                    stageSelfContainedJar(bis, desc);
+                }
 
                 bis.reset();
-                saveApplication(bis, desc);
+                saveApplication(bis, desc, isSelfContainedJar);
             }
 
             installArtifacts(desc);
@@ -326,8 +334,10 @@
     }
 
     // Expands the specified ZIP stream into app-specific directory.
-    private void expandZippedApplication(InputStream stream, ApplicationDescription desc)
+    // Returns true of the application is a self-contained jar rather than an oar file.
+    private boolean expandZippedApplication(InputStream stream, ApplicationDescription desc)
             throws IOException {
+        boolean isSelfContained = false;
         ZipInputStream zis = new ZipInputStream(stream);
         ZipEntry entry;
         File appDir = new File(appsDir, desc.name());
@@ -336,11 +346,67 @@
                 byte[] data = ByteStreams.toByteArray(zis);
                 zis.closeEntry();
                 File file = new File(appDir, entry.getName());
-                createParentDirs(file);
-                write(data, file);
+                if (isTopLevel(file)) {
+                    createParentDirs(file);
+                    write(data, file);
+                } else {
+                    isSelfContained = true;
+                }
             }
         }
         zis.close();
+        return isSelfContained;
+    }
+
+    // Returns true if the specified file is a top-level app file, i.e. app.xml,
+    // features.xml, .jar or a directory; false if anything else.
+    private boolean isTopLevel(File file) {
+        String name = file.getName();
+        return name.equals(APP_XML) || name.endsWith(FEATURES_XML) || name.endsWith(JAR) || file.isDirectory();
+    }
+
+    // Expands the self-contained JAR stream into the app-specific directory,
+    // using the bundle coordinates retrieved from the features.xml file.
+    private void stageSelfContainedJar(InputStream stream, ApplicationDescription desc)
+            throws IOException {
+        // First extract the bundle coordinates
+        String coords = getSelfContainedBundleCoordinates(desc);
+        if (coords == null) {
+            return;
+        }
+
+        // Split the coordinates into segments and build the file name.
+        String[] f = coords.substring(4).split("/");
+        String base = "m2/" + f[0].replace('.', '/') + "/" + f[1] + "/" + f[2] + "/" + f[1] + "-" + f[2];
+        String jarName = base + (f.length < 4 ? "" : "-" + f[3]) + ".jar";
+        String featuresName =  base + "-features.xml";
+
+        // Create the file directory structure and copy the file there.
+        File jar = appFile(desc.name(), jarName);
+        boolean ok = jar.getParentFile().mkdirs();
+        if (ok) {
+            Files.write(toByteArray(stream), jar);
+            Files.copy(appFile(desc.name(), FEATURES_XML), appFile(desc.name(), featuresName));
+            if (!appFile(desc.name(), FEATURES_XML).delete()) {
+                log.warn("Unable to delete self-contained application {} features.xml", desc.name());
+            }
+        } else {
+            throw new IOException("Unable to save self-contained application " + desc.name());
+        }
+    }
+
+    // Returns the bundle coordinates from the features.xml file.
+    private String getSelfContainedBundleCoordinates(ApplicationDescription desc) {
+        try {
+            XMLConfiguration cfg = new XMLConfiguration();
+            cfg.setAttributeSplittingDisabled(true);
+            cfg.setDelimiterParsingDisabled(true);
+            cfg.load(appFile(desc.name(), FEATURES_XML));
+            return cfg.getString("feature.bundle");
+        } catch (ConfigurationException e) {
+            log.warn("Self-contained application {} has no features.xml", desc.name());
+            return null;
+        }
     }
 
     // Saves the specified XML stream into app-specific directory.
@@ -352,11 +418,12 @@
         write(stream, file);
     }
 
-
     // Saves the specified ZIP stream into a file under app-specific directory.
-    private void saveApplication(InputStream stream, ApplicationDescription desc)
+    private void saveApplication(InputStream stream, ApplicationDescription desc,
+                                 boolean isSelfContainedJar)
             throws IOException {
-        Files.write(toByteArray(stream), appFile(desc.name(), desc.name() + OAR));
+        String name = desc.name() + (isSelfContainedJar ? JAR : OAR);
+        Files.write(toByteArray(stream), appFile(desc.name(), name));
     }
 
     // Installs application artifacts into M2 repository.
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 d2c6f3c..1f273cc 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
@@ -89,6 +89,14 @@
     }
 
     @Test
+    public void saveSelfContainedApp() throws IOException {
+        InputStream stream = getClass().getResourceAsStream("app.scj");
+        ApplicationDescription app = aar.saveApplication(stream);
+        validate(app);
+        stream.close();
+    }
+
+    @Test
     public void loadApp() throws IOException {
         saveZippedApp();
         ApplicationDescription app = aar.getApplicationDescription(APP_NAME);
diff --git a/core/common/src/test/resources/org/onosproject/common/app/app.scj b/core/common/src/test/resources/org/onosproject/common/app/app.scj
new file mode 100644
index 0000000..74ba26b
--- /dev/null
+++ b/core/common/src/test/resources/org/onosproject/common/app/app.scj
Binary files differ