/*
* 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.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.wicket.application.IClassResolver;
import org.apache.wicket.authorization.IAuthorizationStrategy;
import org.apache.wicket.feedback.FeedbackMessage;
import org.apache.wicket.feedback.FeedbackMessages;
import org.apache.wicket.feedback.IFeedbackMessageFilter;
import org.apache.wicket.request.ClientInfo;
import org.apache.wicket.session.ISessionStore;
import org.apache.wicket.util.convert.IConverter;
import org.apache.wicket.util.lang.Objects;
import org.apache.wicket.util.string.Strings;
import org.apache.wicket.util.time.Duration;
/**
* Holds information about a user session, including some fixed number of most
* recent pages (and all their nested component information).
* <ul>
* <li><b>Access via RequestCycle </b>- The Session for a {@link RequestCycle}
* can be retrieved by calling {@link RequestCycle#getSession()}.
*
* <li><b>Access via Component </b>- If a RequestCycle object is not available,
* the Session can be retrieved for a Component by calling
* {@link Component#getSession()}. As currently implemented, each Component
* does not itself have a reference to the session that contains it. However,
* the Page component at the root of the containment hierarchy does have a
* reference to the Session that holds the Page. So
* {@link Component#getSession()} traverses the component hierarchy to the root
* Page and then calls {@link Page#getSession()}.
*
* <li><b>Access via Thread Local </b>- In the odd case where neither a
* RequestCycle nor a Component is available, the currently active Session for
* the calling thread can be retrieved by calling the static method
* Session.get(). This last form should only be used if the first two forms
* cannot be used since thread local access can involve a potentially more
* expensive hash map lookup.
*
* <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><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 appopriate locale.
* The session style ("skin") can be set with the setStyle() method.
*
* <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):
* <ul>
* 1. [sourcePath]/name[style][locale].[extension] <br>
* 2. [sourcePath]/name[locale].[extension] <br>
* 3. [sourcePath]/name[style].[extension] <br>
* 4. [sourcePath]/name.[extension] <br>
* 5. [classPath]/name[style][locale].[extension] <br>
* 6. [classPath]/name[locale].[extension] <br>
* 7. [classPath]/name[style].[extension] <br>
* 8. [classPath]/name.[extension] <br>
* </ul>
*
* <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 or youre own. This way the
* session will be reset again in the http session so that the http session
* knows the session is changed.
*
* <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><b>Page Factory </b>- A pluggable implementation of {@link IPageFactory}
* is used to instantiate pages for the session.
*
* <li><b>Removal </b>- Pages can be removed from the Session forcibly by
* calling remove(Page) or removeAll(), although such an action should rarely be
* necessary.
*
* <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.
*
* @author Jonathan Locke
* @author Eelco Hillenius
* @author Igor Vaynberg (ivaynberg)
*/
public abstract class Session implements IClusterable, IConverterLocator
{
/**
* Visitor interface for visiting page maps
*
* @author Jonathan Locke
*/
public static interface IPageMapVisitor
{
/**
* @param pageMap
* The page map
*/
public void pageMap(final IPageMap pageMap);
}
/**
* meta data for recording map map access.
*/
public static final class PageMapAccessMetaData implements IClusterable
{
private static final long serialVersionUID = 1L;
Set pageMapNames = new HashSet(2);
/**
* @param pagemap
* the pagemap to add as used.
* @return the boolean if it was added (didn't already contain the
* pagemap)
*/
public boolean add(IPageMap pagemap)
{
return pageMapNames.add(pagemap.getName());
}
}
private static final class Counter
{
private int count = 0;
}
/**
* Filter that returns all component scoped messages ({@link FeedbackMessage#getReporter()} !=
* null).
*/
public static final IFeedbackMessageFilter MESSAGES_FOR_COMPONENTS = new IFeedbackMessageFilter()
{
private static final long serialVersionUID = 1L;
public boolean accept(FeedbackMessage message)
{
return message.getReporter() != null;
}
};
/** meta data key for missing body tags logging. */
public static final MetaDataKey PAGEMAP_ACCESS_MDK = new MetaDataKey(
PageMapAccessMetaData.class)
{
private static final long serialVersionUID = 1L;
};
/** Name of session attribute under which this session is stored */
public static final String SESSION_ATTRIBUTE_NAME = "session";
/** Thread-local current session. */
private static final ThreadLocal current = new ThreadLocal();
/** A store for dirty objects for one request */
private static final ThreadLocal dirtyObjects = new ThreadLocal();
/** Logging object */
private static final Log log = LogFactory.getLog(Session.class);
/** Attribute prefix for page maps stored in the session */
private static final String pageMapAttributePrefix = "m:";
/**
* Filter that returns all session scoped messages ({@link FeedbackMessage#getReporter()} ==
* null).
*/
private static final IFeedbackMessageFilter RENDERED_SESSION_SCOPED_MESSAGES = new IFeedbackMessageFilter()
{
private static final long serialVersionUID = 1L;
public boolean accept(FeedbackMessage message)
{
return message.getReporter() == null && message.isRendered();
}
};
private static final long serialVersionUID = 1L;
/** A store for touched pages for one request */
private static final ThreadLocal touchedPages = new ThreadLocal();
/** Prefix for attributes holding page map entries */
static final String pageMapEntryAttributePrefix = "p:";
/**
* Checks if the <code>Session</code> threadlocal is set in this thread
*
* @return true if {@link Session#get()} can return the instance of session,
* false otherwise
*/
public static boolean exists()
{
return current.get() != null;
}
/**
* Locate the session for the client of this request in the
* {@link ISessionStore} or create a new one and attach it when none could
* be located and sets it as the current instance for this thread.
* Typically, clients never touch this method, but rather use
* {@link Session#get()}, which does the locating implicitly when not yet
* set as a thread local.
*
* @return The session for the client of this request or a new, unbound
*/
public static final Session findOrCreate()
{
RequestCycle requestCycle = RequestCycle.get();
if (requestCycle == null)
{
throw new IllegalStateException(
"you can only locate or create sessions in the context of a request cycle");
}
Application application = Application.get();
ISessionStore sessionStore = application.getSessionStore();
Session session = sessionStore.lookup(requestCycle.getRequest());
if (session == null)
{
// Create session using session factory
session = application.getSessionFactory().newSession(requestCycle.getRequest(),
requestCycle.getResponse());
// Set the current session
// execute any attach logic now
session.attach();
}
set(session);
return session;
}
/**
* Get the session for the calling thread.
*
* @return Session for calling thread
*/
public static Session get()
{
Session session = (Session)current.get();
if (session == null)
{
session = findOrCreate();
}
return session;
}
/**
* THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL IT.
* <p>
* Sets session for calling thread.
*
* @param session
* The session
*/
public static void set(final Session session)
{
if (session == null)
{
throw new IllegalArgumentException("Argument session can not be null");
}
current.set(session);
}
/**
* THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL IT.
* <p>
* Clears the session for calling thread.
*
*/
public static void unset()
{
current.set(null);
}
/** A number to generate names for auto create pagemaps */
private int autoCreatePageMapCounter = 0;
/**
* Cached instance of agent info which is typically designated by calling
* {@link RequestCycle#newClientInfo()}.
*/
private ClientInfo clientInfo;
/** The converter instance. */
private transient IConverterLocator converterSupplier;
/** True if session state has been changed */
private transient boolean dirty = false;
/** feedback messages */
private FeedbackMessages feedbackMessages = new FeedbackMessages();
/** cached id because you can't access the id after session unbound */
private String id = null;
/** The locale to use when loading resources for this session. */
private Locale locale;
/** Application level meta data. */
private MetaDataEntry[] metaData;
private transient Map pageMapsUsedInRequest;
/** True, if session has been invalidated */
private transient boolean sessionInvalidated = false;
/**
* 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 String style;
/**
* Holds attributes for sessions that are still temporary/ not bound to a
* session store. Only used when {@link #isTemporary()} is true.
*/
private transient Map temporarySessionAttributes;
/** A linked list for last used pagemap queue */
private LinkedList/* <IPageMap> */usedPageMaps = new LinkedList();
/**
* Constructor. Note that {@link RequestCycle} is not available until this
* constructor returns.
*
* @param application
* The application that this is a session of
* @param request
* The current request
* @param response
* The current response
*/
protected Session(Application application, Request request)
{
this.locale = request.getLocale();
if (locale == null)
{
throw new IllegalArgumentException("Parameter 'locale' must not be null");
}
}
/**
* 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.
* </p>
* <p>
* This method should not typically be called by clients
* </p>
*/
public final void bind()
{
ISessionStore store = getSessionStore();
Request request = RequestCycle.get().getRequest();
if (store.lookup(request) == null)
{
synchronized (this)
{
// explicitly create a session
this.id = store.getSessionId(request, true);
// bind it
store.bind(request, this);
if (temporarySessionAttributes != null)
{
for (Iterator i = temporarySessionAttributes.entrySet().iterator(); i.hasNext();)
{
Entry entry = (Entry)i.next();
store.setAttribute(request, String.valueOf(entry.getKey()), entry
.getValue());
}
temporarySessionAttributes = null;
}
}
}
else
{
log.warn("trying to bind an already bound and non-temporary session");
}
}
/**
* Cleans up all rendered feedback messages and any unrendered, dangling
* feedback messages there may be left after that.
*/
public void cleanupFeedbackMessages()
{
// If session scoped, rendered messages got indeed cleaned up, mark the
// session as dirty
if (feedbackMessages.clear(RENDERED_SESSION_SCOPED_MESSAGES) > 0)
{
dirty();
}
// Render anything that was already rendered and what was assigned to a
// page that was just rendered if any
final Page page = RequestCycle.get().getResponsePage();
final Counter notCleanedUpCounter = new Counter();
feedbackMessages.clear(new IFeedbackMessageFilter()
{
private static final long serialVersionUID = 1L;
public boolean accept(FeedbackMessage message)
{
if (message.isRendered())
{
return true;
}
if (page != null)
{
Component reporter = message.getReporter();
if (reporter != null)
{
Page reporterPage = reporter.findPage();
return reporterPage != null && reporterPage.equals(page);
}
}
notCleanedUpCounter.count++;
return false;
}
});
if (log.isDebugEnabled() && notCleanedUpCounter.count > 0)
{
log.debug("still " + notCleanedUpCounter.count + " messages unrendered");
}
}
/**
* Removes all pages from the session. Although this method should rarely be
* needed, it is available (possibly for security reasons).
*/
public final void clear()
{
visitPageMaps(new IPageMapVisitor()
{
public void pageMap(IPageMap pageMap)
{
pageMap.clear();
}
});
}
/**
* Automatically creates a page map, giving it a session unique name.
*
* @return Created PageMap
*/
public synchronized final IPageMap createAutoPageMap()
{
return newPageMap(createAutoPageMapName());
}
/**
* With this call you can create a pagemap name but not create the pagemap
* itself already. It will give the first pagemap name where it couldn't
* find a current pagemap for.
*
* It will return the same name if you call it 2 times in a row.
*
* @return The created pagemap name
*/
public synchronized final String createAutoPageMapName()
{
String name = "wicket-" + autoCreatePageMapCounter;
IPageMap pm = pageMapForName(name, false);
while (pm != null)
{
autoCreatePageMapCounter++;
name = "wicket-" + autoCreatePageMapCounter;
pm = pageMapForName(name, false);
}
return name;
}
/**
* Registers an error feedback message for this session
*
* @param message
* The feedback message
*/
public final void error(final String message)
{
addFeedbackMessage(message, FeedbackMessage.ERROR);
}
/**
* 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 or uses {@link RequestCycle#newClientInfo()} to get
* the info object based on the current request when no client info object
* was set yet, and then caches the returned object; we can expect the
* client to stay the same for the whole session, and implementations of
* {@link RequestCycle#newClientInfo()} might be relatively expensive.
*
* @return the client info object based on this request
*/
public ClientInfo getClientInfo()
{
if (clientInfo == null)
{
this.clientInfo = RequestCycle.get().newClientInfo();
}
return clientInfo;
}
/**
* Gets the converter instance. This method returns the cached converter for
* the current locale. Whenever the locale is changed, the cached value is
* cleared and the converter will be recreated for the new locale on a next
* request.
*
* @param type
* TODO
*
* @return the converter
*/
public final IConverter getConverter(Class/* <?> */type)
{
if (converterSupplier == null)
{
// Let the factory create a new converter
converterSupplier = getApplication().getApplicationSettings()
.getConverterLocatorFactory().newConverterLocator();
}
return converterSupplier.getConverter(type);
}
/**
* @return The default page map
*/
public final IPageMap getDefaultPageMap()
{
return pageMapForName(PageMap.DEFAULT_NAME, true);
}
/**
* 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)
{
id = getSessionStore().getSessionId(RequestCycle.get().getRequest(), false);
// we have one?
if (id != null)
{
dirty();
}
}
return id;
}
/**
* Get this session's locale.
*
* @return This session's locale
*/
public Locale getLocale()
{
return locale;
}
/**
* Gets metadata for this session using the given key.
*
* @param key
* The key for the data
* @return The metadata
* @see MetaDataKey
*/
public final Serializable getMetaData(final MetaDataKey key)
{
return key.get(metaData);
}
/**
* THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL IT.
*
* Get the page for the given path.
*
* @param pageMapName
* The name of the page map where the page is
* @param path
* Component path
* @param versionNumber
* The version of the page required
* @return The page based on the first path component (the page id), or null
* if the requested version of the page cannot be found.
*/
public final Page getPage(final String pageMapName, final String path, final int versionNumber)
{
if (log.isDebugEnabled())
{
log.debug("Getting page [path = " + path + ", versionNumber = " + versionNumber + "]");
}
// Get page map by name, creating the default page map automatically
IPageMap pageMap = pageMapForName(pageMapName, pageMapName == PageMap.DEFAULT_NAME);
if (pageMap != null)
{
synchronized (usedPageMaps) // get a lock so be sure that only one
// is made
{
if (pageMapsUsedInRequest == null)
{
pageMapsUsedInRequest = new HashMap(3);
}
}
synchronized (pageMapsUsedInRequest)
{
long startTime = System.currentTimeMillis();
// TODO For now only use the setting. Might be extended with
// something overridable on request/ page/ request target level
// later
Duration timeout = Application.get().getRequestCycleSettings().getTimeout();
// Get page entry for id and version
Thread t = (Thread)pageMapsUsedInRequest.get(pageMap);
while (t != null && t != Thread.currentThread())
{
try
{
pageMapsUsedInRequest.wait(timeout.getMilliseconds());
}
catch (InterruptedException ex)
{
throw new WicketRuntimeException(ex);
}
t = (Thread)pageMapsUsedInRequest.get(pageMap);
if (t != null && t != Thread.currentThread()
&& (startTime + timeout.getMilliseconds()) < System.currentTimeMillis())
{
// if it is still not the right thread..
// This either points to long running code (a report
// page?) or a deadlock or such
throw new WicketRuntimeException("After " + timeout + " the Pagemap "
+ pageMapName + " is still locked by: " + t
+ ", giving up trying to get the page for path: " + path);
}
}
pageMapsUsedInRequest.put(pageMap, Thread.currentThread());
final String id = Strings.firstPathComponent(path, Component.PATH_SEPARATOR);
Page page = pageMap.get(Integer.parseInt(id), versionNumber);
if (page == null)
{
pageMapsUsedInRequest.remove(pageMap);
pageMapsUsedInRequest.notifyAll();
}
return page;
}
}
return null;
}
/**
* @return The page factory for this session
*/
public final IPageFactory getPageFactory()
{
return getApplication().getSessionSettings().getPageFactory();
}
/**
* @param page
* The page, or null if no page context is available
* @return The page factory for the page, or the default page factory if
* page was null
*/
public final IPageFactory getPageFactory(final Page page)
{
if (page != null)
{
return page.getPageFactory();
}
return getPageFactory();
}
/**
* @return A list of all PageMaps in this session.
*/
public final List getPageMaps()
{
final List list = new ArrayList();
for (final Iterator iterator = getAttributeNames().iterator(); iterator.hasNext();)
{
final String attribute = (String)iterator.next();
if (attribute.startsWith(pageMapAttributePrefix))
{
list.add(getAttribute(attribute));
}
}
return list;
}
/**
* @return Size of this session, including all the pagemaps it contains
*/
public final long getSizeInBytes()
{
long size = Objects.sizeof(this);
for (final Iterator iterator = getPageMaps().iterator(); iterator.hasNext();)
{
final IPageMap pageMap = (IPageMap)iterator.next();
size += pageMap.getSizeInBytes();
}
return size;
}
/**
* 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;
}
/**
* Registers an informational feedback message for this session
*
* @param message
* The feedback message
*/
public final void info(final String message)
{
addFeedbackMessage(message, FeedbackMessage.INFO);
}
/**
* 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()
{
sessionInvalidated = true;
}
/**
* 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()
{
sessionInvalidated = true; // set this for isSessionInvalidated
getSessionStore().invalidate(RequestCycle.get().getRequest());
}
/**
* 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 sessionInvalidated;
}
/**
* 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;
}
/**
* Creates a new page map with a given name
*
* @param name
* The name for the new page map
* @return The newly created page map
*/
public final IPageMap newPageMap(final String name)
{
// Check that session doesn't have too many page maps already
final int maxPageMaps = getApplication().getSessionSettings().getMaxPageMaps();
if (usedPageMaps.size() >= maxPageMaps)
{
IPageMap pm = (IPageMap)usedPageMaps.getFirst();
pm.remove();
}
// Create new page map
final IPageMap pageMap = getSessionStore().createPageMap(name);
setAttribute(attributeForPageMapName(name), pageMap);
dirty();
return pageMap;
}
/**
* Gets a page map for the given name, automatically creating it if need be.
*
* @param pageMapName
* Name of page map, or null for default page map
* @param autoCreate
* True if the page map should be automatically created if it
* does not exist
* @return PageMap for name
*/
public final IPageMap pageMapForName(String pageMapName, final boolean autoCreate)
{
IPageMap pageMap = (IPageMap)getAttribute(attributeForPageMapName(pageMapName));
if (pageMap == null && autoCreate)
{
pageMap = newPageMap(pageMapName);
}
return pageMap;
}
/**
* @param pageMap
* Page map to remove
*/
public final void removePageMap(final IPageMap pageMap)
{
PageMapAccessMetaData pagemapMetaData = (PageMapAccessMetaData)getMetaData(PAGEMAP_ACCESS_MDK);
if (pagemapMetaData != null)
pagemapMetaData.pageMapNames.remove(pageMap.getName());
usedPageMaps.remove(pageMap);
removeAttribute(attributeForPageMapName(pageMap.getName()));
dirty();
}
/**
* THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL IT.
* <p>
* Sets the application that this session is associated with.
*
* @param application
* The application
*/
public final void setApplication(final Application application)
{
}
/**
* 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 overriden.
*
* @param clientInfo
* the client info object
*/
public final void setClientInfo(ClientInfo clientInfo)
{
this.clientInfo = clientInfo;
dirty();
}
/**
* Set the locale for this session.
*
* @param locale
* New locale
*/
public final void setLocale(final Locale locale)
{
if (locale == null)
{
throw new IllegalArgumentException("Parameter 'locale' must not be null");
}
this.locale = locale;
dirty();
}
/**
* 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
*/
public final void setMetaData(final MetaDataKey key, final Serializable object)
{
metaData = key.set(metaData, object);
}
/**
* 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)
{
this.style = style;
dirty();
return this;
}
/**
* THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL IT.
* <p>
* The page will be 'touched' in the session. If it wasn't added yet to the
* pagemap, it will be added to the page map else it will set this page to
* the front.
*
* If another page was removed because of this it will be cleaned up.
*
* @param page
*/
public final void touch(Page page)
{
// store it in a list, so that the pages are really pushed
// to the pagemap when the session does it update/detaches.
// all the pages are then detached
List lst = (List)touchedPages.get();
if (lst == null)
{
lst = new ArrayList();
touchedPages.set(lst);
lst.add(page);
}
else if (!lst.contains(page))
{
lst.add(page);
}
}
/**
* @param visitor
* The visitor to call at each Page in this PageMap.
*/
public final void visitPageMaps(final IPageMapVisitor visitor)
{
for (final Iterator iterator = getAttributeNames().iterator(); iterator.hasNext();)
{
final String attribute = (String)iterator.next();
if (attribute.startsWith(pageMapAttributePrefix))
{
visitor.pageMap((IPageMap)getAttribute(attribute));
}
}
}
/**
* Registers a warning feedback message for this session
*
* @param message
* The feedback message
*/
public final void warn(final String message)
{
addFeedbackMessage(message, FeedbackMessage.WARNING);
}
/**
* Adds a feedback message to the list of messages
*
* @param message
* @param level
*
*/
private void addFeedbackMessage(String message, int level)
{
getFeedbackMessages().add(null, message, level);
dirty();
}
/**
* @param pageMapName
* Name of page map
* @return Session attribute holding page map
*/
private final String attributeForPageMapName(final String pageMapName)
{
return pageMapAttributePrefix + pageMapName;
}
/**
* Any attach logic for session subclasses. Called when a session is set for
* the thread. Note that this is done on demand (lazily): as long as the
* session isn't being used, it is not located or created and this method is
* not called.
*/
protected void attach()
{
}
/**
* 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.
*/
protected void detach()
{
if (sessionInvalidated)
{
invalidateNow();
}
}
/**
* Marks session state as dirty
*/
protected final void dirty()
{
this.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
*/
protected final Object 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
*/
protected final List getAttributeNames()
{
if (!isTemporary())
{
RequestCycle cycle = RequestCycle.get();
if (cycle != null)
{
return getSessionStore().getAttributeNames(cycle.getRequest());
}
}
else
{
if (temporarySessionAttributes != null)
{
return new ArrayList(temporarySessionAttributes.keySet());
}
}
return Collections.EMPTY_LIST;
}
/**
* 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
*/
protected 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
*/
protected final void setAttribute(String name, Object value)
{
if (!isTemporary())
{
RequestCycle cycle = RequestCycle.get();
if (cycle == null)
{
throw new IllegalStateException(
"Cannot set the attribute: no RequestCycle available");
}
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);
}
}
/**
* NOT TO BE CALLED BY FRAMEWORK USERS.
*
* @deprecated obsolete method (was meant for internal book keeping really).
* Clients should override {@link #detach()} instead.
*/
protected final void update()
{
throw new UnsupportedOperationException();
}
/**
* @param page
* The page to add to dirty objects list
*/
void dirtyPage(final Page page)
{
List dirtyObjects = getDirtyObjectsList();
if (!dirtyObjects.contains(page))
{
dirtyObjects.add(page);
}
}
/**
* @param map
* The page map to add to dirty objects list
*/
void dirtyPageMap(final IPageMap map)
{
if (!map.isDefault())
{
usedPageMaps.remove(map);
usedPageMaps.addLast(map);
}
List dirtyObjects = getDirtyObjectsList();
if (!dirtyObjects.contains(map))
{
dirtyObjects.add(map);
}
}
/**
* @return The current thread dirty objects list
*/
List getDirtyObjectsList()
{
List list = (List)dirtyObjects.get();
if (list == null)
{
list = new ArrayList(4);
dirtyObjects.set(list);
}
return list;
}
// TODO remove after deprecation release
/**
* INTERNAL API. The request cycle when detached will call this.
*
*/
final void requestDetached()
{
List touchedPages = (List)Session.touchedPages.get();
Session.touchedPages.set(null);
if (touchedPages != null)
{
for (int i = 0; i < touchedPages.size(); i++)
{
Page page = (Page)touchedPages.get(i);
page.getPageMap().put(page);
}
}
// If state is dirty
if (dirty)
{
if (log.isDebugEnabled())
{
log.debug("update: Session is dirty. Replicating.");
}
// State is no longer dirty
this.dirty = false;
// Set attribute.
setAttribute(SESSION_ATTRIBUTE_NAME, this);
}
else
{
if (log.isDebugEnabled())
{
log.debug("update: Session not dirty.");
}
}
List dirtyObjects = (List)Session.dirtyObjects.get();
Session.dirtyObjects.set(null);
// Go through all dirty entries, replicating any dirty objects
if (dirtyObjects != null)
{
for (final Iterator iterator = dirtyObjects.iterator(); iterator.hasNext();)
{
String attribute = null;
Object object = iterator.next();
if (object instanceof Page)
{
final Page page = (Page)object;
if (page.isStateless())
{
// check, can it be that stateless pages where added to
// the session?
// and should be removed now?
continue;
}
attribute = page.getPageMap().attributeForId(page.getNumericId());
if (getAttribute(attribute) == null)
{
// page removed by another thread. don't add it again.
continue;
}
object = page.getPageMapEntry();
}
else if (object instanceof IPageMap)
{
attribute = attributeForPageMapName(((IPageMap)object).getName());
}
setAttribute(attribute, object);
}
}
if (pageMapsUsedInRequest != null)
{
synchronized (pageMapsUsedInRequest)
{
Thread t = Thread.currentThread();
Iterator it = pageMapsUsedInRequest.entrySet().iterator();
while (it.hasNext())
{
Entry entry = (Entry)it.next();
if (entry.getValue() == t)
{
it.remove();
}
}
pageMapsUsedInRequest.notifyAll();
}
}
}
}