All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.wicket.Session Maven / Gradle / Ivy

Go to download

Wicket is a Java web application framework that takes simplicity, separation of concerns and ease of development to a whole new level. Wicket pages can be mocked up, previewed and later revised using standard WYSIWYG HTML design tools. Dynamic content processing and form handling is all handled in Java code using a first-class component model backed by POJO data beans that can easily be persisted using your favorite technology.

There is a newer version: 10.2.0
Show newest version
/*
 * 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.
 */
package org.apache.wicket;

import java.io.Serializable;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import java.util.regex.Pattern;

import org.apache.wicket.application.IClassResolver;
import org.apache.wicket.authorization.IAuthorizationStrategy;
import org.apache.wicket.core.request.ClientInfo;
import org.apache.wicket.core.util.lang.WicketObjects;
import org.apache.wicket.event.IEvent;
import org.apache.wicket.event.IEventSink;
import org.apache.wicket.feedback.FeedbackMessage;
import org.apache.wicket.feedback.FeedbackMessages;
import org.apache.wicket.feedback.IFeedbackContributor;
import org.apache.wicket.page.IPageManager;
import org.apache.wicket.page.PageAccessSynchronizer;
import org.apache.wicket.request.Request;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.session.ISessionStore;
import org.apache.wicket.util.LazyInitializer;
import org.apache.wicket.util.io.IClusterable;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.lang.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * Holds information about a user session, including some fixed number of most recent pages (and all
 * their nested component information).
 * 
    *
  • Access - the Session can be retrieved either by {@link Component#getSession()} * or by directly calling the static method Session.get(). All classes which extend directly or indirectly * {@link org.apache.wicket.markup.html.WebMarkupContainer} can also use its convenience method * {@link org.apache.wicket.markup.html.WebMarkupContainer#getWebSession()}
  • * *
  • Locale - A session has a Locale property to support localization. The Locale for a * session can be set by calling {@link Session#setLocale(Locale)}. The Locale for a Session * determines how localized resources are found and loaded.
  • * *
  • Style - Besides having an appearance based on locale, resources can also have * different looks in the same locale (a.k.a. "skins"). The style for a session determines the look * which is used within the appropriate locale. The session style ("skin") can be set with the * setStyle() method.
  • * *
  • Resource Loading - Based on the Session locale and style, searching for resources * occurs in the following order (where sourcePath is set via the ApplicationSettings object for the * current Application, and style and locale are Session properties): *
      *
    1. [sourcePath]/name[style][locale].[extension]
    2. *
    3. [sourcePath]/name[locale].[extension]
    4. *
    5. [sourcePath]/name[style].[extension]
    6. *
    7. [sourcePath]/name.[extension]
    8. *
    9. [classPath]/name[style][locale].[extension]
    10. *
    11. [classPath]/name[locale].[extension]
    12. *
    13. [classPath]/name[style].[extension]
    14. *
    15. [classPath]/name.[extension]
    16. *
    *
  • * *
  • Session Properties - Arbitrary objects can be attached to a Session by installing a * session factory on your Application class which creates custom Session subclasses that have * typesafe properties specific to the application (see {@link Application} for details). To * discourage non-typesafe access to Session properties, no setProperty() or getProperty() method is * provided. In a clustered environment, you should take care to call the dirty() method when you * change a property on your own. This way the session will be reset again in the http session so * that the http session knows the session is changed.
  • * *
  • Class Resolver - Sessions have a class resolver ( {@link IClassResolver}) * implementation that is used to locate classes for components such as pages.
  • * *
  • Page Factory - A pluggable implementation of {@link IPageFactory} is used to * instantiate pages for the session.
  • * *
  • Removal - Pages can be removed from the Session forcibly by calling clear(), * although such an action should rarely be necessary.
  • * *
  • Flash Messages - Flash messages are messages that are stored in session and are removed * after they are displayed to the user. Session acts as a store for these messages because they can * last across requests.
  • *
* * @author Jonathan Locke * @author Eelco Hillenius * @author Igor Vaynberg (ivaynberg) */ public abstract class Session implements IClusterable, IEventSink, IMetadataContext, IFeedbackContributor { private static final long serialVersionUID = 1L; /** Logging object */ private static final Logger log = LoggerFactory.getLogger(Session.class); /** records if pages have been unlocked for the current request */ private static final MetaDataKey PAGES_UNLOCKED = new MetaDataKey<>() { private static final long serialVersionUID = 1L; }; /** records if session has been invalidated by the current request */ private static final MetaDataKey SESSION_INVALIDATED = new MetaDataKey<>() { private static final long serialVersionUID = 1L; }; /** Name of session attribute under which this session is stored */ public static final String SESSION_ATTRIBUTE_NAME = "session"; /** * taken from Google Closure Templates BidiUtils * * A regular expression for matching right-to-left language codes. See * {@link #isRtlLanguage} for the design. */ private static final Pattern RTL_LOCALE_RE = Pattern.compile("^(ar|dv|he|iw|fa|nqo|ps|sd|ug|ur|yi|.*[-_](Arab|Hebr|Thaa|Nkoo|Tfng))" + "(?!.*[-_](Latn|Cyrl)($|-|_))($|-|_)"); /** a sequence used for whenever something session-specific needs a unique value */ private final AtomicInteger sequence = new AtomicInteger(1); /** a sequence used for generating page IDs */ private final AtomicInteger pageId = new AtomicInteger(0); /** synchronize page's access by session */ private final Supplier pageAccessSynchronizer; /** * Checks existence of a Session associated with the current thread. * * @return {@code true} if {@link Session#get()} can return the instance of session, * {@code false} otherwise */ public static boolean exists() { Session session = ThreadContext.getSession(); if (session == null) { // no session is available via ThreadContext, so lookup in session store RequestCycle requestCycle = RequestCycle.get(); if (requestCycle != null) { session = Application.get().getSessionStore().lookup(requestCycle.getRequest()); if (session != null) { ThreadContext.setSession(session); } } } return session != null; } /** * Returns session associated to current thread. Always returns a session during a request * cycle, even though the session might be temporary * * @return session. */ public static Session get() { Session session = ThreadContext.getSession(); if (session != null) { return session; } else { return Application.get().fetchCreateAndSetSession(RequestCycle.get()); } } /** * Check if a BCP 47 / III language code indicates an RTL (right-to-left) language, i.e. * either: - a language code explicitly specifying one of the right-to-left * scripts, e.g. "az-Arab", or *

* - a language code specifying one of the languages normally written in a * right-to-left script, e.g. "fa" (Farsi), except ones explicitly * specifying Latin or Cyrillic script (which are the usual LTR (left-to-right) * alternatives). *

* * The list of right-to-left scripts appears in the 100-199 range in, of which Arabic and * Hebrew are by far the most widely used. We also recognize Thaana, N'Ko, * and Tifinagh, which also have significant modern usage. The rest (Syriac, * Samaritan, Mandaic, etc.) seem to have extremely limited or no modern * usage and are not recognized. The languages usually written in a * right-to-left script are taken as those with * Suppress-Script: * Hebr|Arab|Thaa|Nkoo|Tfng, as well as * Sindhi (sd) and Uyghur (ug). The presence of other subtags of the * language code, e.g. regions like EG (Egypt), is ignored. * * @param locale - locale to check * @return true in case passed locale is right-to-left */ public static boolean isRtlLanguage(final Locale locale) { Args.notNull(locale, "locale"); return RTL_LOCALE_RE.matcher(locale.toLanguageTag()).find(); } /** * Cached instance of agent info which is typically designated by calling * {@link Session#getClientInfo()}. */ protected ClientInfo clientInfo; /** True if session state has been changed */ private transient volatile boolean dirty = false; /** feedback messages */ private final FeedbackMessages feedbackMessages = new FeedbackMessages(); /** cached id because you can't access the id after session unbound */ private volatile String id = null; /** The locale to use when loading resources for this session. */ private final AtomicReference locale; /** True if locale's language is RTL (right-to-left) */ private boolean rtlLocale = false; /** Session level meta data. */ private MetaDataEntry[] metaData; /** * Temporary instance of the session store. Should be set on each request as it is not supposed * to go in the session. */ private transient ISessionStore sessionStore; /** Any special "skin" style to use when loading resources. */ private final AtomicReference style = new AtomicReference<>(); /** * Holds attributes for sessions that are still temporary/ not bound to a session store. Only * used when {@link #isTemporary()} is true. *

* Note: this doesn't have to be synchronized, as the only time when this map is used is when a * session is temporary, in which case it won't be shared between requests (it's a per request * instance). *

*/ private transient Map temporarySessionAttributes; /** * Constructor. Note that {@link RequestCycle} is not available until this constructor returns. * * @param request * The current request */ public Session(Request request) { Locale locale = request.getLocale(); if (locale == null) { throw new IllegalStateException( "Request#getLocale() cannot return null, request has to have a locale set on it"); } this.locale = new AtomicReference<>(locale); rtlLocale = isRtlLanguage(locale); pageAccessSynchronizer = new PageAccessSynchronizerProvider(); } /** * Force binding this session to the application's {@link ISessionStore session store} if not * already done so. *

* A Wicket application can operate in a session-less mode as long as stateless pages are used. * Session objects will be then created for each request, but they will only live for that * request. You can recognize temporary sessions by calling {@link #isTemporary()} which * basically checks whether the session's id is null. Hence, temporary sessions have no session * id. *

*

* By calling this method, the session will be bound (made not-temporary) if it was not bound * yet. It is useful for cases where you want to be absolutely sure this session object will be * available in next requests. If the session was already bound ( * {@link ISessionStore#lookup(Request) returns a session}), this call will be a noop. *

*/ public final void bind() { // If there is no request cycle then this is not a normal request but for example a last // modified call. if (RequestCycle.get() == null) { return; } ISessionStore store = getSessionStore(); Request request = RequestCycle.get().getRequest(); if (store.lookup(request) == null) { // explicitly create a session id = store.getSessionId(request, true); // bind it store.bind(request, this); if (temporarySessionAttributes != null) { for (Map.Entry entry : temporarySessionAttributes.entrySet()) { store.setAttribute(request, entry.getKey(), entry.getValue()); } temporarySessionAttributes = null; } } } /** * Removes all pages from the session. Although this method should rarely be needed, it is * available (possibly for security reasons). */ public final void clear() { if (isTemporary() == false) { getPageManager().clear(); } } /** * Registers an error feedback message for this session * * @param message * The feedback message */ @Override public final void error(final Serializable message) { addFeedbackMessage(message, FeedbackMessage.ERROR); } /** * Registers an fatal feedback message for this session * * @param message * The feedback message */ @Override public final void fatal(final Serializable message) { addFeedbackMessage(message, FeedbackMessage.FATAL); } /** * Registers an debug feedback message for this session * * @param message * The feedback message */ @Override public final void debug(final Serializable message) { addFeedbackMessage(message, FeedbackMessage.DEBUG); } /** * Get the application that is currently working with this session. * * @return Returns the application. */ public final Application getApplication() { return Application.get(); } /** * @return The authorization strategy for this session */ public IAuthorizationStrategy getAuthorizationStrategy() { return getApplication().getSecuritySettings().getAuthorizationStrategy(); } /** * @return The class resolver for this Session */ public final IClassResolver getClassResolver() { return getApplication().getApplicationSettings().getClassResolver(); } /** * Gets the client info object for this session. This method lazily gets the new agent info * object for this session. It uses any cached or set ({@link #setClientInfo(ClientInfo)}) * client info object. * * @return the client info object based on this request */ public abstract ClientInfo getClientInfo(); /** * Gets feedback messages stored in session * * @return unmodifiable list of feedback messages */ public final FeedbackMessages getFeedbackMessages() { return feedbackMessages; } /** * Gets the unique id for this session from the underlying SessionStore. May be * null if a concrete session is not yet created. * * @return The unique id for this session or null if it is a temporary session */ public final String getId() { if (id == null) { updateId(); // we have one? if (id != null) { dirty(); } } return id; } private void updateId() { RequestCycle requestCycle = RequestCycle.get(); if (requestCycle != null) { id = getSessionStore().getSessionId(requestCycle.getRequest(), false); } } /** * Get this session's locale. * * @return This session's locale */ public Locale getLocale() { return locale.get(); } /** * Gets metadata for this session using the given key. * * @param key * The key for the data * @param * The type of the metadata. * @return The metadata * @see MetaDataKey */ @Override public synchronized final M getMetaData(final MetaDataKey key) { return key.get(metaData); } /** * @return The page factory for this session */ public IPageFactory getPageFactory() { return getApplication().getPageFactory(); } /** * @return Size of this session */ public final long getSizeInBytes() { return WicketObjects.sizeof(this); } /** * Get the style (see {@link org.apache.wicket.Session}). * * @return Returns the style (see {@link org.apache.wicket.Session}) */ public final String getStyle() { return style.get(); } /** * Registers an informational feedback message for this session * * @param message * The feedback message */ @Override public final void info(final Serializable message) { addFeedbackMessage(message, FeedbackMessage.INFO); } /** * Registers an success feedback message for this session * * @param message * The feedback message */ @Override public final void success(final Serializable message) { addFeedbackMessage(message, FeedbackMessage.SUCCESS); } /** * Invalidates this session at the end of the current request. If you need to invalidate the * session immediately, you can do this by calling invalidateNow(), however this will remove all * Wicket components from this session, which means that you will no longer be able to work with * them. */ public void invalidate() { RequestCycle.get().setMetaData(SESSION_INVALIDATED, true); } /** * Invalidate and remove session store and page manager */ private void destroy() { if (getSessionStore() != null) { sessionStore.invalidate(RequestCycle.get().getRequest()); sessionStore = null; id = null; RequestCycle.get().setMetaData(SESSION_INVALIDATED, false); clientInfo = null; dirty = false; metaData = null; } } /** * Invalidates this session immediately. Calling this method will remove all Wicket components * from this session, which means that you will no longer be able to work with them. */ public void invalidateNow() { if (isSessionInvalidated() == false) { invalidate(); } // clear all pages possibly pending in the request getPageManager().clear(); destroy(); feedbackMessages.clear(); setStyle(null); pageId.set(0); sequence.set(0); temporarySessionAttributes = null; } /** * Replaces the underlying (Web)Session, invalidating the current one and creating a new one. By * calling {@link ISessionStore#invalidate(Request)} and {@link #bind()} * * If you are looking for a mean against session fixation attack, consider to use {@link #changeSessionId()}. */ public void replaceSession() { destroy(); bind(); } /** * Whether the session is invalid now, or will be invalidated by the end of the request. Clients * should rarely need to use this method if ever. * * @return Whether the session is invalid when the current request is done * * @see #invalidate() * @see #invalidateNow() */ public final boolean isSessionInvalidated() { return Boolean.TRUE.equals(RequestCycle.get().getMetaData(SESSION_INVALIDATED)); } /** * Whether this session is temporary. A Wicket application can operate in a session-less mode as * long as stateless pages are used. If this session object is temporary, it will not be * available on a next request. * * @return Whether this session is temporary (which is the same as it's id being null) */ public final boolean isTemporary() { return getId() == null; } /** * THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL IT. *

* Sets the client info object for this session. This will only work when * {@link #getClientInfo()} is not overridden. * * @param clientInfo * the client info object */ public final Session setClientInfo(ClientInfo clientInfo) { this.clientInfo = clientInfo; dirty(); return this; } /** * Set the locale for this session. * * @param locale * New locale */ public Session setLocale(final Locale locale) { Args.notNull(locale, "locale"); if (!Objects.equal(getLocale(), locale)) { this.locale.set(locale); rtlLocale = isRtlLanguage(locale); dirty(); } return this; } /** * Method to determine if language of current locale is RTL (right-to-left) or not * * @return true if language of session locale is RTL (right-to-left), false otherwise */ public boolean isRtlLocale() { return rtlLocale; } /** * Sets the metadata for this session using the given key. If the metadata object is not of the * correct type for the metadata key, an IllegalArgumentException will be thrown. For * information on creating MetaDataKeys, see {@link MetaDataKey}. * * @param key * The singleton key for the metadata * @param object * The metadata object * @throws IllegalArgumentException * @see MetaDataKey */ @Override public final synchronized Session setMetaData(final MetaDataKey key, final M object) { metaData = key.set(metaData, object); dirty(); return this; } /** * Set the style (see {@link org.apache.wicket.Session}). * * @param style * The style to set. * @return the Session object */ public final Session setStyle(final String style) { if (!Objects.equal(getStyle(), style)) { this.style.set(style); dirty(); } return this; } /** * Registers a warning feedback message for this session * * @param message * The feedback message */ @Override public final void warn(final Serializable message) { addFeedbackMessage(message, FeedbackMessage.WARNING); } /** * Adds a feedback message to the list of messages * * @param message * @param level * */ private void addFeedbackMessage(Serializable message, int level) { getFeedbackMessages().add(null, message, level); dirty(); } /** * End the current request. */ public void endRequest() { if (isSessionInvalidated()) { invalidateNow(); } else if (!isTemporary()) { // WICKET-5103 container might have changed id updateId(); } } /** * Any detach logic for session subclasses. This is called on the end of handling a request, * when the RequestCycle is about to be detached from the current thread. */ public void detach() { detachFeedback(); pageAccessSynchronizer.get().unlockAllPages(); RequestCycle.get().setMetaData(PAGES_UNLOCKED, true); } private void detachFeedback() { final int removed = feedbackMessages.clear(getApplication().getApplicationSettings() .getFeedbackMessageCleanupFilter()); if (removed != 0) { dirty(); } feedbackMessages.detach(); } /** * NOT PART OF PUBLIC API, DO NOT CALL * * Detaches internal state of {@link Session} */ public void internalDetach() { if (dirty) { Request request = RequestCycle.get().getRequest(); getSessionStore().flushSession(request, this); } dirty = false; } /** * Marks session state as dirty so that it will be (re)stored in the ISessionStore * at the end of the request. * Note: binds the session if it is temporary */ public final void dirty() { dirty(true); } /** * Marks session state as dirty so that it will be re-stored in the ISessionStore * at the end of the request. * * @param forced * A flag indicating whether the session should be marked as dirty even * when it is temporary. If {@code true} the Session will be bound. */ public final void dirty(boolean forced) { if (isTemporary()) { if (forced) { dirty = true; } } else { dirty = true; } } /** * Gets the attribute value with the given name * * @param name * The name of the attribute to store * @return The value of the attribute */ public final Serializable getAttribute(final String name) { if (!isTemporary()) { RequestCycle cycle = RequestCycle.get(); if (cycle != null) { return getSessionStore().getAttribute(cycle.getRequest(), name); } } else { if (temporarySessionAttributes != null) { return temporarySessionAttributes.get(name); } } return null; } /** * @return List of attributes for this session */ public final List getAttributeNames() { if (!isTemporary()) { RequestCycle cycle = RequestCycle.get(); if (cycle != null) { return Collections.unmodifiableList(getSessionStore().getAttributeNames( cycle.getRequest())); } } else { if (temporarySessionAttributes != null) { return Collections.unmodifiableList(new ArrayList( temporarySessionAttributes.keySet())); } } return Collections.emptyList(); } /** * Gets the session store. * * @return the session store */ protected ISessionStore getSessionStore() { if (sessionStore == null) { sessionStore = getApplication().getSessionStore(); } return sessionStore; } /** * Removes the attribute with the given name. * * @param name * the name of the attribute to remove */ public final void removeAttribute(String name) { if (!isTemporary()) { RequestCycle cycle = RequestCycle.get(); if (cycle != null) { getSessionStore().removeAttribute(cycle.getRequest(), name); } } else { if (temporarySessionAttributes != null) { temporarySessionAttributes.remove(name); } } } /** * Adds or replaces the attribute with the given name and value. * * @param name * The name of the attribute * @param value * The value of the attribute */ public final Session setAttribute(String name, Serializable value) { if (!isTemporary()) { RequestCycle cycle = RequestCycle.get(); if (cycle == null) { throw new IllegalStateException( "Cannot set the attribute: no RequestCycle available. If you get this error when using WicketTester.startPage(Page), make sure to call WicketTester.createRequestCycle() beforehand."); } ISessionStore store = getSessionStore(); Request request = cycle.getRequest(); // extra check on session binding event if (value == this) { Object current = store.getAttribute(request, name); if (current == null) { String id = store.getSessionId(request, false); if (id != null) { // this is a new instance. wherever it came from, bind // the session now store.bind(request, (Session)value); } } } // Set the actual attribute store.setAttribute(request, name, value); } else { // we don't have to synchronize, as it is impossible a temporary // session instance gets shared across threads if (temporarySessionAttributes == null) { temporarySessionAttributes = new HashMap<>(3); } temporarySessionAttributes.put(name, value); } return this; } /** * Retrieves the next available session-unique value * * @return session-unique value */ public int nextSequenceValue() { dirty(false); return sequence.getAndIncrement(); } /** * * @return the next page id */ public int nextPageId() { dirty(false); return pageId.getAndIncrement(); } /** * Returns the {@link IPageManager} instance. * * @return {@link IPageManager} instance. */ public final IPageManager getPageManager() { if (Boolean.TRUE.equals(RequestCycle.get().getMetaData(PAGES_UNLOCKED))) { throw new WicketRuntimeException("The request has been processed. Access to pages is no longer allowed"); } IPageManager manager = Application.get().internalGetPageManager(); return pageAccessSynchronizer.get().adapt(manager); } /** {@inheritDoc} */ @Override public void onEvent(IEvent event) { } /** * A callback method that is executed when the user session is invalidated * either by explicit call to {@link org.apache.wicket.Session#invalidate()} * or due to HttpSession expiration. * *

In case of session expiration this method is called in a non-worker thread, i.e. * there are no thread locals exported for the Application, RequestCycle and Session. * The Session is the current instance. The Application can be found by using * {@link Application#get(String)}. There is no way to get a reference to a RequestCycle

*/ public void onInvalidate() { } /** * Change the id of the underlying (Web)Session if this last one is permanent. *

* Call upon login to protect against session fixation. * * @see "http://www.owasp.org/index.php/Session_Fixation" */ public void changeSessionId() { if (isTemporary()) { return; } id = generateNewSessionId(); } /** * Change the id of the underlying (Web)Session. * * @return the new session id value. */ protected abstract String generateNewSessionId(); /** * Factory method for PageAccessSynchronizer instances * * @param timeout * The configured timeout. See {@link org.apache.wicket.settings.RequestCycleSettings#getTimeout()} * @return A new instance of PageAccessSynchronizer */ protected PageAccessSynchronizer newPageAccessSynchronizer(Duration timeout) { return new PageAccessSynchronizer(timeout); } private final class PageAccessSynchronizerProvider extends LazyInitializer { private static final long serialVersionUID = 1L; @Override protected PageAccessSynchronizer createInstance() { final Duration timeout; if (Application.exists()) { timeout = Application.get().getRequestCycleSettings().getTimeout(); } else { timeout = Duration.ofMinutes(1); log.warn( "PageAccessSynchronizer created outside of application thread, using default timeout: {}", timeout); } return newPageAccessSynchronizer(timeout); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy