001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.session;
018
019import java.io.Serializable;
020import java.util.ArrayList;
021import java.util.Collections;
022import java.util.Enumeration;
023import java.util.List;
024import java.util.Set;
025import java.util.concurrent.CopyOnWriteArraySet;
026
027import javax.servlet.http.HttpServletRequest;
028import javax.servlet.http.HttpSession;
029import javax.servlet.http.HttpSessionBindingEvent;
030import javax.servlet.http.HttpSessionBindingListener;
031
032import org.apache.wicket.Application;
033import org.apache.wicket.Session;
034import org.apache.wicket.markup.MarkupParser;
035import org.apache.wicket.protocol.http.IRequestLogger;
036import org.apache.wicket.protocol.http.WebApplication;
037import org.apache.wicket.request.Request;
038import org.apache.wicket.request.http.WebRequest;
039import org.slf4j.Logger;
040import org.slf4j.LoggerFactory;
041
042/**
043 * Implementation of {@link ISessionStore} that works with web applications and provides some
044 * specific http servlet/ session related functionality.
045 * 
046 * @author jcompagner
047 * @author Eelco Hillenius
048 * @author Matej Knopp
049 */
050public class HttpSessionStore implements ISessionStore
051{
052        private static final Logger log = LoggerFactory.getLogger(HttpSessionStore.class);
053
054        private final Set<UnboundListener> unboundListeners = new CopyOnWriteArraySet<UnboundListener>();
055
056        private final Set<BindListener> bindListeners = new CopyOnWriteArraySet<BindListener>();
057
058        /**
059         * @param request The Wicket request
060         * @return The http servlet request
061         */
062        protected final HttpServletRequest getHttpServletRequest(final Request request)
063        {
064                Object containerRequest = request.getContainerRequest();
065                if (containerRequest == null || (containerRequest instanceof HttpServletRequest) == false)
066                {
067                        throw new IllegalArgumentException("Request must be ServletWebRequest");
068                }
069                return (HttpServletRequest)containerRequest;
070        }
071
072        /**
073         * 
074         * @see HttpServletRequest#getSession(boolean)
075         * 
076         * @param request
077         *            A Wicket request object
078         * @param create
079         *            If true, a session will be created if it is not existing yet
080         * @return The HttpSession associated with this request or null if {@code create} is false and
081         *         the {@code request} has no valid session
082         */
083        final HttpSession getHttpSession(final Request request, final boolean create)
084        {
085                return getHttpServletRequest(request).getSession(create);
086        }
087
088        @Override
089        public final void bind(final Request request, final Session newSession)
090        {
091                if (getWicketSession(request) != newSession)
092                {
093                        // call template method
094                        onBind(request, newSession);
095                        for (BindListener listener : getBindListeners())
096                        {
097                                listener.bindingSession(request, newSession);
098                        }
099
100                        HttpSession httpSession = getHttpSession(request, false);
101
102                        if (httpSession != null)
103                        {
104                                // register an unbinding listener for cleaning up
105                                String applicationKey = Application.get().getName();
106                                httpSession.setAttribute("Wicket:SessionUnbindingListener-" + applicationKey,
107                                        new SessionBindingListener(applicationKey, newSession));
108
109                                // register the session object itself
110                                setWicketSession(request, newSession);
111                        }
112                }
113        }
114
115        @Override
116        public void flushSession(Request request, Session session)
117        {
118                if (getWicketSession(request) != session)
119                {
120                        // this session is not yet bound, bind it
121                        bind(request, session);
122                }
123                else
124                {
125                        setWicketSession(request, session);
126                }
127        }
128
129        @Override
130        public void destroy()
131        {
132        }
133
134        @Override
135        public String getSessionId(final Request request, final boolean create)
136        {
137                String id = null;
138
139                HttpSession httpSession = getHttpSession(request, false);
140                if (httpSession != null)
141                {
142                        id = httpSession.getId();
143                }
144                else if (create)
145                {
146                        httpSession = getHttpSession(request, true);
147                        id = httpSession.getId();
148
149                        IRequestLogger logger = Application.get().getRequestLogger();
150                        if (logger != null)
151                        {
152                                logger.sessionCreated(id);
153                        }
154                }
155                return id;
156        }
157
158        @Override
159        public final void invalidate(final Request request)
160        {
161                HttpSession httpSession = getHttpSession(request, false);
162                if (httpSession != null)
163                {
164                        // tell the app server the session is no longer valid
165                        httpSession.invalidate();
166                }
167        }
168
169        @Override
170        public final Session lookup(final Request request)
171        {
172                String sessionId = getSessionId(request, false);
173                if (sessionId != null)
174                {
175                        return getWicketSession(request);
176                }
177                return null;
178        }
179
180        /**
181         * Reads the Wicket {@link Session} from the {@link HttpSession}'s attribute
182         *
183         * @param request The Wicket request
184         * @return The Wicket Session or {@code null}
185         */
186        protected Session getWicketSession(final Request request)
187        {
188                return (Session) getAttribute(request, Session.SESSION_ATTRIBUTE_NAME);
189        }
190
191        /**
192         * Stores the Wicket {@link Session} in an attribute in the {@link HttpSession}
193         *
194         * @param request The Wicket request
195         * @param session The Wicket session
196         */
197        protected void setWicketSession(final Request request, final Session session)
198        {
199                setAttribute(request, Session.SESSION_ATTRIBUTE_NAME, session);
200        }
201
202        /**
203         * Template method that is called when a session is being bound to the session store. It is
204         * called <strong>before</strong> the session object itself is added to this store (which is
205         * done by calling {@link ISessionStore#setAttribute(Request, String, Serializable)} with key
206         * {@link Session#SESSION_ATTRIBUTE_NAME}.
207         * 
208         * @param request
209         *            The request
210         * @param newSession
211         *            The new session
212         */
213        protected void onBind(final Request request, final Session newSession)
214        {
215        }
216
217        /**
218         * Template method that is called when the session is being detached from the store, which
219         * typically happens when the {@link HttpSession} was invalidated.
220         * 
221         * @param sessionId
222         *            The session id of the session that was invalidated.
223         */
224        protected void onUnbind(final String sessionId)
225        {
226        }
227
228        /**
229         * Gets the prefix for storing variables in the actual session (typically {@link HttpSession})
230         * for this application instance.
231         * 
232         * @param request
233         *            the request
234         * 
235         * @return the prefix for storing variables in the actual session
236         */
237        private String getSessionAttributePrefix(final Request request)
238        {
239                String sessionAttributePrefix = MarkupParser.WICKET;
240
241                if (request instanceof WebRequest)
242                {
243                        sessionAttributePrefix = WebApplication.get().getSessionAttributePrefix(
244                                (WebRequest)request, null);
245                }
246
247                return sessionAttributePrefix;
248        }
249
250        @Override
251        public final Serializable getAttribute(final Request request, final String name)
252        {
253                HttpSession httpSession = getHttpSession(request, false);
254                if (httpSession != null)
255                {
256                        return (Serializable)httpSession.getAttribute(getSessionAttributePrefix(request) + name);
257                }
258                return null;
259        }
260
261        @Override
262        public final List<String> getAttributeNames(final Request request)
263        {
264                List<String> list = new ArrayList<String>();
265                HttpSession httpSession = getHttpSession(request, false);
266                if (httpSession != null)
267                {
268                        @SuppressWarnings("unchecked")
269                        final Enumeration<String> names = httpSession.getAttributeNames();
270                        final String prefix = getSessionAttributePrefix(request);
271                        while (names.hasMoreElements())
272                        {
273                                final String name = names.nextElement();
274                                if (name.startsWith(prefix))
275                                {
276                                        list.add(name.substring(prefix.length()));
277                                }
278                        }
279                }
280                return list;
281        }
282
283        @Override
284        public final void removeAttribute(final Request request, final String name)
285        {
286                HttpSession httpSession = getHttpSession(request, false);
287                if (httpSession != null)
288                {
289                        String attributeName = getSessionAttributePrefix(request) + name;
290
291                        IRequestLogger logger = Application.get().getRequestLogger();
292                        if (logger != null)
293                        {
294                                Object value = httpSession.getAttribute(attributeName);
295                                if (value != null)
296                                {
297                                        logger.objectRemoved(value);
298                                }
299                        }
300                        httpSession.removeAttribute(attributeName);
301                }
302        }
303
304        @Override
305        public final void setAttribute(final Request request, final String name,
306                final Serializable value)
307        {
308                // ignore call if the session was marked invalid
309                HttpSession httpSession = getHttpSession(request, false);
310                if (httpSession != null)
311                {
312                        String attributeName = getSessionAttributePrefix(request) + name;
313                        IRequestLogger logger = Application.get().getRequestLogger();
314                        if (logger != null)
315                        {
316                                if (httpSession.getAttribute(attributeName) == null)
317                                {
318                                        logger.objectCreated(value);
319                                }
320                                else
321                                {
322                                        logger.objectUpdated(value);
323                                }
324                        }
325                        httpSession.setAttribute(attributeName, value);
326                }
327        }
328
329        @Override
330        public final void registerUnboundListener(final UnboundListener listener)
331        {
332                unboundListeners.add(listener);
333        }
334
335        @Override
336        public final void unregisterUnboundListener(final UnboundListener listener)
337        {
338                unboundListeners.remove(listener);
339        }
340
341        @Override
342        public final Set<UnboundListener> getUnboundListener()
343        {
344                return Collections.unmodifiableSet(unboundListeners);
345        }
346
347        /**
348         * Registers listener invoked when session is bound.
349         * 
350         * @param listener
351         */
352        @Override
353        public void registerBindListener(BindListener listener)
354        {
355                bindListeners.add(listener);
356        }
357
358        /**
359         * Unregisters listener invoked when session is bound.
360         * 
361         * @param listener
362         */
363        @Override
364        public void unregisterBindListener(BindListener listener)
365        {
366                bindListeners.remove(listener);
367        }
368
369        /**
370         * @return The list of registered bind listeners
371         */
372        @Override
373        public Set<BindListener> getBindListeners()
374        {
375                return Collections.unmodifiableSet(bindListeners);
376        }
377
378        /**
379         * Reacts on unbinding from the session by cleaning up the session related data.
380         */
381        protected static final class SessionBindingListener
382                implements
383                        HttpSessionBindingListener,
384                        Serializable
385        {
386                private static final long serialVersionUID = 1L;
387
388                /** The unique key of the application within this web application. */
389                private final String applicationKey;
390
391                /**
392                 * The Wicket Session associated with the expiring HttpSession
393                 */
394                private final Session wicketSession;
395
396                /**
397                 * Construct.
398                 * 
399                 * @param applicationKey
400                 *            The unique key of the application within this web application
401                 * @param wicketSession
402                 *            The Wicket Session associated with the expiring http session
403                 */
404                public SessionBindingListener(final String applicationKey, final Session wicketSession)
405                {
406                        this.applicationKey = applicationKey;
407                        this.wicketSession = wicketSession;
408                }
409
410                @Override
411                public void valueBound(final HttpSessionBindingEvent evg)
412                {
413                }
414
415                @Override
416                public void valueUnbound(final HttpSessionBindingEvent evt)
417                {
418                        String sessionId = evt.getSession().getId();
419
420                        log.debug("Session unbound: {}", sessionId);
421
422                        if (wicketSession != null)
423                        {
424                                wicketSession.onInvalidate();
425                        }
426                        
427                        Application application = Application.get(applicationKey);
428                        if (application == null)
429                        {
430                                log.debug("Wicket application with name '{}' not found.", applicationKey);
431                                return;
432                        }
433
434                        ISessionStore sessionStore = application.getSessionStore();
435                        if (sessionStore != null)
436                        {
437                                if (sessionStore instanceof HttpSessionStore)
438                                {
439                                        ((HttpSessionStore) sessionStore).onUnbind(sessionId);
440                                }
441
442                                for (UnboundListener listener : sessionStore.getUnboundListener())
443                                {
444                                        listener.sessionUnbound(sessionId);
445                                }
446                        }
447                }
448        }
449}