/*
 * 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.jetspeed.portalsite.impl;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.servlet.http.HttpSessionActivationListener;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;
import javax.servlet.http.HttpSessionEvent;

import org.apache.jetspeed.om.folder.Folder;
import org.apache.jetspeed.om.page.BaseFragmentsElement;
import org.apache.jetspeed.om.page.DynamicPage;
import org.apache.jetspeed.om.page.FragmentDefinition;
import org.apache.jetspeed.om.page.Page;
import org.apache.jetspeed.om.page.PageTemplate;
import org.apache.jetspeed.page.PageManager;
import org.apache.jetspeed.page.PageManagerEventListener;
import org.apache.jetspeed.page.document.Node;
import org.apache.jetspeed.page.document.NodeException;
import org.apache.jetspeed.page.document.NodeNotFoundException;
import org.apache.jetspeed.page.document.NodeSet;
import org.apache.jetspeed.portalsite.PortalSiteContentTypeMapper;
import org.apache.jetspeed.portalsite.PortalSiteRequestContext;
import org.apache.jetspeed.portalsite.PortalSiteSessionContext;
import org.apache.jetspeed.portalsite.view.AbstractSiteView;
import org.apache.jetspeed.portalsite.view.PhysicalSiteView;
import org.apache.jetspeed.portalsite.view.SearchPathsSiteView;
import org.apache.jetspeed.portalsite.view.SiteViewMenuDefinitionLocator;
import org.apache.jetspeed.profiler.ProfileLocator;
import org.apache.jetspeed.profiler.ProfileLocatorProperty;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class encapsulates managed session state for and
 * interface to the portal-site component and subscribes
 * to page manager and session events to flush stale state.
 *
 * Note that is object is Serializable since it is designed
 * to be cached in the session. However, because this object
 * is cached only for these two reasons:
 *
 * 1. a performance optimization to reuse SiteViews, and 
 * 2. to hold optional folder page history,
 *
 * this object need not be relocatable between J2 instances.
 * Consequently, all data members are marked transient and
 * the isValid() method is used to test whether this object
 * is a valid context for the session or if it was
 * transferred from another server or the persistent session
 * store and needs to be discarded.
 * 
 * @author <a href="mailto:rwatler@apache.org">Randy Watler</a>
 * @version $Id: PortalSiteSessionContextImpl.java 995680 2010-09-10 06:14:22Z rwatler $
 */
public class PortalSiteSessionContextImpl implements PortalSiteSessionContext, PageManagerEventListener, HttpSessionActivationListener, HttpSessionBindingListener, Serializable
{
    /**
     * log - logging instance
     */
    private final static Logger log = LoggerFactory.getLogger(PortalSiteSessionContextImpl.class);

    /**
     * pageManager - PageManager component
     */
    private transient PageManager pageManager;

    /**
     * contentTypeMapper - PortalSiteContentTypeMapper component
     */
    private transient PortalSiteContentTypeMapper contentTypeMapper;

    /**
     * profileLocators - map of session profile locators by locator names
     */
    private transient Map profileLocators;

    /**
     * forceReservedVisible - force reserved/hidden folders visible in site view
     */
    private transient boolean forceReservedVisible;

    /**
     * userPrincipal - session user principal
     */
    private transient String userPrincipal;

    /**
     * siteView - session site view
     */
    private transient AbstractSiteView siteView;

    /**
     * folderPageHistory - map of last page visited by folder 
     */
    private transient Map folderPageHistory;

    /**
     * menuDefinitionLocatorCache - cached menu definition locators for
     *                              absolute menus valid for session
     */
    private transient Map menuDefinitionLocatorCache;

    /**
     * subscribed - flag that indicates whether this context
     *              is subscribed as event listeners
     */
    private transient boolean subscribed;

    /**
     * stale - flag that indicates whether the state
     *         managed by this context is stale
     */
    private transient boolean stale;

    /**
     * locatorsLastUpdateCheck - time stamp of last locators update check.
     */
    private transient long locatorsLastUpdateCheck;

    /**
     * PortalSiteSessionContextImpl - constructor
     *
     * @param pageManager PageManager component instance
     * @param contentTypeMapper PortalSiteContentTypeMapper component instance
     */
    public PortalSiteSessionContextImpl(PageManager pageManager, PortalSiteContentTypeMapper contentTypeMapper)
    {
        this.pageManager = pageManager;
        this.contentTypeMapper = contentTypeMapper;
    }

    /**
     * newRequestContext - create a new request context instance with fallback and history
     *
     * @param requestProfileLocators request profile locators
     * @param requestUserPrincipal request user principal
     * @return new request context instance
     */
    public PortalSiteRequestContext newRequestContext(Map requestProfileLocators, String requestUserPrincipal)
    {
        return new PortalSiteRequestContextImpl(this, requestProfileLocators, requestUserPrincipal, true, true, false, false);
    }

    /**
     * newRequestContext - create a new request context instance with history
     *
     * @param requestProfileLocators request profile locators
     * @param requestUserPrincipal request user principal
     * @param requestFallback flag specifying whether to fallback to root folder
     *                        if locators do not select a page or access is forbidden
     * @return new request context instance
     */
    public PortalSiteRequestContext newRequestContext(Map requestProfileLocators, String requestUserPrincipal, boolean requestFallback)
    {
        return new PortalSiteRequestContextImpl(this, requestProfileLocators, requestUserPrincipal, requestFallback, true, false, false);
    }

    /**
     * newRequestContext - create a new request context instance
     *
     * @param requestProfileLocators request profile locators
     * @param requestUserPrincipal request user principal
     * @param requestFallback flag specifying whether to fallback to root folder
     *                        if locators do not select a page or access is forbidden
     * @param useHistory flag indicating whether to use visited page
     *                   history to select default page per site folder
     * @return new request context instance
     */
    public PortalSiteRequestContext newRequestContext(Map requestProfileLocators, String requestUserPrincipal, boolean requestFallback, boolean useHistory)
    {
        return new PortalSiteRequestContextImpl(this, requestProfileLocators, requestUserPrincipal, requestFallback, useHistory, false, false);
    }

    /**
     * newRequestContext - create a new request context instance
     *
     * @param requestProfileLocators request profile locators
     * @param requestUserPrincipal request user principal
     * @param requestFallback flag specifying whether to fallback to root folder
     *                        if locators do not select a page or access is forbidden
     * @param useHistory flag indicating whether to use visited page
     *                   history to select default page per site folder
     * @param forceReservedVisible force reserved/hidden folders visible in site view
     * @param forceTemplatesAccessible force templates accessible to requests in site view
     * @return new request context instance
     */
    public PortalSiteRequestContext newRequestContext(Map requestProfileLocators, String requestUserPrincipal, boolean requestFallback, boolean useHistory, boolean forceReservedVisible, boolean forceTemplatesAccessible)
    {
        return new PortalSiteRequestContextImpl(this, requestProfileLocators, requestUserPrincipal, requestFallback, useHistory, forceReservedVisible, forceTemplatesAccessible);
    }

    /**
     * newRequestContext - create a new request context instance without profiling
     *                     support with fallback and history
     *
     * @param requestPath request path
     * @param requestServerName request server name
     * @param requestUserPrincipal request user principal
     * @return new request context instance
     */
    public PortalSiteRequestContext newRequestContext(String requestPath, String requestServerName, String requestUserPrincipal)
    {
        return new PortalSiteRequestContextImpl(this, requestPath, requestServerName, requestUserPrincipal, true, true, false);
    }

