ONOS-1290 Implemented OnosAppMojo for packaging and installing ONOS apps as Maven artifacts.

Change-Id: Id9452beea46f37bd0f0737f478f2a2541dc5deb9
diff --git a/tools/package/maven-plugin/pom.xml b/tools/package/maven-plugin/pom.xml
index fc3af91..a477963 100644
--- a/tools/package/maven-plugin/pom.xml
+++ b/tools/package/maven-plugin/pom.xml
@@ -24,10 +24,12 @@
     </parent>
 
     <artifactId>onos-maven-plugin</artifactId>
-    <version>1.2.0-SNAPSHOT</version>
+    <version>1.2-SNAPSHOT</version>
     <packaging>maven-plugin</packaging>
 
-    <description>Maven plugin for packaging ONOS applications or generating component configuration resources</description>
+    <description>Maven plugin for packaging ONOS applications or generating
+        component configuration resources
+    </description>
 
     <dependencies>
         <dependency>
@@ -37,11 +39,35 @@
         </dependency>
 
         <dependency>
+            <groupId>org.apache.maven</groupId>
+            <artifactId>maven-project</artifactId>
+            <version>2.0</version>
+        </dependency>
+
+        <dependency>
             <groupId>com.thoughtworks.qdox</groupId>
             <artifactId>qdox</artifactId>
             <version>2.0-M3</version>
         </dependency>
 
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>18.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>commons-collections</groupId>
+            <artifactId>commons-collections</artifactId>
+            <version>3.2.1</version>
+        </dependency>
+
+        <dependency>
+            <groupId>commons-configuration</groupId>
+            <artifactId>commons-configuration</artifactId>
+            <version>1.10</version>
+        </dependency>
+
         <!-- dependencies to annotations -->
         <dependency>
             <groupId>org.apache.maven.plugin-tools</groupId>
diff --git a/tools/package/maven-plugin/src/main/java/org/onosproject/maven/OnosAppMojo.java b/tools/package/maven-plugin/src/main/java/org/onosproject/maven/OnosAppMojo.java
index 9b39903..9ba2514 100644
--- a/tools/package/maven-plugin/src/main/java/org/onosproject/maven/OnosAppMojo.java
+++ b/tools/package/maven-plugin/src/main/java/org/onosproject/maven/OnosAppMojo.java
@@ -15,49 +15,325 @@
  */
 package org.onosproject.maven;
 
+import com.google.common.io.ByteStreams;
+import org.apache.commons.configuration.ConfigurationException;
+import org.apache.commons.configuration.XMLConfiguration;
 import org.apache.maven.plugin.AbstractMojo;
 import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugins.annotations.Component;
 import org.apache.maven.plugins.annotations.LifecyclePhase;
 import org.apache.maven.plugins.annotations.Mojo;
 import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.project.MavenProject;
+import org.apache.maven.project.MavenProjectHelper;
 
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
 import java.util.List;
