Repository: karaf-cellar
Updated Branches:
  refs/heads/master 36efc8e27 -> 77b8daec7


[KARAF-2488] Provide Cellar HTTP Balancer feature


Project: http://git-wip-us.apache.org/repos/asf/karaf-cellar/repo
Commit: http://git-wip-us.apache.org/repos/asf/karaf-cellar/commit/77b8daec
Tree: http://git-wip-us.apache.org/repos/asf/karaf-cellar/tree/77b8daec
Diff: http://git-wip-us.apache.org/repos/asf/karaf-cellar/diff/77b8daec

Branch: refs/heads/master
Commit: 77b8daec706c5fe3d8b09b459d09ab66faa04ace
Parents: 36efc8e
Author: Jean-Baptiste Onofré <[email protected]>
Authored: Thu Sep 17 21:50:06 2015 +0200
Committer: Jean-Baptiste Onofré <[email protected]>
Committed: Thu Sep 17 21:50:57 2015 +0200

----------------------------------------------------------------------
 assembly/src/main/resources/features.xml        |   7 +
 assembly/src/main/resources/groups.cfg          |   1 +
 assembly/src/main/resources/node.cfg            |   2 +
 http/balancer/pom.xml                           | 119 ++++
 .../http/balancer/BalancedServletUtil.java      |  59 ++
 .../http/balancer/BalancerEventHandler.java     | 139 +++++
 .../balancer/CellarBalancerProxyServlet.java    | 552 +++++++++++++++++++
 .../http/balancer/ClusterBalancerEvent.java     |  60 ++
 .../karaf/cellar/http/balancer/Constants.java   |  21 +
 .../http/balancer/LocalServletListener.java     | 129 +++++
 .../http/balancer/ProxyServletRegistry.java     |  51 ++
 .../http/balancer/ServletSynchronizer.java      | 206 +++++++
 .../balancer/command/ListClusterServlets.java   |  67 +++
 .../http/balancer/internal/osgi/Activator.java  | 121 ++++
 http/pom.xml                                    |  41 ++
 manual/src/main/webapp/manual.conf              |   2 +
 .../main/webapp/user-guide/http-balancer.conf   | 131 +++++
 manual/src/main/webapp/user-guide/index.conf    |   1 +
 pom.xml                                         |   1 +
 19 files changed, 1710 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/karaf-cellar/blob/77b8daec/assembly/src/main/resources/features.xml
----------------------------------------------------------------------
diff --git a/assembly/src/main/resources/features.xml 
b/assembly/src/main/resources/features.xml
index b63eff1..692c1e9 100644
--- a/assembly/src/main/resources/features.xml
+++ b/assembly/src/main/resources/features.xml
@@ -109,5 +109,12 @@
         
<bundle>mvn:org.apache.karaf.cellar/org.apache.karaf.cellar.webconsole/${project.version}</bundle>
     </feature>
 
+    <feature name="cellar-http-balancer" description="Cellar HTTP request 
balancer" version="${project.version}">
+        <feature>cellar-hazelcast</feature>
+        <feature>http</feature>
+        <feature>http-whiteboard</feature>
+        
<bundle>mvn:org.apache.karaf.cellar.http/org.apache.karaf.cellar.http.balancer/${project.version}</bundle>
+    </feature>
+
 </features>
 

http://git-wip-us.apache.org/repos/asf/karaf-cellar/blob/77b8daec/assembly/src/main/resources/groups.cfg
----------------------------------------------------------------------
diff --git a/assembly/src/main/resources/groups.cfg 
b/assembly/src/main/resources/groups.cfg
index a476ef0..7dac669 100644
--- a/assembly/src/main/resources/groups.cfg
+++ b/assembly/src/main/resources/groups.cfg
@@ -53,3 +53,4 @@ default.bundle.sync = cluster
 default.config.sync = cluster
 default.feature.sync = cluster
 default.obr.urls.sync = cluster
+default.balanced.servlet.sync = cluster

http://git-wip-us.apache.org/repos/asf/karaf-cellar/blob/77b8daec/assembly/src/main/resources/node.cfg
----------------------------------------------------------------------
diff --git a/assembly/src/main/resources/node.cfg 
b/assembly/src/main/resources/node.cfg
index 4e389ec..38c841e 100644
--- a/assembly/src/main/resources/node.cfg
+++ b/assembly/src/main/resources/node.cfg
@@ -37,6 +37,8 @@ handler.org.apache.karaf.cellar.event.ClusterEventHandler = 
true
 # OBR event handler
 handler.org.apache.karaf.cellar.obr.ObrBundleEventHandler = true
 handler.org.apache.karaf.cellar.obr.ObrUrlEventHandler = true
+# HTTP balancer event handler
+handler.org.apache.karaf.cellar.http.balancer.BalancerEventHandler = true
 
 #
 # Excluded config properties from the sync