    /**
     * newRequestContext - create a new request context instance without profiling
     *                     support with history
     *
     * @param requestPath request path
     * @param requestServerName request server name
     * @param requestUserPrincipal request user principal
     * @param requestFallback flag specifying whether to fallback to root folder
     *                        if locators do not select a page or access is forbidden
     * @return new request context instance
     */
    public PortalSiteRequestContext newRequestContext(String requestPath, String requestServerName, String requestUserPrincipal, boolean requestFallback)
    {
        return new PortalSiteRequestContextImpl(this, requestPath, requestServerName, requestUserPrincipal, requestFallback, true, false);
    }

    /**
     * newRequestContext - create a new request context instance without profiling
     *                     support
     *
     * @param requestPath request path
     * @param requestServerName request server name
     * @param requestUserPrincipal request user principal
     * @param requestFallback flag specifying whether to fallback to root folder
     *                        if locators do not select a page or access is forbidden
     * @param useHistory flag indicating whether to use visited page
     *                   history to select default page per site folder
     * @return new request context instance
     */
    public PortalSiteRequestContext newRequestContext(String requestPath, String requestServerName, String requestUserPrincipal, boolean requestFallback, boolean useHistory)
    {
        return new PortalSiteRequestContextImpl(this, requestPath, requestServerName, requestUserPrincipal, requestFallback, useHistory, false);
    }

    /**
     * newRequestContext - create a new request context instance without profiling
     *                     support
     *
     * @param requestPath request path
     * @param requestServerName request server name
     * @param requestUserPrincipal request user principal
     * @param requestFallback flag specifying whether to fallback to root folder
     *                        if locators do not select a page or access is forbidden
     * @param useHistory flag indicating whether to use visited page
     *                   history to select default page per site folder
     * @param forceTemplatesAccessible force templates accessible to requests in site view
     * @return new request context instance
     */
    public PortalSiteRequestContext newRequestContext(String requestPath, String requestServerName, String requestUserPrincipal, boolean requestFallback, boolean useHistory, boolean forceTemplatesAccessible)
    {
        return new PortalSiteRequestContextImpl(this, requestPath, requestServerName, requestUserPrincipal, requestFallback, useHistory, forceTemplatesAccessible);
    }

    /**
     * selectRequestPageOrTemplate - select page or template view for request for
     *                               path and server
     *
     * @param requestPath request path
     * @param requestServerName request server name
     * @param requestUserPrincipal request user principal
     * @param requestFallback flag specifying whether to fallback to root folder
     *                        if locators do not select a page or access is forbidden
     * @param useHistory flag indicating whether to use visited page
     *                   history to select default page per site folder
     * @param forceTemplatesAccessible force templates accessible to requests in site view
     * @param requestPageContentPath returned content path associated with selected page
     * @return selected page view for request
     * @throws NodeNotFoundException if not found
     * @throws SecurityException if view access not granted
     */
    public BaseFragmentsElement selectRequestPageOrTemplate(String requestPath, String requestServerName, String requestUserPrincipal, boolean requestFallback, boolean useHistory, boolean forceTemplatesAccessible, String [] requestPageContentPath) throws NodeNotFoundException
    {
        return selectRequestPageOrTemplate(null, requestPath, requestServerName, requestUserPrincipal, requestFallback, useHistory, false, forceTemplatesAccessible, requestPageContentPath);
    }

    /**
     * selectRequestPageOrTemplate - select page or template view for request given
     *                               profile locators
     *
     * @param requestProfileLocators map of profile locators for request
     * @param requestUserPrincipal request user principal
     * @param requestFallback flag specifying whether to fallback to root folder
     *                        if locators do not select a page or access is forbidden
     * @param useHistory flag indicating whether to use visited page
     *                   history to select default page per site folder
     * @param forceReservedVisible force reserved/hidden folders visible for request
     * @param forceTemplatesAccessible force templates accessible to requests in site view
     * @param requestPageContentPath returned content path associated with selected page
     * @return selected page view for request
     * @throws NodeNotFoundException if not found
     * @throws SecurityException if view access not granted
     */
    public BaseFragmentsElement selectRequestPageOrTemplate(Map requestProfileLocators, String requestUserPrincipal, boolean requestFallback, boolean useHistory, boolean forceReservedVisible, boolean forceTemplatesAccessible, String [] requestPageContentPath) throws NodeNotFoundException
    {
        return selectRequestPageOrTemplate(requestProfileLocators, null, null, requestUserPrincipal, requestFallback, useHistory, forceReservedVisible, forceTemplatesAccessible, requestPageContentPath);
    }
    