+import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import static org.codehaus.plexus.util.FileUtils.*;
 
 /**
- * Produces ONOS application archive.
+ * Produces ONOS application archive using the app.xml file information.
  */
 @Mojo(name = "app", defaultPhase = LifecyclePhase.PACKAGE)
 public class OnosAppMojo extends AbstractMojo {
 
-    @Parameter
-    private String name;
+    private static final String APP = "app";
+    private static final String NAME = "[@name]";
+    private static final String VERSION = "[@version]";
+    private static final String FEATURES_REPO = "[@featuresRepo]";
+    private static final String DESCRIPTION = "description";
+    private static final String ARTIFACT = "artifact";
 
-    @Parameter
-    private String version;
+    private static final String APP_XML = "app.xml";
+    private static final String FEATURES_XML = "features.xml";
 
-    @Parameter
-    private String origin;
+    private static final String MVN_URL = "mvn:";
+    private static final String M2_REPOSITORY = ".m2/repository";
+    private static final String M2_PREFIX = "m2";
 
-    @Parameter
-    private String description;
+    private static final String SNAPSHOT = "-SNAPSHOT";
+    private static final String JAR = "jar";
+    private static final String XML = "xml";
+    private static final String APP_ZIP = "oar";
+    private static final String PACKAGE_DIR = "oar";
 
-    @Parameter
-    private String featuresRepo;
+    private static final int BUFFER_ZIZE = 8192;
 
-    @Parameter
-    private String features;
-
-    @Parameter
-    private String permissions;
-
-    @Parameter
+    private String name, version;
+    private String description, featuresRepo;
     private List<String> artifacts;
 
+    /**
+     * The project base directory.
+     */
+    @Parameter(defaultValue = "${basedir}")
+    protected File baseDir;
 
+    /**
+     * The directory where the generated catalogue file will be put.
+     */
+    @Parameter(defaultValue = "${project.build.directory}")
+    protected File dstDirectory;
+
+    /**
+     * The project group ID.
+     */
+    @Parameter(defaultValue = "${project.groupId}")
+    protected String projectGroupId;
+
+    /**
+     * The project artifact ID.
+     */
+    @Parameter(defaultValue = "${project.artifactId}")
+    protected String projectArtifactId;
+
+    /**
+     * The project version.
+     */
+    @Parameter(defaultValue = "${project.version}")
+    protected String projectVersion;
+
+    /**
+     * The project version.
+     */
+    @Parameter(defaultValue = "${project.description}")
+    protected String projectDescription;
+
+    /**
+     * Maven project
+     */
+    @Component
+    private MavenProject project;
+
+    /**
+     * Maven project helper.
+     */
+    @Component
+    private MavenProjectHelper projectHelper;
+
+
+    private File m2Directory;
+    protected File stageDirectory;
+    protected String projectPath;
+    protected String featureVersion;
+
+    @Override
     public void execute() throws MojoExecutionException {
-        getLog().info("Building ONOS application archive " + name + " version " + version);
+        File appFile = new File(baseDir, APP_XML);
+        File featuresFile = new File(baseDir, FEATURES_XML);
 
+        if (!appFile.exists()) {
+            return;
+        }
+
+        m2Directory = new File(System.getProperty("user.home"), M2_REPOSITORY);
+        stageDirectory = new File(dstDirectory, PACKAGE_DIR);
+        featureVersion = projectVersion.replace(SNAPSHOT, "");
+        projectPath = M2_PREFIX + "/" + artifactDir(projectGroupId, projectArtifactId, projectVersion);
+
+        loadAppFile(appFile);
+
+        // If there are any artifacts, stage the
+        if (!artifacts.isEmpty()) {
+            getLog().info("Building ONOS application package for " + name + " (v" + version + ")");
+            artifacts.forEach(a -> getLog().debug("Including artifact: " + a));
+
+            stageDirectory.mkdirs();
+            processAppXml(appFile);
+            processFeaturesXml(featuresFile);
+            processArtifacts();
+            generateAppPackage();
+        }
+    }
+
+    // Loads the app.xml file.
+    private void loadAppFile(File appFile) throws MojoExecutionException {
+        XMLConfiguration xml = new XMLConfiguration();
+        xml.setRootElementName(APP);
+
+        try (FileInputStream stream = new FileInputStream(appFile)) {
+            xml.load(stream);
+            xml.setAttributeSplittingDisabled(true);
+            xml.setDelimiterParsingDisabled(true);
+
+            name = xml.getString(NAME);
+            version = eval(xml.getString(VERSION));
+            description = xml.getString(DESCRIPTION)
+                    .replaceAll("\\$\\{project.description\\}", projectDescription);
+            featuresRepo = eval(xml.getString(FEATURES_REPO));
+
+            artifacts = xml.configurationsAt(ARTIFACT).stream()
+                    .map(cfg -> eval(cfg.getRootNode().getValue().toString()))
+                    .collect(Collectors.toList());
+
+        } catch (ConfigurationException e) {
+            throw new MojoExecutionException("Unable to parse app.xml file", e);
+        } catch (FileNotFoundException e) {
+            throw new MojoExecutionException("Unable to find app.xml file", e);
+        } catch (IOException e) {
+            throw new MojoExecutionException("Unable to read app.xml file", e);
+        }
+    }
+
+    // Processes and stages the app.xml file.
+    private void processAppXml(File appFile) throws MojoExecutionException {
+        try {
+            File file = new File(stageDirectory, APP_XML);
+            forceMkdir(stageDirectory);
+            String s = eval(fileRead(appFile));
+            fileWrite(file.getAbsolutePath(), s);
+        } catch (IOException e) {
+            throw new MojoExecutionException("Unable to process app.xml", e);
+        }
+    }
+
+    private void processFeaturesXml(File featuresFile) throws MojoExecutionException {
+        boolean specified = featuresRepo != null && featuresRepo.length() > 0;
+
+        // If featuresRepo attribute is specified and there is a features.xml
+        // file present, add the features repo as an artifact
+        try {
+            if (specified && featuresFile.exists()) {
+                processFeaturesXml(new FileInputStream(featuresFile));
+            } else if (specified) {
+                processFeaturesXml(getClass().getResourceAsStream(FEATURES_XML));
+            }
+        } catch (FileNotFoundException e) {
+            throw new MojoExecutionException("Unable to find features.xml file", e);
+        } catch (IOException e) {
+            throw new MojoExecutionException("Unable to process features.xml file", e);
+        }
+    }
+
+    // Processes and stages the features.xml file.
+    private void processFeaturesXml(InputStream stream) throws IOException {
+        String featuresArtifact =
+                artifactFile(projectArtifactId, projectVersion, XML, "features");
+        File dstDir = new File(stageDirectory, projectPath);
+        forceMkdir(dstDir);
+        String s = eval(new String(ByteStreams.toByteArray(stream)));
+        fileWrite(new File(dstDir, featuresArtifact).getAbsolutePath(), s);
+    }
+
+    // Stages all artifacts.
+    private void processArtifacts() {
+        artifacts.forEach(this::processArtifact);
+    }
+
+    // Stages the specified artifact.
+    private void processArtifact(String artifact) {
+        if (!artifact.startsWith(MVN_URL)) {
+            getLog().error("Unsupported artifact URL:" + artifact);
+            return;
+        }
+
+        String[] fields = artifact.substring(4).split("/");
+        if (fields.length < 3) {
+            getLog().error("Illegal artifact URL:" + artifact);
+            return;
+        }
+
+        try {
+            String file = artifactFile(fields);
+
+            if (projectGroupId.equals(fields[0]) && projectArtifactId.equals(fields[1])) {
+                // Local artifact is not installed yet, package it from target directory.
+                File dstDir = new File(stageDirectory, projectPath);
+                forceMkdir(dstDir);
+                copyFile(new File(dstDirectory, file), new File(dstDir, file));
+            } else {
+                // Other artifacts are packaged from ~/.m2/repository directory.
+                String m2Path = artifactDir(fields);
+                File srcDir = new File(m2Directory, m2Path);
+                File dstDir = new File(stageDirectory, m2Path);
+                forceMkdir(dstDir);
+                copyFile(new File(srcDir, file), new File(dstDir, file));
+            }
+        } catch (IOException e) {
+            getLog().error("Unable to stage artifact " + artifact + " due to ", e);
+        }
+    }
+
+    // Generates the ONOS package ZIP file.
+    private void generateAppPackage() throws MojoExecutionException {
+        File appZip = new File(dstDirectory, artifactFile(projectArtifactId, projectVersion,
+                                                          APP_ZIP, null));
+        try (FileOutputStream fos = new FileOutputStream(appZip);
+             ZipOutputStream zos = new ZipOutputStream(fos)) {
+            zipDirectory("", stageDirectory, zos);
+            projectHelper.attachArtifact(this.project, APP_ZIP, null, appZip);
+        } catch (IOException e) {
+            throw new MojoExecutionException("Unable to compress application package", e);
+        }
+    }
+
+    // Generates artifact directory name from the specified fields.
+    private String artifactDir(String[] fields) {
+        return artifactDir(fields[0], fields[1], fields[2]);
+    }
+
+    // Generates artifact directory name from the specified elements.
+    private String artifactDir(String gid, String aid, String version) {
+        return gid.replace('.', '/') + "/" + aid.replace('.', '/') + "/" + version;
+    }
+
+    // Generates artifact file name from the specified fields.
+    private String artifactFile(String[] fields) {
+        return fields.length < 5 ?
+                artifactFile(fields[1], fields[2],
+                             (fields.length < 4 ? JAR : fields[3]), null) :
+                artifactFile(fields[1], fields[2], fields[3], fields[4]);
+    }
+
+    // Generates artifact file name from the specified elements.
+    private String artifactFile(String aid, String version, String type,
+                                String classifier) {
+        return classifier == null ? aid + "-" + version + "." + type :
+                aid + "-" + version + "-" + classifier + "." + type;
+    }
+
+    // Returns the given string with project variable substitutions.
+    private String eval(String string) {
+        return string == null ? null :
+                string.replaceAll("\\$\\{project.groupId\\}", projectGroupId)
+                        .replaceAll("\\$\\{project.artifactId\\}", projectArtifactId)
+                        .replaceAll("\\$\\{project.version\\}", projectVersion)
+                        .replaceAll("\\$\\{project.description\\}", description)
+                        .replaceAll("\\$\\{feature.version\\}", featureVersion);
+    }
+
+    // Recursively archives the specified directory into a given ZIP stream.
+    private void zipDirectory(String root, File dir, ZipOutputStream zos)
+            throws IOException {
+        byte[] buffer = new byte[BUFFER_ZIZE];
+        File[] files = dir.listFiles();
+        if (files != null && files.length > 0) {
+            for (File file : files) {
+                if (file.isDirectory()) {
+                    String path = root + file.getName() + "/";
+                    zos.putNextEntry(new ZipEntry(path));
+                    zipDirectory(path, file, zos);
+                    zos.closeEntry();
+                } else {
+                    FileInputStream fin = new FileInputStream(file);
+                    zos.putNextEntry(new ZipEntry(root + file.getName()));
+                    int length;
+                    while ((length = fin.read(buffer)) > 0) {
+                        zos.write(buffer, 0, length);
+                    }
+                    zos.closeEntry();
+                    fin.close();
+                }
+            }
+        }
     }
 }