http://git-wip-us.apache.org/repos/asf/karaf-cellar/blob/77b8daec/http/balancer/pom.xml
----------------------------------------------------------------------
diff --git a/http/balancer/pom.xml b/http/balancer/pom.xml
new file mode 100644
index 0000000..b426b1a
--- /dev/null
+++ b/http/balancer/pom.xml
@@ -0,0 +1,119 @@
+<?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/xsd/maven-4.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.karaf.cellar</groupId>
+        <artifactId>http</artifactId>
+        <version>4.0.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <groupId>org.apache.karaf.cellar.http</groupId>
+    <artifactId>org.apache.karaf.cellar.http.balancer</artifactId>
+    <packaging>bundle</packaging>
+    <name>Apache Karaf :: Cellar :: HTTP :: Balancer</name>
+
+    <properties>
+        <pax.web.version>4.2.0</pax.web.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.ops4j.pax.web</groupId>
+            <artifactId>pax-web-spi</artifactId>
+            <version>${pax.web.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
+            <version>3.1.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient</artifactId>
+            <version>4.5</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.karaf.cellar</groupId>
+            <artifactId>org.apache.karaf.cellar.core</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.karaf.shell</groupId>
+            <artifactId>org.apache.karaf.shell.core</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.karaf</groupId>
+            <artifactId>org.apache.karaf.util</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.core</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.compendium</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.karaf.tooling</groupId>
+                <artifactId>karaf-services-maven-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <configuration>
+                    <instructions>
+                        <Export-Package>
+                            
!org.apache.karaf.cellar.http.balancer.internal.osgi,
+                            org.apache.karaf.cellar.http.balancer*
+                        </Export-Package>
+                        <Import-Package>
+                            org.slf4j;version="[1.6,2)";resolution:=optional,
+                            *
+                        </Import-Package>
+                        <Private-Package>
+                            
org.apache.karaf.cellar.http.balancer.internal.osgi,
+                            org.apache.http*,
+                            org.apache.commons.codec*
+                        </Private-Package>
+                    </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/karaf-cellar/blob/77b8daec/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/BalancedServletUtil.java
----------------------------------------------------------------------
diff --git 
a/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/BalancedServletUtil.java
 
b/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/BalancedServletUtil.java
new file mode 100644
index 0000000..a79a618
--- /dev/null
+++ 
b/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/BalancedServletUtil.java
@@ -0,0 +1,59 @@
+/*
+ * 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.apache.karaf.cellar.http.balancer;
+
+import org.apache.karaf.cellar.core.ClusterManager;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Dictionary;
+
+public class BalancedServletUtil {
+
+    private final static Logger LOGGER = 
LoggerFactory.getLogger(BalancedServletUtil.class);
+
+    private ClusterManager clusterManager;
+    private ConfigurationAdmin configurationAdmin;
+
+    public String constructLocation(String alias) {
+        String httpHost = clusterManager.getNode().getHost();
+        String httpPort = null;
+        try {
+            Configuration configuration = 
configurationAdmin.getConfiguration("org.ops4j.pax.web", null);
+            if (configuration != null) {
+                Dictionary properties = configuration.getProperties();
+                if (properties != null) {
+                    httpPort = (String) 
properties.get("org.osgi.service.http.port");
+                }
+            }
+        } catch (Exception e) {
+            LOGGER.warn("CELLAR HTTP BALANCER: can't get HTTP port number from 
configuration", e);
+        }
+        if (httpPort == null)
+            httpPort = "8181";
+        String location = "http://"; + httpHost + ":" + httpPort + alias;
+        return location;
+    }
+
+    public void setClusterManager(ClusterManager clusterManager) {
+        this.clusterManager = clusterManager;
+    }
+
+    public void setConfigurationAdmin(ConfigurationAdmin configurationAdmin) {
+        this.configurationAdmin = configurationAdmin;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf-cellar/blob/77b8daec/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/BalancerEventHandler.java
----------------------------------------------------------------------
diff --git 
a/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/BalancerEventHandler.java
 
b/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/BalancerEventHandler.java
new file mode 100644
index 0000000..32ca80e
--- /dev/null
+++ 
b/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/BalancerEventHandler.java
@@ -0,0 +1,139 @@
+/*
+ * 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.apache.karaf.cellar.http.balancer;
+
+import org.apache.karaf.cellar.core.ClusterManager;
+import org.apache.karaf.cellar.core.Configurations;
+import org.apache.karaf.cellar.core.GroupManager;
+import org.apache.karaf.cellar.core.control.BasicSwitch;
+import org.apache.karaf.cellar.core.control.Switch;
+import org.apache.karaf.cellar.core.control.SwitchStatus;
+import org.apache.karaf.cellar.core.event.EventHandler;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.Servlet;
+import java.util.Hashtable;
+
+public class BalancerEventHandler implements 
EventHandler<ClusterBalancerEvent> {
+
+    private static final transient Logger LOGGER = 
LoggerFactory.getLogger(BalancerEventHandler.class);
+
+    public static final String SWITCH_ID = 
"org.apache.karaf.cellar.event.http.balancer.handler";
+
+    private final Switch eventSwitch = new BasicSwitch(SWITCH_ID);
+
+    private ClusterManager clusterManager;
+    private GroupManager groupManager;
+    private ConfigurationAdmin configurationAdmin;
+    private BundleContext bundleContext;
+    private ProxyServletRegistry proxyRegistry;
+
+    @Override
+    public void handle(ClusterBalancerEvent event) {
+        if (this.getSwitch().getStatus().equals(SwitchStatus.OFF)) {
+            LOGGER.debug("CELLAR HTTP BALANCER: {} switch is OFF, cluster 
event is not handled", SWITCH_ID);
+            return;
+        }
+
+        if (groupManager == null) {
+            //in rare cases for example right after installation this happens!
+            LOGGER.error("CELLAR HTTP BALANCER: retrieved event {} while 
groupManager is not available yet!", event);
+            return;
+        }
+
+        // check if the group is local
+        if (!groupManager.isLocalGroup(event.getSourceGroup().getName())) {
+            LOGGER.debug("CELLAR HTTP BALANCER: node is not part of the event 
cluster group {}", event.getSourceGroup().getName());
+            return;
+        }
+
+        // check if it's not a "local" event
+        if (event.getSourceNode() != null && 
event.getSourceNode().getId().equalsIgnoreCase(clusterManager.getNode().getId()))
 {
+            LOGGER.trace("CELLAR HTTP BALANCER: cluster event is local (coming 
from local synchronizer or listener)");
+            return;
+        }
+
+        String alias = event.getAlias();
+        if (event.getType() == ClusterBalancerEvent.ADDING) {
+            LOGGER.debug("CELLAR HTTP BALANCER: creating proxy servlet for 
{}", alias);
+            CellarBalancerProxyServlet cellarBalancerProxyServlet = new 
CellarBalancerProxyServlet();
+            cellarBalancerProxyServlet.setLocations(event.getLocations());
+            try {
+                cellarBalancerProxyServlet.init();
+                Hashtable<String, String> properties = new Hashtable<String, 
String>();
+                properties.put("alias", alias);
+                properties.put("cellar.http.balancer.proxy", "true");
+                ServiceRegistration registration = 
bundleContext.registerService(Servlet.class, cellarBalancerProxyServlet, 
properties);
+                proxyRegistry.register(alias, registration);
+            } catch (Exception e) {
+                LOGGER.error("CELLAR HTTP BALANCER: can't start proxy 
servlet", e);
+            }
+        } else if (event.getType() == ClusterBalancerEvent.REMOVING) {
+            if (proxyRegistry.contain(alias)) {
+                LOGGER.debug("CELLAR HTTP BALANCER: removing proxy servlet for 
{}", alias);
+                proxyRegistry.unregister(alias);
+            }
+        }
+    }
+
+    @Override
+    public Class<ClusterBalancerEvent> getType() {
+        return ClusterBalancerEvent.class;
+    }
+
+    @Override
+    public Switch getSwitch() {
+        // load the switch status from the config
+        try {
+            Configuration configuration = 
configurationAdmin.getConfiguration(Configurations.NODE, null);
+            if (configuration != null) {
+                Boolean status = new Boolean((String) 
configuration.getProperties().get(Configurations.HANDLER + "." + 
this.getClass().getName()));
+                if (status) {
+                    eventSwitch.turnOn();
+                } else {
+                    eventSwitch.turnOff();
+                }
+            }
+        } catch (Exception e) {
+            // ignore
+        }
+        return eventSwitch;
+    }
+
+    public void setConfigurationAdmin(ConfigurationAdmin configurationAdmin) {
+        this.configurationAdmin = configurationAdmin;
+    }
+
+    public void setClusterManager(ClusterManager clusterManager) {
+        this.clusterManager = clusterManager;
+    }
+
+    public void setGroupManager(GroupManager groupManager) {
+        this.groupManager = groupManager;
+    }
+
+    public void setBundleContext(BundleContext bundleContext) {
+        this.bundleContext = bundleContext;
+    }
+
+    public void setProxyRegistry(ProxyServletRegistry proxyRegistry) {
+        this.proxyRegistry = proxyRegistry;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf-cellar/blob/77b8daec/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/CellarBalancerProxyServlet.java
----------------------------------------------------------------------
diff --git 
a/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/CellarBalancerProxyServlet.java
 
b/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/CellarBalancerProxyServlet.java
new file mode 100644
index 0000000..edcf672
--- /dev/null
+++ 
b/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/CellarBalancerProxyServlet.java
@@ -0,0 +1,552 @@
+/*
+ * 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.apache.karaf.cellar.http.balancer;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpEntityEnclosingRequest;
+import org.apache.http.HttpHeaders;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.AbortableHttpRequest;
+import org.apache.http.client.params.ClientPNames;
+import org.apache.http.client.params.CookiePolicy;
+import org.apache.http.client.utils.URIUtils;
+import org.apache.http.entity.InputStreamEntity;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
+import org.apache.http.message.BasicHeader;
+import org.apache.http.message.BasicHttpEntityEnclosingRequest;
+import org.apache.http.message.BasicHttpRequest;
+import org.apache.http.message.HeaderGroup;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpParams;
+import org.apache.http.util.EntityUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.reflect.Constructor;
+import java.net.HttpCookie;
+import java.net.URI;
+import java.util.*;
+
+/**
+ * An HTTP reverse proxy/gateway servlet. It is designed to be extended for 
customization
+ * if desired. Most of the work is handled by
+ * <a href="http://hc.apache.org/httpcomponents-client-ga/";>Apache 
HttpClient</a>.
+ * <p>
+ * There are alternatives to a servlet based proxy such as Apache mod_proxy if 
that is available to you. However
+ * this servlet is easily customizable by Java, secure-able by your web 
application's security (e.g. spring-security),
+ * portable across servlet engines, and is embeddable into another web 
application.
+ * </p>
+ * <p>
+ * Inspiration: http://httpd.apache.org/docs/2.0/mod/mod_proxy.html
+ * </p>
+ */
+public class CellarBalancerProxyServlet extends HttpServlet {
+
+    private final static Logger LOGGER = 
LoggerFactory.getLogger(CellarBalancerProxyServlet.class);
+
+    private List<String> locations;
+
+    protected boolean doForwardIP = true;
+    protected boolean doSendUrlFragment = true;
+
+    private HttpClient proxyClient;
+
+    public void setIPForwarding(boolean IPForwarding) {
+        this.doForwardIP = IPForwarding;
+    }
+
+    public void setLocations(List<String> locations) {
+        this.locations = locations;
+    }
+
+    @Override
+    public String getServletInfo() {
+        return "Apache Karaf Cellar Balancer Proxy Servlet";
+    }
+
+    @Override
+    public void init() throws ServletException {
+        HttpParams hcParams = new BasicHttpParams();
+        hcParams.setParameter(ClientPNames.COOKIE_POLICY, 
CookiePolicy.IGNORE_COOKIES);
+        // hcParams.setParameter(ClientPNames.HANDLE_REDIRECTS, 
ClientPNames.ALLOW_CIRCULAR_REDIRECTS);
+        proxyClient = createHttpClient(hcParams);
+    }
+
+    /**
+     * Called from {@link #init(javax.servlet.ServletConfig)}. HttpClient 
offers many opportunities
+     * for customization. By default,
+     * <a 
href="http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/client/SystemDefaultHttpClient.html";>
+     * SystemDefaultHttpClient</a> is used if available, otherwise it falls
+     * back to:
+     * <pre>new DefaultHttpClient(new 
ThreadSafeClientConnManager(),hcParams)</pre>
+     * SystemDefaultHttpClient uses PoolingClientConnectionManager. In any 
case, it should be thread-safe.
+     */
+    @SuppressWarnings({"unchecked", "deprecation"})
+    protected HttpClient createHttpClient(HttpParams hcParams) {
+        try {
+            //as of HttpComponents v4.2, this class is better since it uses 
System
+            // Properties:
+            Class clientClazz = 
Class.forName("org.apache.http.impl.client.SystemDefaultHttpClient");
+            Constructor constructor = 
clientClazz.getConstructor(HttpParams.class);
+            return (HttpClient) constructor.newInstance(hcParams);
+        } catch (ClassNotFoundException e) {
+            //no problem; use v4.1 below
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+
+        //Fallback on using older client:
+        return new DefaultHttpClient(new ThreadSafeClientConnManager(), 
hcParams);
+    }
+
+    @Override
+    public void destroy() {
+        //As of HttpComponents v4.3, clients implement closeable
+        if (proxyClient instanceof Closeable) {//TODO AutoCloseable in Java 1.6
+            try {
+                ((Closeable) proxyClient).close();
+            } catch (IOException e) {
+                log("While destroying servlet, shutting down HttpClient: " + 
e, e);
+            }
+        } else {
+            //Older releases require we do this:
+            if (proxyClient != null)
+                proxyClient.getConnectionManager().shutdown();
+        }
+        super.destroy();
+    }
+
+    @Override
+    protected void service(HttpServletRequest servletRequest, 
HttpServletResponse servletResponse)
+            throws ServletException, IOException {
+
+        String location = locations.get(new 
Random().nextInt(locations.size()));
+        URI locationUri = URI.create(location);
+        HttpHost host = URIUtils.extractHost(locationUri);
+
+        LOGGER.debug("CELLAR HTTP BALANCER: proxying to");
+        LOGGER.debug("CELLAR HTTP BALANCER:     URI: {}", locationUri);
+        LOGGER.debug("CELLAR HTTP BALANCER:     Host: {}", host);
+
+        // Make the Request
+        //note: we won't transfer the protocol version because I'm not sure it 
would truly be compatible
+        String method = servletRequest.getMethod();
+        LOGGER.debug("CELLAR HTTP BALANCER:     Method: {}", method);
+        String proxyRequestUri = rewriteUrlFromRequest(servletRequest, 
location);
+        LOGGER.debug("CELLAR HTTP BALANCER:     Proxy Request URI: {}", 
proxyRequestUri);
+        HttpRequest proxyRequest;
+        //spec: RFC 2616, sec 4.3: either of these two headers signal that 
there is a message body.
+        if (servletRequest.getHeader(HttpHeaders.CONTENT_LENGTH) != null ||
+                servletRequest.getHeader(HttpHeaders.TRANSFER_ENCODING) != 
null) {
+            HttpEntityEnclosingRequest eProxyRequest = new 
BasicHttpEntityEnclosingRequest(method, proxyRequestUri);
+            // Add the input entity (streamed)
+            //  note: we don't bother ensuring we close the servletInputStream 
since the container handles it
+            eProxyRequest.setEntity(new 
InputStreamEntity(servletRequest.getInputStream(), 
servletRequest.getContentLength()));
+            proxyRequest = eProxyRequest;
+        } else
+            proxyRequest = new BasicHttpRequest(method, proxyRequestUri);
+
+        LOGGER.debug("CELLAR HTTP BALANCER:     copying request headers");
+        copyRequestHeaders(servletRequest, proxyRequest, host);
+
+        LOGGER.debug("CELLAR HTTP BALANCER:     set X-Forwarded header");
+        setXForwardedForHeader(servletRequest, proxyRequest);
+
+        HttpResponse proxyResponse = null;
+        try {
+            // Execute the request
+            LOGGER.debug("CELLAR HTTP BALANCER:     executing proxy request");
+            proxyResponse = proxyClient.execute(host, proxyRequest);
+
+            // Process the response
+            int statusCode = proxyResponse.getStatusLine().getStatusCode();
+            LOGGER.debug("CELLAR HTTP BALANCER:     status code: {}", 
statusCode);
+
+            // copying response headers to make sure SESSIONID or other Cookie 
which comes from remote server
+            // will be saved in client when the proxied url was redirected to 
another one.
+            // see issue 
[#51](https://github.com/mitre/HTTP-Proxy-Servlet/issues/51)
+            LOGGER.debug("CELLAR HTTP BALANCER:     copying response headers");
+            copyResponseHeaders(proxyResponse, servletRequest, 
servletResponse);
+
+            if (doResponseRedirectOrNotModifiedLogic(servletRequest, 
servletResponse, proxyResponse, statusCode, location)) {
+                //the response is already "committed" now without any body to 
send
+                return;
+            }
+
+            // Pass the response code. This method with the "reason phrase" is 
deprecated but it's the only way to pass the
+            //  reason along too.
+            //noinspection deprecation
+            LOGGER.debug("CELLAR HTTP BALANCER:     set response status code");
+            servletResponse.setStatus(statusCode, 
proxyResponse.getStatusLine().getReasonPhrase());
+
+            // Send the content to the client
+            LOGGER.debug("CELLAR HTTP BALANCER:     copying response entity");
+            copyResponseEntity(proxyResponse, servletResponse);
+
+        } catch (Exception e) {
+            //abort request, according to best practice with HttpClient
+            if (proxyRequest instanceof AbortableHttpRequest) {
+                AbortableHttpRequest abortableHttpRequest = 
(AbortableHttpRequest) proxyRequest;
+                abortableHttpRequest.abort();
+            }
+            if (e instanceof RuntimeException)
+                throw (RuntimeException) e;
+            if (e instanceof ServletException)
+                throw (ServletException) e;
+            //noinspection ConstantConditions
+            if (e instanceof IOException)
+                throw (IOException) e;
+            throw new RuntimeException(e);
+
+        } finally {
+            // make sure the entire entity was consumed, so the connection is 
released
+            if (proxyResponse != null)
+                consumeQuietly(proxyResponse.getEntity());
+            //Note: Don't need to close servlet outputStream:
+            // 
http://stackoverflow.com/questions/1159168/should-one-call-close-on-httpservletresponse-getoutputstream-getwriter
+        }
+    }
+
+    protected boolean doResponseRedirectOrNotModifiedLogic(
+            HttpServletRequest servletRequest, HttpServletResponse 
servletResponse,
+            HttpResponse proxyResponse, int statusCode, String location)
+            throws ServletException, IOException {
+        // Check if the proxy response is a redirect
+        // The following code is adapted from 
org.tigris.noodle.filters.CheckForRedirect
+        if (statusCode >= HttpServletResponse.SC_MULTIPLE_CHOICES /* 300 */
+                && statusCode < HttpServletResponse.SC_NOT_MODIFIED /* 304 */) 
{
+            Header locationHeader = 
proxyResponse.getLastHeader(HttpHeaders.LOCATION);
+            if (locationHeader == null) {
+                throw new ServletException("Received status code: " + 
statusCode
+                        + " but no " + HttpHeaders.LOCATION + " header was 
found in the response");
+            }
+            // Modify the redirect to go to this proxy servlet rather that the 
proxied host
+            String locStr = rewriteUrlFromResponse(servletRequest, 
locationHeader.getValue(), location);
+
+            servletResponse.sendRedirect(locStr);
+            return true;
+        }
+        // 304 needs special handling.  See:
+        // http://www.ics.uci.edu/pub/ietf/http/rfc1945.html#Code304
+        // We get a 304 whenever passed an 'If-Modified-Since'
+        // header and the data on disk has not changed; server
+        // responds w/ a 304 saying I'm not going to send the
+        // body because the file has not changed.
+        if (statusCode == HttpServletResponse.SC_NOT_MODIFIED) {
+            servletResponse.setIntHeader(HttpHeaders.CONTENT_LENGTH, 0);
+            servletResponse.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+            return true;
+        }
+        return false;
+    }
+
+    protected void closeQuietly(Closeable closeable) {
+        try {
+            closeable.close();
+        } catch (IOException e) {
+            log(e.getMessage(), e);
+        }
+    }
+
+    /**
+     * HttpClient v4.1 doesn't have the
+     * {@link 
org.apache.http.util.EntityUtils#consumeQuietly(org.apache.http.HttpEntity)} 
method.
+     */
+    protected void consumeQuietly(HttpEntity entity) {
+        try {
+            EntityUtils.consume(entity);
+        } catch (IOException e) {//ignore
+            log(e.getMessage(), e);
+        }
+    }
+
+    /**
+     * These are the "hop-by-hop" headers that should not be copied.
+     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
+     * I use an HttpClient HeaderGroup class instead of Set<String> because 
this
+     * approach does case insensitive lookup faster.
+     */
+    protected static final HeaderGroup hopByHopHeaders;
+
+    static {
+        hopByHopHeaders = new HeaderGroup();
+        String[] headers = new String[]{
+                "Connection", "Keep-Alive", "Proxy-Authenticate", 
"Proxy-Authorization",
+                "TE", "Trailers", "Transfer-Encoding", "Upgrade"};
+        for (String header : headers) {
+            hopByHopHeaders.addHeader(new BasicHeader(header, null));
+        }
+    }
+
+    /**
+     * Copy request headers from the servlet client to the proxy request.
+     */
+    protected void copyRequestHeaders(HttpServletRequest servletRequest, 
HttpRequest proxyRequest, HttpHost host) {
+        // Get an Enumeration of all of the header names sent by the client
+        Enumeration enumerationOfHeaderNames = servletRequest.getHeaderNames();
+        while (enumerationOfHeaderNames.hasMoreElements()) {
+            String headerName = (String) 
enumerationOfHeaderNames.nextElement();
+            //Instead the content-length is effectively set via 
InputStreamEntity
+            if (headerName.equalsIgnoreCase(HttpHeaders.CONTENT_LENGTH))
+                continue;
+            if (hopByHopHeaders.containsHeader(headerName))
+                continue;
+
+            Enumeration headers = servletRequest.getHeaders(headerName);
+            while (headers.hasMoreElements()) {//sometimes more than one value
+                String headerValue = (String) headers.nextElement();
+                // In case the proxy host is running multiple virtual servers,
+                // rewrite the Host header to ensure that we get content from
+                // the correct virtual server
+                if (headerName.equalsIgnoreCase(HttpHeaders.HOST)) {
+                    headerValue = host.getHostName();
+                    if (host.getPort() != -1)
+                        headerValue += ":" + host.getPort();
+                } else if 
(headerName.equalsIgnoreCase(org.apache.http.cookie.SM.COOKIE)) {
+                    headerValue = getRealCookie(headerValue);
+                }
+                proxyRequest.addHeader(headerName, headerValue);
+            }
+        }
+    }
+
+    private void setXForwardedForHeader(HttpServletRequest servletRequest,
+                                        HttpRequest proxyRequest) {
+        String headerName = "X-Forwarded-For";
+        if (doForwardIP) {
+            String newHeader = servletRequest.getRemoteAddr();
+            String existingHeader = servletRequest.getHeader(headerName);
+            if (existingHeader != null) {
+                newHeader = existingHeader + ", " + newHeader;
+            }
+            proxyRequest.setHeader(headerName, newHeader);
+        }
+    }
+
+    /**
+     * Copy proxied response headers back to the servlet client.
+     */
+    protected void copyResponseHeaders(HttpResponse proxyResponse, 
HttpServletRequest servletRequest,
+                                       HttpServletResponse servletResponse) {
+        for (Header header : proxyResponse.getAllHeaders()) {
+            if (hopByHopHeaders.containsHeader(header.getName()))
+                continue;
+            if 
(header.getName().equalsIgnoreCase(org.apache.http.cookie.SM.SET_COOKIE) ||
+                    
header.getName().equalsIgnoreCase(org.apache.http.cookie.SM.SET_COOKIE2)) {
+                copyProxyCookie(servletRequest, servletResponse, header);
+            } else {
+                servletResponse.addHeader(header.getName(), header.getValue());
+            }
+        }
+    }
+
+    /**
+     * Copy cookie from the proxy to the servlet client.
+     * Replaces cookie path to local path and renames cookie to avoid 
collisions.
+     */
+    protected void copyProxyCookie(HttpServletRequest servletRequest,
+                                   HttpServletResponse servletResponse, Header 
header) {
+        List<HttpCookie> cookies = HttpCookie.parse(header.getValue());
+        String path = servletRequest.getContextPath(); // path starts with / 
or is empty string
+        path += servletRequest.getServletPath(); // servlet path starts with / 
or is empty string
+
+        for (HttpCookie cookie : cookies) {
+            //set cookie name prefixed w/ a proxy value so it won't collide w/ 
other cookies
+            String proxyCookieName = getCookieNamePrefix() + cookie.getName();
+            Cookie servletCookie = new Cookie(proxyCookieName, 
cookie.getValue());
+            servletCookie.setComment(cookie.getComment());
+            servletCookie.setMaxAge((int) cookie.getMaxAge());
+            servletCookie.setPath(path); //set to the path of the proxy servlet
+            // don't set cookie domain
+            servletCookie.setSecure(cookie.getSecure());
+            servletCookie.setVersion(cookie.getVersion());
+            servletResponse.addCookie(servletCookie);
+        }
+    }
+
+    /**
+     * Take any client cookies that were originally from the proxy and prepare 
them to send to the
+     * proxy.  This relies on cookie headers being set correctly according to 
RFC 6265 Sec 5.4.
+     * This also blocks any local cookies from being sent to the proxy.
+     */
+    protected String getRealCookie(String cookieValue) {
+        StringBuilder escapedCookie = new StringBuilder();
+        String cookies[] = cookieValue.split("; ");
+        for (String cookie : cookies) {
+            String cookieSplit[] = cookie.split("=");
+            if (cookieSplit.length == 2) {
+                String cookieName = cookieSplit[0];
+                if (cookieName.startsWith(getCookieNamePrefix())) {
+                    cookieName = 
cookieName.substring(getCookieNamePrefix().length());
+                    if (escapedCookie.length() > 0) {
+                        escapedCookie.append("; ");
+                    }
+                    
escapedCookie.append(cookieName).append("=").append(cookieSplit[1]);
+                }
+            }
+
+            cookieValue = escapedCookie.toString();
+        }
+        return cookieValue;
+    }
+
+    /**
+     * The string prefixing rewritten cookies.
+     */
+    protected String getCookieNamePrefix() {
+        return "!Proxy!" + getServletConfig().getServletName();
+    }
+
+    /**
+     * Copy response body data (the entity) from the proxy to the servlet 
client.
+     */
+    protected void copyResponseEntity(HttpResponse proxyResponse, 
HttpServletResponse servletResponse) throws IOException {
+        HttpEntity entity = proxyResponse.getEntity();
+        if (entity != null) {
+            OutputStream servletOutputStream = 
servletResponse.getOutputStream();
+            entity.writeTo(servletOutputStream);
+        }
+    }
+
+    /**
+     * Reads the request URI from {@code servletRequest} and rewrites it, 
considering targetUri.
+     * It's used to make the new request.
+     */
+    protected String rewriteUrlFromRequest(HttpServletRequest servletRequest, 
String location) {
+        StringBuilder uri = new StringBuilder(500);
+        uri.append(location);
+        // Handle the path given to the servlet
+        if (servletRequest.getPathInfo() != null) {//ex: /my/path.html
+            uri.append(encodeUriQuery(servletRequest.getPathInfo()));
+        }
+        // Handle the query string & fragment
+        String queryString = servletRequest.getQueryString();//ex:(following 
'?'): name=value&foo=bar#fragment
+        String fragment = null;
+        //split off fragment from queryString, updating queryString if found
+        if (queryString != null) {
+            int fragIdx = queryString.indexOf('#');
+            if (fragIdx >= 0) {
+                fragment = queryString.substring(fragIdx + 1);
+                queryString = queryString.substring(0, fragIdx);
+            }
+        }
+
+        queryString = rewriteQueryStringFromRequest(servletRequest, 
queryString);
+        if (queryString != null && queryString.length() > 0) {
+            uri.append('?');
+            uri.append(encodeUriQuery(queryString));
+        }
+
+        if (doSendUrlFragment && fragment != null) {
+            uri.append('#');
+            uri.append(encodeUriQuery(fragment));
+        }
+        return uri.toString();
+    }
+
+    protected String rewriteQueryStringFromRequest(HttpServletRequest 
servletRequest, String queryString) {
+        return queryString;
+    }
+
+    /**
+     * For a redirect response from the target server, this translates {@code 
theUrl} to redirect to
+     * and translates it to one the original client can use.
+     */
+    protected String rewriteUrlFromResponse(HttpServletRequest servletRequest, 
String theUrl, String location) {
+        //TODO document example paths
+        if (theUrl.startsWith(location)) {
+            String curUrl = servletRequest.getRequestURL().toString();//no 
query
+            String pathInfo = servletRequest.getPathInfo();
+            if (pathInfo != null) {
+                assert curUrl.endsWith(pathInfo);
+                curUrl = curUrl.substring(0, curUrl.length() - 
pathInfo.length());//take pathInfo off
+            }
+            theUrl = curUrl + theUrl.substring(location.length());
+        }
+        return theUrl;
+    }
+
+    /**
+     * Encodes characters in the query or fragment part of the URI.
+     * <p/>
+     * <p>Unfortunately, an incoming URI sometimes has characters disallowed 
by the spec.  HttpClient
+     * insists that the outgoing proxied request has a valid URI because it 
uses Java's {@link URI}.
+     * To be more forgiving, we must escape the problematic characters.  See 
the URI class for the
+     * spec.
+     *
+     * @param in example: name=value&foo=bar#fragment
+     */
+    protected static CharSequence encodeUriQuery(CharSequence in) {
+        //Note that I can't simply use URI.java to encode because it will 
escape pre-existing escaped things.
+        StringBuilder outBuf = null;
+        Formatter formatter = null;
+        for (int i = 0; i < in.length(); i++) {
+            char c = in.charAt(i);
+            boolean escape = true;
+            if (c < 128) {
+                if (asciiQueryChars.get((int) c)) {
+                    escape = false;
+                }
+            } else if (!Character.isISOControl(c) && 
!Character.isSpaceChar(c)) {//not-ascii
+                escape = false;
+            }
+            if (!escape) {
+                if (outBuf != null)
+                    outBuf.append(c);
+            } else {
+                //escape
+                if (outBuf == null) {
+                    outBuf = new StringBuilder(in.length() + 5 * 3);
+                    outBuf.append(in, 0, i);
+                    formatter = new Formatter(outBuf);
+                }
+                //leading %, 0 padded, width 2, capital hex
+                formatter.format("%%%02X", (int) c);//TODO
+            }
+        }
+        return outBuf != null ? outBuf : in;
+    }
+
+    protected static final BitSet asciiQueryChars;
+
+    static {
+        char[] c_unreserved = "_-!.~'()*".toCharArray();//plus alphanum
+        char[] c_punct = ",;:$&+=".toCharArray();
+        char[] c_reserved = "?/[]@".toCharArray();//plus punct
+
+        asciiQueryChars = new BitSet(128);
+        for (char c = 'a'; c <= 'z'; c++) asciiQueryChars.set((int) c);
+        for (char c = 'A'; c <= 'Z'; c++) asciiQueryChars.set((int) c);
+        for (char c = '0'; c <= '9'; c++) asciiQueryChars.set((int) c);
+        for (char c : c_unreserved) asciiQueryChars.set((int) c);
+        for (char c : c_punct) asciiQueryChars.set((int) c);
+        for (char c : c_reserved) asciiQueryChars.set((int) c);
+
+        asciiQueryChars.set((int) '%');//leave existing percent escapes in 
place
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf-cellar/blob/77b8daec/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/ClusterBalancerEvent.java
----------------------------------------------------------------------
diff --git 
a/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/ClusterBalancerEvent.java
 
b/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/ClusterBalancerEvent.java
new file mode 100644
index 0000000..4713a09
--- /dev/null
+++ 
b/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/ClusterBalancerEvent.java
@@ -0,0 +1,60 @@
+/*
+ * 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.apache.karaf.cellar.http.balancer;
+
+import org.apache.karaf.cellar.core.event.Event;
+
+import java.util.List;
+
+public class ClusterBalancerEvent extends Event {
+
+    private String alias;
+    private int type;
+    private List<String> locations;
+
+    public static int ADDING = 0;
+    public static int REMOVING = 1;
+
+    public ClusterBalancerEvent(String alias, int type, List<String> 
locations) {
+        super(alias);
+        this.alias = alias;
+        this.type = type;
+        this.locations = locations;
+    }
+
+    public String getAlias() {
+        return alias;
+    }
+
+    public void setAlias(String alias) {
+        this.alias = alias;
+    }
+
+    public int getType() {
+        return type;
+    }
+
+    public void setType(int type) {
+        this.type = type;
+    }
+
+    public List<String> getLocations() {
+        return locations;
+    }
+
+    public void setLocations(List<String> locations) {
+        this.locations = locations;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf-cellar/blob/77b8daec/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/Constants.java
----------------------------------------------------------------------
diff --git 
a/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/Constants.java
 
b/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/Constants.java
new file mode 100644
index 0000000..4b6d565
--- /dev/null
+++ 
b/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/Constants.java
@@ -0,0 +1,21 @@
+/*
+ * 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.apache.karaf.cellar.http.balancer;
+
+public class Constants {
+
+    public static String BALANCER_MAP = 
"org.apache.karaf.cellar.http.balancer";
+    public static String CATEGORY = "balanced.servlet";
+
+}

http://git-wip-us.apache.org/repos/asf/karaf-cellar/blob/77b8daec/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/LocalServletListener.java
----------------------------------------------------------------------
diff --git 
a/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/LocalServletListener.java
 
b/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/LocalServletListener.java
new file mode 100644
index 0000000..04e7331
--- /dev/null
+++ 
b/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/LocalServletListener.java
@@ -0,0 +1,129 @@
+/*
+ * 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.apache.karaf.cellar.http.balancer;
+
+import org.apache.karaf.cellar.core.ClusterManager;
+import org.apache.karaf.cellar.core.Configurations;
+import org.apache.karaf.cellar.core.Group;
+import org.apache.karaf.cellar.core.GroupManager;
+import org.apache.karaf.cellar.core.control.SwitchStatus;
+import org.apache.karaf.cellar.core.event.EventProducer;
+import org.ops4j.pax.web.service.spi.ServletEvent;
+import org.ops4j.pax.web.service.spi.ServletListener;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.Servlet;
+import java.util.*;
+
+/**
+ * Listen local node servlet event, in order to update the cluster servlet map
+ * and send a cluster event to the other nodes
+ */
+public class LocalServletListener implements ServletListener {
+
+    private final static Logger LOGGER = 
LoggerFactory.getLogger(LocalServletListener.class);
+
+    private ClusterManager clusterManager;
+    private GroupManager groupManager;
+    private ConfigurationAdmin configurationAdmin;
+    private EventProducer eventProducer;
+
+    @Override
+    public synchronized void servletEvent(ServletEvent servletEvent) {
+
+        if (eventProducer.getSwitch().getStatus().equals(SwitchStatus.OFF)) {
+            LOGGER.warn("CELLAR HTTP BALANCER: cluster event producer is OFF");
+            return;
+        }
+
+        Servlet servlet = servletEvent.getServlet();
+        if (servlet != null && 
servlet.getClass().getName().equals(CellarBalancerProxyServlet.class.getName()))
 {
+            LOGGER.trace("CELLAR HTTP BALANCER: ignoring 
CellarBalancerProxyServlet servlet event");
+            return;
+        }
+
+        Set<Group> localGroups = groupManager.listLocalGroups();
+
+        BalancedServletUtil util = new BalancedServletUtil();
+        util.setClusterManager(clusterManager);
+        util.setConfigurationAdmin(configurationAdmin);
+        String alias = servletEvent.getAlias();
+        String location = util.constructLocation(alias);
+
+        for (Group group : localGroups) {
+            Map<String, List<String>> clusterServlets = 
clusterManager.getMap(Constants.BALANCER_MAP + Configurations.SEPARATOR + 
group.getName());
+
+            if (servletEvent.getType() == ServletEvent.DEPLOYED) {
+                // update the cluster servlets
+                List<String> locations = clusterServlets.get(alias);
+                if (locations == null) {
+                    locations = new ArrayList<String>();
+                }
+
+                if (!locations.contains(location)) {
+                    LOGGER.debug("CELLAR HTTP BALANCER: adding location {} to 
servlet {} on cluster", location, alias);
+                    locations.add(location);
+                    clusterServlets.put(alias, locations);
+                    // send cluster event
+                    ClusterBalancerEvent event = new 
ClusterBalancerEvent(alias, ClusterBalancerEvent.ADDING, locations);
+                    event.setSourceGroup(group);
+                    event.setSourceNode(clusterManager.getNode());
+                    eventProducer.produce(event);
+                } else {
+                    LOGGER.debug("CELLAR HTTP BALANCER: location {} already 
defined for servlet {} on cluster", location, alias);
+                }
+            } else if (servletEvent.getType() == ServletEvent.UNDEPLOYED) {
+                List<String> locations = clusterServlets.get(alias);
+                if (locations == null)
+                    locations = new ArrayList<String>();
+                if (locations.contains(location)) {
+                    LOGGER.debug("CELLAR HTTP BALANCER: removing location {} 
for servlet {} on cluster", location, alias);
+                    locations.remove(location);
+                    // update cluster state
+                    clusterServlets.put(alias, locations);
+                    // send cluster event
+                    ClusterBalancerEvent event = new 
ClusterBalancerEvent(alias, ClusterBalancerEvent.REMOVING, locations);
+                    event.setSourceGroup(group);
+                    event.setSourceNode(clusterManager.getNode());
+                    eventProducer.produce(event);
+                }
+                if (locations.isEmpty()) {
+                    LOGGER.debug("CELLAR HTTP BALANCER: destroying servlet {} 
from cluster", alias);
+                    // update the cluster servlets
+                    clusterServlets.remove(alias);
+                }
+            }
+        }
+
+    }
+
+    public void setClusterManager(ClusterManager clusterManager) {
+        this.clusterManager = clusterManager;
+    }
+
+    public void setGroupManager(GroupManager groupManager) {
+        this.groupManager = groupManager;
+    }
+
+    public void setConfigurationAdmin(ConfigurationAdmin configurationAdmin) {
+        this.configurationAdmin = configurationAdmin;
+    }
+
+    public void setEventProducer(EventProducer eventProducer) {
+        this.eventProducer = eventProducer;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf-cellar/blob/77b8daec/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/ProxyServletRegistry.java
----------------------------------------------------------------------
diff --git 
a/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/ProxyServletRegistry.java
 
b/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/ProxyServletRegistry.java
new file mode 100644
index 0000000..db0e373
--- /dev/null
+++ 
b/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/ProxyServletRegistry.java
@@ -0,0 +1,51 @@
+/*
+ * 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.apache.karaf.cellar.http.balancer;
+
+import org.osgi.framework.ServiceRegistration;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class ProxyServletRegistry {
+
+    private Map<String, ServiceRegistration> proxyRegistrations;
+
+    public void register(String alias, ServiceRegistration registration) {
+        proxyRegistrations.put(alias, registration);
+    }
+
+    public void unregister(String alias) {
+        ServiceRegistration registration = proxyRegistrations.remove(alias);
+        if (registration != null) {
+            registration.unregister();
+        }
+    }
+
+    public boolean contain(String alias) {
+        return proxyRegistrations.containsKey(alias);
+    }
+
+    public void init() {
+        proxyRegistrations = new HashMap<String, ServiceRegistration>();
+    }
+
+    public void destroy() {
+        for (ServiceRegistration registration : proxyRegistrations.values()) {
+            registration.unregister();
+        }
+        proxyRegistrations = new HashMap<String, ServiceRegistration>();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf-cellar/blob/77b8daec/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/ServletSynchronizer.java
----------------------------------------------------------------------
diff --git 
a/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/ServletSynchronizer.java
 
b/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/ServletSynchronizer.java
new file mode 100644
index 0000000..d3714a0
--- /dev/null
+++ 
b/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/ServletSynchronizer.java
@@ -0,0 +1,206 @@
+/*
+ * 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.apache.karaf.cellar.http.balancer;
+
+import org.apache.karaf.cellar.core.*;
+import org.apache.karaf.cellar.core.control.SwitchStatus;
+import org.apache.karaf.cellar.core.event.EventProducer;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.Servlet;
+import java.io.IOException;
+import java.util.*;
+
+public class ServletSynchronizer implements Synchronizer {
+
+    private final static Logger LOGGER = 
LoggerFactory.getLogger(ServletSynchronizer.class);
+
+    private ClusterManager clusterManager;
+    private GroupManager groupManager;
+    private ConfigurationAdmin configurationAdmin;
+    private ProxyServletRegistry proxyRegistry;
+    private BundleContext bundleContext;
+    private EventProducer eventProducer;
+
+    public void init() {
+        if (groupManager == null)
+            return;
+        Set<Group> groups = groupManager.listLocalGroups();
+        if (groups != null && !groups.isEmpty()) {
+            for (Group group : groups) {
+                sync(group);
+            }
+        }
+    }
+
+    @Override
+    public void sync(Group group) {
+        String policy = getSyncPolicy(group);
+        if (policy == null) {
+            LOGGER.warn("CELLAR HTTP BALANCER: sync policy is not defined for 
cluster group {}", group.getName());
+        }
+        if (policy.equalsIgnoreCase("cluster")) {
+            LOGGER.debug("CELLAR HTTP BALANCER: sync policy set as 'cluster' 
for cluster group {}", group.getName());
+            LOGGER.debug("CELLAR HTTP BALANCER: updating node from the cluster 
(pull first)");
+            pull(group);
+            LOGGER.debug("CELLAR HTTP BALANCER: updating cluster from the 
local node (push after)");
+            push(group);
+        } else if (policy.equalsIgnoreCase("node")) {
+            LOGGER.debug("CELLAR HTTP BALANCER: sync policy set as 'node' for 
cluster group {}", group.getName());
+            LOGGER.debug("CELLAR HTTP BALANCER: updating cluster from the 
local node (push first)");
+            push(group);
+            LOGGER.debug("CELLAR HTTP BALANCER: updating node from the cluster 
(pull after)");
+            pull(group);
+        } else if (policy.equalsIgnoreCase("clusterOnly")) {
+            LOGGER.debug("CELLAR HTTP BALANCER: sync policy set as 
'clusterOnly' for cluster group " + group.getName());
+            LOGGER.debug("CELLAR HTTP BALANCER: updating node from the cluster 
(pull only)");
+            pull(group);
+        } else if (policy.equalsIgnoreCase("nodeOnly")) {
+            LOGGER.debug("CELLAR HTTP BALANCER: sync policy set as 'nodeOnly' 
for cluster group " + group.getName());
+            LOGGER.debug("CELLAR HTTP BALANCER: updating cluster from the 
local node (push only)");
+            push(group);
+        } else {
+            LOGGER.debug("CELLAR HTTP BALANCER: sync policy set as 'disabled' 
for cluster group " + group.getName());
+            LOGGER.debug("CELLAR HTTP BALANCER: no sync");
+        }
+    }
+
+    @Override
+    public void pull(Group group) {
+        Map<String, List<String>> clusterServlets = 
clusterManager.getMap(Constants.BALANCER_MAP + Configurations.SEPARATOR + 
group.getName());
+        for (String alias : clusterServlets.keySet()) {
+            try {
+                // add a proxy servlet only if the alias is not present locally
+                Collection<ServiceReference<Servlet>> references = 
bundleContext.getServiceReferences(Servlet.class, "(alias=" + alias + ")");
+                if (references.isEmpty()) {
+                    LOGGER.debug("CELLAR HTTP BALANCER: create proxy servlet 
for {}", alias);
+                    CellarBalancerProxyServlet proxyServlet = new 
CellarBalancerProxyServlet();
+                    proxyServlet.setLocations(clusterServlets.get(alias));
+                    proxyServlet.init();
+                    Hashtable<String, String> properties = new 
Hashtable<String, String>();
+                    properties.put("alias", alias);
+                    properties.put("cellar.http.balancer.proxy", "true");
+                    ServiceRegistration registration = 
bundleContext.registerService(Servlet.class, proxyServlet, properties);
+                    proxyRegistry.register(alias, registration);
+                }
+            } catch (Exception e) {
+                LOGGER.warn("CELLAR HTTP BALANCER: can't create proxy servlet 
for {}", alias, e);
+            }
+        }
+    }
+
+    @Override
+    public void push(Group group) {
+
+        if (eventProducer.getSwitch().getStatus().equals(SwitchStatus.OFF)) {
+            LOGGER.warn("CELLAR HTTP BALANCER: cluster event producer is OFF");
+            return;
+        }
+
+        Map<String, List<String>> clusterServlets = 
clusterManager.getMap(Constants.BALANCER_MAP + Configurations.SEPARATOR + 
group.getName());
+        BalancedServletUtil util = new BalancedServletUtil();
+        util.setClusterManager(clusterManager);
+        util.setConfigurationAdmin(configurationAdmin);
+        try {
+            Collection<ServiceReference<Servlet>> references = 
bundleContext.getServiceReferences(Servlet.class, null);
+            for (ServiceReference<Servlet> reference : references) {
+                if (reference.getProperty("alias") != null) {
+                    String alias = (String) reference.getProperty("alias");
+                    String location = util.constructLocation(alias);
+                    Servlet servlet = bundleContext.getService(reference);
+                    if (servlet != null) {
+                        if (!(servlet instanceof CellarBalancerProxyServlet)) {
+                            // update the cluster servlets
+                            List<String> locations = 
clusterServlets.get(alias);
+                            if (locations == null) {
+                                locations = new ArrayList<String>();
+                            }
+
+                            if (!locations.contains(location)) {
+                                LOGGER.debug("CELLAR HTTP BALANCER: adding 
location {} to servlet {} on cluster", location, alias);
+                                locations.add(location);
+                                clusterServlets.put(alias, locations);
+                                // send cluster event
+                                ClusterBalancerEvent event = new 
ClusterBalancerEvent(alias, ClusterBalancerEvent.ADDING, locations);
+                                event.setSourceGroup(group);
+                                event.setSourceNode(clusterManager.getNode());
+                                eventProducer.produce(event);
+                            } else {
+                                LOGGER.debug("CELLAR HTTP BALANCER: location 
{} already defined for servlet {} on cluster", location, alias);
+                            }
+                        }
+                    }
+                } else {
+                    LOGGER.warn("CELLAR HTTP BALANCER: alias property is not 
defined");
+                }
+            }
+        } catch (Exception e) {
+            LOGGER.warn("CELLAR HTTP BALANCER: can't push servlet on cluster", 
e);
+        }
+    }
+
+    /**
+     * Get the balanced servlet sync policy for the given cluster group.
+     *
+     * @param group the cluster group.
+     * @return the current features sync policy for the given cluster group.
+     */
+    @Override
+    public String getSyncPolicy(Group group) {
+        String groupName = group.getName();
+        try {
+            Configuration configuration = 
configurationAdmin.getConfiguration(Configurations.GROUP, null);
+            Dictionary<String, Object> properties = 
configuration.getProperties();
+            if (properties != null) {
+                String propertyKey = groupName + Configurations.SEPARATOR + 
Constants.CATEGORY + Configurations.SEPARATOR + Configurations.SYNC;
+                return properties.get(propertyKey).toString();
+            }
+        } catch (IOException e) {
+            LOGGER.error("CELLAR FEATURE: error while retrieving the sync 
policy", e);
+        }
+
+        return "disabled";
+    }
+
+    public void setClusterManager(ClusterManager clusterManager) {
+        this.clusterManager = clusterManager;
+    }
+
+    public void setGroupManager(GroupManager groupManager) {
+        this.groupManager = groupManager;
+    }
+
+    public void setConfigurationAdmin(ConfigurationAdmin configurationAdmin) {
+        this.configurationAdmin = configurationAdmin;
+    }
+
+    public void setProxyRegistry(ProxyServletRegistry proxyRegistry) {
+        this.proxyRegistry = proxyRegistry;
+    }
+
+    public void setBundleContext(BundleContext bundleContext) {
+        this.bundleContext = bundleContext;
+    }
+
+    public void setEventProducer(EventProducer eventProducer) {
+        this.eventProducer = eventProducer;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf-cellar/blob/77b8daec/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/command/ListClusterServlets.java
----------------------------------------------------------------------
diff --git 
a/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/command/ListClusterServlets.java
 
b/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/command/ListClusterServlets.java
new file mode 100644
index 0000000..34c5c80
--- /dev/null
+++ 
b/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/command/ListClusterServlets.java
@@ -0,0 +1,67 @@
+/*
+ * 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.apache.karaf.cellar.http.balancer.command;
+
+import org.apache.karaf.cellar.core.Configurations;
+import org.apache.karaf.cellar.core.Group;
+import org.apache.karaf.cellar.core.shell.CellarCommandSupport;
+import org.apache.karaf.cellar.core.shell.completer.AllGroupsCompleter;
+import org.apache.karaf.cellar.http.balancer.Constants;
+import org.apache.karaf.shell.api.action.Argument;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.Completion;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.apache.karaf.shell.support.table.ShellTable;
+
+import java.util.List;
+import java.util.Map;
+
+@Command(scope = "cluster", name = "http-list", description = "List the HTTP 
servlets on the cluster")
+@Service
+public class ListClusterServlets extends CellarCommandSupport {
+
+    @Argument(index = 0, name = "group", description = "The cluster group 
name", required = true, multiValued = false)
+    @Completion(AllGroupsCompleter.class)
+    private String groupName;
+
+    @Override
+    public Object doExecute() throws Exception {
+
+        Group group = groupManager.findGroupByName(groupName);
+        if (group == null) {
+            System.err.println("Cluster group " + groupName + " doesn't 
exist");
+            return null;
+        }
+
+        Map<String, List<String>> clusterServlets = 
clusterManager.getMap(Constants.BALANCER_MAP + Configurations.SEPARATOR + 
groupName);
+
+        ShellTable table = new ShellTable();
+        table.column("Alias");
+        table.column("Locations");
+
+        for (String alias : clusterServlets.keySet()) {
+            List<String> locations = clusterServlets.get(alias);
+            StringBuilder builder = new StringBuilder();
+            for (String location : locations) {
+                builder.append(location).append(" ");
+            }
+            table.addRow().addContent(alias, builder.toString());
+        }
+
+        table.print(System.out);
+
+        return null;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf-cellar/blob/77b8daec/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/internal/osgi/Activator.java
----------------------------------------------------------------------
diff --git 
a/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/internal/osgi/Activator.java
 
b/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/internal/osgi/Activator.java
new file mode 100644
index 0000000..3ae2ee5
--- /dev/null
+++ 
b/http/balancer/src/main/java/org/apache/karaf/cellar/http/balancer/internal/osgi/Activator.java
@@ -0,0 +1,121 @@
+/*
+ * 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.apache.karaf.cellar.http.balancer.internal.osgi;
+
+import org.apache.karaf.cellar.core.ClusterManager;
+import org.apache.karaf.cellar.core.GroupManager;
+import org.apache.karaf.cellar.core.Synchronizer;
+import org.apache.karaf.cellar.core.event.EventHandler;
+import org.apache.karaf.cellar.core.event.EventProducer;
+import org.apache.karaf.cellar.http.balancer.BalancerEventHandler;
+import org.apache.karaf.cellar.http.balancer.LocalServletListener;
+import org.apache.karaf.cellar.http.balancer.ProxyServletRegistry;
+import org.apache.karaf.cellar.http.balancer.ServletSynchronizer;
+import org.apache.karaf.util.tracker.BaseActivator;
+import org.apache.karaf.util.tracker.annotation.ProvideService;
+import org.apache.karaf.util.tracker.annotation.RequireService;
+import org.apache.karaf.util.tracker.annotation.Services;
+import org.ops4j.pax.web.service.spi.ServletListener;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Hashtable;
+
+@Services(
+        provides = {
+                @ProvideService(ServletListener.class),
+                @ProvideService(EventHandler.class),
+                @ProvideService(Synchronizer.class)
+        },
+        requires = {
+                @RequireService(ClusterManager.class),
+                @RequireService(GroupManager.class),
+                @RequireService(ConfigurationAdmin.class),
+                @RequireService(EventProducer.class)
+        }
+)
+public class Activator extends BaseActivator {
+
+    private final static Logger LOGGER = 
LoggerFactory.getLogger(Activator.class);
+
+    private ProxyServletRegistry proxyRegistry;
+
+    @Override
+    public void doStart() throws Exception {
+        ClusterManager clusterManager = 
getTrackedService(ClusterManager.class);
+        if (clusterManager == null) {
+            return;
+        }
+        GroupManager groupManager = getTrackedService(GroupManager.class);
+        if (groupManager == null) {
+            return;
+        }
+        ConfigurationAdmin configurationAdmin = 
getTrackedService(ConfigurationAdmin.class);
+        if (configurationAdmin == null) {
+            return;
+        }
+        EventProducer eventProducer = getTrackedService(EventProducer.class);
+        if (eventProducer == null) {
+            return;
+        }
+
+        LOGGER.debug("CELLAR HTTP BALANCER: starting proxy registry");
+        proxyRegistry = new ProxyServletRegistry();
+        proxyRegistry.init();
+
+        LOGGER.debug("CELLAR HTTP BALANCER: starting balancer event handler");
+        BalancerEventHandler balancerEventHandler = new BalancerEventHandler();
+        balancerEventHandler.setClusterManager(clusterManager);
+        balancerEventHandler.setBundleContext(bundleContext);
+        balancerEventHandler.setConfigurationAdmin(configurationAdmin);
+        balancerEventHandler.setGroupManager(groupManager);
+        balancerEventHandler.setProxyRegistry(proxyRegistry);
+        Hashtable props = new Hashtable();
+        props.put("managed", "true");
+        register(EventHandler.class, balancerEventHandler, props);
+
+        LOGGER.debug("CELLAR HTTP BALANCER: starting servlet synchronizer");
+        ServletSynchronizer synchronizer = new ServletSynchronizer();
+        synchronizer.setEventProducer(eventProducer);
+        synchronizer.setClusterManager(clusterManager);
+        synchronizer.setConfigurationAdmin(configurationAdmin);
+        synchronizer.setProxyRegistry(proxyRegistry);
+        synchronizer.setGroupManager(groupManager);
+        synchronizer.setBundleContext(bundleContext);
+        synchronizer.init();
+        register(Synchronizer.class, synchronizer);
+
+        LOGGER.debug("CELLAR HTTP BALANCER: starting local servlet listener");
+        LocalServletListener servletListener = new LocalServletListener();
+        servletListener.setClusterManager(clusterManager);
+        servletListener.setGroupManager(groupManager);
+        servletListener.setConfigurationAdmin(configurationAdmin);
+        servletListener.setEventProducer(eventProducer);
+        props = new Hashtable();
+        props.put("resource", "balanced.servlet");
+        register(ServletListener.class, servletListener, props);
+    }
+
+    @Override
+    public void doStop() {
+        super.doStop();
+
+        if (proxyRegistry != null) {
+            proxyRegistry.destroy();
+            proxyRegistry = null;
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf-cellar/blob/77b8daec/http/pom.xml
----------------------------------------------------------------------
diff --git a/http/pom.xml b/http/pom.xml
new file mode 100644
index 0000000..10f0ae4
--- /dev/null
+++ b/http/pom.xml
@@ -0,0 +1,41 @@
+<?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/xsd/maven-4.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.karaf</groupId>
+        <artifactId>cellar</artifactId>
+        <version>4.0.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <groupId>org.apache.karaf.cellar</groupId>
+    <artifactId>http</artifactId>
+    <packaging>pom</packaging>
+    <name>Apache Karaf :: Cellar :: HTTP</name>
+
+    <modules>
+        <module>balancer</module>
+        <!-- <module>session-replicator</module> -->
+    </modules>
+
+</project>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/karaf-cellar/blob/77b8daec/manual/src/main/webapp/manual.conf
----------------------------------------------------------------------
diff --git a/manual/src/main/webapp/manual.conf 
b/manual/src/main/webapp/manual.conf
index a910a60..c239705 100644
--- a/manual/src/main/webapp/manual.conf
+++ b/manual/src/main/webapp/manual.conf
@@ -36,6 +36,8 @@ h1. User Guide
 {include:user-guide/groups.conf}
 {include:user-guide/obr.conf}
 {include:user-guide/event.conf}
+{include:user-guide/http-balancer.conf}
+{include:user-guide/transport.conf}
 {include:user-guide/cloud.conf}
 
 h1. Architecture Guide

http://git-wip-us.apache.org/repos/asf/karaf-cellar/blob/77b8daec/manual/src/main/webapp/user-guide/http-balancer.conf
----------------------------------------------------------------------
diff --git a/manual/src/main/webapp/user-guide/http-balancer.conf 
b/manual/src/main/webapp/user-guide/http-balancer.conf
new file mode 100644
index 0000000..5673b40
--- /dev/null
+++ b/manual/src/main/webapp/user-guide/http-balancer.conf
@@ -0,0 +1,131 @@
+h1. HTTP Balancer
+
+Apache Karaf Cellar is able to expose servlets local to a node on the cluster.
+It means that a client (browser) can use any node in the cluster, proxying the 
requests to the node actually
+hosting the servlets.
+
+h2. Enable HTTP Balancer
+
+To enable Cellar HTTP Balancer, you have to first install the http and 
http-whiteboard features:
+
+{code}
+karaf@root()> feature:install http
+karaf@root()> feature:install http-whiteboard
+{code}
+
+Now, we install the cellar-http-balancer feature, actually providing the 
balancer:
+
+{code}
+karaf@root()> feature:install cellar-http-balancer
+{code}
+
+Of course, you can use Cellar to spread the installation of the 
cellar-http-balancer feature on all nodes in the
+cluster group:
+
+{code}
+karaf@root()> cluster:feature-install default cellar-http-balancer
+{code}
+
+It's done: the Cellar HTTP Balancer is now enabled. It will expose proxy 
servlets on nodes.
+
+h2. Balancer in action
+
+To illustrate Cellar HTTP Balancer in action, you need at least a cluster with 
two nodes.
+
+On node1, we enable the Cellar HTTP Balancer:
+
+{code}
+karaf@node1()> feature:install http
+karaf@node1()> feature:install http-whiteboard
+karaf@node1()> feature:repo-add cellar 4.0.0
+karaf@node1()> feature:install cellar
+karaf@node1()> cluster:feature-install default cellar-http-balancer
+{code}
+
+Now, we install the webconsole on node1:
+
+{code}
+karaf@node1()> feature:install webconsole
+{code}
+
+We can see the "local" servlets provided by the webconsole feature using the 
http:list command:
+
+{code}
+karaf@node1()> http:list
+ID  | Servlet          | Servlet-Name    | State       | Alias               | 
Url
+------------------------------------------------------------------------------------------------------
+101 | KarafOsgiManager | ServletModel-2  | Undeployed  | /system/console     | 
[/system/console/*]
+103 | GogoPlugin       | ServletModel-7  | Deployed    | /gogo               | 
[/gogo/*]
+102 | FeaturesPlugin   | ServletModel-6  | Deployed    | /features           | 
[/features/*]
+101 | ResourceServlet  | /res            | Deployed    | /system/console/res | 
[/system/console/res/*]
+101 | KarafOsgiManager | ServletModel-11 | Deployed    | /system/console     | 
[/system/console/*]
+105 | InstancePlugin   | ServletModel-9  | Deployed    | /instance           | 
[/instance/*]
+{code}
+
+You can access to the webconsole using a browser on 
http://localhost:8181/system/console.
+
+We can see that Cellar HTTP Balancer exposed the servlets to the cluster, 
using the cluster:http-list command:
+
+{code}
+karaf@node1()> cluster:http-list default
+Alias               | Locations
+-----------------------------------------------------------------
+/system/console/res | http://172.17.42.1:8181/system/console/res
+/gogo               | http://172.17.42.1:8181/gogo
+/instance           | http://172.17.42.1:8181/instance
+/system/console     | http://172.17.42.1:8181/system/console
+/features           | http://172.17.42.1:8181/features
+{code}
+
+
+On another node (node2), we install http, http-whiteboard and cellar features:
+
+{code}
+karaf@node1()> feature:install http
+karaf@node1()> feature:install http-whiteboard
+karaf@node1()> feature:repo-add cellar 4.0.0
+karaf@node1()> feature:install cellar
+{code}
+
+WARNING: if you run the nodes on a single machine, you have to provision 
etc/org.ops4j.pax.web.cfg configuration file
+containing the org.osgi.service.http.port property with a port number 
different to 8181.
+For this example, I use the following etc/org.ops4j.pax.web.cfg file:
+
+{code}
+org.osgi.service.http.port=8041
+{code}
+
+On node1, as we installed the cellar-http-balancer using 
cluster:feature-install command, it's automatically installed
+when node2 joins the default cluster group.
+
+We can see the HTTP endpoints available on the cluster using the 
cluster:http-list command:
+
+{code}
+karaf@node2()> cluster:http-list default
+Alias               | Locations
+-----------------------------------------------------------------
+/system/console/res | http://172.17.42.1:8181/system/console/res
+/gogo               | http://172.17.42.1:8181/gogo
+/instance           | http://172.17.42.1:8181/instance
+/system/console     | http://172.17.42.1:8181/system/console
+/features           | http://172.17.42.1:8181/features
+{code}
+
+If we take a look on the HTTP endpoints locally available on node2 (using 
http:list command), we can see the proxies
+created by Cellar HTTP Balancer:
+
+{code}
+karaf@node2()> http:list
+ID  | Servlet                    | Servlet-Name   | State       | Alias        
       | Url
+---------------------------------------------------------------------------------------------------------------
+100 | CellarBalancerProxyServlet | ServletModel-3 | Deployed    | /gogo        
       | [/gogo/*]
+100 | CellarBalancerProxyServlet | ServletModel-2 | Deployed    | 
/system/console/res | [/system/console/res/*]
+100 | CellarBalancerProxyServlet | ServletModel-6 | Deployed    | /features    
       | [/features/*]
+100 | CellarBalancerProxyServlet | ServletModel-5 | Deployed    | 
/system/console     | [/system/console/*]
+100 | CellarBalancerProxyServlet | ServletModel-4 | Deployed    | /instance    
       | [/instance/*]
+{code}
+
+You can use a browser on http://localhost:8041/system/console: you will 
actually use the webconsole from node1, as
+Cellar HTTP Balancer proxies from node2 to node1.
+
+Cellar HTTP Balancer randomly chooses one endpoint providing the HTTP endpoint.
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/karaf-cellar/blob/77b8daec/manual/src/main/webapp/user-guide/index.conf
----------------------------------------------------------------------
diff --git a/manual/src/main/webapp/user-guide/index.conf 
b/manual/src/main/webapp/user-guide/index.conf
index e568b51..0e7fd5d 100644
--- a/manual/src/main/webapp/user-guide/index.conf
+++ b/manual/src/main/webapp/user-guide/index.conf
@@ -8,5 +8,6 @@ h1. Karaf Cellar User Guide
 * [Groups in Karaf Cellar|/user-guide/groups]
 * [OBR in Karaf Cellar|/user-guide/obr]
 * [OSGi Event broadcast with Karaf Cellar|/user-guide/event]
+* [HTTP Balancer|/user-guide/http-balancer]
 * [DOSGi and Transport|/user-guide/transport]
 * [Discovery Services|/user-guide/cloud]

http://git-wip-us.apache.org/repos/asf/karaf-cellar/blob/77b8daec/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index b7e6059..73376d8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -64,6 +64,7 @@
         <module>shell</module>
         <module>hazelcast</module>
         <module>utils</module>
+        <module>http</module>
         <module>cloud</module>
         <module>kubernetes</module>
         <module>webconsole</module>

Reply via email to