    /**
     * selectRequestPageOrTemplate - select page or template view for request
     *
     * @param requestProfileLocators map of profile locators for request
     * @param requestPath request path if no locators specified
     * @param requestServerName request server name if no locators specified
     * @param requestUserPrincipal request user principal
     * @param requestFallback flag specifying whether to fallback to root folder
     *                        if locators do not select a page or access is forbidden
     * @param useHistory flag indicating whether to use visited page
     *                   history to select default page per site folder
     * @param forceReservedVisible force reserved/hidden folders visible for request
     * @param forceTemplatesAccessible force templates accessible to requests in site view
     * @param requestPageContentPath returned content path associated with selected page
     * @return selected page view for request
     * @throws NodeNotFoundException if not found
     * @throws SecurityException if view access not granted
     */
    private BaseFragmentsElement selectRequestPageOrTemplate(Map requestProfileLocators, String requestPath, String requestServerName, String requestUserPrincipal, boolean requestFallback, boolean useHistory, boolean forceReservedVisible, boolean forceTemplatesAccessible, String [] requestPageContentPath) throws NodeNotFoundException
    {
        // validate and update session profile locators if modified
        if (updateSessionProfileLocators(requestProfileLocators, requestUserPrincipal, forceReservedVisible))
        {
            // extract page request path and server from the locators
            // if specified, otherwise use specified parameters
            ProfileLocator locator = null;
            if (requestProfileLocators != null)
            {
                requestPath = Folder.PATH_SEPARATOR;
                requestServerName = null;
                locator = (ProfileLocator)requestProfileLocators.get(ProfileLocator.PAGE_LOCATOR);
                if (locator != null)
                {
                    // use 'page' locator to determine request page by executing
                    // profile locator to determine path
                    requestPath = getRequestPathFromLocator(locator);
                }
                else
                {
                    // 'page' locator unavailable, use first locator since
                    // all locators should have identical request paths, (do
                    // not execute profile locator though to determine path:
                    // simply use the request path)
                    locator = (ProfileLocator)requestProfileLocators.values().iterator().next();
                    requestPath = locator.getRequestPath();
                }
                requestServerName = locator.getRequestServerName();
            }
            if (log.isDebugEnabled())
            {
                log.debug("Select request page: requestPath="+requestPath+", requestServerName: "+requestServerName);
            }

            // determine content mapping for request path if content type
            // mapper configured for context
            if (contentTypeMapper != null)
            {
                // test for system page or folder request mappings; if system
                // types matched, continue below with native portal resolution
                // of request
                String systemType = contentTypeMapper.mapSystemType(requestPath);
                if ((systemType != null) && contentTypeMapper.isContentTypeFallbackEnabled())
                {
                    // test for exact system match if content fallback enabled;
                    // clear system type if not found
                    AbstractSiteView view = getSiteView();
                    if (view != null)
                    {
                        try
                        {
                            view.getNodeView(requestPath, null, null, !forceTemplatesAccessible, false, false);
                        }
                        catch (NodeNotFoundException nnfe)
                        {
                            // log fallback mapping
                            if (log.isDebugEnabled())
                            {
                                log.debug("System request: requestPath="+requestPath+" does not exist: fallback to content type");
                            }                    
                            systemType = null;
                        }
                    }
                }
                // if not system type, test for content type
                if (systemType == null)
                {
                    // test for content type mappings; if no content type matched
                    // request, continue with native portal resolution of request
                    String contentType = contentTypeMapper.mapContentType(requestPath);
                    if (contentType != null)
                    {
                        // log mapping
                        if (log.isDebugEnabled())
                        {
                            log.debug("Content request: requestPath="+requestPath+", mapped to content type: "+contentType);
                        }
                        
                        // generate external content path if mapping defined
                        String contentPageRequestPath = contentTypeMapper.mapContentRequestPath(requestServerName, contentType, requestPath);
                        if (contentPageRequestPath != null)
                        {
                            // log mapping
                            if (log.isDebugEnabled())
                            {
                                log.debug("Mapped content request to content path: serverName="+requestServerName+", contentType="+contentType+", requestPath="+requestPath+", mapped to: "+contentPageRequestPath);
                            }
                            
                            if (requestPageContentPath != null)
                            {
                                requestPageContentPath[0] = contentPageRequestPath;
                            }
                        }

                        // support request path mapping of system page requests
                        String systemPageRequestPath = contentTypeMapper.mapSystemRequestPath(requestServerName, contentType, requestPath);
                        if (systemPageRequestPath != null)
                        {
                            // verify system page existence
                            AbstractSiteView view = getSiteView();
                            if (view != null)
                            {
                                try
                                {
                                    if (view.getNodeView(systemPageRequestPath, null, null, true, false, false) instanceof Page)
                                    {
                                        systemType = PortalSiteContentTypeMapper.PAGE_SYSTEM_TYPE;
                                    }
                                }
                                catch (NodeNotFoundException nnfe)
                                {
                                }
                            }

                            // log mapping
                            if (systemType != null)
                            {
                                if (log.isDebugEnabled())
                                {
                                    log.debug("Mapped content request to existing system page: serverName="+requestServerName+", contentType="+contentType+", requestPath="+requestPath+", mapped to: "+systemPageRequestPath);
                                }
 
                                requestPath = systemPageRequestPath;
                            }
                        }

                        // if not mapped to a system type, continue with content
                        // dynamic page selection 
                        if (systemType == null)
                        {
                            // support request path mapping of dynamic page requests
                            String dynamicPageRequestPath = contentTypeMapper.mapDynamicRequestPath(requestServerName, contentType, requestPath);
                            if (dynamicPageRequestPath != null)
                            {
                                // log mapping
                                if (log.isDebugEnabled())
                                {
                                    log.debug("Mapped content request to dynamic page: serverName="+requestServerName+", contentType="+contentType+", requestPath="+requestPath+", mapped to: "+dynamicPageRequestPath);
                                }
                                
                                requestPath = dynamicPageRequestPath;
                            }

                            // attempt to match content request against dynamic pages
                            // using profile locators and site view; start at root
                            // folder until path no longer matches and search from
                            // there back up toward the root for dynamic pages by
                            // content type; fallback to wildcard content type
                            return selectContentRequestPage(requestPath, contentType);
                        }
                    }
                }
                else
                {
                    // log mapping
                    if (log.isDebugEnabled())
                    {
                        log.debug("System request: requestPath="+requestPath+", mapped to system type: "+systemType);
                    }                    
                }
            }
            
            // attempt to select request page, folder or template using
            // profile locators and site view; if fallback enabled,
            // fallback on missing node or access exceptions to the parent
            // folders until the root folder access has been attempted
            do
            {
                // attempt to access requested path
                Exception fallbackException = null;
                try
                {
                    return selectRequestPageOrTemplate(requestPath, useHistory, forceTemplatesAccessible);
                }
                catch (NodeNotFoundException nnfe)
                {
                    if (!requestFallback || requestPath.equals(Folder.PATH_SEPARATOR))
                    {
                        throw nnfe;
                    }
                    fallbackException = nnfe;
                }
                catch (SecurityException se)
                {
                    if (!requestFallback || requestPath.equals(Folder.PATH_SEPARATOR))
                    {
                        throw se;
                    }
                    fallbackException = se;
                }

                // compute fallback request path
                if (requestFallback && !requestPath.equals(Folder.PATH_SEPARATOR))
                {
                    // compute parent folder fallback request path
                    String fallbackRequestPath = requestPath;
                    while (fallbackRequestPath.endsWith(Folder.PATH_SEPARATOR))
                    {
                        fallbackRequestPath = fallbackRequestPath.substring(0, fallbackRequestPath.length()-1);
                    }
                    int folderIndex = fallbackRequestPath.lastIndexOf(Folder.PATH_SEPARATOR);
                    if (folderIndex >= 2)
                    {
                        // fallback to parent folder
                        fallbackRequestPath = fallbackRequestPath.substring(0, folderIndex);
                    }
                    else
                    {
                        // fallback to root folder
                        fallbackRequestPath = Folder.PATH_SEPARATOR;
                    }

                    // check fallback path and log fallback operation
                    if (!fallbackRequestPath.equals(requestPath))
                    {
                        // log fallback
                        if (log.isDebugEnabled())
                        {
                            log.debug("Missing/forbidden page selection fallback: request path=" + requestPath + ", attempting fallback request path=" + fallbackRequestPath, fallbackException);
                        }
                        
                        // clear all history entries for fallback
                        // request path in advance to make fallback
                        // page selection more predictable
                        Iterator folderPathIter = getFolderPageHistory().keySet().iterator();
                        while (folderPathIter.hasNext())
                        {
                            String folderPath = (String)folderPathIter.next();
                            if (folderPath.equals(fallbackRequestPath))
                            {
                                folderPathIter.remove();
                                break;
                            }
                        }

                        // retry requested page access
                        requestPath = fallbackRequestPath;
                    }
                }
                else
                {
                    // fallback attempts complete: no page found for user
                    break;
                }
            }
            while (true);
        }

        // no request page available
        throw new NodeNotFoundException("No request page available in site view.");
    }

    /**
     * getRequestPathFromLocator - execute profile locator to extract
     *                             request path using locator rules; this
     *                             is request specific and is not part of
     *                             the site view
     *
     * @param locator profile locator to execute
     * @return request path from profile locator
     */
    private String getRequestPathFromLocator(ProfileLocator locator)
    {
        // use profile iterator to process the initial full
        // set of profile locator properties searching for
        // the first non control/navigation, (i.e. page/path),
        // property that will force the request path if
        // non-null; otherwise default to locator request path
        String requestPath = locator.getRequestPath();
        Iterator locatorIter = locator.iterator();
        if (locatorIter.hasNext())
        {
            ProfileLocatorProperty [] properties = (ProfileLocatorProperty []) locatorIter.next();
            for (int i = 0; (i < properties.length); i++)
            {
                if (!properties[i].isControl() && !properties[i].isNavigation())
                {
                    // request page/path property; append to or replace
                    // using locator specified path
                    String path = properties[i].getValue();
                    if (path != null)
                    {
                        // specified page/path to be appended to request path if
                        // relative; otherwise specified page/path to replace
                        // request path
                        if (!path.startsWith(Folder.PATH_SEPARATOR))
                        {
                            // strip page from request path if required
                            // and append page/path to base request path
                            String basePath = requestPath;
                            if (basePath == null)
                            {
                                basePath = Folder.PATH_SEPARATOR;
                            }
                            else if (basePath.endsWith(Page.DOCUMENT_TYPE))
                            {
                                basePath = basePath.substring(0, basePath.lastIndexOf(Folder.PATH_SEPARATOR)+1);
                            }
                            else if (!basePath.endsWith(Folder.PATH_SEPARATOR))
                            {
                                basePath += Folder.PATH_SEPARATOR;
                            }
                            path = basePath + path;

                            // make sure path ends in page extension
                            // if folder not explicitly specified
                            if (!path.endsWith(Folder.PATH_SEPARATOR) && !path.endsWith(Page.DOCUMENT_TYPE))
                            {
                                path += Page.DOCUMENT_TYPE;
                            }
                        }

                        // detect profile locator request path modification
                        if (!path.equals(requestPath))
                        {
                            // if modified request path ends with default page,
                            // strip default page from path to allow folder level
                            // defaulting to take place: locator should not force
                            // selection of default page when selection of the
                            // folder is implied by use in locator page/path
                            if (path.endsWith(Folder.PATH_SEPARATOR + Folder.FALLBACK_DEFAULT_PAGE))
                            {
                                path = path.substring(0, path.length() - Folder.FALLBACK_DEFAULT_PAGE.length());
                            }
                            
                            // log modified page request
                            if (log.isDebugEnabled() && !path.equals(requestPath))
                            {
                                log.debug("Request path modified by profile locator: request path=" + path + ", original request path=" + requestPath);
                            }
                            return path;
                        }
                    }
                }
            }
        }

        // return locator request path
        return requestPath;
    }

