FELIX-4782 : Implement session handling

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1659596 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/HttpServiceController.java b/http/base/src/main/java/org/apache/felix/http/base/internal/HttpServiceController.java
index 48d88da..81a3149 100644
--- a/http/base/src/main/java/org/apache/felix/http/base/internal/HttpServiceController.java
+++ b/http/base/src/main/java/org/apache/felix/http/base/internal/HttpServiceController.java
@@ -19,11 +19,15 @@
 import java.util.Hashtable;
 
 import javax.servlet.ServletContext;
+import javax.servlet.http.HttpSessionAttributeListener;
+import javax.servlet.http.HttpSessionEvent;
+import javax.servlet.http.HttpSessionListener;
 
 import org.apache.felix.http.api.ExtHttpService;
 import org.apache.felix.http.base.internal.dispatch.Dispatcher;
 import org.apache.felix.http.base.internal.handler.HandlerRegistry;
 import org.apache.felix.http.base.internal.handler.HttpServicePlugin;
+import org.apache.felix.http.base.internal.handler.HttpSessionWrapper;
 import org.apache.felix.http.base.internal.listener.HttpSessionAttributeListenerManager;
 import org.apache.felix.http.base.internal.listener.HttpSessionListenerManager;
 import org.apache.felix.http.base.internal.listener.ServletContextAttributeListenerManager;
@@ -116,12 +120,24 @@
         return requestAttributeListener;
     }
 
-    public HttpSessionListenerManager getSessionListener()
+    public HttpSessionListener getSessionListener()
     {
-        return sessionListener;
+        return new HttpSessionListener() {
+
+            @Override
+            public void sessionDestroyed(final HttpSessionEvent se) {
+                sessionListener.sessionDestroyed(se);
+                whiteboardHttpService.sessionDestroyed(se.getSession(), HttpSessionWrapper.getSessionContextIds(se.getSession()));
+            }
+
+            @Override
+            public void sessionCreated(final HttpSessionEvent se) {
+                sessionListener.sessionCreated(se);
+            }
+        };
     }
 
-    public HttpSessionAttributeListenerManager getSessionAttributeListener()
+    public HttpSessionAttributeListener getSessionAttributeListener()
     {
         return sessionAttributeListener;
     }
@@ -175,10 +191,12 @@
                 servletContext,
                 this.registry,
                 this.runtimeServiceReg.getReference());
