--- /dev/null
+Feature Selector is a simple web application, that allows user to pick features from a list of available features.
+Then, add those selected features to the karaf feature config file "org.apache.karaf.features.cfg" as bootFeatures
+and return a link to download distribution that has new feature config file.
+
+config.properties file present under resources. Here are the details of config.properties -
+
+features=odl-restconf,odl-dlux-core,odl-mdsal-clustering : (Required) Comma separated list of features that you want user to select from.
+
+distribution-filename=distribution-dlux-0.1.0-SNAPSHOT.zip : (Required) File name of the distribution zip
+
+distribution-file-path=/opt/dist : (Optional) Location of the zip file, if you are not providing it within war file
+
+Release distribution zip file can be placed inside the war at location /webapp/download/original, Or
+it could be picked up from the file system path, specified by configuration property distribution-file-path.
+
+Note: Just make sure to provide either external location of distribution directory using property "distribution-file-path" or
+place zip file inside war at /webapp/download/original. One of these is required.
+
+
+
--- /dev/null
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.opendaylight.release</groupId>
+ <artifactId>feature-selector</artifactId>
+ <packaging>war</packaging>
+ <version>0.1.0-SNAPSHOT</version>
+ <name>feature-selector Maven Webapp</name>
+ <url>http://maven.apache.org</url>
+ <properties>
+ <spring.version>3.1.3.RELEASE</spring.version>
+ <jackson.version>1.9.10</jackson.version>
+ </properties>
+ <build>
+ <finalName>feature-selector</finalName>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>3.1</version>
+ <configuration>
+ <source>1.7</source>
+ <target>1.7</target>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+ <dependencies>
+ <!-- Spring 3 dependencies -->
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-core</artifactId>
+ <version>${spring.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-web</artifactId>
+ <version>${spring.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-webmvc</artifactId>
+ <version>${spring.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.codehaus.jackson</groupId>
+ <artifactId>jackson-mapper-asl</artifactId>
+ <version>${jackson.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>jstl</artifactId>
+ <version>1.1.2</version>
+ </dependency>
+
+ <dependency>
+ <groupId>taglibs</groupId>
+ <artifactId>standard</artifactId>
+ <version>1.1.2</version>
+ </dependency>
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>servlet-api</artifactId>
+ <version>2.5</version>
+ </dependency>
+ </dependencies>
+</project>
--- /dev/null
+
+/*
+ * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.release.app;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.ModelMap;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import javax.servlet.ServletContext;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.util.Properties;
+
+
+@Controller
+@RequestMapping("/")
+public class FeatureSelectorController {
+
+ @Autowired
+ ServletContext context;
+ private static Properties properties = readProperties();
+ private static String[] featureList = getFeatureList(properties);
+
+
+ /**
+ * Method to list all the features on the UI
+ *
+ * @param model
+ * @return
+ */
+ @RequestMapping(value="/features", method = RequestMethod.GET)
+ public String viewFeatures(ModelMap model){
+ model.addAttribute("features", featureList);
+ return "index";
+ }
+
+ /**
+ * This method receives a list of the features in a comma separated string.
+ * It creates a new distribution zip that has those features as bootFeature in
+ * karaf config file.
+ * @param features
+ * @return
+ * @throws IOException
+ */
+ @RequestMapping(value="/selectFeatures", method = RequestMethod.POST)
+ @ResponseBody
+ public String selectFeatures(@RequestParam(value="selectedFeatures", required=false) String features) throws IOException {
+ FileUtil fileUtil = FileUtil.getInstance(properties, context);
+
+ // Copy original distribution to unique location
+ Path destFile = fileUtil.copyOriginalDistro(context);
+
+ // Update the config file at destination file
+ fileUtil.updateConfigFileWithFeatures(features, destFile);
+
+ // Return path to download new distribution
+ StringBuilder builder = new StringBuilder();
+ builder.append(context.getContextPath()).append(FileUtil.DEST_DISTRO_PATH).append(destFile.getParent().getFileName())
+ .append("/").append(destFile.getFileName());
+
+ return builder.toString();
+ }
+
+ private static Properties readProperties() {
+ Properties prop = new Properties();
+ String filename = "config.properties";
+
+ try(InputStream input = FeatureSelectorController.class.getClassLoader().getResourceAsStream(filename)) {
+ //load a properties file from class path, inside static method
+ prop.load(input);
+
+ } catch (IOException ex) {
+ ex.printStackTrace();
+ }
+ return prop;
+ }
+
+ private static String[] getFeatureList(Properties prop) {
+ String[] features = null;
+ String feature = prop.getProperty("features");
+ if(feature != null) {
+ features = feature.split(",");
+ }
+ return features;
+ }
+
+
+
+}
--- /dev/null
+
+/*
+ * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.release.app;
+
+import javax.servlet.ServletContext;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.net.URI;
+import java.nio.charset.Charset;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+public class FileUtil {
+ private volatile static FileUtil instance;
+
+ // Path of the source distribution file
+ private Path sourcePath = null;
+
+ // content of karaf config file
+ private String featureConfigContent = null;
+
+ // config file location inside the zip file
+ private String configFileLocation = null;
+
+ public static String FEATURE_SELECTION_REPLACEMENT = "{CHANGE_ME}";
+
+ // location of all generated distributions inside tomcat
+ public static String DEST_DISTRO_PATH = "/download/generated/";
+
+ public static FileUtil getInstance(Properties properties, ServletContext context) throws IOException {
+ if (instance == null) {
+ synchronized (FileUtil.class) {
+ if (instance == null) {
+ instance = new FileUtil(properties, context);
+ }
+ }
+ }
+ return instance;
+ }
+
+ private FileUtil(Properties properties, ServletContext context) throws IOException {
+ initPath(properties, context);
+ loadFeatureConfigFile();
+ }
+
+
+ /**
+ * Based on config param, use provided source distribution or
+ * get it from outside tomcat.
+ *
+ * @param context
+ */
+ private void initPath(Properties properties, ServletContext context) {
+ // Check if external path of file is provided or not
+ String externalFilePath = properties.getProperty("distribution-file-path");
+ String distFileName = properties.getProperty("distribution-filename");
+ if(distFileName == null || distFileName.isEmpty()) {
+ throw new IllegalStateException("Distribution file name is missing in config file.");
+ }
+ if(externalFilePath != null && !externalFilePath.isEmpty()) {
+ sourcePath = Paths.get(externalFilePath + "/" + distFileName);
+ System.out.println("picking external file");
+ } else {
+ sourcePath = Paths.get(context.getRealPath("/download/original/" + distFileName));
+ }
+
+ configFileLocation = "/" + distFileName.substring(0, distFileName.lastIndexOf("."))
+ + "/etc/org.apache.karaf.features.cfg";
+
+ }
+
+ /**
+ *
+ * Load the feature config files's content in String and keep it in memory. Add a CHANGE_ME keyword,
+ * that will be used during each new distribution creation
+ *
+ */
+
+ private void loadFeatureConfigFile() throws IOException {
+ try (FileSystem sourceFileSystem = createZipFileSystem(sourcePath)) {
+ final Path featureConfig = sourceFileSystem.getPath(configFileLocation);
+ try (BufferedReader reader = Files.newBufferedReader(featureConfig, Charset.forName("UTF-8"))) {
+ StringBuilder fileContent = new StringBuilder();
+ for (; ; ) {
+ String line = reader.readLine();
+ if (line == null)
+ break;
+ if (line.contains("featuresBoot=")) {
+ line = line.concat(FEATURE_SELECTION_REPLACEMENT);
+ }
+ fileContent.append(line).append("\n");
+ }
+ featureConfigContent = fileContent.toString();
+ }
+
+ }
+ }
+
+ /**
+ * Creates a zipFile system based on the path of the zip file.
+ * It does not create a zip file, if it does not exists.
+ * @param path
+ * @return
+ * @throws IOException
+ */
+ public FileSystem createZipFileSystem(Path path) throws IOException {
+ final URI uri = URI.create("jar:file:" + path.toUri().getPath());
+
+ final Map<String, String> env = new HashMap<>();
+ env.put("create", "false");
+ return FileSystems.newFileSystem(uri, env);
+ }
+
+
+ /**
+ * Copy the Source distribution to a new location directory created
+ * based on the time stamp on every request
+ * @param context
+ * @return
+ * @throws IOException
+ */
+ public Path copyOriginalDistro(ServletContext context) throws IOException {
+ Date now = new Date();
+ String directoryName = String.valueOf(now.getTime());
+ Path destDir = Paths.get(context.getRealPath(DEST_DISTRO_PATH + directoryName ));
+ Files.createDirectories(destDir);
+ Path destFile = destDir.resolve(sourcePath.getFileName());
+ Files.copy(sourcePath, destFile);
+ return destFile;
+ }
+
+ /**
+ * Creates a new config file with user selected features
+ * and move it inside the new destination zip file
+ */
+ public void updateConfigFileWithFeatures(String features, Path destFile) throws IOException {
+ // Create new config file
+ String fileContent = featureConfigContent.replace(FEATURE_SELECTION_REPLACEMENT, features);
+ Path newConfFile = destFile.getParent().resolve("org.apache.karaf.features.cfg");
+ Files.write(newConfFile, fileContent.getBytes());
+
+ // replace older config file with new one
+ try(FileSystem destFileSystem = createZipFileSystem(destFile)){
+ final Path oldConfFile = destFileSystem.getPath(configFileLocation);
+ Files.move(newConfFile, oldConfFile, StandardCopyOption.REPLACE_EXISTING);
+ }
+ }
+}
--- /dev/null
+features=odl-restconf,odl-l2switch-switch,odl-dlux-core,odl-mdsal-clustering
+distribution-filename=distribution-dlux-0.1.0-SNAPSHOT.zip
+distribution-file-path=
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:mvc="http://www.springframework.org/schema/mvc"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
+ http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
+ http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
+
+ <context:component-scan base-package="org.opendaylight.release.app" />
+
+ <mvc:resources mapping="/js/**" location="/js/" />
+ <mvc:resources mapping="/css/**" location="/css/" />
+ <mvc:resources mapping="/img/**" location="/img/" />
+ <mvc:resources mapping="/download/**" location="/download/" />
+
+ <mvc:annotation-driven />
+
+ <bean
+ class="org.springframework.web.servlet.view.InternalResourceViewResolver">
+ <property name="prefix">
+ <value>/pages/</value>
+ </property>
+ <property name="suffix">
+ <value>.jsp</value>
+ </property>
+ </bean>
+
+</beans>
\ No newline at end of file
--- /dev/null
+<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
+ version="3.0">
+ <display-name>Feature Selector Application</display-name>
+
+ <servlet>
+ <servlet-name>feature-selector</servlet-name>
+ <servlet-class>
+ org.springframework.web.servlet.DispatcherServlet
+ </servlet-class>
+ <load-on-startup>1</load-on-startup>
+ </servlet>
+
+ <servlet-mapping>
+ <servlet-name>feature-selector</servlet-name>
+ <url-pattern>/</url-pattern>
+ </servlet-mapping>
+
+ <security-constraint>
+ <web-resource-collection>
+ <web-resource-name>All Access</web-resource-name>
+ <url-pattern>/download/*</url-pattern>
+ <http-method>GET</http-method>
+ </web-resource-collection>
+ </security-constraint>
+</web-app>
--- /dev/null
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Feature Selector</title>
+
+ <!-- Latest compiled and minified CSS -->
+<link rel="stylesheet" href="http://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
+
+<!-- Optional theme -->
+<link rel="stylesheet" href="http://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap-theme.min.css">
+<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
+ <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
+ <!-- Latest compiled and minified JavaScript -->
+ <script src="http://netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
+ </head>
+ <body>
+ <div class="container">
+
+ <div class="content">
+ <h5> Please select features that you want to be enabled at startup:</h5>
+ <form id="selectFeatures" role="form">
+ <c:forEach var="item" items="${features}">
+ <div class="form-group"><input type="checkbox" value="${item}"> ${item}</input></div>
+ </c:forEach>
+
+ <div class="form-group">
+ <button type="submit" class="btn btn-primary">Submit</button>
+ <a href="#" id="download_dist" class="btn btn-default hidden">Download</a>
+ </div>
+ </form>
+ </div>
+
+
+
+ </div>
+
+
+<script type="text/javascript">
+
+
+jQuery("form").on( "submit", function( event ) {
+ event.preventDefault();
+ jQuery("#download_dist").addClass("hidden");
+ var count = jQuery( "input:checked" ).length;
+ if(count < 1) {
+ alert("Please select a feature");
+ return;
+ }
+ var features = "";
+ for(i=0; i < count; i++) {
+ features = features + "," + jQuery( "input:checked" )[i].value;
+ }
+ var formData = '{"selectedFeatures":"' + features + '"}';
+
+ jQuery.ajax({
+ url: "/feature-selector/selectFeatures",
+ type: "POST",
+ data: "selectedFeatures=" + features,
+ success: function(data) {
+ jQuery("#download_dist").attr("href", data).removeClass("hidden");
+ console.log("success");
+ },
+ error : function(){ console.log("Error");}
+ });
+
+ });
+</script>
+
+ </body>
+</html>
\ No newline at end of file