    /**
     * selectRequestPageOrTemplate - select page or template view for request for
     *                               specified path against site view associated
     *                               with this context
     *
     * @param requestPath request path
     * @param useHistory flag indicating whether to use visited page
     *                   history to select default page per site folder
     * @param forceTemplatesAccessible force templates accessible to requests in site view
     * @return selected page or template view for request
     * @throws NodeNotFoundException if not found
     * @throws SecurityException if view access not granted
     */
    private BaseFragmentsElement selectRequestPageOrTemplate(String requestPath, boolean useHistory, boolean forceTemplatesAccessible) throws NodeNotFoundException
    {
        // save access exceptions
        SecurityException accessException = null;

        // valid SiteView required from session profile locators
        AbstractSiteView view = getSiteView();
        if (view != null)
        {
            // default request to root folder if not specified
            if (requestPath == null)
            {
                requestPath = Folder.PATH_SEPARATOR;
            }
            
            // log page request
            if (log.isDebugEnabled())
            {
                log.debug("Request page: request path=" + requestPath);
            }

            // lookup request path in view for viewable page or folder
            // nodes; note: directly requested pages/folders may be hidden
            // or not viewable
            Node requestNode = null;
            try
            {
                // try page or folder request url
                requestNode = view.getNodeView(requestPath, null, null, !forceTemplatesAccessible, false, false);
            }
            catch (NodeNotFoundException nnfe)
            {
                // if request path ends with default page, strip from
                // request url to retry for folder default
                if (requestPath.endsWith(Folder.PATH_SEPARATOR + Folder.FALLBACK_DEFAULT_PAGE))
                {
                    // retry folder request url
                    requestPath = requestPath.substring(0, requestPath.length() - Folder.FALLBACK_DEFAULT_PAGE.length());
                    requestNode = view.getNodeView(requestPath, null, null, !forceTemplatesAccessible, true, false);
                }
                else
                {
                    // rethrow original exception
                    throw nnfe;
                }
            }
            
            // invoke default page logic to determine folder page
            if (requestNode instanceof Folder)
            {
                Folder requestFolder = (Folder)requestNode;
                
                // support subfolders specified as default pages;
                // find highest subfolder with a default page that
                // specifies a default folder, (not a default page).
                try
                {
                    String defaultFolderName = requestFolder.getDefaultPage();
                    if (defaultFolderName != null)
                    {
                        // do not follow broken default folders
                        Folder defaultRequestFolder = requestFolder;
                        // follow default folders to parent folders
                        while ((defaultRequestFolder != null) && (defaultFolderName != null) &&
                               defaultFolderName.equals(".."))
                        {
                            defaultRequestFolder = (Folder)defaultRequestFolder.getParent();
                            if (defaultRequestFolder != null)
                            {
                                defaultFolderName = defaultRequestFolder.getDefaultPage();
                            }
                            else
                            {
                                defaultFolderName = null;
                            }
                        }
                        // follow default folders to subfolders
                        while ((defaultRequestFolder != null) && (defaultFolderName != null) &&
                               !defaultFolderName.endsWith(Page.DOCUMENT_TYPE) && !defaultFolderName.equals(".."))
                        {
                            defaultRequestFolder = defaultRequestFolder.getFolder(defaultFolderName);
                            defaultFolderName = defaultRequestFolder.getDefaultPage();
                        }
                        // use default request folder
                        if (defaultRequestFolder != null)
                        {
                            requestFolder = defaultRequestFolder;
                        }
                    }
                }
                catch (NodeException ne)
                {
                }
                catch (NodeNotFoundException nnfe)
                {
                }
                catch (SecurityException se)
                {
                    requestFolder = null;
                    accessException = se;
                }

                // only request folders with pages can be
                // selected by request; otherwise, fall back to
                // parent folders assuming that immediate parents
                // will have the most appropriate default page
                NodeSet requestFolderPages = null;
                if (requestFolder != null)
                {
                    try
                    {
                        requestFolderPages = requestFolder.getPages();
                        while (((requestFolderPages == null) || requestFolderPages.isEmpty()) && (requestFolder.getParent() != null))
                        {
                            requestFolder = (Folder)requestFolder.getParent();
                            requestFolderPages = requestFolder.getPages();
                        }
                    }
                    catch (NodeException ne)
                    {
                        requestFolderPages = null;
                    }
                    catch (SecurityException se)
                    {
                        requestFolderPages = null;
                        accessException = se;
                    }
                }
                if ((requestFolder != null) && (requestFolderPages != null) && !requestFolderPages.isEmpty())
                {
                    Page requestPage = null;

                    // attempt to lookup last visited page by folder path;
                    // page id test must be performed since identical paths
                    // may occur in multiple site views
                    if (useHistory)
                    {
                        String requestPageId = (String)getFolderPageHistory().get(requestFolder.getPath());
                        if (requestPageId != null)
                        {
                            // find page by id in request folder pages
                            Iterator requestFolderPagesIter = requestFolderPages.iterator();
                            while ((requestPage == null) && (requestFolderPagesIter.hasNext()))
                            {
                                Page requestFolderPage = (Page)requestFolderPagesIter.next();
                                if (requestPageId.equals(requestFolderPage.getId()))
                                {
                                    requestPage = requestFolderPage;
                                }
                            }
                            
                            // log selected request page
                            if (requestPage != null)
                            {
                                if (log.isDebugEnabled())
                                {
                                    log.debug("Selected folder historical page: path=" + view.getManagedPage(requestPage).getPath());
                                }
                                return requestPage;
                            }
                        }
                    }
                    
                    // get default page for folder view if more than one
                    // page is available to choose from
                    if (requestFolderPages.size() > 1)
                    {
                        String defaultPageName = requestFolder.getDefaultPage();
                        if (defaultPageName == null)
                        {
                            // use fallback default if default page
                            // not explicitly specified
                            defaultPageName = Folder.FALLBACK_DEFAULT_PAGE;
                        }
                        try
                        {
                            // save last visited non-hidden page for folder path
                            // and return default page
                            requestPage = requestFolder.getPage(defaultPageName);
                            if (!requestPage.isHidden())
                            {
                                getFolderPageHistory().put(requestFolder.getPath(), requestPage.getId());
                            }
                            
                            // log selected request page
                            if (log.isDebugEnabled())
                            {
                                log.debug("Selected folder default page: path=" + view.getManagedPage(requestPage).getPath());
                            }
                            return requestPage;
                        }
                        catch (NodeException ne)
                        {
                        }
                        catch (NodeNotFoundException nnfe)
                        {
                        }
                        catch (SecurityException se)
                        {
                            accessException = se;
                        }
                    }
                    
                    // default page not available, select first page
                    // view in request folder; save last visited
                    // non-hidden page for folder path and return default page
                    requestPage = (Page)requestFolderPages.iterator().next();
                    if (!requestPage.isHidden())
                    {
                        getFolderPageHistory().put(requestFolder.getPath(), requestPage.getId());
                    }

                    // log selected request page
                    if (log.isDebugEnabled())
                    {
                        log.debug("Selected first folder page, path=" + view.getManagedPage(requestPage).getPath());
                    }
                    return requestPage;
                }
            }
            else if (requestNode instanceof Page)
            {
                Page requestPage = (Page)requestNode;
                
                // save last visited non-hidden page for folder path
                // and return matched page
                Folder requestFolder = (Folder)requestPage.getParent();
                if (!requestPage.isHidden())
                {
                	getFolderPageHistory().put(requestFolder.getPath(), requestPage.getId());
                }

                // log selected request page
                if (log.isDebugEnabled())
                {
                    log.debug("Selected page, path=" + view.getManagedPage(requestPage).getPath());
                }
                return requestPage;
            }
            else if (forceTemplatesAccessible)
            {
                if (requestNode instanceof PageTemplate)
                {
                    PageTemplate requestPageTemplate = (PageTemplate)requestNode;
                    
                    // log selected request page template
                    if (log.isDebugEnabled())
                    {
                        log.debug("Selected page template, path=" + view.getManagedPageTemplate(requestPageTemplate).getPath());
                    }
                    return requestPageTemplate;
                }
                else if (requestNode instanceof DynamicPage)
                {
                    DynamicPage requestDynamicPage = (DynamicPage)requestNode;
                    
                    // log selected request dynamic page
                    if (log.isDebugEnabled())
                    {
                        log.debug("Selected dynamic page, path=" + view.getManagedDynamicPage(requestDynamicPage).getPath());
                    }
                    return requestDynamicPage;
                }
                else if (requestNode instanceof FragmentDefinition)
                {
                    FragmentDefinition requestFragmentDefinition = (FragmentDefinition)requestNode;
                    
                    // log selected request fragment definition
                    if (log.isDebugEnabled())
                    {
                        log.debug("Selected fragment definition, path=" + view.getManagedFragmentDefinition(requestFragmentDefinition).getPath());
                    }
                    return requestFragmentDefinition;
                }
            }
        }
            
        // no page matched or accessible
        if (accessException != null)
        {
            throw accessException;
        }
        throw new NodeNotFoundException("No page matched " + requestPath + " request in site view.");
    }
    
