/*
 * 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.commons.jxpath.servlet;

import javax.servlet.ServletContext;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.servlet.jsp.PageContext;
import org.apache.commons.jxpath.JXPathContext;
import org.apache.commons.jxpath.JXPathContextFactory;
import org.apache.commons.jxpath.JXPathIntrospector;

/**
 * Static methods that allocate and cache JXPathContexts bound to
 * {@link PageContext}, {@link ServletRequest}, {@link HttpSession}
 * and {@link ServletContext}.
 * <p>
 * The {@link JXPathContext} returned by {@link #getPageContext getPageContext()}
 * provides access to all scopes via the PageContext.findAttribute()
 * method.  Thus, an expression like "foo" will first look for the attribute
 * named "foo" in the "page" context, then the "request" context, then
 * the "session" one and finally in the "application" context.
 * <p>
 * If you need to limit the attibute lookup to just one scope, you can use the
 * pre-definded variables "page", "request", "session" and "application".
 * For example, the expression "$session/foo" extracts the value of the
 * session attribute named "foo".
 * <p>
 * Following are some implementation details. There is a separate JXPathContext
 * for each of the four scopes. These contexts are chained according to the
 * nesting of the scopes.  So, the parent of the "page" JXPathContext is a
 * "request" JXPathContext, whose parent is a "session" JXPathContext (that is
 * if there is a session), whose parent is an "application" context.
 * <p>
 * The  XPath context node for each context is the corresponding object:
 * PageContext, ServletRequest, HttpSession or ServletContext.  This feature can
 * be used by servlets.  A servlet can use one of the methods declared by this
 * class and work with a specific JXPathContext for any scope.
 * <p>
 * Since JXPath chains lookups for variables and extension functions, variables
 * and extension function declared in the outer scopes are also available in
 * the inner scopes.
 * <p>
 * Each  of the four context declares exactly one variable, the value of which
 * is the corresponding object: PageContext, etc.
 * <p>
 * The  "session" variable will be undefined if there is no session for this
 * servlet. JXPath does not automatically create sessions.
 *
 * @author Dmitri Plotnikov
 * @version $Revision: 652845 $ $Date: 2008-05-02 12:46:46 -0500 (Fri, 02 May 2008) $
 */
public final class JXPathServletContexts {

    private static JXPathContextFactory factory;

    static {
        JXPathIntrospector.registerDynamicClass(
                PageScopeContext.class,
                PageScopeContextHandler.class);
        JXPathIntrospector.registerDynamicClass(
                PageContext.class,
                PageContextHandler.class);
        JXPathIntrospector.registerDynamicClass(
                ServletContext.class,
                ServletContextHandler.class);
        JXPathIntrospector.registerDynamicClass(
                ServletRequestAndContext.class,
                ServletRequestHandler.class);
        JXPathIntrospector.registerDynamicClass(
                HttpSessionAndServletContext.class,
                HttpSessionHandler.class);
        factory = JXPathContextFactory.newInstance();
    }

    /**
     * Returns a JXPathContext bound to the "page" scope. Caches that context
     * within the PageContext itself.
     * @param pageContext as described
     * @return JXPathContext
     */
    public static JXPathContext getPageContext(PageContext pageContext) {
        JXPathContext context =
            (JXPathContext) pageContext.getAttribute(Constants.JXPATH_CONTEXT);
        if (context == null) {
            JXPathContext parentContext =
                getRequestContext(
                    pageContext.getRequest(),
                    pageContext.getServletContext());
            context = factory.newContext(parentContext, pageContext);
            context.setVariables(
                new KeywordVariables(
                    Constants.PAGE_SCOPE,
                    new PageScopeContext(pageContext)));
            pageContext.setAttribute(Constants.JXPATH_CONTEXT, context);
        }
        return context;
    }

    /**
     * Returns a JXPathContext bound to the "request" scope. Caches that context
     * within the request itself.
     * @param request as described
     * @param servletContext operative
     * @return JXPathContext
     */
    public static JXPathContext getRequestContext(ServletRequest request,
            ServletContext servletContext) {
        JXPathContext context =
            (JXPathContext) request.getAttribute(Constants.JXPATH_CONTEXT);
        // If we are in an included JSP or Servlet, the request parameter
        // will represent the included URL, but the JXPathContext we have
        // just acquired will represent the outer request.
        if (context != null) {
            ServletRequestAndContext handle =
                (ServletRequestAndContext) context.getContextBean();
            if (handle.getServletRequest() == request) {
                return context;
            }
        }

        JXPathContext parentContext = null;
        if (request instanceof HttpServletRequest) {
            HttpSession session =
                ((HttpServletRequest) request).getSession(false);
            if (session != null) {
                parentContext = getSessionContext(session, servletContext);
            }
            else {
                parentContext = getApplicationContext(servletContext);
            }
        }
        ServletRequestAndContext handle =
            new ServletRequestAndContext(request, servletContext);
        context = factory.newContext(parentContext, handle);
        context.setVariables(
            new KeywordVariables(Constants.REQUEST_SCOPE, handle));
        request.setAttribute(Constants.JXPATH_CONTEXT, context);
        return context;
    }

    /**
     * Returns a JXPathContext bound to the "session" scope. Caches that context
     * within the session itself.
     * @param session as described
     * @param servletContext operative
     * @return JXPathContext
     */
    public static JXPathContext getSessionContext(HttpSession session,
            ServletContext servletContext) {
        JXPathContext context =
            (JXPathContext) session.getAttribute(Constants.JXPATH_CONTEXT);
        if (context == null) {
            JXPathContext parentContext = getApplicationContext(servletContext);
            HttpSessionAndServletContext handle =
                new HttpSessionAndServletContext(session, servletContext);
            context = factory.newContext(parentContext, handle);
            context.setVariables(
                new KeywordVariables(Constants.SESSION_SCOPE, handle));
            session.setAttribute(Constants.JXPATH_CONTEXT, context);
        }
        return context;
    }

    /**
     * Returns  a JXPathContext bound to the "application" scope. Caches that
     * context within the servlet context itself.
     * @param servletContext operative
     * @return JXPathContext
     */
    public static JXPathContext getApplicationContext(
            ServletContext servletContext) {
        JXPathContext context =
            (JXPathContext) servletContext.getAttribute(
                Constants.JXPATH_CONTEXT);
        if (context == null) {
            context = factory.newContext(null, servletContext);
            context.setVariables(
                new KeywordVariables(
                    Constants.APPLICATION_SCOPE,
                    servletContext));
            servletContext.setAttribute(Constants.JXPATH_CONTEXT, context);
        }
        return context;
    }
}