-
-
diff --git a/tools/package/maven-plugin/src/main/java/org/onosproject/maven/OnosCfgMojo.java b/tools/package/maven-plugin/src/main/java/org/onosproject/maven/OnosCfgMojo.java
index b2742d7..3e3e17a 100644
--- a/tools/package/maven-plugin/src/main/java/org/onosproject/maven/OnosCfgMojo.java
+++ b/tools/package/maven-plugin/src/main/java/org/onosproject/maven/OnosCfgMojo.java
@@ -45,13 +45,13 @@
     /**
      * The directory where the generated catalogue file will be put.
      */
-    @Parameter( defaultValue = "${basedir}" )
+    @Parameter(defaultValue = "${basedir}")
     protected File srcDirectory;
 
     /**
      * The directory where the generated catalogue file will be put.
      */
-    @Parameter( defaultValue = "${project.build.outputDirectory}" )
+    @Parameter(defaultValue = "${project.build.outputDirectory}")
     protected File dstDirectory;
 
     @Override
diff --git a/tools/package/maven-plugin/src/main/resources/org/onosproject/maven/features.xml b/tools/package/maven-plugin/src/main/resources/org/onosproject/maven/features.xml
new file mode 100644
index 0000000..15487d9
--- /dev/null
+++ b/tools/package/maven-plugin/src/main/resources/org/onosproject/maven/features.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  ~ 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.
+  -->
+<features xmlns="http://karaf.apache.org/xmlns/features/v1.2.0" name="${project.artifactId}-${feature.version}">
+    <repository>mvn:${project.groupId}/${project.artifactId}/${project.version}/xml/features</repository>
+    <feature name="${project.artifactId}" version="${feature.version}"
+             description="${project.description}">
+        <feature>onos-api</feature>
+        <bundle>mvn:${project.groupId}/${project.artifactId}/${project.version}</bundle>
+    </feature>
+</features>