    /**
     * selectContentRequestPage - select dynamic page view for request for
     *                            specified content request path and content
     *                            type given profile locators and site view
     *                            associated with this context
     *
     * @param requestPath request path
     * @param contentType content type
     * @return selected dynamic page view for request
     * @throws NodeNotFoundException if not found
     * @throws SecurityException if view access not granted
     */
    private DynamicPage selectContentRequestPage(String requestPath, String contentType) throws NodeNotFoundException
    {
        // save access exceptions
        SecurityException accessException = null;

        // valid SiteView required from session profile locators
        AbstractSiteView view = getSiteView();
        if (view != null)
        {
            // default request to root folder if not specified
            if (requestPath == null)
            {
                requestPath = Folder.PATH_SEPARATOR;
            }
            
            // log content page request
            if (log.isDebugEnabled())
            {
                log.debug("Request content page: request path=" + requestPath);
            }
            
            // search for deepest matching content request path
            // that matches request path; start with root folder in view
            Folder contentRequestFolder = (Folder)view.getNodeView(Folder.PATH_SEPARATOR, null, null, true, false, false);
            String contentRequestPath = contentRequestFolder.getPath();
            for (;;)
            {
                // find next path name
                int startOfPathNameIndex = (contentRequestPath.equals(Folder.PATH_SEPARATOR) ? 1 : contentRequestPath.length()+1);
                int endOfPathNameIndex = requestPath.indexOf(Folder.PATH_SEPARATOR, startOfPathNameIndex);
                if ((endOfPathNameIndex == -1) || (endOfPathNameIndex == startOfPathNameIndex))
                {
                    break;
                }
                // find folder in view
                try
                {
                    String pathFolderName = requestPath.substring(startOfPathNameIndex, endOfPathNameIndex);
                    contentRequestFolder = contentRequestFolder.getFolder(pathFolderName);
                    contentRequestPath = contentRequestFolder.getPath();
                }
                catch (Exception e)
                {
                    break;
                }
            }
            
            // determine if content request folder matches request
            // folder assuming request path may include a content
            // document name, but no deeper folders
            boolean matchingContentRequestFolder = true;
            if (contentRequestPath.length() < requestPath.length())
            {
                String requestPathTail = requestPath.substring(contentRequestPath.length());
                matchingContentRequestFolder = (requestPathTail.indexOf(Folder.PATH_SEPARATOR_CHAR, (requestPathTail.startsWith(Folder.PATH_SEPARATOR) ? 1 : 0)) == -1);
            }
            
            // select matching dynamic pages in folders from deepest
            // to root folders in content request path
            while (contentRequestFolder != null)
            {
                // select dynamic page by content type or wildcard match
                try
                {
                    NodeSet dynamicPages = contentRequestFolder.getDynamicPages();
                    if ((dynamicPages != null) && !dynamicPages.isEmpty())
                    {
                        // select matching page
                        DynamicPage matchingPage = null;
                        DynamicPage inheritableMatchingPage = null;
                        DynamicPage wildcardMatchingPage = null;
                        Iterator dynamicPagesIter = dynamicPages.iterator();
                        while (dynamicPagesIter.hasNext())
                        {
                            DynamicPage dynamicPage = (DynamicPage)dynamicPagesIter.next();
                            if ((dynamicPage.getContentType() == null) || dynamicPage.getContentType().equals(DynamicPage.WILDCARD_CONTENT_TYPE))
                            {
                                wildcardMatchingPage = dynamicPage;
                            }
                            else if (dynamicPage.getContentType().equals(contentType))
                            {
                                boolean inheritableDynamicPage = dynamicPage.isInheritable();
                                if (matchingContentRequestFolder && !inheritableDynamicPage)
                                {
                                    matchingPage = dynamicPage;
                                }
                                else if (inheritableDynamicPage)
                                {
                                    inheritableMatchingPage = dynamicPage;
                                }
                            }
                        }
                        // select matching page
                        if (matchingPage != null)
                        {
                            // log selected dynamic page
                            if (log.isDebugEnabled())
                            {
                                log.debug("Selected "+contentType+" content dynamic page, path=" + view.getManagedDynamicPage(matchingPage).getPath());
                            }
                            return matchingPage;
                        }
                        // select inheritable matching page
                        if (inheritableMatchingPage != null)
                        {
                            // log selected dynamic page
                            if (log.isDebugEnabled())
                            {
                                log.debug("Selected "+contentType+" inheritable content dynamic page, path=" + view.getManagedDynamicPage(inheritableMatchingPage).getPath());
                            }
                            return inheritableMatchingPage;
                        }
                        // select wildcard matching page
                        if (wildcardMatchingPage != null)
                        {
                            // log selected dynamic page
                            if (log.isDebugEnabled())
                            {
                                log.debug("Selected "+contentType+" wildcard content dynamic page, path=" + view.getManagedDynamicPage(wildcardMatchingPage).getPath());
                            }
                            return wildcardMatchingPage;
                        }
                    }
                }
                catch (NodeException ne)
                {
                    break;
                }
                catch (SecurityException se)
                {
                    accessException = se;
                }

                // continue search with parent folder
                contentRequestFolder = (Folder)contentRequestFolder.getParent();
                matchingContentRequestFolder = false;
            }
        }
            
        // no dynamic page matched or accessible
        if (accessException != null)
        {
            throw accessException;
        }
        throw new NodeNotFoundException("No dynamic page matched " + requestPath + " request in site view.");
    }