+        this.dispatcher.setWhiteboardHttpService(this.whiteboardHttpService);
     }
 
     public void unregister()
     {
+        this.dispatcher.setWhiteboardHttpService(null);
         if ( this.whiteboardHttpService != null )
         {
             this.whiteboardHttpService.close();
diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/context/ExtServletContext.java b/http/base/src/main/java/org/apache/felix/http/base/internal/context/ExtServletContext.java
index fc7ec8b..ef9f9a0 100644
--- a/http/base/src/main/java/org/apache/felix/http/base/internal/context/ExtServletContext.java
+++ b/http/base/src/main/java/org/apache/felix/http/base/internal/context/ExtServletContext.java
@@ -21,8 +21,14 @@
 import javax.servlet.ServletContext;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSessionAttributeListener;
+import javax.servlet.http.HttpSessionListener;
 
 public interface ExtServletContext extends ServletContext
 {
     boolean handleSecurity(HttpServletRequest req, HttpServletResponse res) throws IOException;
+
+    HttpSessionAttributeListener getHttpSessionAttributeListener();
+
+    HttpSessionListener getHttpSessionListener();
 }
diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/context/ServletContextImpl.java b/http/base/src/main/java/org/apache/felix/http/base/internal/context/ServletContextImpl.java
index 3eba855..23b1a24 100644
--- a/http/base/src/main/java/org/apache/felix/http/base/internal/context/ServletContextImpl.java
+++ b/http/base/src/main/java/org/apache/felix/http/base/internal/context/ServletContextImpl.java
@@ -41,6 +41,8 @@
 import javax.servlet.descriptor.JspConfigDescriptor;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSessionAttributeListener;
+import javax.servlet.http.HttpSessionListener;
 
 import org.apache.felix.http.base.internal.logger.SystemLogger;
 import org.apache.felix.http.base.internal.util.MimeTypes;
@@ -380,6 +382,18 @@
     }
 
     @Override
+    public HttpSessionListener getHttpSessionListener()
+    {
+        return null;
+    }
+
+    @Override
+    public HttpSessionAttributeListener getHttpSessionAttributeListener()
+    {
+        return null;
+    }
+
+    @Override
     public boolean handleSecurity(HttpServletRequest req, HttpServletResponse res) throws IOException
     {
         return this.httpContext.handleSecurity(req, res);
diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/dispatch/Dispatcher.java b/http/base/src/main/java/org/apache/felix/http/base/internal/dispatch/Dispatcher.java
index cc265f5..c657215 100644
--- a/http/base/src/main/java/org/apache/felix/http/base/internal/dispatch/Dispatcher.java
+++ b/http/base/src/main/java/org/apache/felix/http/base/internal/dispatch/Dispatcher.java
@@ -31,6 +31,7 @@
 import static org.apache.felix.http.base.internal.util.UriUtils.removeDotSegments;
 
 import java.io.IOException;
+import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import javax.servlet.DispatcherType;
@@ -53,6 +54,7 @@
 import org.apache.felix.http.base.internal.handler.HttpSessionWrapper;
 import org.apache.felix.http.base.internal.handler.ServletHandler;
 import org.apache.felix.http.base.internal.util.UriUtils;
+import org.apache.felix.http.base.internal.whiteboard.WhiteboardHttpService;
 import org.osgi.service.http.HttpContext;
 import org.osgi.service.useradmin.Authorization;
 
@@ -452,7 +454,12 @@
             {
                 return null;
             }
-            return new HttpSessionWrapper(this.contextId, session, this.servletContext);
+            // check if internal session is available
+            if ( !create && !HttpSessionWrapper.hasSession(this.contextId, session) )
+            {
+                return null;
+            }
+            return new HttpSessionWrapper(this.contextId, session, this.servletContext, false);
         }
 
         @Override
@@ -516,11 +523,18 @@
 
     private final HandlerRegistry handlerRegistry;
 
-    public Dispatcher(HandlerRegistry handlerRegistry)
+    private WhiteboardHttpService whiteboardService;
+
+    public Dispatcher(final HandlerRegistry handlerRegistry)
     {
         this.handlerRegistry = handlerRegistry;
     }
 
+    public void setWhiteboardHttpService(final WhiteboardHttpService service)
+    {
+        this.whiteboardService = service;
+    }
+
     /**
      * Responsible for dispatching a given request to the actual applicable servlet and/or filters in the local registry.
      *
@@ -531,6 +545,13 @@
      */
     public void dispatch(final HttpServletRequest req, final HttpServletResponse res) throws ServletException, IOException
     {
+        // invalid sessions first
+        final HttpSession session = req.getSession(false);
+        if ( session != null )
+        {
+            final Set<Long> ids = HttpSessionWrapper.getExpiredSessionContextIds(session);
+            this.whiteboardService.sessionDestroyed(session, ids);
+        }
         String requestURI = getRequestURI(req);
 
         // Determine which servlets we should forward the request to...
diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/handler/HttpSessionWrapper.java b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/HttpSessionWrapper.java
index 2573cca..346c53c 100644
--- a/http/base/src/main/java/org/apache/felix/http/base/internal/handler/HttpSessionWrapper.java
+++ b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/HttpSessionWrapper.java
@@ -19,70 +19,184 @@
 
 package org.apache.felix.http.base.internal.handler;
 
+import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Enumeration;
-import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
 import java.util.NoSuchElementException;
+import java.util.Set;
 
 import javax.servlet.ServletContext;
 import javax.servlet.http.HttpSession;
+import javax.servlet.http.HttpSessionBindingEvent;
+import javax.servlet.http.HttpSessionBindingListener;
 import javax.servlet.http.HttpSessionContext;
+import javax.servlet.http.HttpSessionEvent;
+
+import org.apache.felix.http.base.internal.context.ExtServletContext;
 
 /**
+ * The session wrapper keeps track of the internal session, manages their attributes
+ * separately and also handles session timeout.
+ *
  * @author <a href="mailto:dev@felix.apache.org">Felix Project Team</a>
  */
-@SuppressWarnings("deprecation")
 public class HttpSessionWrapper implements HttpSession
 {
+    /** All special attributes are prefixed with this prefix. */
     private static final String PREFIX = "org.apache.felix.http.session.context.";
 
-    private static final String SESSION_MAP = "org.apache.felix.http.session.context.map";
+    /** For each internal session, the attributes are prefixed with this followed by the context id */
+    private static final String ATTR_PREFIX = PREFIX + "attr.";
 
+    /** The created time for the internal session (appended with context id) */
+    private static final String ATTR_CREATED = PREFIX + "created.";
+
+    /** The last accessed time for the internal session (appended with context id) */
+    private static final String ATTR_LAST_ACCESSED = PREFIX + "lastaccessed.";
+
+    /** The max inactive time (appended with context id) */
+    private static final String ATTR_MAX_INACTIVE = PREFIX + "maxinactive.";
+
+    /** The underlying container session. */
     private final HttpSession delegate;
-    private final ServletContext context;
 
+    /** The corresponding servlet context. */
+    private final ExtServletContext context;
+
+    /** The id for this session. */
+    private final String sessionId;
+
+    /** The key prefix for attributes belonging to this session. */
     private final String keyPrefix;
 
+    /** Flag to handle the validity of this session. */
     private volatile boolean isInvalid = false;
 
+    /** The time this has been created. */
     private final long created;
 
-    private final long contextId;
+    /** The time this has been last accessed. */
+    private final long lastAccessed;
+
+    /** The max timeout interval. */
+    private int maxTimeout;
+
+    /**
+     * Is this a new session?
+     */
+    private final boolean isNew;
+
+    public static boolean hasSession(final Long contextId, final HttpSession session)
+    {
+        final String sessionId = contextId == null ? "0" : String.valueOf(contextId);
+        return session.getAttribute(ATTR_CREATED + sessionId) != null;
+    }
+
+    public static Set<Long> getExpiredSessionContextIds(final HttpSession session)
+    {
+        final long now = System.currentTimeMillis();
+        final Set<Long> ids = new HashSet<Long>();
+        final Enumeration<String> names = session.getAttributeNames();
+        while ( names.hasMoreElements() )
+        {
+            final String name = names.nextElement();
+            if ( name.startsWith(ATTR_LAST_ACCESSED) )
+            {
+                final String id = name.substring(ATTR_LAST_ACCESSED.length());
+
+                final long lastAccess = (Long)session.getAttribute(name);
+                final Integer maxTimeout = (Integer)session.getAttribute(ATTR_MAX_INACTIVE + id);
+
+                if ( lastAccess + maxTimeout < now )
+                {
+                    ids.add(Long.valueOf(id));
+                }
+            }
+        }
+        return ids;
+    }
+
+    public static Set<Long> getSessionContextIds(final HttpSession session)
+    {
+        final Set<Long> ids = new HashSet<Long>();
+        final Enumeration<String> names = session.getAttributeNames();
+        while ( names.hasMoreElements() )
+        {
+            final String name = names.nextElement();
+            if ( name.startsWith(ATTR_LAST_ACCESSED) )
+            {
+                final String id = name.substring(ATTR_LAST_ACCESSED.length());
+                ids.add(Long.valueOf(id));
+            }
+        }
+        return ids;
+    }
 
     /**
      * Creates a new {@link HttpSessionWrapper} instance.
      */
-    public HttpSessionWrapper(final Long contextId, final HttpSession session,
-            final ServletContext context)
+    public HttpSessionWrapper(final Long contextId,
+            final HttpSession session,
+            final ExtServletContext context,
+            final boolean terminate)
     {
         this.delegate = session;
         this.context = context;
-        this.contextId = (contextId == null ? 0 : contextId);
-        this.keyPrefix = this.contextId == 0 ? null : PREFIX + String.valueOf(contextId) + ".";
-        synchronized ( this.getClass() )
+        this.sessionId = contextId == null ? "0" : String.valueOf(contextId);
+        this.keyPrefix = contextId == null ? null : ATTR_PREFIX + this.sessionId + ".";
+
+        if ( this.keyPrefix != null )
         {
-            @SuppressWarnings("unchecked")
-            Map<Long, Long> sessionMap = (Map<Long, Long>)session.getAttribute(SESSION_MAP);
-            if ( sessionMap == null )
+            final long now = System.currentTimeMillis();
+            if ( session.getAttribute(ATTR_CREATED + this.sessionId) == null )
             {
-                sessionMap = new HashMap<Long, Long>();
+                this.created = now;
+                this.maxTimeout = session.getMaxInactiveInterval();
+                isNew = true;
+
+                session.setAttribute(ATTR_CREATED + this.sessionId, this.created);
+                session.setAttribute(ATTR_MAX_INACTIVE + this.sessionId, this.maxTimeout);
+
+                if ( context.getHttpSessionListener() != null )
+                {
+                    context.getHttpSessionListener().sessionCreated(new HttpSessionEvent(this));
+                }
             }
-            if ( !sessionMap.containsKey(contextId) )
+            else
             {
-                sessionMap.put(contextId, System.currentTimeMillis());
-                session.setAttribute(SESSION_MAP, sessionMap);
+                this.created = (Long)session.getAttribute(ATTR_CREATED + this.sessionId);
+                this.maxTimeout = (Integer)session.getAttribute(ATTR_MAX_INACTIVE + this.sessionId);
+                isNew = false;
             }
-            this.created = sessionMap.get(contextId);
+
+            this.lastAccessed = now;
+            if ( !terminate )
+            {
+                session.setAttribute(ATTR_LAST_ACCESSED + this.sessionId, this.lastAccessed);
+            }
+        }
+        else
+        {
+            this.isNew = session.isNew();
+            this.lastAccessed = session.getLastAccessedTime();
+            this.created = session.getCreationTime();
         }
     }
 
+    /**
+     * Helper method to get the real key within the real session.
+     */
     private String getKey(final String name)
     {
         return this.keyPrefix == null ? name : this.keyPrefix.concat(name);
     }
 
+    /**
+     * Check whether this session is still valid.
+     * @throws IllegalStateException if session is not valid anymore
+     */
     private void checkInvalid()
     {
         if ( this.isInvalid )
@@ -95,7 +209,12 @@
     public Object getAttribute(final String name)
     {
         this.checkInvalid();
-        return this.delegate.getAttribute(this.getKey(name));
+        Object result = this.delegate.getAttribute(this.getKey(name));
+        if ( result instanceof SessionBindingValueListenerWrapper )
+        {
+            result = ((SessionBindingValueListenerWrapper)result).getHttpSessionBindingListener();
+        }
+        return result;
     }
 
     @Override
@@ -112,7 +231,7 @@
                 while ( e.hasMoreElements() )
                 {
                     final String name = e.nextElement();
-                    if ( keyPrefix == null )
+                    if ( keyPrefix == null && !name.startsWith(PREFIX) )
                     {
                         return name;
                     }
@@ -154,37 +273,31 @@
     public String getId()
     {
         this.checkInvalid();
-        return this.delegate.getId() + "-" + String.valueOf(this.contextId);
+        return this.delegate.getId() + "-" + this.sessionId;
     }
 
     @Override
     public long getLastAccessedTime()
     {
         this.checkInvalid();
-        // TODO
-        return this.delegate.getLastAccessedTime();
+        return this.lastAccessed;
     }
 
     @Override
     public int getMaxInactiveInterval()
     {
-        // TODO
-        return this.delegate.getMaxInactiveInterval();
+        // no validity check conforming to the javadocs
+        return this.maxTimeout;
     }
 
     @Override
     public ServletContext getServletContext()
     {
+        // no validity check conforming to the javadocs
         return this.context;
     }
 
     @Override
-    public HttpSessionContext getSessionContext()
-    {
-        return this.delegate.getSessionContext();
-    }
-
-    @Override
     public Object getValue(String name)
     {
         return this.getAttribute(name);
@@ -206,16 +319,43 @@
     public void invalidate()
     {
         this.checkInvalid();
+
+        if ( this.keyPrefix != null )
+        {
+            this.delegate.removeAttribute(ATTR_CREATED + this.sessionId);
+            this.delegate.removeAttribute(ATTR_LAST_ACCESSED + this.sessionId);
+            this.delegate.removeAttribute(ATTR_MAX_INACTIVE + this.sessionId);
+
+            final Enumeration<String> names = this.delegate.getAttributeNames();
+            while ( names.hasMoreElements() )
+            {
+                final String name = names.nextElement();
+
+                if ( name.startsWith(this.keyPrefix) ) {
+                    this.removeAttribute(name.substring(this.keyPrefix.length()));
+                }
+            }
+        }
+
+        // if the session is empty we can invalidate
+        final Enumeration<String> names = this.delegate.getAttributeNames();
+        if ( !names.hasMoreElements() )
+        {
+            this.delegate.invalidate();
+        }
+
+        if ( context.getHttpSessionListener() != null )
+        {
+            context.getHttpSessionListener().sessionDestroyed(new HttpSessionEvent(this));
+        }
         this.isInvalid = true;
-        // TODO
-        this.delegate.invalidate();
     }
 
     @Override
     public boolean isNew()
     {
         this.checkInvalid();
-        return this.delegate.isNew();
+        return this.isNew;
     }
 
     @Override
@@ -228,26 +368,97 @@
     public void removeAttribute(final String name)
     {
         this.checkInvalid();
-        this.delegate.removeAttribute(name);
+        final Object oldValue = this.getAttribute(name);
+        if ( oldValue != null )
+        {
+            this.delegate.removeAttribute(this.getKey(name));
+            if ( this.keyPrefix != null && oldValue instanceof HttpSessionBindingListener )
+            {
+                ((HttpSessionBindingListener)oldValue).valueUnbound(new HttpSessionBindingEvent(this, name));
+            }
+            if ( this.context.getHttpSessionAttributeListener() != null )
+            {
+                this.context.getHttpSessionAttributeListener().attributeRemoved(new HttpSessionBindingEvent(this, name, oldValue));
+            }
+        }
     }
 
     @Override
     public void removeValue(final String name)
     {
-        this.removeAttribute(this.getKey(name));
+        this.removeAttribute(name);
     }
 
     @Override
     public void setAttribute(final String name, final Object value)
     {
         this.checkInvalid();
-        this.delegate.setAttribute(this.getKey(name), value);
+        final Object oldValue = this.getAttribute(name);
+        // wrap http session binding listener to avoid container calling it!
+        if ( this.keyPrefix != null && value instanceof HttpSessionBindingListener )
+        {
+            this.delegate.setAttribute(this.getKey(name),
+                    new SessionBindingValueListenerWrapper((HttpSessionBindingListener)value));
+        }
+        else
+        {
+            this.delegate.setAttribute(this.getKey(name), value);
+        }
+        if ( this.keyPrefix != null && value instanceof HttpSessionBindingListener )
+        {
+            ((HttpSessionBindingListener)value).valueBound(new HttpSessionBindingEvent(this, name));
+        }
+
+        if ( this.context.getHttpSessionAttributeListener() != null )
+        {
+            if ( oldValue != null )
+            {
+                this.context.getHttpSessionAttributeListener().attributeReplaced(new HttpSessionBindingEvent(this, name, oldValue));
+            }
+            else
+            {
+                this.context.getHttpSessionAttributeListener().attributeAdded(new HttpSessionBindingEvent(this, name, value));
+            }
+        }
     }
 
     @Override
-    public void setMaxInactiveInterval(int interval)
+    public void setMaxInactiveInterval(final int interval)
     {
-        // TODO
-        this.delegate.setMaxInactiveInterval(interval);
+        if ( this.delegate.getMaxInactiveInterval() < interval )
+        {
+            this.delegate.setMaxInactiveInterval(interval);
+        }
+        if ( this.keyPrefix != null )
+        {
+            this.maxTimeout = interval;
+            this.delegate.setAttribute(ATTR_MAX_INACTIVE + this.sessionId, interval);
+        }
+    }
+
+    @Override
+    @SuppressWarnings("deprecation")
+    public HttpSessionContext getSessionContext()
+    {
+        // no need to check validity conforming to the javadoc
+        return this.delegate.getSessionContext();
+    }
+
+    private static final class SessionBindingValueListenerWrapper implements Serializable
+    {
+
+        private static final long serialVersionUID = 4009563108883768425L;
+
+        private final HttpSessionBindingListener listener;
+
+        public SessionBindingValueListenerWrapper(final HttpSessionBindingListener listener)
+        {
+            this.listener = listener;
+        }
+
+        public HttpSessionBindingListener getHttpSessionBindingListener()
+        {
+            return listener;
+        }
     }
 }
diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/ContextHandler.java b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/ContextHandler.java
index fed1f4b..af5bc80 100644
--- a/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/ContextHandler.java
+++ b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/ContextHandler.java
@@ -18,7 +18,7 @@
 
 import java.util.HashMap;
 import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentSkipListMap;
 
 import javax.annotation.Nonnull;
 import javax.servlet.ServletContext;
@@ -26,13 +26,20 @@
 import javax.servlet.ServletContextAttributeListener;
 import javax.servlet.ServletContextEvent;
 import javax.servlet.ServletContextListener;
+import javax.servlet.http.HttpSessionAttributeListener;
+import javax.servlet.http.HttpSessionBindingEvent;
+import javax.servlet.http.HttpSessionEvent;
+import javax.servlet.http.HttpSessionListener;
 
 import org.apache.felix.http.base.internal.context.ExtServletContext;
+import org.apache.felix.http.base.internal.runtime.HttpSessionAttributeListenerInfo;
+import org.apache.felix.http.base.internal.runtime.HttpSessionListenerInfo;
 import org.apache.felix.http.base.internal.runtime.ServletContextAttributeListenerInfo;
 import org.apache.felix.http.base.internal.runtime.ServletContextHelperInfo;
 import org.apache.felix.http.base.internal.runtime.ServletContextListenerInfo;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.ServiceObjects;
+import org.osgi.framework.ServiceReference;
 import org.osgi.service.http.context.ServletContextHelper;
 
 public final class ContextHandler implements Comparable<ContextHandler>
@@ -50,7 +57,16 @@
     private final Map<Long, ServletContextListener> listeners = new HashMap<Long, ServletContextListener>();
 
     /** Servlet context attribute listeners. */
-    private final Map<Long, ServletContextAttributeListener> contextAttributeListeners = new ConcurrentHashMap<Long, ServletContextAttributeListener>();
+    private final Map<ServiceReference<ServletContextAttributeListener>, ServletContextAttributeListener> contextAttributeListeners =
+                      new ConcurrentSkipListMap<ServiceReference<ServletContextAttributeListener>, ServletContextAttributeListener>();
+
+    /** Session attribute listeners. */
+    private final Map<ServiceReference<HttpSessionAttributeListener>, HttpSessionAttributeListener> sessionAttributeListeners =
+            new ConcurrentSkipListMap<ServiceReference<HttpSessionAttributeListener>, HttpSessionAttributeListener>();
+
+    /** Session listeners. */
+    private final Map<ServiceReference<HttpSessionListener>, HttpSessionListener> sessionListeners =
+            new ConcurrentSkipListMap<ServiceReference<HttpSessionListener>, HttpSessionListener>();
 
     /** The http bundle. */
     private final Bundle bundle;
@@ -70,32 +86,7 @@
                 info.getName(),
                 info.getPrefix(),
                 info.getInitParameters(),
-                new ServletContextAttributeListener() {
-
-                    @Override
-                    public void attributeReplaced(final ServletContextAttributeEvent event) {
-                        for(final ServletContextAttributeListener l : contextAttributeListeners.values())
-                        {
-                            l.attributeReplaced(event);
-                        }
-                    }
-
-                    @Override
-                    public void attributeRemoved(final ServletContextAttributeEvent event) {
-                        for(final ServletContextAttributeListener l : contextAttributeListeners.values())
-                        {
-                            l.attributeReplaced(event);
-                        }
-                    }
-
-                    @Override
-                    public void attributeAdded(final ServletContextAttributeEvent event) {
-                        for(final ServletContextAttributeListener l : contextAttributeListeners.values())
-                        {
-                            l.attributeReplaced(event);
-                        }
-                    }
-                });
+                getContextAttributeListener());
     }
 
     /**
@@ -175,7 +166,9 @@
                     holder.servletContextHelper = so.getService();
                     holder.servletContext = new PerBundleServletContextImpl(bundle,
                             this.sharedContext,
-                            holder.servletContextHelper);
+                            holder.servletContextHelper,
+                            this.getSessionListener(),
+                            this.getSessionAttributeListener());
                     this.contextMap.put(key, holder);
                 }
             }
@@ -219,7 +212,7 @@
             final  ServletContextAttributeListener service = bundle.getBundleContext().getServiceObjects(info.getServiceReference()).getService();
             if ( service != null )
             {
-                this.contextAttributeListeners.put(info.getServiceId(), service);
+                this.contextAttributeListeners.put(info.getServiceReference(), service);
             }
         }
     }
@@ -230,7 +223,7 @@
      */
     public void removeListener(@Nonnull final ServletContextAttributeListenerInfo info)
     {
-        final  ServletContextAttributeListener service = this.contextAttributeListeners.remove(info.getServiceId());
+        final  ServletContextAttributeListener service = this.contextAttributeListeners.remove(info.getServiceReference());
         if ( service != null )
         {
             final ServiceObjects<ServletContextAttributeListener> so =  bundle.getBundleContext().getServiceObjects(info.getServiceReference());
@@ -240,6 +233,72 @@
         }
     }
 
+    /**
+     * Add session attribute listener
+     * @param info
+     */
+    public void addListener(@Nonnull final HttpSessionAttributeListenerInfo info)
+    {
+        final ServiceObjects<HttpSessionAttributeListener> so =  bundle.getBundleContext().getServiceObjects(info.getServiceReference());
+        if ( so != null )
+        {
+            final  HttpSessionAttributeListener service = bundle.getBundleContext().getServiceObjects(info.getServiceReference()).getService();
+            if ( service != null )
+            {
+                this.sessionAttributeListeners.put(info.getServiceReference(), service);
+            }
+        }
+    }
+
+    /**
+     * Remove session attribute listener
+     * @param info
+     */
+    public void removeListener(@Nonnull final HttpSessionAttributeListenerInfo info)
+    {
+        final  HttpSessionAttributeListener service = this.sessionAttributeListeners.remove(info.getServiceReference());
+        if ( service != null )
+        {
+            final ServiceObjects<HttpSessionAttributeListener> so =  bundle.getBundleContext().getServiceObjects(info.getServiceReference());
+            if ( so != null ) {
+                so.ungetService(service);
+            }
+        }
+    }
+
+    /**
+     * Add session listener
+     * @param info
+     */
+    public void addListener(@Nonnull final HttpSessionListenerInfo info)
+    {
+        final ServiceObjects<HttpSessionListener> so =  bundle.getBundleContext().getServiceObjects(info.getServiceReference());
+        if ( so != null )
+        {
+            final  HttpSessionListener service = bundle.getBundleContext().getServiceObjects(info.getServiceReference()).getService();
+            if ( service != null )
+            {
+                this.sessionListeners.put(info.getServiceReference(), service);
+            }
+        }
+    }
+
+    /**
+     * Remove session listener
+     * @param info
+     */
+    public void removeListener(@Nonnull final HttpSessionListenerInfo info)
+    {
+        final  HttpSessionListener service = this.sessionListeners.remove(info.getServiceReference());
+        if ( service != null )
+        {
+            final ServiceObjects<HttpSessionListener> so =  bundle.getBundleContext().getServiceObjects(info.getServiceReference());
+            if ( so != null ) {
+                so.ungetService(service);
+            }
+        }
+    }
+
     private static final class ContextHolder
     {
         public long counter;
@@ -247,4 +306,85 @@
         public ServletContextHelper servletContextHelper;
     }
 
+    public HttpSessionAttributeListener getSessionAttributeListener()
+    {
+        return new HttpSessionAttributeListener() {
+
+            @Override
+            public void attributeReplaced(final HttpSessionBindingEvent event) {
+                for(final HttpSessionAttributeListener l : sessionAttributeListeners.values())
+                {
+                    l.attributeReplaced(event);
+                }
+            }
+
+            @Override
+            public void attributeRemoved(final HttpSessionBindingEvent event) {
+                for(final HttpSessionAttributeListener l : sessionAttributeListeners.values())
+                {
+                    l.attributeReplaced(event);
+                }
+            }
+
+            @Override
+            public void attributeAdded(final HttpSessionBindingEvent event) {
+                for(final HttpSessionAttributeListener l : sessionAttributeListeners.values())
+                {
+                    l.attributeReplaced(event);
+                }
+            }
+        };
+    }
+
+    private ServletContextAttributeListener getContextAttributeListener()
+    {
+        return new ServletContextAttributeListener() {
+
+            @Override
+            public void attributeReplaced(final ServletContextAttributeEvent event) {
+                for(final ServletContextAttributeListener l : contextAttributeListeners.values())
+                {
+                    l.attributeReplaced(event);
+                }
+            }
+
+            @Override
+            public void attributeRemoved(final ServletContextAttributeEvent event) {
+                for(final ServletContextAttributeListener l : contextAttributeListeners.values())
+                {
+                    l.attributeReplaced(event);
+                }
+            }
+
+            @Override
+            public void attributeAdded(final ServletContextAttributeEvent event) {
+                for(final ServletContextAttributeListener l : contextAttributeListeners.values())
+                {
+                    l.attributeReplaced(event);
+                }
+            }
+        };
+    }
+
+    public HttpSessionListener getSessionListener()
+    {
+        return new HttpSessionListener() {
+
+            @Override
+            public void sessionCreated(final HttpSessionEvent se) {
+                for(final HttpSessionListener l : sessionListeners.values())
+                {
+                    l.sessionCreated(se);
+                }
+            }
+
+            @Override
+            public void sessionDestroyed(final HttpSessionEvent se) {
+                for(final HttpSessionListener l : sessionListeners.values())
+                {
+                    l.sessionDestroyed(se);
+                }
+            }
+        };
+    }
 }
diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/PerBundleServletContextImpl.java b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/PerBundleServletContextImpl.java
index 7fce2e4..e64db90 100644
--- a/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/PerBundleServletContextImpl.java
+++ b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/PerBundleServletContextImpl.java
@@ -37,6 +37,8 @@
 import javax.servlet.descriptor.JspConfigDescriptor;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSessionAttributeListener;
+import javax.servlet.http.HttpSessionListener;
 
 import org.apache.felix.http.base.internal.context.ExtServletContext;
 import org.osgi.framework.Bundle;
@@ -54,14 +56,20 @@
     private final Bundle bundle;
     private final ServletContext delegatee;
     private final ServletContextHelper contextHelper;
+    private final HttpSessionListener sessionListener;
+    private final HttpSessionAttributeListener sessionAttributeListener;
 
     public PerBundleServletContextImpl(final Bundle bundle,
             final ServletContext sharedContext,
-            final ServletContextHelper delegatee)
+            final ServletContextHelper delegatee,
+            final HttpSessionListener sessionListener,
+            final HttpSessionAttributeListener sessionAttributeListener)
     {
         this.bundle = bundle;
         this.delegatee = sharedContext;
         this.contextHelper = delegatee;
+        this.sessionListener = sessionListener;
+        this.sessionAttributeListener = sessionAttributeListener;
     }
 
     @Override
