ONOS-542 Defining application subsystem interfaces & public constructs.

Change-Id: Iba0d2cb69dace5beee8a68def9918059ce755b5c
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
new file mode 100644
index 0000000..90e6005
--- /dev/null
+++ b/core/common/src/main/java/org/onosproject/common/app/ApplicationArchive.java
@@ -0,0 +1,267 @@
+/*
+ * 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.
+ */
+package org.onosproject.common.app;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.io.ByteStreams;
+import com.google.common.io.Files;
+import org.apache.commons.configuration.ConfigurationException;
+import org.apache.commons.configuration.XMLConfiguration;
+import org.onlab.util.Tools;
+import org.onosproject.app.ApplicationDescription;
+import org.onosproject.app.ApplicationEvent;
+import org.onosproject.app.ApplicationException;
+import org.onosproject.app.ApplicationStoreDelegate;
+import org.onosproject.app.DefaultApplicationDescription;
+import org.onosproject.core.Permission;
+import org.onosproject.core.Version;
+import org.onosproject.store.AbstractStore;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.Set;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import static com.google.common.io.ByteStreams.toByteArray;
+import static com.google.common.io.Files.createParentDirs;
+import static com.google.common.io.Files.write;
+
+/**
+ * Facility for reading application archive stream and managing application
+ * directory structure.
+ */
+public class ApplicationArchive
+        extends AbstractStore<ApplicationEvent, ApplicationStoreDelegate> {
+
+    private static final String NAME = "[@name]";
+    private static final String ORIGIN = "[@origin]";
+    private static final String VERSION = "[@version]";
+    private static final String FEATURES_REPO = "[@featuresRepo]";
+    private static final String FEATURES = "[@features]";
+    private static final String DESCRIPTION = "description";
+
+    private static Logger log = LoggerFactory.getLogger(ApplicationArchive.class);
+    private static final String APP_XML = "app.xml";
+    private static final String APPS_ROOT = "data/apps/";
+
+    private File appsDir = new File(APPS_ROOT);
+
+    /**
+     * Sets the root directory where application artifacts are kept.
+     *
+     * @param appsRoot top-level applications directory path
+     */
+    protected void setAppsRoot(String appsRoot) {
+        this.appsDir = new File(appsRoot);
+    }
+
+    /**
+     * Returns the root directory where application artifacts are kept.
+     *
+     * @return top-level applications directory path
+     */
+    protected String getAppsRoot() {
+        return appsDir.getPath();
+    }
+
+    /**
+     * Returns the set of installed application names.
+     *
+     * @return installed application names
+     */
+    public Set<String> getApplicationNames() {
+        ImmutableSet.Builder<String> names = ImmutableSet.builder();
+        File[] files = appsDir.listFiles(File::isDirectory);
+        if (files != null) {
+            for (File file : files) {
+                names.add(file.getName());
+            }
+        }
+        return names.build();
+    }
+
+    /**
+     * Loads the application descriptor from the specified application archive
+     * stream and saves the stream in the appropriate application archive
+     * directory.
+     *
+     * @param appName application name
+     * @return application descriptor
+     * @throws org.onosproject.app.ApplicationException if unable to read application description
+     */
+    public ApplicationDescription getApplicationDescription(String appName) {
+        try {
+            return loadAppDescription(new XMLConfiguration(appFile(appName, APP_XML)));
+        } catch (Exception e) {
+            throw new ApplicationException("Unable to get app description", e);
+        }
+    }
+
+    /**
+     * Loads the application descriptor from the specified application archive
+     * stream and saves the stream in the appropriate application archive
+     * directory.
+     *
+     * @param stream application archive stream
+     * @return application descriptor
+     * @throws org.onosproject.app.ApplicationException if unable to read the
+     *                                                  archive stream or store
+     *                                                  the application archive
+     */
+    public ApplicationDescription saveApplication(InputStream stream) {
+        try (InputStream ais = stream) {
+            byte[] cache = toByteArray(ais);
+            InputStream bis = new ByteArrayInputStream(cache);
+            ApplicationDescription desc = parseAppDescription(bis);
+            bis.reset();
+
+            expandApplication(bis, desc);
+            bis.reset();
+
+            saveApplication(bis, desc);
+            installArtifacts(desc);
+            return desc;
+        } catch (IOException e) {
+            throw new ApplicationException("Unable to save application", e);
+        }
+    }
+
+    /**
+     * Purges the application archive directory.
+     *
+     * @param appName application name
+     */
+    public void purgeApplication(String appName) {
+        try {
+            Tools.removeDirectory(new File(appsDir, appName));
+        } catch (IOException e) {
+            throw new ApplicationException("Unable to purge application " + appName, e);
+        }
+    }
+
+    /**
+     * Returns application archive stream for the specified application.
+     *
+     * @param appName application name
+     * @return application archive stream
+     */
+    public InputStream getApplicationInputStream(String appName) {
+        try {
+            return new FileInputStream(appFile(appName, appName + ".zip"));
+        } catch (FileNotFoundException e) {
+            throw new ApplicationException("Application " + appName + " not found");
+        }
+    }
+
+    // Scans the specified ZIP stream for app.xml entry and parses it producing
+    // an application descriptor.
+    private ApplicationDescription parseAppDescription(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 = new byte[(int) entry.getSize()];
+                    ByteStreams.readFully(zis, data);
+                    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);
+                    }
+                }
+                zis.closeEntry();
+            }
+        }
+        throw new IOException("Unable to locate " + APP_XML);
+    }
+
+    private ApplicationDescription loadAppDescription(XMLConfiguration cfg) {
+        cfg.setAttributeSplittingDisabled(true);
+        String name = cfg.getString(NAME);
+        Version version = Version.version(cfg.getString(VERSION));
+        String desc = cfg.getString(DESCRIPTION);
+        String origin = cfg.getString(ORIGIN);
+        Set<Permission> perms = ImmutableSet.of();
+        String featRepo = cfg.getString(FEATURES_REPO);
+        URI featuresRepo = featRepo != null ? URI.create(featRepo) : null;
+        Set<String> features = ImmutableSet.copyOf(cfg.getString(FEATURES).split(","));
+
+        return new DefaultApplicationDescription(name, version, desc, origin,
+                                                 perms, featuresRepo, features);
+    }
+
+    // Expands the specified ZIP stream into app-specific directory.
+    private void expandApplication(InputStream stream, ApplicationDescription desc)
+            throws IOException {
+        ZipInputStream zis = new ZipInputStream(stream);
+        ZipEntry entry;
+        File appDir = new File(appsDir, desc.name());
+        while ((entry = zis.getNextEntry()) != null) {
+            byte[] data = new byte[(int) entry.getSize()];
+            ByteStreams.readFully(zis, data);
+            zis.closeEntry();
+
+            File file = new File(appDir, entry.getName());
+            createParentDirs(file);
+            write(data, file);
+        }
+        zis.close();
+    }
+
+    // Saves the specified ZIP stream into a file under app-specific directory.
+    private void saveApplication(InputStream stream, ApplicationDescription desc)
+            throws IOException {
+        Files.write(toByteArray(stream), appFile(desc.name(), desc.name() + ".zip"));
+    }
+
+    // Installs application artifacts into M2 repository.
+    private void installArtifacts(ApplicationDescription desc) {
+        // FIXME: implement M2 repository copy
+    }
+
+    protected boolean setActive(String appName) {
+        try {
+            return appFile(appName, "active").createNewFile();
+        } catch (IOException e) {
+            throw new ApplicationException("Unable to mark app as active", e);
+        }
+    }
+
+    protected boolean clearActive(String appName) {
+        return appFile(appName, "active").delete();
+    }
+
+    protected boolean isActive(String appName) {
+        return appFile(appName, "active").exists();
+    }
+
+
+    // Returns the name of the file located under the specified app directory.
+    private File appFile(String appName, String fileName) {
+        return new File(new File(appsDir, appName), fileName);
+    }
+
+}
diff --git a/core/common/src/main/java/org/onosproject/common/app/package-info.java b/core/common/src/main/java/org/onosproject/common/app/package-info.java
new file mode 100644
index 0000000..898bad7
--- /dev/null
+++ b/core/common/src/main/java/org/onosproject/common/app/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+/**
+ * Common facilities for construction of application management subsystem.
+ */
+package org.onosproject.common.app;
\ No newline at end of file
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
new file mode 100644
index 0000000..c1b8e82
--- /dev/null
+++ b/core/common/src/test/java/org/onosproject/common/app/ApplicationArchiveTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.
+ */
+package org.onosproject.common.app;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.io.ByteStreams;
+import org.junit.Before;
+import org.junit.Test;
+import org.onosproject.app.ApplicationDescription;
+import org.onosproject.app.ApplicationException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Set;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.onosproject.app.DefaultApplicationDescriptionTest.*;
+
+public class ApplicationArchiveTest {
+
+    static final String ROOT = "/tmp/app-junit";
+
+    private ApplicationArchive aar = new ApplicationArchive();
+
+    @Before
+    public void setUp() {
+        aar.setAppsRoot(ROOT);
+    }
+
+    private void validate(ApplicationDescription app) {
+        assertEquals("incorrect name", APP_NAME, app.name());
+        assertEquals("incorrect version", VER, app.version());
+        assertEquals("incorrect origin", ORIGIN, app.origin());
+
+        assertEquals("incorrect description", DESC, app.description());
+        assertEquals("incorrect features URI", FURL, app.featuresRepo().get());
+        assertEquals("incorrect permissions", PERMS, app.permissions());
+        assertEquals("incorrect features", FEATURES, app.features());
+    }
+
+    @Test
+    public void saveApp() throws IOException {
+        InputStream stream = getClass().getResourceAsStream("app.zip");
+        ApplicationDescription app = aar.saveApplication(stream);
+        validate(app);
+    }
+
+    @Test
+    public void loadApp() throws IOException {
+        saveApp();
+        ApplicationDescription app = aar.getApplicationDescription(APP_NAME);
+        validate(app);
+    }
+
+    @Test
+    public void getAppNames() throws IOException {
+        saveApp();
+        Set<String> names = aar.getApplicationNames();
+        assertEquals("incorrect names", ImmutableSet.of(APP_NAME), names);
+    }
+
+    @Test
+    public void purgeApp() throws IOException {
+        saveApp();
+        aar.purgeApplication(APP_NAME);
+        assertEquals("incorrect names", ImmutableSet.of(), aar.getApplicationNames());
+    }
+
+    @Test
+    public void getAppStream() throws IOException {
+        saveApp();
+        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(expected = ApplicationException.class)
+    public void getBadAppDesc() throws IOException {
+        aar.getApplicationDescription("org.foo.BAD");
+    }
+
+    @Test(expected = ApplicationException.class)
+    public void getBadAppStream() throws IOException {
+        aar.getApplicationInputStream("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
new file mode 100644
index 0000000..3b8bc62
--- /dev/null
+++ b/core/common/src/test/resources/org/onosproject/common/app/app.xml
@@ -0,0 +1,20 @@
+<!--
+  ~ 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.foo.app" origin="Circus" version="1.2.a"
+        featuresRepo="mvn:org.foo-features/1.2a/xml/features"
+        features="foo,bar">
+    <description>Awesome application from Circus, Inc.</description>
+</app>
diff --git a/core/common/src/test/resources/org/onosproject/common/app/app.zip b/core/common/src/test/resources/org/onosproject/common/app/app.zip
new file mode 100644
index 0000000..07705e7
--- /dev/null
+++ b/core/common/src/test/resources/org/onosproject/common/app/app.zip
Binary files differ