    /**
     * getRequestRootFolder - select root folder view for given profile locators
     *
     * @param requestUserPrincipal request user principal
     * @return root folder view for request
     * @throws NodeNotFoundException if not found
     * @throws SecurityException if view access not granted
     */
    public Folder getRequestRootFolder(String requestUserPrincipal) throws NodeNotFoundException
    {
        return getRequestRootFolder(null, requestUserPrincipal, false);
    }

    /**
     * getRequestRootFolder - select root folder view for given profile locators
     *
     * @param requestProfileLocators map of profile locators for request
     * @param requestUserPrincipal request user principal
     * @param requestForceReservedVisible force reserved/hidden folders visible for request
     * @return root folder view for request
     * @throws NodeNotFoundException if not found
     * @throws SecurityException if view access not granted
     */
    public Folder getRequestRootFolder(Map requestProfileLocators, String requestUserPrincipal, boolean requestForceReservedVisible) throws NodeNotFoundException
    {
        // validate and update session profile locators if modified
        if (updateSessionProfileLocators(requestProfileLocators, requestUserPrincipal, requestForceReservedVisible))
        {
            // valid site view required from session profile locators
            AbstractSiteView view = getSiteView();
            if (view != null)
            {
                // return root folder view from session site view
                return view.getRootFolderView();
            }
        }

        // no root folder available
        throw new NodeNotFoundException("No root folder available in site view.");
    }

    /**
     * updateSessionProfileLocators - detect modification of and update cached
     *                                session profile locators
     *
     * @param requestProfileLocators map of profile locators for request
     * @param requestUserPrincipal request user principal
     * @param requestForceReservedVisible force reserved/hidden folders visible for request
     * @return profile locators validation flag
     */
    private boolean updateSessionProfileLocators(Map requestProfileLocators, String requestUserPrincipal, boolean requestForceReservedVisible)
    {
        // valid request profile locators are required
        if ((requestProfileLocators == null) || !requestProfileLocators.isEmpty())
        {
            // detect stale session, modification of user
            // principal, or changed profile locators for
            // this session context
            boolean userUpdate = false;
            boolean locatorsUpdate = false;
            boolean forceReservedVisibleUpdate = false;
            boolean updated = false;
            synchronized (this)
            {
                userUpdate = (((userPrincipal == null) && (requestUserPrincipal != null)) ||
                              ((userPrincipal != null) && !userPrincipal.equals(requestUserPrincipal)));
                locatorsUpdate = !locatorsEquals(profileLocators, requestProfileLocators);
                forceReservedVisibleUpdate = (forceReservedVisible != requestForceReservedVisible);
                if (stale || userUpdate || locatorsUpdate || forceReservedVisibleUpdate)
                {
                    // reset cached session profile locators, view,
                    // folder page history, menu definition locators,
                    // and stale flag
                    clearSessionProfileLocators();
                    profileLocators = requestProfileLocators;
                    forceReservedVisible = requestForceReservedVisible;
                    userPrincipal = requestUserPrincipal;
                    updated = true;
                }
                locatorsLastUpdateCheck = System.currentTimeMillis();
            }

            // log session context setup and update
            if (updated && log.isDebugEnabled())
            {
                StringBuffer debug = new StringBuffer();
                if (userUpdate)
                {
                    debug.append("Updated user");
                    if (locatorsUpdate)
                    {
                        debug.append("/locators");
                    }
                    if (forceReservedVisibleUpdate)
                    {
                        debug.append("/force reserved visible");
                    }
                    if (stale)
                    {
                        debug.append("/stale");
                    }
                }
                else if (locatorsUpdate)
                {
                    debug.append("Updated locators");
                    if (forceReservedVisibleUpdate)
                    {
                        debug.append("/force reserved visible");
                    }
                    if (stale)
                    {
                        debug.append("/stale");
                    }
                }
                else if (forceReservedVisibleUpdate)
                {
                    debug.append("Updated force reserved visible");
                    if (stale)
                    {
                        debug.append("/stale");
                    }
                }
                else
                {
                    debug.append("Updated stale");
                }
                debug.append(" context: user=" + requestUserPrincipal + ", profileLocators=(");
                if (requestProfileLocators != null)
                {
                    boolean firstEntry = true;
                    Iterator entriesIter = requestProfileLocators.entrySet().iterator();
                    while (entriesIter.hasNext())
                    {
                        Map.Entry entry = (Map.Entry)entriesIter.next();
                        String locatorName = (String)entry.getKey();
                        ProfileLocator locator = (ProfileLocator)entry.getValue();
                        if (!firstEntry)
                        {
                            debug.append(",");
                        }
                        else
                        {
                            firstEntry = false;
                        }
                        debug.append(locatorName);
                        debug.append("=");
                        debug.append(locator.toString());
                    }
                }
                else
                {
                    debug.append("null");
                }
                debug.append(")");
                log.debug(debug.toString());
            }

            // return valid
            return true;
        }

        // return invalid
        return false;
    }

    /**
     * clearSessionProfileLocators - clear cache session profile locators
     */
    private synchronized void clearSessionProfileLocators()
    {
        // clear cached session profile locators, view,
        // folder page history, menu definition locators,
        // and stale flag
        profileLocators = null;
        userPrincipal = null;
        siteView = null;
        folderPageHistory = null;
        if (menuDefinitionLocatorCache != null)
        {
            menuDefinitionLocatorCache.clear();
        }
        stale = false;
    }

    /**
     * getSiteView - lookup and/or create site view for
     *               profile locators of this context
     *
     * @return site view instance
     */
    public AbstractSiteView getSiteView()
    {
        // get or create site view
        AbstractSiteView view = siteView;
        while (view == null)
        {
            // access site view and test for creation
            Map createViewProfileLocators = null;
            String createViewUserPrincipal = null;
            boolean createView = false;
            synchronized (this)
            {
                view = siteView;
                createView = ((view == null) && (pageManager != null));
                createViewProfileLocators = profileLocators;
                createViewUserPrincipal = userPrincipal;
            }

            // create new site view if necessary
            boolean viewCreated = false;
            if (createView)
            {
                // create site view outside of synchronized state; this is
                // required since construction of site view requires access
                // to the page manager and page manager event notifications
                // may arrive during construction of the site view which
                // might then result in synchronized deadlock with page
                // manager or page manager cache internals
                if (createViewProfileLocators != null)
                {
                    view = new SearchPathsSiteView(pageManager, createViewProfileLocators, forceReservedVisible);
                }
                else
                {
                    view = new PhysicalSiteView(pageManager, userPrincipal);                    
                }

                // update site view if not already made available by another
                // request thread
                synchronized (this)
                {
                    if ((siteView == null) && (pageManager != null) && (profileLocators == createViewProfileLocators) && (userPrincipal == createViewUserPrincipal))
                    {
                        siteView = view;
                        viewCreated = true;
                    }
                    view = siteView;
                }
            }
        
            // log site view creation
            if (viewCreated && log.isDebugEnabled())
            {
                if (view instanceof SearchPathsSiteView)
                {
                    log.debug("Created search path site view: search paths=" + ((SearchPathsSiteView)view).getSearchPathsString());
                }
                else if (view instanceof PhysicalSiteView)
                {
                    log.debug("Created physical site view");
                }
            }
        }
        
        return view;
    }

