Package org.apache.wicket

Source Code of org.apache.wicket.Session

/*
* 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.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.protocol.http.IgnoreAjaxRequestException;
import org.apache.wicket.request.ClientInfo;
import org.apache.wicket.session.ISessionStore;
import org.apache.wicket.util.lang.Objects;
import org.apache.wicket.util.string.AppendingStringBuffer;
import org.apache.wicket.util.string.Strings;
import org.apache.wicket.util.time.Duration;
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 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 appropriate 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 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><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
{
  /**
   * 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<String> pageMapNames = new HashSet<String>(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());
    }
  }

  /** a sequence used for whenever something session-specific needs a unique value */
  private int sequence = 1;

  /** meta data key for missing body tags logging. */
  public static final MetaDataKey<PageMapAccessMetaData> PAGEMAP_ACCESS_MDK = new MetaDataKey<PageMapAccessMetaData>()
  {
    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<Session> current = new ThreadLocal<Session>();

  /** A store for dirty objects for one request */
  private static final ThreadLocal<List<IClusterable>> dirtyObjects = new ThreadLocal<List<IClusterable>>();

  /** Logging object */
  private static final Logger log = LoggerFactory.getLogger(Session.class);

  /** Attribute prefix for page maps stored in the session */
  private static final String pageMapAttributePrefix = "m:";

  private static final long serialVersionUID = 1L;

  /** A store for touched pages for one request */
  private static final ThreadLocal<List<Page<?>>> touchedPages = new ThreadLocal<List<Page<?>>>();

  /** Prefix for attributes holding page map entries */
  static final String pageMapEntryAttributePrefix = "p:";

  /** */
  private int pageIdCounter = 0;

  /**
   * 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");
    }
    Response response = requestCycle.getResponse();
    Request request = requestCycle.getRequest();
    return findOrCreate(request, response);
  }

  /**
   * @param response
   * @param request
   * @return The Session that is found in the current request or created if not.
   */
  public static Session findOrCreate(Request request, Response response)
  {
    Application application = Application.get();
    ISessionStore sessionStore = application.getSessionStore();
    Session session = sessionStore.lookup(request);

    if (session == null)
    {
      // Create session using session factory
      session = application.newSession(request, response);

      dirtyObjects.set(null);
      touchedPages.set(null);
    }

    // set thread local
    set(session);

    return session;
  }

  /**
   * Get the session for the calling thread.
   *
   * @return Session for calling thread
   */
  public static Session get()
  {
    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. Also triggers {@link #attach()} being called.
   *
   * @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);

    // execute any attach logic now
    session.attach();
  }

  /**
   * 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;

  /** True if session state has been changed */
  private transient boolean dirty = false;

  /** feedback messages */
  private final 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;

  /**
   * We need to know both thread that keeps the pagemap lock and the RequestCycle
   */
  private static class PageMapsUsedInRequestEntry
  {
    Thread thread;
    RequestCycle requestCycle;
  };

  private transient Map<IPageMap, PageMapsUsedInRequestEntry> 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.
   * <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, Object> temporarySessionAttributes;

  /** A linked list for last used pagemap queue */
  private final LinkedList/* <IPageMap> */<IPageMap> usedPageMaps = new LinkedList<IPageMap>();

  /**
   * Constructor. Note that {@link RequestCycle} is not available until this constructor returns.
   *
   * @param request
   *            The current request
   */
  public Session(Request request)
  {
    locale = request.getLocale();
    if (locale == null)
    {
      throw new IllegalArgumentException("Parameter 'locale' must not be null");
    }
  }

  /**
   * Constructor. Note that {@link RequestCycle} is not available until this constructor returns.
   *
   * @deprecated Use #Session(Request)
   *
   * @param application
   *            The application that this is a session of
   * @param request
   *            The current request
   */
  @Deprecated
  protected Session(Application application, Request request)
  {
    this(request);
  }

  /**
   * 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 (Entry<String, Object> entry : temporarySessionAttributes.entrySet())
        {
          store.setAttribute(request, String.valueOf(entry.getKey()), entry.getValue());
        }
        temporarySessionAttributes = null;
      }
    }
  }

  /**
   * Cleans up all rendered feedback messages and any unrendered, dangling feedback messages there
   * may be left after that.
   */
  public abstract void cleanupFeedbackMessages();


  /**
   * 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 final IPageMap createAutoPageMap()
  {
    return newPageMap(createAutoPageMapName());
  }

  protected int currentCreateAutoPageMapCounter()
  {
    return autoCreatePageMapCounter;
  }

  protected void incrementCreateAutoPageMapCounter()
  {
    ++autoCreatePageMapCounter;
  }

  /**
   * 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 = getAutoPageMapNamePrefix() + currentCreateAutoPageMapCounter() +
      getAutoPageMapNameSuffix();
    IPageMap pm = pageMapForName(name, false);
    while (pm != null)
    {
      incrementCreateAutoPageMapCounter();
      name = getAutoPageMapNamePrefix() + currentCreateAutoPageMapCounter() +
        getAutoPageMapNameSuffix();
      pm = pageMapForName(name, false);
    }
    return name;
  }

  /**
   * @return The prefixed string default "wicket-".
   */
  protected String getAutoPageMapNamePrefix()
  {
    return "wicket-";
  }

  /**
   * @return The suffix default an empty string.
   */
  protected String getAutoPageMapNameSuffix()
  {
    return "";
  }

  /**
   * 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)
    {
      clientInfo = RequestCycle.get().newClientInfo();
    }
    return clientInfo;
  }

  /**
   * @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
   * @param <M>
   *            The type of the metadata.
   * @return The metadata
   * @see MetaDataKey
   */
  public final <M extends Serializable> M getMetaData(final MetaDataKey<M> key)
  {
    return key.get(metaData);
  }

  /**
   * When a regular request on certain page with certain version is being processed, we don't
   * allow ajax requests to same page and version.
   *
   * @param lockedRequestCycle
   * @return whether current request is valid or should be discarded
   */
  protected boolean isCurrentRequestValid(RequestCycle lockedRequestCycle)
  {
    return true;
  }

  /**
   * THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL IT.
   *
   * Returns the page with given id and versionNumber. It keeps asking pageMaps for given page
   * until it finds one that contains it.
   *
   * @param pageId
   * @param versionNumber
   * @return The page of that pageid and version, null if not found
   */
  public final Page<?> getPage(final int pageId, final int versionNumber)
  {
    if (Application.get().getSessionSettings().isPageIdUniquePerSession() == false)
    {
      throw new IllegalStateException(
        "To call this method ISessionSettings.setPageIdUniquePerSession must be set to true");
    }

    List<IPageMap> pageMaps = getPageMaps();

    for (IPageMap pageMap : pageMaps)
    {
      if (pageMap.containsPage(pageId, versionNumber))
      {
        return getPage(pageMap.getName(), "" + pageId, versionNumber);
      }
    }

    return null;
  }

  /**
   * 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<IPageMap, PageMapsUsedInRequestEntry>(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();

        PageMapsUsedInRequestEntry entry = pageMapsUsedInRequest.get(pageMap);

        // Get page entry for id and version
        Thread t = entry != null ? entry.thread : null;
        while (t != null && t != Thread.currentThread())
        {
          if (isCurrentRequestValid(entry.requestCycle) == false)
          {
            // we need to ignore this request. That's because it is
            // an ajax request
            // while regular page request is being processed
            throw new IgnoreAjaxRequestException();
          }

          try
          {
            pageMapsUsedInRequest.wait(timeout.getMilliseconds());
          }
          catch (InterruptedException ex)
          {
            throw new WicketRuntimeException(ex);
          }

          entry = pageMapsUsedInRequest.get(pageMap);
          t = entry != null ? entry.thread : null;

          if (t != null && t != Thread.currentThread() &&
            (startTime + timeout.getMilliseconds()) < System.currentTimeMillis())
          {
            AppendingStringBuffer asb = new AppendingStringBuffer(100);
            asb.append("After " + timeout + " the Pagemap " + pageMapName +
              " is still locked by: " + t +
              ", giving up trying to get the page for path: " + path);
            // if it is still not the right thread..
            // This either points to long running code (a report
            // page?) or a deadlock or such
            try
            {
              StackTraceElement[] stackTrace = t.getStackTrace();
              asb.append("\n\tBegin of stack trace of " + t);
              for (StackTraceElement stackTraceElement : stackTrace)
              {
                asb.append("\n\t");
                asb.append(stackTraceElement);
              }
              asb.append("\n\tEnd of stack trace of " + t);
            }
            catch (Exception e)
            {
              // ignore
            }
            throw new WicketRuntimeException(asb.toString());
          }
        }

        PageMapsUsedInRequestEntry newEntry = new PageMapsUsedInRequestEntry();
        newEntry.thread = Thread.currentThread();
        newEntry.requestCycle = RequestCycle.get();
        pageMapsUsedInRequest.put(pageMap, newEntry);
        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();
        }
        else
        {
          // attach the page now.
          page.onPageAttached();
          touch(page);
        }
        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<IPageMap> getPageMaps()
  {
    final List<IPageMap> list = new ArrayList<IPageMap>();
    for (String attribute : getAttributeNames())
    {
      if (attribute.startsWith(pageMapAttributePrefix))
      {
        list.add((IPageMap)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 (IPageMap pageMap : getPageMaps())
    {
      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();
    synchronized (usedPageMaps)
    {
      if (usedPageMaps.size() >= maxPageMaps)
      {
        IPageMap pm = 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 = getMetaData(PAGEMAP_ACCESS_MDK);
    if (pagemapMetaData != null)
    {
      pagemapMetaData.pageMapNames.remove(pageMap.getName());
    }

    synchronized (usedPageMaps)
    {
      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 overridden.
   *
   * @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<Page<?>> lst = touchedPages.get();
    if (lst == null)
    {
      lst = new ArrayList<Page<?>>();
      touchedPages.set(lst);
      lst.add(page);
    }
    else if (!lst.contains(page))
    {
      lst.add(page);
    }
  }

  /**
   * THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL IT.
   * <p>
   * This method will remove a page that was previously added via touch()
   *
   * @param page
   */
  public final void untouch(Page<?> page)
  {
    List<Page<?>> lst = touchedPages.get();
    if (lst != null)
    {
      lst.remove(page);
    }
  }

  /**
   * @param visitor
   *            The visitor to call at each Page in this PageMap.
   */
  public final void visitPageMaps(final IPageMapVisitor visitor)
  {
    for (final Iterator<String> iterator = getAttributeNames().iterator(); iterator.hasNext();)
    {
      final String attribute = 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.
   */
  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 so that it will be flushed at the end of the request.
   */
  public final void dirty()
  {
    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<String> getAttributeNames()
  {
    if (!isTemporary())
    {
      RequestCycle cycle = RequestCycle.get();
      if (cycle != null)
      {
        return getSessionStore().getAttributeNames(cycle.getRequest());
      }
    }
    else
    {
      if (temporarySessionAttributes != null)
      {
        return 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
   */
  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.  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<String, Object>(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.
   */
  @Deprecated
  protected final void update()
  {
    throw new UnsupportedOperationException();
  }


  /**
   * @param page
   *            The page to add to dirty objects list
   */
  void dirtyPage(final Page<?> page)
  {
    List<IClusterable> 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())
    {
      synchronized (usedPageMaps)
      {
        usedPageMaps.remove(map);
        usedPageMaps.addLast(map);
      }
    }
    List<IClusterable> dirtyObjects = getDirtyObjectsList();
    if (!dirtyObjects.contains(map))
    {
      dirtyObjects.add(map);
    }
  }

  /**
   * @return The current thread dirty objects list
   */
  List<IClusterable> getDirtyObjectsList()
  {
    List<IClusterable> list = dirtyObjects.get();
    if (list == null)
    {
      list = new ArrayList<IClusterable>(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<Page<?>> touchedPages = Session.touchedPages.get();
    Session.touchedPages.set(null);
    if (touchedPages != null)
    {
      for (int i = 0; i < touchedPages.size(); i++)
      {
        Page<?> page = touchedPages.get(i);
        page.getPageMap().put(page);
        dirty = true;
      }
    }

    // If state is dirty
    if (dirty)
    {
      // State is no longer dirty
      dirty = false;

      // Set attribute.
      setAttribute(SESSION_ATTRIBUTE_NAME, this);
    }
    else
    {
      if (log.isDebugEnabled())
      {
        log.debug("update: Session not dirty.");
      }
    }

    List<IClusterable> dirtyObjects = Session.dirtyObjects.get();
    Session.dirtyObjects.set(null);

    Map<String, Object> tempMap = new HashMap<String, Object>();

    // Go through all dirty entries, replicating any dirty objects
    if (dirtyObjects != null)
    {
      for (final Iterator<IClusterable> iterator = dirtyObjects.iterator(); iterator.hasNext();)
      {
        String attribute = null;
        Object object = iterator.next();
        if (object instanceof Page)
        {
          final Page<?> page = (Page<?>)object;
          if (page.isPageStateless())
          {
            // 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());
        }

        // we might override some attributes, so we use a temporary map
        // and then just copy the last values to real sesssion
        tempMap.put(attribute, object);
      }
    }

    // in case we have dirty attributes, set them to session
    if (tempMap.isEmpty() == false)
    {
      for (Entry<String, Object> entry : tempMap.entrySet())
      {
        setAttribute(entry.getKey(), entry.getValue());
      }
    }

    if (pageMapsUsedInRequest != null)
    {
      synchronized (pageMapsUsedInRequest)
      {
        Thread t = Thread.currentThread();
        Iterator<Entry<IPageMap, PageMapsUsedInRequestEntry>> it = pageMapsUsedInRequest.entrySet()
          .iterator();
        while (it.hasNext())
        {
          Entry<IPageMap, PageMapsUsedInRequestEntry> entry = it.next();
          if ((entry.getValue()).thread == t)
          {
            it.remove();
          }
        }
        pageMapsUsedInRequest.notifyAll();
      }
    }
  }

  /**
   *
   * @return the next page id
   */
  synchronized protected int nextPageId()
  {
    return pageIdCounter++;
  }

  /**
   * Retrieves the next available session-unique value
   *
   * @return session-unique value
   */
  public synchronized int nextSequenceValue()
  {
    return sequence++;
  }
}
TOP

Related Classes of org.apache.wicket.Session

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.