/*
 * 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).
 * <ul>
 * <li><b>Access</b> - 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()}</li>
 * 
 * <li><b>Locale</b> - 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.</li>
 * 
 * <li><b>Style</b> - 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.</li>
 * 
 * <li><b>Resource Loading</b> - 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):
 * <ol>
 * <li> [sourcePath]/name[style][locale].[extension]</li>
 * <li> [sourcePath]/name[locale].[extension]</li>
 * <li> [sourcePath]/name[style].[extension]</li>
 * <li> [sourcePath]/name.[extension]</li>
 * <li> [classPath]/name[style][locale].[extension]</li>
 * <li> [classPath]/name[locale].[extension]</li>
 * <li> [classPath]/name[style].[extension]</li>
 * <li> [classPath]/name.[extension]</li>
 * </ol>
 * </li>
 * 
 * <li><b>Session Properties</b> - 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.</li>
 * 
 * <li><b>Class Resolver</b> - Sessions have a class resolver ( {@link IClassResolver})
 * implementation that is used to locate classes for components such as pages.</li>
 * 
 * <li><b>Page Factory</b> - A pluggable implementation of {@link IPageFactory} is used to
 * instantiate pages for the session.</li>
 * 
 * <li><b>Removal</b> - Pages can be removed from the Session forcibly by calling clear(),
 * although such an action should rarely be necessary.</li>
 * 
 * <li><b>Flash Messages</b> - 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.</li>
 * </ul>
 * 
 * @author Jonathan Locke
 * @author Eelco Hillenius
 * @author Igor Vaynberg (ivaynberg)
 */
public abstract class Session implements IClusterable, IEventSink, IMetadataContext<Serializable, Session>, 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<Boolean> PAGES_UNLOCKED = new MetaDataKey<>()
	{
		private static final long serialVersionUID = 1L;
	};
	
	/** records if session has been invalidated by the current request */
	private static final MetaDataKey<Boolean> 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> pageAccessSynchronizer;

	/**
	 * Checks existence of a <code>Session</code> 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
	 * <p>
	 * - 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).
	 * <p>
	 * <a href="http://www.unicode.org/iso15924/iso15924-num.html">
	 * The list of right-to-left scripts appears in the 100-199 range in</a>, 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 
	 * <a href="http://www.iana.org/assignments/language-subtag-registry">Suppress-Script</a>:
	 * 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 <code>true</code> 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> 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<String> style = new AtomicReference<>();

	/**
	 * Holds attributes for sessions that are still temporary/ not bound to a session store. Only
	 * used when {@link #isTemporary()} is true.
	 * <p>
	 * 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).
	 * </p>
	 */
	private transient Map<String, Serializable> 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.
	 * <p>
	 * 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.
	 * </p>
	 * <p>
	 * 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.
	 * </p>
	 */
	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<String, Serializable> 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
	 * <code>null</code> 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 <M>
	 *            The type of the metadata.
	 * @return The metadata
	 * @see MetaDataKey
	 */
	@Override
	public synchronized final <M extends Serializable> M getMetaData(final MetaDataKey<M> 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.
	 * <p>
	 * 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 <code>true</code> if language of session locale is RTL (right-to-left), <code>false</code> 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 <M extends Serializable> Session setMetaData(final MetaDataKey<M> 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.
	 * <strong>Note</strong>: 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<String> 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<String>(
					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.
	 *
	 * <p>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</p>
	 */
	public void onInvalidate()
	{
	}
	
	/**
	 * Change the id of the underlying (Web)Session if this last one is permanent.
	 * <p>
	 * 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<PageAccessSynchronizer>
	{
		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);
		}
	}

}