    /**
     * getPageManager - return PageManager component instance
     *
     * @return PageManager instance
     */
    public PageManager getPageManager()
    {
        return pageManager;
    }

    /**
     * getContentTypeMapper - return PortalSiteContentTypeMapper component instance
     *
     * @return PortalSiteContentTypeMapper instance
     */
    public PortalSiteContentTypeMapper getContentTypeMapper()
    {
        return contentTypeMapper;
    }
    
    /**
     * isValid - return flag indicating whether this context instance
     *           is valid or if it is stale after being persisted and
     *           reloaded as session state
     *
     * @return valid context status
     */
    public boolean isValid()
    {
        // existent transient page manager implies valid context 
        return (pageManager != null);
    }

    /**
     * getProfileLocators - get session profile locators
     */
    public Map getProfileLocators()
    {
        return profileLocators;
    }

    /**
     * getStandardMenuNames - get set of available standard menu names
     *  
     * @return menu names set
     */
    public Set getStandardMenuNames()
    {
        // return standard menu names defined for site view
        AbstractSiteView view = getSiteView();
        return ((view != null) ? view.getStandardMenuNames() : null);            
    }

    /**
     * getMenuDefinitionLocators - get list of node view menu definition
     *                             locators from site view
     *
     * @param node site view node view
     * @return definition locator list
     */
    public List getMenuDefinitionLocators(Node node)
    {
        // return menu definition locators for node in site view
        AbstractSiteView view = getSiteView();
        return ((view != null) ? view.getMenuDefinitionLocators(node) : null);            
    }

    /**
     * getMenuDefinitionLocator - get named node view menu definition
     *                            locator from site view
     *
     * @param node site view node view
     * @param name menu definition name
     * @return menu definition locator
     */
    public SiteViewMenuDefinitionLocator getMenuDefinitionLocator(Node node, String name)
    {
        // return named menu definition locator for node in site view
        AbstractSiteView view = getSiteView();
        return ((view != null) ? view.getMenuDefinitionLocator(node, name) : null);            
    }

    /**
     * getManagedPageOrTemplate - get managed page, page template, dynamic page, or
     *                            fragment definition instance from page view
     *  
     * @param page page view
     * @return managed page
     */
    public BaseFragmentsElement getManagedPageOrTemplate(BaseFragmentsElement pageOrTemplate)
    {
        // return managed page or template in site view
        AbstractSiteView view = getSiteView();
        if (pageOrTemplate instanceof Page)
        {
            return ((view != null) ? view.getManagedPage((Page)pageOrTemplate) : null);
        }
        else if (pageOrTemplate instanceof PageTemplate)
        {
            return ((view != null) ? view.getManagedPageTemplate((PageTemplate)pageOrTemplate) : null);
        }
        else if (pageOrTemplate instanceof DynamicPage)
        {
            return ((view != null) ? view.getManagedDynamicPage((DynamicPage)pageOrTemplate) : null);
        }
        else if (pageOrTemplate instanceof FragmentDefinition)
        {
            return ((view != null) ? view.getManagedFragmentDefinition((FragmentDefinition)pageOrTemplate) : null);
        }
        return null;
    }

    /**
     * getManagedPageTemplate - get concrete page template instance from page
     *                          template view
     *  
     * @param pageTemplate page template view
     * @return managed page template
     */
    public PageTemplate getManagedPageTemplate(PageTemplate pageTemplate)
    {
        // return managed page template in site view
        AbstractSiteView view = getSiteView();
        return ((view != null) ? view.getManagedPageTemplate(pageTemplate) : null);            
    }

    /**
     * getManagedDynamicPage - get concrete dynamic page instance from dynamic
     *                         page view
     *  
     * @param dynamicPage dynamic page view
     * @return managed dynamic page
     */
    public DynamicPage getManagedDynamicPage(DynamicPage dynamicPage)
    {
        // return managed dynamic page in site view
        AbstractSiteView view = getSiteView();
        return ((view != null) ? view.getManagedDynamicPage(dynamicPage) : null);            
    }

    /**
     * getManagedFragmentDefinition - get concrete dynamic page instance from
     *                                fragment definition view
     *  
     * @param fragmentDefinition fragment definition view
     * @return managed fragment definition
     */
    public FragmentDefinition getManagedFragmentDefinition(FragmentDefinition fragmentDefinition)
    {
        // return managed fragment definition in site view
        AbstractSiteView view = getSiteView();
        return ((view != null) ? view.getManagedFragmentDefinition(fragmentDefinition) : null);            
    }

    /**
     * getUserFolderPath - return primary concrete root user folder path
     *
     * @param requestProfileLocators map of profile locators for request
     * @param requestUserPrincipal request user principal
     * @param forceReservedVisible force reserved/hidden folders visible for request
     * @return user folder path or null
     */
    public String getUserFolderPath(Map requestProfileLocators, String requestUserPrincipal, boolean requestForceReservedVisible)
    {
        // validate and update session profile locators if modified
        if (updateSessionProfileLocators(requestProfileLocators, requestUserPrincipal, forceReservedVisible))
        {
            // return user folder path in site view
            AbstractSiteView view = getSiteView();
            return ((view != null) ? view.getUserFolderPath() : null);
        }
        return null;
    }

    /**
     * getBaseFolderPath - return primary concrete root base folder path
     *
     * @param requestProfileLocators map of profile locators for request
     * @param requestUserPrincipal request user principal
     * @param forceReservedVisible force reserved/hidden folders visible for request
     * @return base folder path or null
     */
    public String getBaseFolderPath(Map requestProfileLocators, String requestUserPrincipal, boolean requestForceReservedVisible)
    {
        // validate and update session profile locators if modified
        if (updateSessionProfileLocators(requestProfileLocators, requestUserPrincipal, forceReservedVisible))
        {
            // return base folder path in site view
            AbstractSiteView view = getSiteView();
            return ((view != null) ? view.getBaseFolderPath() : null);
        }
        return null;
    }

    /**
     * getMenuDefinitionLocatorCache - get menu definition locators cache
     *                                 for absolute menus
     *
     * @return menu definition locators cache
     */
    public Map getMenuDefinitionLocatorCache()
    {
        return menuDefinitionLocatorCache;
    }

    /**
     * setMenuDefinitionLocatorCache - set menu definition locators cache
     *                                 for absolute menus
     *
     * @return menu definition locators cache
     */
    public void setMenuDefinitionLocatorCache(Map cache)
    {
        menuDefinitionLocatorCache = cache;
    }
    