@@ -73,6 +81,18 @@
     }
 
     @Override
+    public HttpSessionListener getHttpSessionListener()
+    {
+        return this.sessionListener;
+    }
+
+    @Override
+    public HttpSessionAttributeListener getHttpSessionAttributeListener()
+    {
+        return this.sessionAttributeListener;
+    }
+
+    @Override
     public ClassLoader getClassLoader()
     {
         return this.bundle.adapt(BundleWiring.class).getClassLoader();
diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/ServletContextHelperManager.java b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/ServletContextHelperManager.java
index 6e623a3..2319dfe 100644
--- a/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/ServletContextHelperManager.java
+++ b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/ServletContextHelperManager.java
@@ -24,13 +24,17 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.TreeMap;
 
 import javax.annotation.Nonnull;
 import javax.servlet.ServletContext;
+import javax.servlet.ServletContextListener;
 
 import org.apache.felix.http.base.internal.logger.SystemLogger;
 import org.apache.felix.http.base.internal.runtime.AbstractInfo;
 import org.apache.felix.http.base.internal.runtime.FilterInfo;
+import org.apache.felix.http.base.internal.runtime.HttpSessionAttributeListenerInfo;
+import org.apache.felix.http.base.internal.runtime.HttpSessionListenerInfo;
 import org.apache.felix.http.base.internal.runtime.ResourceInfo;
 import org.apache.felix.http.base.internal.runtime.ServletContextAttributeListenerInfo;
 import org.apache.felix.http.base.internal.runtime.ServletContextHelperInfo;
@@ -134,8 +138,9 @@
 
         this.httpService.registerContext(handler);
 
-        // context listeners first
+        final Map<ServiceReference<ServletContextListener>, ServletContextListenerInfo> listeners = new TreeMap<ServiceReference<ServletContextListener>, ServletContextListenerInfo>();
         final List<WhiteboardServiceInfo<?>> services = new ArrayList<WhiteboardServiceInfo<?>>();
+
         for(final Map.Entry<WhiteboardServiceInfo<?>, List<ContextHandler>> entry : this.servicesMap.entrySet())
         {
             if ( entry.getKey().getContextSelectionFilter().match(handler.getContextInfo().getServiceReference()) )
@@ -143,7 +148,8 @@
                 entry.getValue().add(handler);
                 if ( entry.getKey() instanceof ServletContextListenerInfo )
                 {
-                    handler.initialized((ServletContextListenerInfo)entry.getKey());
+                    final ServletContextListenerInfo info = (ServletContextListenerInfo)entry.getKey();
+                    listeners.put(info.getServiceReference(), info);
                 }
                 else
                 {
@@ -151,6 +157,11 @@
                 }
             }
         }
