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