    /**
     * locatorsEquals - test profile locator maps for equivalence
     *                  ignoring request specifics
     *
     * @param locators0 request profile locator map
     * @param locators1 request profile locator map
     * @return boolean flag indicating equivalence
     */
    private static boolean locatorsEquals(Map locators0, Map locators1)
    {
        // trivial comparisons
        if (locators0 == locators1)
        {
            return true;
        }
        if ((locators0 == null) || (locators1 == null))
        {
            return false;
        }

        // compare locator map sizes
        if (locators0.size() != locators1.size())
        {
            return false;
        }

        // compare locator map entries
        Iterator entriesIter = locators0.entrySet().iterator();
        if (entriesIter.hasNext())
        {
            Map.Entry entry = (Map.Entry)entriesIter.next();
            ProfileLocator locator0 = (ProfileLocator)entry.getValue();
            ProfileLocator locator1 = (ProfileLocator)locators1.get(entry.getKey());
            if (locator1 == null)
            {
                return false;
            }

            // compare locators using the most specific,
            // (i.e. first), locator properties array
            // returned by the locator iterator
            ProfileLocatorProperty [] properties0 = (ProfileLocatorProperty [])locator0.iterator().next();
            ProfileLocatorProperty [] properties1 = (ProfileLocatorProperty [])locator1.iterator().next();
            if ((properties0 != null) || (properties1 != null))
            {
                if ((properties0 == null) || (properties1 == null) || (properties0.length != properties1.length))
                {
                    return false;
                }
                
                // compare ordered locator properties
                for (int i = 0, limit = properties0.length; (i < limit); i++)
                {
                    // compare property names, control flags, navigation flags,
                    // and values. note: properties values are compared only for
                    // control or navigation properties; otherwise they are
                    // assumed to contain variable request paths that should
                    // be treated as equivalent
                    if (!properties0[i].getName().equals(properties1[i].getName()) ||
                        (properties0[i].isControl() && !properties1[i].isControl()) ||
                        (properties0[i].isNavigation() && !properties1[i].isNavigation()) ||
                        ((properties0[i].isControl() || properties0[i].isNavigation()) && 
                         (((properties0[i].getValue() == null) && (properties1[i].getValue() != null)) ||
                          ((properties0[i].getValue() != null) && !properties0[i].getValue().equals(properties1[i].getValue())))))
                    {
                        return false;
                    }
                }
            }
        }
        return true;
    }

    /**
     * locatorRequestPath - extract request specific path from profile locator
     *                      using request path from locator
     *
     * @param locator request profile locator
     * @return request path
     */     
    private static String locatorRequestPath(ProfileLocator locator)
    {
        // use request path in locator as default
        return locatorRequestPath(locator, locator.getRequestPath());
    }

    /**
     * locatorRequestPath - extract request specific path from profile locator
     *
     * @param locator request profile locator
     * @param requestPath request path
     * @return request path
     */
    private static String locatorRequestPath(ProfileLocator locator, String requestPath)
    {
        // search locator using the most specific,
        // (i.e. first), locator properties array
        // returned by the locator iterator and return
        // first valued property that is not a control
        // or navigation type
        ProfileLocatorProperty [] properties = (ProfileLocatorProperty [])locator.iterator().next();
        for (int i = 0, limit = properties.length; (i < limit); i++)
        {
            if (!properties[i].isControl() && !properties[i].isNavigation() && (properties[i].getValue() != null))
            {
                // use specified locator path
                String locatorPath = properties[i].getValue();

                // return specified locatorPath if absolute
                if (locatorPath.startsWith(Folder.PATH_SEPARATOR))
                {
                    return locatorPath;
                }

                // page names and relative paths are assumed relative to
                // request path and that any locator paths with no url
                // separator should have the page extension appended
                // get default page if page path null
                if ((locatorPath.indexOf(Folder.PATH_SEPARATOR) == -1) && !locatorPath.endsWith(Page.DOCUMENT_TYPE))
                {
                    locatorPath += Page.DOCUMENT_TYPE;
                }
            
                // append locator path to request path, replacing
                // requested pages and preserving requested folders
                boolean rootFolderRequest = requestPath.equals(Folder.PATH_SEPARATOR);
                boolean folderRequest = (!requestPath.endsWith(Page.DOCUMENT_TYPE));
                int lastSeparatorIndex = requestPath.lastIndexOf(Folder.PATH_SEPARATOR_CHAR);
                if ((lastSeparatorIndex > 0) && (!folderRequest || requestPath.endsWith(Folder.PATH_SEPARATOR)))
                {
                    // append locator to request path base path
                    return requestPath.substring(0, lastSeparatorIndex) + Folder.PATH_SEPARATOR + locatorPath;
                }
                else if (!rootFolderRequest && folderRequest)
                {
                    // append locator to request path root folder
                    return requestPath + Folder.PATH_SEPARATOR + locatorPath;
                }
                else
                {
                    // use root folder locator
                    return Folder.PATH_SEPARATOR + locatorPath;
                }
            }
        }
        return requestPath;
    }

    /* (non-Javadoc)
     * @see org.apache.jetspeed.page.PageManagerEventListener#newNode(org.apache.jetspeed.page.document.Node)
     */
    public void newNode(Node node)
    {
        // equivalent to node updated event
        updatedNode(node);
    }

    /* (non-Javadoc)
     * @see org.apache.jetspeed.page.PageManagerEventListener#updatedNode(org.apache.jetspeed.page.document.Node)
     */
    public void updatedNode(Node node)
    {
        // set stale flag to force session context state reset
        synchronized (this)
        {
            stale = true;
        }

        // log updated node event
        if (log.isDebugEnabled())
        {
            if (node != null)
            {
                log.debug("Page manager update event, (node=" + node.getPath() + "): set session context state stale");
            }
            else
            {
                log.debug("Page manager update event: set session context state stale");
            }
        }
    }

    /* (non-Javadoc)
     * @see org.apache.jetspeed.page.PageManagerEventListener#removedNode(org.apache.jetspeed.page.document.Node)
     */
    public void removedNode(Node node)
    {
        // equivalent to node updated event
        updatedNode(node);
    }

    /* (non-Javadoc)
     * @see org.apache.jetspeed.page.PageManagerEventListener#reapNodes(long)
     */
    public synchronized void reapNodes(long interval)
    {
        // reap page manager nodes for idle sessions to free
        // system resources; the site view will be lazily
        // recalculated if the session is resumed
        if ((locatorsLastUpdateCheck > 0) && (System.currentTimeMillis()-locatorsLastUpdateCheck > interval))
        {
            siteView = null;
        }
    }

    /**
     * sessionDidActivate - notification that the session has just
     *                      been activated
     *
     * @param event session activation event
     */
    public void sessionDidActivate(HttpSessionEvent event)
    {
        // set stale flag to force session context state reset
        synchronized (this)
        {
            stale = true;
        }

        // log activation event
        if (log.isDebugEnabled())
        {
            log.debug("Session activation event: set session context state stale");
        }
    }
    
    /**
     * sessionWillPassivate - notification that the session is about
     *                        to be passivated
     *
     * @param event session activation event
     */
    public void sessionWillPassivate(HttpSessionEvent event)
    {
        // clear session context state
        clearSessionProfileLocators();

        // log activation event
        if (log.isDebugEnabled())
        {
            log.debug("Session deactivation event: clear session context state");
        }
    }

    /**
     * valueBound - notifies this context that it is being bound to
     *              a session and identifies the session
     *
     * @param event session binding event
     */
    public void valueBound(HttpSessionBindingEvent event)
    {
        // subscribe this session context to page manager events
        synchronized (this)
        {
            if (!subscribed && (pageManager != null))
            {
                pageManager.addListener(this);
                subscribed = true;
            }
        }

        // log binding event
        if (log.isDebugEnabled())
        {
            log.debug("Session bound event: setup page manager listener");
        }
    }
    
    /**
     * valueUnbound - notifies this context that it is being unbound
     *                from a session and identifies the session
     *
     * @param event session binding event
     */
    public void valueUnbound(HttpSessionBindingEvent event)
    {
        synchronized (this)
        {
            // unsubscribe this session context to page manager events
            if (subscribed && (pageManager != null))
            {
                pageManager.removeListener(this);
                subscribed = false;
            }

            // clear session context state
            clearSessionProfileLocators();
        }

        // log binding event
        if (log.isDebugEnabled())
        {
            log.debug("Session unbound event: clear page manager listener and session context state");
        }
    }

	private synchronized Map getFolderPageHistory()
    {
		if (folderPageHistory == null)
        {
			folderPageHistory = new HashMap();
		}
		return folderPageHistory;
	}
}