+        // context listeners first
+        for(final ServletContextListenerInfo info : listeners.values())
+        {
+            handler.initialized(info);
+        }
         // now register services
         for(final WhiteboardServiceInfo<?> info : services)
         {
@@ -167,7 +178,7 @@
         this.httpService.unregisterContext(handler);
 
         // context listeners last
-        final List<ServletContextListenerInfo> listeners = new ArrayList<ServletContextListenerInfo>();
+        final Map<ServiceReference<ServletContextListener>, ServletContextListenerInfo> listeners = new TreeMap<ServiceReference<ServletContextListener>, ServletContextListenerInfo>();
         final Iterator<Map.Entry<WhiteboardServiceInfo<?>, List<ContextHandler>>> i = this.servicesMap.entrySet().iterator();
         while ( i.hasNext() )
         {
@@ -176,7 +187,8 @@
             {
                 if ( entry.getKey() instanceof ServletContextListenerInfo )
                 {
-                    listeners.add((ServletContextListenerInfo)entry.getKey());
+                    final ServletContextListenerInfo info = (ServletContextListenerInfo)entry.getKey();
+                    listeners.put(info.getServiceReference(), info);
                 }
                 else
                 {
@@ -187,7 +199,7 @@
                 }
             }
         }
-        for(final ServletContextListenerInfo info : listeners)
+        for(final ServletContextListenerInfo info : listeners.values())
         {
             handler.destroyed(info);
         }
@@ -373,6 +385,14 @@
         {
             handler.addListener((ServletContextAttributeListenerInfo)info );
         }
+        else if ( info instanceof HttpSessionAttributeListenerInfo )
+        {
+            handler.addListener((HttpSessionAttributeListenerInfo)info );
+        }
+        else if ( info instanceof HttpSessionListenerInfo )
+        {
+            handler.addListener((HttpSessionListenerInfo)info );
+        }
     }
 
     /**
@@ -398,6 +418,14 @@
         {
             handler.removeListener((ServletContextAttributeListenerInfo)info );
         }
+        else if ( info instanceof HttpSessionAttributeListenerInfo )
+        {
+            handler.removeListener((HttpSessionAttributeListenerInfo)info );
+        }
+        else if ( info instanceof HttpSessionListenerInfo )
+        {
+            handler.removeListener((HttpSessionListenerInfo)info );
+        }
     }
 
     /**
@@ -423,4 +451,20 @@
         }
         return true;
     }
+
+    public ContextHandler getContextHandler(final Long contextId)
+    {
+        synchronized ( this.contextMap )
+        {
+            for(final List<ContextHandler> handlerList : this.contextMap.values())
+            {
+                final ContextHandler h = handlerList.get(0);
+                if ( h.getContextInfo().getServiceId() == contextId )
+                {
+                    return h;
+                }
+            }
+        }
+        return null;
+    }
 }
diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/WhiteboardHttpService.java b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/WhiteboardHttpService.java
index e5f194c..c285ba9 100644
--- a/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/WhiteboardHttpService.java
+++ b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/WhiteboardHttpService.java
@@ -17,15 +17,19 @@
 package org.apache.felix.http.base.internal.whiteboard;
 
 import java.util.ArrayList;
+import java.util.Set;
 
 import javax.annotation.Nonnull;
 import javax.servlet.Filter;
 import javax.servlet.Servlet;
 import javax.servlet.ServletContext;
 import javax.servlet.ServletException;
+import javax.servlet.http.HttpSession;
 
+import org.apache.felix.http.base.internal.context.ExtServletContext;
 import org.apache.felix.http.base.internal.handler.FilterHandler;
 import org.apache.felix.http.base.internal.handler.HandlerRegistry;
+import org.apache.felix.http.base.internal.handler.HttpSessionWrapper;
 import org.apache.felix.http.base.internal.handler.ServletHandler;
 import org.apache.felix.http.base.internal.runtime.FilterInfo;
 import org.apache.felix.http.base.internal.runtime.ResourceInfo;
@@ -221,4 +225,18 @@
     {
         this.handlerRegistry.remove(contextHandler.getContextInfo());
     }
+
+    public void sessionDestroyed(@Nonnull final HttpSession session, final Set<Long> contextIds)
+    {
+        for(final Long contextId : contextIds)
+        {
+            final ContextHandler handler = this.contextManager.getContextHandler(contextId);
+            if ( handler != null )
+            {
+                final ExtServletContext context = handler.getServletContext(this.bundleContext.getBundle());
+                new HttpSessionWrapper(contextId, session, context, true).invalidate();
+                handler.ungetServletContext(this.bundleContext.getBundle());
+            }
+        }
+    }
 }