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