FELIX-1485: Adding AdminService support to the web console
git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@804868 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/karaf/assembly/src/main/filtered-resources/features.xml b/karaf/assembly/src/main/filtered-resources/features.xml
index f18ba6a..64d20c5 100644
--- a/karaf/assembly/src/main/filtered-resources/features.xml
+++ b/karaf/assembly/src/main/filtered-resources/features.xml
@@ -60,6 +60,7 @@
<!-- TODO: uncomment when FELIX-1133 is resolved
<bundle>mvn:org.apache.felix.karaf.webconsole/org.apache.felix.karaf.webconsole.branding/${version}</bundle>
-->
+ <bundle>mvn:org.apache.felix.karaf.webconsole/org.apache.felix.karaf.webconsole.admin/${version}</bundle>
<bundle>mvn:org.apache.felix.karaf.webconsole/org.apache.felix.karaf.webconsole.features/${version}</bundle>
<bundle>mvn:org.apache.felix.karaf.webconsole/org.apache.felix.karaf.webconsole.gogo/${version}</bundle>
</feature>
diff --git a/karaf/gshell/gshell-admin/src/main/resources/OSGI-INF/blueprint/gshell-admin.xml b/karaf/gshell/gshell-admin/src/main/resources/OSGI-INF/blueprint/gshell-admin.xml
index 74cad8b..d92260a 100644
--- a/karaf/gshell/gshell-admin/src/main/resources/OSGI-INF/blueprint/gshell-admin.xml
+++ b/karaf/gshell/gshell-admin/src/main/resources/OSGI-INF/blueprint/gshell-admin.xml
@@ -87,5 +87,7 @@
<bean id="instanceCompleter" class="org.apache.felix.karaf.gshell.admin.internal.completers.InstanceCompleter">
<property name="adminService" ref="adminService" />
</bean>
+
+ <service ref="adminService" interface="org.apache.felix.karaf.gshell.admin.AdminService" />
</blueprint>
diff --git a/karaf/webconsole/admin/pom.xml b/karaf/webconsole/admin/pom.xml
new file mode 100644
index 0000000..a47f66c
--- /dev/null
+++ b/karaf/webconsole/admin/pom.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<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">
+
+ <!--
+
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You 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.
+ -->
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.felix.karaf.webconsole</groupId>
+ <artifactId>webconsole</artifactId>
+ <version>1.2.0-SNAPSHOT</version>
+ </parent>
+
+ <groupId>org.apache.felix.karaf.webconsole</groupId>
+ <artifactId>org.apache.felix.karaf.webconsole.admin</artifactId>
+ <packaging>bundle</packaging>
+ <version>1.2.0-SNAPSHOT</version>
+ <name>Apache Felix Karaf :: Web Console :: Admin Plugin</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.osgi.core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.osgi.compendium</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.apache.felix.webconsole</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>servlet-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.felix.karaf.gshell</groupId>
+ <artifactId>org.apache.felix.karaf.gshell.admin</artifactId>
+ <version>1.2.0-SNAPSHOT</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>commons-logging</groupId>
+ <artifactId>commons-logging</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.json</groupId>
+ <artifactId>json</artifactId>
+ <version>20070829</version>
+ <scope>compile</scope>
+ <optional>true</optional>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <configuration>
+ <instructions>
+ <Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
+ <Export-Package>org.apache.felix.karaf.webconsole.admin;version=${pom.version}</Export-Package>
+ <Embed-Dependency>
+ <!-- Required for JSON data transfer -->
+ <!-- TODO: this needs to be put in a common place for reuse. -->
+ json
+ </Embed-Dependency>
+ </instructions>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git a/karaf/webconsole/admin/src/main/java/org/apache/felix/karaf/webconsole/admin/AdminPlugin.java b/karaf/webconsole/admin/src/main/java/org/apache/felix/karaf/webconsole/admin/AdminPlugin.java
new file mode 100644
index 0000000..f3d62ac
--- /dev/null
+++ b/karaf/webconsole/admin/src/main/java/org/apache/felix/karaf/webconsole/admin/AdminPlugin.java
@@ -0,0 +1,334 @@
+/*
+ * Copyright 2009 Marcin.
+ *
+ * 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.
+ * under the License.
+ */
+package org.apache.felix.karaf.webconsole.admin;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.net.URL;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.felix.karaf.gshell.admin.AdminService;
+import org.apache.felix.karaf.gshell.admin.Instance;
+import org.apache.felix.webconsole.AbstractWebConsolePlugin;
+import org.json.JSONException;
+import org.osgi.framework.BundleContext;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.json.JSONWriter;
+
+/**
+ * Felix Web Console plugin for interacting with the {@link AdminService}
+ */
+public class AdminPlugin extends AbstractWebConsolePlugin {
+
+ public static final String NAME = "admin";
+ public static final String LABEL = "Admin";
+ private String adminJs = "/admin/res/ui/admin.js";
+ private BundleContext bundleContext;
+ private AdminService adminService;
+ private Log log = LogFactory.getLog(AdminPlugin.class);
+ private ClassLoader classLoader;
+
+ /**
+ * Blueprint lifecycle callback methods
+ */
+ public void start() {
+ super.activate(bundleContext);
+ this.classLoader = this.getClass().getClassLoader();
+ this.log.info(LABEL + " plugin activated");
+ }
+
+ public void stop() {
+ this.log.info(LABEL + " plugin deactivated");
+ super.deactivate();
+ }
+
+ @Override
+ public String getTitle() {
+ return LABEL;
+ }
+
+ @Override
+ public String getLabel() {
+ return NAME;
+ }
+
+ @Override
+ protected void renderContent(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
+ final PrintWriter pw = res.getWriter();
+
+ String appRoot = (String) req.getAttribute("org.apache.felix.webconsole.internal.servlet.OsgiManager.appRoot");
+ final String adminScriptTag = "<script src='" + appRoot + this.adminJs + "' language='JavaScript'></script>";
+ pw.println(adminScriptTag);
+
+ pw.println("<script type='text/javascript'>");
+ pw.println("// <![CDATA[");
+ pw.println("var imgRoot = '" + appRoot + "/res/imgs';");
+ pw.println("// ]]>");
+ pw.println("</script>");
+
+ pw.println("<div id='plugin_content'/>");
+
+ pw.println("<script type='text/javascript'>");
+ pw.println("// <![CDATA[");
+ pw.print("renderAdmin( ");
+ writeJSON(pw);
+ pw.println(" )");
+ pw.println("// ]]>");
+ pw.println("</script>");
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
+ boolean success = false;
+
+ String action = req.getParameter("action");
+ String name = req.getParameter("name");
+
+ if (action == null) {
+ success = true;
+ } else if ("create".equals(action)) {
+ int port = parsePortNumber(req.getParameter("port"));
+ String location = parseString(req.getParameter("location"));
+ success = createInstance(name, port, location);
+ } else if ("destroy".equals(action)) {
+ success = destroyInstance(name);
+ } else if ("start".equals(action)) {
+ String javaOpts = req.getParameter("javaOpts");
+ success = startInstance(name, javaOpts);
+ } else if ("stop".equals(action)) {
+ success = stopInstance(name);
+ }
+
+ if (success) {
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ }
+ this.renderJSON(res, null);
+ } else {
+ super.doPost(req, res);
+ }
+ }
+
+ /*
+ * Parse the String value, returning <code>null</code> if the String is empty
+ */
+ private String parseString(String value) {
+ if (value != null && value.trim().length() == 0) {
+ value = null;
+ }
+ return value;
+ }
+
+ /*
+ * Parse the port number for the String given, returning 0 if the String does not represent an integer
+ */
+ private int parsePortNumber(String port) {
+ try {
+ return Integer.parseInt(port);
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+
+ protected URL getResource(String path) {
+ path = path.substring(NAME.length() + 1);
+ URL url = this.classLoader.getResource(path);
+ try {
+ InputStream ins = url.openStream();
+ if (ins == null) {
+ this.log.error("failed to open " + url);
+ }
+ } catch (IOException e) {
+ this.log.error(e.getMessage(), e);
+ }
+ return url;
+ }
+
+ private void renderJSON(final HttpServletResponse response, final String feature) throws IOException {
+ response.setContentType("application/json");
+ response.setCharacterEncoding("UTF-8");
+
+ final PrintWriter pw = response.getWriter();
+ writeJSON(pw);
+ }
+
+ private void writeJSON(final PrintWriter pw) {
+ final JSONWriter jw = new JSONWriter(pw);
+ final Instance[] instances = adminService.getInstances();
+ try {
+ jw.object();
+ jw.key("status");
+ jw.value(getStatusLine());
+ jw.key("instances");
+ jw.array();
+ for (Instance i : instances) {
+ instanceInfo(jw, i);
+ }
+ jw.endArray();
+ jw.endObject();
+ } catch (JSONException ex) {
+ Logger.getLogger(AdminPlugin.class.getName()).log(Level.SEVERE, null, ex);
+ } catch (Exception ex) {
+ Logger.getLogger(AdminPlugin.class.getName()).log(Level.SEVERE, null, ex);
+ }
+ }
+
+ private void instanceInfo(JSONWriter jw, Instance instance) throws JSONException, Exception {
+ jw.object();
+ jw.key("pid");
+ jw.value(instance.getPid());
+ jw.key("name");
+ jw.value(instance.getName());
+ jw.key("port");
+ jw.value(instance.getPort());
+ jw.key("state");
+ jw.value(instance.getState());
+ jw.key("location");
+ jw.value(instance.getLocation());
+ jw.key("actions");
+ jw.array();
+ action(jw, "destroy", "Destroy", "delete");
+ if (instance.getState().equals(Instance.STARTED)) {
+ action(jw, "stop", "Stop", "stop");
+ } else if (instance.getState().equals(Instance.STARTING)) {
+ action(jw, "stop", "Stop", "stop");
+ } else if (instance.getState().equals(Instance.STOPPED)) {
+ action(jw, "start", "Start", "start");
+ }
+ jw.endArray();
+ jw.endObject();
+ }
+
+ private void action(JSONWriter jw, String op, String title, String image) throws JSONException {
+ jw.object();
+ jw.key("op").value(op);
+ jw.key("title").value(title);
+ jw.key("image").value(image);
+ jw.endObject();
+ }
+
+ private String getStatusLine() {
+ final Instance[] instances = adminService.getInstances();
+ int started = 0, starting = 0, stopped = 0;
+ for (Instance instance : instances) {
+ try {
+ if (instance.getState().equals(Instance.STARTED)) {
+ started++;
+ } else if (instance.getState().equals(Instance.STARTING)) {
+ starting++;
+ } else if (instance.getState().equals(Instance.STOPPED)) {
+ stopped++;
+ }
+ } catch (Exception ex) {
+ Logger.getLogger(AdminPlugin.class.getName()).log(Level.SEVERE, null, ex);
+ }
+ }
+ final StringBuffer buffer = new StringBuffer();
+ buffer.append("Instance information: ");
+ buffer.append(instances.length);
+ buffer.append(" instance");
+ if (instances.length != 1) {
+ buffer.append('s');
+ }
+ buffer.append(" in total");
+ if (started == instances.length) {
+ buffer.append(" - all started");
+ } else {
+ if (started != 0) {
+ buffer.append(", ");
+ buffer.append(started);
+ buffer.append(" started");
+ }
+ if (starting != 0) {
+ buffer.append(", ");
+ buffer.append(starting);
+ buffer.append(" starting");
+ }
+ buffer.append('.');
+ }
+ return buffer.toString();
+ }
+
+ private boolean createInstance(String name, int port, String location) {
+ try {
+ adminService.createInstance(name, port, location);
+ return true;
+ } catch (Exception ex) {
+ Logger.getLogger(AdminPlugin.class.getName()).log(Level.SEVERE, null, ex);
+ }
+ return false;
+ }
+
+ private boolean destroyInstance(String name) {
+ try {
+ Instance instance = adminService.getInstance(name);
+ if (instance != null) {
+ instance.destroy();
+ return true;
+ }
+ } catch (Exception ex) {
+ Logger.getLogger(AdminPlugin.class.getName()).log(Level.SEVERE, null, ex);
+ }
+ return false;
+ }
+
+ private boolean startInstance(String name, String javaOpts) {
+ try {
+ Instance instance = adminService.getInstance(name);
+ if (instance != null) {
+ instance.start(javaOpts);
+ return true;
+ }
+ } catch (Exception ex) {
+ Logger.getLogger(AdminPlugin.class.getName()).log(Level.SEVERE, null, ex);
+ }
+ return false;
+ }
+
+ private boolean stopInstance(String name) {
+ try {
+ Instance instance = adminService.getInstance(name);
+ if (instance != null) {
+ instance.stop();
+ return true;
+ }
+ } catch (Exception ex) {
+ Logger.getLogger(AdminPlugin.class.getName()).log(Level.SEVERE, null, ex);
+ }
+ return false;
+ }
+
+ /**
+ * @param adminService the adminService to set
+ */
+ public void setAdminService(AdminService adminService) {
+ this.adminService = adminService;
+ }
+
+ /**
+ * @param bundleContext the bundleContext to set
+ */
+ public void setBundleContext(BundleContext bundleContext) {
+ this.bundleContext = bundleContext;
+ }
+}
diff --git a/karaf/webconsole/admin/src/main/resources/OSGI-INF/blueprint/webconsole-admin.xml b/karaf/webconsole/admin/src/main/resources/OSGI-INF/blueprint/webconsole-admin.xml
new file mode 100644
index 0000000..9d65b92
--- /dev/null
+++ b/karaf/webconsole/admin/src/main/resources/OSGI-INF/blueprint/webconsole-admin.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You 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.
+
+-->
+<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
+ xmlns:cm="http://www.osgi.org/xmlns/blueprint-cm/v1.0.0">
+
+ <reference id="adminService" interface="org.apache.felix.karaf.gshell.admin.AdminService" />
+
+ <bean id="adminPlugin" class="org.apache.felix.karaf.webconsole.admin.AdminPlugin" init-method="start" destroy-method="stop">
+ <property name="adminService" ref="adminService" />
+ <property name="bundleContext" ref="blueprintBundleContext" />
+ </bean>
+
+ <service ref="adminPlugin" interface="javax.servlet.Servlet" >
+ <service-properties>
+ <entry key="felix.webconsole.label" value="admin"/>
+ </service-properties>
+ </service>
+
+</blueprint>
diff --git a/karaf/webconsole/admin/src/main/resources/res/ui/admin.js b/karaf/webconsole/admin/src/main/resources/res/ui/admin.js
new file mode 100644
index 0000000..caf62e0
--- /dev/null
+++ b/karaf/webconsole/admin/src/main/resources/res/ui/admin.js
@@ -0,0 +1,142 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+function renderAdmin( data ) {
+ $(document).ready( function() {
+ renderView();
+ renderData( data );
+ } );
+}
+
+function renderView() {
+ renderStatusLine();
+ var txt = "<form method='post'><div class='table'><table id='create_instance_table' class='tablelayout'><tbody>" +
+ "<tr><input type='hidden' name='action' value='create'/>" +
+ "<td>Name: <input id='name' type='text' name='name' style='width:70%' colspan='2'/></td>" +
+ "<td>Port: <input id='port' type='text' name='port' style='width:70%' colspan='2'/></td>" +
+ "<td>Location: <input id='location' type='text' name='location' style='width:70%' colspan='2'/></td>" +
+ "<td class='col_Actions'><input type='button' value='Create' onclick='createInstance()'/></td>" +
+ "</tr></tbody></table></div></form><br/>";
+ $("#plugin_content").append( txt );
+ renderTable( "Karaf Instances", "instances_table", ["Pid", "Name", "Port", "State", "Location", "Actions"] );
+ renderStatusLine();
+}
+
+function createInstance() {
+ var name = document.getElementById( "name" ).value;
+ var port = document.getElementById( "port" ).value;
+ var location = document.getElementById( "location" ).value;
+ postCreateInstance( name, port, location );
+}
+
+function postCreateInstance( /* String */ name, /* String */ port, /* String */ location ) {
+ $.post( pluginRoot, {"action": "create", "name": name, "port": port, "location": location}, function( data ) {
+ renderData( data );
+ }, "json" );
+}
+
+function renderStatusLine() {
+ $("#plugin_content").append( "<div class='fullwidth'><div class='statusline'/></div>" );
+}
+
+function renderTable( /* String */ title, /* String */ id, /* array of Strings */ columns ) {
+ var txt = "<div class='table'><table class='tablelayout'><tbody><tr>" +
+ "<td style='color:#6181A9;background-color:#e6eeee'>" +
+ title + "</td></tr></tbody></table>" +
+ "<table id='" + id + "' class='tablelayout'><thead><tr>";
+ for ( var name in columns ) {
+ txt = txt + "<th class='col_" + columns[name] + "' style='border-top:#e6eeee'>" + columns[name] + "</th>";
+ }
+ txt = txt + "</tr></thead><tbody></tbody></table></div>";
+ $("#plugin_content").append( txt );
+}
+
+function renderData( /* Object */ data ) {
+ renderStatusData( data.status );
+ renderInstancesTableData( data.instances );
+ $("#instances_table").tablesorter( {
+ headers: {
+ 5: {
+ sorter: false
+ }
+ },
+ sortList: [[0,0]],
+ } );
+}
+
+function renderStatusData( /* String */ status ) {
+ $(".statusline").empty().append( status );
+}
+
+function renderInstancesTableData( /* array of Objects */ instances ) {
+ $("#instances_table > tbody > tr").remove();
+ for ( var idx in instances ) {
+ var trElement = tr( null, {
+ id: instances[idx].pid
+ } );
+ renderInstanceData( trElement, instances[idx] );
+ $("#instances_table > tbody").append( trElement );
+ }
+ $("#instances_table").trigger( "update" );
+}
+
+function renderInstanceData( /* Element */ parent, /* Object */ instance ) {
+ parent.appendChild( td( null, null, [ text( instance.pid ) ] ) );
+ parent.appendChild( td( null, null, [ text( instance.name ) ] ) );
+ parent.appendChild( td( null, null, [ text( instance.port ) ] ) );
+ parent.appendChild( td( null, null, [ text( instance.state ) ] ) );
+ parent.appendChild( td( null, null, [ text( instance.location ) ] ) );
+ var actionsTd = td( null, null );
+ var div = createElement( "div", null, {
+ style: {
+ "text-align": "left"
+ }
+ } );
+ actionsTd.appendChild( div );
+
+ for ( var a in instance.actions ) {
+ instanceButton( div, instance.name, instance.actions[a] );
+ }
+ parent.appendChild( actionsTd );
+}
+
+function instanceButton( /* Element */ parent, /* String */ name, /* Obj */ action ) {
+ var input = createElement( "input", null, {
+ type: 'image',
+ style: {
+ "margin-left": "10px"
+ },
+ title: action.title,
+ alt: action.title,
+ src: imgRoot + '/bundle_' + action.image + '.png'
+ } );
+ $(input).click( function() {
+ changeInstanceState( action.op, name )
+ } );
+ parent.appendChild( input );
+}
+
+function changeInstanceState( /* String */ action, /* String */ name) {
+ $.post( pluginRoot, {
+ "action": action,
+ "name": name
+ }, function( data ) {
+ renderData( data );
+ }, "json" );
+}
+
+
diff --git a/karaf/webconsole/pom.xml b/karaf/webconsole/pom.xml
index 24c116f..69bc87c 100644
--- a/karaf/webconsole/pom.xml
+++ b/karaf/webconsole/pom.xml
@@ -38,6 +38,7 @@
<module>features</module>
<module>gogo</module>
<module>branding</module>
+ <module>admin</module>
</modules>
</project>